diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index e53b0f9..d3a4edf 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -33,18 +33,13 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa */ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { $store = $this->order->getStore(); - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $profile_storage = $this->entityTypeManager->getStorage('profile'); - $billing_profile = $profile_storage->create([ - 'type' => 'customer', - 'uid' => $this->order->getCustomerId(), - ]); - } $pane_form['profile'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => $this->t('Select an address'), + '#profile' => $this->getBillingProfile(), + '#profile_type' => 'customer', + '#profile_uid' => $this->order->getCustomerId(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; @@ -59,4 +54,22 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa $this->order->setBillingProfile($pane_form['profile']['#profile']); } + + /** + * Gets the billing profile. + * + * @return \Drupal\profile\Entity\ProfileInterface + * The billing profile. + */ + protected function getBillingProfile() { + if ($billing_profile = $this->order->getBillingProfile()) { + return $billing_profile; + } + $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ + 'type' => 'customer', + 'uid' => $this->order->getCustomerId(), + ]); + return $billing_profile; + } + } diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php index 6aacf1c..4427d1e 100644 --- a/modules/order/src/Element/ProfileSelect.php +++ b/modules/order/src/Element/ProfileSelect.php @@ -3,9 +3,12 @@ namespace Drupal\commerce_order\Element; use Drupal\commerce\Element\CommerceElementTrait; +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\RenderElement; +use Drupal\Core\Url; use Drupal\profile\Entity\ProfileInterface; /** @@ -15,7 +18,9 @@ use Drupal\profile\Entity\ProfileInterface; * @code * $form['billing_profile'] = [ * '#type' => 'commerce_profile_select', - * '#default_value' => $profile, + * '#profile' => $profile, + * '#profile_type' => 'customer', + * '#profile_uid' => \Drupal::currentUser()->id(), * '#default_country' => 'FR', * '#available_countries' => ['US', 'FR'], * ]; @@ -24,6 +29,10 @@ use Drupal\profile\Entity\ProfileInterface; * $form['billing_profile']['#profile']. Due to Drupal core limitations the * profile can't be accessed via $form_state->getValue('billing_profile'). * + * Note: + * This element always behaves as required. For optional behavior add + * a checkbox above the element that hides it on #ajax. + * * @RenderElement("commerce_profile_select") */ class ProfileSelect extends RenderElement { @@ -36,20 +45,31 @@ class ProfileSelect extends RenderElement { public function getInfo() { $class = get_class($this); return [ + '#tree' => TRUE, + '#title' => t('Select a profile'), + // The element doesn't use #default_value / #value. Use #profile instead. + '#default_value' => NULL, + // The preselected profile. A profile entity, or NULL. + '#profile' => NULL, + // Needed for creating new profiles, since #profile is not always given. + '#profile_type' => NULL, + '#profile_uid' => 0, + // Whether profiles should always be loaded in the latest revision. + // Disable when editing historical data, such as placed orders. + '#profile_latest_revision' => TRUE, // The country to select if the address widget doesn't have a default. '#default_country' => NULL, // A list of country codes. If empty, all countries will be available. '#available_countries' => [], + // Display options to administer profiles. + '#operation_buttons' => TRUE, - // The profile entity operated on. Required. - '#default_value' => NULL, '#process' => [ [$class, 'attachElementSubmit'], - [$class, 'processForm'], + [$class, 'processElement'], ], '#element_validate' => [ [$class, 'validateElementSubmit'], - [$class, 'validateForm'], ], '#commerce_element_submit' => [ [$class, 'submitForm'], @@ -59,28 +79,23 @@ class ProfileSelect extends RenderElement { } /** - * Builds the element form. + * Validates the element properties. * * @param array $element - * The form element being processed. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * @param array $complete_form - * The complete form structure. + * The form element. * * @throws \InvalidArgumentException - * Thrown when #default_value is empty or not an entity, or when - * #available_countries is not an array of country codes. - * - * @return array - * The processed form element. + * Thrown if an element property is invalid, or empty but required. */ - public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) { - if (empty($element['#default_value'])) { - throw new \InvalidArgumentException('The commerce_profile_select element requires the #default_value property.'); + public static function validateElementProperties(array $element) { + if (empty($element['#profile_type'])) { + throw new \InvalidArgumentException('The commerce_profile_select #profile_type property must be provided.'); + } + if (isset($element['#profile']) && !($element['#profile'] instanceof ProfileInterface)) { + throw new \InvalidArgumentException('The commerce_profile_select #profile property must be a profile entity.'); } - elseif (isset($element['#default_value']) && !($element['#default_value'] instanceof ProfileInterface)) { - throw new \InvalidArgumentException('The commerce_profile_select #default_value property must be a profile entity.'); + if (!empty($element['#profile_uid']) && !is_numeric($element['#profile_uid'])) { + throw new \InvalidArgumentException('The commerce_profile_select #profile_uid property must be a numeric entity ID.'); } if (!is_array($element['#available_countries'])) { throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.'); @@ -91,8 +106,123 @@ class ProfileSelect extends RenderElement { $element['#default_country'] = NULL; } } + } + + /** + * Builds the element form. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed form element. + */ + public static function processElement(array $element, FormStateInterface $form_state) { + self::validateElementProperties($element); + if (count(self::loadProfiles($element)) > 0 ) { + $element = self::addSelectList($element, $form_state); + if (!empty($element['#operation_buttons'])) { + $element = self::addModalLinks($element, $form_state); + } + } + else { + $element = self::addInlineForm($element, $form_state); + } + + return $element; + } + + /** + * Prepare options for select list and modal links. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Array of element, profile options and selected option. + */ + protected static function prepareOptions(array $element, FormStateInterface $form_state) { + $element_state = self::getElementState($element['#parents'], $form_state); + // If the provided profile is in a previous revision, make sure it stays + // available as an option when $element['#profile'] gets changed. + if (empty($element['#profile_latest_revision']) && empty($element_state['fixed_option'])) { + if (!empty($element['#profile']) && !$element['#profile']->isDefaultRevision()) { + $element_state['fixed_option'] = [ + 'id' => self::buildOptionId($element['#profile'], $element['#profile_latest_revision']), + 'label' => $element['#profile']->label() . ' ' . t('(Original)'), + ]; + } + } + $profiles = self::loadProfiles($element); + $profile_options = []; + foreach ($profiles as $profile) { + $option_id = self::buildOptionId($profile, $element['#profile_latest_revision']); + $profile_options[$option_id] = $profile->label(); + } + $user_input = $form_state->getUserInput(); + $selected_option = NestedArray::getValue($user_input, array_merge($element['#parents'], ['profile_selection'])); + if (!empty($selected_option)) { + // Keep the last selection in element state, so that the element knows + // where to return the customer upon cancelling form mode. + $element_state['selected_option'] = $selected_option; + } + elseif (!empty($element_state['selected_option'])) { + $selected_option = $element_state['selected_option']; + } + elseif ($default_profile = self::selectDefaultProfile($element, $profiles)) { + $selected_option = self::buildOptionId($default_profile, $element['#profile_latest_revision']); + } + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + $id_parts = explode('-', $selected_option); + if (!empty($id_parts[1])) { + $element['#profile'] = $profile_storage->loadRevision($id_parts[1]); + } + elseif (!empty($id_parts[0])) { + $element['#profile'] = $profile_storage->load($id_parts[0]); + } + else { + $element['#profile'] = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#profile_uid'], + ]); + } + // Persist any changes made to $element_state. + self::setElementState($element['#parents'], $form_state, $element_state); + + return [ + $element, + $profile_options, + $selected_option, + ]; + } - $element['#profile'] = $element['#default_value']; + /** + * Provide render array for an inline form. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Render array for an inline form. + */ + protected static function addInlineForm(array $element, FormStateInterface $form_state) { + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + if (empty($element['#profile'])) { + $element['#profile'] = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#profile_uid'], + ]); + } $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); $form_display->buildForm($element['#profile'], $element, $form_state); if (!empty($element['address']['widget'][0])) { @@ -108,26 +238,120 @@ class ProfileSelect extends RenderElement { $widget_element['address']['#available_countries'] = $element['#available_countries']; } } + return $element; + } + + /** + * Provide render array for select list. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Render array for an inline form. + */ + protected static function addSelectList(array $element, FormStateInterface $form_state) { + list($element, $profile_options, $selected_option) = self::prepareOptions($element, $form_state); + $id_prefix = implode('-', $element['#parents']); + $wrapper_id = Html::getUniqueId("$id_prefix-ajax-wrapper"); + // Pass the id along to other methods. + $element['#wrapper_id'] = $wrapper_id; + $view_builder = \Drupal::entityTypeManager()->getViewBuilder('profile'); + $element['profile_selection'] = [ + '#type' => 'select', + '#title' => $element['#title'], + '#options' => $profile_options, + '#default_value' => $selected_option, + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#limit_validation_errors' => [], + '#weight' => -1, + '#access' => !empty($profile_options), + ]; + $element['profile'] = $view_builder->view($element['#profile'], 'default'); + $element['#prefix'] = "
"; + $element['#suffix'] = '
'; return $element; } /** - * Validates the element form. + * Provide render array for modal links. * * @param array $element - * The form element. + * The form element being processed. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * - * @throws \Exception - * Thrown if button-level #validate handlers are detected on the parent - * form, as a protection against buggy behavior. + * @return array + * Render array for an inline form. */ - public static function validateForm(array &$element, FormStateInterface $form_state) { - $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); - $form_display->extractFormValues($element['#profile'], $element, $form_state); - $form_display->validateFormValues($element['#profile'], $element, $form_state); + protected static function addModalLinks(array $element, FormStateInterface $form_state) { + $url = Url::fromRoute('entity.profile.type.user_profile_form.add', ['user' => $element['#profile']->getOwnerId(), 'profile_type' => $element['#profile_type']]); + $url->mergeOptions(['query' => ['destination' => \Drupal::service('path.current')->getPath()]]); + $element['new'] = [ + '#type' => 'link', + '#url' => $url, + '#title' => t('Add new profile'), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'button', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + ]; + if (!$element['#profile']->isNew()) { + $url = Url::fromRoute('entity.profile.edit_form', ['profile' => $element['#profile']->id()]); + $url->mergeOptions(['query' => ['destination' => \Drupal::service('path.current')->getPath()]]); + $element['edit'] = [ + '#type' => 'link', + '#url' => $url, + '#title' => t('Edit profile'), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'button', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + ]; + } + $element['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + return $element; + } + + /** + * Loads related profiles. + * + * @param array $element + * The form element being processed. + * + * @return \Drupal\profile\ProfileStorageInterface[] + * A list of loaded profiles. + */ + protected static function loadProfiles(array $element) { + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + /** @var \Drupal\user\UserStorageInterface $user_storage */ + $user_storage = \Drupal::entityTypeManager()->getStorage('user'); + $user = $element['#profile_uid'] ? $user_storage->load($element['#profile_uid']) : NULL; + $profiles = []; + if ($user) { + $profiles = $profile_storage->loadMultipleByUser($user, $element['#profile_type'], TRUE); + } + return $profiles; } /** @@ -144,4 +368,92 @@ class ProfileSelect extends RenderElement { $element['#profile']->save(); } + /** + * Ajax callback. + */ + public static function ajaxRefresh(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1)); + return $element; + } + + /** + * Builds the option ID for the given profile. + * + * @param \Drupal\profile\Entity\ProfileInterface $profile + * The profile. + * @param bool $use_latest_revision + * (optional) Use the latest revision, defaults to TRUE. + * + * @return string + * The option ID. + */ + public static function buildOptionId(ProfileInterface $profile, $use_latest_revision = TRUE) { + $option_id = $profile->id(); + if (!$use_latest_revision) { + $option_id .= '-' . $profile->getRevisionId(); + } + return $option_id; + } + + /** + * Selects the default profile for the given element. + * + * @param array $element + * The element. + * @param array $profiles + * The available profiles. + * + * @return \Drupal\profile\Entity\ProfileInterface|null + * The default profile, or NULL if none found. + */ + public static function selectDefaultProfile(array $element, array $profiles) { + $default_profile = NULL; + if (!empty($element['#profile'])) { + $default_profile = $element['#profile']; + } + elseif (!empty($profiles)) { + $default_profile = reset($profiles); + foreach ($profiles as $profile) { + if ($profile->isDefault()) { + $default_profile = $profile; + break; + } + } + } + return $default_profile; + } + + /** + * Sets the element state. + * + * @param array $parents + * The element parents. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $element_state + * The element state. + */ + public static function setElementState(array $parents, FormStateInterface $form_state, array $element_state) { + $parents = array_merge(['element_state', '#parents'], $parents); + NestedArray::setValue($form_state->getStorage(), $parents, $element_state); + } + + /** + * Gets the element state. + * + * @param array $parents + * The element parents. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The element state. + */ + public static function getElementState(array $parents, FormStateInterface $form_state) { + $parents = array_merge(['element_state', '#parents'], $parents); + $element_state = (array) NestedArray::getValue($form_state->getStorage(), $parents); + return $element_state; + } + } diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php index 2d0987c..fecdcc8 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php @@ -3,13 +3,10 @@ namespace Drupal\commerce_order\Plugin\Field\FieldWidget; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of 'commerce_billing_profile'. @@ -22,50 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * } * ) */ -class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new BillingProfileWidget object. - * - * @param string $plugin_id - * The plugin_id for the widget. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The definition of the field to which the widget is associated. - * @param array $settings - * The widget settings. - * @param array $third_party_settings - * Any third party settings. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $plugin_id, - $plugin_definition, - $configuration['field_definition'], - $configuration['settings'], - $configuration['third_party_settings'], - $container->get('entity_type.manager') - ); - } +class BillingProfileWidget extends WidgetBase { /** * {@inheritdoc} @@ -75,20 +29,14 @@ class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginI $order = $items[$delta]->getEntity(); $store = $order->getStore(); - if (!$items[$delta]->isEmpty()) { - $profile = $items[$delta]->entity; - } - else { - $profile = $this->entityTypeManager->getStorage('profile')->create([ - 'type' => 'customer', - 'uid' => $order->getCustomerId(), - ]); - } - $element['#type'] = 'fieldset'; $element['profile'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $profile, + '#title' => $this->t('Select an address'), + '#profile' => $items[$delta]->entity, + '#profile_type' => 'customer', + '#profile_uid' => $order->getCustomerId(), + '#profile_latest_revision' => $order->getState()->value == 'draft', '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml new file mode 100644 index 0000000..466dd4c --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml @@ -0,0 +1,9 @@ +commerce_order_test.profile_select_form: + path: '/commerce_order_test/profile_select_test_form' + defaults: + _form: '\Drupal\commerce_order_test\Form\ProfileSelectTestForm' + _title: 'Profile select test form' + requirements: + _access: 'TRUE' + options: + no_cache: TRUE diff --git a/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php new file mode 100644 index 0000000..1c59cb8 --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php @@ -0,0 +1,43 @@ + 'commerce_profile_select', + '#profile_type' => 'customer', + '#profile_uid' => \Drupal::currentUser()->id(), + '#available_countries' => ['FR', 'DE', 'HU', 'RS', 'US'], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $profile = $form['profile']['#profile']; + drupal_set_message($this->t('Profile selected: :label', [':label' => $profile->label()])); + } + +} diff --git a/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php new file mode 100644 index 0000000..638a7ae --- /dev/null +++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php @@ -0,0 +1,288 @@ + 'HU', + 'given_name' => 'Gustav', + 'family_name' => 'Mahler', + 'address_line1' => 'Teréz körút 7', + 'locality' => 'Budapest', + 'postal_code' => '1067', + ]; + + /** + * Profile address values. + * + * @var array + */ + protected $address2 = [ + 'country_code' => 'DE', + 'given_name' => 'Johann Sebastian', + 'family_name' => 'Bach', + 'address_line1' => 'Thomaskirchhof 15', + 'locality' => 'Leipzig', + 'postal_code' => '04109', + ]; + + /** + * The profile storage. + * + * @var \Drupal\profile\ProfileStorageInterface + */ + protected $profileStorage; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'commerce_order', + 'commerce_order_test', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->profileStorage = $this->container->get('entity_type.manager')->getStorage('profile'); + } + + /** + * {@inheritdoc} + */ + protected function getAdministratorPermissions() { + return [ + 'view own customer profile', + 'update own customer profile', + ] + parent::getAdministratorPermissions(); + } + + /** + * Tests creating a profile as an authenticated user. + */ + public function testAnonymous() { + // Create a profile for a different user to make sure it's not shown. + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + + $this->drupalLogout(); + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->pageTextContains('Profile select test form'); + $this->assertSession()->pageTextNotContains('Select a profile'); + $this->getSession()->getPage()->fillField('profile[address][0][address][country_code]', $this->address1['country_code']); + $this->waitForAjaxToFinish(); + + $edit = []; + foreach ($this->address1 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['profile[address][0][address][' . $key . ']'] = $value; + } + $this->submitForm($edit, 'Submit'); + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile = $this->profileStorage->load(1); + $this->assertSession()->responseContains(sprintf('Profile selected: %s', $profile->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile->get('address')->first(); + $this->assertEquals($this->address1['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address->getPostalCode()); + } + + /** + * Tests selecting a profile as an authenticated user. + */ + public function testAuthenticated() { + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + /** @var \Drupal\profile\Entity\ProfileInterface $profile2 */ + $profile2 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address2, + 'is_default' => TRUE, + ]); + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->pageTextContains('Profile select test form'); + $this->assertSession()->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $this->assertSession()->pageTextContains($this->address2['locality']); + + $this->getSession()->getPage()->fillField('Select a profile', $profile1->id()); + $this->waitForAjaxToFinish(); + $this->assertSession()->pageTextContains($this->address1['locality']); + $this->submitForm([], 'Submit'); + $this->assertSession()->responseContains(sprintf('Profile selected: %s', $profile1->label())); + + $this->profileStorage->resetCache([$profile1->id()]); + $profile1 = $this->profileStorage->load($profile1->id()); + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile1->get('address')->first(); + // Assert that field values have not changed. + $this->assertEquals($this->address1['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + + /** + * Tests creating the initial profile as an authenticated user. + */ + public function testAuthenticatedCreate() { + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->pageTextContains('Profile select test form'); + $this->assertSession()->pageTextNotContains('Select a profile'); + $this->getSession()->getPage()->fillField('profile[address][0][address][country_code]', $this->address1['country_code']); + $this->waitForAjaxToFinish(); + + $edit = []; + foreach ($this->address1 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['profile[address][0][address][' . $key . ']'] = $value; + } + $this->submitForm($edit, 'Submit'); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->profileStorage->load(1); + $this->assertSession()->responseContains(sprintf('Profile selected: %s', $profile1->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address1 */ + $address1 = $profile1->get('address')->first(); + $this->assertEquals($this->address1['country_code'], $address1->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address1->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address1->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address1->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address1->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address1->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(1, $profiles); + + $this->getSession()->getPage()->clickLink('Add new profile'); + $this->waitForAjaxToFinish(); + $edit = []; + foreach ($this->address2 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['address][0][address][' . $key . ']'] = $value; + } + $this->submitForm($edit, 'Save and make default'); + + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Submit'); + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile2 = $this->profileStorage->load(2); + $this->assertSession()->responseContains(sprintf('Profile selected: %s', $profile2->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address2 */ + $address2 = $profile2->get('address')->first(); + $this->assertEquals($this->address1['country_code'], $address2->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address2->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address2->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address2->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address2->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address2->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + + /** + * Tests editing a profile as an authenticated user. + * + * @group debug + */ + public function testAuthenticatedEdit() { + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + $profile1->save(); + /** @var \Drupal\profile\Entity\ProfileInterface $profile2 */ + $profile2 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address2, + 'is_default' => TRUE, + ]); + $profile2->save(); + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->pageTextContains('Profile select test form'); + $this->assertSession()->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $this->assertSession()->pageTextContains($this->address2['locality']); + $this->getSession()->getPage()->clickLink('Edit profile'); + $this->waitForAjaxToFinish(); + + foreach ($this->address2 as $key => $value) { + $this->assertSession()->fieldValueEquals('address][0][address][' . $key . ']', $value); + } + $this->getSession()->getPage()->fillField('Street address', 'Andrássy út 22'); + $this->submitForm([], 'Save'); + + $this->profileStorage->resetCache([$profile2->id()]); + $profile2 = $this->profileStorage->load($profile2->id()); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile2->get('address')->first(); + $this->submitForm([], 'Submit'); + + $this->assertSession()->responseContains(sprintf('Profile selected: %s', $profile2->label())); + // Assert that field values have not changed. + $this->assertEquals($this->address2['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address2['given_name'], $address->getGivenName()); + $this->assertEquals($this->address2['family_name'], $address->getFamilyName()); + $this->assertEquals('Andrássy út 22', $address->getAddressLine1()); + $this->assertEquals($this->address2['locality'], $address->getLocality()); + $this->assertEquals($this->address2['postal_code'], $address->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + +} diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index a91aabe..92be4b9 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -260,17 +260,13 @@ class PaymentInformation extends CheckoutPaneBase { */ protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state) { $store = $this->order->getStore(); - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ - 'uid' => $this->order->getCustomerId(), - 'type' => 'customer', - ]); - } $pane_form['billing_information'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => $this->t('Select an address'), + '#profile' => $this->order->getBillingProfile(), + '#profile_type' => 'customer', + '#profile_uid' => $this->order->getCustomerId(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 44c6c69..616d6a4 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -6,7 +6,6 @@ use Drupal\commerce_payment\CreditCard; use Drupal\commerce_payment\Exception\DeclineException; use Drupal\commerce_payment\Exception\PaymentGatewayException; use Drupal\Core\Form\FormStateInterface; -use Drupal\profile\Entity\Profile; class PaymentMethodAddForm extends PaymentGatewayFormBase { @@ -54,16 +53,6 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase { /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; - /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ - $billing_profile = $payment_method->getBillingProfile(); - if (!$billing_profile) { - /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ - $billing_profile = Profile::create([ - 'type' => 'customer', - 'uid' => $payment_method->getOwnerId(), - ]); - } - if ($order = $this->routeMatch->getParameter('commerce_order')) { $store = $order->getStore(); } @@ -76,7 +65,9 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase { $form['billing_information'] = [ '#parents' => array_merge($form['#parents'], ['billing_information']), '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => t('Select an address'), + '#profile_type' => 'customer', + '#profile_uid' => $payment_method->getOwnerId(), '#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL, '#available_countries' => $store ? $store->getBillingCountries() : [], ];