Commit b1cfa002 authored by bojanz's avatar bojanz

Issue #2785591: Finalize the off-site API.

parent 93d56ede
......@@ -13,7 +13,7 @@ payment_method_icons:
offsite_redirect:
version: VERSION
js:
js/offiste-redirect.js: {}
js/offsite-redirect.js: {}
dependencies:
- core/jquery
- core/drupal
......
......@@ -63,10 +63,10 @@ entity.commerce_payment_method.collection:
user:
type: entity:user
commerce_payment.checkout.cancel:
path: '/checkout/{commerce_order}/{step}/cancel'
commerce_payment.checkout.return:
path: '/checkout/{commerce_order}/{step}/return'
defaults:
_controller: '\Drupal\commerce_payment\Controller\PaymentCheckoutController::cancelPaymentPage'
_controller: '\Drupal\commerce_payment\Controller\OffsitePaymentController::returnCheckoutPage'
requirements:
_custom_access: '\Drupal\commerce_checkout\Controller\CheckoutController::checkAccess'
_module_dependencies: commerce_checkout
......@@ -74,10 +74,11 @@ commerce_payment.checkout.cancel:
parameters:
commerce_order:
type: entity:commerce_order
commerce_payment.checkout.return:
path: '/checkout/{commerce_order}/{step}/return'
commerce_payment.checkout.cancel:
path: '/checkout/{commerce_order}/{step}/cancel'
defaults:
_controller: '\Drupal\commerce_payment\Controller\PaymentCheckoutController::returnPaymentPage'
_controller: '\Drupal\commerce_payment\Controller\OffsitePaymentController::cancelCheckoutPage'
requirements:
_custom_access: '\Drupal\commerce_checkout\Controller\CheckoutController::checkAccess'
_module_dependencies: commerce_checkout
......@@ -85,3 +86,14 @@ commerce_payment.checkout.return:
parameters:
commerce_order:
type: entity:commerce_order
commerce_payment.notify:
path: '/payment/notify/{commerce_payment_gateway}'
defaults:
_controller: '\Drupal\commerce_payment\Controller\OffsitePaymentController::notifyPage'
requirements:
_access: 'TRUE'
options:
parameters:
commerce_payment_gateway:
type: entity:commerce_payment_gateway
......@@ -3,39 +3,62 @@
namespace Drupal\commerce_payment\Controller;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface;
use Drupal\Core\Access\AccessException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PaymentCheckoutController {
/**
* Provides endpoints for off-site payments.
*/
class OffsitePaymentController {
/**
* Controller callback for offsite payments cancelled.
* Provides the "return" checkout payment page.
*
* Redirects to the next checkout page, completing checkout.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $commerce_order
* The order.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
public function cancelPaymentPage(OrderInterface $commerce_order) {
public function returnCheckoutPage(OrderInterface $commerce_order, Request $request) {
/** @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());
try {
$payment_gateway_plugin->onReturn($commerce_order, $request);
$redirect_step = $checkout_flow_plugin->getNextStepId();
}
catch (PaymentGatewayException $e) {
\Drupal::logger('commerce_payment')->error($e->getMessage());
drupal_set_message(t('Payment failed at the payment server. Please review your information and try again.'), 'error');
$redirect_step = $checkout_flow_plugin->getPreviousStepId();
}
$checkout_flow_plugin->redirectToStep($redirect_step);
}
/**
* Controller callback for offsite payments returned.
* Provides the "cancel" checkout payment page.
*
* Redirects to the previous checkout page.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $commerce_order
* The order.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
public function returnPaymentPage(OrderInterface $commerce_order) {
public function cancelCheckoutPage(OrderInterface $commerce_order, Request $request) {
/** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
$payment_gateway = $commerce_order->payment_gateway->entity;
$payment_gateway_plugin = $payment_gateway->getPlugin();
......@@ -43,11 +66,38 @@ class PaymentCheckoutController {
throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class);
}
$payment_gateway_plugin->onRedirectReturn($commerce_order);
$payment_gateway_plugin->onCancel($commerce_order, $request);
/** @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());
$checkout_flow_plugin->redirectToStep($checkout_flow_plugin->getPreviousStepId());
}
/**
* Provides the "notify" page.
*
* Called the "IPN" or "status" page by some payment providers.
*
* @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $commerce_payment_gateway
* The payment gateway.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
public function notifyPage(PaymentGatewayInterface $commerce_payment_gateway, Request $request) {
$payment_gateway_plugin = $commerce_payment_gateway->getPlugin();
if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) {
throw new AccessException('Invalid payment gateway provided.');
}
$response = $payment_gateway_plugin->onNotify($request);
if (!$response) {
$response = new Response('', 200);
}
return $response;
}
}
......@@ -4,9 +4,9 @@ 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\Exception\DeclineException;
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;
......@@ -146,10 +146,16 @@ class PaymentProcess extends CheckoutPaneBase implements ContainerFactoryPluginI
$payment->payment_method = $this->order->payment_method->entity;
$payment_gateway_plugin->createPayment($payment, $this->configuration['capture']);
$this->checkoutFlow->redirectToStep($this->checkoutFlow->getNextStepId());
}
catch (DeclineException $e) {
$message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.');
drupal_set_message($message, 'error');
$this->redirectToPreviousStep();
}
catch (PaymentGatewayException $e) {
drupal_set_message($e->getMessage(), 'error');
\Drupal::logger('commerce_payment')->error($e->getMessage());
$message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.');
drupal_set_message($message, 'error');
$this->redirectToPreviousStep();
}
}
......@@ -160,19 +166,12 @@ class PaymentProcess extends CheckoutPaneBase implements ContainerFactoryPluginI
'#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';
}
$complete_form['actions']['next']['#value'] = $this->t('Proceed to @gateway', [
'@gateway' => $payment_gateway_plugin->getDisplayLabel(),
]);
return $pane_form;
}
else {
drupal_set_message($this->t('Sorry, we can currently only support on site payment gateways.'), 'error');
$this->redirectToPreviousStep();
}
}
/**
......
......@@ -4,6 +4,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides the base class for off-site payment gateways.
......@@ -13,8 +14,8 @@ abstract class OffsitePaymentGatewayBase extends PaymentGatewayBase implements O
/**
* {@inheritdoc}
*/
public function getRedirectCancelUrl(OrderInterface $order) {
return Url::fromRoute('commerce_payment.checkout.cancel', [
public function getReturnUrl(OrderInterface $order) {
return Url::fromRoute('commerce_payment.checkout.return', [
'commerce_order' => $order->id(),
'step' => 'payment',
], ['absolute' => TRUE]);
......@@ -23,11 +24,39 @@ abstract class OffsitePaymentGatewayBase extends PaymentGatewayBase implements O
/**
* {@inheritdoc}
*/
public function getRedirectReturnUrl(OrderInterface $order) {
return Url::fromRoute('commerce_payment.checkout.return', [
public function getCancelUrl(OrderInterface $order) {
return Url::fromRoute('commerce_payment.checkout.cancel', [
'commerce_order' => $order->id(),
'step' => 'payment',
], ['absolute' => TRUE]);
}
/**
* {@inheritdoc}
*/
public function getNotifyUrl() {
return Url::fromRoute('commerce_payment.notify', [
'commerce_payment_gateway' => $this->entityId,
], ['absolute' => TRUE]);
}
/**
* {@inheritdoc}
*/
public function onReturn(OrderInterface $order, Request $request) {}
/**
* {@inheritdoc}
*/
public function onCancel(OrderInterface $order, Request $request) {
drupal_set_message($this->t('You have canceled checkout at @gateway but may resume the checkout process here when you are ready.', [
'@gateway' => $this->getDisplayLabel(),
]));
}
/**
* {@inheritdoc}
*/
public function onNotify(Request $request) {}
}
......@@ -3,48 +3,103 @@
namespace Drupal\commerce_payment\Plugin\Commerce\PaymentGateway;
use Drupal\commerce_order\Entity\OrderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines the base interface for off-site payment gateways.
*
* Off-site payment flow:
* 1) Customer hits the "payment" checkout step.
* 2) The PaymentProcess checkout pane shows the "offsite-payment" plugin form.
* 3) The plugin form performs a redirect or shows an iFrame.
* 4) The customer provides their payment details to the payment provider.
* 5) The payment provider redirects the customer back to the return url.
* 6) A payment is created in either onReturn() or onNotify().
*
* If the payment provider supports asynchronous notifications (IPNs), then
* creating the payment in onNotify() is preferred, since it is guaranteed to
* be called even if the customer does not return to the site.
*
* If the customer declines to provide their payment details, and cancels
* the payment at the payment provider, they will be redirected back to the
* cancel url.
*/
interface OffsitePaymentGatewayInterface extends PaymentGatewayInterface {
/**
* Gets the URL for offiste redirect cancel.
* Gets the URL to the "return" page.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
*
* @return \Drupal\Core\Url
* The Url object
* The "return" page url.
*/
public function getRedirectCancelUrl(OrderInterface $order);
public function getReturnUrl(OrderInterface $order);
/**
* Gets the URL for offiste redirect return.
* Gets the URL to the "cancel" page.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
*
* @return \Drupal\Core\Url
* The Url object
* The "cancel" page url.
*/
public function getRedirectReturnUrl(OrderInterface $order);
public function getCancelUrl(OrderInterface $order);
/**
* Invoked when the off-site payment return.
* Gets the URL to the "notify" page.
*
* When supported, this page is called asynchronously to notify the site of
* payment changes (new payment or capture/void/refund of an existing one).
*
* @return \Drupal\Core\Url
* The "notify" page url.
*/
public function getNotifyUrl();
/**
* Processes the "return" request.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @throws \Drupal\commerce_payment\Exception\PaymentGatewayException
* Thrown when the request is invalid or the payment failed.
*/
public function onRedirectReturn(OrderInterface $order);
public function onReturn(OrderInterface $order, Request $request);
/**
* Invoked when the off-site payment way cancelled or failed.
* Processes the "cancel" request.
*
* Allows the payment gateway to clean up any data added to the $order, set
* a message for the customer.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
public function onCancel(OrderInterface $order, Request $request);
/**
* Processes the "notify" request.
*
* Note:
* This method can't throw exceptions on failure because some payment
* providers expect an error response to be returned in that case.
* Therefore, the method can log the error itself and then choose which
* response to return.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response|null
* The response, or NULL to return an empty HTTP 200 response.
*/
public function onRedirectCancel(OrderInterface $order);
public function onNotify(Request $request);
}
<?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();
}
......@@ -117,9 +117,6 @@ 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;
}
......
<?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.
}
}
<?php
namespace Drupal\commerce_payment\PluginForm;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the base class for payment off-site forms.
*
* Payment gateways are expected to inherit from this class and provide a
* buildConfigurationForm() method which calls buildRedirectForm() with the
* right parameters.
*/
abstract class PaymentOffsiteForm extends PaymentGatewayFormBase {
// The redirect methods.
const REDIRECT_GET = 'get';
const REDIRECT_POST = 'post';
/**
* Builds the redirect form.
*
* @param array $form
* The plugin form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $redirect_url
* The redirect url.
* @param array $data
* Data that should be sent along.
* @param string $redirect_method
* The redirect method (REDIRECT_GET or REDIRECT_POST constant).
*
* @return array
* The redirect form, if $redirect_method is REDIRECT_POST.
*
* @throws NeedsRedirectException
* The redirect exception, if $redirect_method is REDIRECT_GET.
*/
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';
foreach ($data as $key => $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' => '<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,
// Plugin forms are embedded using #process, so it's too late to attach
// another #process to $form itself, it must be on a sub-element.
'#process' => [
[get_class($this), 'processRedirectForm'],
],
'#action' => $redirect_url,
];
}
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 redirect.
*
* Sets the form #action, adds a class for the JS to target.
* Workaround for buildConfigurationForm() not receiving $complete_form.
*
* @param array $element
* The form element whose value is being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed form element.
*/
public static function processRedirectForm(array $element, FormStateInterface $form_state, array &$complete_form) {
$complete_form['#action'] = $element['#action'];
$complete_form['#attributes']['class'][] = 'payment-redirect-form';
unset($element['#action']);
return $element;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// Nothing. Off-site payment gateways do not submit forms to Drupal.
}
}
......@@ -10,7 +10,7 @@ use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
/**
* Tests the integration between payments and checkout.
* Tests the integration between off-site payments and checkout.
*
* @group commerce
*/
......@@ -77,7 +77,7 @@ class PaymentCheckoutOffsiteRedirectTest extends CommerceBrowserTestBase {
'plugin' => 'example_offsite_redirect',
]);
$gateway->getPlugin()->setConfiguration([
'method' => 'redirect_post',
'redirect_method' => 'post',
'payment_method_types' => ['credit_card'],
]);
$gateway->save();
......@@ -93,7 +93,7 @@ class PaymentCheckoutOffsiteRedirectTest extends CommerceBrowserTestBase {
}
/**
* Tests than an order can go through checkout steps.
* Tests the off-site redirect using the POST redirect method.
*/
public function testCheckoutWithOffsiteRedirectPost() {
$this->drupalGet($this->product->toUrl()->toString());
......@@ -116,7 +116,7 @@ class PaymentCheckoutOffsiteRedirectTest extends CommerceBrowserTestBase {
$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->submitForm([], 'Proceed to Example');
$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;
......@@ -129,13 +129,12 @@ class PaymentCheckoutOffsiteRedirectTest extends CommerceBrowserTestBase {
}
/**
* Tests the transaction mode in Authorize Only.
* Tests the off-site redirect using the GET redirect method.
*/
public function testCheckoutWithOffsiteRedirect302() {
// Set checkout flow to authorize only.
public function testCheckoutWithOffsiteRedirectGet() {
$payment_gateway = PaymentGateway::load('example_offsite_redirect');
$payment_gateway->getPlugin()->setConfiguration([
'method' => 'redirect_302',
'redirect_method' => 'get',
'payment_method_types' => ['credit_card'],
]);
$payment_gateway->save();
......
......@@ -170,7 +170,7 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase {
$this->assertSession()->pageTextContains('Order Summary');
$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('The payment was declined');
$this->assertSession()->pageTextContains('We encountered an error processing your payment method. Please verify your details and try again.');
$this->assertSession()->addressEquals('checkout/1/order_information');
// Verify a payment was not created.
......
......@@ -4,9 +4,10 @@ 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:
redirect_method:
type: string
label: 'Whether to use POST or 302 Redirect'
......@@ -5,17 +5,17 @@ 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;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides the Offsite Redirect payment gateway.
* Provides the Off-site Redirect payment gateway.
*
* @CommercePaymentGateway(
* id = "example_offsite_redirect",
* label = "Example Offsite (Redirect)",
* display_label = "Example Offsite (Redirect)",
* label = "Example (Off-site redirect)",
* display_label = "Example",
* forms = {
* "offsite-payment" = "Drupal\commerce_payment_example\PluginForm\Offsite\OffsiteRedirectPaymentForm",
* "offsite-payment" = "Drupal\commerce_payment_example\PluginForm\OffsiteRedirect\PaymentOffsiteForm",
* },
* payment_method_types = {"credit_card"},
* credit_card_types = {
......@@ -23,14 +23,14 @@ use Drupal\Core\Url;
* },
* )
*/
class OffsiteRedirectExample extends OffsitePaymentGatewayBase implements OffsiteRedirectExampleInterface {
class OffsiteRedirect extends OffsitePaymentGatewayBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'mode' => 'redirect_post',