Skip to content
Merged
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 @@ -2,6 +2,7 @@ import {
PaymentStrategyFactory,
toResolvableModule,
} from '@bigcommerce/checkout-sdk/payment-integration-api';
import { createPayPalCommerceSdk } from '@bigcommerce/checkout-sdk/paypal-commerce-utils';
import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui';

import createPayPalCommerceIntegrationService from '../create-paypal-commerce-integration-service';
Expand All @@ -15,6 +16,7 @@ const createPayPalCommerceAlternativeMethodsPaymentStrategy: PaymentStrategyFact
new PayPalCommerceAlternativeMethodsPaymentStrategy(
paymentIntegrationService,
createPayPalCommerceIntegrationService(paymentIntegrationService),
createPayPalCommerceSdk(),
new LoadingIndicator({
containerStyles: LOADING_INDICATOR_STYLES,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
getBillingAddress,
PaymentIntegrationServiceMock,
} from '@bigcommerce/checkout-sdk/payment-integrations-test-utils';
import {
createPayPalCommerceSdk,
PayPalCommerceSdk,
} from '@bigcommerce/checkout-sdk/paypal-commerce-utils';
import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui';

import {
Expand Down Expand Up @@ -43,6 +47,7 @@ describe('PayPalCommerceAlternativeMethodsPaymentStrategy', () => {
let paypalCommerceIntegrationService: PayPalCommerceIntegrationService;
let paypalSdk: PayPalSDK;
let strategy: PayPalCommerceAlternativeMethodsPaymentStrategy;
let paypalCommerceSdk: PayPalCommerceSdk;

const paypalOrderId = 'paypal123';

Expand Down Expand Up @@ -78,10 +83,12 @@ describe('PayPalCommerceAlternativeMethodsPaymentStrategy', () => {
loadingIndicator = new LoadingIndicator();
paypalCommerceIntegrationService = getPayPalCommerceIntegrationServiceMock();
paymentIntegrationService = new PaymentIntegrationServiceMock();
paypalCommerceSdk = createPayPalCommerceSdk();

strategy = new PayPalCommerceAlternativeMethodsPaymentStrategy(
paymentIntegrationService,
paypalCommerceIntegrationService,
paypalCommerceSdk,
loadingIndicator,
);

Expand All @@ -93,10 +100,7 @@ describe('PayPalCommerceAlternativeMethodsPaymentStrategy', () => {
'getBillingAddressOrThrow',
).mockReturnValue(billingAddress);

jest.spyOn(paypalCommerceIntegrationService, 'loadPayPalSdk').mockReturnValue(paypalSdk);
jest.spyOn(paypalCommerceIntegrationService, 'getPayPalSdkOrThrow').mockReturnValue(
paypalSdk,
);
jest.spyOn(paypalCommerceSdk, 'getPayPalApmsSdk').mockReturnValue(paypalSdk);
jest.spyOn(paypalCommerceIntegrationService, 'createOrder').mockReturnValue(paypalOrderId);
jest.spyOn(paypalCommerceIntegrationService, 'submitPayment').mockReturnValue(undefined);
jest.spyOn(paypalCommerceIntegrationService, 'getOrderStatus').mockReturnValue(
Expand Down Expand Up @@ -223,15 +227,13 @@ describe('PayPalCommerceAlternativeMethodsPaymentStrategy', () => {

await strategy.initialize(initializationOptions);

expect(paypalCommerceIntegrationService.loadPayPalSdk).not.toHaveBeenCalled();
expect(paypalCommerceSdk.getPayPalApmsSdk).not.toHaveBeenCalled();
});

it('loads paypal sdk', async () => {
await strategy.initialize(initializationOptions);

expect(paypalCommerceIntegrationService.loadPayPalSdk).toHaveBeenCalledWith(
defaultMethodId,
);
expect(paypalCommerceSdk.getPayPalApmsSdk).toHaveBeenCalledWith(paymentMethod, 'USD');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
PaymentArgumentInvalidError,
PaymentInitializeOptions,
PaymentIntegrationService,
PaymentMethodClientUnavailableError,
PaymentMethodInvalidError,
PaymentRequestOptions,
PaymentStrategy,
} from '@bigcommerce/checkout-sdk/payment-integration-api';
import { PayPalApmSdk, PayPalCommerceSdk } from '@bigcommerce/checkout-sdk/paypal-commerce-utils';
import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui';

import PayPalCommerceIntegrationService from '../paypal-commerce-integration-service';
Expand All @@ -30,10 +32,12 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
private loadingIndicatorContainer?: string;
private orderId?: string;
private paypalButton?: PayPalCommerceButtons;
private paypalApms?: PayPalApmSdk;

constructor(
private paymentIntegrationService: PaymentIntegrationService,
private paypalCommerceIntegrationService: PayPalCommerceIntegrationService,
private paypalCommerceSdk: PayPalCommerceSdk,
private loadingIndicator: LoadingIndicator,
) {}

Expand Down Expand Up @@ -84,7 +88,10 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
return;
}

await this.paypalCommerceIntegrationService.loadPayPalSdk(methodId);
this.paypalApms = await this.paypalCommerceSdk.getPayPalApmsSdk(
paymentMethod,
state.getCartOrThrow().currency.code,
);

this.loadingIndicatorContainer = paypalOptions.container.split('#')[1];

Expand Down Expand Up @@ -141,7 +148,7 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
gatewayId: string,
paypalOptions: PayPalCommerceAlternativeMethodsPaymentOptions,
): void {
const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow();
const paypalAmpsSdk = this.getPaypalAmpsSdkOrThrow();

const state = this.paymentIntegrationService.getState();
const paymentMethod = state.getPaymentMethodOrThrow<PayPalCommerceInitializationData>(
Expand All @@ -164,7 +171,7 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
paypalOptions.onValidate(actions.resolve, actions.reject),
};

this.paypalButton = paypalSdk.Buttons(buttonOptions);
this.paypalButton = paypalAmpsSdk.Buttons(buttonOptions);

if (!this.paypalButton.isEligible()) {
return;
Expand Down Expand Up @@ -241,7 +248,7 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
methodId: string,
paypalOptions: PayPalCommerceAlternativeMethodsPaymentOptions,
): void {
const paypalSdk = this.paypalCommerceIntegrationService.getPayPalSdkOrThrow();
const paypalAmpsSdk = this.getPaypalAmpsSdkOrThrow();
const state = this.paymentIntegrationService.getState();
const { firstName, lastName, email } = state.getBillingAddressOrThrow();

Expand Down Expand Up @@ -272,7 +279,7 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
},
};

const paypalPaymentFields = paypalSdk.PaymentFields(fieldsOptions);
const paypalPaymentFields = paypalAmpsSdk.PaymentFields(fieldsOptions);

paypalPaymentFields.render(apmFieldsContainer);
}
Expand All @@ -298,4 +305,12 @@ export default class PayPalCommerceAlternativeMethodsPaymentStrategy implements
private isNonInstantPaymentMethod(methodId: string): boolean {
return methodId.toUpperCase() in NonInstantAlternativePaymentMethods;
}

private getPaypalAmpsSdkOrThrow() {
if (!this.paypalApms) {
throw new PaymentMethodClientUnavailableError();
}

return this.paypalApms;
}
}
68 changes: 68 additions & 0 deletions packages/paypal-commerce-utils/src/paypal-commerce-sdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { getPayPalCommerceAcceleratedCheckoutPaymentMethod, getPayPalFastlaneSdk } from './mocks';
import PayPalCommerceSdk from './paypal-commerce-sdk';
import {
PayPalApmSdk,
PayPalCommerceHostWindow,
PayPalFastlaneSdk,
PayPalMessagesSdk,
Expand All @@ -19,23 +20,39 @@ describe('PayPalCommerceSdk', () => {
let paymentMethod: PaymentMethod;
let paypalFastlaneSdk: PayPalFastlaneSdk;
let subject: PayPalCommerceSdk;
let mockAPMPaymentMethod: PaymentMethod;

const paypalMessagesSdk: PayPalMessagesSdk = {
Messages: jest.fn(),
};

const paypalApmsSdk: PayPalApmSdk = {
Buttons: jest.fn(),
PaymentFields: jest.fn(),
};

const sessionId = '8a232bf4-d9ba-4621-a1a9-ed8f685f92d1';
const expectedSessionId = sessionId.replace(/-/g, '');

beforeEach(() => {
loader = createScriptLoader();
paymentMethod = getPayPalCommerceAcceleratedCheckoutPaymentMethod();
mockAPMPaymentMethod = {
...paymentMethod,
id: 'oxxo',
initializationData: {
...paymentMethod.initializationData,
enabledAlternativePaymentMethods: ['oxxo'],
availableAlternativePaymentMethods: ['oxxo'],
},
};
paypalFastlaneSdk = getPayPalFastlaneSdk();
subject = new PayPalCommerceSdk(loader);

jest.spyOn(loader, 'loadScript').mockImplementation(() => {
(window as PayPalCommerceHostWindow).paypalFastlaneSdk = paypalFastlaneSdk;
(window as PayPalCommerceHostWindow).paypalMessages = paypalMessagesSdk;
(window as PayPalCommerceHostWindow).paypalApms = paypalApmsSdk;

return Promise.resolve();
});
Expand All @@ -44,6 +61,7 @@ describe('PayPalCommerceSdk', () => {
afterEach(() => {
(window as PayPalCommerceHostWindow).paypalFastlaneSdk = undefined;
(window as PayPalCommerceHostWindow).paypalMessages = undefined;
(window as PayPalCommerceHostWindow).paypalApms = undefined;

jest.clearAllMocks();
});
Expand Down Expand Up @@ -175,4 +193,54 @@ describe('PayPalCommerceSdk', () => {
expect(result).toEqual(paypalMessagesSdk);
});
});

describe('#getPayPalApmsSdk()', () => {
it('throws an error if clientId is not defined in payment method while getting configuration for PayPal Sdk', async () => {
try {
await subject.getPayPalApmsSdk(
{
...mockAPMPaymentMethod,
initializationData: {
...mockAPMPaymentMethod.initializationData,
clientId: undefined,
},
},
'USD',
);
} catch (error: unknown) {
expect(error).toBeInstanceOf(MissingDataError);
}
});

it('loads APMs sdk script', async () => {
await subject.getPayPalApmsSdk(mockAPMPaymentMethod, 'USD');

expect(loader.loadScript).toHaveBeenCalledWith(
'https://www.paypal.com/sdk/js?client-id=abc&merchant-id=JTS4DY7XFSQZE&enable-funding=oxxo&commit=true&components=buttons%2Cpayment-fields&currency=USD&intent=capture',
{
async: true,
attributes: {
'data-namespace': 'paypalApms',
'data-partner-attribution-id': '1123JLKJASD12',
},
},
);
});

it('throws an error if there was an issue with loading APMs sdk', async () => {
jest.spyOn(loader, 'loadScript').mockImplementation(jest.fn());

try {
await subject.getPayPalApmsSdk(mockAPMPaymentMethod, 'USD');
} catch (error: unknown) {
expect(error).toBeInstanceOf(PaymentMethodClientUnavailableError);
}
});

it('returns PayPal APMs Sdk', async () => {
const result = await subject.getPayPalApmsSdk(paymentMethod, 'USD');

expect(result).toEqual(paypalApmsSdk);
});
});
});
62 changes: 62 additions & 0 deletions packages/paypal-commerce-utils/src/paypal-commerce-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ export default class PayPalCommerceSdk {
return this.window.paypalFastlaneSdk;
}

async getPayPalApmsSdk(
paymentMethod: PaymentMethod<PayPalCommerceInitializationData>,
currencyCode: string,
) {
if (!this.window.paypalApms) {
const config = this.getPayPalApmSdkConfiguration(paymentMethod, currencyCode);

await this.loadPayPalSdk(config);

if (!this.window.paypalApms) {
throw new PaymentMethodClientUnavailableError();
}
}

return this.window.paypalApms;
}

async getPayPalMessages(
paymentMethod: PaymentMethod<PayPalCommerceInitializationData>,
currencyCode: string,
Expand Down Expand Up @@ -126,6 +143,51 @@ export default class PayPalCommerceSdk {
};
}

private getPayPalApmSdkConfiguration(
paymentMethod: PaymentMethod<PayPalCommerceInitializationData>,
currencyCode: string,
): PayPalSdkConfig {
const { initializationData } = paymentMethod;

if (!initializationData || !initializationData.clientId) {
throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod);
}

const {
intent,
clientId,
merchantId,
buyerCountry,
attributionId,
isDeveloperModeApplicable,
availableAlternativePaymentMethods = [],
enabledAlternativePaymentMethods = [],
} = initializationData;

const enableAPMsFunding = enabledAlternativePaymentMethods;
const disableAPMsFunding = availableAlternativePaymentMethods.filter(
(apm: string) => !enabledAlternativePaymentMethods.includes(apm),
);

return {
options: {
'client-id': clientId,
'merchant-id': merchantId,
'enable-funding': enableAPMsFunding.length > 0 ? enableAPMsFunding : undefined,
'disable-funding': disableAPMsFunding.length > 0 ? disableAPMsFunding : undefined,
commit: true,
components: ['buttons', 'payment-fields'],
currency: currencyCode,
intent,
...(isDeveloperModeApplicable && { 'buyer-country': buyerCountry }),
},
attributes: {
'data-partner-attribution-id': attributionId,
'data-namespace': 'paypalApms',
},
};
}

private getPayPalSdkMessagesConfiguration(
paymentMethod: PaymentMethod<PayPalCommerceInitializationData>,
currencyCode: string,
Expand Down
Loading