Skip to content
24 changes: 23 additions & 1 deletion packages/core/src/common/http-request/responses.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Response } from '@bigcommerce/request-sender';

import { ErrorResponseBody } from '@bigcommerce/checkout-sdk/payment-integration-api';

import { PaymentResponse } from '../../payment';
import { GqlPaymentMethodResponse, PaymentResponse } from '../../payment';
import { HeadlessPaymentMethod } from '../../payment/gql-payment';

export function getResponse<T>(
body: T,
Expand Down Expand Up @@ -38,6 +39,27 @@ export function getPaymentResponse<T>(
};
}

export function getHeadlessPaymentResponse(
site: HeadlessPaymentMethod,
headers = {},
status = 200,
statusText = 'OK',
): Response<GqlPaymentMethodResponse> {
return {
body: {
data: {
site,
},
},
status,
statusText,
headers: {
'content-type': 'application/json',
...headers,
},
};
}

export function getErrorResponse(
body = getErrorResponseBody(),
headers = {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GqlPaymentMethodType } from './gql-payment-method-type';

const GqlPaymentMethodConfig: Record<string, GqlPaymentMethodType> = {
paypalcommerce: GqlPaymentMethodType.PAYPALCOMMERCE,
paypalcommercecredit: GqlPaymentMethodType.PAYPALCOMMERCECREDIT,
braintree: GqlPaymentMethodType.BRAINTREE,
};

export default GqlPaymentMethodConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import GqlPaymentMethod from './gql-payment-method';

export interface GqlPaymentMethodResponse {
data: {
site: GqlPaymentMethod;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum GqlPaymentMethodType {
PAYPALCOMMERCE = 'paypalcommerce.paypal',
PAYPALCOMMERCECREDIT = 'paypalcommerce.paypalcredit',
BRAINTREE = 'braintree.paypal',
}
7 changes: 7 additions & 0 deletions packages/core/src/payment/gql-payment/gql-payment-method.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default interface GqlPaymentMethod {
paymentWalletWithInitializationData: {
clientToken?: string;
// INFO:: initializationData given in base64 format
initializationData?: string;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RequestOptions } from '../../common/http-request';

export default interface GqlPaymentRequestOptions extends RequestOptions {
body: { entityId: string };
}
6 changes: 6 additions & 0 deletions packages/core/src/payment/gql-payment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as GqlPaymentMethod } from './gql-payment-method';
export { default as GqlPaymentMethodConfig } from './gql-payment-method-config';
export { default as GqlPaymentRequestOptions } from './gql-payment-request-options';

export { GqlPaymentMethodType } from './gql-payment-method-type';
export { GqlPaymentMethodResponse } from './gql-payment-method-response';
19 changes: 19 additions & 0 deletions packages/core/src/payment/headless-payment-methods.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GqlPaymentMethod } from './gql-payment';

export const initializationData = {
merchantId: '100000',
paymentButtonStyles: {
checkoutTopButtonStyles: { color: 'blue', label: 'checkout', height: '36' },
},
};

export const encodedInitializationData = btoa(JSON.stringify(initializationData));

export function getHeadlessPaymentMethod(): GqlPaymentMethod {
return {
paymentWalletWithInitializationData: {
clientToken: 'clientToken',
initializationData: encodedInitializationData,
},
};
}
6 changes: 6 additions & 0 deletions packages/core/src/payment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export { default as isHostedInstrumentLike } from './is-hosted-intrument-like';
export { default as isNonceLike } from './is-nonce-like';
export { default as isVaultedInstrument } from './is-vaulted-instrument';
export { default as PaymentActionCreator } from './payment-action-creator';
export {
GqlPaymentMethod,
GqlPaymentMethodConfig,
GqlPaymentRequestOptions,
GqlPaymentMethodResponse,
} from './gql-payment';
export {
default as Payment,
CreditCardInstrument,
Expand Down
120 changes: 120 additions & 0 deletions packages/core/src/payment/payment-method-action-creator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ describe('PaymentMethodActionCreator', () => {
Promise.resolve(paymentMethodsResponse),
);

jest.spyOn(
paymentMethodRequestSender,
'loadPaymentWalletWithInitializationData',
).mockReturnValue(Promise.resolve(paymentMethodResponse));

jest.spyOn(store.getState().cart, 'getCartOrThrow').mockReturnValue(getCheckout().cart);
});

Expand Down Expand Up @@ -195,6 +200,121 @@ describe('PaymentMethodActionCreator', () => {
});
});

describe('#loadPaymentWalletWithInitializationData()', () => {
it('loads payment wallet method', async () => {
const methodId = 'braintree';

await from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(methodId)(store),
).toPromise();

expect(
paymentMethodRequestSender.loadPaymentWalletWithInitializationData,
).toHaveBeenCalledWith(methodId, undefined);
});

it('loads payment wallet method with timeout', async () => {
const methodId = 'braintree';
const options = {
timeout: createTimeout(),
};

await from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(
methodId,
options,
)(store),
).toPromise();

expect(
paymentMethodRequestSender.loadPaymentWalletWithInitializationData,
).toHaveBeenCalledWith(methodId, options);
});

it('emits actions if able to load payment wallet method', async () => {
const methodId = 'braintree';
const actions = await from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(methodId)(store),
)
.pipe(toArray())
.toPromise();

expect(actions).toEqual([
{ type: PaymentMethodActionType.LoadPaymentMethodRequested, meta: { methodId } },
{
type: PaymentMethodActionType.LoadPaymentMethodSucceeded,
meta: { methodId },
payload: paymentMethodResponse.body,
},
]);
});

it('emits actions with cached values if available', async () => {
const methodId = 'braintree';
const options = { useCache: true };
const actions = await merge(
from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(
methodId,
options,
)(store),
),
from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(
methodId,
options,
)(store),
),
)
.pipe(toArray())
.toPromise();

expect(
paymentMethodRequestSender.loadPaymentWalletWithInitializationData,
).toHaveBeenCalledTimes(1);
expect(actions).toEqual([
{ type: PaymentMethodActionType.LoadPaymentMethodRequested, meta: { methodId } },
{ type: PaymentMethodActionType.LoadPaymentMethodRequested, meta: { methodId } },
{
type: PaymentMethodActionType.LoadPaymentMethodSucceeded,
meta: { methodId },
payload: paymentMethodResponse.body,
},
{
type: PaymentMethodActionType.LoadPaymentMethodSucceeded,
meta: { methodId },
payload: paymentMethodResponse.body,
},
]);
});

it('emits error actions if unable to load payment wallet method', async () => {
jest.spyOn(
paymentMethodRequestSender,
'loadPaymentWalletWithInitializationData',
).mockReturnValue(Promise.reject(errorResponse));

const methodId = 'braintree';
const errorHandler = jest.fn((action) => of(action));
const actions = await from(
paymentMethodActionCreator.loadPaymentWalletWithInitializationData(methodId)(store),
)
.pipe(catchError(errorHandler), toArray())
.toPromise();

expect(errorHandler).toHaveBeenCalled();
expect(actions).toEqual([
{ type: PaymentMethodActionType.LoadPaymentMethodRequested, meta: { methodId } },
{
type: PaymentMethodActionType.LoadPaymentMethodFailed,
meta: { methodId },
payload: errorResponse,
error: true,
},
]);
});
});

describe('#loadPaymentMethodsByIds()', () => {
it('loads payment methods data', async () => {
const methodId = 'braintree';
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/payment/payment-method-action-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,43 @@ export default class PaymentMethodActionCreator {
});
}

@cachableAction
loadPaymentWalletWithInitializationData(
methodId: string,
options?: RequestOptions & ActionOptions,
): ThunkAction<LoadPaymentMethodAction, InternalCheckoutSelectors> {
return () =>
Observable.create((observer: Observer<LoadPaymentMethodAction>) => {
observer.next(
createAction(PaymentMethodActionType.LoadPaymentMethodRequested, undefined, {
methodId,
}),
);

this._requestSender
.loadPaymentWalletWithInitializationData(methodId, options)
.then((response) => {
observer.next(
createAction(
PaymentMethodActionType.LoadPaymentMethodSucceeded,
response.body,
{ methodId },
),
);
observer.complete();
})
.catch((response) => {
observer.error(
createErrorAction(
PaymentMethodActionType.LoadPaymentMethodFailed,
response,
{ methodId },
),
);
});
});
}

private _filterApplePay(methods: PaymentMethod[]): PaymentMethod[] {
return filter(methods, (method) => {
if (method.id === APPLEPAYID && !isApplePayWindow(window)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
} from '@bigcommerce/request-sender';

import { ContentType, INTERNAL_USE_ONLY, SDK_VERSION_HEADERS } from '../common/http-request';
import { getResponse } from '../common/http-request/responses.mock';
import { getHeadlessPaymentResponse, getResponse } from '../common/http-request/responses.mock';

import { GqlPaymentMethodResponse } from './gql-payment';
import { getHeadlessPaymentMethod, initializationData } from './headless-payment-methods.mock';
import PaymentMethod from './payment-method';
import PaymentMethodRequestSender from './payment-method-request-sender';
import { getPaymentMethod, getPaymentMethods } from './payment-methods.mock';
Expand Down Expand Up @@ -136,4 +138,43 @@ describe('PaymentMethodRequestSender', () => {
});
});
});

describe('#loadPaymentWalletWithInitializationData()', () => {
let response: Response<GqlPaymentMethodResponse>;

beforeEach(() => {
response = getHeadlessPaymentResponse(getHeadlessPaymentMethod());
jest.spyOn(requestSender, 'post').mockReturnValue(Promise.resolve(response));
});

it('loads headless payment method', async () => {
const walletInitData =
await paymentMethodRequestSender.loadPaymentWalletWithInitializationData(
'paypalcommerce',
);

expect(requestSender.post).toHaveBeenCalledWith(
'http://localhost/api/wallet-buttons/get-initialization-data',
expect.objectContaining({
body: {
entityId: 'paypalcommerce.paypal',
},
}),
);

expect(walletInitData).toEqual(
expect.objectContaining({
body: {
initializationData,
clientToken: 'clientToken',
id: 'paypalcommerce',
config: {},
method: '',
supportedCards: [],
type: 'PAYMENT_TYPE_API',
},
}),
);
});
});
});
Loading