Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ export interface PayPalCommerceButtons {
render(id: string): void;
close(): void;
isEligible(): boolean;
hasReturned?(): boolean;
resume?(): void;
}

export interface PayPalCommerceButtonsOptions {
Expand Down Expand Up @@ -607,6 +609,7 @@ export interface PayPalCreateOrderRequestBody extends HostedInstrument, VaultedI
metadataId?: string;
setupToken?: boolean;
fastlaneToken?: string;
userAgent?: string;
}

export enum PayPalOrderStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getBuyNowCart,
getBuyNowCartRequestBody,
getCart,
getConfig,
getConsignment,
getShippingOption,
PaymentIntegrationServiceMock,
Expand Down Expand Up @@ -101,6 +102,9 @@ describe('PayPalCommerceButtonStrategy', () => {
type: 'type_shipping',
};

const storeConfig = getConfig().storeConfig;
const resumeMock = jest.fn();

beforeEach(() => {
buyNowCart = getBuyNowCart();
cart = getCart();
Expand Down Expand Up @@ -241,6 +245,8 @@ describe('PayPalCommerceButtonStrategy', () => {
isEligible: jest.fn(() => true),
render: jest.fn(),
close: jest.fn(),
hasReturned: jest.fn().mockReturnValue(true),
resume: resumeMock,
};
},
);
Expand Down Expand Up @@ -368,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({
Expand All @@ -378,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),
Expand Down Expand Up @@ -480,10 +517,74 @@ 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');

await new Promise((resolve) => process.nextTick(resolve));

expect(paypalCommerceIntegrationService.createOrder).toHaveBeenCalledWith(
'paypalcommerce',
);
});

it('creates paypal order with user agent', async () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla',
configurable: true,
});
jest.spyOn(
paymentIntegrationService.getState(),
'getStoreConfigOrThrow',
).mockReturnValue({
...storeConfig,
checkoutSettings: {
...storeConfig.checkoutSettings,
features: {
'PAYPAL-5716.app_switch_functionality': true,
},
},
});
await strategy.initialize(initializationOptions);

eventEmitter.emit('createOrder');
Expand All @@ -492,6 +593,7 @@ describe('PayPalCommerceButtonStrategy', () => {

expect(paypalCommerceIntegrationService.createOrder).toHaveBeenCalledWith(
'paypalcommerce',
{ userAgent: 'Mozilla' },
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -96,6 +101,7 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat
paypalcommerce: PayPalCommerceButtonInitializeOptions,
): void {
const { buyNowInitializeOptions, style, onComplete, onEligibilityFailure } = paypalcommerce;
const userAgent = navigator?.userAgent || '';

const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow();
const state = this.paymentIntegrationService.getState();
Expand All @@ -104,7 +110,12 @@ export default class PayPalCommerceButtonStrategy implements CheckoutButtonStrat
const { isHostedCheckoutEnabled } = paymentMethod.initializationData || {};

const defaultCallbacks = {
createOrder: () => this.paypalCommerceIntegrationService.createOrder('paypalcommerce'),
...(this.isPaypalCommerceAppSwitchEnabled() && { appSwitchWhenAvailable: true }),
createOrder: () =>
this.paypalCommerceIntegrationService.createOrder(
'paypalcommerce',
...(this.isPaypalCommerceAppSwitchEnabled() ? [{ userAgent }] : []),
),
onApprove: ({ orderID }: ApproveCallbackPayload) =>
this.paypalCommerceIntegrationService.tokenizePayment(methodId, orderID),
};
Expand Down Expand Up @@ -134,7 +145,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 {
Expand Down Expand Up @@ -250,4 +265,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<PayPalCommerceInitializationData>(
this.methodId || '',
);
const { isHostedCheckoutEnabled } = paymentMethod.initializationData || {};

return (
!isHostedCheckoutEnabled &&
isExperimentEnabled(features, 'PAYPAL-5716.app_switch_functionality')
);
}
}