From f9875d38613f7a69c0db90b934e299b9c017462b Mon Sep 17 00:00:00 2001 From: Matt Glaman <nmd.matt@gmail.com> Date: Sat, 3 Dec 2016 11:23:40 -0600 Subject: [PATCH] Issue #2785591: Initial implementation of an off-site API. --- .../CheckoutFlow/CheckoutFlowBase.php | 23 ++- .../CheckoutFlow/CheckoutFlowInterface.php | 10 + .../CheckoutFlow/MultistepDefault.php | 1 + .../payment/commerce_payment.libraries.yml | 9 + modules/payment/commerce_payment.routing.yml | 23 +++ modules/payment/js/offiste-redirect.js | 23 +++ .../Controller/PaymentCheckoutController.php | 53 ++++++ .../CheckoutPane/PaymentInformation.php | 126 ++++++++++--- .../Commerce/CheckoutPane/PaymentProcess.php | 60 +++--- .../OffsitePaymentGatewayBase.php | 33 ++++ .../OffsitePaymentGatewayInterface.php | 50 +++++ ...OffsiteRedirectPaymentGatewayInterface.php | 22 +++ .../PaymentGateway/PaymentGatewayBase.php | 3 + .../src/PluginForm/OffsitePaymentForm.php | 43 +++++ .../PaymentCheckoutOffsiteRedirectTest.php | 172 ++++++++++++++++++ .../commerce_payment_example.routing.yml | 16 ++ .../commerce_payment_example.schema.yml | 6 + .../Controller/DummyRedirectController.php | 75 ++++++++ .../PaymentGateway/OffsiteRedirectExample.php | 107 +++++++++++ .../OffsiteRedirectExampleInterface.php | 9 + .../Offsite/OffsiteRedirectPaymentForm.php | 51 ++++++ 21 files changed, 858 insertions(+), 57 deletions(-) create mode 100644 modules/payment/js/offiste-redirect.js create mode 100644 modules/payment/src/Controller/PaymentCheckoutController.php create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayBase.php create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayInterface.php create mode 100644 modules/payment/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectPaymentGatewayInterface.php create mode 100644 modules/payment/src/PluginForm/OffsitePaymentForm.php create mode 100644 modules/payment/tests/src/Functional/PaymentCheckoutOffsiteRedirectTest.php create mode 100644 modules/payment_example/commerce_payment_example.routing.yml create mode 100644 modules/payment_example/src/Controller/DummyRedirectController.php create mode 100644 modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExample.php create mode 100644 modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExampleInterface.php create mode 100644 modules/payment_example/src/PluginForm/Offsite/OffsiteRedirectPaymentForm.php diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php index 2e66e9b71..ab651dc94 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php @@ -2,12 +2,14 @@ namespace Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow; +use Drupal\commerce\Response\NeedsRedirectException; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -147,6 +149,22 @@ abstract class CheckoutFlowBase extends PluginBase implements CheckoutFlowInterf return isset($step_ids[$current_index + 1]) ? $step_ids[$current_index + 1] : NULL; } + /** + * {@inheritdoc} + */ + public function redirectToStep($step_id) { + $this->order->checkout_step = $step_id; + if ($step_id == 'complete') { + $transition = $this->order->getState()->getWorkflow()->getTransition('place'); + $this->order->getState()->applyTransition($transition); + } + $this->order->save(); + throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [ + 'commerce_order' => $this->order->id(), + 'step' => $step_id, + ])->toString()); + } + /** * {@inheritdoc} */ @@ -373,11 +391,6 @@ abstract class CheckoutFlowBase extends PluginBase implements CheckoutFlowInterf } // Hide the actions element if it has no buttons. $actions['#access'] = isset($actions['previous']) || isset($actions['next']); - // Once these two steps are reached, the user can't go back. - if (in_array($this->stepId, ['offsite_payment', 'complete'])) { - $actions['#access'] = FALSE; - } - return $actions; } diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowInterface.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowInterface.php index 9986afc23..127fd4009 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowInterface.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowInterface.php @@ -56,6 +56,16 @@ interface CheckoutFlowInterface extends FormInterface, ConfigurablePluginInterfa */ public function getNextStepId(); + /** + * Redirects an order to a specific step in the checkout. + * + * @param string $step_id + * The step ID to redirect to. + * + * @throws \Drupal\commerce\Response\NeedsRedirectException + */ + public function redirectToStep($step_id); + /** * Gets the defined steps. * diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/MultistepDefault.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/MultistepDefault.php index c063190d9..6d5258245 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/MultistepDefault.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/MultistepDefault.php @@ -29,6 +29,7 @@ class MultistepDefault extends CheckoutFlowWithPanesBase { 'review' => [ 'label' => $this->t('Review'), 'next_label' => $this->t('Continue to review'), + 'previous_label' => $this->t('Cancel payment'), 'has_order_summary' => TRUE, ], ] + parent::getSteps(); diff --git a/modules/payment/commerce_payment.libraries.yml b/modules/payment/commerce_payment.libraries.yml index 37225adb1..ed2a64176 100644 --- a/modules/payment/commerce_payment.libraries.yml +++ b/modules/payment/commerce_payment.libraries.yml @@ -9,3 +9,12 @@ payment_method_icons: css: theme: css/commerce_payment.payment_method_icons.css: {} + +offsite_redirect: + version: VERSION + js: + js/offiste-redirect.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings diff --git a/modules/payment/commerce_payment.routing.yml b/modules/payment/commerce_payment.routing.yml index ed5156d0d..04f8c0e5c 100644 --- a/modules/payment/commerce_payment.routing.yml +++ b/modules/payment/commerce_payment.routing.yml @@ -62,3 +62,26 @@ entity.commerce_payment_method.collection: parameters: user: type: entity:user + +commerce_payment.checkout.cancel: + path: '/checkout/{commerce_order}/{step}/cancel' + defaults: + _controller: '\Drupal\commerce_payment\Controller\PaymentCheckoutController::cancelPaymentPage' + requirements: + _custom_access: '\Drupal\commerce_checkout\Controller\CheckoutController::checkAccess' + _module_dependencies: commerce_checkout + options: + parameters: + commerce_order: + type: entity:commerce_order +commerce_payment.checkout.return: + path: '/checkout/{commerce_order}/{step}/return' + defaults: + _controller: '\Drupal\commerce_payment\Controller\PaymentCheckoutController::returnPaymentPage' + requirements: + _custom_access: '\Drupal\commerce_checkout\Controller\CheckoutController::checkAccess' + _module_dependencies: commerce_checkout + options: + parameters: + commerce_order: + type: entity:commerce_order diff --git a/modules/payment/js/offiste-redirect.js b/modules/payment/js/offiste-redirect.js new file mode 100644 index 000000000..8b3b1b150 --- /dev/null +++ b/modules/payment/js/offiste-redirect.js @@ -0,0 +1,23 @@ +/** + * @file + * Defines behaviors for the payment redirect form. + */ +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the commercePaymentRedirect behavior. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the commercePaymentRedirect behavior. + */ + Drupal.behaviors.commercePaymentRedirect = { + attach: function (context) { + $('.payment-redirect-form', context).find('input[type="submit"]').trigger('click'); + } + }; + +})(jQuery, Drupal, drupalSettings); diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php new file mode 100644 index 000000000..19af26549 --- /dev/null +++ b/modules/payment/src/Controller/PaymentCheckoutController.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\commerce_payment\Controller; + +use Drupal\commerce_order\Entity\OrderInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; +use Drupal\Core\Access\AccessException; + +class PaymentCheckoutController { + + /** + * Controller callback for offsite payments cancelled. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $commerce_order + * The order. + */ + public function cancelPaymentPage(OrderInterface $commerce_order) { + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $commerce_order->payment_gateway->entity; + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class); + } + + $payment_gateway_plugin->onRedirectCancel($commerce_order); + /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */ + $checkout_flow = $commerce_order->checkout_flow->entity; + $checkout_flow_plugin = $checkout_flow->getPlugin(); + $checkout_flow_plugin->redirectToStep($checkout_flow_plugin->getPreviousStepId()); + } + + /** + * Controller callback for offsite payments returned. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $commerce_order + * The order. + */ + public function returnPaymentPage(OrderInterface $commerce_order) { + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $commerce_order->payment_gateway->entity; + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class); + } + + $payment_gateway_plugin->onRedirectReturn($commerce_order); + /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */ + $checkout_flow = $commerce_order->checkout_flow->entity; + $checkout_flow_plugin = $checkout_flow->getPlugin(); + $checkout_flow_plugin->redirectToStep($checkout_flow_plugin->getNextStepId()); + } + +} diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index d7bdbdbd9..abb7c6a7d 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -4,13 +4,16 @@ 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_payment\Entity\PaymentGatewayInterface as EntityPaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\RendererInterface; +use Drupal\profile\Entity\Profile; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -80,10 +83,11 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu * {@inheritdoc} */ public function buildPaneSummary() { + $summary = ''; /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ $payment_gateway = $this->order->payment_gateway->entity; if (!$payment_gateway) { - return ''; + return $summary; } $payment_gateway_plugin = $payment_gateway->getPlugin(); @@ -95,10 +99,12 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu } else { $billing_profile = $this->order->getBillingProfile(); - $profile_view_builder = $this->entityTypeManager->getViewBuilder('profile'); - $profile_view = $profile_view_builder->view($billing_profile, 'default'); - $summary = $payment_gateway->getPlugin()->getDisplayLabel(); - $summary .= $this->renderer->render($profile_view); + if ($billing_profile) { + $profile_view_builder = $this->entityTypeManager->getViewBuilder('profile'); + $profile_view = $profile_view_builder->view($billing_profile, 'default'); + $summary = $payment_gateway->getPlugin()->getDisplayLabel(); + $summary .= $this->renderer->render($profile_view); + } } return $summary; @@ -110,8 +116,6 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); - /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ - $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $payment_gateways */ $payment_gateways = $payment_gateway_storage->loadMultipleForOrder($this->order); // When no payment gateways are defined, throw an error and fail reliably. @@ -122,8 +126,57 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu // @todo Support multiple gateways. /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ $payment_gateway = reset($payment_gateways); + + $pane_form['payment_gateway'] = [ + '#type' => 'value', + '#value' => $payment_gateway->id(), + ]; + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface) { + $this->attachPaymentMethodForm($payment_gateway, $pane_form, $form_state); + } + else { + /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ + $billing_profile = $this->order->getBillingProfile(); + if (!$billing_profile) { + $billing_profile = Profile::create([ + 'uid' => $this->order->getCustomerId(), + 'type' => 'customer', + ]); + } + + $form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default'); + $form_display->buildForm($billing_profile, $pane_form, $form_state); + // Remove the details wrapper from the address field. + if (!empty($pane_form['address']['widget'][0])) { + $pane_form['address']['widget'][0]['#type'] = 'container'; + } + // Store the billing profile for the validate/submit methods. + $pane_form['#entity'] = $billing_profile; + } + + return $pane_form; + } + + /** + * Creates the payment method selection form for supported gateways. + * + * @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway + * The payment gateway. + * @param array $pane_form + * The pane form, containing the following basic properties: + * - #parents: Identifies the position of the pane form in the overall + * parent form, and identifies the location where the field values are + * placed within $form_state->getValues(). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + */ + protected function attachPaymentMethodForm(EntityPaymentGatewayInterface $payment_gateway, array &$pane_form, FormStateInterface $form_state) { + /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + $payment_gateway_plugin = $payment_gateway->getPlugin(); $options = []; $default_option = NULL; $customer = $this->order->getCustomer(); @@ -174,8 +227,6 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu '#default_value' => $payment_method, ]; } - - return $pane_form; } /** @@ -192,8 +243,23 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu */ public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { $values = $form_state->getValue($pane_form['#parents']); - if (!isset($values['payment_method'])) { - $form_state->setError($complete_form, $this->noPaymentGatewayErrorMessage()); + + /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ + $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $payment_gateway_storage->load($values['payment_gateway']); + + if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) { + if (!isset($values['payment_method'])) { + $form_state->setError($complete_form, $this->noPaymentGatewayErrorMessage()); + } + } + else { + /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ + $billing_profile = $pane_form['#entity']; + $form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default'); + $form_display->extractFormValues($billing_profile, $pane_form, $form_state); + $form_display->validateFormValues($billing_profile, $pane_form, $form_state); } } @@ -202,19 +268,37 @@ class PaymentInformation extends CheckoutPaneBase implements ContainerFactoryPlu */ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { $values = $form_state->getValue($pane_form['#parents']); - if (is_numeric($values['payment_method'])) { - /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ - $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); - $payment_method = $payment_method_storage->load($values['payment_method']); + + /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ + $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $payment_gateway_storage->load($values['payment_gateway']); + + if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) { + if (is_numeric($values['payment_method'])) { + /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + $payment_method = $payment_method_storage->load($values['payment_method']); + } + else { + $payment_method = $values['add_payment_method']; + } + + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $this->order->payment_gateway = $payment_method->getPaymentGateway(); + $this->order->payment_method = $payment_method; + $this->order->setBillingProfile($payment_method->getBillingProfile()); } else { - $payment_method = $values['add_payment_method']; + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $this->order->payment_gateway = $payment_gateway; + /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ + $billing_profile = $pane_form['#entity']; + $form_display = EntityFormDisplay::collectRenderDisplay($billing_profile, 'default'); + $form_display->extractFormValues($billing_profile, $pane_form, $form_state); + $billing_profile->save(); + $this->order->setBillingProfile($billing_profile); } - - /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ - $this->order->payment_gateway = $payment_method->getPaymentGateway(); - $this->order->payment_method = $payment_method; - $this->order->setBillingProfile($payment_method->getBillingProfile()); } /** diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php index 783b95a3b..de0067553 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php @@ -2,15 +2,15 @@ namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane; -use Drupal\commerce\Response\NeedsRedirectException; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase; use Drupal\commerce_payment\Exception\PaymentGatewayException; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsiteRedirectPaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -133,31 +133,19 @@ class PaymentProcess extends CheckoutPaneBase implements ContainerFactoryPluginI $payment_gateway = $this->order->payment_gateway->entity; $payment_gateway_plugin = $payment_gateway->getPlugin(); + $payment_storage = $this->entityTypeManager->getStorage('commerce_payment'); + $payment = $payment_storage->create([ + 'state' => 'new', + 'amount' => $this->order->getTotalPrice(), + 'payment_gateway' => $payment_gateway->id(), + 'order_id' => $this->order->id(), + ]); + if ($payment_gateway_plugin instanceof OnsitePaymentGatewayInterface) { try { - $payment_storage = $this->entityTypeManager->getStorage('commerce_payment'); - $payment = $payment_storage->create([ - 'state' => 'new', - 'amount' => $this->order->getTotalPrice(), - 'payment_gateway' => $payment_gateway->id(), - 'payment_method' => $this->order->payment_method->entity, - 'order_id' => $this->order->id(), - ]); + $payment->payment_method = $this->order->payment_method->entity; $payment_gateway_plugin->createPayment($payment, $this->configuration['capture']); - - $next_step_id = $this->checkoutFlow->getNextStepId(); - // @todo Add a checkout flow method for completing checkout. - if ($next_step_id == 'complete') { - $transition = $this->order->getState()->getWorkflow()->getTransition('place'); - $this->order->getState()->applyTransition($transition); - } - $this->order->checkout_step = $next_step_id; - $this->order->save(); - - throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [ - 'commerce_order' => $this->order->id(), - 'step' => $next_step_id, - ])->toString()); + $this->checkoutFlow->redirectToStep($this->checkoutFlow->getNextStepId()); } catch (PaymentGatewayException $e) { @@ -165,6 +153,22 @@ class PaymentProcess extends CheckoutPaneBase implements ContainerFactoryPluginI $this->redirectToPreviousStep(); } } + elseif ($payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { + $pane_form['offsite_payment'] = [ + '#type' => 'commerce_payment_gateway_form', + '#operation' => 'offsite-payment', + '#default_value' => $payment, + ]; + + if ($payment_gateway_plugin instanceof OffsiteRedirectPaymentGatewayInterface) { + $redirect_url = $payment_gateway_plugin->getRedirectUrl(); + // Make sure the redirect URL is the root form's action. + $complete_form['#action'] = $redirect_url; + $complete_form['#attributes']['class'][] = 'payment-redirect-form'; + } + + return $pane_form; + } else { drupal_set_message($this->t('Sorry, we can currently only support on site payment gateways.'), 'error'); $this->redirectToPreviousStep(); @@ -183,13 +187,7 @@ class PaymentProcess extends CheckoutPaneBase implements ContainerFactoryPluginI $previous_step_id = $pane->getStepId(); } } - - $this->order->checkout_step = $previous_step_id; - $this->order->save(); - throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [ - 'commerce_order' => $this->order->id(), - 'step' => $previous_step_id, - ])->toString()); + $this->checkoutFlow->redirectToStep($previous_step_id); } } diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayBase.php new file mode 100644 index 000000000..da3ae1af3 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayBase.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway; + +use Drupal\commerce_order\Entity\OrderInterface; +use Drupal\Core\Url; + +/** + * Provides the base class for off-site payment gateways. + */ +abstract class OffsitePaymentGatewayBase extends PaymentGatewayBase implements OffsitePaymentGatewayInterface { + + /** + * {@inheritdoc} + */ + public function getRedirectCancelUrl(OrderInterface $order) { + return Url::fromRoute('commerce_payment.checkout.cancel', [ + 'commerce_order' => $order->id(), + 'step' => 'payment', + ], ['absolute' => TRUE]); + } + + /** + * {@inheritdoc} + */ + public function getRedirectReturnUrl(OrderInterface $order) { + return Url::fromRoute('commerce_payment.checkout.return', [ + 'commerce_order' => $order->id(), + 'step' => 'payment', + ], ['absolute' => TRUE]); + } + +} diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayInterface.php new file mode 100644 index 000000000..d00e9c3f8 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsitePaymentGatewayInterface.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway; + +use Drupal\commerce_order\Entity\OrderInterface; + +/** + * Defines the base interface for off-site payment gateways. + */ +interface OffsitePaymentGatewayInterface extends PaymentGatewayInterface { + + /** + * Gets the URL for offiste redirect cancel. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $order + * The order. + * + * @return \Drupal\Core\Url + * The Url object + */ + public function getRedirectCancelUrl(OrderInterface $order); + + /** + * Gets the URL for offiste redirect return. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $order + * The order. + * + * @return \Drupal\Core\Url + * The Url object + */ + public function getRedirectReturnUrl(OrderInterface $order); + + /** + * Invoked when the off-site payment return. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $order + * The order. + */ + public function onRedirectReturn(OrderInterface $order); + + /** + * Invoked when the off-site payment way cancelled or failed. + * + * @param \Drupal\commerce_order\Entity\OrderInterface $order + * The order. + */ + public function onRedirectCancel(OrderInterface $order); + +} diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectPaymentGatewayInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectPaymentGatewayInterface.php new file mode 100644 index 000000000..7bc3a6488 --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectPaymentGatewayInterface.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway; + +/** + * Defines the base interface for off-site payment gateways. + */ +interface OffsiteRedirectPaymentGatewayInterface extends OffsitePaymentGatewayInterface { + + /** + * Gets the off-site redirect URL. + * + * If this is TRUE, a form wrapper and JavaScript snippet will be added that + * submits the off-site payment form, causing a redirect to the payment + * gateway's payment page. + * + * @return bool + * Whether to automatically redirect or not. + */ + public function getRedirectUrl(); + +} diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php index 5f8343747..db5920768 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php @@ -107,6 +107,9 @@ abstract class PaymentGatewayBase extends PluginBase implements PaymentGatewayIn if ($this instanceof SupportsRefundsInterface) { $default_forms['refund-payment'] = 'Drupal\commerce_payment\PluginForm\PaymentRefundForm'; } + if ($this instanceof OffsitePaymentGatewayInterface) { + $default_forms['offsite-payment'] = 'Drupal\commerce_payment\PluginForm\OffsitePaymentForm'; + } return $default_forms; } diff --git a/modules/payment/src/PluginForm/OffsitePaymentForm.php b/modules/payment/src/PluginForm/OffsitePaymentForm.php new file mode 100644 index 000000000..d61fc07cd --- /dev/null +++ b/modules/payment/src/PluginForm/OffsitePaymentForm.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\commerce_payment\PluginForm; + +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsiteRedirectPaymentGatewayInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +class OffsitePaymentForm extends PaymentGatewayFormBase { + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ + $payment = $this->entity; + $payment_gateway_plugin = $payment->getPaymentGateway()->getPlugin(); + + if ($payment_gateway_plugin instanceof OffsiteRedirectPaymentGatewayInterface) { + $form['#attached']['library'][] = 'commerce_payment/offsite_redirect'; + $form['help'] = [ + '#markup' => '<div class="checkout-help">' . t('Please wait while you are redirected to the payment server. If nothing happens within 10 seconds, please click on the button below.') . '</div>', + '#weight' => -10, + ]; + } + + // Manually set parents so all of the off-site redirect inputs are on + // the root of the form. + foreach (Element::children($form) as $child) { + $form[$child]['#parents'] = [$child]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Nothing. Off-site payment gateways do not submit forms to Drupal. + } + +} diff --git a/modules/payment/tests/src/Functional/PaymentCheckoutOffsiteRedirectTest.php b/modules/payment/tests/src/Functional/PaymentCheckoutOffsiteRedirectTest.php new file mode 100644 index 000000000..0d732da75 --- /dev/null +++ b/modules/payment/tests/src/Functional/PaymentCheckoutOffsiteRedirectTest.php @@ -0,0 +1,172 @@ +<?php + +namespace Drupal\Tests\commerce_payment\Functional; + +use Drupal\commerce_order\Entity\Order; +use Drupal\commerce_payment\Entity\Payment; +use Drupal\commerce_payment\Entity\PaymentGateway; +use Drupal\commerce_store\StoreCreationTrait; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase; + +/** + * Tests the integration between payments and checkout. + * + * @group commerce + */ +class PaymentCheckoutOffsiteRedirectTest extends CommerceBrowserTestBase { + + use StoreCreationTrait; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * The product. + * + * @var \Drupal\commerce_product\Entity\ProductInterface + */ + protected $product; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'commerce_product', + 'commerce_cart', + 'commerce_checkout', + 'commerce_payment', + 'commerce_payment_example', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $store = $this->createStore('Demo', 'demo@example.com', 'default', TRUE); + + $variation = $this->createEntity('commerce_product_variation', [ + 'type' => 'default', + 'sku' => strtolower($this->randomMachineName()), + 'price' => [ + 'number' => '29.99', + 'currency_code' => 'USD', + ], + ]); + + /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ + $this->product = $this->createEntity('commerce_product', [ + 'type' => 'default', + 'title' => 'My product', + 'variations' => [$variation], + 'stores' => [$store], + ]); + + /** @var \Drupal\commerce_payment\Entity\PaymentGateway $gateway */ + $gateway = PaymentGateway::create([ + 'id' => 'example_offsite_redirect', + 'label' => 'Example', + 'plugin' => 'example_offsite_redirect', + ]); + $gateway->getPlugin()->setConfiguration([ + 'method' => 'redirect_post', + 'payment_method_types' => ['credit_card'], + ]); + $gateway->save(); + + // Cheat so we don't need JS to interact w/ Address field widget. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $customer_form_display */ + $customer_form_display = EntityFormDisplay::load('profile.customer.default'); + $address_component = $customer_form_display->getComponent('address'); + $address_component['settings']['default_country'] = 'US'; + $customer_form_display->setComponent('address', $address_component); + $customer_form_display->save(); + + } + + /** + * Tests than an order can go through checkout steps. + */ + public function testCheckoutWithOffsiteRedirectPost() { + $this->drupalGet($this->product->toUrl()->toString()); + $this->submitForm([], 'Add to cart'); + $cart_link = $this->getSession()->getPage()->findLink('your cart'); + $cart_link->click(); + $this->submitForm([], 'Checkout'); + $this->assertSession()->pageTextContains('Order Summary'); + $this->submitForm([ + 'payment_information[address][0][given_name]' => 'Johnny', + 'payment_information[address][0][family_name]' => 'Appleseed', + 'payment_information[address][0][address_line1]' => '123 New York Drive', + 'payment_information[address][0][locality]' => 'New York City', + 'payment_information[address][0][administrative_area]' => 'NY', + 'payment_information[address][0][postal_code]' => '10001', + ], 'Continue to review'); + $this->assertSession()->pageTextContains('Contact information'); + $this->assertSession()->pageTextContains($this->loggedInUser->getEmail()); + $this->assertSession()->pageTextContains('Payment information'); + $this->assertSession()->pageTextContains('Order Summary'); + $this->submitForm([], 'Pay and complete purchase'); + // No JS so we need to manually click the button to submit payment. + $this->submitForm([], 'Pay and complete purchase'); + $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); + $order = Order::load(1); + $payment_gateway = $order->payment_gateway->entity; + $this->assertEquals('example_offsite_redirect', $payment_gateway->id()); + + // Verify that a payment was created. + $payment = Payment::load(1); + $this->assertNotNull($payment); + $this->assertEquals($payment->getAmount(), $order->getTotalPrice()); + } + + /** + * Tests the transaction mode in Authorize Only. + */ + public function testCheckoutWithOffsiteRedirect302() { + // Set checkout flow to authorize only. + $payment_gateway = PaymentGateway::load('example_offsite_redirect'); + $payment_gateway->getPlugin()->setConfiguration([ + 'method' => 'redirect_302', + 'payment_method_types' => ['credit_card'], + ]); + $payment_gateway->save(); + + $this->drupalGet($this->product->toUrl()->toString()); + $this->submitForm([], 'Add to cart'); + $cart_link = $this->getSession()->getPage()->findLink('your cart'); + $cart_link->click(); + $this->submitForm([], 'Checkout'); + $this->assertSession()->pageTextContains('Order Summary'); + $this->submitForm([ + 'payment_information[address][0][given_name]' => 'Johnny', + 'payment_information[address][0][family_name]' => 'Appleseed', + 'payment_information[address][0][address_line1]' => '123 New York Drive', + 'payment_information[address][0][locality]' => 'New York City', + 'payment_information[address][0][administrative_area]' => 'NY', + 'payment_information[address][0][postal_code]' => '10001', + ], 'Continue to review'); + $this->assertSession()->pageTextContains('Contact information'); + $this->assertSession()->pageTextContains($this->loggedInUser->getEmail()); + $this->assertSession()->pageTextContains('Payment information'); + $this->assertSession()->pageTextContains('Order Summary'); + $this->submitForm([], 'Pay and complete purchase'); + $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); + $order = Order::load(1); + $payment_gateway = $order->payment_gateway->entity; + $this->assertEquals('example_offsite_redirect', $payment_gateway->id()); + // Verify that a payment was created. + $payment = Payment::load(1); + $this->assertNotNull($payment); + $this->assertEquals($payment->getAmount(), $order->getTotalPrice()); + } + +} diff --git a/modules/payment_example/commerce_payment_example.routing.yml b/modules/payment_example/commerce_payment_example.routing.yml new file mode 100644 index 000000000..1a513b763 --- /dev/null +++ b/modules/payment_example/commerce_payment_example.routing.yml @@ -0,0 +1,16 @@ +commerce_payment_example.dummy_redirect_post: + path: 'commerce_payment_example/dummy_redirect_post' + defaults: + _controller: '\Drupal\commerce_payment_example\Controller\DummyRedirectController::post' + options: + no_cache: TRUE + requirements: + _access: 'TRUE' +commerce_payment_example.dummy_redirect_302: + path: 'commerce_payment_example/dummy_redirect_302' + defaults: + _controller: '\Drupal\commerce_payment_example\Controller\DummyRedirectController::on302' + options: + no_cache: TRUE + requirements: + _access: 'TRUE' diff --git a/modules/payment_example/config/schema/commerce_payment_example.schema.yml b/modules/payment_example/config/schema/commerce_payment_example.schema.yml index c80f9e40f..00bcb019e 100644 --- a/modules/payment_example/config/schema/commerce_payment_example.schema.yml +++ b/modules/payment_example/config/schema/commerce_payment_example.schema.yml @@ -4,3 +4,9 @@ commerce_payment.commerce_payment_gateway.plugin.example_onsite: api_key: type: string label: 'API key' +commerce_payment.commerce_payment_gateway.plugin.example_offsite_redirect: + type: commerce_payment_gateway_configuration + mapping: + method: + type: string + label: 'Whether to use POST or 302 Redirect' diff --git a/modules/payment_example/src/Controller/DummyRedirectController.php b/modules/payment_example/src/Controller/DummyRedirectController.php new file mode 100644 index 000000000..48d4d578c --- /dev/null +++ b/modules/payment_example/src/Controller/DummyRedirectController.php @@ -0,0 +1,75 @@ +<?php + +namespace Drupal\commerce_payment_example\Controller; + +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Routing\TrustedRedirectResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * This is a dummy controller for mocking an off-site gateway. + */ +class DummyRedirectController implements ContainerInjectionInterface { + + /** + * The current request. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $currentRequest; + + /** + * Constructs a new DummyRedirectController object. + * + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + */ + public function __construct(RequestStack $request_stack) { + $this->currentRequest = $request_stack->getCurrentRequest(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('request_stack') + ); + } + + /** + * Callback method which accepts POST. + * + * @throws \Drupal\commerce\Response\NeedsRedirectException + */ + public function post() { + $cancel = $this->currentRequest->request->get('cancel'); + $return = $this->currentRequest->request->get('return'); + $total = $this->currentRequest->request->get('total'); + + if ($total > 20) { + return new TrustedRedirectResponse($return); + } + + return new TrustedRedirectResponse($cancel); + } + + /** + * Callback method which reacts to GET from a 302 redirect. + * + * @throws \Drupal\commerce\Response\NeedsRedirectException + */ + public function on302() { + $cancel = $this->currentRequest->query->get('cancel'); + $return = $this->currentRequest->query->get('return'); + $total = $this->currentRequest->query->get('total'); + + if ($total > 20) { + return new TrustedRedirectResponse($return); + } + + return new TrustedRedirectResponse($cancel); + } + +} diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExample.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExample.php new file mode 100644 index 000000000..abc2d67c8 --- /dev/null +++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExample.php @@ -0,0 +1,107 @@ +<?php + +namespace Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway; + +use Drupal\commerce_order\Entity\OrderInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Provides the Offsite Redirect payment gateway. + * + * @CommercePaymentGateway( + * id = "example_offsite_redirect", + * label = "Example Offsite (Redirect)", + * display_label = "Example Offsite (Redirect)", + * forms = { + * "offsite-payment" = "Drupal\commerce_payment_example\PluginForm\Offsite\OffsiteRedirectPaymentForm", + * }, + * payment_method_types = {"credit_card"}, + * credit_card_types = { + * "amex", "dinersclub", "discover", "jcb", "maestro", "mastercard", "visa", + * }, + * ) + */ +class OffsiteRedirectExample extends OffsitePaymentGatewayBase implements OffsiteRedirectExampleInterface { + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'mode' => 'redirect_post', + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + $form['method'] = [ + '#type' => 'radios', + '#title' => $this->t('Method'), + '#options' => [ + 'redirect_post' => $this->t('Redirect via POST'), + 'redirect_302' => $this->t('Redirect via 302 header'), + ], + ]; + + 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['method'] = $values['method']; + } + } + + /** + * {@inheritdoc} + */ + public function onRedirectReturn(OrderInterface $order) { + $current_request = \Drupal::getContainer()->get('request_stack')->getCurrentRequest(); + // Create the payment. + $payment_storage = \Drupal::entityTypeManager()->getStorage('commerce_payment'); + $payment = $payment_storage->create([ + 'state' => 'authorization', + 'amount' => $order->getTotalPrice(), + // Gateway plugins cannot reach their matching config entity directly. + 'payment_gateway' => $order->payment_gateway->entity->id(), + 'order_id' => $order->id(), + 'test' => $this->getMode() == 'test', + 'remote_id' => $current_request->query->get('txn_id'), + 'remote_state' => $current_request->query->get('payment_status'), + 'authorized' => REQUEST_TIME, + ]); + $payment->save(); + drupal_set_message('Payment was processed'); + } + + /** + * {@inheritdoc} + */ + public function onRedirectCancel(OrderInterface $order) { + drupal_set_message('Payment was cancelled.', 'warning'); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + // If using a 302 method most gateways require you to do some kind of + // "handshake" procedure to send them order data and they return a unique + // URL. So we only return one for POST methods. + if ($this->configuration['method'] == 'redirect_post') { + return Url::fromRoute('commerce_payment_example.dummy_redirect_post')->toString(); + } + } + +} diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExampleInterface.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExampleInterface.php new file mode 100644 index 000000000..5162417a5 --- /dev/null +++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirectExampleInterface.php @@ -0,0 +1,9 @@ +<?php + +namespace Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway; + +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsiteRedirectPaymentGatewayInterface; + +interface OffsiteRedirectExampleInterface extends OffsiteRedirectPaymentGatewayInterface { + +} diff --git a/modules/payment_example/src/PluginForm/Offsite/OffsiteRedirectPaymentForm.php b/modules/payment_example/src/PluginForm/Offsite/OffsiteRedirectPaymentForm.php new file mode 100644 index 000000000..fa3848987 --- /dev/null +++ b/modules/payment_example/src/PluginForm/Offsite/OffsiteRedirectPaymentForm.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\commerce_payment_example\PluginForm\Offsite; + +use Drupal\commerce\Response\NeedsRedirectException; +use Drupal\commerce_payment\PluginForm\OffsitePaymentForm; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +class OffsiteRedirectPaymentForm extends OffsitePaymentForm { + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ + $payment = $this->entity; + /** @var \Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\OffsiteRedirectExampleInterface $payment_gateway_plugin */ + $payment_gateway_plugin = $payment->getPaymentGateway()->getPlugin(); + $order = $payment->getOrder(); + $mode = $payment_gateway_plugin->getConfiguration()['method']; + + if ($mode == 'redirect_post') { + $form['cancel'] = [ + '#type' => 'hidden', + '#value' => $payment_gateway_plugin->getRedirectCancelUrl($order)->toString(), + ]; + $form['return'] = [ + '#type' => 'hidden', + '#value' => $payment_gateway_plugin->getRedirectReturnUrl($order)->toString(), + ]; + $form['total'] = [ + '#type' => 'hidden', + '#value' => $payment->getAmount()->getNumber(), + ]; + } + else { + throw new NeedsRedirectException(Url::fromRoute('commerce_payment_example.dummy_redirect_302', [], [ + 'absolute' => TRUE, + 'query' => [ + 'cancel' => $payment_gateway_plugin->getRedirectCancelUrl($order)->toString(), + 'return' => $payment_gateway_plugin->getRedirectReturnUrl($order)->toString(), + 'total' => $payment->getAmount()->getNumber(), + ], + ])->toString()); + } + + return parent::buildConfigurationForm($form, $form_state); + } + +} -- GitLab