diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
index e53b0f92..99de2881 100644
--- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
+++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
@@ -33,6 +33,7 @@ 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');
@@ -44,7 +45,11 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa
$pane_form['profile'] = [
'#type' => 'commerce_profile_select',
+ '#title' => $this->t('Select an address'),
+ '#create_title' => $this->t('+ Enter a new address'),
'#default_value' => $billing_profile,
+ '#profile_type' => 'customer',
+ '#profile_uid' => $this->order->getCustomerId(),
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
];
diff --git a/modules/order/commerce_order.libraries.yml b/modules/order/commerce_order.libraries.yml
index 3005b4e8..654f394f 100644
--- a/modules/order/commerce_order.libraries.yml
+++ b/modules/order/commerce_order.libraries.yml
@@ -9,3 +9,15 @@ total_summary:
css:
layout:
css/commerce_order.total_summary.css: {}
+
+profile_select:
+ version: VERSION
+ js:
+ js/profile-select.js: {}
+ css:
+ component:
+ css/commerce_order.profile_select.css: {}
+ dependencies:
+ - core/jquery
+ - core/jquery.once
+ - core/drupal
diff --git a/modules/order/config/install/profile.type.customer.yml b/modules/order/config/install/profile.type.customer.yml
index c1cb63ef..c455be72 100644
--- a/modules/order/config/install/profile.type.customer.yml
+++ b/modules/order/config/install/profile.type.customer.yml
@@ -9,3 +9,4 @@ label: Customer
registration: false
multiple: true
weight: 0
+use_revisions: true
diff --git a/modules/order/css/commerce_order.profile_select.css b/modules/order/css/commerce_order.profile_select.css
new file mode 100644
index 00000000..3f0946d7
--- /dev/null
+++ b/modules/order/css/commerce_order.profile_select.css
@@ -0,0 +1,16 @@
+.profile-select .visible-on-edit {
+ display: none;
+}
+.profile-select.editing .visible-on-edit {
+ display: block;
+}
+.profile-select.editing .hidden-on-edit {
+ display: none;
+}
+
+.profile-select.creating .visible-on-create {
+ display: block;
+}
+.profile-select.creating .hidden-on-create {
+ display: none;
+}
diff --git a/modules/order/js/profile-select.js b/modules/order/js/profile-select.js
new file mode 100644
index 00000000..f48b6e6d
--- /dev/null
+++ b/modules/order/js/profile-select.js
@@ -0,0 +1,39 @@
+(function ($, Drupal) {
+ Drupal.behaviors.profileSelect = {
+ attach: function (context) {
+ function toggleRequired($profileSelect, required) {
+ $profileSelect.find('.required').each(function (key, el) {
+ el.required = required;
+ });
+ }
+
+ var $selects = $(context).find('.profile-select').once();
+ if ($selects.length > 0) {
+ $selects.each(function (index, el) {
+ var $profileSelect = $(el);
+ var $inputs = $profileSelect.find('input:not([type=submit]):not([type=button]),select,textarea');
+ $profileSelect.data('originalValues', $inputs.serializeArray());
+ toggleRequired($profileSelect, false);
+
+ $profileSelect.find('.edit-profile').once().click(function (event) {
+ event.preventDefault();
+ toggleRequired($profileSelect, true);
+ $profileSelect.toggleClass('editing');
+
+ $profileSelect.find('.cancel-edit-profile').once().click(function (event) {
+ event.preventDefault();
+ $profileSelect.toggleClass('editing');
+ toggleRequired($profileSelect, false);
+ var originalValues = $profileSelect.data('originalValues');
+ $.each(originalValues, function (key, field) {
+ console.log(field);
+ console.log($profileSelect.find('[name="' + field['name'] + '"]').val());
+ $profileSelect.find('[name="' + field['name'] + '"]').val(field['value']);
+ });
+ });
+ });
+ });
+ }
+ }
+ };
+})(jQuery, Drupal);
diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php
index 6aacf1c6..aa20d935 100644
--- a/modules/order/src/Element/ProfileSelect.php
+++ b/modules/order/src/Element/ProfileSelect.php
@@ -3,10 +3,16 @@
namespace Drupal\commerce_order\Element;
use Drupal\commerce\Element\CommerceElementTrait;
+use Drupal\commerce\EntityHelper;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
+use Drupal\Core\Session\AccountInterface;
use Drupal\profile\Entity\ProfileInterface;
+use Drupal\user\Entity\User;
/**
* Provides a form element for selecting a customer profile.
@@ -16,13 +22,17 @@ use Drupal\profile\Entity\ProfileInterface;
* $form['billing_profile'] = [
* '#type' => 'commerce_profile_select',
* '#default_value' => $profile,
+ * '#profile_type' => 'customer',
+ * '#profile_uid' => \Drupal::currentUser()->id(),
* '#default_country' => 'FR',
* '#available_countries' => ['US', 'FR'],
* ];
* @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').
+ * - $form_state->getValue('billing_profile')
+ * Or, (kept for backwards compatibility)
+ * - $form['billing_profile']['#profile'].
*
* @RenderElement("commerce_profile_select")
*/
@@ -36,16 +46,28 @@ class ProfileSelect extends RenderElement {
public function getInfo() {
$class = get_class($this);
return [
+ '#title' => t('Select a profile'),
+ '#create_title' => t('+ Enter a new profile'),
+
+ // Needed for creating new profiles, since #default_value may be empty.
+ // @todo need to implement.
+ '#profile_type' => NULL,
+ '#profile_uid' => NULL,
+
+ // 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.
+ // The profile entity operated on.
'#default_value' => NULL,
'#process' => [
[$class, 'attachElementSubmit'],
- [$class, 'processForm'],
+ [$class, 'processElement'],
],
'#element_validate' => [
[$class, 'validateElementSubmit'],
@@ -54,32 +76,31 @@ class ProfileSelect extends RenderElement {
'#commerce_element_submit' => [
[$class, 'submitForm'],
],
+ '#after_build' => [
+ [$class, 'clearValues'],
+ ],
'#theme_wrappers' => ['container'],
];
}
/**
- * Builds the element form.
+ * Validates the element properties.
+ *
+ * This also provides support breaking changes made that added the
+ * profile_type and profile_uid values instead of passing a default
+ * value.
*
* @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) {
+ public static function validateElementProperties(array &$element) {
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)) {
+ elseif (!($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'])) {
@@ -91,12 +112,178 @@ 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.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ public static function processElement(array $element, FormStateInterface $form_state, array &$complete_form) {
+ self::validateElementProperties($element);
+
+ $element['#attached']['library'][] = 'commerce_order/profile_select';
+ $element['#attributes']['class'][] = 'profile-select';
+
+ /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
+ $profile_storage = \Drupal::entityTypeManager()->getStorage('profile');
+ /** @var \Drupal\profile\Entity\ProfileInterface $default_profile */
+ $default_profile = $element['#default_value'];
+
+ $current_user = \Drupal::currentUser();
+
+ // This is the latest revision if reports that is the default revision,
+ // and the element allows editing the current revision through the
+ // #profile_latest_revision flag.
+ $default_value_is_latest_revision = $default_profile->isDefaultRevision() && $element['#profile_latest_revision'];
+ $default_profile_label = $default_profile->label();
+ // @todo Remove ?: check after https://www.drupal.org/project/commerce/issues/2995325
+ $owner = $default_profile->getOwner() ?: User::getAnonymousUser();
+ $profile_type = $default_profile->bundle();
+
+ // If the owner is a registered user, load their other active profiles for
+ // selection and reuse.
+ $available_profiles = static::getAvailableProfiles($owner, $profile_type);
+ // If the default value is a new profile, automatically select their
+ // default profile.
+ if ($default_profile->isNew()) {
+ foreach ($available_profiles as $available_profile) {
+ if ($available_profile->isDefault()) {
+ $element['#default_value'] = $available_profile;
+ $default_profile = $available_profile;
+ break;
+ }
+ }
+ }
+ // Handle a form rebuild and grab the selected profile value.
+ $selected_available_profile = $form_state->getValue(array_merge($element['#parents'], ['available_profiles']));
+ if ($selected_available_profile) {
+ if ($selected_available_profile == '_new') {
+ $selected_available_profile = $profile_storage->create([
+ 'type' => $default_profile->bundle(),
+ 'uid' => $default_profile->getOwnerId(),
+ ]);
+ }
+ // We are still going to use the existing profile, which is referenced at
+ // a previous revision.
+ elseif ($selected_available_profile == '_existing') {
+ $selected_available_profile = $default_profile;
+ }
+ else {
+ $selected_available_profile = $profile_storage->load($selected_available_profile);
+ }
+ $element['#default_value'] = $selected_available_profile;
+ $default_profile = $selected_available_profile;
+ }
+
+ // Set #profile to keep BC.
+ $element['#profile'] = $default_profile;
+
+ $id_prefix = implode('-', $element['#parents']);
+ $wrapper_id = Html::getId($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 the profile is new, apply the `creating` class so that the form is
+ // displayed automatically.
+ if ($default_profile->isNew()) {
+ $element['#attributes']['class'][] = 'creating';
+ }
+
+ $available_profiles_default_value = $default_profile->id() ?: '_new';
+ $available_profiles_options = EntityHelper::extractLabels($available_profiles);
+
+ if ($owner->hasPermission('create customer profile') || $current_user->hasPermission('create customer profile')) {
+ $available_profiles_options += ['_new' => $element['#create_title']];
+ }
+
+ // If the original default value is not the default revision, ensure it
+ // persists as an option to prevent unexpected changes in data.
+ if (!$default_value_is_latest_revision) {
+ $available_profiles_options = [
+ '_existing' => t(':label (Original)', [':label' => $default_profile_label]),
+ ] + $available_profiles_options;
+ $available_profiles_default_value = '_existing';
+ }
+
+ $element['available_profiles'] = [
+ '#type' => 'select',
+ '#title' => $element['#title'],
+ '#options' => $available_profiles_options,
+ '#default_value' => $available_profiles_default_value,
+ '#access' => !empty($available_profiles),
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $wrapper_id,
+ ],
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#attributes' => [
+ 'class' => ['available-profiles'],
+ ],
+ ];
+
+ $view_display = EntityViewDisplay::collectRenderDisplay($default_profile, 'default');
+ $element['profile_view'] = $view_display->build($element['#default_value']);
+ $element['profile_view']['#prefix'] = '';
+ $element['profile_view']['#suffix'] = '
';
+ $element['profile_view']['#access'] = !$default_profile->isNew();
+ $element['profile_view']['edit'] = [
+ '#type' => 'button',
+ '#value' => t('Edit'),
+ '#limit_validation_errors' => [],
+ '#attributes' => [
+ 'class' => ['edit-profile'],
+ ],
+ '#access' => $default_profile->isDefaultRevision() && ($default_profile->access('update', $owner) || $default_profile->access('update', $current_user)),
+ // Ensure this edit button shows below any other fields.
+ '#weight' => 100,
+ ];
+
+ $form_display = EntityFormDisplay::collectRenderDisplay($default_profile, 'default');
+ $element['profile_form'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['visible-on-edit visible-on-create'],
+ ],
+ '#access' => $default_profile->isDefaultRevision(),
+ '#parents' => $element['#parents'],
+ 'cancel' => [
+ '#type' => 'button',
+ '#value' => t('Cancel changes'),
+ '#limit_validation_errors' => [],
+ '#weight' => 100,
+ '#attributes' => [
+ 'class' => [
+ 'cancel-edit-profile',
+ 'hidden-on-create',
+ ],
+ ],
+ ],
+ ];
+
+ $form_display->buildForm($default_profile, $element['profile_form'], $form_state);
- $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];
+ // Adjust the address widget on the profile, if present.
+ if (!empty($element['profile_form']['address']['widget'][0])) {
+ $widget_element = &$element['profile_form']['address']['widget'][0];
// Remove the details wrapper from the address widget.
$widget_element['#type'] = 'container';
// Provide a default country.
@@ -108,7 +295,6 @@ class ProfileSelect extends RenderElement {
$widget_element['address']['#available_countries'] = $element['#available_countries'];
}
}
-
return $element;
}
@@ -125,9 +311,15 @@ 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);
+ $selected_available_profile = self::getSelectedAvailableProfile($element, $form_state);
+ $form_display = EntityFormDisplay::collectRenderDisplay($element['#default_value'], 'default');
+ $form_display->extractFormValues($selected_available_profile, $element, $form_state);
+ $form_display->validateFormValues($selected_available_profile, $element, $form_state);
+
+ // Set the profile as a value in the `profile` key of the form state.
+ $element_clone = $element;
+ $element_clone['#parents'][] = 'profile';
+ $form_state->setValueForElement($element_clone, $selected_available_profile);
}
/**
@@ -137,11 +329,129 @@ class ProfileSelect extends RenderElement {
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ * @throws \Drupal\Core\Entity\EntityStorageException
*/
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();
+ $selected_available_profile = self::getSelectedAvailableProfile($element, $form_state);
+ $form_display = EntityFormDisplay::collectRenderDisplay($selected_available_profile, 'default');
+ $form_display->extractFormValues($selected_available_profile, $element, $form_state);
+
+ // If the profile was modified, enforce a new revision.
+ // When the _existing option is chosen, there will be no changes reported
+ // preventing an accidental flag for the revision.
+ if ($selected_available_profile->hasTranslationChanges()) {
+ // If this is an old revision, we want to save directly to it, and not
+ // a new revision. But if it is the latest revision, we want to ensure
+ // that changes don't affect references to it.
+ if ($selected_available_profile->isLatestRevision()) {
+ $selected_available_profile->setNewRevision(TRUE);
+ }
+ $selected_available_profile->save();
+ }
+
+ // Set the profile as a value in the `profile` key of the form state.
+ $element_clone = $element;
+ $element_clone['#parents'][] = 'profile';
+ $form_state->setValueForElement($element_clone, $selected_available_profile);
+ $element['#profile'] = $selected_available_profile;
+ }
+
+ /**
+ * Gets the available profiles for the user that can be selected.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The account.
+ * @param string $profile_type
+ * The profile type.
+ *
+ * @return array|\Drupal\profile\Entity\ProfileInterface[]
+ * An array of profiles.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ protected static function getAvailableProfiles(AccountInterface $account, $profile_type) {
+ /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
+ $profile_storage = \Drupal::entityTypeManager()->getStorage('profile');
+ $available_profiles = [];
+ if ($account->isAuthenticated()) {
+ $available_profiles = $profile_storage->loadMultipleByUser($account, $profile_type, TRUE);
+ }
+ return $available_profiles;
+ }
+
+ /**
+ * Gets the selected available profile.
+ *
+ * @param array $element
+ * The form element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\profile\Entity\ProfileInterface
+ * The selected profile.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ protected static function getSelectedAvailableProfile(array $element, FormStateInterface $form_state) {
+ /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
+ $profile_storage = \Drupal::entityTypeManager()->getStorage('profile');
+ $selected_available_profile = $form_state->getValue(array_merge($element['#parents'], ['available_profiles']));
+ if ($selected_available_profile == '_new') {
+ return $profile_storage->create([
+ 'type' => $element['#default_value']->bundle(),
+ 'uid' => $element['#default_value']->getOwnerId(),
+ ]);
+ }
+ // We are still going to use the existing profile, which is referenced at
+ // a previous revision.
+ elseif ($selected_available_profile == '_existing') {
+ return $element['#default_value'];
+ }
+ else {
+ return $profile_storage->load($selected_available_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;
+ }
+
+ /**
+ * Clears dependent form values when the profile changes.
+ *
+ * Clears all input, so that the default values for a new profile form will
+ * be used, instead of the last input.
+ */
+ public static function clearValues(array $element, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ if (!$triggering_element) {
+ return $element;
+ }
+ if (end($triggering_element['#array_parents']) != 'available_profiles') {
+ return $element;
+ }
+
+ $triggering_element_parents = array_slice($triggering_element['#array_parents'], 0, -1);
+ $input = &$form_state->getUserInput();
+
+ if (NestedArray::keyExists($input, $triggering_element_parents)) {
+ // Remove any input for profile fields.
+ array_walk(NestedArray::getValue($input, $triggering_element_parents), function (&$item, $key) {
+ $item = ($key == 'available_profiles') ? $item : NULL;
+ });
+ }
+
+ return $element;
}
}
diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
index 5fe768c1..1f8a6914 100644
--- a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
+++ b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
@@ -88,7 +88,10 @@ class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginI
$element['#type'] = 'fieldset';
$element['profile'] = [
'#type' => 'commerce_profile_select',
+ '#title' => $this->t('Select an address'),
+ '#create_title' => $this->t('+ Enter a new address'),
'#default_value' => $profile,
+ '#profile_latest_revision' => $order->getState()->value == 'draft',
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
];
@@ -107,6 +110,7 @@ class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginI
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$new_values = [];
foreach ($values as $delta => $value) {
+ // @todo Get the value from $value rather than element directly.
$element = NestedArray::getValue($form, $value['array_parents']);
$new_values[$delta]['entity'] = $element['profile']['#profile'];
}
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 00000000..7d62a766
--- /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 00000000..3131495c
--- /dev/null
+++ b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php
@@ -0,0 +1,51 @@
+ 'commerce_profile_select',
+ '#title' => $this->t('Select a profile'),
+ '#default_value' => Profile::create([
+ 'type' => 'customer',
+ 'uid' => \Drupal::currentUser()->id(),
+ ]),
+ '#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', 'profile']);
+ drupal_set_message($this->t('Profile selected: :label', [':label' => $profile->label()]));
+ }
+
+}
diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php
index 9410fe36..4c7041de 100644
--- a/modules/order/tests/src/Functional/OrderAdminTest.php
+++ b/modules/order/tests/src/Functional/OrderAdminTest.php
@@ -15,29 +15,6 @@ use Drupal\profile\Entity\Profile;
*/
class OrderAdminTest extends OrderBrowserTestBase {
- /**
- * The profile to test against.
- *
- * @var \Drupal\profile\Entity\Profile
- */
- protected $billingProfile;
-
- /**
- * {@inheritdoc}
- */
- protected function setUp() {
- parent::setUp();
- \Drupal::service('module_installer')->install(['profile']);
-
- $profile_values = [
- 'type' => 'customer',
- 'uid' => 1,
- 'status' => 1,
- ];
- $this->billingProfile = Profile::create($profile_values);
- $this->billingProfile->save();
- }
-
/**
* Tests creating/editing an Order.
*/
@@ -75,7 +52,10 @@ class OrderAdminTest extends OrderBrowserTestBase {
'order_items[form][inline_entity_form][unit_price][0][amount][number]' => '9.99',
];
$this->submitForm($edit, 'Create order item');
- $this->submitForm([], t('Edit'));
+ // There are two "edit" buttons, one for the profile and one for the order
+ // items.
+ // @todo AJAX on the order item IEF shouldn't cause read mode on profile.
+ $this->getSession()->getPage()->pressButton('edit-order-items-entities-0-actions-ief-entity-edit');
$this->assertSession()->fieldExists('order_items[form][inline_entity_form][entities][0][form][purchased_entity][0][target_id]');
$this->assertSession()->fieldExists('order_items[form][inline_entity_form][entities][0][form][quantity][0][value]');
$this->assertSession()->fieldExists('order_items[form][inline_entity_form][entities][0][form][unit_price][0][amount][number]');
@@ -117,7 +97,7 @@ class OrderAdminTest extends OrderBrowserTestBase {
$this->drupalGet('/admin/commerce/orders');
$order_number = $this->getSession()->getPage()->find('css', 'tr td.views-field-order-number');
- $this->assertEquals(1, count($order_number), 'Order exists in the table.');
+ $this->assertNotEmpty($order_number, 'Order exists in the table.');
$order = Order::load(1);
$this->assertEquals(1, count($order->getItems()));
@@ -137,6 +117,12 @@ class OrderAdminTest extends OrderBrowserTestBase {
]);
$order->save();
+ /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_store */
+ $order_item_store = $this->container->get('entity_type.manager')->getStorage('commerce_order_item');
+ $order_item = $order_item_store->createFromPurchasableEntity($this->variation);
+ $order_item->save();
+ $order->addItem($order_item);
+
$adjustments = [];
$adjustments[] = new Adjustment([
'type' => 'custom',
@@ -158,6 +144,44 @@ class OrderAdminTest extends OrderBrowserTestBase {
$this->assertSession()->fieldValueEquals('adjustments[1][definition][label]', 'Handling fee');
$this->assertSession()->optionExists('adjustments[2][type]', 'Custom');
$this->assertSession()->optionNotExists('adjustments[2][type]', 'Test order adjustment type');
+
+ $this->getSession()->getPage()->fillField('First name', 'Frederick');
+ $this->getSession()->getPage()->fillField('Last name', 'Pabst');
+ $this->getSession()->getPage()->fillField('Street address', 'Pabst Blue Ribbon Dr');
+ $this->getSession()->getPage()->fillField('City', 'Milwaukee');
+ $this->getSession()->getPage()->fillField('State', 'WI');
+ $this->getSession()->getPage()->fillField('Zip code', '53177');
+
+ $this->getSession()->getPage()->pressButton('Save');
+
+ $this->drupalGet($order->toUrl('edit-form'));
+
+ $this->assertSession()->selectExists('Select an address');
+ $this->assertSession()->optionExists('Select an address', 'Pabst Blue Ribbon Dr (Original)');
+ $this->assertSession()->optionExists('Select an address', 'Pabst Blue Ribbon Dr');
+ $this->assertSession()->optionExists('Select an address', '+ Enter a new address');
+
+ $this->assertSession()->pageTextContains('Frederick Pabst');
+ $this->assertSession()->pageTextContains('Pabst Blue Ribbon Dr');
+ $this->assertSession()->pageTextContains('Milwaukee, WI 53177');
+ $this->assertSession()->pageTextContains('United States');
+
+ // Ensure the billing profile keeps the same revision data.
+ $this->container->get('entity_type.manager')->getStorage('commerce_order')->resetCache();
+ $order = Order::load($order->id());
+ // We reload it, because the order will give us a specific revision.
+ /** @var \Drupal\profile\Entity\Profile $billing_profile */
+ $billing_profile = Profile::load($order->getBillingProfile()->id());
+ $billing_profile->setNewRevision();
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $billing_profile->get('address')->first();
+ $address->set('given_name', 'Joseph');
+ $address->set('family_name', 'Schlitz ');
+ $billing_profile->save();
+
+ $this->drupalGet($order->toUrl('edit-form'));
+ $this->assertSession()->pageTextContains('Frederick Pabst');
+ $this->assertSession()->pageTextNotContains('Joseph Schlitz');
}
/**
diff --git a/modules/order/tests/src/Functional/OrderBrowserTestBase.php b/modules/order/tests/src/Functional/OrderBrowserTestBase.php
index 97216422..e2474c0a 100644
--- a/modules/order/tests/src/Functional/OrderBrowserTestBase.php
+++ b/modules/order/tests/src/Functional/OrderBrowserTestBase.php
@@ -36,6 +36,9 @@ abstract class OrderBrowserTestBase extends CommerceBrowserTestBase {
'administer commerce_order',
'administer commerce_order_type',
'access commerce_order overview',
+ 'administer profile',
+ 'create customer profile',
+ 'update own customer profile',
], parent::getAdministratorPermissions());
}
diff --git a/modules/order/tests/src/FunctionalJavascript/OrderAdminTest.php b/modules/order/tests/src/FunctionalJavascript/OrderAdminTest.php
new file mode 100644
index 00000000..181daf80
--- /dev/null
+++ b/modules/order/tests/src/FunctionalJavascript/OrderAdminTest.php
@@ -0,0 +1,68 @@
+drupalGet(Url::fromRoute('entity.commerce_order.add_page'));
+ $this->getSession()->getPage()->checkField('New customer');
+ $this->waitForAjaxToFinish();
+ $this->getSession()->getPage()->fillField('Email', 'email@example.com');
+ $this->getSession()->getPage()->pressButton('Create');
+
+ $this->getSession()->getPage()->fillField('First name', 'Celia');
+ $this->getSession()->getPage()->fillField('Last name', 'Engeseth');
+ $this->getSession()->getPage()->fillField('Street address', '8502 Pilgrim St.');
+ $this->getSession()->getPage()->fillField('City', 'Mokena');
+ $this->getSession()->getPage()->fillField('State', 'IL');
+ $this->getSession()->getPage()->fillField('Zip code', '60448');
+
+ $product_variation_field = $this->getSession()->getPage()->find('named', ['field', 'Product variation']);
+ $product_variation_field->setValue($this->variation->getTitle());
+ $this->getSession()->getDriver()->keyDown($product_variation_field->getXpath(), ' ');
+ $this->assertSession()->waitOnAutocomplete();
+ /** @var \Behat\Mink\Element\NodeElement[] $results */
+ $results = $this->getSession()->getPage()->findAll('css', '.ui-autocomplete li');
+ $this->assertCount(1, $results);
+ $results[0]->click();
+ $this->getSession()->getPage()->checkField('Override the unit price');
+ $this->getSession()->getPage()->fillField('Unit price', '12.00');
+ $this->getSession()->getPage()->pressButton('Create order item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+
+ $this->getSession()->getPage()->pressButton('Add new order item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->fieldExists('Product variation');
+ // Order item IEF does not affect profile_select for billing.
+ $this->assertSession()->fieldValueEquals('Street address', '8502 Pilgrim St.');
+
+ $open_ief = $this->getSession()->getPage()->find('css', '[data-drupal-selector="edit-order-items-form-inline-entity-form"]');
+ $open_ief->pressButton('Cancel');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->getSession()->getPage()->pressButton('Save');
+
+ $this->assertSession()->pageTextContains('The order has been successfully saved.');
+ $this->getSession()->getPage()->clickLink('Edit');
+ $this->saveHtmlOutput();
+
+ $this->assertSession()->fieldExists('Select an address');
+ $this->assertSession()->optionExists('Select an address', '8502 Pilgrim St.');
+ $this->assertSession()->optionExists('Select an address', '+ Enter a new address');
+ }
+
+}
diff --git a/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php
new file mode 100644
index 00000000..6b70ab3b
--- /dev/null
+++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php
@@ -0,0 +1,326 @@
+ '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 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(sprintf('Profile selected: %s', $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([
+ 'create customer profile',
+ 'update own customer profile',
+ ]);
+ $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(sprintf('Profile selected: %s', $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([
+ 'create customer profile',
+ 'update own customer profile',
+ ]);
+ $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()->pageTextContains('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(sprintf('Profile selected: %s', $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([
+ 'create customer profile',
+ 'update own customer profile',
+ ]);
+ $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(sprintf('Profile selected: %s', $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.
+ */
+ public function testProfileSelectAuthenticatedEdit() {
+ $account = $this->createUser([
+ 'create customer profile',
+ 'update own customer profile',
+ ]);
+ /** @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);
+ // 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(sprintf('Profile selected: %s', $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());
+ }
+
+ /**
+ * Tests that editing and then canceling does not change data on save.
+ *
+ * @group debug
+ */
+ public function testEditThenCancelDataIntegrity() {
+ $account = $this->createUser([
+ 'create customer profile',
+ 'update own customer profile',
+ ]);
+ /** @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->getSession()->getPage()->pressButton('Edit');
+ $this->getSession()->getPage()->fillField('Street address', 'Andrássy út 22');
+ $this->getSession()->getPage()->pressButton('Cancel changes');
+ $this->getSession()->getPage()->pressButton('Submit');
+
+ $this->profileStorage->resetCache([$profile_address1->id()]);
+ $profile_address1 = $this->profileStorage->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());
+
+ }
+
+}
diff --git a/modules/order/tests/src/Kernel/ProfileSelectTest.php b/modules/order/tests/src/Kernel/ProfileSelectTest.php
new file mode 100644
index 00000000..8681c525
--- /dev/null
+++ b/modules/order/tests/src/Kernel/ProfileSelectTest.php
@@ -0,0 +1,774 @@
+installConfig(['commerce_order']);
+ $this->installEntitySchema('profile');
+ $this->formBuilder = $this->container->get('form_builder');
+
+ // Create a uid1 so permissions don't get bypassed later on.
+ $uid1 = $this->createUser();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'profile_select_test_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ // Default basic element definition.
+ $form['profile'] = [
+ '#type' => 'commerce_profile_select',
+ '#title' => 'Select an address',
+ '#create_title' => '+ Enter a new address',
+ '#default_value' => $form_state->get('profile') ?: Profile::create([
+ 'type' => 'customer',
+ 'uid' => $form_state->get('user') ?: User::getAnonymousUser(),
+ ]),
+ '#profile_latest_revision' => TRUE,
+ '#default_country' => 'US',
+ '#available_countries' => ['HU', 'FR', 'US', 'RS', 'DE'],
+ ];
+
+ switch ($this->formTestCase) {
+ case 'testValidateElementPropertiesDefaultValueEmpty':
+ $form['profile']['#default_value'] = NULL;
+ break;
+
+ case 'testValidateElementPropertiesDefaultValueInstance':
+ $form['profile']['#default_value'] = '14';
+ break;
+
+ case 'testValidateElementPropertiesAvailableCountries':
+ $form['profile']['#available_countries'] = 'US';
+ break;
+
+ case 'testDefaultCountryIsNotValid':
+ $form['profile']['#default_country'] = 'CA';
+ break;
+
+ default:
+ // Do nothing, the default definition is enough to test with.
+ break;
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ // If the form is being validated.
+ // The value has been set on the "profile" key of the element.
+ $profile = $form_state->getValue(['profile', 'profile']);
+ $this->assertInstanceOf(Profile::class, $profile);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {}
+
+ /**
+ * Tests that the element expects a default value.
+ */
+ public function testValidateElementPropertiesDefaultValueEmpty() {
+ $this->setExpectedException(\InvalidArgumentException::class, 'The commerce_profile_select element requires the #default_value property.');
+ $this->formTestCase = __FUNCTION__;
+ $this->buildTestForm();
+ }
+
+ /**
+ * Tests that the element expects a default value of ProfileInterface.
+ */
+ public function testValidateElementPropertiesDefaultValueInstance() {
+ $this->setExpectedException(\InvalidArgumentException::class, 'The commerce_profile_select #default_value property must be a profile entity.');
+ $this->formTestCase = __FUNCTION__;
+ $this->buildTestForm();
+ }
+
+ /**
+ * Tests that the element expects a default value of ProfileInterface.
+ */
+ public function testValidateElementPropertiesAvailableCountries() {
+ $this->setExpectedException(\InvalidArgumentException::class, 'The commerce_profile_select #available_countries property must be an array.');
+ $this->formTestCase = __FUNCTION__;
+ $this->buildTestForm();
+ }
+
+ /**
+ * Tests that an invalid default country resets to NULL.
+ */
+ public function testValidateElementPropertiesDefaultCountry() {
+ $this->formTestCase = 'testDefaultCountryIsValid';
+ $form = $this->buildTestForm();
+ $this->assertEquals('US', $form['profile']['#default_country']);
+
+ $this->formTestCase = 'testDefaultCountryIsNotValid';
+ $form = $this->buildTestForm();
+ $this->assertNull($form['profile']['#default_country']);
+ }
+
+ /**
+ * Tests the available profiles select list.
+ *
+ * Ensures:
+ * - Anonymous users do not see the select list
+ * - Users without existing profiles do not see the select list
+ * - Users with existing profiles see the select list
+ * - The select list defaults to the current user's profile.
+ * - The element's default value is the user's default profile.
+ */
+ public function testAvailableProfilesSelectList() {
+ $this->formTestCase = __FUNCTION__;
+
+ // Test as anonymous user, which should never show the select list.
+ $form = $this->buildTestForm();
+ $this->assertFalse($form['profile']['available_profiles']['#access']);
+
+ // Test that a user without previous profiles does not see the select list.
+ $user = $this->createUser([], [
+ 'create customer profile',
+ ]);
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ ]);
+ $this->assertFalse($form['profile']['available_profiles']['#access']);
+
+ // Create profiles for the user, assert the select list is available.
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ ]);
+
+ $this->assertTrue($form['profile']['available_profiles']['#access']);
+ $this->assertCount(3, $form['profile']['available_profiles']['#options']);
+ $this->assertEquals([
+ $test_profile1->id() => $test_profile1->label(),
+ $test_profile2->id() => $test_profile2->label(),
+ '_new' => '+ Enter a new address',
+ ], $form['profile']['available_profiles']['#options']);
+ $this->assertEquals($test_profile1->id(), $form['profile']['available_profiles']['#default_value']);
+ $this->assertEquals($test_profile1->id(), $form['profile']['#default_value']->id());
+
+ // If we mark the test_profile2 as default, it should be the default option.
+ $test_profile2->setDefault(TRUE);
+ $test_profile2->save();
+
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ ]);
+ $this->assertEquals($test_profile2->id(), $form['profile']['available_profiles']['#default_value']);
+ $this->assertEquals($test_profile2->id(), $form['profile']['#default_value']->id());
+ }
+
+ /**
+ * Tests that the element default value respects provided profile.
+ */
+ public function testAvailableProfilesListWithProvidedDefaultValue() {
+ $user = $this->createUser();
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ // Pass the second profile to form, so it is the one being modified.
+ $form = $this->buildTestForm([
+ 'profile' => $test_profile2,
+ ]);
+ $this->assertEquals($test_profile2->id(), $form['profile']['available_profiles']['#default_value']);
+ $this->assertEquals($test_profile2->id(), $form['profile']['#default_value']->id());
+ }
+
+ /**
+ * Tests that the #profile attribute contains the profile value.
+ */
+ public function testProfilePropertyOnElement() {
+ $form = $this->buildTestForm();
+ $this->assertInstanceOf(
+ Profile::class,
+ $form['profile']['#profile']
+ );
+ $this->assertInstanceOf(
+ Profile::class,
+ $form['profile']['#default_value']
+ );
+ }
+
+ /**
+ * Tests using a previous revision with the profile select element.
+ *
+ * This asserts that a previous revision passed into the element will not
+ * be allowed to be modified.
+ */
+ public function testLatestRevision() {
+ $user = $this->createUser([], [
+ 'create customer profile',
+ ]);
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ $test_profile2_revision_id = $test_profile2->getRevisionId();
+
+ // Mark it as default, and create a new revision.
+ $test_profile2 = $this->reloadEntity($test_profile2);
+ $test_profile2->setDefault(TRUE);
+ $test_profile2->setNewRevision();
+ $test_profile2->save();
+
+ $this->assertNotEquals(
+ $test_profile2_revision_id,
+ $test_profile2->getRevisionId()
+ );
+
+ $original_test_profile2 = $this->container->get('entity_type.manager')
+ ->getStorage('profile')
+ ->loadRevision($test_profile2_revision_id);
+
+ $this->assertFalse($original_test_profile2->isDefaultRevision());
+ $this->assertTrue($test_profile2->isDefaultRevision());
+
+ // Pass the second profile to form, so it is the one being modified.
+ $form = $this->buildTestForm([
+ 'profile' => $original_test_profile2,
+ ]);
+ $this->assertCount(4, $form['profile']['available_profiles']['#options']);
+ $this->assertEquals([
+ '_existing' => t(':label (Original)', [':label' => $original_test_profile2->label()]),
+ $test_profile1->id() => $test_profile1->label(),
+ $test_profile2->id() => $test_profile2->label(),
+ '_new' => '+ Enter a new address',
+ ], $form['profile']['available_profiles']['#options']);
+ $this->assertEquals('_existing', $form['profile']['available_profiles']['#default_value']);
+ $this->assertFalse($form['profile']['profile_view']['edit']['#access']);
+ }
+
+ /**
+ * Tess the element when passing values from the select list.
+ */
+ public function testAvailableProfilesFormStateValue() {
+ $user = $this->createUser();
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ $test_profile2_revision_id = $test_profile2->getRevisionId();
+
+ // Mark it as default, and create a new revision.
+ $test_profile2 = $this->reloadEntity($test_profile2);
+ $test_profile2->setDefault(TRUE);
+ $test_profile2->setNewRevision();
+ $test_profile2->save();
+
+ $this->assertNotEquals(
+ $test_profile2_revision_id,
+ $test_profile2->getRevisionId()
+ );
+
+ $original_test_profile2 = $this->container->get('entity_type.manager')
+ ->getStorage('profile')
+ ->loadRevision($test_profile2_revision_id);
+
+ $this->assertFalse($original_test_profile2->isDefaultRevision());
+ $this->assertTrue($test_profile2->isDefaultRevision());
+
+ // Pass a previous revision to the form, but specify we want a new profile.
+ $form = $this->buildTestForm([
+ 'profile' => $original_test_profile2,
+ 'values' => [
+ 'profile' => [
+ 'available_profiles' => '_new',
+ ],
+ ],
+ 'input' => [
+ 'profile' => [
+ 'available_profiles' => '_new',
+ ],
+ ],
+ ]);
+ $this->assertEquals('_new', $form['profile']['available_profiles']['#value']);
+ $this->assertTrue($form['profile']['#profile']->isNew());
+
+ // Pass a previous revision to the form, but specify the first test profile.
+ $form = $this->buildTestForm([
+ 'profile' => $original_test_profile2,
+ 'values' => [
+ 'profile' => [
+ 'available_profiles' => $test_profile1->id(),
+ ],
+ ],
+ 'input' => [
+ 'profile' => [
+ 'available_profiles' => $test_profile1->id(),
+ ],
+ ],
+ ]);
+ $this->assertEquals($test_profile1->id(), $form['profile']['available_profiles']['#value']);
+ $this->assertEquals($test_profile1->id(), $form['profile']['#profile']->id());
+
+ // Pass in the latest revision for the default value, and ensure that we
+ // do not receive `_existing` as the option.
+ $form = $this->buildTestForm([
+ 'profile' => $original_test_profile2,
+ 'values' => [
+ 'profile' => [
+ 'available_profiles' => $test_profile2->id(),
+ ],
+ ],
+ 'input' => [
+ 'profile' => [
+ 'available_profiles' => $test_profile2->id(),
+ ],
+ ],
+ ]);
+ $this->assertEquals($test_profile2->id(), $form['profile']['available_profiles']['#value']);
+ $this->assertEquals($test_profile2->id(), $form['profile']['#profile']->id());
+ }
+
+ /**
+ * Tests that the `_new` option is controlled by permission access.
+ */
+ public function testCreateAccess() {
+ // Create a user who has profiles, but does not have the ability to create
+ // new. This replicates sites where users have a set of profiles to select
+ // from based on custom logic but cannot create new ones.
+ $user = $this->createUser([], []);
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ ]);
+
+ $this->assertCount(2, $form['profile']['available_profiles']['#options']);
+ $this->assertEquals([
+ $test_profile1->id() => $test_profile1->label(),
+ $test_profile2->id() => $test_profile2->label(),
+ ], $form['profile']['available_profiles']['#options']);
+ }
+
+ /**
+ * Tests editing an available profile is based on permissions.
+ */
+ public function testEditAccess() {
+ // Create a user who has profiles, but does not have the ability to create
+ // new. This replicates sites where users have a set of profiles to select
+ // from based on custom logic but cannot create new ones.
+ $user = $this->createUser([], [
+ 'create customer profile',
+ ]);
+ $test_profile1 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'organization' => '',
+ 'country_code' => 'FR',
+ 'postal_code' => '75002',
+ 'locality' => 'Paris',
+ 'address_line1' => 'A french street',
+ 'given_name' => 'John',
+ 'family_name' => 'LeSmith',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile1->setDefault(TRUE);
+ $test_profile1->save();
+ $test_profile2 = Profile::create([
+ 'type' => 'customer',
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ 'uid' => $user->id(),
+ ]);
+ $test_profile2->save();
+
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ 'profile' => $test_profile1,
+ ]);
+
+ $this->assertCount(3, $form['profile']['available_profiles']['#options']);
+ $this->assertEquals($test_profile1->id(), $form['profile']['available_profiles']['#default_value']);
+
+ $this->assertFalse($form['profile']['profile_view']['edit']['#access']);
+
+ $user = $this->createUser([], [
+ 'create customer profile',
+ 'update own profile',
+ 'update own customer profile',
+ ]);
+ $test_profile1->setOwner($user);
+ $test_profile1->save();
+ $test_profile2->setOwner($user);
+ $test_profile2->save();
+
+ $form = $this->buildTestForm([
+ 'user' => $user,
+ 'profile' => $test_profile1,
+ ]);
+ $this->assertTrue($form['profile']['profile_view']['edit']['#access']);
+ }
+
+ /**
+ * Tests the validation of the element.
+ *
+ * Assertions are made in this forms validate method. The main assertion
+ * is that the proposed profile is set as the element value during the
+ * validation process, so that other elements can validate against that
+ * selected profile.
+ *
+ * @see \Drupal\Tests\commerce_order\Kernel\ProfileSelectTest::validateForm
+ */
+ public function testElementValidation() {
+ $form = $this->buildTestForm();
+
+ $form_validator = $this->container->get('form_validator');
+
+ $form_state = new FormState();
+ $form_state->setProgrammed();
+ $form_validator->validateForm($this->getFormId(), $form, $form_state);
+ }
+
+ /**
+ * Tests the submission of the element.
+ */
+ public function testElementSubmission() {
+ $user = $this->createUser([], [
+ 'create customer profile',
+ ]);
+ $form_state = new FormState();
+ $form_state->setFormState([
+ 'user' => $user,
+ 'values' => [
+ 'profile' => [
+ 'available_profiles' => '_new',
+ 'address' => [
+ 0 => [
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'input' => [
+ 'profile' => [
+ 'available_profiles' => '_new',
+ 'address' => [
+ 0 => [
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Frederick',
+ 'family_name' => 'Pabst',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ $this->formBuilder->submitForm($this, $form_state);
+
+ $complete_form = $form_state->getCompleteForm();
+ // Assert the profile is stored on the element's #profile property and
+ // has been saved.
+ $profile = $complete_form['profile']['#profile'];
+ $this->assertInstanceOf(Profile::class, $profile);
+ $this->assertFalse($profile->isNew());
+
+ // Assert the profile was set to be the element value as well.
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile */
+ $profile = $form_state->getValue(['profile', 'profile']);
+ $this->assertInstanceOf(Profile::class, $profile);
+ $this->assertFalse($profile->isNew());
+
+ $profile_storage = $this->container->get('entity_type.manager')->getStorage('profile');
+ /** @var \Drupal\profile\Entity\ProfileInterface $initial_profile_revision */
+ $initial_profile_revision = $profile_storage->loadRevision($profile->getRevisionId());
+
+ // Resubmit the form with the profile we created. However, we're going to
+ // modify the address. This should cause a new revision to be created.
+ $form_state = new FormState();
+ $form_state->setFormState([
+ 'user' => $user,
+ 'profile' => $profile,
+ 'values' => [
+ 'profile' => [
+ 'available_profiles' => $profile->id(),
+ 'address' => [
+ 0 => [
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Joseph',
+ 'family_name' => 'Schlitz',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'input' => [
+ 'profile' => [
+ 'available_profiles' => $profile->id(),
+ 'address' => [
+ 0 => [
+ 'address' => [
+ 'country_code' => 'US',
+ 'postal_code' => '53177',
+ 'locality' => 'Milwaukee',
+ 'address_line1' => 'Pabst Blue Ribbon Dr',
+ 'administrative_area' => 'WI',
+ 'given_name' => 'Joseph',
+ 'family_name' => 'Schlitz',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ $this->formBuilder->submitForm($this, $form_state);
+
+ /** @var \Drupal\profile\Entity\ProfileInterface $updated_profile */
+ $updated_profile = $form_state->getValue(['profile', 'profile']);
+ $this->assertInstanceOf(Profile::class, $updated_profile);
+ $this->assertEquals($initial_profile_revision->id(), $updated_profile->id());
+ $this->assertNotEquals(
+ $initial_profile_revision->getRevisionId(),
+ $updated_profile->getRevisionId()
+ );
+ }
+
+ /**
+ * Build the test form.
+ *
+ * @param array $form_state_additions
+ * An array of values to add to the form state.
+ *
+ * @return array
+ * The rendered form.
+ *
+ * @throws \Drupal\Core\Form\EnforcedResponseException
+ * @throws \Drupal\Core\Form\FormAjaxException
+ */
+ protected function buildTestForm(array $form_state_additions = []) {
+ // Programmatically submit the form.
+ $form_state = new FormState();
+ $form_state->setProgrammed();
+ $form_state->setProcessInput();
+ $form_state->setFormState($form_state_additions);
+ $form = $this->formBuilder->buildForm($this, $form_state);
+
+ // If form values were passed, rebuild the form to simulate AJAX.
+ if (!empty($form_state_additions['values'])) {
+ $form_state->setMethod('GET');
+ $form_state->setValues($form_state_additions['values']);
+ $form = $this->formBuilder->rebuildForm($this->getFormId(), $form_state, $form);
+ }
+ return $form;
+ }
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
index a91aabeb..77942152 100644
--- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
@@ -270,6 +270,8 @@ class PaymentInformation extends CheckoutPaneBase {
$pane_form['billing_information'] = [
'#type' => 'commerce_profile_select',
+ '#title' => t('Select an address'),
+ '#create_title' => t('+ Enter a new address'),
'#default_value' => $billing_profile,
'#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 07f8b82c..42e0e2de 100644
--- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
@@ -74,6 +74,8 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase {
$form['billing_information'] = [
'#parents' => array_merge($form['#parents'], ['billing_information']),
'#type' => 'commerce_profile_select',
+ '#title' => t('Select an address'),
+ '#create_title' => t('+ Enter a new address'),
'#default_value' => $billing_profile,
'#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL,
'#available_countries' => $store ? $store->getBillingCountries() : [],
diff --git a/modules/payment/src/PluginForm/PaymentMethodEditForm.php b/modules/payment/src/PluginForm/PaymentMethodEditForm.php
index 8d195b22..a96a5a5c 100644
--- a/modules/payment/src/PluginForm/PaymentMethodEditForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodEditForm.php
@@ -67,6 +67,8 @@ class PaymentMethodEditForm extends PaymentGatewayFormBase implements ContainerI
$form['billing_information'] = [
'#parents' => array_merge($form['#parents'], ['billing_information']),
'#type' => 'commerce_profile_select',
+ '#title' => t('Select an address'),
+ '#create_title' => t('+ Enter a new address'),
'#default_value' => $billing_profile,
'#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL,
'#available_countries' => $store ? $store->getBillingCountries() : [],
diff --git a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php
index f77716ad..fe9acf50 100644
--- a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php
+++ b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php
@@ -237,6 +237,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase {
$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 an address');
+ $this->assertSession()->pageTextContains('Pabst Blue Ribbon Dr');
}
/**
@@ -371,7 +373,6 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase {
$radio_button = $this->getSession()->getPage()->findField('Example');
$radio_button->click();
$this->waitForAjaxToFinish();
-
$this->submitForm([
'payment_information[billing_information][address][0][address][given_name]' => 'Johnny',
'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed',