From 8e2b717bf582fabcb9d02078553b67c27b8f6e8d Mon Sep 17 00:00:00 2001 From: bc-yaroslav-zhmutskyi Date: Mon, 29 Dec 2025 09:22:59 +0200 Subject: [PATCH] feat(payment): PAYPAL-6142 3DS not required create order --- ...payments-fastlane-payment-strategy.spec.ts | 37 +++++++++-- ...erce-payments-fastlane-payment-strategy.ts | 64 +++++++++---------- .../src/bigcommerce-payments-types.ts | 4 ++ ...commerce-fastlane-payment-strategy.spec.ts | 33 ++++++++-- ...ypal-commerce-fastlane-payment-strategy.ts | 43 +++++++------ packages/paypal-utils/src/paypal-types.ts | 4 ++ 6 files changed, 124 insertions(+), 61 deletions(-) diff --git a/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.spec.ts b/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.spec.ts index 36af8c8bc9..df45a4c054 100644 --- a/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.spec.ts +++ b/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.spec.ts @@ -8,6 +8,7 @@ import { getBigCommercePaymentsFastlanePaymentMethod, getPayPalFastlaneAuthenticationResultMock, getPayPalFastlaneSdk, + LiabilityShiftEnum, PayPalFastlane, PayPalFastlaneAuthenticationState, PayPalFastlaneSdk, @@ -551,7 +552,7 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { ...threeDomainSecureComponentMock, isEligible: jest.fn().mockReturnValue(Promise.resolve(true)), show: jest.fn().mockResolvedValue({ - liabilityShift: 'YES', + liabilityShift: LiabilityShiftEnum.Yes, }), }, }; @@ -580,7 +581,7 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { ...threeDomainSecureComponentMock, isEligible: jest.fn().mockReturnValue(Promise.resolve(true)), show: jest.fn().mockResolvedValue({ - liabilityShift: 'UNKNOWN', + liabilityShift: LiabilityShiftEnum.Unknown, }), }, }; @@ -656,7 +657,7 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'possible', + liabilityShift: LiabilityShiftEnum.Possible, authenticationState: 'succeeded', nonce: 'bigcommerce_payments_fastlane_instrument_id_nonce', }), @@ -695,7 +696,7 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'NO', + liabilityShift: LiabilityShiftEnum.No, authenticationState: 'success', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -721,7 +722,7 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'possible', + liabilityShift: LiabilityShiftEnum.Possible, authenticationState: 'errored', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -739,6 +740,32 @@ describe('BigCommercePaymentsFastlanePaymentStrategy', () => { expect(error).toBeInstanceOf(Error); } }); + + it('creates order with payment token when 3ds is on and isEligible false', async () => { + const bigCommerceFastlaneSdkMock = { + ...paypalFastlaneSdk, + ThreeDomainSecureClient: { + ...threeDomainSecureComponentMock, + isEligible: jest.fn().mockReturnValue(Promise.resolve(false)), + }, + }; + + jest.spyOn(bigCommercePaymentsSdk, 'getPayPalFastlaneSdk').mockImplementation(() => + Promise.resolve(bigCommerceFastlaneSdkMock), + ); + + await strategy.initialize(initializationOptions); + + await strategy.execute(executeOptions); + + expect(bigCommercePaymentsRequestSender.createOrder).toHaveBeenCalledWith( + methodId, + { + cartId: cart.id, + fastlaneToken: 'paypal_fastlane_instrument_id_nonce', + }, + ); + }); }); it('throws specific error when get 422 error on payment request', async () => { diff --git a/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.ts b/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.ts index c7e123e5eb..ff411c9432 100644 --- a/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.ts +++ b/packages/bigcommerce-payments-integration/src/bigcommerce-payments-fastlane/bigcommerce-payments-fastlane-payment-strategy.ts @@ -11,6 +11,7 @@ import { PayPalFastlaneSdk, PayPalSdkHelper, TDSecureAuthenticationState, + TDSecureVerificationMethod, } from '@bigcommerce/checkout-sdk/bigcommerce-payments-utils'; import { CardInstrument, @@ -337,11 +338,9 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme this.isBigcommercePaymentsFastlaneThreeDSAvailable() && paymentMethod.config.is3dsEnabled; - if (!is3DSEnabled) { - await this.createOrder(instrumentId); - } + const nonce = is3DSEnabled ? await this.get3DSNonce(instrumentId) : instrumentId; - const fastlaneToken = is3DSEnabled ? await this.get3DSNonce(instrumentId) : instrumentId; + await this.createOrder(nonce); return { methodId, @@ -349,7 +348,7 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme formattedPayload: { paypal_fastlane_token: { order_id: this.orderId, - token: fastlaneToken, + token: nonce, }, }, }, @@ -379,14 +378,12 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme this.isBigcommercePaymentsFastlaneThreeDSAvailable() && paymentMethod.config.is3dsEnabled; - if (!is3DSEnabled) { - await this.createOrder(id); - } - const { shouldSaveInstrument = false, shouldSetAsDefaultInstrument = false } = isHostedInstrumentLike(paymentData) ? paymentData : {}; - const fastlaneToken = is3DSEnabled ? await this.get3DSNonce(id) : id; + const nonce = is3DSEnabled ? await this.get3DSNonce(id) : id; + + await this.createOrder(nonce); return { methodId, @@ -397,7 +394,7 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme formattedPayload: { paypal_fastlane_token: { order_id: this.orderId, - token: fastlaneToken, + token: nonce, }, }, }, @@ -426,7 +423,7 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme * 3DSecure methods * * */ - private async get3DSNonce(paypalNonce: string): Promise { + private async get3DSNonce(nonce: string): Promise { const state = this.paymentIntegrationService.getState(); const cart = state.getCartOrThrow(); const order = state.getOrderOrThrow(); @@ -441,8 +438,8 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme const threeDomainSecureParameters = { amount: order.orderAmount.toFixed(2), currency: cart.currency.code, - nonce: paypalNonce, - threeDSRequested: this.threeDSVerificationMethod === 'SCA_ALWAYS', + nonce, + threeDSRequested: this.threeDSVerificationMethod === TDSecureVerificationMethod.Always, transactionContext: { experience_context: { locale: 'en-US', @@ -456,35 +453,38 @@ export default class BigCommercePaymentsFastlanePaymentStrategy implements Payme threeDomainSecureParameters, ); + if ( + this.threeDSVerificationMethod === TDSecureVerificationMethod.Always && + !isThreeDomainSecureEligible + ) { + throw new PaymentMethodInvalidError(); + } + if (isThreeDomainSecureEligible) { - const { liabilityShift, authenticationState, nonce } = - await threeDomainSecureComponent.show(); + const { + liabilityShift, + authenticationState, + nonce: threeDSNonce, + } = await threeDomainSecureComponent.show(); if ( liabilityShift === LiabilityShiftEnum.No || - liabilityShift === LiabilityShiftEnum.Unknown + liabilityShift === LiabilityShiftEnum.Unknown || + authenticationState === TDSecureAuthenticationState.Errored || + authenticationState === TDSecureAuthenticationState.Cancelled ) { throw new PaymentMethodInvalidError(); } - await this.createOrder(paypalNonce); - - if (authenticationState === TDSecureAuthenticationState.Succeeded) { - return nonce; - } - - // Cancelled or errored, merchant can choose to send the customer back to 3D Secure or submit a payment and or vault the payment token. - if (authenticationState === TDSecureAuthenticationState.Errored) { - throw new PaymentMethodInvalidError(); - } - - if (authenticationState === TDSecureAuthenticationState.Cancelled) { - console.error('3DS check was canceled'); - throw new PaymentMethodInvalidError(); + if ( + authenticationState === TDSecureAuthenticationState.Succeeded && + [LiabilityShiftEnum.Yes, LiabilityShiftEnum.Possible].includes(liabilityShift) + ) { + return threeDSNonce; } } - return paypalNonce; + return nonce; } /** diff --git a/packages/bigcommerce-payments-utils/src/bigcommerce-payments-types.ts b/packages/bigcommerce-payments-utils/src/bigcommerce-payments-types.ts index 0f9882bd32..ff249a0b5f 100644 --- a/packages/bigcommerce-payments-utils/src/bigcommerce-payments-types.ts +++ b/packages/bigcommerce-payments-utils/src/bigcommerce-payments-types.ts @@ -147,6 +147,10 @@ export interface PayPalGooglePaySdk { Googlepay(): GooglePay; } +export enum TDSecureVerificationMethod { + Always = 'SCA_ALWAYS', +} + export enum TDSecureAuthenticationState { Succeeded = 'succeeded', Cancelled = 'cancelled', diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.spec.ts index c868c05a8e..553c3f8220 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.spec.ts @@ -32,6 +32,7 @@ import { } from '@bigcommerce/checkout-sdk/paypal-utils'; import PayPalCommerceRequestSender from '../paypal-commerce-request-sender'; +import { LiabilityShiftEnum } from '../paypal-commerce-types'; import PayPalCommerceFastlanePaymentStrategy from './paypal-commerce-fastlane-payment-strategy'; @@ -565,7 +566,7 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'YES', + liabilityShift: LiabilityShiftEnum.Yes, authenticationState: 'succeeded', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -591,7 +592,7 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'UNKNOWN', + liabilityShift: LiabilityShiftEnum.Unknown, authenticationState: 'succeeded', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -637,7 +638,7 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'POSSIBLE', + liabilityShift: LiabilityShiftEnum.Possible, authenticationState: 'succeeded', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -676,7 +677,7 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'NO', + liabilityShift: LiabilityShiftEnum.No, authenticationState: 'success', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -702,7 +703,7 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { ThreeDomainSecureClient: { ...threeDomainSecureComponentMock, show: jest.fn().mockReturnValue({ - liabilityShift: 'POSSIBLE', + liabilityShift: LiabilityShiftEnum.Possible, authenticationState: 'errored', nonce: 'paypal_fastlane_instrument_id_nonce_3ds', }), @@ -720,6 +721,28 @@ describe('PayPalCommerceFastlanePaymentStrategy', () => { expect(error).toBeInstanceOf(Error); } }); + + it('creates order with payment token when 3ds is on and isEligible false', async () => { + const paypalFastlaneSdkMock = { + ...paypalFastlaneSdk, + ThreeDomainSecureClient: { + ...threeDomainSecureComponentMock, + isEligible: jest.fn().mockReturnValue(Promise.resolve(false)), + }, + }; + + jest.spyOn(paypalSdkScriptLoader, 'getPayPalFastlaneSdk').mockImplementation(() => + Promise.resolve(paypalFastlaneSdkMock), + ); + await strategy.initialize(initializationOptions); + + await strategy.execute(executeOptions); + + expect(paypalCommerceRequestSender.createOrder).toHaveBeenCalledWith(methodId, { + cartId: cart.id, + fastlaneToken: 'paypal_fastlane_instrument_id_nonce', + }); + }); }); }); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.ts index dcb13f4851..dbafc32263 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-fastlane/paypal-commerce-fastlane-payment-strategy.ts @@ -29,6 +29,7 @@ import { PayPalInitializationData, PayPalSdkScriptLoader, TDSecureAuthenticationState, + TDSecureVerificationMethod, } from '@bigcommerce/checkout-sdk/paypal-utils'; import PayPalCommerceRequestSender from '../paypal-commerce-request-sender'; @@ -325,11 +326,9 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr const paymentMethod = state.getPaymentMethodOrThrow(methodId); const is3DSEnabled = paymentMethod.config.is3dsEnabled; - if (!is3DSEnabled) { - await this.createOrder(instrumentId); - } + const nonce = is3DSEnabled ? await this.get3DSNonce(instrumentId) : instrumentId; - const fastlaneToken = is3DSEnabled ? await this.get3DSNonce(instrumentId) : instrumentId; + await this.createOrder(nonce); return { methodId, @@ -337,7 +336,7 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr formattedPayload: { paypal_fastlane_token: { order_id: this.orderId, - token: fastlaneToken, + token: nonce, }, }, }, @@ -363,11 +362,9 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr const is3DSEnabled = paymentMethod.config.is3dsEnabled; - if (!is3DSEnabled) { - await this.createOrder(id); - } + const nonce = is3DSEnabled ? await this.get3DSNonce(id) : id; - const fastlaneToken = is3DSEnabled ? await this.get3DSNonce(id) : id; + await this.createOrder(nonce); const { shouldSaveInstrument = false, shouldSetAsDefaultInstrument = false } = isHostedInstrumentLike(paymentData) ? paymentData : {}; @@ -381,7 +378,7 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr formattedPayload: { paypal_fastlane_token: { order_id: this.orderId, - token: fastlaneToken, + token: nonce, }, }, }, @@ -407,7 +404,7 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr * 3DSecure methods * * */ - private async get3DSNonce(paypalNonce: string): Promise { + private async get3DSNonce(nonce: string): Promise { const state = this.paymentIntegrationService.getState(); const cart = state.getCartOrThrow(); const order = state.getOrderOrThrow(); @@ -422,8 +419,8 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr const threeDomainSecureParameters = { amount: order.orderAmount.toFixed(2), currency: cart.currency.code, - nonce: paypalNonce, - threeDSRequested: this.threeDSVerificationMethod === 'SCA_ALWAYS', + nonce, + threeDSRequested: this.threeDSVerificationMethod === TDSecureVerificationMethod.Always, transactionContext: { experience_context: { locale: 'en-US', @@ -437,9 +434,19 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr threeDomainSecureParameters, ); + if ( + this.threeDSVerificationMethod === TDSecureVerificationMethod.Always && + !isThreeDomainSecureEligible + ) { + throw new PaymentMethodInvalidError(); + } + if (isThreeDomainSecureEligible) { - const { liabilityShift, authenticationState, nonce } = - await threeDomainSecureComponent.show(); + const { + liabilityShift, + authenticationState, + nonce: threeDSNonce, + } = await threeDomainSecureComponent.show(); if ( liabilityShift === LiabilityShiftEnum.No || @@ -454,13 +461,11 @@ export default class PaypalCommerceFastlanePaymentStrategy implements PaymentStr authenticationState === TDSecureAuthenticationState.Succeeded && [LiabilityShiftEnum.Yes, LiabilityShiftEnum.Possible].includes(liabilityShift) ) { - await this.createOrder(nonce); - - return nonce; + return threeDSNonce; } } - return paypalNonce; + return nonce; } /** diff --git a/packages/paypal-utils/src/paypal-types.ts b/packages/paypal-utils/src/paypal-types.ts index 8994661690..531360902f 100644 --- a/packages/paypal-utils/src/paypal-types.ts +++ b/packages/paypal-utils/src/paypal-types.ts @@ -254,6 +254,10 @@ interface ThreeDomainSecureClientShowResponse { nonce: string; // Enriched nonce or the original nonce } +export enum TDSecureVerificationMethod { + Always = 'SCA_ALWAYS', +} + export enum TDSecureAuthenticationState { Succeeded = 'succeeded', Cancelled = 'cancelled',