Off-site (redirect) payment gateways

This documentation will explain how to set up an off-site payment gateway. Off-site payment is enabled through a redirect from the Payment checkout page to the payment service, with customers ideally being returned back to the Payment page upon success or failure so they can be moved forward or backward in the checkout process as the case may require.

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.

We'll continue with the QuickPay payment gateway example that was started in the Creating payment gateways documentation. In that example, we created a custom module, configuration schema, payment plugin, and configuration form methods. With the configuration all set up, we can proceed to configure the checkout.

Checkout

In the annotation for the QuickPay payment gateway plugin (RedirectCheckout), we defined the offsite-payment form class:

 *    forms = {
 *     "offsite-payment" = "Drupal\commerce_quickpay\PluginForm\RedirectCheckoutForm",
 *   },

This defines a form that Drupal Commerce will redirect to, when the user clicks the Pay and complete purchase button:

Pay and complete purchase

We only need to implement one method, buildConfigurationForm(), for the RedirectCheckoutForm form. This is a pretty straightforward Drupal form, and it should not hold any surprises. For this example, we will set a lot of hidden fields and automatically redirect the user to QuickPay.

<?php

namespace Drupal\commerce_quickpay\PluginForm;

use Drupal\commerce_payment\PluginForm\PaymentOffsiteForm;
use Drupal\Core\Form\FormStateInterface;

class RedirectCheckoutForm extends PaymentOffsiteForm {

  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $configuration = $this->getConfiguration();

    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
    $payment = $this->entity;

    $data['version'] = 'v10';
    $data['private_key'] = $configuration['private_key'];
    $data['api_key'] = $configuration['api_key'];

    return $this->buildRedirectForm(
      $form,
      $form_state,
      'https://payment.quickpay.net',
      $data,
      PaymentOffsiteForm::REDIRECT_POST
    );
  }
}

Again remember that this is just for illustration purposes; the real QuickPay implementation requires a lot more details.

That completes our Checkout implementation. Next, we need to handle the returning user.

Return from payment provider

When the user returns from the payment provider, we need to validate that the payment actually succeeded. To do this, we'll implement the onReturn() method in the RedirectCheckout class. If the payment failed, the method should throw a PaymentGatewayException. This will reset the payment.

Payment error message

If the payment was successful, the method should create a payment and store it:

public function onReturn(OrderInterface $order, Request $request) {
    if ($request->something_that_marks_a_failure) {
        throw new PaymentGatewayException('Payment failed!');
    }

    $payment_storage = $this->entityTypeManager->getStorage('commerce_payment');
    $payment = $payment_storage->create([
      'state' => 'completed',
      'amount' => $order->getTotalPrice(),
      'payment_gateway' => $this->entityId,
      'order_id' => $order->id(),
      'remote_id' => $request->request->get('remote_id'),
      'remote_state' => $request->request->get('remote_state'),
    ]);

    $payment->save();
}

In this example code, we've simply used if ($request->something_that_marks_a_failure). In an actual implementation, you would need to use logic specific to your payment provider and do comprehensive error-checking. Typically, you will also want to log the information returned by the provider. See How to Log Messages in Drupal 8 for more information.

Also, note that the payment provider you are integrating with might have different ways to complete a payment. Some payment providers, including QuickPay, will not pass any sensitive information in the request when returning. You may need to implement a callback method or submit a request for additional information from the provider.

And that's it. You should now be able to implement checkouts for the off-site payment provider of your choice.

Found errors? Think you can improve this documentation? edit this page