broadleaf:
cartoperation:
service:
checkout:
checkout-lookup-3ds-transactions-enabled:
STRIPE: true
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.
Before getting started…
Familiarize yourself with Broadleaf’s 3DS patterns.
Familiarize yourself with the Stripe 3DS documentation.
When an Authorize or AuthorizeAndCapture transaction is attempted with Stripe, Radar Rules are used to determine if 3DS verification is required before the transaction is permitted.
This means that when using Stripe, you should always attempt your checkout transactions as you normally would, but if 3DS verification is required, then the CheckoutResponse#failureType
will be 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.
Stripe has built-in rules related 3DS, but you can customize the rules via the Stripe dashboard, if you wish to do so.
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:
STRIPE: true
As part of the Stripe 3DS solution, we use webhooks to ensure that transaction results are known by the Broadleaf ecosystem.
In the Stripe Dashboard, declare the webhook url using the following structure to hit the PaymentTransactionServices webhook endpoint: https://${host}/api/payment/webhooks/STRIPE
Note
|
Make sure to replace ${host} with the value relevant to your environment
|
To ensure the validity of inbound requests, we confirm that the request’s signature matches the value that we calculate using a secret key provided by Stripe. To define this key in your Broadleaf ecosystem, the following property must be declared:
broadleaf:
stripe:
rest:
webhook-endpoint-secret: {Your webhook endpoint secret}
Note
|
If your solution requires different Stripe accounts per tenant or per application, then you’ll want to leverage the ability to provide application-discriminated and/or tenant-discriminated values for the webhook secret. For example:
"mytenant" & "myapplication" being the Broadleaf Tenant and Application ids. To correctly identify & engage these application-discriminated and/or tenant-discriminated webhook secrets, you’ll need to pass the relevant application and/or tenant ids as parameters via the webhook requests. This should be done by defining the webhook url using the following parameters: |
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 paymentIntentReturnURL = `${protocol}://${host}/checkout/3ds-verification?gatewayType=STRIPE`
The return url should be declared when creating the payment in PaymentTransactionServices, using the PAYMENT_INTENT_RETURN_URL
key in the paymentMethodProperties
map.
{
paymentMethodProperties: {
...
PAYMENT_INTENT_RETURN_URL: "https://{your_host}/checkout/3ds-verification?gatewayType=STRIPE",
...
}
}
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.
|
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
& render the 3DS verification page. Depending on your needs, it’s possible to redirect to this url or use an iframe. The example below supports both approaches.
Once the verification step is completed, the browser will be redirected back to the location declared by Payment#paymentMethodProperties#PAYMENT_INTENT_RETURN_URL
. 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, omit } 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);
const requestParams = omitRequestParamsForGatewayType(
gatewayType,
request
);
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(
() => redirectOrSubmitOrder(verificationSuccessRedirectUrl),
3000
);
}
if (
transactionStatus === 'SUCCESS' ||
transactionStatus === 'AWAITING_ASYNC_RESULTS'
) {
redirectOrSubmitOrder(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 redirectOrSubmitOrder = verificationSuccessRedirectUrl => {
if (isOpenedInIframe()) {
// submit the order if this page is opened in the iframe
// 'submit-order-button' - the id of the button to submit the order
window.parent.document.getElementById('submit-order-button').click();
} else {
// the order will be submitted on the review page if '3ds-verification-success=true'
window.location.href = verificationSuccessRedirectUrl;
}
};
const redirectTo = redirectUrl => {
if (isOpenedInIframe()) {
window.parent.location.href = redirectUrl;
} else {
window.location.href = redirectUrl;
}
};
const isOpenedInIframe = (): boolean => {
return window !== window.top;
};
const omitRequestParamsForGatewayType = (
gatewayType: string,
requestParams?: Record<string, string>
): Record<string, string> => {
if ('STRIPE' === gatewayType) {
// do not send the client secret key
return omit(requestParams, ['payment_intent_client_secret']);
}
return requestParams;
};
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 or load the page in the iframe.
import { FC, useState, 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 [threeDSecureVerificationUrl, setThreeDSecureVerificationUrl] =
useState<string>();
const threeDSVerificationResult = query['3ds-verification-success'];
useEffect(() => {
if (threeDSVerificationResult === 'true') {
if (!resolving && cart) {
// submit the order if the 3DS verification result is successful
// this is used when the redirect is used instead of iframe
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) {
// isRedirectToBankPage - your implementation that is used to control whether the customer is redirected to the bank's page or it is loaded in the iframe
if (isRedirectToBankPage()) {
// redirect to the 3DS verification page instead of using the iframe
router.push(verificationUrl);
} else {
// this URL will be loaded using the iframe in the modal dialog
setThreeDSecureVerificationUrl(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"
/>
<Modal
setIsOpen={() => setThreeDSecureVerificationUrl(undefined)}
isOpen={!!threeDSecureVerificationUrl && !submitting}
title='D-Secure Authentication'
onCancel={() => {
setThreeDSecureVerificationUrl(undefined);
}}
>
<iframe
id="3ds"
src={threeDSecureVerificationUrl}
className="flex-grow sm:w-full"
height="500"
/>
</Modal>
</div>
);
};