diff --git a/packages/paypal-commerce-integration/src/paypal-commerce-types.ts b/packages/paypal-commerce-integration/src/paypal-commerce-types.ts index 28082589f5..c55a08fd56 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce-types.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce-types.ts @@ -361,6 +361,8 @@ export interface PayPalCommerceButtons { render(id: string): void; close(): void; isEligible(): boolean; + hasReturned?(): boolean; + resume?(): void; } export interface PayPalCommerceButtonsOptions { diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts index 0f56699d3a..030fa13521 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.spec.ts @@ -70,8 +70,6 @@ describe('PayPalCommerceButtonStrategy', () => { paypalcommerce: buyNowPayPalCommerceOptions, }; - const storeConfig = getConfig().storeConfig; - const paypalCommerceOptions: PayPalCommerceButtonInitializeOptions = { style: { height: 45, @@ -104,6 +102,9 @@ describe('PayPalCommerceButtonStrategy', () => { type: 'type_shipping', }; + const storeConfig = getConfig().storeConfig; + const resumeMock = jest.fn(); + beforeEach(() => { buyNowCart = getBuyNowCart(); cart = getCart(); @@ -244,6 +245,8 @@ describe('PayPalCommerceButtonStrategy', () => { isEligible: jest.fn(() => true), render: jest.fn(), close: jest.fn(), + hasReturned: jest.fn().mockReturnValue(true), + resume: resumeMock, }; }, ); @@ -371,6 +374,18 @@ describe('PayPalCommerceButtonStrategy', () => { describe('#renderButton', () => { it('initializes PayPal button to render (default flow)', async () => { + jest.spyOn( + paymentIntegrationService.getState(), + 'getStoreConfigOrThrow', + ).mockReturnValue({ + ...storeConfig, + checkoutSettings: { + ...storeConfig.checkoutSettings, + features: { + 'PAYPAL-5716.app_switch_functionality': false, + }, + }, + }); await strategy.initialize(initializationOptions); expect(paypalSdk.Buttons).toHaveBeenCalledWith({ @@ -381,10 +396,29 @@ describe('PayPalCommerceButtonStrategy', () => { }); }); + it('calls PayPal button resume', async () => { + jest.spyOn( + paymentIntegrationService.getState(), + 'getStoreConfigOrThrow', + ).mockReturnValue({ + ...storeConfig, + checkoutSettings: { + ...storeConfig.checkoutSettings, + features: { + 'PAYPAL-5716.app_switch_functionality': true, + }, + }, + }); + await strategy.initialize(initializationOptions); + + expect(resumeMock).toHaveBeenCalled(); + }); + it('initializes PayPal button to render (buy now flow)', async () => { await strategy.initialize(buyNowInitializationOptions); expect(paypalSdk.Buttons).toHaveBeenCalledWith({ + appSwitchWhenAvailable: true, fundingSource: paypalSdk.FUNDING.PAYPAL, style: paypalCommerceOptions.style, createOrder: expect.any(Function), @@ -521,10 +555,46 @@ describe('PayPalCommerceButtonStrategy', () => { defaultButtonContainerId, ); }); + + it('initializes PayPal button to render with appSwitch flag', async () => { + jest.spyOn( + paymentIntegrationService.getState(), + 'getStoreConfigOrThrow', + ).mockReturnValue({ + ...storeConfig, + checkoutSettings: { + ...storeConfig.checkoutSettings, + features: { + 'PAYPAL-5716.app_switch_functionality': true, + }, + }, + }); + await strategy.initialize(initializationOptions); + + expect(paypalSdk.Buttons).toHaveBeenCalledWith({ + appSwitchWhenAvailable: true, + fundingSource: paypalSdk.FUNDING.PAYPAL, + style: paypalCommerceOptions.style, + createOrder: expect.any(Function), + onApprove: expect.any(Function), + }); + }); }); describe('#createOrder', () => { it('creates paypal order', async () => { + jest.spyOn( + paymentIntegrationService.getState(), + 'getStoreConfigOrThrow', + ).mockReturnValue({ + ...storeConfig, + checkoutSettings: { + ...storeConfig.checkoutSettings, + features: { + 'PAYPAL-5716.app_switch_functionality': false, + }, + }, + }); await strategy.initialize(initializationOptions); eventEmitter.emit('createOrder'); diff --git a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts index 948103f197..b892f1273a 100644 --- a/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts +++ b/packages/paypal-commerce-integration/src/paypal-commerce/paypal-commerce-button-strategy.ts @@ -21,8 +21,11 @@ import { import PayPalCommerceButtonInitializeOptions, { WithPayPalCommerceButtonInitializeOptions, } from './paypal-commerce-button-initialize-options'; +import { isExperimentEnabled } from '@bigcommerce/checkout-sdk/utility'; export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrategy { + private methodId?: string; + constructor( private paymentIntegrationService: PaymentIntegrationService, private paypalCommerceIntegrationService: PayPalCommerceIntegrationService, @@ -68,6 +71,8 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat ); } + this.methodId = methodId; + if (!isBuyNowFlow) { // Info: default checkout should not be loaded for BuyNow flow, // since there is no checkout session available for that. @@ -105,7 +110,9 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat paymentMethod.initializationData || {}; const defaultCallbacks = { - createOrder: () => this.paypalCommerceIntegrationService.createOrder('paypalcommerce'), + ...(this.isPaypalCommerceAppSwitchEnabled() && { appSwitchWhenAvailable: true }), + createOrder: () => + this.paypalCommerceIntegrationService.createOrder('paypalcommerce'), onApprove: ({ orderID }: ApproveCallbackPayload) => this.paypalCommerceIntegrationService.tokenizePayment(methodId, orderID), }; @@ -137,7 +144,11 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat const paypalButton = paypalSdk.Buttons(buttonRenderOptions); if (paypalButton.isEligible()) { - paypalButton.render(`#${containerId}`); + if (paypalButton.hasReturned?.() && this.isPaypalCommerceAppSwitchEnabled()) { + paypalButton.resume?.(); + } else { + paypalButton.render(`#${containerId}`); + } } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { onEligibilityFailure(); } else { @@ -253,4 +264,23 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat throw error; } } + + /** + * + * PayPal AppSwitch enabling handling + * + */ + private isPaypalCommerceAppSwitchEnabled(): boolean { + const state = this.paymentIntegrationService.getState(); + const features = state.getStoreConfigOrThrow().checkoutSettings.features; + const paymentMethod = state.getPaymentMethodOrThrow( + this.methodId || '', + ); + const { isHostedCheckoutEnabled } = paymentMethod.initializationData || {}; + + return ( + !isHostedCheckoutEnabled && + isExperimentEnabled(features, 'PAYPAL-5716.app_switch_functionality') + ); + } }