Broadleaf Microservices
  • v1.0.0-latest-prod

3DS With MyFatoorah

Improved 3DS Pattern with 2.1.1-GA & Beyond

From the 2.1.1-GA ReleaseTrain version, the pattern to execute the 3DS interaction was improved. See the Improved 3DS/HPP pattern integration example for more information.

Legacy 3DS Pattern

Prerequisites

Before getting started…​

The Big Picture

When executing an Authorize or AuthorizeAndCapture transaction with the Embedded Payment pattern, MyFatoorah will always prompt for 3DS verification.

This means that when using MyFatoorah, you can expect the initial attempt to checkout to return a CheckoutResponse#failureType of PAYMENT_REQUIRES_3DS_VERIFICATION & the related payment transaction failure detail (CheckoutResponse#paymentTransactionFailureDetails) will have a failureType of PAYMENT_REQUIRES_3DS_VERIFICATION & a populated threeDSecureVerificationUrl. The user’s browser should be redirected to this url to complete the verification process.

Once the verification process is completed, the checkout attempt can be resubmitted where we’ll attempt to lookup the 3DS transaction results & continue the checkout process if the transaction was successful. Simultaneously, we also leverage webhook events as a secondary means of gathering & recording transaction results.

Note

MyFatoorah does not support webhook notifications for Authorize transactions. Due to the risk of the Broadleaf ecosystem not being aware of transaction results, we suggest using AuthorizeAndCapture transactions.

By default, MyFatoorah is configured to use separate Authorize and Capture interactions (MyFatoorah docs here). If you wish to enable AuthorizeAndCapture transactions (i.e. Authorize and Capture in a single request), then you must request this configuration via the MyFatoorah team. Keep in mind that if this change is made, you won’t be able to execute Authorize transactions.

Configuration

Cart Operation Service Configuration

As mentioned in the Broadleaf 3DS docs, one of the primary ways for understanding transaction results after 3DS verification is successfully completed.

To enable this transaction lookup in the checkout workflow, add the following required property:

broadleaf:
  cartoperation:
    service:
      checkout:
        checkout-lookup-3ds-transactions-enabled:
          MY_FATOORAH: true

Declaring the Return Url to be used after 3DS Verification

When the customer finishes the authentication process, the redirect sends them back to the declared return url.

An example of how to build this return URL from your frontend application:

const protocol = window.location.protocol.slice(0, -1);
const host = window.location.host;

const callBackUrl = `${protocol}://${host}/checkout/3ds-verification?gatewayType=MY_FATOORAH`
const errorUrl = `${protocol}://${host}/checkout/3ds-verification?3ds-verification-success=false`

The return & error urls should be declared when creating the payment in PaymentTransactionServices, using the callBackUrl & errorUrl keys in the paymentMethodProperties map.

{
  paymentMethodProperties: {
    ...
    callBackUrl: "https://{your_host}/checkout/3ds-verification?gatewayType=MY_FATOORAH",
    errorUrl: "https://{your_host}/checkout/3ds-verification?3ds-verification-success=false",
    ...
  }
}
Note
In our demo application, Heat Clinic, the frontend application handles the request to this return url, so that it becomes aware of the verification result & can progress to the next step in the checkout process. We recommend creating a separate page to check the 3DS verification result. In our example below, we use /checkout/3ds-verification page to do so.

Rendering the 3DS Verification Page & Handling the Return Url Request

When attempting a checkout, if you receive a CheckoutResponse with failureType = PAYMENT_REQUIRES_3DS_VERIFICATION, then you’ll need to gather the threeDSecureVerificationUrl from the CheckoutResponse#paymentTransactionFailureDetails & redirect to the 3DS verification page.

Once the verification step is completed, the browser will be redirected back to the callBackUrl specified in the paymentMethodProperties. If the verification was successful, then we suggest automatically submitting checkout. If the verification was not successful, then we suggest providing a message to the customer highlighting the failure & requesting a different form of payment.

Note
The /checkout/3ds-verification page is used to check the status of the 3DS authentication.
import React, { FC, useEffect, useState } from 'react';

import { useRouter } from 'next/router';
import { isEmpty, set } from 'lodash';

import { useHandleTransactionResult } from '@broadleaf/payment-react';

const ThreeDSecureVerification: FC = () => {
  const router = useRouter();
  const { query } = router;

  // the PaymentClient from '@broadleaf/commerce-cart'
  const paymentClient = ...;
  // the AuthState from '@broadleaf/payment-js'
  const authState = ...;

  const [transactionResult, setTransactionResult] =
    useState<Record<string, unknown>>();
  const { handleTransactionResult, error } = useHandleTransactionResult({
    paymentClient,
    authState,
  });

  const protocol = window.location.protocol.slice(0, -1);
  const host = window.location.host;
  const search = window.location.search;

  // Redirect to this page if the 3DS authentication is successful and this page is not opened in the iframe
  const verificationSuccessRedirectUrl = `${protocol}://${host}/checkout/review?3ds-verification-success=true`;
  // Redirect to this page if the 3DS authentication is failed
  const verificationFailureRedirectUrl = `${protocol}://${host}/checkout/review?3ds-verification-success=false`;
  // The 3DS verification can be successful, however, the 3DS result transaction itself (AUTHORIZE) can fail
  const transactionFailureRedirectUrl = `${protocol}://${host}/checkout/review?$3ds-transaction-success=false`;

  const threeDSVerificationResult = query['3ds-verification-success'];

  useEffect(() => {
    if (threeDSVerificationResult !== 'false') {
      const { gatewayType, ...request } = parseParams(search);

      handleTransactionResult(gatewayType, requestParams).then(response => {
        setTransactionResult(response);
      });
    } else {
      redirectTo(verificationFailureRedirectUrl);
    }
  }, [
    verificationFailureRedirectUrl,
    handleTransactionResult,
    router,
    search,
    threeDSVerificationResult,
  ]);

  const transactionStatus =
    transactionResult?.transactionExecutionDetails[0]?.transactionStatus;

  if (
    transactionResult &&
    transactionStatus !== 'SUCCESS' &&
    transactionStatus !== 'AWAITING_ASYNC_RESULTS'
  ) {
    redirectTo(transactionFailureRedirectUrl);
  }

  if (error) {
    /*
    if there's an error, we'll still submit the order after the timeout to leverage other 3DS
    verification fall back options, e.g. webhook & 3DS result lookup. The timeout is to ensure
    webhooks would be processed by the time we re-submit the order
     */
    console.error(
      'Error encountered while verifying 3DS transaction result: ',
      error
    );
    setTimeout(
      () => {
        // submit the order on the review page if '3ds-verification-success=true'
        redirectTo(verificationSuccessRedirectUrl);
      },
      3000
    );
  }

  if (
    transactionStatus === 'SUCCESS' ||
    transactionStatus === 'AWAITING_ASYNC_RESULTS'
  ) {
    // submit the order on the review page if '3ds-verification-success=true'
    redirectTo(verificationSuccessRedirectUrl);
  }

  // show any loader
  return <PageLoader loading={true} />;
};

const parseParams = (search?: string): Record<string, never> => {
  if (isEmpty(search)) {
    return {};
  }

  search = search?.startsWith('?') ? search.slice(1) : search;

  return search
    ?.split('&')
    .map(param => {
      // eslint-disable-next-line prefer-const
      let [key, value] = param.split('=');
      key = decodeURIComponent(key);
      return { key, value };
    })
    .reduce((accumulator, { key, value }) => {
      set(accumulator, key, value);
      return accumulator;
    }, {});
};

const redirectTo = redirectUrl => {
  window.location.href = redirectUrl;
};

Order Review Page

This page is used to review and submit the order. If the payment requires 3DS authentication, the submission will fail and the response will contain the bank’s page URL to verify 3DS. This URL can be used to redirect the customer.

import { FC, useEffect } from 'react';

import { useRouter } from 'next/router';
import { get, find } from 'lodash';

const CheckoutReview: FC = () => {
  const router = useRouter();
  const { query } = router;
  // resolve cart
  const { cart, resolving } = ...;

  // useHandleSubmitCart - your hook implementation to submit the order
  const { error, submitting, onSubmit } = useHandleSubmitCart();

  const threeDSVerificationResult = query['3ds-verification-success'];

  useEffect(() => {
    if (threeDSVerificationResult === 'true') {
      if (!resolving && cart) {
        // submit the order if the 3DS verification result is successful
        onSubmit();
      }

      if (error) {
        // delete '3ds-verification-success' parameter from the URL
        delete router.query['3ds-verification-success'];
        router.replace(router, undefined, { shallow: true });
      }
    }
  }, [threeDSVerificationResult, router, onSubmit, cart, error, resolving]);

  useEffect(() => {
    if ( get(error, 'failureType') === 'PAYMENT_REQUIRES_3DS_VERIFICATION') {
      const threeDSDetails = find(
        get(error, 'paymentTransactionFailureDetails'),
        ({ failureType }) => failureType === 'REQUIRES_3DS_VERIFICATION'
      );

      const verificationUrl = get(
        threeDSDetails,
        'threeDSecureVerificationUrl'
      );

      if (verificationUrl) {
        // redirect to the 3DS verification page
        router.push(verificationUrl);
      }
    }
  }, [error, router]);

  const threeDSVerificationFailedError =
    router.query['3ds-verification-success'] === 'false'
      ? '3DS verification failed'
      : undefined;

  const threeDSTransactionFailedError =
    router.query['3ds-transaction-success'] === 'false'
      ? 'We are unable to process your payment'
      : undefined;
  return (
    <div>
      {threeDSVerificationFailedError && (
        <strong className="block my-4 text-red-600 text-lg font-normal">
          {threeDSVerificationFailedError}
        </strong>
      )}
      {threeDSTransactionFailedError && (
        <strong className="block my-4 text-red-600 text-lg font-normal">
          {threeDSTransactionFailedError}
        </strong>
      )}

      ...

      <SubmitButton
          id="submit-order-button"
          disabled={submitting}
          label='Submit Order'
          onClick={onSubmit}
          type="button"
        />
    </div>
  );
};

Using Webhooks to Gather 3DS Transaction Results

As part of the MyFatoorah 3DS integration, we listen to TransactionsStatusChanged webhook events to gather transaction results once the customer verifies their ownership of the payment method.

For more information on how to configure these required webhooks in your environment, see the webhook setup notes in the environment setup guide.

Important
The TransactionsStatusChanged is only triggered for AuthorizeAndCapture transactions. When Authorize transactions are used during checkout, MyFatoorah doesn’t send a webhook event notifying us of the transaction results. Because of this, we recommend using AuthorizeAndCapture transactions during checkout.
Note
For more details on how Broadleaf makes use of webhooks as part of our overall 3DS pattern, take a look at our gateway-agnostic 3DS documentation.