From 078e46481db5ab7a850955bcf197c763c188a24c Mon Sep 17 00:00:00 2001 From: "nick.tsybulko" Date: Wed, 4 Mar 2026 14:14:42 +0100 Subject: [PATCH] feat(payment): PAYPAL-5793 Update PayPalCommerceCreditPaymentStrategy by providing PaypalUtilsService from paypal-utils package --- ...paypal-commerce-credit-payment-strategy.ts | 13 +- ...l-commerce-credit-payment-strategy.spec.ts | 175 ++++++++---------- ...paypal-commerce-credit-payment-strategy.ts | 47 ++--- packages/paypal-utils/src/index.ts | 1 + .../src/paypal-commerce-constants.ts | 4 + packages/paypal-utils/src/paypal-types.ts | 2 + 6 files changed, 115 insertions(+), 127 deletions(-) create mode 100644 packages/paypal-utils/src/paypal-commerce-constants.ts diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/create-paypal-commerce-credit-payment-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/create-paypal-commerce-credit-payment-strategy.ts index f020a6f70a..ad212708db 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/create-paypal-commerce-credit-payment-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/create-paypal-commerce-credit-payment-strategy.ts @@ -2,12 +2,13 @@ import { PaymentStrategyFactory, toResolvableModule, } from '@bigcommerce/checkout-sdk/payment-integration-api'; -import { createPayPalCommerceSdk } from '@bigcommerce/checkout-sdk/paypal-commerce-utils'; +import { + createPayPalIntegrationService, + createPayPalSdkScriptLoader, + LOADING_INDICATOR_STYLES, +} from '@bigcommerce/checkout-sdk/paypal-utils'; import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui'; -import createPayPalCommerceIntegrationService from '../create-paypal-commerce-integration-service'; -import { LOADING_INDICATOR_STYLES } from '../paypal-commerce-constants'; - import PayPalCommerceCreditPaymentStrategy from './paypal-commerce-credit-payment-strategy'; const createPayPalCommerceCreditPaymentStrategy: PaymentStrategyFactory< @@ -15,11 +16,11 @@ const createPayPalCommerceCreditPaymentStrategy: PaymentStrategyFactory< > = (paymentIntegrationService) => new PayPalCommerceCreditPaymentStrategy( paymentIntegrationService, - createPayPalCommerceIntegrationService(paymentIntegrationService), + createPayPalIntegrationService(paymentIntegrationService), new LoadingIndicator({ containerStyles: LOADING_INDICATOR_STYLES, }), - createPayPalCommerceSdk(), + createPayPalSdkScriptLoader(), ); export default toResolvableModule(createPayPalCommerceCreditPaymentStrategy, [ diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.spec.ts index 9bfda13147..a4a9306a63 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.spec.ts @@ -12,23 +12,18 @@ import { } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { PaymentIntegrationServiceMock } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; import { - createPayPalCommerceSdk, - PayPalCommerceSdk, - PayPalMessagesSdk, -} from '@bigcommerce/checkout-sdk/paypal-commerce-utils'; -import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui'; - -import { - getPayPalCommerceIntegrationServiceMock, - getPayPalCommercePaymentMethod, + createPayPalSdkScriptLoader, + getPayPalIntegrationServiceMock, + getPayPalPaymentMethod, getPayPalSDKMock, -} from '../mocks'; -import PayPalCommerceIntegrationService from '../paypal-commerce-integration-service'; -import { - PayPalCommerceButtonsOptions, - PayPalCommerceHostWindow, + PayPalButtonsOptions, + PayPalHostWindow, + PayPalIntegrationService, + PayPalMessagesSdk, PayPalSDK, -} from '../paypal-commerce-types'; + PayPalSdkScriptLoader, +} from '@bigcommerce/checkout-sdk/paypal-utils'; +import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui'; import PayPalCommerceCreditPaymentInitializeOptions from './paypal-commerce-credit-payment-initialize-options'; import PayPalCommerceCreditPaymentStrategy from './paypal-commerce-credit-payment-strategy'; @@ -38,10 +33,10 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { let loadingIndicator: LoadingIndicator; let paymentIntegrationService: PaymentIntegrationService; let paymentMethod: PaymentMethod; - let paypalCommerceIntegrationService: PayPalCommerceIntegrationService; + let paypalIntegrationService: PayPalIntegrationService; let paypalSdk: PayPalSDK; let strategy: PayPalCommerceCreditPaymentStrategy; - let paypalCommerceSdk: PayPalCommerceSdk; + let payPalSdkScriptLoader: PayPalSdkScriptLoader; let payPalMessagesSdk: PayPalMessagesSdk; const paypalOrderId = 'paypal123'; @@ -69,94 +64,90 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { }; paypalSdk = getPayPalSDKMock(); - paymentMethod = getPayPalCommercePaymentMethod(); + paymentMethod = getPayPalPaymentMethod(); paymentMethod.id = defaultMethodId; paymentMethod.initializationData.orderId = undefined; loadingIndicator = new LoadingIndicator(); - paypalCommerceIntegrationService = getPayPalCommerceIntegrationServiceMock(); + paypalIntegrationService = getPayPalIntegrationServiceMock(); paymentIntegrationService = new PaymentIntegrationServiceMock(); - paypalCommerceSdk = createPayPalCommerceSdk(); + payPalSdkScriptLoader = createPayPalSdkScriptLoader(); strategy = new PayPalCommerceCreditPaymentStrategy( paymentIntegrationService, - paypalCommerceIntegrationService, + paypalIntegrationService, loadingIndicator, - paypalCommerceSdk, + payPalSdkScriptLoader, ); jest.spyOn(paymentIntegrationService.getState(), 'getPaymentMethodOrThrow').mockReturnValue( paymentMethod, ); - jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockResolvedValue(paypalSdk); - jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue( - paypalSdk, - ); - jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockResolvedValue(''); - jest.spyOn(paypalCommerceIntegrationService, 'submitPayment').mockResolvedValue(); + jest.spyOn(paypalIntegrationService, 'loadPayPalSdk').mockResolvedValue(paypalSdk); + jest.spyOn(paypalIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue(paypalSdk); + jest.spyOn(paypalIntegrationService, 'createOrder').mockResolvedValue(''); + jest.spyOn(paypalIntegrationService, 'submitPayment').mockResolvedValue(); jest.spyOn(loadingIndicator, 'show').mockReturnValue(undefined); jest.spyOn(loadingIndicator, 'hide').mockReturnValue(undefined); - jest.spyOn(paypalSdk, 'Buttons').mockImplementation( - (options: PayPalCommerceButtonsOptions) => { - eventEmitter.on('createOrder', () => { - if (options.createOrder) { - options.createOrder(); - } - }); + jest.spyOn(paypalSdk, 'Buttons').mockImplementation((options: PayPalButtonsOptions) => { + eventEmitter.on('createOrder', () => { + if (options.createOrder) { + options.createOrder(); + } + }); - eventEmitter.on('onClick', () => { - if (options.onClick) { - options.onClick( - { fundingSource: defaultMethodId }, - { - reject: jest.fn(), - resolve: jest.fn(), - }, - ); - } - }); + eventEmitter.on('onClick', () => { + if (options.onClick) { + options.onClick( + { fundingSource: defaultMethodId }, + { + reject: jest.fn(), + resolve: jest.fn(), + }, + ); + } + }); - eventEmitter.on('onApprove', () => { - if (options.onApprove) { - options.onApprove( - { orderID: paypalOrderId }, - { - order: { - get: jest.fn(), - }, + eventEmitter.on('onApprove', () => { + if (options.onApprove) { + options.onApprove( + { orderID: paypalOrderId }, + { + order: { + get: jest.fn(), }, - ); - } - }); + }, + ); + } + }); - eventEmitter.on('onCancel', () => { - if (options.onCancel) { - options.onCancel(); - } - }); + eventEmitter.on('onCancel', () => { + if (options.onCancel) { + options.onCancel(); + } + }); - eventEmitter.on('onError', () => { - if (options.onError) { - options.onError(new Error()); - } - }); + eventEmitter.on('onError', () => { + if (options.onError) { + options.onError(new Error()); + } + }); - return { - isEligible: jest.fn(() => true), - render: jest.fn(), - close: jest.fn(), - }; - }, - ); + return { + isEligible: jest.fn(() => true), + render: jest.fn(), + close: jest.fn(), + }; + }); }); afterEach(() => { jest.clearAllMocks(); - delete (window as PayPalCommerceHostWindow).paypal; + delete (window as PayPalHostWindow).paypal; }); it('creates an instance of the PayPal Commerce payment strategy', () => { @@ -191,15 +182,13 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { await strategy.initialize(initializationOptions); - expect(paypalCommerceIntegrationService.loadPayPalSdk).not.toHaveBeenCalled(); + expect(paypalIntegrationService.loadPayPalSdk).not.toHaveBeenCalled(); }); it('loads paypal sdk', async () => { await strategy.initialize(initializationOptions); - expect(paypalCommerceIntegrationService.loadPayPalSdk).toHaveBeenCalledWith( - defaultMethodId, - ); + expect(paypalIntegrationService.loadPayPalSdk).toHaveBeenCalledWith(defaultMethodId); }); }); @@ -235,17 +224,15 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { it('renders PayPal Credit button if PayPal PayLater button is not eligible', async () => { const paypalCommerceSdkRenderMock = jest.fn(); - jest.spyOn(paypalSdk, 'Buttons').mockImplementation( - (options: PayPalCommerceButtonsOptions) => { - return { - close: jest.fn(), - render: paypalCommerceSdkRenderMock, - isEligible: jest.fn(() => { - return options.fundingSource === paypalSdk.FUNDING.CREDIT; - }), - }; - }, - ); + jest.spyOn(paypalSdk, 'Buttons').mockImplementation((options: PayPalButtonsOptions) => { + return { + close: jest.fn(), + render: paypalCommerceSdkRenderMock, + isEligible: jest.fn(() => { + return options.fundingSource === paypalSdk.FUNDING.CREDIT; + }), + }; + }); await strategy.initialize(initializationOptions); @@ -319,7 +306,7 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { await new Promise((resolve) => process.nextTick(resolve)); - expect(paypalCommerceIntegrationService.createOrder).toHaveBeenCalledWith( + expect(paypalIntegrationService.createOrder).toHaveBeenCalledWith( 'paypalcommercecreditcheckout', ); }); @@ -482,7 +469,7 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { await strategy.execute(payload); - expect(paypalCommerceIntegrationService.submitPayment).toHaveBeenCalledWith( + expect(paypalIntegrationService.submitPayment).toHaveBeenCalledWith( payload.payment.methodId, paypalOrderId, ); @@ -534,7 +521,7 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { div.setAttribute('id', defaultMessageContainerId); document.body.appendChild(div); - jest.spyOn(paypalCommerceSdk, 'getPayPalMessages').mockImplementation(() => + jest.spyOn(payPalSdkScriptLoader, 'getPayPalMessages').mockImplementation(() => Promise.resolve(payPalMessagesSdk), ); jest.spyOn(payPalMessagesSdk, 'Messages').mockImplementation(() => ({ @@ -607,7 +594,7 @@ describe('PayPalCommerceCreditPaymentStrategy', () => { it('does not execute PayPal button initialization logic if bannerContainerId is provided', async () => { await strategy.initialize(options); - expect(paypalCommerceIntegrationService.loadPayPalSdk).not.toHaveBeenCalledWith( + expect(paypalIntegrationService.loadPayPalSdk).not.toHaveBeenCalledWith( defaultMethodId, ); }); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.ts index d3c0d6527f..a3b265e1c7 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-credit/paypal-commerce-credit-payment-strategy.ts @@ -11,23 +11,20 @@ import { PaymentStrategy, } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { + ApproveCallbackPayload, + ClickCallbackActions, getPaypalMessagesStylesFromBNPLConfig, MessagingOptions, PayPalBNPLConfigurationItem, - PayPalCommerceInitializationData, - PayPalCommerceSdk, + PayPalButtons, + PayPalButtonsOptions, + PayPalInitializationData, + PayPalIntegrationService, PayPalMessagesSdk, -} from '@bigcommerce/checkout-sdk/paypal-commerce-utils'; + PayPalSdkScriptLoader, +} from '@bigcommerce/checkout-sdk/paypal-utils'; import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui'; -import PayPalCommerceIntegrationService from '../paypal-commerce-integration-service'; -import { - ApproveCallbackPayload, - ClickCallbackActions, - PayPalCommerceButtons, - PayPalCommerceButtonsOptions, -} from '../paypal-commerce-types'; - import PayPalCommerceCreditPaymentInitializeOptions, { WithPayPalCommerceCreditPaymentInitializeOptions, } from './paypal-commerce-credit-payment-initialize-options'; @@ -35,13 +32,13 @@ import PayPalCommerceCreditPaymentInitializeOptions, { export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrategy { private loadingIndicatorContainer?: string; private orderId?: string; - private paypalButton?: PayPalCommerceButtons; + private paypalButton?: PayPalButtons; constructor( private paymentIntegrationService: PaymentIntegrationService, - private paypalCommerceIntegrationService: PayPalCommerceIntegrationService, + private paypalIntegrationService: PayPalIntegrationService, private loadingIndicator: LoadingIndicator, - private paypalCommerceSdk: PayPalCommerceSdk, + private payPalSdkScriptLoader: PayPalSdkScriptLoader, ) {} async initialize( @@ -66,8 +63,7 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat await this.paymentIntegrationService.loadPaymentMethod(methodId); const state = this.paymentIntegrationService.getState(); - const paymentMethod = - state.getPaymentMethodOrThrow(methodId); + const paymentMethod = state.getPaymentMethodOrThrow(methodId); const { paypalBNPLConfiguration = [], orderId } = paymentMethod.initializationData || {}; const { bannerContainerId, container } = paypalOptions; @@ -88,7 +84,7 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat return; } - const paypalMessages = await this.paypalCommerceSdk.getPayPalMessages( + const paypalMessages = await this.payPalSdkScriptLoader.getPayPalMessages( paymentMethod, state.getCartOrThrow().currency.code, ); @@ -106,7 +102,7 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat return; } - await this.paypalCommerceIntegrationService.loadPayPalSdk(methodId); + await this.paypalIntegrationService.loadPayPalSdk(methodId); this.loadingIndicatorContainer = container?.split('#')[1]; @@ -125,7 +121,7 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat } await this.paymentIntegrationService.submitOrder(order, options); - await this.paypalCommerceIntegrationService.submitPayment(payment.methodId, this.orderId); + await this.paypalIntegrationService.submitPayment(payment.methodId, this.orderId); } finalize(): Promise { @@ -155,11 +151,10 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat ); } - const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow(); + const paypalSdk = this.paypalIntegrationService.getPayPalSdkOrThrow(); const state = this.paymentIntegrationService.getState(); - const paymentMethod = - state.getPaymentMethodOrThrow(methodId); + const paymentMethod = state.getPaymentMethodOrThrow(methodId); const { paymentButtonStyles } = paymentMethod.initializationData || {}; const { checkoutPaymentButtonStyles } = paymentButtonStyles || {}; @@ -173,15 +168,13 @@ export default class PayPalCommerceCreditPaymentStrategy implements PaymentStrat return; } - const buttonOptions: PayPalCommerceButtonsOptions = { + const buttonOptions: PayPalButtonsOptions = { fundingSource, - style: this.paypalCommerceIntegrationService.getValidButtonStyle( + style: this.paypalIntegrationService.getValidButtonStyle( checkoutPaymentButtonStyles, ), createOrder: () => - this.paypalCommerceIntegrationService.createOrder( - 'paypalcommercecreditcheckout', - ), + this.paypalIntegrationService.createOrder('paypalcommercecreditcheckout'), onClick: (_, actions) => this.handleClick(actions, onValidate), onApprove: (data) => this.handleApprove(data, submitForm), onCancel: () => this.toggleLoadingIndicator(false), diff --git a/packages/paypal-utils/src/index.ts b/packages/paypal-utils/src/index.ts index 5df0cdf1a8..bb55dc904e 100644 --- a/packages/paypal-utils/src/index.ts +++ b/packages/paypal-utils/src/index.ts @@ -1,6 +1,7 @@ export * from './paypal-types'; export * from './mocks'; export * from './utils'; +export * from './paypal-commerce-constants'; // TODO: this export should be moved to ./utils/index.ts file export { default as isPaypalProviderError } from './utils/is-paypal-provider-error'; diff --git a/packages/paypal-utils/src/paypal-commerce-constants.ts b/packages/paypal-utils/src/paypal-commerce-constants.ts new file mode 100644 index 0000000000..0301da327e --- /dev/null +++ b/packages/paypal-utils/src/paypal-commerce-constants.ts @@ -0,0 +1,4 @@ +export const LOADING_INDICATOR_STYLES = { + 'background-color': 'rgba(0, 0, 0, 0.4)', + 'z-index': '1000', +}; diff --git a/packages/paypal-utils/src/paypal-types.ts b/packages/paypal-utils/src/paypal-types.ts index 8d4ba4ba22..3185a09014 100644 --- a/packages/paypal-utils/src/paypal-types.ts +++ b/packages/paypal-utils/src/paypal-types.ts @@ -381,6 +381,8 @@ export interface PayPalButtons { render(id: string): void; close(): void; isEligible(): boolean; + hasReturned?(): boolean; + resume?(): void; } export interface PayPalButtonsOptions {