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