import AdyenCheckout from '@adyen/adyen-web';
import DropinElement from '@adyen/adyen-web/dist/types/components/Dropin';
import UIElement from '@adyen/adyen-web/dist/types/components/UIElement';
import '@adyen/adyen-web/dist/adyen.css';
The server-side supports the Sessions flow with additional methods implementation. This documentation contains an example of the client-side integration using Web Drop-in
solution with React js
.
See Start integrating with Web Drop-in to learn more.
Use the Get Adyen Web guide to install the Drop-in
package to your application.
Note
|
The examples shown in this guide are built using the 5.64.0 version of Adyen’s Drop-in library. |
import AdyenCheckout from '@adyen/adyen-web';
import DropinElement from '@adyen/adyen-web/dist/types/components/Dropin';
import UIElement from '@adyen/adyen-web/dist/types/components/UIElement';
import '@adyen/adyen-web/dist/adyen.css';
The Adyen
payment form creates a payment session using the POST /api/payment/adyen/create-session
endpoint.
After that, the session id
and sessionData
are used in the configuration object for the Drop-in
component that then is mounted to the <div id="dropin-container"></div>
DOM container element.
When customer type the payment details and clicks Pay
button, the new Payment
is created in PaymentTransactionServices
and the request to process checkout POST /api/cart-operations/checkout/${cartId}/process
is sent.
NOTE:
The Drop-in component returns the state
object that contains data
. This data
contains the sensitive information that shouldn’t be stored but is required to execute the payment transaction. The process checkout
request has an ability to send such data in the request body. For example:
await submitCart({
sensitivePaymentMethodData: [
{
paymentId: paymentSummary.paymentId,
paymentMethodProperties: {
ADYEN_PAYMENT_DATA: {
// data object returned by Drop-in component
...data,
// customer email address
shopperEmail
},
},
},
],
});
NOTE
: The following example redirects the customer to the order confirmation page after receiving a successful 3DS authentication result. See Frontend Changes for details on how it can be implemented.
import { FC, useState, useEffect } from 'react';
import { isNil, join } from 'lodash';
import AdyenCheckout from '@adyen/adyen-web';
import DropinElement from '@adyen/adyen-web/dist/types/components/Dropin';
import {
ShopperDetails,
PaymentMethodOptions,
} from '@adyen/adyen-web/dist/types/types';
import UIElement from '@adyen/adyen-web/dist/types/components/UIElement';
import '@adyen/adyen-web/dist/adyen.css';
import {
DefaultPaymentType,
PaymentMethodOptionResponse,
UpdatePaymentRequest,
} from '@broadleaf/commerce-cart';
import { useFormatMessage, usePaymentAuthState } from '@app/common/hooks';
import { AdyenConfig } from '@app/common/utils';
import {
useAdyenPaymentServicesClient,
usePaymentClient,
} from '@app/common/contexts';
import { useAdyenCreatePaymentSessionRequest } from '@broadleaf/adyen-payment-services-react';
import {
CreatePaymentSessionRequest,
CreatePaymentSessionResponse,
} from '@broadleaf/adyen-payment-services-api';
import {
useFormatAmountInMinorUnits,
useGetPaymentAmount,
useGetPaymentCallbackUrl,
useHandleMoneyAddition,
} from '@app/checkout/hooks';
import { useCartContext } from '@app/cart/contexts';
import {
useHandleOnPaymentCompleted,
useHandleSubmitAdyenPayment,
} from './adyen-hooks';
import { useSubmitPaymentRequest } from '@broadleaf/payment-react';
import { Address } from '@broadleaf/commerce-customer';
import messages from '@app/checkout/messages';
type Props = {
paymentOptions: Array<PaymentMethodOptionResponse>;
};
type AdyenPaymentFormType = FC<Props> & {
TYPE: 'ADYEN';
};
export const AdyenPaymentForm: AdyenPaymentFormType = () => {
const formatMessage = useFormatMessage();
const { cart } = useCartContext();
const authState = usePaymentAuthState();
const adyenPaymentServicesClient = useAdyenPaymentServicesClient();
const { createPaymentSession, error: createPaymentSessionError } =
useAdyenCreatePaymentSessionRequest({
adyenPaymentServicesClient,
authState,
});
const [session, setSession] = useState<CreatePaymentSessionResponse>();
const [dropinComponent, setDropinComponent] = useState<DropinElement>();
const paymentClient = usePaymentClient();
const { handleUpdatePaymentInfo } = useSubmitPaymentRequest({
authState,
payments: undefined,
ownerId: cart?.id,
owningUserEmailAddress: cart.emailAddress,
paymentClient,
multiplePaymentsAllowed: false,
rejectOnError: true,
});
const getPaymentAmount = useGetPaymentAmount({
gatewayType: AdyenPaymentForm.TYPE,
});
const getPaymentCallbackUrl = useGetPaymentCallbackUrl();
const [sessionIsCreating, setSessionIsCreating] = useState<boolean>(false);
const formatAmountInMinorUnits = useFormatAmountInMinorUnits();
const handleMoneyAddition = useHandleMoneyAddition();
useEffect(() => {
if (!session && cart && !sessionIsCreating) {
setSessionIsCreating(true);
const amount = getPaymentAmount();
const returnUrl = getPaymentCallbackUrl({
gatewayType: AdyenPaymentForm.TYPE,
});
const countryCode = cart.fulfillmentGroups[0]?.address?.country;
const request = {
amount,
reference: cart.id,
returnUrl,
countryCode,
//hardcode, backend will know what to do with that when customer logged in
recurringProcessingModel: 'CardOnFile',
storePaymentMethodMode: 'askForConsent',
shopperEmail: authState.isAuthenticated
? authState.customerEmail
: cart.emailAddress,
} as CreatePaymentSessionRequest;
createPaymentSession(request)
.then(response => setSession(response))
.finally(() => setSessionIsCreating(false));
}
}, [
authState,
cart,
session,
setSession,
createPaymentSession,
getPaymentAmount,
getPaymentCallbackUrl,
sessionIsCreating,
setSessionIsCreating,
formatAmountInMinorUnits,
handleMoneyAddition,
]);
const [error, setError] = useState();
const handleOnSubmit = useHandleSubmitAdyenPayment({ setError });
const handleOnPaymentCompleted = useHandleOnPaymentCompleted({
setUIElement: setDropinComponent,
});
const [paymentData, setPaymentData] = useState();
const [submitState, setSubmitState] = useState();
const countryCode = cart?.fulfillmentGroups[0]?.address?.country || 'US';
useEffect(() => {
if (session && !dropinComponent && !sessionIsCreating) {
const amount = getPaymentAmount();
const adyenAmountInMinorUnits = {
value: formatAmountInMinorUnits(amount),
currency: amount.currency,
};
const configuration = {
environment: 'test',
clientKey: AdyenConfig.CLIENT_KEY,
analytics: {
enabled: false,
},
session: {
id: session.id, // Unique identifier for the payment session.
sessionData: session.sessionData, // The payment session data.
},
paymentMethodsConfiguration: {
card: {
hasHolderName: true,
holderNameRequired: true,
billingAddressRequired: true,
},
},
onSubmit: (state, dropin) => {
const paymentType = PaymentTypeHelper.getPaymentType();
handleOnSubmit({
state,
paymentType: PaymentTypeHelper.getPaymentType(),
component: dropin,
sessionId: session.id,
}).then(paymentSummary => {
paymentSummaryStore.setValue(paymentSummary);
});
},
onPaymentCompleted: handleOnPaymentCompleted,
onError: (error, component) => {
console.error(error.name, error.message, error.stack);
setError(error);
},
};
AdyenCheckout(configuration).then(checkout => {
const dComponent = checkout
.create('dropin', {
showStoredPaymentMethods: true,
onSelect: (dropin: UIElement) => {
if (dropin.type.toLowerCase() === 'card') {
PaymentTypeHelper.setPaymentType(
DefaultPaymentType.CREDIT_CARD
);
}
},
})
.mount('#dropin-container');
setDropinComponent(dComponent);
});
}
return () => {
if (dropinComponent) {
dropinComponent.unmount();
setDropinComponent(undefined);
}
};
// eslint-disable-next-line
}, [
session,
countryCode,
dropinComponent,
setDropinComponent,
sessionIsCreating,
handleOnSubmit,
handleOnPaymentCompleted,
getPaymentAmount,
formatAmountInMinorUnits,
PaymentTypeHelper,
]);
return (
<div>
<h2 className="py-4 text-xl font-medium">
{formatMessage(messages.paymentMethod)}
</h2>
<div id="dropin-container"></div>
{(createPaymentSessionError || error) && (
<strong className="block my-4 text-red-600 text-lg font-normal">
{formatMessage(messages.genericError)}
</strong>
)}
</div>
);
};
const createPaymentSummaryStore = () => {
let value = null;
return {
setValue(newValue: unknown) {
value = newValue;
},
getValue() {
return value;
},
};
};
const paymentSummaryStore = createPaymentSummaryStore();
const createPaymentTypeHolder = () => {
let paymentType = 'CREDIT_CARD';
return {
getPaymentType() {
return paymentType;
},
setPaymentType(type: string) {
paymentType = type;
},
};
};
const PaymentTypeHelper = createPaymentTypeHolder();
AdyenPaymentForm.TYPE = 'ADYEN';
export const useHandleSubmitAdyenPayment = (
props: HandleSubmitProps = {}
): HandleSubmitResponse => {
const { setError = noop } = props;
const cartState = useCartContext();
const { cart } = cartState;
const paymentClient = usePaymentClient();
const authState = usePaymentAuthState();
const { handleSubmitPaymentInfo } = useSubmitPaymentRequest({
authState,
payments: undefined,
ownerId: cart?.id,
owningUserEmailAddress: cart.emailAddress,
paymentClient,
multiplePaymentsAllowed: false,
rejectOnError: true,
});
const getPaymentAmount = useGetPaymentAmount({
gatewayType: AdyenPaymentForm.TYPE,
});
const getPaymentCallbackUrl = useGetPaymentCallbackUrl();
const { error, onSubmit: submitCart } = useHandleSubmitCart();
const [adyenComponent, setAdyenComponent] = useState<UIElement>();
useEffect(() => {
if (!adyenComponent) {
return;
}
const errorType = get(error, 'failureType');
if (
errorType === 'PAYMENT_REQUIRES_3DS_VERIFICATION' ||
errorType === 'PAYMENT_REQUIRES_EXTERNAL_INTERACTION' ||
errorType === 'PAYMENT_REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION'
) {
const errorDetails = find(
get(error, 'paymentTransactionFailureDetails'),
({ failureType }) =>
failureType === 'REQUIRES_3DS_VERIFICATION' ||
failureType === 'REQUIRES_EXTERNAL_INTERACTION' ||
failureType === 'REQUIRES_HOSTED_PAYMENT_PAGE_INTERACTION'
);
const action = get(errorDetails, 'nextAction.attributes');
adyenComponent.handleAction(action);
} else if (errorType && error) {
setError(error);
}
}, [error, adyenComponent, setError]);
const getLineItems = useGetLineItems();
return useEventCallback(
async ({
state,
paymentType,
component,
sessionId,
paymentData,
}: HandleSubmitParameters) => {
setError(undefined);
const data = get(state, 'data');
setAdyenComponent(component);
const billingAddress = getBillingAddress(state, paymentType, paymentData);
// these are required to pass to the backend as part of the payment
const returnUrl = getPaymentCallbackUrl({
gatewayType: AdyenPaymentForm.TYPE,
});
const amount = getPaymentAmount(cart);
const paymentRequest = {
name: AdyenPaymentForm.TYPE,
type: paymentType,
gatewayType: AdyenPaymentForm.TYPE,
amount,
subtotal: cart.cartPricing.subtotal,
adjustmentsTotal: cart.cartPricing.adjustmentsTotal,
fulfillmentTotal: cart.cartPricing.fulfillmentTotal,
taxTotal: cart.cartPricing.totalTax,
isSingleUsePaymentMethod: true,
shouldArchiveExistingPayments: true,
paymentMethodProperties: {
sessionId,
returnUrl,
},
billingAddress,
} as PaymentRequest;
let paymentSummary;
try {
paymentSummary = await handleSubmitPaymentInfo(paymentRequest);
if (paymentSummary) {
pushGtmAddPayment(cart, paymentSummary);
const countryCode = cart.fulfillmentGroups[0]?.address?.country;
const lineItems = getLineItems(cart);
const checkoutResponse = await submitCart({
sensitivePaymentMethodData: [
{
paymentId: paymentSummary.paymentId,
paymentMethodProperties: {
ADYEN_PAYMENT_DATA: {
...data,
shopperEmail: authState.isAuthenticated
? authState.customerEmail
: cart.emailAddress,
lineItems,
countryCode: countryCode,
},
},
},
],
});
return checkoutResponse?.paymentSummaries[0];
}
} catch (err) {
console.error('There was an error adding payment information', err);
setError(err);
}
},
[]
);
};
export const useHandleOnPaymentCompleted = (
props: PaymentCompletedProps = {}
) => {
const { setUIElement } = props;
const router = useRouter();
const { cart } = useCartContext();
const authState = usePaymentAuthState();
return useEventCallback(async (result, element: UIElement) => {
const resultCode = get(result, 'resultCode');
if ('Authorised' === resultCode) {
const emailAddress = authState.isAuthenticated
? undefined
: cart.emailAddress;
await router.push({
pathname: '/checkout/payment-confirmation',
query: {
cart_id: cart.id,
email_address: emailAddress,
payment_finalization_status: 'FINALIZED',
payment_result_status: 'SUCCESS',
gateway_type: AdyenPaymentForm.TYPE,
},
});
} else {
element.unmount();
setUIElement && setUIElement(undefined);
await router.push({
pathname: '/checkout/payment',
query: {
payment_finalization_status: 'REQUIRES_PAYMENT_MODIFICATION',
payment_result_status:
'Cancelled' === resultCode ? 'PAYMENT_CANCELED' : 'PAYMENT_FAILED',
gateway_type: AdyenPaymentForm.TYPE,
},
});
}
}, []);
};
The form above uses the Adyen’s form for the billing address. If you need to use your own form, make the next changes:
const AdyenPaymentForm: AdyenPaymentFormType = () => {
...
const [usingStoredPaymentMethod, setUsingStoredPaymentMethod] =
useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// the Ref is used to get the Formik form values
const formikBillingAddressRef = useRef();
useEffect(() => {
if (session && !dropinComponent && !sessionIsCreating) {
const configuration = {
...
paymentMethodsConfiguration: {
card: {
hasHolderName: true,
holderNameRequired: true,
},
},
onSubmit: (state, dropin) => {
setIsSubmitting(true);
// get the billing address from the Formik
const adyenBillingAddress = get(formikBillingAddressRef, 'current.values');
handleOnSubmit(
state,
dropin,
session.id,
paymentType,
adyenBillingAddress
).finally(() => {
setIsSubmitting(false);
});
},
...
};
AdyenCheckout(configuration).then(checkout => {
const dComponent = checkout
.create('dropin', {
showStoredPaymentMethods: true,
onSelect: (dropin: UIElement) => {
...
const storedPaymentMethodId = get(
dropin.data,
'paymentMethod.storedPaymentMethodId'
);
// determine if the stored payment method is selected
setUsingStoredPaymentMethod(!!storedPaymentMethodId);
},
})
.mount('#dropin-container');
setDropinComponent(dComponent);
});
}
...
}, [
session,
dropinComponent,
setDropinComponent,
sessionIsCreating,
handleOnSubmit,
handleOnPaymentCompleted,
setIsSubmitting,
formikBillingAddressRef,
setUsingStoredPaymentMethod,
paymentType,
setPaymentType,
]);
return (
<div>
...
{/* Add the Billing address form. When the saved payment method is used, the billing address is not required */}
{!usingStoredPaymentMethod && (
<BillingAddressForm
innerRef={formikBillingAddressRef}
isSubmitting={isSubmitting}
/>
)}
...
</div>
);
};
const BillingAddressForm = ({ innerRef, isSubmitting }) => {
return (
<Formik
innerRef={innerRef}
initialValues={...}
validateOnBlur
validateOnChange={false}
onSubmit={() => {
//do nothing
}}
>
{() => (
<div>
{/* Billing address form. */}
</div>
)}
</Formik>
);
};
type AddressWithStreetAndHouseNumber = {
// Adyen billing address format
};
const useHandleOnSubmit = ({ setError }) => {
...
return useEventCallback(
async (
state,
dropin: DropinElement,
sessionId: string,
paymentType: string,
adyenBillingAddress?: AddressWithStreetAndHouseNumber
) => {
...
let blcBillingAddress: Address;
if (!isEmpty(adyenBillingAddress)) {
// convert "adyenBillingAddress" to the Address
}
const paymentRequest = {
...,
billingAddress: blcBillingAddress,
} as PaymentRequest;
let paymentSummary;
try {
paymentSummary = await handleSubmitPaymentInfo(paymentRequest);
if (paymentSummary) {
await submitCart({
sensitivePaymentMethodData: [
{
paymentId: paymentSummary.paymentId,
paymentMethodProperties: {
ADYEN_PAYMENT_DATA: {
...data,
billingAddress: adyenBillingAddress,
shopperEmail: authState.isAuthenticated
? authState.customerEmail
: cart.emailAddress,
},
},
},
],
});
}
} catch (err) {
...
}
},
[]
);
};
It is possible to save the payment method for future use during checkout or use a separate page where the authenticated customer can create/delete the payment methods.
We do not store the payment methods for Adyen as it has an ability to do so. We only have 2 API endpoints to read the payment methods - GET /api/payment/adyen/saved-payment-methods
, and remove it - DELETE /api/payment/adyen/saved-payment-methods/{savedPaymentMethodId}
.
You can use the SDK hooks for these endpoints. For example:
import { useAdyenPaymentServicesClient } from '@app/common/contexts';
import {
useAdyenListSavedPaymentMethods,
useAdyenDeleteSavedPaymentMethod,
} from '@broadleaf/adyen-payment-services-react';
...
const authState = usePaymentAuthState();
const adyenPaymentServicesClient = useAdyenPaymentServicesClient();
// list the saved payment methods
const { adyenSavedPaymentMethods, listSavedPaymentMethods } =
useAdyenListSavedPaymentMethods({
adyenPaymentServicesClient,
authState,
});
// delete the saved payment method
const { adyenDeleteSavedPaymentMethod } = useAdyenDeleteSavedPaymentMethod({
adyenPaymentServicesClient,
authState,
});
To store the new payment method (e.g. in a My Account context) we recommend using the Drop-in component with Sessions flow.
import { FC, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
const { v4 } = require('uuid');
import { usePaymentAuthState } from '@app/common/hooks';
import {
CreatePaymentSessionRequest,
CreatePaymentSessionResponse,
} from '@broadleaf/adyen-payment-services-api';
import { useAdyenCreatePaymentSessionRequest } from '@broadleaf/adyen-payment-services-react';
import AdyenCheckout from '@adyen/adyen-web';
import DropinElement from '@adyen/adyen-web/dist/types/components/Dropin';
import '@adyen/adyen-web/dist/adyen.css';
import {
useAdyenPaymentServicesClient,
useCurrencyContext,
useLocationContext,
} from '@app/common/contexts';
import { OnPaymentCompletedData } from '@adyen/adyen-web/dist/types/components/types';
export const AdyenSavedPaymentForm: FC = () => {
const { replace } = useRouter();
const { push } = useRouter();
const authState = usePaymentAuthState();
const {
session,
sessionIsCreating,
error: createPaymentSessionError,
} = useCreateSession();
const [dropinComponent, setDropinComponent] = useState<DropinElement>();
const [error, setError] = useState<boolean>(false);
const { currentCurrency } = useCurrencyContext();
useEffect(() => {
if (
authState.isAuthenticated &&
session &&
!dropinComponent &&
!sessionIsCreating
) {
// zero amount authorization
const amount = {
value: 0.0,
currency: currentCurrency || 'USD',
};
const configuration = {
setStatusAutomatically: true,
environment: 'test',
clientKey: '{Client Key}',
analytics: {
enabled: false,
},
session: {
id: session.id, // Unique identifier for the payment session.
sessionData: session.sessionData, // The payment session data.
},
paymentMethodsConfiguration: {
card: {
name: 'Credit or debit card',
maskSecurityCode: true,
amount: amount,
hasHolderName: true,
holderNameRequired: true,
billingAddressRequired: true,
},
},
onPaymentCompleted: (data: OnPaymentCompletedData) => {
const resultCode = data.resultCode;
if (
resultCode === 'Authorised' ||
resultCode === 'Received' ||
resultCode === 'Pending'
) {
push('/my-account/payments');
} else {
setError(true);
}
},
onError: (err, component) => {
console.error(err.name, err.message, err.stack, component);
setError(true);
},
};
AdyenCheckout(configuration).then(checkout => {
const dComponent = checkout
.create('dropin', { showStoredPaymentMethods: false })
.mount('#dropin-container');
setDropinComponent(dComponent);
});
}
return () => {
if (dropinComponent) {
dropinComponent.unmount();
setDropinComponent(undefined);
}
};
}, [
session,
dropinComponent,
setDropinComponent,
sessionIsCreating,
push,
authState.isAuthenticated,
currentCurrency,
]);
if (!authState.isAuthenticated) {
replace('my-account/sign-in');
return null;
}
return (
<div className="container flex flex-col mx-auto px-4 py-8 lg:flex-row xl:px-0">
<div className="flex-1 px-4 lg:px-0">
<h2 className="text-4xl font-bold">Add new Payment Method</h2>
<div id="dropin-container"></div>
{(createPaymentSessionError || error) && (
<strong className="block my-4 text-red-600 text-lg font-normal">
An unexpected error occurred. Please try again later.
</strong>
)}
</div>
</div>
);
};
const useCreateSession = () => {
const authState = usePaymentAuthState();
const [session, setSession] = useState<CreatePaymentSessionResponse>();
const [sessionIsCreating, setSessionIsCreating] = useState<boolean>(false);
const { protocol, host } = useLocationContext();
const returnUrl = `${protocol}://${host}/my-account/payments`;
const { currentCurrency } = useCurrencyContext();
const adyenPaymentServicesClient = useAdyenPaymentServicesClient();
const { createPaymentSession, error } = useAdyenCreatePaymentSessionRequest({
adyenPaymentServicesClient,
authState,
});
useEffect(() => {
if (authState.isAuthenticated && !session && !sessionIsCreating) {
setSessionIsCreating(true);
const amount = {
amount: 0,
currency: currentCurrency || 'USD',
};
const countryCode = 'US';
const request = {
amount: amount,
reference: v4(),
returnUrl,
countryCode,
shopperEmail: authState.customerEmail,
//hardcode, backend will know what to do with that
recurringProcessingModel: 'CardOnFile',
storePaymentMethodMode: 'enabled',
} as CreatePaymentSessionRequest;
createPaymentSession(request)
.then(response => setSession(response))
.finally(() => setSessionIsCreating(false));
}
}, [
authState,
session,
setSession,
createPaymentSession,
sessionIsCreating,
setSessionIsCreating,
returnUrl,
currentCurrency,
]);
return {
session,
sessionIsCreating,
error,
};
};
The form above uses the Adyen’s form for the billing address. If you need to use your own form, make the next changes:
export const AdyenSavedPaymentForm: FC<Props> = props => {
...
useEffect(() => {
if (
authState.isAuthenticated &&
session &&
!dropinComponent &&
!sessionIsCreating
) {
const amount = {
value: 0.0,
currency: currentCurrency || 'USD',
};
const configuration = {
...
paymentMethodsConfiguration: {
card: {
name: 'Credit or debit card',
maskSecurityCode: true,
amount: amount,
hasHolderName: true,
holderNameRequired: true,
},
},
beforeSubmit: (data, dropin, actions) => {
// add billing address from your custom form
return actions.resolve({
...data,
billingAddress: {
street: 'Some Street',
houseNumberOrName: '3',
postalCode: 'A9A9A9',
city: 'City',
stateOrProvince: 'NY',
},
});
},
...
};
...
}, [
session,
dropinComponent,
setDropinComponent,
sessionIsCreating,
push,
authState.isAuthenticated,
currentCurrency,
]);
...
};