diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index e53b0f92a..0d7a71852 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -33,20 +33,14 @@ public function buildPaneSummary() { */ 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'), + '#default_value' => $this->order->getBillingProfile(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), + '#profile_type' => 'customer', + '#owner_uid' => $this->order->getCustomerId(), ]; return $pane_form; @@ -56,7 +50,8 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, * {@inheritdoc} */ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { - $this->order->setBillingProfile($pane_form['profile']['#profile']); + $values = $form_state->getValue($pane_form['#parents']); + $this->order->setBillingProfile($values['profile']); } } diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php index 6aacf1c64..1d4d261aa 100644 --- a/modules/order/src/Element/ProfileSelect.php +++ b/modules/order/src/Element/ProfileSelect.php @@ -3,9 +3,11 @@ 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\Render\Element\FormElement; use Drupal\profile\Entity\ProfileInterface; /** @@ -18,15 +20,14 @@ * '#default_value' => $profile, * '#default_country' => 'FR', * '#available_countries' => ['US', 'FR'], + * '#profile_type' => 'customer', + * '#owner_uid' => \Drupal::currentUser()->id(), * ]; * @endcode - * To access the profile in validation or submission callbacks, use - * $form['billing_profile']['#profile']. Due to Drupal core limitations the - * profile can't be accessed via $form_state->getValue('billing_profile'). * - * @RenderElement("commerce_profile_select") + * @FormElement("commerce_profile_select") */ -class ProfileSelect extends RenderElement { +class ProfileSelect extends FormElement { use CommerceElementTrait; @@ -40,9 +41,14 @@ public function getInfo() { '#default_country' => NULL, // A list of country codes. If empty, all countries will be available. '#available_countries' => [], + '#title' => t('Select a profile'), + '#create_title' => t('+ Enter a new profile'), // The profile entity operated on. Required. - '#default_value' => NULL, + '#default_value' => '_new', + '#owner_uid' => 0, + // Provide default to not break contrib which have outdated elements. + '#profile_type' => 'customer', '#process' => [ [$class, 'attachElementSubmit'], [$class, 'processForm'], @@ -59,6 +65,26 @@ public function getInfo() { } /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if (!empty($input['profile_selection'])) { + $value = $input['profile_selection']; + } + elseif ($element['#default_value'] instanceof ProfileInterface) { + $value = $element['#default_value']->id(); + } + elseif (!empty($element['#default_value'])) { + $value = $element['#default_value']; + } + else { + $value = '_new'; + } + + return $value; + } + + /** * Builds the element form. * * @param array $element @@ -76,36 +102,116 @@ public function getInfo() { * 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.'); - } - 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 (!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; - } + + if (empty($element['#profile_type'])) { + throw new \InvalidArgumentException('The commerce_profile_select #profile_type property must be provided.'); } + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = $entity_type_manager->getStorage('profile'); + /** @var \Drupal\profile\Entity\ProfileTypeInterface $profile_type */ + $profile_type = $entity_type_manager->getStorage('profile_type')->load($element['#profile_type']); + + $user_profiles = []; + /** @var \Drupal\user\UserInterface $user */ + $user = $entity_type_manager->getStorage('user')->load($element['#owner_uid']); - $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 (!$user->isAnonymous()) { + // If the user exists, attempt to load other profiles for selection. + foreach ($profile_storage->loadMultipleByUser($user, $profile_type->id(), TRUE) as $existing_profile) { + $user_profiles[$existing_profile->id()] = $existing_profile->label(); + + // If this is the first form build, set the element's value based on + // the user's default profile. + if (!$form_state->isProcessingInput() && $existing_profile->isDefault()) { + $element['#value'] = $existing_profile->id(); + } } - // 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 = [ + '#tree' => TRUE, + '#prefix' => '
', + '#suffix' => '
', + // Pass the id along to other methods. + '#wrapper_id' => $wrapper_id, + '#element_mode' => $form_state->get('element_mode') ?: 'view', + ] + $element; + + if (!empty($user_profiles)) { + $element['profile_selection'] = [ + '#title' => $element['#title'], + '#options' => $user_profiles + ['_new' => $element['#create_title']], + '#type' => 'select', + '#weight' => -5, + '#default_value' => $element['#value'], + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#element_mode' => 'view', + ]; + } + else { + $element['profile_selection'] = [ + '#type' => 'value', + '#value' => '_new', + '#element_mode' => 'create', + ]; + } + + /** @var \Drupal\profile\Entity\ProfileInterface $element_profile */ + if ($element['#value'] == '_new') { + $element_profile = $profile_storage->create([ + 'type' => $profile_type->id(), + 'uid' => $user->id(), + ]); + $element['#element_mode'] = 'create'; + } + else { + $element_profile = $profile_storage->load($element['#value']); + } + + // Viewing a profile. + if (!$element_profile->isNew() && $element['#element_mode'] == 'view') { + $view_builder = $entity_type_manager->getViewBuilder('profile'); + $element['rendered_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' => 'edit_profile', + '#element_mode' => 'edit', + ]; + } + else { + $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); + $form_display->buildForm($element_profile, $element, $form_state); + + // @todo Loop over all possible address fields. + 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']; + } + // Limit the available countries. + if (!empty($element['#available_countries'])) { + $widget_element['address']['#available_countries'] = $element['#available_countries']; + } } } @@ -125,9 +231,29 @@ public static function processForm(array $element, FormStateInterface $form_stat * 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); + $value = $form_state->getValue($element['#parents']); + + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = $entity_type_manager->getStorage('profile'); + /** @var \Drupal\profile\Entity\ProfileInterface $element_profile */ + if ($value['profile_selection'] == '_new') { + $element_profile = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#owner_uid'], + ]); + } + else { + $element_profile = $profile_storage->load($value['profile_selection']); + } + + if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { + $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); + $form_display->extractFormValues($element_profile, $element, $form_state); + $form_display->validateFormValues($element_profile, $element, $form_state); + } + + $form_state->setValueForElement($element, $element_profile); } /** @@ -139,9 +265,33 @@ public static function validateForm(array &$element, FormStateInterface $form_st * 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_profile = $form_state->getValue($element['#parents']); + + if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { + $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); + $form_display->extractFormValues($element_profile, $element, $form_state); + $element_profile->save(); + } + + $form_state->setValueForElement($element, $element_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(); + $form_state->set('element_mode', $triggering_element['#element_mode']); + $form_state->setRebuild(); } } diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php index 2d0987c94..32e00a0c7 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php @@ -2,7 +2,6 @@ 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; @@ -75,22 +74,16 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $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' => t('+ Enter a new address'), + '#default_value' => $profile = $items[$delta]->entity, '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), + '#profile_type' => 'customer', + '#owner_uid' => $order->getCustomerId(), ]; // Workaround for massageFormValues() not getting $element. $element['array_parents'] = [ @@ -107,8 +100,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { $new_values = []; foreach ($values as $delta => $value) { - $element = NestedArray::getValue($form, $value['array_parents']); - $new_values[$delta]['entity'] = $element['profile']['#profile']; + $new_values[$delta]['entity'] = $value['profile']; } return $new_values; } 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 000000000..7d62a7662 --- /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_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 000000000..30b0555f8 --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php @@ -0,0 +1,45 @@ + 'commerce_profile_select', + '#title' => $this->t('Profile'), + '#default_value' => NULL, + '#profile_type' => 'customer', + '#owner_uid' => \Drupal::currentUser()->id(), + '#available_countries' => ['HU', 'FR', 'US', 'RS', 'DE'], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $profile = $form_state->getValue('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 000000000..63bfeff6d --- /dev/null +++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php @@ -0,0 +1,303 @@ + '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', + ]; + + /** + * @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 the profile select form element for anonymous user. + */ + public function testAnonymous() { + $this->drupalLogout(); + $address_fields = $this->address1; + $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', $address_fields['country_code']); + $this->waitForAjaxToFinish(); + + $edit = []; + foreach ($address_fields 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(new FormattableMarkup('Profile selected: :label', [':label' => $profile->label()])); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile->get('address')->first(); + $this->assertEquals($address_fields['country_code'], $address->getCountryCode()); + $this->assertEquals($address_fields['given_name'], $address->getGivenName()); + $this->assertEquals($address_fields['family_name'], $address->getFamilyName()); + $this->assertEquals($address_fields['address_line1'], $address->getAddressLine1()); + $this->assertEquals($address_fields['locality'], $address->getLocality()); + $this->assertEquals($address_fields['postal_code'], $address->getPostalCode()); + } + + /** + * Tests the profile select form element for anonymous user. + */ + public function testAuthenticatedNoExistingProfiles() { + $account = $this->createUser(); + $this->drupalLogin($account); + + $address_fields = $this->address1; + $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', $address_fields['country_code']); + $this->waitForAjaxToFinish(); + + $edit = []; + foreach ($address_fields 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(new FormattableMarkup('Profile selected: :label', [':label' => $profile->label()])); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile->get('address')->first(); + $this->assertEquals($address_fields['country_code'], $address->getCountryCode()); + $this->assertEquals($address_fields['given_name'], $address->getGivenName()); + $this->assertEquals($address_fields['family_name'], $address->getFamilyName()); + $this->assertEquals($address_fields['address_line1'], $address->getAddressLine1()); + $this->assertEquals($address_fields['locality'], $address->getLocality()); + $this->assertEquals($address_fields['postal_code'], $address->getPostalCode()); + } + + /** + * Tests the profile select form element for authenticated user. + */ + public function testProfileSelectAuthenticated() { + $account = $this->createUser(); + + $profile_storage = $this->container->get('entity_type.manager')->getStorage('profile'); + /** @var \Drupal\profile\Entity\ProfileInterface $profile_address1 */ + $profile_address1 = $profile_storage->create([ + 'type' => 'customer', + 'uid' => $account->id(), + 'address' => $this->address1, + ]); + $profile_address1->save(); + /** @var \Drupal\profile\Entity\ProfileInterface $profile_address2 */ + $profile_address2 = $profile_storage->create([ + 'type' => 'customer', + 'uid' => $account->id(), + 'address' => $this->address2, + ]); + $profile_address2->setDefault(TRUE); + $profile_address2->save(); + + $this->drupalLogin($account); + $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', $profile_address1->id()); + $this->waitForAjaxToFinish(); + $this->assertSession()->pageTextContains($this->address1['locality']); + $this->submitForm([], 'Submit'); + $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile_address1->label()])); + + $profile_storage->resetCache([$profile_address1->id()]); + $profile_address1 = $profile_storage->load($profile_address1->id()); + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile_address1->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()); + } + + /** + * Tests the profile select form element for authenticated user. + */ + public function testProfileSelectAuthenticatedCreateNew() { + $account = $this->createUser(); + $address_fields = $this->address2; + /** @var \Drupal\profile\Entity\ProfileInterface $profile_address1 */ + $profile_address1 = $this->profileStorage->create([ + 'type' => 'customer', + 'uid' => $account->id(), + 'address' => $this->address1, + ]); + $profile_address1->save(); + + $this->drupalLogin($account); + $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', $address_fields['country_code']); + $this->waitForAjaxToFinish(); + $edit = []; + foreach ($address_fields as $key => $value) { + if ($key == 'country_code') { + continue; + } + $edit['profile[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()); + } + + /** + * Tests the profile select form element for authenticated user. + * + * @group debug + */ + public function testProfileSelectAuthenticatedEdit() { + $account = $this->createUser(); + /** @var \Drupal\profile\Entity\ProfileInterface $profile_address1 */ + $profile_address1 = $this->profileStorage->create([ + 'type' => 'customer', + 'uid' => $account->id(), + 'address' => $this->address1, + ]); + $profile_address1->save(); + /** @var \Drupal\profile\Entity\ProfileInterface $profile_address2 */ + $profile_address2 = $this->profileStorage->create([ + 'type' => 'customer', + 'uid' => $account->id(), + 'address' => $this->address2, + ]); + $profile_address2->setDefault(TRUE); + $profile_address2->save(); + + $this->drupalLogin($account); + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $this->assertSession()->statusCodeEquals(200); + + // Edit a profile. + $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[address][0][address][' . $key . ']', $value); + } + $this->getSession()->getPage()->fillField('Street address', 'Andrássy út 22'); + $this->submitForm([], 'Submit'); + + $this->profileStorage->resetCache([$profile_address2->id()]); + $profile_address2 = $this->profileStorage->load($profile_address2->id()); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile_address2->get('address')->first(); + + $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile_address2->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()); + } + +} diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 2b2e1b0eb..67e2d5eb7 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -135,19 +135,15 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, } 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' => t('+ Enter a new address'), + '#default_value' => $this->order->getBillingProfile(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), + '#profile_type' => 'customer', + '#owner_uid' => $this->order->getCustomerId(), ]; } @@ -347,7 +343,7 @@ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state else { $this->order->set('payment_gateway', $payment_gateway); $this->order->set('payment_method', NULL); - $this->order->setBillingProfile($pane_form['billing_information']['#profile']); + $this->order->setBillingProfile($values['billing_information']); } } diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 0a3ece9eb..566b8bafc 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -6,7 +6,6 @@ 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,11 +53,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; - /** @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(); } @@ -71,9 +65,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $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'), '#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL, '#available_countries' => $store ? $store->getBillingCountries() : [], + '#profile_type' => 'customer', + '#owner_uid' => $payment_method->getOwnerId(), ]; return $form; @@ -109,9 +106,9 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s } /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; - $payment_method->setBillingProfile($form['billing_information']['#profile']); $values = $form_state->getValue($form['#parents']); + $payment_method->setBillingProfile($values['billing_information']); /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface $payment_gateway_plugin */ $payment_gateway_plugin = $this->plugin; // The payment method form is customer facing. For security reasons diff --git a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php index 31221e0d9..90cd64556 100644 --- a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php +++ b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php @@ -246,7 +246,8 @@ public function testPaymentInformation() { $this->drupalGet('checkout/1'); $radio_button = $page->findField('Example'); $this->assertNull($radio_button); - $this->assertSession()->fieldExists('payment_information[billing_information][address][0][address][postal_code]'); + $this->assertSession()->fieldExists('Select a profile'); + $this->assertSession()->pageTextContains('Pabst Blue Ribbon Dr'); } /** @@ -379,7 +380,8 @@ public function testCheckoutWithOffsiteRedirectPost() { $radio_button = $this->getSession()->getPage()->findField('Example'); $radio_button->click(); $this->waitForAjaxToFinish(); - + $this->getSession()->getPage()->selectFieldOption('Select a profile', '_new'); + $this->waitForAjaxToFinish(); $this->submitForm([ 'payment_information[billing_information][address][0][address][given_name]' => 'Johnny', 'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed', @@ -426,7 +428,8 @@ public function testCheckoutWithOffsiteRedirectGet() { $this->drupalGet($this->product->toUrl()->toString()); $this->submitForm([], 'Add to cart'); $this->drupalGet('checkout/1'); - + $this->getSession()->getPage()->selectFieldOption('Select a profile', '_new'); + $this->waitForAjaxToFinish(); $this->submitForm([ 'payment_information[billing_information][address][0][address][given_name]' => 'Johnny', 'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed', diff --git a/tests/src/FunctionalJavascript/JavascriptTestTrait.php b/tests/src/FunctionalJavascript/JavascriptTestTrait.php index 5f12b9231..ffca7de27 100644 --- a/tests/src/FunctionalJavascript/JavascriptTestTrait.php +++ b/tests/src/FunctionalJavascript/JavascriptTestTrait.php @@ -89,4 +89,27 @@ protected function createScreenshot($set_background_color = TRUE) { $this->htmlOutputCounter++; } + /** + * Creates a screenshot. + * + * @param bool $set_background_color + * (optional) By default this method will set the background color to white. + * Set to FALSE to override this behaviour. + * + * @throws \Behat\Mink\Exception\UnsupportedDriverActionException + * When operation not supported by the driver. + * @throws \Behat\Mink\Exception\DriverException + * When the operation cannot be done. + */ + protected function createScreenshot($set_background_color = TRUE) { + $jpg_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.jpg'; + $session = $this->getSession(); + if ($set_background_color) { + $session->executeScript("document.body.style.backgroundColor = 'white';"); + } + $image = $session->getScreenshot(); + file_put_contents($this->htmlOutputDirectory . '/' . $jpg_output_filename, $image); + $this->htmlOutputCounter++; + } + }