diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index 0d7a7185..d7350a94 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -14,7 +14,7 @@ use Drupal\Core\Form\FormStateInterface; * wrapper_element = "fieldset", * ) */ -class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterface { +class BillingInformation extends BillingInformationPaneBase { /** * {@inheritdoc} @@ -41,7 +41,7 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa '#available_countries' => $store->getBillingCountries(), '#profile_type' => 'customer', '#owner_uid' => $this->order->getCustomerId(), - ]; + ] + $this->getProfileSelectOptions(); return $pane_form; } diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformationPaneBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformationPaneBase.php new file mode 100644 index 0000000..9c558d4 --- /dev/null +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformationPaneBase.php @@ -0,0 +1,111 @@ + FALSE, + 'reuse_profile_label' => 'My shipping address is the same as my billing address.', + 'reuse_profile_default' => FALSE, + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationSummary() { + if (!empty($this->configuration['reuse_profile'])) { + $summary = $this->t('Allow reuse of shipping profile: Yes') . '
'; + $summary .= $this->t('Reuse shipping profile label: @label', [ + '@label' => $this->configuration['reuse_profile_label'] + ]) . '
'; + $summary .= $this->t('Reuse shipping profile by default: @default', [ + '@default' => ($this->configuration['reuse_profile_default']) + ? $this->t('Yes') + : $this->t('No') + ]); + } + else { + $summary = $this->t('Allow reuse of shipping profile: No'); + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $visible_state = [['.js-reuse-shipping-profile' => ['checked' => TRUE]]]; + + $form['reuse_profile'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow reuse of shipping profile for billing'), + '#default_value' => $this->configuration['reuse_profile'], + ]; + $form['reuse_profile']['#attributes']['class'][] = 'js-reuse-shipping-profile'; + $form['reuse_profile_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Reuse shipping profile label'), + '#default_value' => $this->configuration['reuse_profile_label'], + '#states' => [ + 'visible' => $visible_state, + ] + ]; + $form['reuse_profile_default'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Reuse shipping profile by default'), + '#default_value' => $this->configuration['reuse_profile_default'], + '#states' => [ + 'visible' => $visible_state, + ] + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + if (!$form_state->getErrors()) { + $values = $form_state->getValue($form['#parents']); + $this->configuration['reuse_profile'] = !empty($values['reuse_profile']); + $this->configuration['reuse_profile_label'] = $values['reuse_profile_label']; + $this->configuration['reuse_profile_default'] = !empty($values['reuse_profile_default']); + } + } + + protected function getProfileSelectOptions() { + $options = []; + + if (!empty($this->configuration['reuse_profile'])) { + $reuse_label = !empty($this->configuration['reuse_profile_label']) + ? $this->configuration['reuse_profile_label'] + : NULL; + $reuse_default = isset($this->configuration['reuse_profile_default']) + ? $this->configuration['reuse_profile_default'] + : FALSE; + + $options = [ + '#reuse_profile_label' => $reuse_label, + '#reuse_profile_source' => 'commerce_order_get_shipping_profile', + '#reuse_profile_default' => $reuse_default + ]; + } + + return $options; + } +} diff --git a/modules/order/commerce_order.module b/modules/order/commerce_order.module index 195092db..6ff6505a 100644 --- a/modules/order/commerce_order.module +++ b/modules/order/commerce_order.module @@ -194,3 +194,41 @@ function commerce_order_mail($key, &$message, $params) { $message['subject'] = $params['subject']; $message['body'][] = $params['body']; } + +/** + * Determine the current shipping profile for reuse as the billing profile. + * + * @param array $element + * The element reusing the shipping profile. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param array $complete_form + * The complete checkout form. + * + * @return \Drupal\profile\Entity\ProfileInterface|NULL + * The shipping profile, or NULL if none can be found. + */ +function commerce_order_get_shipping_profile(array $element, FormStateInterface $form_state, array $complete_form) { + $profile = NULL; + + $storage = $form_state->getStorage(); + if (isset($storage['pane_shipping_information[shipping_profile]']['profile'])) { + $profile = $storage['pane_shipping_information[shipping_profile]']['profile']; + } elseif (isset($complete_form['shipping_information']['shipping_profile']['#profile'])) { + $profile = $complete_form['shipping_information']['shipping_profile']['#profile']; + } else { + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ + $order = \Drupal::routeMatch()->getParameter('commerce_order'); + + if ($order instanceof \Drupal\commerce_order\Entity\OrderInterface) { + if (!$order->get('shipments')->isEmpty()) { + /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */ + $shipment = $order->get('shipments')->entity; + $profile = $shipment->getShippingProfile(); + } + } + } + + return $profile; +} + diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php index 2ed0ff81..ba7c4d59 100644 --- a/modules/order/src/Element/ProfileSelect.php +++ b/modules/order/src/Element/ProfileSelect.php @@ -7,8 +7,10 @@ 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; use Drupal\Core\Render\Element\FormElement; use Drupal\profile\Entity\ProfileInterface; +use Symfony\Component\HttpFoundation\Request; /** * Provides a form element for selecting a customer profile. @@ -22,6 +24,9 @@ use Drupal\profile\Entity\ProfileInterface; * '#available_countries' => ['US', 'FR'], * '#profile_type' => 'customer', * '#owner_uid' => \Drupal::currentUser()->id(), + * '#reuse_profile_label' => $this->t('My billing address is the same as my shipping address.'), + * '#reuse_profile_source' => 'commerce_shipping_get_shipping_profile', + * '#reuse_profile_default' => FALSE, * ]; * @endcode * @@ -41,6 +46,13 @@ class ProfileSelect extends FormElement { '#default_country' => NULL, // A list of country codes. If empty, all countries will be available. '#available_countries' => [], + // The label for the reuse profile checkbox. If empty, checkbox is hidden. + '#reuse_profile_label' => NULL, + // The function to call to return the profile to reuse. + '#reuse_profile_source' => NULL, + // Whether the reuse checkbox should be checked by default. + '#reuse_profile_default' => FALSE, + '#title' => t('Select a profile'), '#create_title' => t('+ Enter a new profile'), @@ -167,6 +179,18 @@ class ProfileSelect extends FormElement { ]; } + if (empty($element['#name'])) { + list($name) = explode('--', $element['#id']); + $element['#name'] = 'profile-select--' . $name; + } + + $storage = $form_state->getStorage(); + $reuse_profile = (isset($storage['pane_' . $element['#name']]['reuse_profile'])) + ? $storage['pane_' . $element['#name']]['reuse_profile'] + : $element['#reuse_profile_default']; + $storage['pane_' . $element['#name']]['reuse_profile'] = $reuse_profile; + $form_state->setStorage($storage); + /** @var \Drupal\profile\Entity\ProfileInterface $element_profile */ if ($element['#value'] == '_new') { $element_profile = $profile_storage->create([ @@ -219,6 +243,29 @@ class ProfileSelect extends FormElement { } } + $called_class = get_called_class(); + $reuse_enabled = (!empty($element['#reuse_profile_label']) && !empty($element['#reuse_profile_source'])); + if ($reuse_enabled) { + $element['reuse_profile'] = [ + '#title' => $element['#reuse_profile_label'], + '#type' => 'checkbox', + '#weight' => -5, + '#default_value' => $reuse_profile, + '#ajax' => [ + 'callback' => [$called_class, 'reuseProfileAjax'], + 'wrapper' => $wrapper_id, + ], + '#element_validate' => [[$called_class, 'reuseProfileValidate']] + ]; + } + if ($reuse_profile) { + foreach (Element::children($element) as $key) { + if (!in_array($key, ['reuse_profile'])) { + $element[$key]['#access'] = FALSE; + } + } + } + return $element; } @@ -282,10 +329,16 @@ class ProfileSelect extends FormElement { $element_profile = $profile_storage->load($value['profile_selection']); } - if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { - $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); - $form_display->extractFormValues($element_profile, $element, $form_state); - $form_display->validateFormValues($element_profile, $element, $form_state); + $pane_id = $element['#name']; + $storage = $form_state->getStorage(); + if (!isset($storage['pane_' . $pane_id]['reuse_profile']) || !$storage['pane_' . $pane_id]['reuse_profile']) { + + if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { + $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); + $form_display->extractFormValues($element_profile, $element, $form_state); + $form_display->validateFormValues($element_profile, $element, $form_state); + } + } $form_state->setValueForElement($element, $element_profile); @@ -311,20 +364,37 @@ class ProfileSelect extends FormElement { * The current state of the form. */ public static function submitForm(array &$element, FormStateInterface $form_state) { - /** @var \Drupal\profile\Entity\ProfileInterface $element_profile */ - $element_profile = $form_state->getValue($element['#parents']); - if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { - $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); - $form_display->extractFormValues($element_profile, $element, $form_state); - if ($element_profile->isNew()) { - $element_profile->save(); + $pane_id = $element['#name']; + $storage = $form_state->getStorage(); + if (isset($storage['pane_' . $pane_id]['reuse_profile']) && $storage['pane_' . $pane_id]['reuse_profile']) { + if (is_numeric($element['#reuse_profile_source'])) { + // Load profile by ID + $element_profile = \Drupal::entityTypeManager() + ->getStorage('profile') + ->load($element['#reuse_profile_source']); + } + else { + // Load profile from a callback + $element_profile = call_user_func($element['#reuse_profile_source'], $element, $form_state, $form_state->getCompleteForm()); + } + } else { + /** @var \Drupal\profile\Entity\ProfileInterface $element_profile */ + $element_profile = $form_state->getValue($element['#parents']); + if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) { + + $form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default'); + $form_display->extractFormValues($element_profile, $element, $form_state); + if ($element_profile->isNew()) { + $element_profile->save(); + } } - - $element['#default_value'] = $element_profile; - $element['#value'] = $element_profile->id(); } + $element['#default_value'] = $element_profile; + $element['#profile'] = $element_profile; + $element['#value'] = $element_profile->id(); + $form_state->setValueForElement($element, $element_profile); } @@ -349,6 +419,44 @@ class ProfileSelect extends FormElement { $form_state->setRebuild(); } + /** + * Reuse profile AJAX callback. + * + * @param array $form + * The complete form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Symfony\Component\HttpFoundation\Request $request + * The Request object. + * + * @return array + * The form element replace the wrapper with. + */ + public static function reuseProfileAjax(array &$form, FormStateInterface $form_state, Request $request) { + $triggering_element = $form_state->getTriggeringElement(); + $array_parents = $triggering_element['#array_parents']; + array_pop($array_parents); + return NestedArray::getValue($form, $array_parents); + } + /** + * The #element_validate callback for the reuse profile checkbox. + * + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public static function reuseProfileValidate(array $element, FormStateInterface $form_state) { + $form = $form_state->getCompleteForm(); + $profile_element_parents = $element['#parents']; + array_pop($profile_element_parents); + $profile_element = NestedArray::getValue($form, $profile_element_parents); + $pane_id = $profile_element['#name']; + $storage = $form_state->getStorage(); + $storage['pane_' . $pane_id]['reuse_profile'] = $element['#value']; + $form_state->setStorage($storage); + } + /** * Check if the address arrays are equal or not. In the equality comparition, * empty string is regarded as the same with NULL. diff --git a/modules/payment/src/Element/PaymentGatewayForm.php b/modules/payment/src/Element/PaymentGatewayForm.php index ae43de8c..db84f1f4 100644 --- a/modules/payment/src/Element/PaymentGatewayForm.php +++ b/modules/payment/src/Element/PaymentGatewayForm.php @@ -21,6 +21,8 @@ use Drupal\Core\Render\Element\RenderElement; * // On submit, the payment method will be created remotely, and the * // entity updated, for access via $form_state->getValue('payment_method') * '#default_value' => $payment_method, + * // Additional options to pass into the commerce_profile_select element. + * '#profile_select_options' => [], * ]; * @endcode * diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 2c47e8b4..36aaa680 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -3,7 +3,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; -use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase; +use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\BillingInformationPaneBase; use Drupal\commerce_payment\PaymentOption; use Drupal\commerce_payment\PaymentOptionsBuilderInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; @@ -26,7 +26,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * wrapper_element = "fieldset", * ) */ -class PaymentInformation extends CheckoutPaneBase { +class PaymentInformation extends BillingInformationPaneBase { /** * The current user. @@ -198,13 +198,14 @@ class PaymentInformation extends CheckoutPaneBase { // Store the options for submitPaneForm(). $pane_form['#payment_options'] = $options; + $profile_select_options = $this->getProfileSelectOptions(); $default_payment_gateway_id = $default_option->getPaymentGatewayId(); $payment_gateway = $payment_gateways[$default_payment_gateway_id]; if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) { - $pane_form = $this->buildPaymentMethodForm($pane_form, $form_state, $default_option); + $pane_form = $this->buildPaymentMethodForm($pane_form, $form_state, $default_option, $profile_select_options); } else { - $pane_form = $this->buildBillingProfileForm($pane_form, $form_state); + $pane_form = $this->buildBillingProfileForm($pane_form, $form_state, $profile_select_options); } return $pane_form; @@ -220,10 +221,12 @@ class PaymentInformation extends CheckoutPaneBase { * @param \Drupal\commerce_payment\PaymentOption $payment_option * The payment option. * + * @param array $profile_select_options + * * @return array * The modified pane form. */ - protected function buildPaymentMethodForm(array $pane_form, FormStateInterface $form_state, PaymentOption $payment_option) { + protected function buildPaymentMethodForm(array $pane_form, FormStateInterface $form_state, PaymentOption $payment_option, array $profile_select_options) { if ($payment_option->getPaymentMethodId() && !$payment_option->getPaymentMethodTypeId()) { // Editing payment methods at checkout is not supported. return $pane_form; @@ -241,6 +244,7 @@ class PaymentInformation extends CheckoutPaneBase { $pane_form['add_payment_method'] = [ '#type' => 'commerce_payment_gateway_form', '#operation' => 'add-payment-method', + '#profile_select_options' => $profile_select_options, '#default_value' => $payment_method, ]; @@ -252,13 +256,14 @@ class PaymentInformation extends CheckoutPaneBase { * * @param array $pane_form * The pane form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state of the parent form. + * @param FormStateInterface $form_state + * @param array $profile_select_options + * The profile select options. * * @return array * The modified pane form. */ - protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state) { + protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state, $profile_select_options) { $store = $this->order->getStore(); $pane_form['billing_information'] = [ @@ -270,7 +275,7 @@ class PaymentInformation extends CheckoutPaneBase { '#available_countries' => $store->getBillingCountries(), '#profile_type' => 'customer', '#owner_uid' => $this->order->getCustomerId(), - ]; + ] + $profile_select_options; return $pane_form; } diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 76b8cf78..4efb92e9 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -72,7 +72,7 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase { '#available_countries' => $store ? $store->getBillingCountries() : [], '#profile_type' => 'customer', '#owner_uid' => $payment_method->getOwnerId(), - ]; + ] + $form['#profile_select_options']; return $form; }