From 9f258ca93f865f3a0273d26a2085f73579da068b Mon Sep 17 00:00:00 2001 From: bc-nick Date: Tue, 7 Jan 2025 13:03:36 +0100 Subject: [PATCH] feat(payment): PAYPAL-4937 added loadPaymentWalletWithInitializationData method --- packages/core/src/config/config-selector.ts | 7 + packages/core/src/config/config-state.ts | 1 + .../headless-payment-method-config.ts | 1 + .../headless-payment-method-type.ts | 1 + .../payment-method-action-creator.spec.ts | 135 ++++++++++++++++++ .../payment/payment-method-action-creator.ts | 51 +++++++ .../src/payment-integration-selectors.ts | 2 + .../payment-integration-service.mock.ts | 1 + 8 files changed, 199 insertions(+) diff --git a/packages/core/src/config/config-selector.ts b/packages/core/src/config/config-selector.ts index b1f94f4244..eb8830834c 100644 --- a/packages/core/src/config/config-selector.ts +++ b/packages/core/src/config/config-selector.ts @@ -18,6 +18,7 @@ export default interface ConfigSelector { getHost(): string | undefined; getLocale(): string | undefined; getVariantIdentificationToken(): string | undefined; + getStorefrontJwtToken(): string | undefined; getLoadError(): Error | undefined; isLoading(): boolean; } @@ -56,6 +57,11 @@ export function createConfigSelectorFactory(): ConfigSelectorFactory { }, ); + const getStorefrontJwtToken = createSelector( + (state: ConfigState) => state.meta && state.meta.storefrontJwtToken, + (data) => () => data, + ); + const getStoreConfig = createSelector( (state: ConfigState) => state.data, (_: ConfigState, { formState }: ConfigSelectorDependencies) => formState && formState.data, @@ -120,6 +126,7 @@ export function createConfigSelectorFactory(): ConfigSelectorFactory { getContextConfig: getContextConfig(state), getExternalSource: getExternalSource(state), getHost: getHost(state), + getStorefrontJwtToken: getStorefrontJwtToken(state), getLocale: getLocale(state), getVariantIdentificationToken: getVariantIdentificationToken(state), getLoadError: getLoadError(state), diff --git a/packages/core/src/config/config-state.ts b/packages/core/src/config/config-state.ts index 15ded51e82..7a39cccc98 100644 --- a/packages/core/src/config/config-state.ts +++ b/packages/core/src/config/config-state.ts @@ -12,6 +12,7 @@ export interface ConfigMetaState { variantIdentificationToken?: string; host?: string; locale?: string; + storefrontJwtToken?: string; } export interface ConfigErrorsState { diff --git a/packages/core/src/payment/headless-payment/headless-payment-method-config.ts b/packages/core/src/payment/headless-payment/headless-payment-method-config.ts index 7b842a642c..cc501bd24d 100644 --- a/packages/core/src/payment/headless-payment/headless-payment-method-config.ts +++ b/packages/core/src/payment/headless-payment/headless-payment-method-config.ts @@ -3,6 +3,7 @@ import { HeadlessPaymentMethodType } from './headless-payment-method-type'; const HeadlessPaymentMethodConfig: Record = { paypalcommerce: HeadlessPaymentMethodType.PAYPALCOMMERCE, paypalcommercecredit: HeadlessPaymentMethodType.PAYPALCOMMERCECREDIT, + braintree: HeadlessPaymentMethodType.BRAINTREE, }; export default HeadlessPaymentMethodConfig; diff --git a/packages/core/src/payment/headless-payment/headless-payment-method-type.ts b/packages/core/src/payment/headless-payment/headless-payment-method-type.ts index 7d3a68a126..734e4eca81 100644 --- a/packages/core/src/payment/headless-payment/headless-payment-method-type.ts +++ b/packages/core/src/payment/headless-payment/headless-payment-method-type.ts @@ -1,4 +1,5 @@ export enum HeadlessPaymentMethodType { PAYPALCOMMERCE = 'paypalcommerce.paypal', PAYPALCOMMERCECREDIT = 'paypalcommerce.paypalcredit', + BRAINTREE = 'braintree.paypal', } diff --git a/packages/core/src/payment/payment-method-action-creator.spec.ts b/packages/core/src/payment/payment-method-action-creator.spec.ts index cb1742d73f..4eba1c7767 100644 --- a/packages/core/src/payment/payment-method-action-creator.spec.ts +++ b/packages/core/src/payment/payment-method-action-creator.spec.ts @@ -42,7 +42,16 @@ describe('PaymentMethodActionCreator', () => { Promise.resolve(paymentMethodsResponse), ); + jest.spyOn( + paymentMethodRequestSender, + 'loadPaymentWalletWithInitializationData', + ).mockReturnValue(Promise.resolve(paymentMethodResponse)); + jest.spyOn(store.getState().cart, 'getCartOrThrow').mockReturnValue(getCheckout().cart); + + jest.spyOn(store.getState().config, 'getStorefrontJwtToken').mockReturnValue( + 'storefront_jwt_token', + ); }); describe('#loadPaymentMethods()', () => { @@ -195,6 +204,132 @@ 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, { + headers: { + Authorization: `Bearer storefront_jwt_token`, + 'Content-Type': 'application/json', + }, + }); + }); + + 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, { + headers: { + Authorization: `Bearer storefront_jwt_token`, + 'Content-Type': 'application/json', + }, + ...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'; diff --git a/packages/core/src/payment/payment-method-action-creator.ts b/packages/core/src/payment/payment-method-action-creator.ts index 68997b3fa6..4355c73d7e 100644 --- a/packages/core/src/payment/payment-method-action-creator.ts +++ b/packages/core/src/payment/payment-method-action-creator.ts @@ -4,6 +4,7 @@ import { Observable, Observer } from 'rxjs'; import { InternalCheckoutSelectors } from '../checkout'; import { ActionOptions, cachableAction } from '../common/data-store'; +import { MissingDataError, MissingDataErrorType } from '../common/error/errors'; import { RequestOptions } from '../common/http-request'; import { @@ -160,6 +161,56 @@ export default class PaymentMethodActionCreator { }); } + @cachableAction + loadPaymentWalletWithInitializationData( + methodId: string, + options?: RequestOptions & ActionOptions, + ): ThunkAction { + return (store) => + Observable.create((observer: Observer) => { + const state = store.getState(); + const jwtToken = state.config.getStorefrontJwtToken(); + + if (!jwtToken) { + throw new MissingDataError(MissingDataErrorType.MissingPaymentToken); + } + + observer.next( + createAction(PaymentMethodActionType.LoadPaymentMethodRequested, undefined, { + methodId, + }), + ); + + this._requestSender + .loadPaymentWalletWithInitializationData(methodId, { + headers: { + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + }, + ...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)) { diff --git a/packages/payment-integration-api/src/payment-integration-selectors.ts b/packages/payment-integration-api/src/payment-integration-selectors.ts index a75d417cdc..5f6564f08a 100644 --- a/packages/payment-integration-api/src/payment-integration-selectors.ts +++ b/packages/payment-integration-api/src/payment-integration-selectors.ts @@ -85,6 +85,8 @@ export default interface PaymentIntegrationSelectors { getInstrumentsMeta(): InstrumentMeta | undefined; + getStorefrontJwtToken(): string | undefined; + getOrderMeta(): OrderMetaState | undefined; getPaymentMethodsMeta(): PaymentMethodMeta | undefined; diff --git a/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts b/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts index 5bd6cad0fe..c7deac7aed 100644 --- a/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts +++ b/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts @@ -59,6 +59,7 @@ const state = { getPaymentRedirectUrl: jest.fn(), getPaymentRedirectUrlOrThrow: jest.fn(), isPaymentDataRequired: jest.fn(), + getStorefrontJwtToken: jest.fn(), }; const createBuyNowCart = jest.fn(() => Promise.resolve(getCart()));