diff --git a/modules/payment/commerce_payment.routing.yml b/modules/payment/commerce_payment.routing.yml index 74a92e43..f6961460 100644 --- a/modules/payment/commerce_payment.routing.yml +++ b/modules/payment/commerce_payment.routing.yml @@ -101,3 +101,29 @@ commerce_payment.notify: parameters: commerce_payment_gateway: type: entity:commerce_payment_gateway + +commerce_payment.offsite_payment_method.return: + path: '/user/{user}/payment-methods/{payment_gateway}/return' + defaults: + _controller: '\Drupal\commerce_payment\Controller\OffsitePaymentMethodUserPageController::returnPage' + requirements: + _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' + options: + parameters: + user: + type: entity:user + payment_gateway: + type: entity:commerce_payment_gateway + +commerce_payment.offsite_payment_method.cancel: + path: '/user/{user}/payment-methods/{payment_gateway}/cancel' + defaults: + _controller: '\Drupal\commerce_payment\Controller\OffsitePaymentMethodUserPageController::cancelPage' + requirements: + _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' + options: + parameters: + user: + type: entity:user + payment_gateway: + type: entity:commerce_payment_gateway diff --git a/modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php b/modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php new file mode 100644 index 00000000..06d2ea01 --- /dev/null +++ b/modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php @@ -0,0 +1,158 @@ +logger = $logger; + $this->entityTypeManager = $entity_type_manager; + $this->stringTranslation = $translation; + $this->currentUser = $current_user; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('logger.channel.commerce_payment'), + $container->get('entity_type.manager'), + $container->get('string_translation'), + $container->get('current_user'), + $container->get('messenger') + ); + } + + /** + * Provides the "return" offsite payment method page. + * + * @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway + * The payment gateway entity. + * @param \Drupal\Core\Session\AccountInterface $user + * The user entity. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function returnPage(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface $payment_gateway_plugin */ + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsOnUserPageInterface::class); + } + try { + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $payment_method = $payment_method_storage->create([ + // Payment method type may depend on the request payload. We create + // payment method stub based on default payment method type available + // for payment gateway plugin but module developers can swap it + // with custom payment method. Keep in mind that $payment_method is + // passed by reference and code below relies on that reference to add + // payment method to the order. + 'type' => $payment_gateway_plugin->getDefaultPaymentMethodType()->getPluginId(), + 'payment_gateway' => $payment_gateway, + 'uid' => $user->id(), + 'payment_gateway_mode' => $payment_gateway_plugin->getMode(), + ]); + // In case payment gateway supports payment method. + $payment_details = $request->isMethod('GET') ? $request->query->all() : $request->request->all(); + $payment_gateway_plugin->createPaymentMethod($payment_method, $payment_details); + return new RedirectResponse(Url::fromRoute('entity.commerce_payment_method.collection', ['user' => $user->id()])->toString()); + } + catch (PaymentGatewayException $e) { + $this->logger->error($e->getMessage()); + $this->messenger->addError($this->t('Payment method creation has failed.')); + return new RedirectResponse(Url::fromRoute('entity.commerce_payment_method.collection', ['user' => $user->id()])->toString()); + } + } + + /** + * Provides the "cancel" offsite payment method page. + * + * @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway + * The payment gateway entity. + * @param \Drupal\Core\Session\AccountInterface $user + * The user entity. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function cancelPage(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsOnUserPageInterface::class); + } + $payment_gateway_plugin->onCancelOffsitePaymentMethodOnUserPage($request); + return new RedirectResponse(Url::fromRoute('entity.commerce_payment_method.collection', ['user' => $user->id()])->toString()); + } + +} diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php index 84159e11..b684f4cc 100644 --- a/modules/payment/src/Controller/PaymentCheckoutController.php +++ b/modules/payment/src/Controller/PaymentCheckoutController.php @@ -7,8 +7,10 @@ use Drupal\commerce_checkout\CheckoutOrderManagerInterface; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_payment\Exception\PaymentGatewayException; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Core\Access\AccessException; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; @@ -42,6 +44,13 @@ class PaymentCheckoutController implements ContainerInjectionInterface { */ protected $logger; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * Constructs a new PaymentCheckoutController object. * @@ -51,11 +60,14 @@ class PaymentCheckoutController implements ContainerInjectionInterface { * The messenger. * @param \Psr\Log\LoggerInterface $logger * The logger. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager. */ - public function __construct(CheckoutOrderManagerInterface $checkout_order_manager, MessengerInterface $messenger, LoggerInterface $logger) { + public function __construct(CheckoutOrderManagerInterface $checkout_order_manager, MessengerInterface $messenger, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager) { $this->checkoutOrderManager = $checkout_order_manager; $this->messenger = $messenger; $this->logger = $logger; + $this->entityTypeManager = $entity_type_manager; } /** @@ -65,7 +77,8 @@ class PaymentCheckoutController implements ContainerInjectionInterface { return new static( $container->get('commerce_checkout.checkout_order_manager'), $container->get('messenger'), - $container->get('logger.channel.commerce_payment') + $container->get('logger.channel.commerce_payment'), + $container->get('entity_type.manager') ); } @@ -94,6 +107,38 @@ class PaymentCheckoutController implements ContainerInjectionInterface { $checkout_flow = $order->get('checkout_flow')->entity; $checkout_flow_plugin = $checkout_flow->getPlugin(); + if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface) { + try { + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $payment_method = $payment_method_storage->create([ + // Payment method type may depend on the request payload. We create + // payment method stub based on default payment method type available + // for payment gateway plugin but module developers can swap it + // with custom payment method. Keep in mind that $payment_method is + // passed by reference and code below relies on that reference to add + // payment method to the order. + 'type' => $payment_gateway_plugin->getDefaultPaymentMethodType()->getPluginId(), + 'payment_gateway' => $payment_gateway, + 'uid' => $order->getCustomerId(), + 'billing_profile' => $order->getBillingProfile(), + ]); + // In case payment gateway supports payment method. + $payment_details = $request->isMethod('GET') ? $request->query->all() : $request->request->all(); + $payment_gateway_plugin->createPaymentMethod($payment_method, $payment_details); + + // Add payment method to the order. + $order->set('payment_method', $payment_method); + $order->save(); + } + catch (PaymentGatewayException $e) { + // If creating payment method failed we allow onReturn to handle + // creating Payment, which is required to fulfill the payment. This + // exception is muted and logged. + $this->logger->error($e->getMessage()); + } + } + try { $payment_gateway_plugin->onReturn($order, $request); $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id); diff --git a/modules/payment/src/Form/PaymentMethodAddForm.php b/modules/payment/src/Form/PaymentMethodAddForm.php index f4dc0472..09c9d212 100644 --- a/modules/payment/src/Form/PaymentMethodAddForm.php +++ b/modules/payment/src/Form/PaymentMethodAddForm.php @@ -3,11 +3,13 @@ namespace Drupal\commerce_payment\Form; use Drupal\commerce\InlineFormManager; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -71,7 +73,7 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); $payment_gateway = $payment_gateway_storage->loadForUser($user); // @todo Move this check to the access handler. - if (!$payment_gateway || !($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface)) { + if (!$payment_gateway || (!($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) && !($payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface))) { throw new AccessDeniedHttpException(); } $form_state->set('payment_gateway', $payment_gateway); @@ -152,6 +154,8 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa 'payment_gateway' => $form_state->get('payment_gateway'), 'uid' => $form_state->getBuildInfo()['args'][0]->id(), ]); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = $form_state->get('payment_gateway'); $inline_form = $this->inlineFormManager->createInstance('payment_gateway_form', [ 'operation' => 'add-payment-method', ], $payment_method); @@ -160,7 +164,12 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa '#parents' => ['payment_method'], '#inline_form' => $inline_form, ]; + if ($payment_gateway && $payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { + $form['payment_method']['#return_url'] = $this->buildReturnUrl($form_state->getBuildInfo()['args'][0]->id(), $payment_gateway->id())->toString(); + $form['payment_method']['#cancel_url'] = $this->buildCancelUrl($form_state->getBuildInfo()['args'][0]->id(), $payment_gateway->id())->toString(); + } $form['payment_method'] = $inline_form->buildInlineForm($form['payment_method'], $form_state); + $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('Save'), @@ -170,6 +179,42 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa return $form; } + /** + * Builds the URL to the "return" page. + * + * @param int $uid + * The user id the payment method is being added for. + * @param string $gateway_id + * The payment gateway id of the payment gateway + * + * @return \Drupal\Core\Url + * The "return" page URL. + */ + protected function buildReturnUrl(int $uid, string $gateway_id) { + return Url::fromRoute('commerce_payment.offsite_payment_method.return', [ + 'user' => $uid, + 'payment_gateway' => $gateway_id, + ], ['absolute' => TRUE]); + } + + /** + * Builds the URL to the "cancel" page. + * + * @param int $uid + * The user id the payment method is being added for. + * @param string $gateway_id + * The payment gateway id of the payment gateway. + * + * @return \Drupal\Core\Url + * The "cancel" page URL. + */ + protected function buildCancelUrl(int $uid, string $gateway_id) { + return Url::fromRoute('commerce_payment.offsite_payment_method.cancel', [ + 'user' => $uid, + 'payment_gateway' => $gateway_id, + ], ['absolute' => TRUE]); + } + /** * {@inheritdoc} */ diff --git a/modules/payment/src/PaymentGatewayStorage.php b/modules/payment/src/PaymentGatewayStorage.php index 55417710..2fe968a6 100644 --- a/modules/payment/src/PaymentGatewayStorage.php +++ b/modules/payment/src/PaymentGatewayStorage.php @@ -5,6 +5,7 @@ namespace Drupal\commerce_payment; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_payment\Event\FilterPaymentGatewaysEvent; use Drupal\commerce_payment\Event\PaymentEvents; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface; @@ -70,7 +71,7 @@ class PaymentGatewayStorage extends ConfigEntityStorage implements PaymentGatewa public function loadForUser(UserInterface $account) { $payment_gateways = $this->loadByProperties(['status' => TRUE]); $payment_gateways = array_filter($payment_gateways, function ($payment_gateway) { - return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface; + return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface || $payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; }); // @todo Implement resolving logic. $payment_gateway = reset($payment_gateways); diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index e713d6b5..8f4b874d 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -5,8 +5,10 @@ namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane; use Drupal\commerce\InlineFormManager; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase; +use Drupal\commerce_payment\Entity\PaymentMethodInterface; use Drupal\commerce_payment\PaymentOption; use Drupal\commerce_payment\PaymentOptionsBuilderInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -206,10 +208,15 @@ class PaymentInformation extends CheckoutPaneBase { $default_payment_gateway_id = $default_option->getPaymentGatewayId(); $payment_gateway = $payment_gateways[$default_payment_gateway_id]; - if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) { + $payment_gateway_plugin = $payment_gateway->getPlugin(); + // If this payment gateway plugin supports stored payment methods, we build + // the "add-payment-method" plugin form. However, we skip if this is an + // off-site payment gateway, since payment method creation is part of the + // payment process which occurs later. + if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface && !$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { $pane_form = $this->buildPaymentMethodForm($pane_form, $form_state, $default_option); } - elseif ($payment_gateway->getPlugin()->collectsBillingInformation()) { + elseif ($payment_gateway_plugin->collectsBillingInformation() && !$default_option->getPaymentMethodId()) { $pane_form = $this->buildBillingProfileForm($pane_form, $form_state); } @@ -349,7 +356,8 @@ class PaymentInformation extends CheckoutPaneBase { $billing_profile = $inline_form->getEntity(); $this->order->setBillingProfile($billing_profile); // The billing profile is provided either because the order is free, - // or the selected gateway is off-site. If it's the former, stop here. + // or the selected gateway does not support stored payment methods. + // If it's the former, stop here. if ($this->order->isPaid() || $this->order->getTotalPrice()->isZero()) { return; } @@ -366,38 +374,61 @@ class PaymentInformation extends CheckoutPaneBase { return; } - if ($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) { - if (!empty($selected_option->getPaymentMethodTypeId())) { - /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */ - $inline_form = $pane_form['add_payment_method']['#inline_form']; - // The payment method was just created. - $payment_method = $inline_form->getEntity(); + $payment_gateway_plugin = $payment_gateway->getPlugin(); + if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface) { + // @todo this would be solved if we could re-use the add_payment_method inline form. + if ($payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) { + // This is a stored off-site gateway payment method. + if ($selected_option->getPaymentMethodId()) { + /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + $payment_method = $payment_method_storage->load($selected_option->getPaymentMethodId()); + assert($payment_method instanceof PaymentMethodInterface); + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $this->order->set('payment_gateway', $payment_method->getPaymentGateway()); + $this->order->set('payment_method', $payment_method); + $this->order->setBillingProfile($payment_method->getBillingProfile()); + } + // The new payment method is created when the transaction processes. + else { + $this->order->set('payment_gateway', $payment_gateway); + $this->order->set('payment_method', NULL); + } } + // Handle onsite. else { - /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ - $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); - $payment_method = $payment_method_storage->load($selected_option->getPaymentMethodId()); - } + if (!empty($selected_option->getPaymentMethodTypeId())) { + /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */ + $inline_form = $pane_form['add_payment_method']['#inline_form']; + // The payment method was just created. + $payment_method = $inline_form->getEntity(); + } + else { + /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + $payment_method = $payment_method_storage->load($selected_option->getPaymentMethodId()); + } - /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ - $this->order->set('payment_gateway', $payment_method->getPaymentGateway()); - $this->order->set('payment_method', $payment_method); - // Copy the billing information to the order. - $payment_method_profile = $payment_method->getBillingProfile(); - if ($payment_method_profile) { - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ - 'type' => 'customer', - 'uid' => 0, - ]); + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ + $this->order->set('payment_gateway', $payment_method->getPaymentGateway()); + $this->order->set('payment_method', $payment_method); + // Copy the billing information to the order. + $payment_method_profile = $payment_method->getBillingProfile(); + if ($payment_method_profile) { + $billing_profile = $this->order->getBillingProfile(); + if (!$billing_profile) { + $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ + 'type' => 'customer', + 'uid' => 0, + ]); + } + $billing_profile->populateFromProfile($payment_method_profile); + // The data field is not copied by default but needs to be. + // For example, both profiles need to have an address_book_profile_id. + $billing_profile->populateFromProfile($payment_method_profile, ['data']); + $billing_profile->save(); + $this->order->setBillingProfile($billing_profile); } - $billing_profile->populateFromProfile($payment_method_profile); - // The data field is not copied by default but needs to be. - // For example, both profiles need to have an address_book_profile_id. - $billing_profile->populateFromProfile($payment_method_profile, ['data']); - $billing_profile->save(); - $this->order->setBillingProfile($billing_profile); } } else { diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php index 4bff60ad..a43c9349 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php @@ -10,7 +10,7 @@ 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\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Link; @@ -173,9 +173,9 @@ class PaymentProcess extends CheckoutPaneBase { $payment = $this->createPayment($payment_gateway); $next_step_id = $this->checkoutFlow->getNextStepId($this->getStepId()); - if ($payment_gateway_plugin instanceof OnsitePaymentGatewayInterface) { + if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface && $payment_method = $this->order->payment_method->entity) { try { - $payment->payment_method = $this->order->payment_method->entity; + $payment->payment_method = $payment_method; $payment_gateway_plugin->createPayment($payment, $this->configuration['capture']); $this->checkoutFlow->redirectToStep($next_step_id); } diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/OnsitePaymentGatewayInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/OnsitePaymentGatewayInterface.php index 6108efe9..53153144 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentGateway/OnsitePaymentGatewayInterface.php +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/OnsitePaymentGatewayInterface.php @@ -2,8 +2,6 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway; -use Drupal\commerce_payment\Entity\PaymentInterface; - /** * Defines the base interface for on-site payment gateways. * @@ -29,20 +27,4 @@ use Drupal\commerce_payment\Entity\PaymentInterface; */ interface OnsitePaymentGatewayInterface extends PaymentGatewayInterface, SupportsStoredPaymentMethodsInterface { - /** - * Creates a payment. - * - * @param \Drupal\commerce_payment\Entity\PaymentInterface $payment - * The payment. - * @param bool $capture - * Whether the created payment should be captured (VS authorized only). - * Allowed to be FALSE only if the plugin supports authorizations. - * - * @throws \InvalidArgumentException - * If $capture is FALSE but the plugin does not support authorizations. - * @throws \Drupal\commerce_payment\Exception\PaymentGatewayException - * Thrown when the transaction fails for any reason. - */ - public function createPayment(PaymentInterface $payment, $capture = TRUE); - } diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsOnUserPageInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsOnUserPageInterface.php new file mode 100644 index 00000000..8d06b52a --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsOnUserPageInterface.php @@ -0,0 +1,24 @@ + $value) { + $form[$key] = [ + '#type' => 'hidden', + '#value' => $value, + // Ensure the correct keys by sending values from the form root. + '#parents' => [$key], + ]; + } + // The key is prefixed with 'commerce_' to prevent conflicts with $data. + $form['commerce_message'] = [ + '#markup' => '
' . t('Please wait while you are redirected to the payment server. If nothing happens within 10 seconds, please click on the button below.') . '
', + '#weight' => -10, + ]; + } + else { + $redirect_url = Url::fromUri($redirect_url, ['absolute' => TRUE, 'query' => $data])->toString(); + throw new NeedsRedirectException($redirect_url); + } + + return $form; + } + + /** + * Prepares the complete form for a POST redirect. + * + * Sets the form #action, adds a class for the JS to target. + * Workaround for buildConfigurationForm() not receiving $complete_form. + * + * @param array $form + * The plugin form. + * @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 processRedirectForm(array $form, FormStateInterface $form_state, array &$complete_form) { + $complete_form['#action'] = $form['#redirect_url']; + $complete_form['#attributes']['class'][] = 'payment-redirect-form'; + // The form actions are hidden by default, but needed in this case. + $complete_form['actions']['#access'] = TRUE; + foreach (Element::children($complete_form['actions']) as $element_name) { + $complete_form['actions'][$element_name]['#access'] = TRUE; + } + + 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/src/PluginForm/PaymentOffsiteForm.php b/modules/payment/src/PluginForm/PaymentOffsiteForm.php index c080b9bb..b2c8eb9e 100644 --- a/modules/payment/src/PluginForm/PaymentOffsiteForm.php +++ b/modules/payment/src/PluginForm/PaymentOffsiteForm.php @@ -60,6 +60,7 @@ abstract class PaymentOffsiteForm extends PaymentGatewayFormBase { protected function buildRedirectForm(array $form, FormStateInterface $form_state, $redirect_url, array $data, $redirect_method = self::REDIRECT_GET) { if ($redirect_method == self::REDIRECT_POST) { $form['#attached']['library'][] = 'commerce_payment/offsite_redirect'; + $form['#attributes']['class'][] = 'payment-redirect-form'; $form['#process'][] = [get_class($this), 'processRedirectForm']; $form['#redirect_url'] = $redirect_url; diff --git a/modules/payment/tests/src/Functional/PaymentGatewayTest.php b/modules/payment/tests/src/Functional/PaymentGatewayTest.php index 4dd28184..6faec59e 100644 --- a/modules/payment/tests/src/Functional/PaymentGatewayTest.php +++ b/modules/payment/tests/src/Functional/PaymentGatewayTest.php @@ -38,9 +38,9 @@ class PaymentGatewayTest extends CommerceBrowserTestBase { $edit = [ 'label' => 'Example', - 'plugin' => 'example_offsite_redirect', - 'configuration[example_offsite_redirect][redirect_method]' => 'post', - 'configuration[example_offsite_redirect][mode]' => 'test', + 'plugin' => 'example_stored_offsite_redirect', + 'configuration[example_stored_offsite_redirect][redirect_method]' => 'post', + 'configuration[example_stored_offsite_redirect][mode]' => 'test', 'status' => '1', // Setting the 'id' can fail if focus switches to another field. // This is a bug in the machine name JS that can be reproduced manually. @@ -53,7 +53,7 @@ class PaymentGatewayTest extends CommerceBrowserTestBase { $payment_gateway = PaymentGateway::load('example'); $this->assertEquals('example', $payment_gateway->id()); $this->assertEquals('Example', $payment_gateway->label()); - $this->assertEquals('example_offsite_redirect', $payment_gateway->getPluginId()); + $this->assertEquals('example_stored_offsite_redirect', $payment_gateway->getPluginId()); $this->assertEmpty($payment_gateway->getConditions()); $this->assertEquals('AND', $payment_gateway->getConditionOperator()); $this->assertEquals(TRUE, $payment_gateway->status()); diff --git a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php index 1e3e36ae..66e47106 100644 --- a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php +++ b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php @@ -156,6 +156,18 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase { ]); $payment_gateway->save(); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ + $payment_gateway = PaymentGateway::create([ + 'id' => 'stored_offsite', + 'label' => 'Stored off-site', + 'plugin' => 'example_stored_offsite_redirect', + 'configuration' => [ + 'redirect_method' => 'post', + 'payment_method_types' => ['credit_card'], + ], + ]); + $payment_gateway->save(); + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ $payment_gateway = PaymentGateway::create([ 'id' => 'manual', @@ -259,6 +271,9 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase { $manual_gateway = PaymentGateway::load('manual'); $manual_gateway->setStatus(FALSE); $manual_gateway->save(); + $stored_offsite_gateway = PaymentGateway::load('stored_offsite'); + $stored_offsite_gateway->setStatus(FALSE); + $stored_offsite_gateway->save(); // A single radio button should be selected and hidden. $this->drupalGet('checkout/1'); @@ -544,6 +559,9 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase { $manual_gateway = PaymentGateway::load('manual'); $manual_gateway->setStatus(FALSE); $manual_gateway->save(); + $manual_gateway = PaymentGateway::load('stored_offsite'); + $manual_gateway->setStatus(FALSE); + $manual_gateway->save(); $payment_gateway = PaymentGateway::load('offsite'); $payment_gateway->setPluginConfiguration([ @@ -673,6 +691,164 @@ class PaymentCheckoutTest extends CommerceWebDriverTestBase { $this->assertCount(1, $payments); } + /** + * Tests checkout with a stored off-site gateway (POST redirect method). + */ + public function testCheckoutWithStoredOffsiteRedirectPost() { + $this->storedOffsiteCheckoutAssertions(1, 1, NULL, TRUE); + } + + /** + * Tests checkout with a stored off-site gateway (POST redirect method, + * manual). + * + * In this scenario the customer must click the submit button on the payment + * page in order to proceed to the gateway. + */ + public function testCheckoutWithStoredOffsiteRedirectPostManual() { + $payment_gateway = PaymentGateway::load('stored_offsite'); + $payment_gateway->setPluginConfiguration([ + 'redirect_method' => 'post_manual', + 'payment_method_types' => ['credit_card'], + ]); + $payment_gateway->save(); + + $this->storedOffsiteCheckoutAssertions(1, 1, NULL, TRUE, 'post_manual'); + } + + /** + * Tests checkout with a stored off-site gateway (GET redirect method). + */ + public function testCheckoutWithStoredOffsiteRedirectGet() { + // Checkout must work when the off-site gateway is alone, and the + // radio button hidden. + $onsite_gateway = PaymentGateway::load('onsite'); + $onsite_gateway->setStatus(FALSE); + $onsite_gateway->save(); + $manual_gateway = PaymentGateway::load('manual'); + $manual_gateway->setStatus(FALSE); + $manual_gateway->save(); + $manual_gateway = PaymentGateway::load('offsite'); + $manual_gateway->setStatus(FALSE); + $manual_gateway->save(); + + $payment_gateway = PaymentGateway::load('stored_offsite'); + $payment_gateway->setPluginConfiguration([ + 'redirect_method' => 'get', + 'payment_method_types' => ['credit_card'], + ]); + $payment_gateway->save(); + + $this->storedOffsiteCheckoutAssertions(1, 1); + $this->storedOffsiteCheckoutAssertions(2, 2, 3); + } + + /** + * Assertions for stored offsite gateway checkouts. + * + * @param int $order_id + * The expected order id. + * @param int $payment_id + * The expected payment id. + * @param int $payment_method_id + * The payment method id to select on checkout. Leave it on NULL to create + * a new payment method. + * @param boolean $multiple_gateways + * If we expect only one gateway to show on checkout (ie.e. no radios). + * @param string $redirect_mehtod + * Set it to 'post_manual' if the payment step needs manual interaction. + */ + protected function storedOffsiteCheckoutAssertions($order_id, $payment_id, $payment_method_id = NULL, $multiple_gateways = FALSE, $redirect_mehtod = 'post') { + $this->drupalGet($this->product->toUrl()->toString()); + $this->submitForm([], 'Add to cart'); + $this->drupalGet('checkout/' . $order_id); + if (!$payment_method_id) { + if ($multiple_gateways) { + $radio_button = $this->getSession()->getPage()->findField('Stored offsite'); + $radio_button->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + } + $this->assertRenderedAddress($this->defaultAddress, 'payment_information[billing_information]'); + $this->submitForm([], 'Continue to review'); + } + else { + $this->assertSession()->pageTextContains('Visa ending in 1111'); + $this->submitForm([ + 'payment_information[payment_method]' => $payment_method_id, + ], 'Continue to review'); + } + + $this->assertSession()->pageTextContains('Payment information'); + $this->assertSession()->pageTextContains('Example'); + $this->assertSession()->pageTextContains('Bryan Centarro'); + $this->assertSession()->pageTextContains('9 Drupal Ave'); + $this->submitForm([], 'Pay and complete purchase'); + if ($redirect_mehtod == 'post_manual') { + $order = Order::load($order_id); + $this->assertSession()->addressEquals('checkout/' . $order_id . '/payment'); + $this->assertTrue($order->isLocked()); + $this->assertEquals('stored_offsite', $order->get('payment_gateway')->target_id); + $this->submitForm([], 'Proceed to Stored offsite'); + } + $this->assertSession()->pageTextContains('Your order number is ' . $order_id . '. You can view your order on your account page when logged in.'); + + \Drupal::entityTypeManager()->getStorage('commerce_order')->resetCache(['1']); + $order = Order::load(1); + $this->assertEquals('stored_offsite', $order->get('payment_gateway')->target_id); + $this->assertFalse($order->isLocked()); + // Verify that a payment was created. + $payment = Payment::load($payment_id); + $this->assertNotNull($payment); + $this->assertEquals($payment->getAmount(), $order->getTotalPrice()); + // Verify that a reusable payment method was created. + $payment_method = $payment->getPaymentMethod(); + $this->assertEquals(TRUE, $payment_method->isReusable()); + $this->assertEquals('stored_offsite', $payment_method->getPaymentGatewayId()); + $this->assertEquals(3, $payment_method->id()); + } + + /** + * Tests checkout with an off-site gateway (GET redirect method) that fails. + * + * The off-site form throws an exception, simulating an API fail. + */ + public function testFailedCheckoutWithStoredOffsiteRedirectGet() { + $payment_gateway = PaymentGateway::load('offsite'); + $payment_gateway->setPluginConfiguration([ + 'redirect_method' => 'get', + 'payment_method_types' => ['credit_card'], + ]); + $payment_gateway->save(); + + $this->drupalGet($this->product->toUrl()->toString()); + $this->submitForm([], 'Add to cart'); + $this->drupalGet('checkout/1'); + $radio_button = $this->getSession()->getPage()->findField('Example'); + $radio_button->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertRenderedAddress($this->defaultAddress, 'payment_information[billing_information]'); + $this->getSession()->getPage()->pressButton('billing_edit'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->submitForm([ + 'payment_information[billing_information][address][0][address][family_name]' => 'FAIL', + ], 'Continue to review'); + $this->assertSession()->pageTextContains('Payment information'); + $this->assertSession()->pageTextContains('Example'); + $this->assertSession()->pageTextContains('Bryan FAIL'); + $this->assertSession()->pageTextContains('9 Drupal Ave'); + $this->submitForm([], 'Pay and complete purchase'); + $this->assertSession()->pageTextNotContains('Your order number is 1. You can view your order on your account page when logged in.'); + $this->assertSession()->pageTextContains('We encountered an unexpected error processing your payment. Please try again later.'); + $this->assertSession()->addressEquals('checkout/1/order_information'); + + $order = Order::load(1); + $this->assertFalse($order->isLocked()); + // Verify a payment was not created. + $payment = Payment::load(1); + $this->assertNull($payment); + } + /** * Tests checkout with a manual gateway. */ diff --git a/modules/payment_example/commerce_payment_example.routing.yml b/modules/payment_example/commerce_payment_example.routing.yml index 1a513b76..f22e6118 100644 --- a/modules/payment_example/commerce_payment_example.routing.yml +++ b/modules/payment_example/commerce_payment_example.routing.yml @@ -6,6 +6,7 @@ commerce_payment_example.dummy_redirect_post: no_cache: TRUE requirements: _access: 'TRUE' + commerce_payment_example.dummy_redirect_302: path: 'commerce_payment_example/dummy_redirect_302' defaults: @@ -14,3 +15,12 @@ commerce_payment_example.dummy_redirect_302: no_cache: TRUE requirements: _access: 'TRUE' + +commerce_payment_example.dummy_redirect_payment_method_post: + path: 'commerce_payment_example/dummy_redirect_payment_method_post' + defaults: + _controller: '\Drupal\commerce_payment_example\Controller\DummyRedirectController::postPaymentMethod' + 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 159a37d5..8a7f0340 100644 --- a/modules/payment_example/config/schema/commerce_payment_example.schema.yml +++ b/modules/payment_example/config/schema/commerce_payment_example.schema.yml @@ -11,3 +11,10 @@ commerce_payment.commerce_payment_gateway.plugin.example_offsite_redirect: redirect_method: type: string label: 'Redirect method' + +commerce_payment.commerce_payment_gateway.plugin.example_stored_offsite_redirect: + type: commerce_payment_gateway_configuration + mapping: + redirect_method: + type: string + label: 'Redirect method' diff --git a/modules/payment_example/src/Controller/DummyRedirectController.php b/modules/payment_example/src/Controller/DummyRedirectController.php index 48d4d578..6324e572 100644 --- a/modules/payment_example/src/Controller/DummyRedirectController.php +++ b/modules/payment_example/src/Controller/DummyRedirectController.php @@ -47,14 +47,37 @@ class DummyRedirectController implements ContainerInjectionInterface { $cancel = $this->currentRequest->request->get('cancel'); $return = $this->currentRequest->request->get('return'); $total = $this->currentRequest->request->get('total'); - + $query = [ + 'remote_payment_method_id' => 'pm-32768342', + 'cardexpdate' => '1228', + ]; if ($total > 20) { - return new TrustedRedirectResponse($return); + return new TrustedRedirectResponse($return . '?' . $this->buildQuery($query)); } return new TrustedRedirectResponse($cancel); } + /** + * Callback method which accepts POST. + */ + public function postPaymentMethod() { + $month = rand(1,12); + if ($month % 2) { + $return = $this->currentRequest->request->get('return'); + $month = str_pad($month, 2, '0', STR_PAD_LEFT); + $query = [ + 'remote_payment_method_id' => 'pm-32768342', + 'cardexpdate' => $month . '28', + ]; + return new TrustedRedirectResponse($return . '?' . $this->buildQuery($query)); + } + else { + $cancel = $this->currentRequest->request->get('cancel'); + return new TrustedRedirectResponse($cancel); + } + } + /** * Callback method which reacts to GET from a 302 redirect. * @@ -64,12 +87,37 @@ class DummyRedirectController implements ContainerInjectionInterface { $cancel = $this->currentRequest->query->get('cancel'); $return = $this->currentRequest->query->get('return'); $total = $this->currentRequest->query->get('total'); + $query = [ + 'remote_payment_method_id' => 'pm-32768342', + 'cardexpdate' => '1228', + ]; if ($total > 20) { - return new TrustedRedirectResponse($return); + return new TrustedRedirectResponse($return . '?' . $this->buildQuery($query)); } return new TrustedRedirectResponse($cancel); } + /** + * Builds query parameters passed back to the site. + * + * @param array $query + * Query parameters. + * + * @return string + * Query string. + */ + protected function buildQuery(array $query = []) { + $query = $query + [ + 'cardnomask' => 'xxxxxxxxxxxx1111', + 'cardprefix' => '4111', + 'cardexpdate' => '1228', + 'txn_id' => '12345678', + 'payment_status' => 'OK', + 'customtext' => 'Srećno', + ]; + return http_build_query($query); + } + } diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/StoredOffsiteRedirect.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/StoredOffsiteRedirect.php new file mode 100644 index 00000000..e464589b --- /dev/null +++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/StoredOffsiteRedirect.php @@ -0,0 +1,208 @@ +messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('plugin.manager.commerce_payment_type'), + $container->get('plugin.manager.commerce_payment_method_type'), + $container->get('datetime.time'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) { + $payment_method->card_type = CreditCard::detectType($payment_details['cardprefix'])->getId(); + $payment_method->card_number = substr($payment_details['cardnomask'], -4); + $payment_method->card_exp_month = substr($payment_details['cardexpdate'], 0, 2); + $payment_method->card_exp_year = substr($payment_details['cardexpdate'], -2); + + $payment_method->setRemoteId($payment_details['txn_id']); + $expires = CreditCard::calculateExpirationTimestamp($payment_method->card_exp_month->value, $payment_method->card_exp_year->value); + $payment_method->setExpiresTime($expires); + $payment_method->save(); + } + + /** + * {@inheritdoc} + */ + public function deletePaymentMethod(PaymentMethodInterface $payment_method) { + $payment_method->delete(); + } + + /** + * {@inheritdoc} + */ + public function createPayment(PaymentInterface $payment, $capture = TRUE) { + $this->assertPaymentState($payment, ['new']); + $payment_method = $payment->getPaymentMethod(); + $this->assertPaymentMethod($payment_method); + + // Perform the create payment request here, throw an exception if it fails. + // See \Drupal\commerce_payment\Exception for the available exceptions. + // Remember to take into account $capture when performing the request. + $amount = $payment->getAmount(); + $payment_method_token = $payment_method->getRemoteId(); + // The remote ID returned by the request. + $remote_id = '123456'; + $next_state = $capture ? 'completed' : 'authorization'; + + $payment->setState($next_state); + $payment->setRemoteId($remote_id); + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function onReturn(OrderInterface $order, Request $request) { + // @todo Add examples of request validation. + // Note: Since requires_billing_information is FALSE, the order is + // not guaranteed to have a billing profile. Confirm that + // $order->getBillingProfile() is not NULL before trying to use it. + $payment_storage = $this->entityTypeManager->getStorage('commerce_payment'); + $payment = $payment_storage->create([ + 'state' => 'completed', + 'amount' => $order->getBalance(), + 'payment_gateway' => $this->parentEntity->id(), + 'order_id' => $order->id(), + 'remote_id' => $request->query->get('txn_id'), + 'remote_state' => $request->query->get('payment_status'), + 'payment_method' => $order->get('payment_method')->target_id, + ]); + $payment->save(); + } + + /** + * {@inheritdoc} + */ + public function onReturnOffsitePaymentMethodOnUserPage(PaymentMethodInterface $payment_method, array $payment_details) { + // This is the exact copy of + // Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\Onsite::createPaymentMethodOnUserPage(). + $required_keys = [ + // The expected keys are payment gateway specific and usually match + // the PaymentMethodAddForm form elements. They are expected to be valid. + 'cardprefix', 'cardnomask', 'cardexpdate', + ]; + foreach ($required_keys as $required_key) { + if (empty($payment_details[$required_key])) { + throw new \InvalidArgumentException(sprintf('$payment_details must contain the %s key.', $required_key)); + } + } + // Add a built in test for testing decline exceptions. + // Note: Since requires_billing_information is FALSE, the payment method + // is not guaranteed to have a billing profile. Confirm tha + // $payment_method->getBillingProfile() is not NULL before trying to use it. + if ($billing_profile = $payment_method->getBillingProfile()) { + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $billing_address */ + $billing_address = $billing_profile->get('address')->first(); + if ($billing_address->getPostalCode() == '53141') { + throw new HardDeclineException('The payment method was declined'); + } + } + + // Perform the create request here, throw an exception if it fails. + // See \Drupal\commerce_payment\Exception for the available exceptions. + // You might need to do different API requests based on whether the + // payment method is reusable: $payment_method->isReusable(). + // Non-reusable payment methods usually have an expiration timestamp. + $payment_method->card_type = CreditCard::detectType($payment_details['cardprefix'])->getId(); + // Only the last 4 numbers are safe to store. + $payment_method->card_number = substr($payment_details['cardnomask'], -4); + $payment_method->card_exp_month = substr($payment_details['cardexpdate'], 0, 2); + $payment_method->card_exp_year = substr($payment_details['cardexpdate'], -2); + $expires = CreditCard::calculateExpirationTimestamp($payment_method->card_exp_month->value, $payment_method->card_exp_year->value); + $payment_method->setExpiresTime($expires); + // The remote ID returned by the request. + $payment_method->setRemoteId($payment_details['remote_payment_method_id']); + $payment_method->save(); + $this->messenger->addMessage($this->t('A new payment method has been created.')); + } + + /** + * {@inheritdoc} + */ + public function onCancelOffsitePaymentMethodOnUserPage(Request $request) { + $this->messenger->addWarning($this->t('Payment method creation was canceled.')); + } + +} diff --git a/modules/payment_example/src/PluginForm/StoredOffsiteRedirect/PaymentMethodAddForm.php b/modules/payment_example/src/PluginForm/StoredOffsiteRedirect/PaymentMethodAddForm.php new file mode 100644 index 00000000..611af63b --- /dev/null +++ b/modules/payment_example/src/PluginForm/StoredOffsiteRedirect/PaymentMethodAddForm.php @@ -0,0 +1,28 @@ +toString(); + + $data = [ + 'return' => $form['#return_url'], + 'cancel' => $form['#cancel_url'], + ]; + + return $this->buildRedirectForm($form, $form_state, $redirect_url, $data, $redirect_method); + } + +}