import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { find, get, groupBy, isEmpty, join, toString, noop } from 'lodash';
import { useRouter } from 'next/router';
import UIElement from '@adyen/adyen-web/dist/types/components/UIElement';
import Checkout from '@adyen/adyen-web/dist/types/core';
import AdyenCheckout from '@adyen/adyen-web';
import { useCartContext } from '@app/cart/contexts';
import { useEventCallback, usePaymentAuthState } from '@app/common/hooks';
import { AdyenPaymentForm } from '@app/checkout/components';
import {
useAdyenPaymentServicesClient,
useCheckoutClient,
useLocaleContext,
usePaymentClient,
usePreviewOptions,
} from '@app/common/contexts';
import { useSubmitPaymentRequest } from '@broadleaf/payment-react';
import {
useFormatAmountInMinorUnits,
useGetPaymentAmount,
useGetPaymentCallbackUrl,
useHandleMoneyAddition,
useHandleMoneySubtraction,
useHandleSubmitCart,
} from '@app/checkout/hooks';
import {
Address,
Cart,
CheckoutClientCallOptions,
DefaultPaymentType,
GuestTokenResponse,
PaymentRequest,
PaymentSummary,
PricedFulfillmentOption,
} from '@broadleaf/commerce-cart';
import {
AdyenConfig,
pushGtmAddPayment,
pushGtmAddShipping,
} from '@app/common/utils';
import { useAdyenGetPaymentMethods } from '@broadleaf/adyen-payment-services-react';
import {
useFetchFulfillmentOptionsForCartItems,
useSelectFulfillmentOption,
useUpdateFulfillmentGroupAddress,
} from '@app/cart/hooks';
import { getShipGroupReferenceNumber } from '@app/cart/utils';
import { useAuth } from '@broadleaf/auth-react';
import { useCsrContext } from '@app/csr/context';
import { useGetCustomerAccessToken } from '@app/auth/hooks';
type Props = {
setErrorMsg: (msg: string) => void;
};
type CreateCheckoutResponse = {
checkout: Checkout;
};
export const useCreateAdyenCheckout = ({
setErrorMsg,
}: Props): CreateCheckoutResponse => {
const getPaymentAmount = useGetPaymentAmount({
gatewayType: AdyenPaymentForm.TYPE,
});
const { currentLocale: locale } = useLocaleContext();
const [checkout, setCheckout] = useState<Checkout>();
const authState = usePaymentAuthState();
const adyenPaymentServicesClient = useAdyenPaymentServicesClient();
const { getPaymentMethods } = useAdyenGetPaymentMethods({
authState,
adyenPaymentServicesClient,
});
const shopperReference = authState.isAuthenticated
? authState.customerId
: undefined;
useEffect(() => {
async function createAdyenCheckout() {
if (!checkout && locale) {
const amount = getPaymentAmount();
const paymentMethodsResponse = await getPaymentMethods({
amount,
shopperLocale: locale,
countryCode: 'US',
shopperReference,
});
const adyenCheckout = await AdyenCheckout({
environment: 'test',
clientKey: AdyenConfig.CLIENT_KEY,
analytics: {
enabled: false,
},
locale: locale,
countryCode: 'US',
showPayButton: true,
paymentMethodsResponse,
onError: err => {
console.error(err?.cause);
setErrorMsg(toString(err));
},
});
setCheckout(adyenCheckout);
}
}
void createAdyenCheckout();
}, [
checkout,
getPaymentAmount,
getPaymentMethods,
locale,
setErrorMsg,
shopperReference,
]);
return {
checkout,
};
};
type HandleSubmitProps = {
setError?: unknown;
};
type HandleSubmitParameters = {
state: unknown;
component?: UIElement;
paymentType: string;
sessionId?: string;
paymentData?: unknown;
};
type HandleSubmitResponse = {
(params: HandleSubmitParameters): Promise<PaymentSummary | undefined>;
};
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);
}
},
[]
);
};
const getBillingAddress = (
state,
paymentType,
paymentData
): Address | undefined => {
if (
paymentType === DefaultPaymentType.CREDIT_CARD ||
paymentType === 'KLARNA'
) {
return getBillingDetailsForCreditCard(get(state, 'data'));
} else if (
paymentType === DefaultPaymentType.GOOGLE_PAY ||
paymentType === DefaultPaymentType.GOOGLE_PAY_EXPRESS
) {
return getBillingDetailsForGooglePayment(paymentData);
} else if (
paymentType === DefaultPaymentType.APPLE_PAY ||
paymentType === DefaultPaymentType.APPLE_PAY_EXPRESS
) {
return getBillingDetailsForApplePayment(paymentData);
}
};
const getBillingDetailsForCreditCard = (data): Address | undefined => {
const bAddress = get(data, 'billingAddress', {});
if (isEmpty(bAddress)) {
return;
}
const holderName = get(data, 'paymentMethod.holderName');
return {
fullName: holderName,
addressLine1: join([bAddress.street, bAddress.houseNumberOrName], ', '),
city: bAddress.city,
stateProvinceRegion: bAddress.stateOrProvince,
country: bAddress.country,
postalCode: bAddress.postalCode,
};
};
const getBillingDetailsForGooglePayment = (googlePaymentData): Address => {
const googleBillingAddress = get(
googlePaymentData,
'paymentMethodData.info.billingAddress'
);
if (isEmpty(googleBillingAddress)) {
return;
}
return {
fullName: googleBillingAddress.name,
addressLine1: googleBillingAddress.address1,
addressLine2: googleBillingAddress.address2,
city: googleBillingAddress.locality,
stateProvinceRegion: googleBillingAddress.administrativeArea,
country: googleBillingAddress.countryCode,
postalCode: googleBillingAddress.postalCode,
};
};
const getBillingDetailsForApplePayment = (
applePaymentData: ApplePayJS.ApplePayPaymentAuthorizedEvent
): Address => {
const appleBillingAddress = applePaymentData.payment.billingContact;
if (isEmpty(appleBillingAddress)) {
return;
}
return {
fullName: `${appleBillingAddress.givenName} ${appleBillingAddress.familyName}`,
addressLine1: appleBillingAddress.addressLines[0],
addressLine2:
appleBillingAddress.addressLines.length > 1
? appleBillingAddress.addressLines[1]
: undefined,
city: appleBillingAddress.locality,
stateProvinceRegion: appleBillingAddress.administrativeArea,
country: appleBillingAddress.countryCode,
postalCode: appleBillingAddress.postalCode,
};
};
export const useGetLineItems = () => {
const handleMoneyAddition = useHandleMoneyAddition();
const handleMoneySubtraction = useHandleMoneySubtraction();
const formatAmountInMinorUnits = useFormatAmountInMinorUnits();
return useEventCallback((cart: Cart) => {
const cartItemById = groupBy(cart.cartItems, 'id');
return cart.fulfillmentGroups[0].fulfillmentItems.map(fi => {
const fulfillmentCosts = {
amount: handleMoneyAddition(
fi.fulfillmentTotal,
get(fi, 'proratedFulfillmentCharge')
),
currency: fi.merchandiseTotalAmount.currency,
};
const fulfillmentCostWithAdjustmentsApplied = {
amount: handleMoneySubtraction(
fulfillmentCosts,
fi.proratedFulfillmentGroupAdjustments
),
currency: fi.merchandiseTotalAmount.currency,
};
let merchandiseAndFulfillmentTax = {
amount: 0,
currency: fi.merchandiseTotalAmount.currency,
};
fi.fulfillmentItemTaxDetails.forEach(taxDetail => {
merchandiseAndFulfillmentTax = {
amount: handleMoneyAddition(
taxDetail.taxCalculated,
merchandiseAndFulfillmentTax
),
currency: fi.merchandiseTotalAmount.currency,
};
});
const withTax = {
amount: handleMoneyAddition(
fulfillmentCostWithAdjustmentsApplied,
merchandiseAndFulfillmentTax
),
currency: fi.merchandiseTotalAmount.currency,
};
let finalAmount = withTax;
if (fi.merchandiseTaxableAmount) {
finalAmount = {
amount: handleMoneyAddition(withTax, fi.merchandiseTaxableAmount),
currency: fi.merchandiseTotalAmount.currency,
};
}
return {
description: cartItemById[fi.cartItemId][0].name,
sku: cartItemById[fi.cartItemId][0].sku,
quantity: fi.quantity,
amountIncludingTax: formatAmountInMinorUnits(finalAmount),
imageUrl: cartItemById[fi.cartItemId][0].imageAsset?.contentUrl,
};
});
}, []);
};
type PaymentCompletedProps = {
setUIElement?: Dispatch<SetStateAction<UIElement | undefined>>;
};
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,
},
});
}
}, []);
};
export const useUpdateContactInfoOrGenerateGuestToken = () => {
const { isAuthenticated } = useAuth();
const { cart, setCart, guestToken, setGuestToken } = useCartContext();
const { csrAnonymous } = useCsrContext();
const checkoutClient = useCheckoutClient();
const getCustomerToken = useGetCustomerAccessToken();
const preview = usePreviewOptions();
return useEventCallback(
async (emailAddress?: string): Promise<Cart> => {
let newCart = cart;
try {
if (
emailAddress &&
(guestToken || (isAuthenticated && !csrAnonymous))
) {
const accessToken = await getCustomerToken();
newCart = await checkoutClient.updateContactInfoInCart(
cart.id,
{ emailAddress },
{
accessToken,
guestToken: guestToken?.tokenString,
preview,
version: cart.version,
}
);
} else if (!guestToken) {
const accessToken = await getCustomerToken();
const options = {
accessToken,
guestToken: guestToken?.tokenString,
preview,
version: cart.version,
} as CheckoutClientCallOptions;
let response: GuestTokenResponse;
if (emailAddress) {
response = await checkoutClient.checkoutAsGuest(
cart.id,
{ emailAddress },
options
);
} else {
response = await checkoutClient.generateGuestToken(
cart.id,
options
);
}
newCart = response.cart;
setGuestToken(response.token);
}
if (newCart) {
setCart(newCart);
}
} catch (err) {
console.error(
'There was an error updating the checkout information',
err
);
return Promise.reject(err);
}
return newCart;
},
[
isAuthenticated,
cart,
setCart,
guestToken,
setGuestToken,
checkoutClient,
getCustomerToken,
preview,
]
);
};
type UpdateFulfillmentAddressResponse = {
cart: Cart;
fulfillmentOptions: PricedFulfillmentOption[];
};
export const useUpdateFulfillmentAddress = ({ buildFulfillmentAddress }) => {
const { setCart } = useCartContext();
const { updateFulfillmentGroupAddress } = useUpdateFulfillmentGroupAddress();
const updateContactInfoOrGenerateGuestToken =
useUpdateContactInfoOrGenerateGuestToken();
return useEventCallback(
async (shippingAddress): Promise<UpdateFulfillmentAddressResponse> => {
let newCart: Cart;
try {
newCart = await updateContactInfoOrGenerateGuestToken();
const shipGroupReferenceNumber = getShipGroupReferenceNumber(newCart);
const fulfillmentAddress = buildFulfillmentAddress(shippingAddress);
const response = await updateFulfillmentGroupAddress(
fulfillmentAddress,
newCart,
shipGroupReferenceNumber
);
newCart = response?.cart;
if (newCart) {
setCart(newCart);
const fulfillmentOptions =
response.fulfillmentOptionResponse.groupFulfillmentOptions[
shipGroupReferenceNumber
];
return {
cart: newCart,
fulfillmentOptions,
};
}
} catch (err) {
console.error(
'There was an error updating the checkout information',
err
);
}
},
[]
);
};
export const useUpdateSelectedFulfillmentOption = () => {
const { setCart } = useCartContext();
const { fulfillmentOptions } = useFetchFulfillmentOptionsForCartItems();
const { selectFulfillmentOption } = useSelectFulfillmentOption();
return useEventCallback(
async (selectedShippingOptionId: string): Promise<Cart> => {
let newCart: Cart;
try {
const selectedOption = find(fulfillmentOptions, [
'description',
selectedShippingOptionId,
]);
const response = await selectFulfillmentOption(selectedOption);
newCart = response?.cart;
if (newCart) {
setCart(newCart);
pushGtmAddShipping(newCart);
return newCart;
}
} catch (err) {
console.error(
'There was an error updating the checkout information',
err
);
}
},
[fulfillmentOptions]
);
};