diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
index e53b0f9..0feec59 100644
--- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
+++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php
@@ -33,18 +33,14 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa
*/
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
$store = $this->order->getStore();
- $billing_profile = $this->order->getBillingProfile();
- if (!$billing_profile) {
- $profile_storage = $this->entityTypeManager->getStorage('profile');
- $billing_profile = $profile_storage->create([
- 'type' => 'customer',
- 'uid' => $this->order->getCustomerId(),
- ]);
- }
$pane_form['profile'] = [
'#type' => 'commerce_profile_select',
- '#default_value' => $billing_profile,
+ '#title' => $this->t('Select an address'),
+ '#create_title' => t('+ Enter a new address'),
+ '#profile' => $this->order->getBillingProfile(),
+ '#profile_type' => 'customer',
+ '#profile_uid' => $this->order->getCustomerId(),
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
];
diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php
index 6aacf1c..0f36739 100644
--- a/modules/order/src/Element/ProfileSelect.php
+++ b/modules/order/src/Element/ProfileSelect.php
@@ -3,6 +3,8 @@
namespace Drupal\commerce_order\Element;
use Drupal\commerce\Element\CommerceElementTrait;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
@@ -15,7 +17,9 @@ use Drupal\profile\Entity\ProfileInterface;
* @code
* $form['billing_profile'] = [
* '#type' => 'commerce_profile_select',
- * '#default_value' => $profile,
+ * '#profile' => $profile,
+ * '#profile_type' => 'customer',
+ * '#profile_uid' => \Drupal::currentUser()->id(),
* '#default_country' => 'FR',
* '#available_countries' => ['US', 'FR'],
* ];
@@ -24,6 +28,10 @@ use Drupal\profile\Entity\ProfileInterface;
* $form['billing_profile']['#profile']. Due to Drupal core limitations the
* profile can't be accessed via $form_state->getValue('billing_profile').
*
+ * Note:
+ * This element always behaves as required. For optional behavior add
+ * a checkbox above the element that hides it on #ajax.
+ *
* @RenderElement("commerce_profile_select")
*/
class ProfileSelect extends RenderElement {
@@ -36,16 +44,27 @@ class ProfileSelect extends RenderElement {
public function getInfo() {
$class = get_class($this);
return [
+ '#tree' => TRUE,
+ '#title' => t('Select a profile'),
+ '#create_title' => t('+ Enter a new profile'),
+ // The element doesn't use #default_value / #value. Use #profile instead.
+ '#default_value' => NULL,
+ // The preselected profile. A profile entity, or NULL.
+ '#profile' => NULL,
+ // Needed for creating new profiles, since #profile is not always given.
+ '#profile_type' => NULL,
+ '#profile_uid' => 0,
+ // Whether profiles should always be loaded in the latest revision.
+ // Disable when editing historical data, such as placed orders.
+ '#profile_latest_revision' => TRUE,
// The country to select if the address widget doesn't have a default.
'#default_country' => NULL,
// A list of country codes. If empty, all countries will be available.
'#available_countries' => [],
- // The profile entity operated on. Required.
- '#default_value' => NULL,
'#process' => [
[$class, 'attachElementSubmit'],
- [$class, 'processForm'],
+ [$class, 'processElement'],
],
'#element_validate' => [
[$class, 'validateElementSubmit'],
@@ -59,6 +78,36 @@ class ProfileSelect extends RenderElement {
}
/**
+ * Validates the element properties.
+ *
+ * @param array $element
+ * The form element.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if an element property is invalid, or empty but required.
+ */
+ public static function validateElementProperties(array $element) {
+ if (empty($element['#profile_type'])) {
+ throw new \InvalidArgumentException('The commerce_profile_select #profile_type property must be provided.');
+ }
+ if (isset($element['#profile']) && !($element['#profile'] instanceof ProfileInterface)) {
+ throw new \InvalidArgumentException('The commerce_profile_select #profile property must be a profile entity.');
+ }
+ if (!empty($element['#profile_uid']) && !is_numeric($element['#profile_uid'])) {
+ throw new \InvalidArgumentException('The commerce_profile_select #profile_uid property must be a numeric entity ID.');
+ }
+ if (!is_array($element['#available_countries'])) {
+ throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.');
+ }
+ // Make sure that the specified default country is available.
+ if (!empty($element['#default_country']) && !empty($element['#available_countries'])) {
+ if (!in_array($element['#default_country'], $element['#available_countries'])) {
+ $element['#default_country'] = NULL;
+ }
+ }
+ }
+
+ /**
* Builds the element form.
*
* @param array $element
@@ -68,47 +117,158 @@ class ProfileSelect extends RenderElement {
* @param array $complete_form
* The complete form structure.
*
- * @throws \InvalidArgumentException
- * Thrown when #default_value is empty or not an entity, or when
- * #available_countries is not an array of country codes.
- *
* @return array
* The processed form element.
*/
- public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) {
- if (empty($element['#default_value'])) {
- throw new \InvalidArgumentException('The commerce_profile_select element requires the #default_value property.');
+ public static function processElement(array $element, FormStateInterface $form_state, array &$complete_form) {
+ self::validateElementProperties($element);
+
+ $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']);
+ // If the provided profile is in a previous revision, make sure it stays
+ // available as an option when $element['#profile'] gets changed.
+ if (empty($element['#profile_latest_revision']) && empty($element_state['fixed_option'])) {
+ if (!empty($element['#profile']) && !$element['#profile']->isDefaultRevision()) {
+ $element_state['fixed_option'] = [
+ 'id' => self::buildOptionId($element['#profile'], $element['#profile_latest_revision']),
+ 'label' => $element['#profile']->label() . ' ' . t('(Original)'),
+ ];
+ }
}
- elseif (isset($element['#default_value']) && !($element['#default_value'] instanceof ProfileInterface)) {
- throw new \InvalidArgumentException('The commerce_profile_select #default_value property must be a profile entity.');
+
+ /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
+ $profile_storage = \Drupal::entityTypeManager()->getStorage('profile');
+ $user_storage = \Drupal::entityTypeManager()->getStorage('user');
+ $user = $element['#profile_uid'] ? $user_storage->load($element['#profile_uid']) : NULL;
+ $profiles = [];
+ if ($user) {
+ $profiles = $profile_storage->loadMultipleByUser($user, $element['#profile_type'], TRUE);
}
- if (!is_array($element['#available_countries'])) {
- throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.');
+ $profile_options = [];
+ foreach ($profiles as $profile) {
+ $option_id = self::buildOptionId($profile, $element['#profile_latest_revision']);
+ $profile_options[$option_id] = $profile->label();
}
- // Make sure that the specified default country is available.
- if (!empty($element['#default_country']) && !empty($element['#available_countries'])) {
- if (!in_array($element['#default_country'], $element['#available_countries'])) {
- $element['#default_country'] = NULL;
+ if (!empty($element_state['fixed_option'])) {
+ $option_id = $element_state['fixed_option']['id'];
+ $profile_options[$option_id] = $element_state['fixed_option']['label'];
+ }
+
+ $user_input = $form_state->getUserInput();
+ $selected_option = NestedArray::getValue($user_input, array_merge($element['#parents'], ['profile_selection']));
+ if (!empty($selected_option)) {
+ // Keep the last selection in element state, so that the element knows
+ // where to return the customer upon cancelling form mode.
+ $element_state['selected_option'] = $selected_option;
+ }
+ elseif (!empty($element_state['selected_option'])) {
+ $selected_option = $element_state['selected_option'];
+ }
+ else {
+ $selected_option = '_new';
+ if ($default_profile = self::selectDefaultProfile($element, $profiles)) {
+ $selected_option = self::buildOptionId($default_profile, $element['#profile_latest_revision']);
}
}
- $element['#profile'] = $element['#default_value'];
- $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
- $form_display->buildForm($element['#profile'], $element, $form_state);
- if (!empty($element['address']['widget'][0])) {
- $widget_element = &$element['address']['widget'][0];
- // Remove the details wrapper from the address widget.
- $widget_element['#type'] = 'container';
- // Provide a default country.
- if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) {
- $widget_element['address']['#default_value']['country_code'] = $element['#default_country'];
+ if ($selected_option == '_new') {
+ $element['#profile'] = $profile_storage->create([
+ 'type' => $element['#profile_type'],
+ 'uid' => $element['#profile_uid'],
+ ]);
+ $element_state['mode'] = 'form';
+ }
+ else {
+ $id_parts = explode('-', $selected_option);
+ if (!empty($id_parts[1])) {
+ $element['#profile'] = $profile_storage->loadRevision($id_parts[1]);
+ }
+ else {
+ $element['#profile'] = $profile_storage->load($id_parts[0]);
}
- // Limit the available countries.
- if (!empty($element['#available_countries'])) {
- $widget_element['address']['#available_countries'] = $element['#available_countries'];
+ }
+
+ $id_prefix = implode('-', $element['#parents']);
+ $wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
+ $element = [
+ '#prefix' => '
',
+ '#suffix' => '
',
+ // Pass the id along to other methods.
+ '#wrapper_id' => $wrapper_id,
+ ] + $element;
+
+ if ($element_state['mode'] == 'view') {
+ $element['profile_selection'] = [
+ '#type' => 'select',
+ '#title' => $element['#title'],
+ '#options' => $profile_options + ['_new' => $element['#create_title']],
+ '#default_value' => $selected_option,
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $wrapper_id,
+ ],
+ '#limit_validation_errors' => [
+ array_merge($element['#parents'], ['profile_selection']),
+ ],
+ '#weight' => -1,
+ '#access' => !empty($profile_options),
+ ];
+
+ $view_builder = \Drupal::entityTypeManager()->getViewBuilder('profile');
+ $element['profile'] = $view_builder->view($element['#profile'], 'default');
+
+ $element['edit_button'] = [
+ '#type' => 'submit',
+ '#value' => t('Edit'),
+ '#limit_validation_errors' => [],
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $wrapper_id,
+ ],
+ '#submit' => [[get_called_class(), 'ajaxSubmit']],
+ '#name' => implode('_', $element['#parents']) . '_edit',
+ '#element_mode' => 'form',
+ // When given a historical profile, don't allow it to be edited.
+ '#access' => $element['#profile_latest_revision'] || $element['#profile']->isDefaultRevision(),
+ ];
+ }
+ else {
+ $element['form'] = [
+ '#parents' => array_merge($element['#parents'], ['form']),
+ ];
+ $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
+ $form_display->buildForm($element['#profile'], $element['form'], $form_state);
+ if (!empty($element['form']['address']['widget'][0])) {
+ $widget_element = &$element['form']['address']['widget'][0];
+ // Remove the details wrapper from the address widget.
+ $widget_element['#type'] = 'container';
+ // Provide a default country.
+ if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) {
+ $widget_element['address']['#default_value']['country_code'] = $element['#default_country'];
+ }
+ // Limit the available countries.
+ if (!empty($element['#available_countries'])) {
+ $widget_element['address']['#available_countries'] = $element['#available_countries'];
+ }
}
+
+ $element['cancel_button'] = [
+ '#type' => 'submit',
+ '#value' => t('Return to address selection'),
+ '#limit_validation_errors' => [],
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $wrapper_id,
+ ],
+ '#submit' => [[get_called_class(), 'ajaxSubmit']],
+ '#name' => implode('_', $element['#parents']) . '_cancel',
+ '#element_mode' => 'view',
+ '#access' => !empty($profiles),
+ ];
}
+ // Persist any changes made to $element_state.
+ self::setElementState($element['#parents'], $form_state, $element_state);
+
return $element;
}
@@ -125,9 +285,12 @@ class ProfileSelect extends RenderElement {
* form, as a protection against buggy behavior.
*/
public static function validateForm(array &$element, FormStateInterface $form_state) {
- $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
- $form_display->extractFormValues($element['#profile'], $element, $form_state);
- $form_display->validateFormValues($element['#profile'], $element, $form_state);
+ $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']);
+ if ($element_state['mode'] == 'form') {
+ $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
+ $form_display->extractFormValues($element['#profile'], $element['form'], $form_state);
+ $form_display->validateFormValues($element['#profile'], $element['form'], $form_state);
+ }
}
/**
@@ -139,9 +302,116 @@ class ProfileSelect extends RenderElement {
* The current state of the form.
*/
public static function submitForm(array &$element, FormStateInterface $form_state) {
- $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
- $form_display->extractFormValues($element['#profile'], $element, $form_state);
- $element['#profile']->save();
+ $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']);
+ if ($element_state['mode'] == 'form') {
+ $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
+ $form_display->extractFormValues($element['#profile'], $element['form'], $form_state);
+ if ($element['#profile']->hasTranslationChanges()) {
+ $element['#profile']->save();
+ }
+ }
+ }
+
+ /**
+ * Builds the option ID for the given profile.
+ *
+ * @param \Drupal\profile\Entity\ProfileInterface $profile
+ * The profile.
+ * @param bool $use_latest_revision
+ *
+ * @return string
+ * The option ID.
+ */
+ public static function buildOptionId(ProfileInterface $profile, $use_latest_revision = TRUE) {
+ $option_id = $profile->id();
+ if (!$use_latest_revision) {
+ $option_id .= '-' . $profile->getRevisionId();
+ }
+ return $option_id;
+ }
+
+ /**
+ * Selects the default profile for the given element.
+ *
+ * @param array $element
+ * The element.
+ * @param array $profiles
+ * The available profiles.
+ *
+ * @return \Drupal\profile\Entity\ProfileInterface|null
+ * The default profile, or NULL if none found.
+ */
+ public static function selectDefaultProfile(array $element, array $profiles) {
+ $default_profile = NULL;
+ if (!empty($element['#profile'])) {
+ $default_profile = $element['#profile'];
+ }
+ elseif (!empty($profiles)) {
+ $default_profile = reset($profiles);
+ foreach ($profiles as $profile) {
+ if ($profile->isDefault()) {
+ $default_profile = $profile;
+ break;
+ }
+ }
+ }
+
+ return $default_profile;
+ }
+
+ /**
+ * Ajax callback.
+ */
+ public static function ajaxRefresh(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
+ return $element;
+ }
+
+ /**
+ * Ajax submit callback.
+ */
+ public static function ajaxSubmit(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
+ $element_state = self::getElementState($element['#parents'], $form_state, ['mode' => 'view']);
+ $element_state['mode'] = $triggering_element['#element_mode'];
+ self::setElementState($element['#parents'], $form_state, $element_state);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Gets the element state.
+ *
+ * @param array $parents
+ * The element parents.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $defaults
+ * The defaults.
+ *
+ * @return array
+ * The element state.
+ */
+ public static function getElementState(array $parents, FormStateInterface $form_state, array $defaults) {
+ $parents = array_merge(['element_state', '#parents'], $parents);
+ $element_state = (array) NestedArray::getValue($form_state->getStorage(), $parents);
+ return $element_state + $defaults;
+ }
+
+ /**
+ * Sets the element state.
+ *
+ * @param array $parents
+ * The element parents.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $element_state
+ * The element state.
+ */
+ public static function setElementState(array $parents, FormStateInterface $form_state, array $element_state) {
+ $parents = array_merge(['element_state', '#parents'], $parents);
+ NestedArray::setValue($form_state->getStorage(), $parents, $element_state);
}
}
diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
index 2d0987c..94fe1af 100644
--- a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
+++ b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php
@@ -3,13 +3,10 @@
namespace Drupal\commerce_order\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of 'commerce_billing_profile'.
@@ -22,50 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* }
* )
*/
-class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
-
- /**
- * The entity type manager.
- *
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
- */
- protected $entityTypeManager;
-
- /**
- * Constructs a new BillingProfileWidget object.
- *
- * @param string $plugin_id
- * The plugin_id for the widget.
- * @param mixed $plugin_definition
- * The plugin implementation definition.
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The definition of the field to which the widget is associated.
- * @param array $settings
- * The widget settings.
- * @param array $third_party_settings
- * Any third party settings.
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity type manager.
- */
- public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) {
- parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
-
- $this->entityTypeManager = $entity_type_manager;
- }
-
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $plugin_id,
- $plugin_definition,
- $configuration['field_definition'],
- $configuration['settings'],
- $configuration['third_party_settings'],
- $container->get('entity_type.manager')
- );
- }
+class BillingProfileWidget extends WidgetBase {
/**
* {@inheritdoc}
@@ -75,20 +29,15 @@ class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginI
$order = $items[$delta]->getEntity();
$store = $order->getStore();
- if (!$items[$delta]->isEmpty()) {
- $profile = $items[$delta]->entity;
- }
- else {
- $profile = $this->entityTypeManager->getStorage('profile')->create([
- 'type' => 'customer',
- 'uid' => $order->getCustomerId(),
- ]);
- }
-
$element['#type'] = 'fieldset';
$element['profile'] = [
'#type' => 'commerce_profile_select',
- '#default_value' => $profile,
+ '#title' => $this->t('Select an address'),
+ '#create_title' => $this->t('+ Enter a new address'),
+ '#profile' => $items[$delta]->entity,
+ '#profile_type' => 'customer',
+ '#profile_uid' => $order->getCustomerId(),
+ '#profile_latest_revision' => $order->getState()->value == 'draft',
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
];
diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml
new file mode 100644
index 0000000..466dd4c
--- /dev/null
+++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml
@@ -0,0 +1,9 @@
+commerce_order_test.profile_select_form:
+ path: '/commerce_order_test/profile_select_test_form'
+ defaults:
+ _form: '\Drupal\commerce_order_test\Form\ProfileSelectTestForm'
+ _title: 'Profile select test form'
+ requirements:
+ _access: 'TRUE'
+ options:
+ no_cache: TRUE
diff --git a/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php
new file mode 100644
index 0000000..6d47d7a
--- /dev/null
+++ b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php
@@ -0,0 +1,43 @@
+ 'commerce_profile_select',
+ '#profile_type' => 'customer',
+ '#profile_uid' => \Drupal::currentUser()->id(),
+ '#available_countries' => ['FR', 'DE', 'HU', 'RS', 'US'],
+ ];
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $profile = $form['profile']['#profile'];
+ drupal_set_message($this->t('Profile selected: :label', [':label' => $profile->label()]));
+ }
+
+}
\ No newline at end of file
diff --git a/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php
new file mode 100644
index 0000000..56fd173
--- /dev/null
+++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php
@@ -0,0 +1,296 @@
+ 'HU',
+ 'given_name' => 'Gustav',
+ 'family_name' => 'Mahler',
+ 'address_line1' => 'Teréz körút 7',
+ 'locality' => 'Budapest',
+ 'postal_code' => '1067',
+ ];
+
+ /**
+ * Profile address values.
+ *
+ * @var array
+ */
+ protected $address2 = [
+ 'country_code' => 'DE',
+ 'given_name' => 'Johann Sebastian',
+ 'family_name' => 'Bach',
+ 'address_line1' => 'Thomaskirchhof 15',
+ 'locality' => 'Leipzig',
+ 'postal_code' => '04109',
+ ];
+
+ /**
+ * The profile storage.
+ *
+ * @var \Drupal\profile\ProfileStorageInterface
+ */
+ protected $profileStorage;
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = [
+ 'commerce_order_test',
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->profileStorage = $this->container->get('entity_type.manager')->getStorage('profile');
+ }
+
+ /**
+ * Tests creating a profile as an authenticated user.
+ */
+ public function testAnonymous() {
+ // Create a profile for a different user to make sure it's not shown.
+ $profile1 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address1,
+ ]);
+
+ $this->drupalLogout();
+ $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->fieldNotExists('Select a profile');
+ $this->getSession()->getPage()->fillField('profile[form][address][0][address][country_code]', $this->address1['country_code']);
+ $this->waitForAjaxToFinish();
+
+ $edit = [];
+ foreach ($this->address1 as $key => $value) {
+ if ($key == 'country_code') {
+ continue;
+ }
+ $edit['profile[form][address][0][address][' . $key . ']'] = $value;
+ }
+ $this->submitForm($edit, 'Submit');
+
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile */
+ $profile = $this->profileStorage->load(1);
+ $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile->label()]));
+
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $profile->get('address')->first();
+ $this->assertEquals($this->address1['country_code'], $address->getCountryCode());
+ $this->assertEquals($this->address1['given_name'], $address->getGivenName());
+ $this->assertEquals($this->address1['family_name'], $address->getFamilyName());
+ $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1());
+ $this->assertEquals($this->address1['locality'], $address->getLocality());
+ $this->assertEquals($this->address1['postal_code'], $address->getPostalCode());
+ }
+
+ /**
+ * Tests selecting a profile as an authenticated user.
+ */
+ public function testAuthenticated() {
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */
+ $profile1 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address1,
+ ]);
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile2 */
+ $profile2 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address2,
+ 'is_default' => TRUE,
+ ]);
+
+ $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->fieldExists('Select a profile');
+ // The last created profile should be selected by default.
+ $this->assertSession()->pageTextContains($this->address2['locality']);
+
+ $this->getSession()->getPage()->fillField('Select a profile', $profile1->id());
+ $this->waitForAjaxToFinish();
+ $this->assertSession()->pageTextContains($this->address1['locality']);
+ $this->submitForm([], 'Submit');
+ $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile1->label()]));
+
+ $this->profileStorage->resetCache([$profile1->id()]);
+ $profile1 = $this->profileStorage->load($profile1->id());
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $profile1->get('address')->first();
+ // Assert that field values have not changed.
+ $this->assertEquals($this->address1['country_code'], $address->getCountryCode());
+ $this->assertEquals($this->address1['given_name'], $address->getGivenName());
+ $this->assertEquals($this->address1['family_name'], $address->getFamilyName());
+ $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1());
+ $this->assertEquals($this->address1['locality'], $address->getLocality());
+ $this->assertEquals($this->address1['postal_code'], $address->getPostalCode());
+
+ $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE);
+ $this->assertCount(2, $profiles);
+ }
+
+ /**
+ * Tests creating the initial profile as an authenticated user.
+ */
+ public function testAuthenticatedEmpty() {
+ $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->fieldNotExists('Select a profile');
+ $this->getSession()->getPage()->fillField('Country', $this->address1['country_code']);
+ $this->waitForAjaxToFinish();
+
+ $edit = [];
+ foreach ($this->address1 as $key => $value) {
+ if ($key == 'country_code') {
+ continue;
+ }
+ $edit['profile[form][address][0][address][' . $key . ']'] = $value;
+ }
+ $this->submitForm($edit, 'Submit');
+
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile */
+ $profile = $this->profileStorage->load(1);
+ $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile->label()]));
+
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $profile->get('address')->first();
+ $this->assertEquals($this->address1['country_code'], $address->getCountryCode());
+ $this->assertEquals($this->address1['given_name'], $address->getGivenName());
+ $this->assertEquals($this->address1['family_name'], $address->getFamilyName());
+ $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1());
+ $this->assertEquals($this->address1['locality'], $address->getLocality());
+ $this->assertEquals($this->address1['postal_code'], $address->getPostalCode());
+
+ $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE);
+ $this->assertCount(1, $profiles);
+ }
+
+ /**
+ * Tests creating a profile as an authenticated user.
+ */
+ public function testAuthenticatedCreate() {
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */
+ $profile1 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address1,
+ ]);;
+
+ $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->fieldExists('Select a profile');
+ // The last created profile should be selected by default.
+ $this->assertSession()->pageTextContains($this->address1['locality']);
+
+ $this->getSession()->getPage()->fillField('Select a profile', '_new');
+ $this->waitForAjaxToFinish();
+ $this->getSession()->getPage()->fillField('Country', $this->address2['country_code']);
+ $this->waitForAjaxToFinish();
+ $edit = [];
+ foreach ($this->address2 as $key => $value) {
+ if ($key == 'country_code') {
+ continue;
+ }
+ $edit['profile[form][address][0][address][' . $key . ']'] = $value;
+ }
+
+ $this->submitForm($edit, 'Submit');
+
+ $new_profile = $this->profileStorage->load(2);
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $new_profile->get('address')->first();
+
+ $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $new_profile->label()]));
+ // Assert that field values have not changed.
+ $this->assertEquals($this->address2['country_code'], $address->getCountryCode());
+ $this->assertEquals($this->address2['given_name'], $address->getGivenName());
+ $this->assertEquals($this->address2['family_name'], $address->getFamilyName());
+ $this->assertEquals($this->address2['address_line1'], $address->getAddressLine1());
+ $this->assertEquals($this->address2['locality'], $address->getLocality());
+ $this->assertEquals($this->address2['postal_code'], $address->getPostalCode());
+
+ $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE);
+ $this->assertCount(2, $profiles);
+ }
+
+ /**
+ * Tests editing a profile as an authenticated user.
+ *
+ * @group debug
+ */
+ public function testAuthenticatedEdit() {
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */
+ $profile1 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address1,
+ ]);
+ /** @var \Drupal\profile\Entity\ProfileInterface $profile2 */
+ $profile2 = $this->createEntity('profile', [
+ 'type' => 'customer',
+ 'uid' => $this->adminUser->id(),
+ 'address' => $this->address2,
+ 'is_default' => TRUE,
+ ]);
+
+ $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->fieldExists('Select a profile');
+ // The last created profile should be selected by default.
+ $this->assertSession()->pageTextContains($this->address2['locality']);
+ $this->getSession()->getPage()->pressButton('Edit');
+ $this->waitForAjaxToFinish();
+
+ foreach ($this->address2 as $key => $value) {
+ $this->assertSession()->fieldValueEquals('profile[form][address][0][address][' . $key . ']', $value);
+ }
+ $this->getSession()->getPage()->fillField('Street address', 'Andrássy út 22');
+ $this->submitForm([], 'Submit');
+
+ $this->profileStorage->resetCache([$profile2->id()]);
+ $profile2 = $this->profileStorage->load($profile2->id());
+
+ /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
+ $address = $profile2->get('address')->first();
+
+ $this->assertSession()->responseContains(new FormattableMarkup('Profile selected: :label', [':label' => $profile2->label()]));
+ // Assert that field values have not changed.
+ $this->assertEquals($this->address2['country_code'], $address->getCountryCode());
+ $this->assertEquals($this->address2['given_name'], $address->getGivenName());
+ $this->assertEquals($this->address2['family_name'], $address->getFamilyName());
+ $this->assertEquals('Andrássy út 22', $address->getAddressLine1());
+ $this->assertEquals($this->address2['locality'], $address->getLocality());
+ $this->assertEquals($this->address2['postal_code'], $address->getPostalCode());
+
+ $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE);
+ $this->assertCount(2, $profiles);
+ }
+
+}
diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
index 2b2e1b0..a27d180 100644
--- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
+++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php
@@ -135,17 +135,14 @@ class PaymentInformation extends CheckoutPaneBase {
}
else {
$store = $this->order->getStore();
- $billing_profile = $this->order->getBillingProfile();
- if (!$billing_profile) {
- $billing_profile = $this->entityTypeManager->getStorage('profile')->create([
- 'uid' => $this->order->getCustomerId(),
- 'type' => 'customer',
- ]);
- }
$pane_form['billing_information'] = [
'#type' => 'commerce_profile_select',
- '#default_value' => $billing_profile,
+ '#title' => $this->t('Select an address'),
+ '#create_title' => $this->t('+ Enter a new address'),
+ '#profile' => $this->order->getBillingProfile(),
+ '#profile_type' => 'customer',
+ '#profile_uid' => $this->order->getCustomerId(),
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
];
diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
index 0a3ece9..9b09eaf 100644
--- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
@@ -54,16 +54,6 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase {
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
$payment_method = $this->entity;
- /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */
- $billing_profile = $payment_method->getBillingProfile();
- if (!$billing_profile) {
- /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */
- $billing_profile = Profile::create([
- 'type' => 'customer',
- 'uid' => $payment_method->getOwnerId(),
- ]);
- }
-
if ($order = $this->routeMatch->getParameter('commerce_order')) {
$store = $order->getStore();
}
@@ -76,7 +66,10 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase {
$form['billing_information'] = [
'#parents' => array_merge($form['#parents'], ['billing_information']),
'#type' => 'commerce_profile_select',
- '#default_value' => $billing_profile,
+ '#title' => t('Select an address'),
+ '#create_title' => t('+ Enter a new address'),
+ '#profile_type' => 'customer',
+ '#profile_uid' => $payment_method->getOwnerId(),
'#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL,
'#available_countries' => $store ? $store->getBillingCountries() : [],
];