Skip to content

Paypal 5258 #2818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getScriptLoader } from '@bigcommerce/script-loader';

import {
BraintreeLocalPayment,
BraintreeOrderStatus,
BraintreeScriptLoader,
BraintreeSdk,
getBraintreeLocalPaymentMock,
Expand Down Expand Up @@ -35,12 +36,15 @@ import {
} from '../mocks/braintree.mock';

import BraintreeLocalMethodsPaymentStrategy from './braintree-local-methods-payment-strategy';
import BraintreeRequestSender from '../braintree-request-sender';
import { createRequestSender } from '@bigcommerce/request-sender';

describe('BraintreeLocalMethodsPaymentStrategy', () => {
let strategy: BraintreeLocalMethodsPaymentStrategy;
let paymentIntegrationService: PaymentIntegrationService;
let braintreeLocalPaymentMock: BraintreeLocalPayment;
let braintreeSdk: BraintreeSdk;
let braintreeRequestSender: BraintreeRequestSender;
let braintreeScriptLoader: BraintreeScriptLoader;
let paymentMethodMock: PaymentMethod;
let storeConfigMock: StoreConfig;
Expand All @@ -60,6 +64,8 @@ describe('BraintreeLocalMethodsPaymentStrategy', () => {
braintreelocalmethods,
};

braintreeRequestSender = new BraintreeRequestSender(createRequestSender());

beforeEach(() => {
paymentIntegrationService = new PaymentIntegrationServiceMock();
braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window);
Expand All @@ -68,6 +74,7 @@ describe('BraintreeLocalMethodsPaymentStrategy', () => {
strategy = new BraintreeLocalMethodsPaymentStrategy(
paymentIntegrationService,
braintreeSdk,
braintreeRequestSender,
loadingIndicator,
);

Expand Down Expand Up @@ -316,6 +323,74 @@ describe('BraintreeLocalMethodsPaymentStrategy', () => {
);
});

it('initialize polling mechanism', async () => {
jest.spyOn(braintreeRequestSender, 'getOrderStatus').mockResolvedValue({
status: BraintreeOrderStatus.Approved,
});

const validBraintreeResponse = {
body: {
additional_action_required: {
data: {
order_id_saved_successfully: true, // This property is required
},
},
},
};
jest.spyOn(paymentIntegrationService, 'submitOrder').mockRejectedValue(
validBraintreeResponse
);
const payload = {
payment: {
methodId: 'braintreelocalmethods',
gatewayId: 'braintreelocalmethods',
},
};

await strategy.initialize(initializationOptions);

try {
await strategy.execute(payload);
} catch (error) {
expect(braintreeRequestSender.getOrderStatus).toHaveBeenCalled();
}

});

it('stop polling mechanism if corresponding status received', async () => {
const validBraintreeResponse = {
body: {
additional_action_required: {
data: {
order_id_saved_successfully: true,
},
},
},
};
jest.spyOn(paymentIntegrationService, 'submitOrder').mockRejectedValue(
validBraintreeResponse
);
jest.spyOn(braintreeRequestSender, 'getOrderStatus').mockResolvedValue({
status: BraintreeOrderStatus.PollingError,
});

const payload = {
payment: {
methodId: 'braintreelocalmethods',
gatewayId: 'braintreelocalmethods',
},
};

jest.spyOn(global, 'clearTimeout');

try {
await strategy.initialize(initializationOptions);
await strategy.execute(payload);
} catch (e) {
expect(clearTimeout).toHaveBeenCalled();
}
});

it('starts Braintree LPM flow (opens popup) when orderId was successfully saved on BE side', async () => {
const startPaymentMock = jest.fn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
BraintreeLPMPaymentStartData,
BraintreeLPMStartPaymentError,
BraintreeOrderSavedResponse,
BraintreeOrderStatus,
BraintreeRedirectError,
BraintreeSdk,
NonInstantLocalPaymentMethods,
Expand All @@ -32,24 +33,36 @@ import {
BraintreeLocalMethodsPaymentInitializeOptions,
WithBraintreeLocalMethodsPaymentInitializeOptions,
} from './braintree-local-methods-payment-initialize-options';
import { noop } from 'lodash';
import BraintreeRequestSender from '../braintree-request-sender';

const POLLING_INTERVAL = 3000;
const MAX_POLLING_TIME = 300000;

export default class BraintreeLocalMethodsPaymentStrategy implements PaymentStrategy {
private braintreelocalmethods?: BraintreeLocalMethodsPaymentInitializeOptions;
private braintreeLocalPayment?: BraintreeLocalPayment;
private loadingIndicatorContainer?: string;
private orderId?: string;
private gatewayId?: string;
private isLPMsUpdateExperimentEnabled = false;
private pollingTimer = 0;
private stopPolling = noop;

constructor(
private paymentIntegrationService: PaymentIntegrationService,
private braintreeSdk: BraintreeSdk,
private braintreeRequestSender: BraintreeRequestSender,
private loadingIndicator: LoadingIndicator,
private pollingInterval: number = POLLING_INTERVAL,
private maxPollingIntervalTime: number = MAX_POLLING_TIME,
) {}

async initialize(
options: PaymentInitializeOptions & WithBraintreeLocalMethodsPaymentInitializeOptions,
): Promise<void> {
const { gatewayId, methodId, braintreelocalmethods } = options;
this.gatewayId = gatewayId;

if (!methodId) {
throw new InvalidArgumentError(
Expand Down Expand Up @@ -244,14 +257,22 @@ export default class BraintreeLocalMethodsPaymentStrategy implements PaymentStra
paymentData,
});
} catch (error: unknown) {

if (
this.isBraintreeOrderSavedResponse(error) &&
error.body.additional_action_required.data.order_id_saved_successfully
) {
// Start method call initiates the popup
start();

return;
return await new Promise((resolve, reject) => {
this.initializePollingMechanism(
methodId,
resolve,
reject,
this.gatewayId,
);
});
}

throw error;
Expand Down Expand Up @@ -334,6 +355,7 @@ export default class BraintreeLocalMethodsPaymentStrategy implements PaymentStra
private handleError(error: unknown) {
const { onError } = this.braintreelocalmethods || {};

this.resetPollingMechanism();
this.toggleLoadingIndicator(false);

if (onError && typeof onError === 'function') {
Expand Down Expand Up @@ -379,4 +401,91 @@ export default class BraintreeLocalMethodsPaymentStrategy implements PaymentStra

return body.additional_action_required?.data.hasOwnProperty('order_id_saved_successfully');
}

/**
*
* Polling mechanism
*
*
* */
private async initializePollingMechanism(
methodId: string,
resolvePromise: () => void,
rejectPromise: () => void,
gatewayId?: string,
): Promise<void> {
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(resolve, this.pollingInterval);

this.stopPolling = () => {
clearTimeout(timeout);
this.toggleLoadingIndicator(false);

return reject();
};
});

try {
this.pollingTimer += this.pollingInterval;

const orderStatus = await this.braintreeRequestSender.getOrderStatus(
'braintreelocalmethods',
{
params: {
useMetadata: false,
},
},
);

const isOrderPending = orderStatus.status === BraintreeOrderStatus.Pending;
const isOrderApproved = orderStatus.status === BraintreeOrderStatus.Approved;
const isPollingError = orderStatus.status === BraintreeOrderStatus.PollingError;

if (isOrderApproved) {
this.deinitializePollingMechanism();

return resolvePromise();
}

if (isPollingError) {
return rejectPromise();
}

if (!isOrderApproved && isOrderPending && this.pollingTimer < this.maxPollingIntervalTime) {
return await this.initializePollingMechanism(
methodId,
resolvePromise,
rejectPromise,
gatewayId,
);
}

this.reinitializeStrategy({
methodId,
gatewayId,
braintreelocalmethods: this.braintreelocalmethods,
});

this.handleError(new Error('Timeout Error'));
} catch (error) {
this.handleError(error);
rejectPromise();
}
}

private deinitializePollingMechanism(): void {
this.stopPolling();
this.pollingTimer = 0;
}

private resetPollingMechanism(): void {
this.deinitializePollingMechanism();
}

private reinitializeStrategy(
options: PaymentInitializeOptions & WithBraintreeLocalMethodsPaymentInitializeOptions
) {
this.deinitialize();
this.initialize(options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
import { LoadingIndicator } from '@bigcommerce/checkout-sdk/ui';

import BraintreeLocalMethodsPaymentStrategy from './braintree-local-methods-payment-strategy';
import BraintreeRequestSender from '../braintree-request-sender';
import { createRequestSender } from '@bigcommerce/request-sender';

const createBraintreeLocalMethodsPaymentStrategy: PaymentStrategyFactory<
BraintreeLocalMethodsPaymentStrategy
Expand All @@ -20,10 +22,13 @@ const createBraintreeLocalMethodsPaymentStrategy: PaymentStrategyFactory<
const braintreeSdk = new BraintreeSdk(
new BraintreeScriptLoader(getScriptLoader(), braintreeHostWindow),
);
const requestSender = createRequestSender();
const braintreeRequestSender = new BraintreeRequestSender(requestSender);

return new BraintreeLocalMethodsPaymentStrategy(
paymentIntegrationService,
braintreeSdk,
braintreeRequestSender,
new LoadingIndicator({ styles: { backgroundColor: 'black' } }),
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createRequestSender, RequestSender } from '@bigcommerce/request-sender';
import BraintreeRequestSender from './braintree-request-sender';
import { getResponse } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils';
import { ContentType, INTERNAL_USE_ONLY, SDK_VERSION_HEADERS } from '@bigcommerce/checkout-sdk/payment-integration-api';

describe('BraintreeRequestSender', () => {
let requestSender: RequestSender;
let braintreeRequestSender: BraintreeRequestSender;

beforeEach(() => {
requestSender = createRequestSender();
braintreeRequestSender = new BraintreeRequestSender(requestSender);

const requestResponseMock = getResponse({ orderId: 123 });

jest.spyOn(requestSender, 'get').mockReturnValue(Promise.resolve(requestResponseMock));
jest.spyOn(requestSender, 'post').mockReturnValue(Promise.resolve(requestResponseMock));
jest.spyOn(requestSender, 'put').mockReturnValue(Promise.resolve(requestResponseMock));
});

it('requests order status', async () => {
const headers = {
'X-API-INTERNAL': INTERNAL_USE_ONLY,
'Content-Type': ContentType.Json,
...SDK_VERSION_HEADERS,
};

await braintreeRequestSender.getOrderStatus();

expect(requestSender.get).toHaveBeenCalledWith(
'/api/storefront/initialization/braintreelocalmethods',
expect.objectContaining({
headers,
}),
);
});

it('requests order status with proper data', async () => {
const headers = {
'X-API-INTERNAL': INTERNAL_USE_ONLY,
'Content-Type': ContentType.Json,
...SDK_VERSION_HEADERS,
};

await braintreeRequestSender.getOrderStatus('braintreelocalmethods', {
params: { useMetaData: false },
});

expect(requestSender.get).toHaveBeenCalledWith(
'/api/storefront/initialization/braintreelocalmethods',
expect.objectContaining({
headers,
}),
);
});
});
31 changes: 31 additions & 0 deletions packages/braintree-integration/src/braintree-request-sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { RequestSender } from '@bigcommerce/request-sender';
import {
ContentType,
INTERNAL_USE_ONLY,
RequestOptions,
SDK_VERSION_HEADERS,
} from '@bigcommerce/checkout-sdk/payment-integration-api';
import { BraintreeOrderStatusData } from '@bigcommerce/checkout-sdk/braintree-utils';

export default class BraintreeRequestSender {
constructor(private requestSender: RequestSender) {}

async getOrderStatus(
methodId = 'braintreelocalmethods',
options?: RequestOptions,
): Promise<BraintreeOrderStatusData> {
const url = `/api/storefront/initialization/${methodId}`;
const headers = {
'X-API-INTERNAL': INTERNAL_USE_ONLY,
'Content-Type': ContentType.Json,
...SDK_VERSION_HEADERS,
};

const res = await this.requestSender.get<BraintreeOrderStatusData>(url, {
headers,
...options,
});

return res.body;
}
}
Loading