diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index e53b0f9..0feec59 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -33,18 +33,14 @@ 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'), + '#create_title' => t('+ Enter a new 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/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php index 6aacf1c..0f36739 100644 --- a/modules/order/src/Element/ProfileSelect.php +++ b/modules/order/src/Element/ProfileSelect.php @@ -3,6 +3,8 @@ 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; @@ -15,7 +17,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 +28,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,16 +44,27 @@ class ProfileSelect extends RenderElement { public function getInfo() { $class = get_class($this); return [ + '#tree' => TRUE, + '#title' => t('Select a profile'), + '#create_title' => t('+ Enter a new 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' => [], - // The profile entity operated on. Required. - '#default_value' => NULL, '#process' => [ [$class, 'attachElementSubmit'], - [$class, 'processForm'], + [$class, 'processElement'], ], '#element_validate' => [ [$class, 'validateElementSubmit'], @@ -59,6 +78,36 @@ class ProfileSelect extends RenderElement { } /** + * Validates the element properties. + * + * @param array $element + * The form element. + * + * @throws \InvalidArgumentException + * Thrown if an element property is invalid, or empty but required. + */ + 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.'); + } + 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.'); + } + // Make sure that the specified default country is available. + if (!empty($element['#default_country']) && !empty($element['#available_countries'])) { + if (!in_array($element['#default_country'], $element['#available_countries'])) { + $element['#default_country'] = NULL; + } + } + } + + /** * Builds the element form. * * @param array $element @@ -68,47 +117,158 @@ class ProfileSelect extends RenderElement { * @param array $complete_form * The complete form structure. * - * @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. */ - 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 processElement(array $element, FormStateInterface $form_state, array &$complete_form) { + self::validateElementProperties($element); + + $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']); + // 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)'), + ]; + } } - 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.'); + + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + $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); } - if (!is_array($element['#available_countries'])) { - throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.'); + $profile_options = []; + foreach ($profiles as $profile) { + $option_id = self::buildOptionId($profile, $element['#profile_latest_revision']); + $profile_options[$option_id] = $profile->label(); } - // Make sure that the specified default country is available. - if (!empty($element['#default_country']) && !empty($element['#available_countries'])) { - if (!in_array($element['#default_country'], $element['#available_countries'])) { - $element['#default_country'] = NULL; + if (!empty($element_state['fixed_option'])) { + $option_id = $element_state['fixed_option']['id']; + $profile_options[$option_id] = $element_state['fixed_option']['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']; + } + else { + $selected_option = '_new'; + if ($default_profile = self::selectDefaultProfile($element, $profiles)) { + $selected_option = self::buildOptionId($default_profile, $element['#profile_latest_revision']); } } - $element['#profile'] = $element['#default_value']; - $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); - $form_display->buildForm($element['#profile'], $element, $form_state); - if (!empty($element['address']['widget'][0])) { - $widget_element = &$element['address']['widget'][0]; - // Remove the details wrapper from the address widget. - $widget_element['#type'] = 'container'; - // Provide a default country. - if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) { - $widget_element['address']['#default_value']['country_code'] = $element['#default_country']; + if ($selected_option == '_new') { + $element['#profile'] = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#profile_uid'], + ]); + $element_state['mode'] = 'form'; + } + else { + $id_parts = explode('-', $selected_option); + if (!empty($id_parts[1])) { + $element['#profile'] = $profile_storage->loadRevision($id_parts[1]); + } + else { + $element['#profile'] = $profile_storage->load($id_parts[0]); } - // Limit the available countries. - if (!empty($element['#available_countries'])) { - $widget_element['address']['#available_countries'] = $element['#available_countries']; + } + + $id_prefix = implode('-', $element['#parents']); + $wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper'); + $element = [ + '#prefix' => '
', + '#suffix' => '
', + // Pass the id along to other methods. + '#wrapper_id' => $wrapper_id, + ] + $element; + + if ($element_state['mode'] == 'view') { + $element['profile_selection'] = [ + '#type' => 'select', + '#title' => $element['#title'], + '#options' => $profile_options + ['_new' => $element['#create_title']], + '#default_value' => $selected_option, + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#limit_validation_errors' => [ + array_merge($element['#parents'], ['profile_selection']), + ], + '#weight' => -1, + '#access' => !empty($profile_options), + ]; + + $view_builder = \Drupal::entityTypeManager()->getViewBuilder('profile'); + $element['profile'] = $view_builder->view($element['#profile'], 'default'); + + $element['edit_button'] = [ + '#type' => 'submit', + '#value' => t('Edit'), + '#limit_validation_errors' => [], + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#submit' => [[get_called_class(), 'ajaxSubmit']], + '#name' => implode('_', $element['#parents']) . '_edit', + '#element_mode' => 'form', + // When given a historical profile, don't allow it to be edited. + '#access' => $element['#profile_latest_revision'] || $element['#profile']->isDefaultRevision(), + ]; + } + else { + $element['form'] = [ + '#parents' => array_merge($element['#parents'], ['form']), + ]; + $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); + $form_display->buildForm($element['#profile'], $element['form'], $form_state); + if (!empty($element['form']['address']['widget'][0])) { + $widget_element = &$element['form']['address']['widget'][0]; + // Remove the details wrapper from the address widget. + $widget_element['#type'] = 'container'; + // Provide a default country. + if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) { + $widget_element['address']['#default_value']['country_code'] = $element['#default_country']; + } + // Limit the available countries. + if (!empty($element['#available_countries'])) { + $widget_element['address']['#available_countries'] = $element['#available_countries']; + } } + + $element['cancel_button'] = [ + '#type' => 'submit', + '#value' => t('Return to address selection'), + '#limit_validation_errors' => [], + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#submit' => [[get_called_class(), 'ajaxSubmit']], + '#name' => implode('_', $element['#parents']) . '_cancel', + '#element_mode' => 'view', + '#access' => !empty($profiles), + ]; } + // Persist any changes made to $element_state. + self::setElementState($element['#parents'], $form_state, $element_state); + return $element; } @@ -125,9 +285,12 @@ class ProfileSelect extends RenderElement { * form, as a protection against buggy behavior. */ 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); + $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']); + if ($element_state['mode'] == 'form') { + $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); + $form_display->extractFormValues($element['#profile'], $element['form'], $form_state); + $form_display->validateFormValues($element['#profile'], $element['form'], $form_state); + } } /** @@ -139,9 +302,116 @@ class ProfileSelect extends RenderElement { * The current state of the form. */ public static function submitForm(array &$element, FormStateInterface $form_state) { - $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); - $form_display->extractFormValues($element['#profile'], $element, $form_state); - $element['#profile']->save(); + $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']); + if ($element_state['mode'] == 'form') { + $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); + $form_display->extractFormValues($element['#profile'], $element['form'], $form_state); + if ($element['#profile']->hasTranslationChanges()) { + $element['#profile']->save(); + } + } + } + + /** + * Builds the option ID for the given profile. + * + * @param \Drupal\profile\Entity\ProfileInterface $profile + * The profile. + * @param bool $use_latest_revision + * + * @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; + } + + /** + * 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; + } + + /** + * Ajax submit callback. + */ + public static function ajaxSubmit(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1)); + $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']); + $element_state['mode'] = $triggering_element['#element_mode']; + self::setElementState($element['#parents'], $form_state, $element_state); + $form_state->setRebuild(); + } + + /** + * Gets the element state. + * + * @param array $parents + * The element parents. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $defaults + * The defaults. + * + * @return array + * The element state. + */ + public static function getElementState(array $parents, FormStateInterface $form_state, array $defaults) { + $parents = array_merge(['element_state', '#parents'], $parents); + $element_state = (array) NestedArray::getValue($form_state->getStorage(), $parents); + return $element_state + $defaults; + } + + /** + * 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); } } diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php index 2d0987c..94fe1af 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,15 @@ 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'), + '#create_title' => $this->t('+ Enter a new 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..6d47d7a --- /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()])); + } + +} \ No newline at end of file diff --git a/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php new file mode 100644 index 0000000..56fd173 --- /dev/null +++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php @@ -0,0 +1,296 @@ + '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; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'commerce_order_test', + ]; + + /** + * @inheritDoc + */ + protected function setUp() { + parent::setUp(); + + $this->profileStorage = $this->container->get('entity_type.manager')->getStorage('profile'); + } + + /** + * 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()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('Select a profile'); + $this->getSession()->getPage()->fillField('profile[form][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[form][address][0][address][' . $key . ']'] = $value; + } + $this->submitForm($edit, 'Submit'); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile = $this->profileStorage->load(1); + $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $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()->statusCodeEquals(200); + $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(new FormattableMarkup('Profile selected: :label', [':label' => $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 testAuthenticatedEmpty() { + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('Select a profile'); + $this->getSession()->getPage()->fillField('Country', $this->address1['country_code']); + $this->waitForAjaxToFinish(); + + $edit = []; + foreach ($this->address1 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['profile[form][address][0][address][' . $key . ']'] = $value; + } + $this->submitForm($edit, 'Submit'); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile = $this->profileStorage->load(1); + $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $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()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(1, $profiles); + } + + /** + * Tests creating a profile as an authenticated user. + */ + public function testAuthenticatedCreate() { + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]);; + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $this->assertSession()->pageTextContains($this->address1['locality']); + + $this->getSession()->getPage()->fillField('Select a profile', '_new'); + $this->waitForAjaxToFinish(); + $this->getSession()->getPage()->fillField('Country', $this->address2['country_code']); + $this->waitForAjaxToFinish(); + $edit = []; + foreach ($this->address2 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['profile[form][address][0][address][' . $key . ']'] = $value; + } + + $this->submitForm($edit, 'Submit'); + + $new_profile = $this->profileStorage->load(2); + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $new_profile->get('address')->first(); + + $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $new_profile->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($this->address2['address_line1'], $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); + } + + /** + * 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, + ]); + /** @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()->statusCodeEquals(200); + $this->assertSession()->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $this->assertSession()->pageTextContains($this->address2['locality']); + $this->getSession()->getPage()->pressButton('Edit'); + $this->waitForAjaxToFinish(); + + foreach ($this->address2 as $key => $value) { + $this->assertSession()->fieldValueEquals('profile[form][address][0][address][' . $key . ']', $value); + } + $this->getSession()->getPage()->fillField('Street address', 'Andrássy út 22'); + $this->submitForm([], 'Submit'); + + $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->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $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 2b2e1b0..a27d180 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -135,17 +135,14 @@ class PaymentInformation extends CheckoutPaneBase { } else { $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'), + '#create_title' => $this->t('+ Enter a new 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 0a3ece9..9b09eaf 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -54,16 +54,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 +66,10 @@ 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'), + '#create_title' => t('+ Enter a new address'), + '#profile_type' => 'customer', + '#profile_uid' => $payment_method->getOwnerId(), '#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL, '#available_countries' => $store ? $store->getBillingCountries() : [], ];