diff --git a/packages/adyen-integration/src/adyenv3/AdyenV3PaymentMethod.tsx b/packages/adyen-integration/src/adyenv3/AdyenV3PaymentMethod.tsx index 657e7a7477..870c73063c 100644 --- a/packages/adyen-integration/src/adyenv3/AdyenV3PaymentMethod.tsx +++ b/packages/adyen-integration/src/adyenv3/AdyenV3PaymentMethod.tsx @@ -3,9 +3,10 @@ import { type AdyenValidationState, type CardInstrument, type PaymentInitializeOptions, + type PaymentMethod, } from '@bigcommerce/checkout-sdk'; import { createAdyenV3PaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/adyen'; -import React, { type FunctionComponent, useCallback, useRef, useState } from 'react'; +import React, { type FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; import { type HostedWidgetComponentProps } from '@bigcommerce/checkout/hosted-widget-integration'; import { @@ -13,7 +14,7 @@ import { type PaymentMethodResolveId, toResolvableComponent, } from '@bigcommerce/checkout/payment-integration-api'; -import { FormContext, LoadingOverlay } from '@bigcommerce/checkout/ui'; +import { FormContext, LoadingOverlay, Modal } from '@bigcommerce/checkout/ui'; import AdyenV3CardValidation from './AdyenV3CardValidation'; import AdyenV3Form from './AdyenV3Form'; @@ -48,17 +49,41 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ shouldShowModal: true, }); + const groupedMethods = (method.initializationData as { groupedMethods?: PaymentMethod[] } | null)?.groupedMethods; + const isGrouped = Boolean(groupedMethods?.length); + const [selectedVariantMethod, setSelectedVariantMethod] = useState(method); + const [shouldRenderAdditionalActionContentModal, setShouldRenderAdditionalActionContentModal] = useState(false); const [isAdditionalActionContentModalVisible, setIsAdditionalActionContentModalVisible] = useState(false); const [cardValidationState, setCardValidationState] = useState(); - const containerId = `adyen-${method.id}-component-field`; - const additionalActionContainerId = `adyen-${method.id}-additional-action-component-field`; - const cardVerificationContainerId = `adyen-${method.id}-tsv-component-field`; - const component = method.id; + const containerId = `adyen-${selectedVariantMethod.id}-component-field`; + const additionalActionContainerId = `adyen-${selectedVariantMethod.id}-additional-action-component-field`; + const cardVerificationContainerId = `adyen-${selectedVariantMethod.id}-tsv-component-field`; + const component = selectedVariantMethod.id; const shouldHideInstrumentExpiryDate = component === AdyenV3PaymentMethodType.bcmc; + const handleVariantChange = useCallback((variantId: string) => { + const variant = groupedMethods?.find((m) => m.id === variantId); + + if (variant && variant.id !== selectedVariantMethod.id) { + setSelectedVariantMethod(variant); + + paymentForm.setFieldValue('methodIdOverride', variant.id); + } + }, [groupedMethods, paymentForm, selectedVariantMethod.id]); + + useEffect(() => { + if (!isGrouped) { + return; + } + + return () => { + paymentForm.setFieldValue('methodIdOverride', undefined); + }; + }, []); + const onBeforeLoad = useCallback((shopperInteraction: boolean) => { ref.current.shouldShowModal = shopperInteraction; @@ -103,6 +128,7 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ return checkoutService.initializePayment({ ...options, + methodId: component, integrations: [createAdyenV3PaymentStrategy], adyenv3: { cardVerificationContainerId: @@ -138,6 +164,34 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ ], ); + useEffect(() => { + if (!isGrouped) { + return; + } + + paymentForm.setValidationSchema(method, null); + paymentForm.setSubmit(method, null); + + void initializeAdyenPayment( + { methodId: component, gatewayId: method.gateway }, + undefined as unknown as CardInstrument, + ).catch((error: unknown) => { + if (onUnhandledError && error instanceof Error) { + onUnhandledError(error); + } + }); + + return () => { + paymentForm.setValidationSchema(method, null); + paymentForm.setSubmit(method, null); + + void checkoutService.deinitializePayment({ + gatewayId: method.gateway, + methodId: component, + }); + }; + }, [initializeAdyenPayment]); + const validateInstrument = ( shouldShowNumberField: boolean, selectedInstrument: CardInstrument, @@ -179,29 +233,57 @@ const AdyenV3PaymentMethod: FunctionComponent = ({ return ( - - - + {isGrouped ? ( + <> + +
+ +
+ + {!shouldRenderAdditionalActionContentModal && ( +
+ )} + + ) : ( + + + + )} ); }; diff --git a/packages/core/src/app/payment/Payment.tsx b/packages/core/src/app/payment/Payment.tsx index 3d613a0e77..f5e951bf49 100644 --- a/packages/core/src/app/payment/Payment.tsx +++ b/packages/core/src/app/payment/Payment.tsx @@ -64,19 +64,54 @@ interface PaymentMethodSelectionParams { checkout: Checkout; methods: PaymentMethod[]; consignments?: Consignment[]; + checkoutSettings?: CheckoutSettings; getPaymentMethod: (methodId: string, gatewayId?: string) => PaymentMethod | undefined; paymentProviderCustomer?: PaymentProviderCustomer; } +const groupMethodsByPrefix = (methods: PaymentMethod[], prefix: string): PaymentMethod[] => { + const group = methods.filter((m) => m.id.startsWith(prefix)); + + if (group.length <= 1) { + return methods; + } + + const sorted = [...group].sort((a, b) => { + const toNum = (id: string) => parseInt(id.slice(prefix.length), 10) || 0; + + return toNum(a.id) - toNum(b.id); + }); + + const [first] = sorted; + const representative: PaymentMethod = { + ...first, + config: { + ...first.config, + displayName: first.config.displayName?.replace(/^\d+x\s+/i, '') ?? first.config.displayName, + }, + initializationData: { + ...(first.initializationData as Record), + groupedMethods: sorted, + }, + }; + + return methods.flatMap((m) => { + if (!m.id.startsWith(prefix)) return [m]; + if (m.id === first.id) return [representative]; + + return []; + }); +}; + const getDefaultPaymentMethod = ({ checkout, + checkoutSettings, consignments, getPaymentMethod, methods, paymentProviderCustomer, }: PaymentMethodSelectionParams): { filteredMethods: PaymentMethod[]; defaultMethod?: PaymentMethod } => { let filteredMethods = methods; - // TODO: In accordance with the checkout team, this functionality is temporary and will be implemented in the backend instead. if (paymentProviderCustomer?.stripeLinkAuthenticationState) { const stripeUpePaymentMethod = filteredMethods.filter( @@ -94,6 +129,12 @@ const getDefaultPaymentMethod = ({ return method.id !== PaymentMethodId.BraintreeLocalPaymentMethod; }); + if (isExperimentEnabled(checkoutSettings, 'PAYMENTS-XXXX.oney_facilypay_grouping', false)) { + const GROUPED_METHOD_ID_PREFIXES = ['facilypay_']; + + filteredMethods = GROUPED_METHOD_ID_PREFIXES.reduce(groupMethodsByPrefix, filteredMethods); + } + if (consignments && consignments.length > 1) { const multiShippingIncompatibleMethodIds: string[] = [ PaymentMethodId.AmazonPay, @@ -550,6 +591,7 @@ const Payment= (props: PaymentProps & WithCheckoutPaymentProps & WithLanguagePro const defaultMethod = checkout ? getDefaultPaymentMethod({ checkout, + checkoutSettings: updatedState.data.getConfig()?.checkoutSettings, consignments: updatedState.data.getConsignments(), getPaymentMethod: updatedState.data.getPaymentMethod, methods, diff --git a/packages/core/src/app/payment/mapToOrderRequestBody.ts b/packages/core/src/app/payment/mapToOrderRequestBody.ts index c2c49d561a..d598a097ac 100644 --- a/packages/core/src/app/payment/mapToOrderRequestBody.ts +++ b/packages/core/src/app/payment/mapToOrderRequestBody.ts @@ -18,8 +18,9 @@ export default function mapToOrderRequestBody( return {}; } - const { paymentProviderRadio, ...rest } = values; - const { methodId, gatewayId } = parseUniquePaymentMethodId(paymentProviderRadio); + const { paymentProviderRadio, methodIdOverride, ...rest } = values; + const { methodId: baseMethodId, gatewayId } = parseUniquePaymentMethodId(paymentProviderRadio); + const methodId = methodIdOverride || baseMethodId; const payload: OrderRequestBody = { payment: { gatewayId, methodId }, };