diff --git a/modules/payment/commerce_payment.module b/modules/payment/commerce_payment.module index e1db90fe3b..09c5dc0f6e 100755 --- a/modules/payment/commerce_payment.module +++ b/modules/payment/commerce_payment.module @@ -93,6 +93,8 @@ function template_preprocess_commerce_payment_method(array &$variables) { '#markup' => $payment_method->label(), ], ]; + $expires = $payment_method->getExpiresTime(); + $variables['payment_method_expires'] = $expires ? date('n/Y', $expires) : t('Never'); foreach (Element::children($variables['elements']) as $key) { $variables['payment_method'][$key] = $variables['elements'][$key]; } diff --git a/modules/payment/commerce_payment.workflows.yml b/modules/payment/commerce_payment.workflows.yml index f7d8fb42de..f9be3498ec 100644 --- a/modules/payment/commerce_payment.workflows.yml +++ b/modules/payment/commerce_payment.workflows.yml @@ -48,3 +48,48 @@ payment_default: label: 'Refund payment' from: [capture_completed, capture_partially_refunded] to: capture_refunded + +payment_manual: + id: payment_manual + group: commerce_payment + label: 'Manual' + states: + new: + label: 'New' + pending: + label: 'Pending' + completed: + label: 'Completed' + refunded: + label: 'Refunded' + partially_refunded: + label: 'Partially refunded' + expired: + label: 'Expired' + canceled: + label: 'Canceled' + transitions: + pending_payment: + label: 'Pending payment' + from: [new] + to: pending + cancel: + label: 'Cancel payment' + from: [pending] + to: canceled + expire: + label: 'Expire payment' + from: [pending] + to: expired + payment_received: + label: 'Payment received' + from: [pending] + to: completed + partially_refund: + label: 'Partially refund payment' + from: [completed] + to: partially_refunded + refund: + label: 'Refund payment' + from: [completed, partially_refunded] + to: refunded diff --git a/modules/payment/config/schema/commerce_payment.schema.yml b/modules/payment/config/schema/commerce_payment.schema.yml index 24881a5b24..d1dedb6ad3 100644 --- a/modules/payment/config/schema/commerce_payment.schema.yml +++ b/modules/payment/config/schema/commerce_payment.schema.yml @@ -20,6 +20,20 @@ commerce_payment.commerce_payment_gateway.*: commerce_payment.commerce_payment_gateway.plugin.*: type: commerce_payment_gateway_configuration +commerce_payment.commerce_payment_gateway.plugin.manual: + type: commerce_payment_gateway_configuration + mapping: + reusable: + type: boolean + label: 'Reusable' + expires: + type: string + label: 'Expires' + instructions: + type: text_format + label: 'Instructions' + translatable: true + commerce_payment_gateway_configuration: type: mapping mapping: diff --git a/modules/payment/src/PaymentMethodStorage.php b/modules/payment/src/PaymentMethodStorage.php index 76b6fc4c72..ec300d5eb9 100644 --- a/modules/payment/src/PaymentMethodStorage.php +++ b/modules/payment/src/PaymentMethodStorage.php @@ -82,8 +82,11 @@ public function loadReusable(UserInterface $account, PaymentGatewayInterface $pa $query = $this->getQuery() ->condition('uid', $account->id()) ->condition('payment_gateway', $payment_gateway->id()) - ->condition('reusable', TRUE) + ->condition('reusable', TRUE); + $group = $query->orConditionGroup() ->condition('expires', $this->time->getRequestTime(), '>') + ->condition('expires', 0); + $query->condition($group) ->sort('created', 'DESC'); $result = $query->execute(); if (empty($result)) { diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index e0121a344b..bdc344bd4d 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -228,11 +228,11 @@ protected function buildPaymentMethodOptions(array $payment_gateways) { $payment_method_types = $payment_gateway_plugin->getPaymentMethodTypes(); foreach ($payment_method_types as $payment_method_type_id => $payment_method_type) { $option_id = 'new--' . $payment_method_type_id . '--' . $payment_gateway->id(); - $option_label = $payment_method_type->getCreateLabel(); + $option_label = $payment_method_type->getCreateLabel($payment_gateway); if ($payment_method_type_counts[$payment_method_type_id] > 1) { // Append the payment gateway label to avoid duplicate labels. $option_label = $this->t('@payment_method_label (@payment_gateway_label)', [ - '@payment_method_label' => $payment_method_type->getCreateLabel(), + '@payment_method_label' => $payment_method_type->getCreateLabel($payment_gateway), '@payment_gateway_label' => $payment_gateway_plugin->getDisplayLabel(), ]); } diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php new file mode 100644 index 0000000000..984c07d03b --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInstructions.php @@ -0,0 +1,44 @@ +order->get('payment_gateway')->isEmpty()) { + return []; + } + + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $this->order->payment_gateway->entity; + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayInterface $payment_gateway_plugin */ + $payment_gateway_plugin = $payment_gateway->getPlugin(); + + if ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface && $payment_gateway_plugin->getPaymentInstructions()) { + $pane_form += $payment_gateway_plugin->getPaymentInstructions(); + return $pane_form; + } + + return []; + } + +} diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php index b9d3bc0098..af9ee4e05d 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php @@ -6,6 +6,7 @@ use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_payment\Exception\DeclineException; use Drupal\commerce_payment\Exception\PaymentGatewayException; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface; use Drupal\Core\Form\FormStateInterface; @@ -101,6 +102,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, $payment_gateway_plugin = $payment_gateway->getPlugin(); $payment_storage = $this->entityTypeManager->getStorage('commerce_payment'); + /** @var \Drupal\commerce_payment\Entity\Payment $payment */ $payment = $payment_storage->create([ 'state' => 'new', 'amount' => $this->order->getTotalPrice(), @@ -143,6 +145,24 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, return $pane_form; } + elseif ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) { + try { + $payment->payment_method = $this->order->payment_method->entity; + $payment_gateway_plugin->createPayment($payment); + $this->checkoutFlow->redirectToStep($next_step_id); + } + catch (DeclineException $e) { + $message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.'); + drupal_set_message($message, 'error'); + $this->redirectToPreviousStep(); + } + catch (PaymentGatewayException $e) { + \Drupal::logger('commerce_payment')->error($e->getMessage()); + $message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.'); + drupal_set_message($message, 'error'); + $this->redirectToPreviousStep(); + } + } else { $this->checkoutFlow->redirectToStep($next_step_id); } diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php new file mode 100644 index 0000000000..439aceeca7 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/HasPaymentInstructionsInterface.php @@ -0,0 +1,19 @@ + FALSE, + 'expires' => '', + 'instructions' => [ + 'value' => '', + 'format' => 'plain_text', + ], + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + $form['manual'] = [ + '#type' => 'fieldset', + '#title' => t('Manual payment settings'), + ]; + $form['manual']['reusable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Reusable'), + '#description' => $this->t('Check if you want to have reusable payment methods for this gateway.'), + '#default_value' => $this->configuration['reusable'], + ]; + $form['manual']['expires'] = [ + '#type' => 'textfield', + '#title' => $this->t('Expires'), + '#description' => $this->t('An offset from the current time such as "@example1", "@example2 or "@example3". Leave empty for never expires.', ['@example1' => '1 year', '@example2' => '3 months', '@example3' => '60 days']), + '#default_value' => $this->configuration['expires'], + '#size' => 10, + ]; + $form['manual']['instructions'] = [ + '#type' => 'text_format', + '#title' => $this->t('Instructions'), + '#description' => $this->t('Manual payment instructions to be displayed to customer on checkout.'), + '#default_value' => $this->configuration['instructions']['value'], + '#format' => $this->configuration['instructions']['format'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + + if (!$form_state->getErrors()) { + $form_parents = $form['#parents']; + $form_parents[] = 'manual'; + $values = $form_state->getValue($form_parents); + if (!empty($values['expires'])) { + $convert = strtotime($values['expires']); + if ($convert == -1 || $convert === FALSE) { + $form_state->setError($form['manual']['expires'], $this->t('Invalid offset time format.')); + } + if ($convert < \Drupal::time()->getRequestTime()) { + $form_state->setError($form['manual']['expires'], $this->t('Future offset time is needed for Expires.')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + if (!$form_state->getErrors()) { + $form_parents = $form['#parents']; + $form_parents[] = 'manual'; + $values = $form_state->getValue($form_parents); + $this->configuration['instructions'] = $values['instructions']; + $this->configuration['reusable'] = $values['reusable']; + $this->configuration['expires'] = $values['expires']; + } + } + + /** + * {@inheritdoc} + */ + public function createPayment(PaymentInterface $payment, $capture = TRUE) { + if ($payment->getState()->value != 'new') { + throw new \InvalidArgumentException('The provided payment is in an invalid state.'); + } + $payment_method = $payment->getPaymentMethod(); + if (empty($payment_method)) { + throw new \InvalidArgumentException('The provided payment has no payment method referenced.'); + } + if ($payment_method->isExpired()) { + throw new HardDeclineException('The provided payment method has expired'); + } + + $test = $this->getMode() == 'test'; + $payment->setTest($test); + $payment->state = 'pending'; + $payment->setAuthorizedTime(\Drupal::time()->getRequestTime()); + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function completePayment(PaymentInterface $payment, Price $amount = NULL) { + if ($payment->getState()->value != 'pending') { + throw new \InvalidArgumentException('Only payments in the "authorization" state can be captured.'); + } + + // If not specified, capture the entire amount. + $amount = $amount ?: $payment->getAmount(); + + $payment->state = 'completed'; + $payment->setAmount($amount); + $payment->setCapturedTime(\Drupal::time()->getRequestTime()); + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function cancelPayment(PaymentInterface $payment) { + if ($payment->getState()->value != 'pending') { + throw new \InvalidArgumentException('Only payments in the "authorization" state can be voided.'); + } + + $payment->state = 'canceled'; + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function refundPayment(PaymentInterface $payment, Price $amount = NULL) { + if (!in_array($payment->getState()->value, ['completed', 'partially_refunded'])) { + throw new \InvalidArgumentException('Only payments in the "completed" and "partially_refunded" states can be refunded.'); + } + // If not specified, refund the entire amount. + $amount = $amount ?: $payment->getAmount(); + + // Validate the requested amount. + $balance = $payment->getBalance(); + if ($amount->greaterThan($balance)) { + throw new InvalidRequestException(sprintf("Can't refund more than %s.", $balance->__toString())); + } + + $old_refunded_amount = $payment->getRefundedAmount(); + $new_refunded_amount = $old_refunded_amount->add($amount); + if ($new_refunded_amount->lessThan($payment->getAmount())) { + $payment->state = 'partially_refunded'; + } + else { + $payment->state = 'refunded'; + } + + $payment->setRefundedAmount($new_refunded_amount); + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) { + // No expected keys required for Manual payments. + // Set expires according with configuration. + $expires = $this->configuration['expires'] ? strtotime($this->configuration['expires']) : 0; + // The remote ID returned by the request. + $remote_id = $payment_method->getOwnerId(); + + $payment_method->setRemoteId($remote_id); + $payment_method->setReusable($this->configuration['reusable']); + $payment_method->setExpiresTime($expires); + $payment_method->save(); + } + + /** + * {@inheritdoc} + */ + public function deletePaymentMethod(PaymentMethodInterface $payment_method) { + // Delete the local entity. + $payment_method->delete(); + } + + /** + * {@inheritdoc} + */ + public function getPaymentInstructions() { + $element = NULL; + $instructions = $this->configuration['instructions']; + if (!empty($instructions['value'])) { + $element['instructions'] = [ + '#markup' => check_markup($instructions['value'], $instructions['format']), + ]; + } + + return $element; + } + +} diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php new file mode 100644 index 0000000000..f8a21cab00 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/ManualPaymentGatewayBase.php @@ -0,0 +1,10 @@ +getLabel(); }, $this->paymentMethodTypes); - $form['mode'] = [ - '#type' => 'radios', - '#title' => $this->t('Mode'), - '#options' => $modes, - '#default_value' => $this->configuration['mode'], - '#required' => TRUE, - '#access' => !empty($modes), - ]; + if (count($modes) > 1) { + // Ajax sometimes mixes up with modes. + if (!in_array($this->configuration['mode'], array_keys($modes))) { + $this->configuration = $this->defaultConfiguration(); + } + $form['mode'] = [ + '#type' => 'radios', + '#title' => $this->t('Mode'), + '#options' => $modes, + '#default_value' => $this->configuration['mode'], + '#required' => TRUE, + '#access' => !empty($modes), + ]; + } + else { + $form['mode'] = [ + '#type' => 'value', + '#value' => $this->configuration['mode'], + ]; + } + if (count($payment_method_types) > 1) { $form['payment_method_types'] = [ '#type' => 'checkboxes', @@ -277,7 +295,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - if (!$form_state->getErrors()) { + if (!$form_state->getErrors() && $form_state->isSubmitted()) { $values = $form_state->getValue($form['#parents']); $values['payment_method_types'] = array_filter($values['payment_method_types']); @@ -306,8 +324,23 @@ public function buildPaymentOperations(PaymentInterface $payment) { 'access' => $access, ]; } + if ($this instanceof SupportsManualWorkflowInterface) { + $access = $payment->getState()->value == 'pending'; + $operations['complete'] = [ + 'title' => $this->t('Complete'), + 'page_title' => $this->t('Complete payment'), + 'plugin_form' => 'complete-payment', + 'access' => $access, + ]; + $operations['cancel'] = [ + 'title' => $this->t('Cancel payment'), + 'page_title' => $this->t('Cancel payment'), + 'plugin_form' => 'cancel-payment', + 'access' => $access, + ]; + } if ($this instanceof SupportsRefundsInterface) { - $access = in_array($payment->getState()->value, ['capture_completed', 'capture_partially_refunded']); + $access = in_array($payment->getState()->value, ['capture_completed', 'capture_partially_refunded', 'completed']); $operations['refund'] = [ 'title' => $this->t('Refund'), 'page_title' => $this->t('Refund payment'), diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php new file mode 100644 index 0000000000..43553518a7 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsManualWorkflowInterface.php @@ -0,0 +1,41 @@ +getPaymentGateway(); + // Use billing profile address data to identify the payment method. + if ($billing_profile = $payment_method->getBillingProfile()) { + /** @var \Drupal\address\AddressInterface $address */ + $address = $billing_profile->address->first(); + $name = $address->getGivenName() . ' ' . $address->getFamilyName(); + $location = $address->getAddressLine1() . ', ' . $address->getLocality(); + $args = [ + '@gateway_title' => $payment_gateway->label(), + '@name' => $name, + '@location' => $location, + ]; + $label = $this->t('@gateway_title for @name (@location)', $args); + } + else { + $args = [ + '@gateway_title' => $payment_gateway->label(), + ]; + $label = $this->t('Manual - @gateway_title', $args); + } + + return $label; + } + + /** + * {@inheritdoc} + */ + public function getCreateLabel(PaymentGatewayInterface $payment_gateway) { + return $payment_gateway->label(); + } + + /** + * {@inheritdoc} + */ + public function buildFieldDefinitions() { + // Probably the fields for the Offline payments should be done in the UI. + return []; + } + +} diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeBase.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeBase.php index 140864ae2e..7d8576fc55 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeBase.php +++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeBase.php @@ -2,6 +2,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType; +use Drupal\commerce_payment\Entity\PaymentGatewayInterface; use Drupal\Core\Plugin\PluginBase; /** @@ -19,7 +20,7 @@ public function getLabel() { /** * {@inheritdoc} */ - public function getCreateLabel() { + public function getCreateLabel(PaymentGatewayInterface $payment_gateway) { return $this->pluginDefinition['create_label']; } diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php index 3ffc5adc98..416ad1df54 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php +++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php @@ -3,6 +3,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType; use Drupal\commerce\BundlePluginInterface; +use Drupal\commerce_payment\Entity\PaymentGatewayInterface; use Drupal\commerce_payment\Entity\PaymentMethodInterface; /** @@ -21,10 +22,13 @@ public function getLabel(); /** * Gets the payment method type create label. * + * @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway + * The payment gateway. + * * @return string * The payment method type create label. */ - public function getCreateLabel(); + public function getCreateLabel(PaymentGatewayInterface $payment_gateway); /** * Builds a label for the given payment method. diff --git a/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php new file mode 100644 index 0000000000..ee4d28caf7 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentManual.php @@ -0,0 +1,23 @@ +entity; + + $form['#theme'] = 'confirm_form'; + $form['#attributes']['class'][] = 'confirmation'; + $form['#page_title'] = t('Are you sure you want to cancel the %label payment?', [ + '%label' => $payment->label(), + ]); + $form['#success_message'] = t('Payment canceled.'); + $form['description'] = [ + '#markup' => t('This action cannot be undone.'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ + $payment = $this->entity; + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsManualWorkflowInterface $payment_gateway_plugin */ + $payment_gateway_plugin = $this->plugin; + $payment_gateway_plugin->cancelPayment($payment); + } + +} diff --git a/modules/payment/src/PluginForm/PaymentCompleteForm.php b/modules/payment/src/PluginForm/PaymentCompleteForm.php new file mode 100644 index 0000000000..efd848b5f3 --- /dev/null +++ b/modules/payment/src/PluginForm/PaymentCompleteForm.php @@ -0,0 +1,23 @@ +getValue($form['#parents']); + $amount = new Price($values['amount']['number'], $values['amount']['currency_code']); + /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ + $payment = $this->entity; + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsManualWorkflowInterface $payment_gateway_plugin */ + $payment_gateway_plugin = $this->plugin; + $payment_gateway_plugin->completePayment($payment, $amount); + } + +} diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 0a3ece9eba..64dd9b220e 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -51,6 +51,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta elseif ($payment_method->bundle() == 'paypal') { $form['payment_details'] = $this->buildPayPalForm($form['payment_details'], $form_state); } + elseif ($payment_method->bundle() == 'manual') { + $form['payment_details'] = $this->buildManualForm($form['payment_details'], $form_state); + } /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; @@ -251,6 +254,30 @@ protected function submitCreditCardForm(array $element, FormStateInterface $form $this->entity->card_exp_year = $values['expiration']['year']; } + /** + * Builds the Manual form. + * + * Empty by default because there is no generic Manual form, it's always + * payment gateway specific. + * + * @param array $element + * The target element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the complete form. + * + * @return array + * The built manual form. + */ + protected function buildManualForm(array $element, FormStateInterface $form_state) { + // Placeholder for the PayPal mail. + $element['manual'] = [ + '#type' => 'hidden', + '#value' => '', + ]; + + return $element; + } + /** * Builds the PayPal form. * diff --git a/modules/payment/templates/commerce-payment-method.html.twig b/modules/payment/templates/commerce-payment-method.html.twig index 43283beba6..d165b49454 100644 --- a/modules/payment/templates/commerce-payment-method.html.twig +++ b/modules/payment/templates/commerce-payment-method.html.twig @@ -24,7 +24,7 @@ {{ payment_method.label }}