diff --git a/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.spec.ts b/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.spec.ts index eb53e96d23..eca807311c 100644 --- a/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.spec.ts +++ b/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.spec.ts @@ -64,7 +64,6 @@ describe('BraintreePaypalCreditButtonStrategy', () => { const braintreePaypalCreditOptions: BraintreePaypalCreditButtonInitializeOptions = { shouldProcessPayment: false, style: { height: 45 }, - // shippingAddress: {}, // TODO: <--- onAuthorizeError: jest.fn(), onPaymentError: jest.fn(), onError: jest.fn(), diff --git a/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.ts b/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.ts index fc4c202ae6..4fdc2225cc 100644 --- a/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.ts +++ b/packages/braintree-integration/src/braintree-paypal-credit/braintree-paypal-credit-button-strategy.ts @@ -83,7 +83,6 @@ export default class BraintreePaypalCreditButtonStrategy implements CheckoutButt currencyCode = state.getCartOrThrow().currency.code; } - // Should payment method be loaded here? const paymentMethod = state.getPaymentMethodOrThrow(methodId); const { clientToken, config, initializationData } = paymentMethod; @@ -206,9 +205,6 @@ export default class BraintreePaypalCreditButtonStrategy implements CheckoutButt this.buyNowCartId = buyNowCart?.id; - // Should we load checkout once again? Since it was loaded in initialization method - // await this.paymentIntegrationService.loadDefaultCheckout(); - // needed only for non buy now flow const state = this.paymentIntegrationService.getState(); const customer = state.getCustomer(); const paymentMethod: PaymentMethod = diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-initialize-options.ts similarity index 77% rename from packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts rename to packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-initialize-options.ts index 4b9cc07ab6..812c66cfd4 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-options.ts +++ b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-initialize-options.ts @@ -1,21 +1,34 @@ -import { Address } from '../../../address'; -import { BuyNowCartRequestBody } from '../../../cart'; -import { StandardError } from '../../../common/error/errors'; -import { BraintreeError } from '../../../payment/strategies/braintree'; -import { PaypalStyleOptions } from '../../../payment/strategies/paypal'; +import { BraintreeError, PaypalStyleOptions } from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + Address, + BuyNowCartRequestBody, + StandardError, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; -export interface BraintreePaypalButtonInitializeOptions { +export default interface BraintreePaypalButtonInitializeOptions { /** - * @internal - * This is an internal property and therefore subject to change. DO NOT USE. + * The options that are required to initialize Buy Now functionality. */ - shouldProcessPayment?: boolean; + buyNowInitializeOptions?: { + getBuyNowCartRequestBody?(): BuyNowCartRequestBody | void; + }; + + /** + * The option that used to initialize a PayPal script with provided currency code. + */ + currencyCode?: string; /** * The ID of a container which the messaging should be inserted. */ messagingContainerId?: string; + /** + * @internal + * This is an internal property and therefore subject to change. DO NOT USE. + */ + shouldProcessPayment?: boolean; + /** * A set of styling options for the checkout button. */ @@ -57,16 +70,11 @@ export interface BraintreePaypalButtonInitializeOptions { * */ onEligibilityFailure?(): void; +} +export interface WithBraintreePaypalButtonInitializeOptions { /** - * The option that used to initialize a PayPal script with provided currency code. - */ - currencyCode?: string; - - /** - * The options that are required to initialize Buy Now functionality. + * The options that are required to initialize Braintree PayPal wallet button on Product and Cart page. */ - buyNowInitializeOptions?: { - getBuyNowCartRequestBody?(): BuyNowCartRequestBody | void; - }; + braintreepaypal?: BraintreePaypalButtonInitializeOptions; } diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.spec.ts similarity index 69% rename from packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts rename to packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.spec.ts index 04c9a88076..ca0cf024d2 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.spec.ts +++ b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.spec.ts @@ -1,98 +1,79 @@ import { createFormPoster, FormPoster } from '@bigcommerce/form-poster'; -import { createRequestSender } from '@bigcommerce/request-sender'; import { getScriptLoader } from '@bigcommerce/script-loader'; import { EventEmitter } from 'events'; -import { BraintreeScriptLoader } from '@bigcommerce/checkout-sdk/braintree-utils'; -import { CartSource } from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { CartRequestSender } from '../../../cart'; -import BuyNowCartRequestBody from '../../../cart/buy-now-cart-request-body'; -import { getCart } from '../../../cart/carts.mock'; -import { - CheckoutActionCreator, - CheckoutRequestSender, - CheckoutStore, - createCheckoutStore, -} from '../../../checkout'; -import { getCheckoutStoreState } from '../../../checkout/checkouts.mock'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, -} from '../../../common/error/errors'; -import { ConfigActionCreator, ConfigRequestSender } from '../../../config'; -import { FormFieldsActionCreator, FormFieldsRequestSender } from '../../../form'; -import { PaymentMethod } from '../../../payment'; -import { getBraintree } from '../../../payment/payment-methods.mock'; import { BraintreeDataCollector, + BraintreeError, + BraintreeHostWindow, + BraintreeIntegrationService, BraintreePaypalCheckout, BraintreePaypalCheckoutCreator, - BraintreeSDKCreator, -} from '../../../payment/strategies/braintree'; -import { + BraintreeScriptLoader, + getBraintree, getDataCollectorMock, getPayPalCheckoutCreatorMock, getPaypalCheckoutMock, -} from '../../../payment/strategies/braintree/braintree.mock'; -import { PaypalButtonOptions, - PaypalHostWindow, PaypalSDK, -} from '../../../payment/strategies/paypal'; -import { getPaypalMock } from '../../../payment/strategies/paypal/paypal.mock'; -import { getShippingAddress } from '../../../shipping/shipping-addresses.mock'; -import { CheckoutButtonInitializeOptions } from '../../checkout-button-options'; -import CheckoutButtonMethodType from '../checkout-button-method-type'; - -import { BraintreePaypalButtonInitializeOptions } from './braintree-paypal-button-options'; +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + BuyNowCartCreationError, + BuyNowCartRequestBody, + Cart, + CartSource, + CheckoutButtonInitializeOptions, + InvalidArgumentError, + MissingDataError, + MissingDataErrorType, + PaymentIntegrationService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { + getBuyNowCart, + getCart, + getCustomer, + getShippingAddress, + PaymentIntegrationServiceMock, +} from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; + +import { getPaypalSDKMock } from '../mocks/paypal.mock'; + +import BraintreePaypalButtonInitializeOptions, { + WithBraintreePaypalButtonInitializeOptions, +} from './braintree-paypal-button-initialize-options'; import BraintreePaypalButtonStrategy from './braintree-paypal-button-strategy'; describe('BraintreePaypalButtonStrategy', () => { - let braintreeSDKCreator: BraintreeSDKCreator; - let braintreeScriptLoader: BraintreeScriptLoader; - let braintreePaypalCheckoutCreatorMock: BraintreePaypalCheckoutCreator; - let braintreePaypalCheckoutMock: BraintreePaypalCheckout; - let checkoutActionCreator: CheckoutActionCreator; + let buyNowCart: Cart; + let cart: Cart; let dataCollector: BraintreeDataCollector; let eventEmitter: EventEmitter; + let braintreeIntegrationService: BraintreeIntegrationService; + let braintreePaypalCheckoutMock: BraintreePaypalCheckout; + let braintreePaypalCheckoutCreatorMock: BraintreePaypalCheckoutCreator; + let braintreeScriptLoader: BraintreeScriptLoader; let formPoster: FormPoster; - let paymentMethodMock: PaymentMethod; + let paymentIntegrationService: PaymentIntegrationService; + let paymentMethod: PaymentMethod; let paypalButtonElement: HTMLDivElement; let paypalMessageElement: HTMLDivElement; let paypalSdkMock: PaypalSDK; - let store: CheckoutStore; let strategy: BraintreePaypalButtonStrategy; const defaultButtonContainerId = 'braintree-paypal-button-mock-id'; const defaultMessageContainerId = 'braintree-paypal-message-mock-id'; const braintreePaypalOptions: BraintreePaypalButtonInitializeOptions = { - messagingContainerId: defaultMessageContainerId, - style: { - height: 45, - }, + messagingContainerId: defaultMessageContainerId, // only available on cart page + shouldProcessPayment: false, + style: { height: 45 }, onAuthorizeError: jest.fn(), onPaymentError: jest.fn(), onError: jest.fn(), onEligibilityFailure: jest.fn(), }; - const initializationOptions: CheckoutButtonInitializeOptions = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, - containerId: defaultButtonContainerId, - braintreepaypal: braintreePaypalOptions, - }; - - const cartRequestSender = new CartRequestSender(createRequestSender()); - - const buyNowCartMock = { - ...getCart(), - id: 999, - source: CartSource.BuyNow, - }; - const buyNowCartRequestBody: BuyNowCartRequestBody = { source: CartSource.BuyNow, lineItems: [ @@ -107,8 +88,15 @@ describe('BraintreePaypalButtonStrategy', () => { ], }; + const initializationOptions: CheckoutButtonInitializeOptions & + WithBraintreePaypalButtonInitializeOptions = { + methodId: 'braintreepaypal', + containerId: defaultButtonContainerId, + braintreepaypal: braintreePaypalOptions, + }; + const buyNowInitializationOptions: CheckoutButtonInitializeOptions = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: 'braintreepaypal', containerId: defaultButtonContainerId, braintreepaypal: { ...braintreePaypalOptions, @@ -119,41 +107,51 @@ describe('BraintreePaypalButtonStrategy', () => { }, }; - beforeEach(() => { - braintreePaypalCheckoutMock = getPaypalCheckoutMock(); - braintreePaypalCheckoutCreatorMock = getPayPalCheckoutCreatorMock( - braintreePaypalCheckoutMock, - false, - ); - dataCollector = getDataCollectorMock(); - paypalSdkMock = getPaypalMock(); - eventEmitter = new EventEmitter(); + const getSDKPayPalCheckoutMockWithErrorCallbackCall = () => { + return jest.fn( + ( + _options: unknown, + _successCallback: unknown, + errorCallback: (err: BraintreeError) => void, + ) => { + errorCallback({ type: 'UNKNOWN', code: '234' } as BraintreeError); - store = createCheckoutStore(getCheckoutStoreState()); - checkoutActionCreator = new CheckoutActionCreator( - new CheckoutRequestSender(createRequestSender()), - new ConfigActionCreator(new ConfigRequestSender(createRequestSender())), - new FormFieldsActionCreator(new FormFieldsRequestSender(createRequestSender())), + return Promise.resolve(braintreePaypalCheckoutMock); + }, ); - braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window); - braintreeSDKCreator = new BraintreeSDKCreator(braintreeScriptLoader); - formPoster = createFormPoster(); - - (window as PaypalHostWindow).paypal = paypalSdkMock; + }; - strategy = new BraintreePaypalButtonStrategy( - store, - checkoutActionCreator, - cartRequestSender, - braintreeSDKCreator, - formPoster, - window, + const getSDKPaypalCheckoutMockWithSuccessCallbackCall = ( + braintreePaypalCheckoutPayloadMock: BraintreePaypalCheckout, + ) => { + return jest.fn( + ( + _options: unknown, + successCallback: (braintreePaypalCheckout: BraintreePaypalCheckout) => void, + ) => { + successCallback(braintreePaypalCheckoutPayloadMock); + + return Promise.resolve(braintreePaypalCheckoutMock); + }, ); + }; - paymentMethodMock = { + beforeEach(() => { + buyNowCart = getBuyNowCart(); + cart = getCart(); + dataCollector = getDataCollectorMock(); + eventEmitter = new EventEmitter(); + paymentMethod = { ...getBraintree(), clientToken: 'myToken', }; + paypalSdkMock = getPaypalSDKMock(); + (window as BraintreeHostWindow).paypal = paypalSdkMock; + braintreePaypalCheckoutMock = getPaypalCheckoutMock(); + braintreePaypalCheckoutCreatorMock = getPayPalCheckoutCreatorMock( + braintreePaypalCheckoutMock, + false, + ); paypalButtonElement = document.createElement('div'); paypalButtonElement.id = defaultButtonContainerId; @@ -163,44 +161,63 @@ describe('BraintreePaypalButtonStrategy', () => { paypalMessageElement.id = defaultMessageContainerId; document.body.appendChild(paypalMessageElement); - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve(store.getState())); - jest.spyOn(store.getState().paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( - paymentMethodMock, + formPoster = createFormPoster(); + paymentIntegrationService = new PaymentIntegrationServiceMock(); + braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window); + braintreeIntegrationService = new BraintreeIntegrationService( + braintreeScriptLoader, + window, ); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(braintreeSDKCreator, 'getClient').mockReturnValue(paymentMethodMock.clientToken); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(braintreeSDKCreator, 'getDataCollector').mockReturnValue(dataCollector); - jest.spyOn(braintreeScriptLoader, 'loadPaypalCheckout').mockReturnValue( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreePaypalCheckoutCreatorMock, + + strategy = new BraintreePaypalButtonStrategy( + paymentIntegrationService, + formPoster, + braintreeIntegrationService, + window, ); + jest.spyOn(formPoster, 'postForm').mockImplementation(() => {}); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(checkoutActionCreator, 'loadDefaultCheckout').mockImplementation(() => {}); + jest.spyOn(paymentIntegrationService.getState(), 'getPaymentMethodOrThrow').mockReturnValue( + paymentMethod, + ); + jest.spyOn(paymentIntegrationService.getState(), 'getCartOrThrow').mockReturnValue(cart); + jest.spyOn(paymentIntegrationService.getState(), 'getCustomer').mockReturnValue( + getCustomer(), + ); + jest.spyOn(paymentIntegrationService, 'loadDefaultCheckout').mockImplementation(jest.fn()); + jest.spyOn(braintreeIntegrationService, 'getPaypalCheckout').mockImplementation( + getSDKPaypalCheckoutMockWithSuccessCallbackCall(braintreePaypalCheckoutMock), + ); + + jest.spyOn(braintreeIntegrationService, 'getDataCollector').mockResolvedValue( + dataCollector, + ); + jest.spyOn(braintreeIntegrationService, 'removeElement').mockImplementation(jest.fn()); + jest.spyOn(braintreeScriptLoader, 'loadPaypalCheckout').mockResolvedValue( + braintreePaypalCheckoutCreatorMock, + ); jest.spyOn(paypalSdkMock, 'Buttons').mockImplementation((options: PaypalButtonOptions) => { eventEmitter.on('createOrder', () => { - if (options.createOrder) { + if (typeof options.createOrder === 'function') { options.createOrder().catch(() => {}); } }); eventEmitter.on('approve', () => { - if (options.onApprove) { + if (typeof options.onApprove === 'function') { options.onApprove({ payerId: 'PAYER_ID' }).catch(() => {}); } }); + eventEmitter.on('click', () => { + if (typeof options.onClick === 'function') { + options.onClick(); + } + }); + return { + close: jest.fn(), isEligible: jest.fn(() => true), render: jest.fn(), }; @@ -214,7 +231,7 @@ describe('BraintreePaypalButtonStrategy', () => { afterEach(() => { jest.clearAllMocks(); - delete (window as PaypalHostWindow).paypal; + delete (window as BraintreeHostWindow).paypal; if (document.getElementById(defaultButtonContainerId)) { document.body.removeChild(paypalButtonElement); @@ -225,15 +242,13 @@ describe('BraintreePaypalButtonStrategy', () => { } }); - it('creates an instance of the braintree paypal checkout button strategy', () => { + it('creates an instance of the braintree paypal button button strategy', () => { expect(strategy).toBeInstanceOf(BraintreePaypalButtonStrategy); }); describe('#initialize()', () => { it('throws error if methodId is not provided', async () => { - const options = { - containerId: defaultButtonContainerId, - } as CheckoutButtonInitializeOptions; + const options = {} as CheckoutButtonInitializeOptions; try { await strategy.initialize(options); @@ -242,9 +257,10 @@ describe('BraintreePaypalButtonStrategy', () => { } }); - it('throws an error if containerId is not provided', async () => { + it('throws an error if braintreepaypal is not provided', async () => { const options = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: 'braintreepaypal', + containerId: defaultButtonContainerId, } as CheckoutButtonInitializeOptions; try { @@ -254,10 +270,11 @@ describe('BraintreePaypalButtonStrategy', () => { } }); - it('throws an error if braintreepaypal is not provided', async () => { + it('throws an error if container id is not provided', async () => { const options = { - containerId: defaultButtonContainerId, - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: 'braintreepaypal', + containerId: '', + braintreepaypal: {}, } as CheckoutButtonInitializeOptions; try { @@ -267,8 +284,31 @@ describe('BraintreePaypalButtonStrategy', () => { } }); + it('throws an error if braintreepaypal.currencyCode is not provided (BuyNow flow)', async () => { + try { + await strategy.initialize({ + methodId: 'braintreepaypal', + containerId: defaultButtonContainerId, + braintreepaypal: { + ...braintreePaypalOptions, + buyNowInitializeOptions: { + getBuyNowCartRequestBody: jest.fn(), + }, + }, + }); + } catch (error) { + expect(error).toBeInstanceOf(InvalidArgumentError); + } + }); + + it('does not load default checkout for BuyNowFlow', async () => { + await strategy.initialize(buyNowInitializationOptions); + + expect(paymentIntegrationService.loadDefaultCheckout).not.toHaveBeenCalled(); + }); + it('throws error if client token is missing', async () => { - paymentMethodMock.clientToken = undefined; + paymentMethod.clientToken = undefined; try { await strategy.initialize(initializationOptions); @@ -277,45 +317,46 @@ describe('BraintreePaypalButtonStrategy', () => { } }); - it('initializes braintree sdk creator', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); - - await strategy.initialize(initializationOptions); + it('throws error if initialization data is missing', async () => { + paymentMethod.initializationData = undefined; - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( - paymentMethodMock.clientToken, - ); + try { + await strategy.initialize(initializationOptions); + } catch (error) { + expect(error).toBeInstanceOf(MissingDataError); + } }); - it('initializes braintree paypal checkout', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); + it('initializes braintree integration service', async () => { + braintreeIntegrationService.initialize = jest.fn(); + braintreeIntegrationService.getPaypalCheckout = jest.fn(); await strategy.initialize(initializationOptions); - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( - paymentMethodMock.clientToken, + expect(braintreeIntegrationService.initialize).toHaveBeenCalledWith( + paymentMethod.clientToken, ); - expect(braintreeSDKCreator.getPaypalCheckout).toHaveBeenCalled(); }); - it('calls braintreeSdk with proper options', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); - paymentMethodMock.initializationData = { - ...paymentMethodMock.initializationData, - isCreditEnabled: true, + it('initializes braintree paypal checkout with proper options', async () => { + braintreeIntegrationService.initialize = jest.fn(); + braintreeIntegrationService.getPaypalCheckout = jest.fn(); + paymentMethod.initializationData = { + ...paymentMethod.initializationData, + isCreditEnabled: false, currency: 'USD', intent: undefined, }; await strategy.initialize(initializationOptions); - expect(braintreeSDKCreator.getPaypalCheckout).toHaveBeenCalledWith( + expect(braintreeIntegrationService.initialize).toHaveBeenCalledWith( + paymentMethod.clientToken, + ); + expect(braintreeIntegrationService.getPaypalCheckout).toHaveBeenCalledWith( { currency: 'USD', - isCreditEnabled: true, + isCreditEnabled: false, intent: undefined, }, expect.any(Function), @@ -323,15 +364,18 @@ describe('BraintreePaypalButtonStrategy', () => { ); }); - it('does not load default checkout for BuyNowFlow', async () => { - await strategy.initialize(buyNowInitializationOptions); + it('calls onError callback option on paypal checkout creation failure', async () => { + braintreeIntegrationService.getPaypalCheckout = + getSDKPayPalCheckoutMockWithErrorCallbackCall(); + + await strategy.initialize(initializationOptions); - expect(checkoutActionCreator.loadDefaultCheckout).not.toHaveBeenCalled(); + expect(initializationOptions.braintreepaypal?.onError).toHaveBeenCalled(); }); it('throws an error if buy now cart request body data is not provided', async () => { const buyNowInitializationOptions: CheckoutButtonInitializeOptions = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: 'braintreepaypal', containerId: defaultButtonContainerId, braintreepaypal: { ...braintreePaypalOptions, @@ -353,13 +397,26 @@ describe('BraintreePaypalButtonStrategy', () => { ); }); + it('throws an error if there was an issue with buy now cart creation (Buy Now flow)', async () => { + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.reject(new Error()), + ); + + await strategy.initialize(buyNowInitializationOptions); + + eventEmitter.emit('createOrder'); + + await new Promise((resolve) => process.nextTick(resolve)); + + expect(braintreePaypalOptions.onPaymentError).toHaveBeenCalledWith( + new BuyNowCartCreationError(), + ); + }); + it('creates order with Buy Now cart id (Buy Now flow)', async () => { - jest.spyOn(cartRequestSender, 'createBuyNowCart').mockReturnValue({ - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - body: buyNowCartMock, - }); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCart), + ); await strategy.initialize(buyNowInitializationOptions); @@ -374,46 +431,21 @@ describe('BraintreePaypalButtonStrategy', () => { expect(formPoster.postForm).toHaveBeenCalledWith( '/checkout.php', expect.objectContaining({ - cart_id: buyNowCartMock.id, + cart_id: buyNowCart.id, }), ); }); - it('calls braintree paypal checkout create method', async () => { - await strategy.initialize(initializationOptions); - - expect(braintreePaypalCheckoutCreatorMock.create).toHaveBeenCalled(); - }); - - it('calls onError callback option if the error was caught on paypal checkout creation', async () => { - braintreePaypalCheckoutCreatorMock = getPayPalCheckoutCreatorMock(undefined, true); - - jest.spyOn(braintreeScriptLoader, 'loadPaypalCheckout').mockReturnValue( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreePaypalCheckoutCreatorMock, - ); - - await strategy.initialize(initializationOptions); - - expect(initializationOptions.braintreepaypal?.onError).toHaveBeenCalled(); - }); - - it('renders PayPal checkout message', async () => { - const cartMock = getCart(); - - jest.spyOn(store.getState().cart, 'getCartOrThrow').mockReturnValue(cartMock); - + it('renders Braintree PayPal message', async () => { await strategy.initialize(initializationOptions); expect(paypalSdkMock.Messages).toHaveBeenCalledWith({ - amount: 190, + amount: cart.cartAmount, placement: 'cart', }); }); - it('renders PayPal checkout button', async () => { + it('renders Braintree PayPal button', async () => { await strategy.initialize(initializationOptions); expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ @@ -428,13 +460,27 @@ describe('BraintreePaypalButtonStrategy', () => { }); }); - it('does not render PayPal checkout button and calls onEligibilityFailure callback', async () => { + it('removes Braintree PayPal button and message containers when paypal is not available in window', async () => { + delete (window as BraintreeHostWindow).paypal; + + await strategy.initialize(initializationOptions); + + expect(braintreeIntegrationService.removeElement).toHaveBeenCalledWith( + defaultMessageContainerId, + ); + expect(braintreeIntegrationService.removeElement).toHaveBeenCalledWith( + defaultButtonContainerId, + ); + }); + + it('does not render Braintree PayPal button and calls onEligibilityFailure callback', async () => { const renderMock = jest.fn(); - jest.spyOn(paypalSdkMock, 'Buttons').mockImplementationOnce(() => { + jest.spyOn(paypalSdkMock, 'Buttons').mockImplementation(() => { return { isEligible: jest.fn(() => false), render: renderMock, + close: jest.fn(), }; }); @@ -455,8 +501,8 @@ describe('BraintreePaypalButtonStrategy', () => { expect(renderMock).not.toHaveBeenCalled(); }); - it('renders PayPal checkout button in production environment if payment method is in test mode', async () => { - paymentMethodMock.config.testMode = false; + it('renders Braintree PayPal button in production environment if payment method is in test mode', async () => { + paymentMethod.config.testMode = false; await strategy.initialize(initializationOptions); @@ -465,24 +511,7 @@ describe('BraintreePaypalButtonStrategy', () => { ); }); - it('loads checkout details when customer is ready to pay', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('createOrder'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(checkoutActionCreator.loadDefaultCheckout).toHaveBeenCalledTimes(2); - }); - it('sets up PayPal payment flow with provided address', async () => { - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(store.getState().checkout, 'getCheckoutOrThrow').mockReturnValue({ - outstandingBalance: 22, - }); - await strategy.initialize({ ...initializationOptions, braintreepaypal: { @@ -523,7 +552,9 @@ describe('BraintreePaypalButtonStrategy', () => { }); it('sets up PayPal payment flow with no address when null is passed', async () => { - jest.spyOn(store.getState().customer, 'getCustomer').mockReturnValue(undefined); + jest.spyOn(paymentIntegrationService.getState(), 'getCustomer').mockReturnValue( + undefined, + ); await strategy.initialize({ ...initializationOptions, @@ -712,12 +743,12 @@ describe('BraintreePaypalButtonStrategy', () => { describe('#deinitialize()', () => { it('teardowns braintree sdk creator on strategy deinitialize', async () => { - braintreeSDKCreator.teardown = jest.fn(); + braintreeIntegrationService.teardown = jest.fn(); await strategy.initialize(initializationOptions); await strategy.deinitialize(); - expect(braintreeSDKCreator.teardown).toHaveBeenCalled(); + expect(braintreeIntegrationService.teardown).toHaveBeenCalled(); }); }); }); diff --git a/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.ts b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.ts new file mode 100644 index 0000000000..cf38812131 --- /dev/null +++ b/packages/braintree-integration/src/braintree-paypal/braintree-paypal-button-strategy.ts @@ -0,0 +1,305 @@ +import { FormPoster } from '@bigcommerce/form-poster'; + +import { + BraintreeError, + BraintreeHostWindow, + BraintreeInitializationData, + BraintreeIntegrationService, + BraintreePaypalCheckout, + BraintreePaypalSdkCreatorConfig, + BraintreeTokenizePayload, + isBraintreeError, + PaypalAuthorizeData, +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + BuyNowCartCreationError, + BuyNowCartRequestBody, + CheckoutButtonInitializeOptions, + CheckoutButtonStrategy, + InvalidArgumentError, + MissingDataError, + MissingDataErrorType, + PaymentIntegrationService, + PaymentMethod, + StandardError, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import getValidButtonStyle from '../get-valid-button-style'; +import mapToBraintreeShippingAddressOverride from '../map-to-braintree-shipping-address-override'; + +import BraintreePaypalButtonInitializeOptions, { + WithBraintreePaypalButtonInitializeOptions, +} from './braintree-paypal-button-initialize-options'; + +export default class BraintreePaypalButtonStrategy implements CheckoutButtonStrategy { + private buyNowCartId: string | undefined; + + constructor( + private paymentIntegrationService: PaymentIntegrationService, + private formPoster: FormPoster, + private braintreeIntegrationService: BraintreeIntegrationService, + private braintreeHostWindow: BraintreeHostWindow, + ) {} + + async initialize( + options: CheckoutButtonInitializeOptions & WithBraintreePaypalButtonInitializeOptions, + ): Promise { + const { braintreepaypal, containerId, methodId } = options; + + if (!methodId) { + throw new InvalidArgumentError( + 'Unable to initialize payment because "options.methodId" argument is not provided.', + ); + } + + if (!containerId) { + throw new InvalidArgumentError( + `Unable to initialize payment because "options.containerId" argument is not provided.`, + ); + } + + if (!braintreepaypal) { + throw new InvalidArgumentError( + `Unable to initialize payment because "options.braintreepaypal" argument is not provided.`, + ); + } + + let state = this.paymentIntegrationService.getState(); + let currencyCode: string; + + if (braintreepaypal.buyNowInitializeOptions) { + if (!braintreepaypal.currencyCode) { + throw new InvalidArgumentError( + `Unable to initialize payment because "options.braintreepaypalcredit.currencyCode" argument is not provided.`, + ); + } + + currencyCode = braintreepaypal.currencyCode; + } else { + await this.paymentIntegrationService.loadDefaultCheckout(); + + state = this.paymentIntegrationService.getState(); + currencyCode = state.getCartOrThrow().currency.code; + } + + const paymentMethod = state.getPaymentMethodOrThrow(methodId); + const { clientToken, config, initializationData } = paymentMethod; + + if (!clientToken || !initializationData) { + throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); + } + + const paypalCheckoutOptions: Partial = { + currency: currencyCode, + intent: initializationData.intent, + isCreditEnabled: initializationData.isCreditEnabled, + }; + + const paypalCheckoutSuccessCallback = ( + braintreePaypalCheckout: BraintreePaypalCheckout, + ) => { + this.renderPayPalMessages(braintreepaypal.messagingContainerId); + this.renderPayPalButton( + braintreePaypalCheckout, + braintreepaypal, + containerId, + methodId, + !!config.testMode, + ); + }; + const paypalCheckoutErrorCallback = (error: BraintreeError) => + this.handleError(error, containerId, braintreepaypal.onError); + + this.braintreeIntegrationService.initialize(clientToken); + await this.braintreeIntegrationService.getPaypalCheckout( + paypalCheckoutOptions, + paypalCheckoutSuccessCallback, + paypalCheckoutErrorCallback, + ); + } + + async deinitialize(): Promise { + await this.braintreeIntegrationService.teardown(); + } + + private renderPayPalMessages(messagingContainerId?: string): void { + const isMessageContainerAvailable = + messagingContainerId && Boolean(document.getElementById(messagingContainerId)); + const { paypal } = this.braintreeHostWindow; + + if (isMessageContainerAvailable && paypal) { + const state = this.paymentIntegrationService.getState(); + const amount = state.getCartOrThrow().cartAmount; + + const paypalMessagesRender = paypal.Messages({ + amount, + placement: 'cart', + }); + + paypalMessagesRender.render(`#${messagingContainerId}`); + } else { + this.braintreeIntegrationService.removeElement(messagingContainerId); + } + } + + private renderPayPalButton( + braintreePaypalCheckout: BraintreePaypalCheckout, + braintreepaypal: BraintreePaypalButtonInitializeOptions, + containerId: string, + methodId: string, + testMode: boolean, + ): void { + const { style, shouldProcessPayment, onAuthorizeError, onEligibilityFailure } = + braintreepaypal; + const { paypal } = this.braintreeHostWindow; + + if (paypal) { + const buttonStyle = style ? getValidButtonStyle(style) : {}; + + const paypalButtonRender = paypal.Buttons({ + env: testMode ? 'sandbox' : 'production', + fundingSource: paypal.FUNDING.PAYPAL, + style: buttonStyle, + createOrder: () => + this.setupPayment(braintreePaypalCheckout, braintreepaypal, methodId), + onApprove: (authorizeData: PaypalAuthorizeData) => + this.tokenizePayment( + authorizeData, + braintreePaypalCheckout, + methodId, + shouldProcessPayment, + onAuthorizeError, + ), + }); + + if (paypalButtonRender.isEligible()) { + paypalButtonRender.render(`#${containerId}`); + } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { + onEligibilityFailure(); + } + } else { + this.braintreeIntegrationService.removeElement(containerId); + } + } + + private async setupPayment( + braintreePaypalCheckout: BraintreePaypalCheckout, + braintreepaypal: BraintreePaypalButtonInitializeOptions, + methodId: string, + ): Promise { + const { onPaymentError, shippingAddress, buyNowInitializeOptions } = braintreepaypal; + + try { + const buyNowCart = + typeof buyNowInitializeOptions?.getBuyNowCartRequestBody === 'function' + ? await this.createBuyNowCart( + buyNowInitializeOptions.getBuyNowCartRequestBody(), + ) + : undefined; + + this.buyNowCartId = buyNowCart?.id; + + const state = this.paymentIntegrationService.getState(); + const customer = state.getCustomer(); + const paymentMethod: PaymentMethod = + state.getPaymentMethodOrThrow(methodId); + + const amount = buyNowCart ? buyNowCart.cartAmount : state.getCartOrThrow().cartAmount; + const currencyCode = buyNowCart + ? braintreepaypal.currencyCode + : state.getCartOrThrow().currency.code; + + const address = shippingAddress || customer?.addresses[0]; + + const shippingAddressOverride = address + ? mapToBraintreeShippingAddressOverride(address) + : undefined; + + return await braintreePaypalCheckout.createPayment({ + flow: 'checkout', + enableShippingAddress: true, + shippingAddressEditable: false, + shippingAddressOverride, + amount, + currency: currencyCode, + offerCredit: false, + intent: paymentMethod.initializationData?.intent, + }); + } catch (error: unknown) { + if (onPaymentError) { + if (isBraintreeError(error) || error instanceof StandardError) { + onPaymentError(error); + } + } + + throw error; + } + } + + private async tokenizePayment( + authorizeData: PaypalAuthorizeData, + braintreePaypalCheckout: BraintreePaypalCheckout, + methodId: string, + shouldProcessPayment?: boolean, + onError?: (error: BraintreeError | StandardError) => void, + ): Promise { + try { + const { deviceData } = await this.braintreeIntegrationService.getDataCollector({ + paypal: true, + }); + const tokenizePayload = await braintreePaypalCheckout.tokenizePayment(authorizeData); + const { details, nonce } = tokenizePayload; + const billingAddress = + this.braintreeIntegrationService.mapToLegacyBillingAddress(details); + const shippingAddress = + this.braintreeIntegrationService.mapToLegacyShippingAddress(details); + + this.formPoster.postForm('/checkout.php', { + payment_type: 'paypal', + provider: methodId, + action: shouldProcessPayment ? 'process_payment' : 'set_external_checkout', + nonce, + device_data: deviceData, + billing_address: JSON.stringify(billingAddress), + shipping_address: JSON.stringify(shippingAddress), + ...(this.buyNowCartId && { cart_id: this.buyNowCartId }), + }); + + return tokenizePayload; + } catch (error) { + if (onError) { + if (isBraintreeError(error) || error instanceof StandardError) { + onError(error); + } + } + + throw error; + } + } + + private async createBuyNowCart(buyNowCardRequestBody?: BuyNowCartRequestBody | void) { + if (!buyNowCardRequestBody) { + throw new MissingDataError(MissingDataErrorType.MissingCart); + } + + try { + return await this.paymentIntegrationService.createBuyNowCart(buyNowCardRequestBody); + } catch (error) { + throw new BuyNowCartCreationError(); + } + } + + private handleError( + error: unknown, + buttonContainerId: string, + onErrorCallback?: (error: BraintreeError | StandardError) => void, + ): void { + this.braintreeIntegrationService.removeElement(buttonContainerId); + + if (onErrorCallback && isBraintreeError(error)) { + onErrorCallback(error); + } else { + throw error; + } + } +} diff --git a/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.spec.ts b/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.spec.ts new file mode 100644 index 0000000000..d8ff95e0e7 --- /dev/null +++ b/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.spec.ts @@ -0,0 +1,19 @@ +import { PaymentIntegrationService } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { PaymentIntegrationServiceMock } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; + +import BraintreePaypalCreditButtonStrategy from './braintree-paypal-button-strategy'; +import createBraintreePaypalCreditButtonStrategy from './create-braintree-paypal-button-strategy'; + +describe('createBraintreePaypalCreditButtonStrategy', () => { + let paymentIntegrationService: PaymentIntegrationService; + + beforeEach(() => { + paymentIntegrationService = new PaymentIntegrationServiceMock(); + }); + + it('instantiates braintree paypal credit button strategy', () => { + const strategy = createBraintreePaypalCreditButtonStrategy(paymentIntegrationService); + + expect(strategy).toBeInstanceOf(BraintreePaypalCreditButtonStrategy); + }); +}); diff --git a/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.ts b/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.ts new file mode 100644 index 0000000000..6578dcf7e7 --- /dev/null +++ b/packages/braintree-integration/src/braintree-paypal/create-braintree-paypal-button-strategy.ts @@ -0,0 +1,33 @@ +import { createFormPoster } from '@bigcommerce/form-poster'; +import { getScriptLoader } from '@bigcommerce/script-loader'; + +import { + BraintreeHostWindow, + BraintreeIntegrationService, + BraintreeScriptLoader, +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + CheckoutButtonStrategyFactory, + toResolvableModule, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import BraintreePaypalButtonStrategy from './braintree-paypal-button-strategy'; + +const createBraintreePaypalButtonStrategy: CheckoutButtonStrategyFactory< + BraintreePaypalButtonStrategy +> = (paymentIntegrationService) => { + const braintreeHostWindow: BraintreeHostWindow = window; + const braintreeIntegrationService = new BraintreeIntegrationService( + new BraintreeScriptLoader(getScriptLoader(), braintreeHostWindow), + braintreeHostWindow, + ); + + return new BraintreePaypalButtonStrategy( + paymentIntegrationService, + createFormPoster(), + braintreeIntegrationService, + braintreeHostWindow, + ); +}; + +export default toResolvableModule(createBraintreePaypalButtonStrategy, [{ id: 'braintreepaypal' }]); diff --git a/packages/braintree-integration/src/index.ts b/packages/braintree-integration/src/index.ts index 5da31b9f9e..0c24f1dc20 100644 --- a/packages/braintree-integration/src/index.ts +++ b/packages/braintree-integration/src/index.ts @@ -7,9 +7,11 @@ export { WithBraintreeAchPaymentInitializeOptions } from './braintree-ach/braint /** * Braintree PayPal strategies */ +export { default as createBraintreePaypalButtonStrategy } from './braintree-paypal/create-braintree-paypal-button-strategy'; export { default as createBraintreePaypalCustomerStrategy } from './braintree-paypal/create-braintree-paypal-customer-strategy'; -export { WithBraintreePaypalCustomerInitializeOptions } from './braintree-paypal/braintree-paypal-customer-initialize-options'; export { default as createBraintreePaypalPaymentStrategy } from './braintree-paypal/create-braintree-paypal-payment-strategy'; +export { WithBraintreePaypalCustomerInitializeOptions } from './braintree-paypal/braintree-paypal-customer-initialize-options'; +export { WithBraintreePaypalButtonInitializeOptions } from './braintree-paypal/braintree-paypal-button-initialize-options'; /** * Braintree PayPal Credit strategies diff --git a/packages/core/src/checkout-buttons/checkout-button-options.ts b/packages/core/src/checkout-buttons/checkout-button-options.ts index 9de22e8716..a29276f3c0 100644 --- a/packages/core/src/checkout-buttons/checkout-button-options.ts +++ b/packages/core/src/checkout-buttons/checkout-button-options.ts @@ -1,7 +1,6 @@ import { RequestOptions } from '../common/http-request'; import { CheckoutButtonMethodType } from './strategies'; -import { BraintreePaypalButtonInitializeOptions } from './strategies/braintree'; import { PaypalButtonInitializeOptions } from './strategies/paypal'; export { CheckoutButtonInitializeOptions } from '../generated/checkout-button-initialize-options'; @@ -19,12 +18,6 @@ export interface CheckoutButtonOptions extends RequestOptions { export interface BaseCheckoutButtonInitializeOptions extends CheckoutButtonOptions { [key: string]: unknown; - /** - * The options that are required to facilitate Braintree PayPal. They can be - * omitted unless you need to support Braintree PayPal. - */ - braintreepaypal?: BraintreePaypalButtonInitializeOptions; - /** * The ID of a container which the checkout button should be inserted. */ diff --git a/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts b/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts index e16b927a33..b29bd22110 100644 --- a/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts +++ b/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts @@ -48,8 +48,8 @@ describe('CheckoutButtonStrategyActionCreator', () => { ); strategy = new MockButtonStrategy(); store = createCheckoutStore(); - (registryV2 = createCheckoutButtonRegistryV2(createPaymentIntegrationService(store))), - registry.register(CheckoutButtonMethodType.BRAINTREE_PAYPAL, () => strategy); + registryV2 = createCheckoutButtonRegistryV2(createPaymentIntegrationService(store)); + registry.register(CheckoutButtonMethodType.MASTERPASS, () => strategy); jest.spyOn(paymentMethodActionCreator, 'loadPaymentMethod').mockReturnValue(() => // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) @@ -64,7 +64,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { ); options = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: CheckoutButtonMethodType.MASTERPASS, containerId: 'checkout-button', }; @@ -79,14 +79,14 @@ describe('CheckoutButtonStrategyActionCreator', () => { await from(strategyActionCreator.initialize(options)(store)).pipe(toArray()).toPromise(); expect(paymentMethodActionCreator.loadPaymentMethod).toHaveBeenCalledWith( - CheckoutButtonMethodType.BRAINTREE_PAYPAL, + CheckoutButtonMethodType.MASTERPASS, { useCache: true }, ); }); it('loads required payment method for provided currency', async () => { const optionsMock = { - methodId: CheckoutButtonMethodType.BRAINTREE_PAYPAL, + methodId: CheckoutButtonMethodType.MASTERPASS, containerId: 'checkout-button', currencyCode: 'USD', }; @@ -103,7 +103,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { }; expect(paymentMethodActionCreator.loadPaymentMethod).toHaveBeenCalledWith( - CheckoutButtonMethodType.BRAINTREE_PAYPAL, + CheckoutButtonMethodType.MASTERPASS, expectedPaymentMethodOptions, ); }); @@ -114,7 +114,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { await from(strategyActionCreator.initialize(options)(store)).pipe(toArray()).toPromise(); - expect(registry.get).toHaveBeenCalledWith(CheckoutButtonMethodType.BRAINTREE_PAYPAL); + expect(registry.get).toHaveBeenCalledWith(CheckoutButtonMethodType.MASTERPASS); expect(strategy.initialize).toHaveBeenCalledWith(options); }); @@ -138,12 +138,12 @@ describe('CheckoutButtonStrategyActionCreator', () => { await from(strategyActionCreator.initialize(options)(store)).pipe(toArray()).toPromise(); - expect(registry.get).not.toHaveBeenCalledWith(CheckoutButtonMethodType.BRAINTREE_PAYPAL); + expect(registry.get).not.toHaveBeenCalledWith(CheckoutButtonMethodType.MASTERPASS); expect(strategy.initialize).not.toHaveBeenCalledWith(options); }); it('emits actions indicating initialization progress', async () => { - const methodId = CheckoutButtonMethodType.BRAINTREE_PAYPAL; + const methodId = CheckoutButtonMethodType.MASTERPASS; const containerId = 'checkout-button'; const actions = await from( strategyActionCreator.initialize({ methodId, containerId })(store), @@ -169,7 +169,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { }); it('throws error if unable to load required payment method', async () => { - const methodId = CheckoutButtonMethodType.BRAINTREE_PAYPAL; + const methodId = CheckoutButtonMethodType.MASTERPASS; const containerId = 'checkout-button'; const expectedError = new Error('Unable to load payment method'); @@ -207,7 +207,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { }); it('throws error if unable to initialize strategy', async () => { - const methodId = CheckoutButtonMethodType.BRAINTREE_PAYPAL; + const methodId = CheckoutButtonMethodType.MASTERPASS; const containerId = 'checkout-button'; const expectedError = new Error('Unable to initialize strategy'); @@ -260,7 +260,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { await from(strategyActionCreator.deinitialize(options)(store)).pipe(toArray()).toPromise(); - expect(registry.get).toHaveBeenCalledWith(CheckoutButtonMethodType.BRAINTREE_PAYPAL); + expect(registry.get).toHaveBeenCalledWith(CheckoutButtonMethodType.MASTERPASS); expect(strategy.deinitialize).toHaveBeenCalled(); }); diff --git a/packages/core/src/checkout-buttons/create-checkout-button-registry.spec.ts b/packages/core/src/checkout-buttons/create-checkout-button-registry.spec.ts index 12aea42642..66a5447b01 100644 --- a/packages/core/src/checkout-buttons/create-checkout-button-registry.spec.ts +++ b/packages/core/src/checkout-buttons/create-checkout-button-registry.spec.ts @@ -6,7 +6,8 @@ import { Registry } from '../common/registry'; import createCheckoutButtonRegistry from './create-checkout-button-registry'; import { CheckoutButtonStrategy } from './strategies'; -import { BraintreePaypalButtonStrategy } from './strategies/braintree'; +import { MasterpassButtonStrategy } from './strategies/masterpass'; +import { PaypalButtonStrategy } from './strategies/paypal'; describe('createCheckoutButtonRegistry', () => { let registry: Registry; @@ -22,7 +23,11 @@ describe('createCheckoutButtonRegistry', () => { ); }); - it('returns registry with Braintree PayPal registered', () => { - expect(registry.get('braintreepaypal')).toEqual(expect.any(BraintreePaypalButtonStrategy)); + it('returns registry with PayPal Express registered', () => { + expect(registry.get('paypalexpress')).toEqual(expect.any(PaypalButtonStrategy)); + }); + + it('returns registry with Masterpass registered', () => { + expect(registry.get('masterpass')).toEqual(expect.any(MasterpassButtonStrategy)); }); }); diff --git a/packages/core/src/checkout-buttons/create-checkout-button-registry.ts b/packages/core/src/checkout-buttons/create-checkout-button-registry.ts index 738f41bf01..a333b1c695 100644 --- a/packages/core/src/checkout-buttons/create-checkout-button-registry.ts +++ b/packages/core/src/checkout-buttons/create-checkout-button-registry.ts @@ -2,19 +2,14 @@ import { FormPoster } from '@bigcommerce/form-poster'; import { RequestSender } from '@bigcommerce/request-sender'; import { getScriptLoader } from '@bigcommerce/script-loader'; -import { BraintreeScriptLoader } from '@bigcommerce/checkout-sdk/braintree-utils'; - -import { CartRequestSender } from '../cart'; import { CheckoutActionCreator, CheckoutRequestSender, CheckoutStore } from '../checkout'; import { Registry } from '../common/registry'; import { ConfigActionCreator, ConfigRequestSender } from '../config'; import { FormFieldsActionCreator, FormFieldsRequestSender } from '../form'; -import { BraintreeSDKCreator } from '../payment/strategies/braintree'; import { MasterpassScriptLoader } from '../payment/strategies/masterpass'; import { PaypalScriptLoader } from '../payment/strategies/paypal'; import { CheckoutButtonMethodType, CheckoutButtonStrategy } from './strategies'; -import { BraintreePaypalButtonStrategy } from './strategies/braintree'; import { MasterpassButtonStrategy } from './strategies/masterpass'; import { PaypalButtonStrategy } from './strategies/paypal'; @@ -34,24 +29,6 @@ export default function createCheckoutButtonRegistry( new FormFieldsActionCreator(new FormFieldsRequestSender(requestSender)), ); - const braintreeSdkCreator = new BraintreeSDKCreator( - new BraintreeScriptLoader(scriptLoader, window), - ); - const cartRequestSender = new CartRequestSender(requestSender); - - registry.register( - CheckoutButtonMethodType.BRAINTREE_PAYPAL, - () => - new BraintreePaypalButtonStrategy( - store, - checkoutActionCreator, - cartRequestSender, - braintreeSdkCreator, - formPoster, - window, - ), - ); - registry.register( CheckoutButtonMethodType.MASTERPASS, () => diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts b/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts deleted file mode 100644 index 1ac33fdf5b..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-paypal-button-strategy.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { FormPoster } from '@bigcommerce/form-poster'; - -import { Cart, CartRequestSender } from '../../../cart'; -import { BuyNowCartCreationError } from '../../../cart/errors'; -import { CheckoutActionCreator, CheckoutStore, InternalCheckoutSelectors } from '../../../checkout'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, - StandardError, -} from '../../../common/error/errors'; -import PaymentMethod from '../../../payment/payment-method'; -import { - BraintreeError, - BraintreePaypalCheckout, - BraintreePaypalSdkCreatorConfig, - BraintreeSDKCreator, - BraintreeTokenizePayload, - mapToBraintreeShippingAddressOverride, -} from '../../../payment/strategies/braintree'; -import isBraintreeError from '../../../payment/strategies/braintree/is-braintree-error'; -import { PaypalAuthorizeData, PaypalHostWindow } from '../../../payment/strategies/paypal'; -import { CheckoutButtonInitializeOptions } from '../../checkout-button-options'; -import CheckoutButtonStrategy from '../checkout-button-strategy'; - -import { BraintreePaypalButtonInitializeOptions } from './braintree-paypal-button-options'; -import getValidButtonStyle from './get-valid-button-style'; -import mapToLegacyBillingAddress from './map-to-legacy-billing-address'; -import mapToLegacyShippingAddress from './map-to-legacy-shipping-address'; - -type BuyNowInitializeOptions = Pick< - BraintreePaypalButtonInitializeOptions, - 'buyNowInitializeOptions' ->; - -export default class BraintreePaypalButtonStrategy implements CheckoutButtonStrategy { - private _buyNowCart?: Cart; - - constructor( - private _store: CheckoutStore, - private _checkoutActionCreator: CheckoutActionCreator, - private _cartRequestSender: CartRequestSender, - private _braintreeSDKCreator: BraintreeSDKCreator, - private _formPoster: FormPoster, - private _window: PaypalHostWindow, - ) {} - - async initialize(options: CheckoutButtonInitializeOptions): Promise { - const { braintreepaypal, containerId, methodId } = options; - const { messagingContainerId, onError } = braintreepaypal || {}; - - if (!methodId) { - throw new InvalidArgumentError( - 'Unable to initialize payment because "options.methodId" argument is not provided.', - ); - } - - if (!containerId) { - throw new InvalidArgumentError( - `Unable to initialize payment because "options.containerId" argument is not provided.`, - ); - } - - if (!braintreepaypal) { - throw new InvalidArgumentError( - `Unable to initialize payment because "options.braintreepaypal" argument is not provided.`, - ); - } - - let state: InternalCheckoutSelectors; - let paymentMethod: PaymentMethod; - let currencyCode: string; - - if (braintreepaypal.buyNowInitializeOptions) { - state = this._store.getState(); - paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - - if (!braintreepaypal.currencyCode) { - throw new InvalidArgumentError( - `Unable to initialize payment because "options.braintreepaypal.currencyCode" argument is not provided.`, - ); - } - - currencyCode = braintreepaypal.currencyCode; - } else { - state = await this._store.dispatch(this._checkoutActionCreator.loadDefaultCheckout()); - paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - currencyCode = state.cart.getCartOrThrow().currency.code; - } - - const { clientToken, initializationData } = paymentMethod; - - if (!clientToken || !initializationData) { - throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); - } - - const paypalCheckoutOptions: Partial = { - currency: currencyCode, - intent: initializationData.intent, - isCreditEnabled: initializationData.isCreditEnabled, - }; - - const paypalCheckoutSuccessCallback = ( - braintreePaypalCheckout: BraintreePaypalCheckout, - ) => { - this._renderPayPalComponents( - braintreePaypalCheckout, - braintreepaypal, - containerId, - methodId, - Boolean(paymentMethod.config.testMode), - ); - }; - const paypalCheckoutErrorCallback = (error: BraintreeError) => - this._handleError(error, containerId, messagingContainerId, onError); - - this._braintreeSDKCreator.initialize(clientToken); - await this._braintreeSDKCreator.getPaypalCheckout( - paypalCheckoutOptions, - paypalCheckoutSuccessCallback, - paypalCheckoutErrorCallback, - ); - } - - deinitialize(): Promise { - this._braintreeSDKCreator.teardown(); - - return Promise.resolve(); - } - - private _renderPayPalComponents( - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypal: BraintreePaypalButtonInitializeOptions, - containerId: string, - methodId: string, - testMode: boolean, - ): void { - const { messagingContainerId } = braintreepaypal; - - this._renderPayPalMessages(messagingContainerId); - this._renderPayPalButton( - braintreePaypalCheckout, - braintreepaypal, - containerId, - methodId, - testMode, - ); - } - - private _renderPayPalButton( - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypal: BraintreePaypalButtonInitializeOptions, - containerId: string, - methodId: string, - testMode: boolean, - ): void { - const { style, shouldProcessPayment, onAuthorizeError, onEligibilityFailure } = - braintreepaypal; - const { paypal } = this._window; - const fundingSource = paypal?.FUNDING.PAYPAL; - - if (paypal && fundingSource) { - const validButtonStyle = style ? getValidButtonStyle(style) : {}; - - const paypalButtonRender = paypal.Buttons({ - env: testMode ? 'sandbox' : 'production', - fundingSource, - style: validButtonStyle, - createOrder: () => - this._setupPayment(braintreePaypalCheckout, braintreepaypal, methodId), - onApprove: (authorizeData: PaypalAuthorizeData) => - this._tokenizePayment( - authorizeData, - braintreePaypalCheckout, - methodId, - shouldProcessPayment, - onAuthorizeError, - ), - }); - - if (paypalButtonRender.isEligible()) { - paypalButtonRender.render(`#${containerId}`); - } else if (onEligibilityFailure && typeof onEligibilityFailure === 'function') { - onEligibilityFailure(); - } - } else { - this._removeElement(containerId); - } - } - - private _renderPayPalMessages(messagingContainerId?: string): void { - const isMessageContainerAvailable = - messagingContainerId && Boolean(document.getElementById(messagingContainerId)); - const { paypal } = this._window; - - if (paypal && isMessageContainerAvailable) { - const { checkout } = this._store.getState(); - const grandTotal = checkout.getCheckoutOrThrow().outstandingBalance; - - const paypalMessagesRender = paypal.Messages({ - amount: grandTotal, - placement: 'cart', - }); - - paypalMessagesRender.render(`#${messagingContainerId}`); - } else { - this._removeElement(messagingContainerId); - } - } - - private async _setupPayment( - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypal: BraintreePaypalButtonInitializeOptions, - methodId: string, - ): Promise { - const { buyNowInitializeOptions, shippingAddress, onPaymentError } = braintreepaypal; - let state: InternalCheckoutSelectors; - - try { - this._buyNowCart = await this._createBuyNowCart({ buyNowInitializeOptions }); - - if (this._buyNowCart) { - state = this._store.getState(); - } else { - state = await this._store.dispatch( - this._checkoutActionCreator.loadDefaultCheckout(), - ); - } - - const customer = state.customer.getCustomer(); - const paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - - const amount = this._buyNowCart - ? this._buyNowCart.cartAmount - : state.checkout.getCheckoutOrThrow().outstandingBalance; - const currencyCode = - braintreepaypal.currencyCode ?? state.config.getStoreConfigOrThrow().currency.code; - - const address = shippingAddress || customer?.addresses[0]; - const shippingAddressOverride = address - ? mapToBraintreeShippingAddressOverride(address) - : undefined; - - return await braintreePaypalCheckout.createPayment({ - flow: 'checkout', - enableShippingAddress: true, - shippingAddressEditable: false, - shippingAddressOverride, - amount, - currency: currencyCode, - offerCredit: false, - intent: paymentMethod.initializationData?.intent, - }); - } catch (error) { - if (onPaymentError) { - if (isBraintreeError(error) || error instanceof StandardError) { - onPaymentError(error); - } - } - - throw error; - } - } - - private async _createBuyNowCart({ buyNowInitializeOptions }: BuyNowInitializeOptions) { - if (typeof buyNowInitializeOptions?.getBuyNowCartRequestBody === 'function') { - const cartRequestBody = buyNowInitializeOptions.getBuyNowCartRequestBody(); - - if (!cartRequestBody) { - throw new MissingDataError(MissingDataErrorType.MissingCart); - } - - try { - const { body: cart } = await this._cartRequestSender.createBuyNowCart( - cartRequestBody, - ); - - return cart; - } catch (error) { - throw new BuyNowCartCreationError(); - } - } - } - - private async _tokenizePayment( - authorizeData: PaypalAuthorizeData, - braintreePaypalCheckout: BraintreePaypalCheckout, - methodId: string, - shouldProcessPayment?: boolean, - onError?: (error: BraintreeError | StandardError) => void, - ): Promise { - try { - const { deviceData } = await this._braintreeSDKCreator.getDataCollector({ - paypal: true, - }); - const tokenizePayload = await braintreePaypalCheckout.tokenizePayment(authorizeData); - const { details, nonce } = tokenizePayload; - const buyNowCartId = this._buyNowCart?.id; - - this._formPoster.postForm('/checkout.php', { - payment_type: 'paypal', - provider: methodId, - action: shouldProcessPayment ? 'process_payment' : 'set_external_checkout', - nonce, - device_data: deviceData, - billing_address: JSON.stringify(mapToLegacyBillingAddress(details)), - shipping_address: JSON.stringify(mapToLegacyShippingAddress(details)), - ...(buyNowCartId && { cart_id: buyNowCartId }), - }); - - return tokenizePayload; - } catch (error) { - if (onError) { - if (isBraintreeError(error) || error instanceof StandardError) { - onError(error); - } - } - - throw error; - } - } - - private _handleError( - error: BraintreeError, - buttonContainerId: string, - messagingContainerId?: string, - onErrorCallback?: (error: BraintreeError) => void, - ): void { - this._removeElement(buttonContainerId); - this._removeElement(messagingContainerId); - - if (onErrorCallback) { - onErrorCallback(error); - } - } - - private _removeElement(elementId?: string): void { - const element = elementId && document.getElementById(elementId); - - if (element) { - element.remove(); - } - } -} diff --git a/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.spec.ts b/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.spec.ts deleted file mode 100644 index bd1ee9b177..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - PaypalButtonStyleColorOption, - PaypalButtonStyleLayoutOption, - PaypalButtonStyleShapeOption, - PaypalButtonStyleSizeOption, -} from '../../../payment/strategies/paypal'; - -import getValidButtonStyle from './get-valid-button-style'; - -describe('#getValidButtonStyle()', () => { - it('returns valid button style', () => { - const stylesMock = { - color: PaypalButtonStyleColorOption.SIlVER, - fundingicons: true, - height: 55, - layout: PaypalButtonStyleLayoutOption.HORIZONTAL, - shape: PaypalButtonStyleShapeOption.RECT, - size: PaypalButtonStyleSizeOption.SMALL, - tagline: true, - }; - - const expects = { - ...stylesMock, - }; - - expect(getValidButtonStyle(stylesMock)).toEqual(expects); - }); - - it('returns button style with rect shape if shape is not provided', () => { - const stylesMock = { - color: PaypalButtonStyleColorOption.SIlVER, - fundingicons: true, - height: 55, - layout: PaypalButtonStyleLayoutOption.HORIZONTAL, - shape: undefined, - size: PaypalButtonStyleSizeOption.SMALL, - tagline: true, - }; - - const expects = { - ...stylesMock, - shape: PaypalButtonStyleShapeOption.RECT, - }; - - expect(getValidButtonStyle(stylesMock)).toEqual(expects); - }); - - it('returns styles with updated height if height value is bigger than expected', () => { - const stylesMock = { - color: PaypalButtonStyleColorOption.SIlVER, - fundingicons: true, - height: 110, - layout: PaypalButtonStyleLayoutOption.HORIZONTAL, - shape: PaypalButtonStyleShapeOption.RECT, - size: PaypalButtonStyleSizeOption.SMALL, - tagline: true, - }; - - const expects = { - ...stylesMock, - height: 55, - }; - - expect(getValidButtonStyle(stylesMock)).toEqual(expects); - }); - - it('returns styles with updated height if height value is less than expected', () => { - const stylesMock = { - color: PaypalButtonStyleColorOption.SIlVER, - fundingicons: true, - height: 10, - layout: PaypalButtonStyleLayoutOption.HORIZONTAL, - shape: PaypalButtonStyleShapeOption.RECT, - size: PaypalButtonStyleSizeOption.SMALL, - tagline: true, - }; - - const expects = { - ...stylesMock, - height: 25, - }; - - expect(getValidButtonStyle(stylesMock)).toEqual(expects); - }); -}); diff --git a/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.ts b/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.ts deleted file mode 100644 index dd16073983..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/get-valid-button-style.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isNil, omitBy } from 'lodash'; - -import { PaypalStyleOptions } from '../../../payment/strategies/paypal'; - -export default function getValidButtonStyle(style: PaypalStyleOptions): PaypalStyleOptions { - const { color, fundingicons, height, layout, shape, size, tagline } = style; - - const validStyles = { - color, - fundingicons, - height: getValidHeight(height), - layout, - shape: shape || 'rect', - size, - tagline, - }; - - return omitBy(validStyles, isNil); -} - -function getValidHeight(height?: number): number { - const minHeight = 25; - const maxHeight = 55; - - if (typeof height !== 'number' || height > maxHeight) { - return maxHeight; - } - - if (height < minHeight) { - return minHeight; - } - - return height; -} diff --git a/packages/core/src/checkout-buttons/strategies/braintree/index.ts b/packages/core/src/checkout-buttons/strategies/braintree/index.ts deleted file mode 100644 index b18083c147..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Braintree PayPal Button Strategy -export { default as BraintreePaypalButtonStrategy } from './braintree-paypal-button-strategy'; -export { BraintreePaypalButtonInitializeOptions } from './braintree-paypal-button-options'; diff --git a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.spec.ts b/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.spec.ts deleted file mode 100644 index 9029ef2aec..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import mapToLegacyBillingAddress from './map-to-legacy-billing-address'; - -describe('mapToLegacyBillingAddress()', () => { - const detailsMock = { - username: 'johndoe', - email: 'test@test.com', - payerId: '1122abc', - firstName: 'John', - lastName: 'Doe', - countryCode: 'US', - phone: '55555555555', - billingAddress: { - line1: 'billing_line1', - line2: 'billing_line2', - city: 'billing_city', - state: 'billing_state', - postalCode: '03444', - countryCode: 'US', - }, - shippingAddress: { - recipientName: 'John Doe', - line1: 'shipping_line1', - line2: 'shipping_line2', - city: 'shipping_city', - state: 'shipping_state', - postalCode: '03444', - countryCode: 'US', - }, - }; - - it('maps details to legacy billing address using billing details as main address', () => { - const props = detailsMock; - - const expects = { - email: detailsMock.email, - first_name: detailsMock.firstName, - last_name: detailsMock.lastName, - phone_number: detailsMock.phone, - address_line_1: detailsMock.billingAddress.line1, - address_line_2: detailsMock.billingAddress.line2, - city: detailsMock.billingAddress.city, - state: detailsMock.billingAddress.state, - country_code: detailsMock.billingAddress.countryCode, - postal_code: detailsMock.billingAddress.postalCode, - }; - - expect(mapToLegacyBillingAddress(props)).toEqual(expects); - }); - - it('maps details to legacy billing address using shipping details as main address if billing details is not provided', () => { - const props = { - ...detailsMock, - billingAddress: undefined, - }; - - const expects = { - email: detailsMock.email, - first_name: detailsMock.firstName, - last_name: detailsMock.lastName, - phone_number: detailsMock.phone, - address_line_1: detailsMock.shippingAddress.line1, - address_line_2: detailsMock.shippingAddress.line2, - city: detailsMock.shippingAddress.city, - state: detailsMock.shippingAddress.state, - country_code: detailsMock.shippingAddress.countryCode, - postal_code: detailsMock.shippingAddress.postalCode, - }; - - expect(mapToLegacyBillingAddress(props)).toEqual(expects); - }); -}); diff --git a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.ts b/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.ts deleted file mode 100644 index 5b1dcd0451..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-billing-address.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { LegacyAddress } from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { BraintreeDetails } from '../../../payment/strategies/braintree'; - -export default function mapToLegacyBillingAddress( - details: BraintreeDetails, -): Partial { - const { billingAddress, email, firstName, lastName, phone, shippingAddress } = details; - - const address = billingAddress || shippingAddress; - - return { - email, - first_name: firstName, - last_name: lastName, - phone_number: phone, - address_line_1: address?.line1, - address_line_2: address?.line2, - city: address?.city, - state: address?.state, - country_code: address?.countryCode, - postal_code: address?.postalCode, - }; -} diff --git a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.spec.ts b/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.spec.ts deleted file mode 100644 index 3de93334a7..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import mapToLegacyShippingAddress from './map-to-legacy-shipping-address'; - -describe('mapToLegacyShippingAddress()', () => { - const detailsMock = { - email: 'test@test.com', - phone: '55555555555', - shippingAddress: { - recipientName: 'John Doe', - line1: 'shipping_line1', - line2: 'shipping_line2', - city: 'shipping_city', - state: 'shipping_state', - postalCode: '03444', - countryCode: 'US', - }, - }; - - it('maps details to legacy shipping address', () => { - const props = { - ...detailsMock, - billingAddress: undefined, - }; - - const expects = { - email: detailsMock.email, - first_name: 'John', - last_name: 'Doe', - phone_number: detailsMock.phone, - address_line_1: detailsMock.shippingAddress.line1, - address_line_2: detailsMock.shippingAddress.line2, - city: detailsMock.shippingAddress.city, - state: detailsMock.shippingAddress.state, - country_code: detailsMock.shippingAddress.countryCode, - postal_code: detailsMock.shippingAddress.postalCode, - }; - - expect(mapToLegacyShippingAddress(props)).toEqual(expects); - }); -}); diff --git a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.ts b/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.ts deleted file mode 100644 index ae2d8c5e60..0000000000 --- a/packages/core/src/checkout-buttons/strategies/braintree/map-to-legacy-shipping-address.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LegacyAddress } from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { BraintreeDetails } from '../../../payment/strategies/braintree'; - -export default function mapToLegacyShippingAddress( - details: BraintreeDetails, -): Partial { - const { email, phone, shippingAddress } = details; - - const recipientName = shippingAddress?.recipientName || ''; - const [firstName, lastName] = recipientName.split(' '); - - return { - email, - first_name: firstName || '', - last_name: lastName || '', - phone_number: phone, - address_line_1: shippingAddress?.line1, - address_line_2: shippingAddress?.line2, - city: shippingAddress?.city, - state: shippingAddress?.state, - country_code: shippingAddress?.countryCode, - postal_code: shippingAddress?.postalCode, - }; -} diff --git a/packages/core/src/customer/create-customer-strategy-registry.ts b/packages/core/src/customer/create-customer-strategy-registry.ts index af8fd7e670..57753fac30 100644 --- a/packages/core/src/customer/create-customer-strategy-registry.ts +++ b/packages/core/src/customer/create-customer-strategy-registry.ts @@ -1,4 +1,3 @@ -import { createFormPoster } from '@bigcommerce/form-poster'; import { RequestSender } from '@bigcommerce/request-sender'; import { getScriptLoader } from '@bigcommerce/script-loader'; @@ -7,11 +6,6 @@ import { Registry } from '../common/registry'; import { ConfigActionCreator, ConfigRequestSender } from '../config'; import { FormFieldsActionCreator, FormFieldsRequestSender } from '../form'; import { PaymentMethodActionCreator, PaymentMethodRequestSender } from '../payment'; -import { createPaymentIntegrationService } from '../payment-integration'; -import { - createBraintreeVisaCheckoutPaymentProcessor, - VisaCheckoutScriptLoader, -} from '../payment/strategies/braintree'; import { MasterpassScriptLoader } from '../payment/strategies/masterpass'; import { RemoteCheckoutActionCreator, RemoteCheckoutRequestSender } from '../remote-checkout'; import { @@ -20,12 +14,9 @@ import { SpamProtectionRequestSender, } from '../spam-protection'; -import createCustomerStrategyRegistryV2 from './create-customer-strategy-registry-v2'; import CustomerActionCreator from './customer-action-creator'; import CustomerRequestSender from './customer-request-sender'; -import CustomerStrategyActionCreator from './customer-strategy-action-creator'; import { CustomerStrategy } from './strategies'; -import { BraintreeVisaCheckoutCustomerStrategy } from './strategies/braintree'; import { DefaultCustomerStrategy } from './strategies/default'; import { MasterpassCustomerStrategy } from './strategies/masterpass'; import { SquareCustomerStrategy } from './strategies/square'; @@ -43,7 +34,6 @@ export default function createCustomerStrategyRegistry( new ConfigActionCreator(new ConfigRequestSender(requestSender)), new FormFieldsActionCreator(new FormFieldsRequestSender(requestSender)), ); - const formPoster = createFormPoster(); const paymentMethodActionCreator = new PaymentMethodActionCreator( new PaymentMethodRequestSender(requestSender), ); @@ -62,24 +52,6 @@ export default function createCustomerStrategyRegistry( spamProtectionActionCreator, ); - const paymentIntegrationService = createPaymentIntegrationService(store); - const customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService); - - registry.register( - 'braintreevisacheckout', - () => - new BraintreeVisaCheckoutCustomerStrategy( - store, - checkoutActionCreator, - paymentMethodActionCreator, - new CustomerStrategyActionCreator(registry, customerRegistryV2), - remoteCheckoutActionCreator, - createBraintreeVisaCheckoutPaymentProcessor(scriptLoader, requestSender), - new VisaCheckoutScriptLoader(scriptLoader), - formPoster, - ), - ); - registry.register( 'squarev2', () => diff --git a/packages/core/src/customer/customer-request-options.ts b/packages/core/src/customer/customer-request-options.ts index e6e6effab7..61e7d75a85 100644 --- a/packages/core/src/customer/customer-request-options.ts +++ b/packages/core/src/customer/customer-request-options.ts @@ -1,9 +1,5 @@ import { RequestOptions } from '../common/http-request'; -import { - BraintreePaypalCreditCustomerInitializeOptions, - BraintreeVisaCheckoutCustomerInitializeOptions, -} from './strategies/braintree'; import { MasterpassCustomerInitializeOptions } from './strategies/masterpass'; export { CustomerInitializeOptions } from '../generated/customer-initialize-options'; @@ -32,18 +28,6 @@ export interface CustomerRequestOptions extends RequestOptions { export interface BaseCustomerInitializeOptions extends CustomerRequestOptions { [key: string]: unknown; - /** - * The options that are required to facilitate Braintree Credit. They can be - * omitted unless you need to support Braintree Credit. - */ - braintreepaypalcredit?: BraintreePaypalCreditCustomerInitializeOptions; - - /** - * The options that are required to initialize the customer step of checkout - * when using Visa Checkout provided by Braintree. - */ - braintreevisacheckout?: BraintreeVisaCheckoutCustomerInitializeOptions; - /** * The options that are required to initialize the Masterpass payment method. * They can be omitted unless you need to support Masterpass. diff --git a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-options.ts b/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-options.ts deleted file mode 100644 index b640249be2..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-options.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { StandardError } from '../../../common/error/errors'; -import { BraintreeError } from '../../../payment/strategies/braintree'; - -export default interface BraintreePaypalCreditCustomerInitializeOptions { - /** - * The ID of a container which the checkout button should be inserted into. - */ - container: string; - - buttonHeight?: number; - - /** - * A callback that gets called on any error instead of submit payment or authorization errors. - * - * @param error - The error object describing the failure. - */ - onError?(error: BraintreeError | StandardError): void; - - /** - * A callback that gets called when wallet button clicked - */ - onClick?(): void; -} diff --git a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.spec.ts b/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.spec.ts deleted file mode 100644 index 6fc54f3de4..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.spec.ts +++ /dev/null @@ -1,595 +0,0 @@ -import { Action, createAction } from '@bigcommerce/data-store'; -import { createFormPoster, FormPoster } from '@bigcommerce/form-poster'; -import { createRequestSender } from '@bigcommerce/request-sender'; -import { createScriptLoader, getScriptLoader } from '@bigcommerce/script-loader'; -import { EventEmitter } from 'events'; -import { Observable, of } from 'rxjs'; - -import { BraintreeScriptLoader } from '@bigcommerce/checkout-sdk/braintree-utils'; -import { DefaultCheckoutButtonHeight } from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { - CheckoutActionCreator, - CheckoutRequestSender, - CheckoutStore, - createCheckoutStore, -} from '../../../checkout'; -import { getCheckoutStoreState } from '../../../checkout/checkouts.mock'; -import { MutationObserverFactory } from '../../../common/dom'; -import { InvalidArgumentError, MissingDataError } from '../../../common/error/errors'; -import { ConfigActionCreator, ConfigRequestSender } from '../../../config'; -import { getConfig } from '../../../config/configs.mock'; -import { FormFieldsActionCreator, FormFieldsRequestSender } from '../../../form'; -import { - PaymentMethod, - PaymentMethodActionCreator, - PaymentMethodActionType, - PaymentStrategyType, -} from '../../../payment'; -import { getBraintree } from '../../../payment/payment-methods.mock'; -import { - BraintreeDataCollector, - BraintreePaypalCheckout, - BraintreePaypalCheckoutCreator, - BraintreeSDKCreator, -} from '../../../payment/strategies/braintree'; -import { - getDataCollectorMock, - getPayPalCheckoutCreatorMock, - getPaypalCheckoutMock, -} from '../../../payment/strategies/braintree/braintree.mock'; -import { - PaypalButtonOptions, - PaypalButtonStyleColorOption, - PaypalHostWindow, - PaypalSDK, -} from '../../../payment/strategies/paypal'; -import { getPaypalMock } from '../../../payment/strategies/paypal/paypal.mock'; -import { - GoogleRecaptcha, - GoogleRecaptchaScriptLoader, - GoogleRecaptchaWindow, - SpamProtectionActionCreator, - SpamProtectionRequestSender, -} from '../../../spam-protection'; -import CustomerActionCreator from '../../customer-action-creator'; -import { CustomerInitializeOptions } from '../../customer-request-options'; -import CustomerRequestSender from '../../customer-request-sender'; - -import BraintreePaypalCreditCustomerInitializeOptions from './braintree-paypal-credit-customer-options'; -import BraintreePaypalCreditCustomerStrategy from './braintree-paypal-credit-customer-strategy'; - -describe('BraintreePaypalCreditCustomerStrategy', () => { - let braintreeSDKCreator: BraintreeSDKCreator; - let braintreeScriptLoader: BraintreeScriptLoader; - let braintreePaypalCheckoutCreatorMock: BraintreePaypalCheckoutCreator; - let braintreePaypalCheckoutMock: BraintreePaypalCheckout; - let checkoutActionCreator: CheckoutActionCreator; - let dataCollector: BraintreeDataCollector; - let eventEmitter: EventEmitter; - let formPoster: FormPoster; - let paymentMethodMock: PaymentMethod; - let paypalButtonElement: HTMLDivElement; - let paypalSdkMock: PaypalSDK; - let store: CheckoutStore; - let strategy: BraintreePaypalCreditCustomerStrategy; - let paymentMethodActionCreator: PaymentMethodActionCreator; - let customerActionCreator: CustomerActionCreator; - let loadPaymentMethodAction: Observable; - let googleRecaptcha: GoogleRecaptcha; - let googleRecaptchaMockWindow: GoogleRecaptchaWindow; - let googleRecaptchaScriptLoader: GoogleRecaptchaScriptLoader; - - const defaultButtonContainerId = 'braintree-paypal-button-mock-id'; - - const braintreePaypalCreditOptions: BraintreePaypalCreditCustomerInitializeOptions = { - container: defaultButtonContainerId, - onClick: jest.fn(), - onError: jest.fn(), - }; - - const initializationOptions: CustomerInitializeOptions = { - methodId: PaymentStrategyType.BRAINTREE_PAYPAL_CREDIT, - braintreepaypalcredit: braintreePaypalCreditOptions, - }; - - beforeEach(() => { - braintreePaypalCheckoutMock = getPaypalCheckoutMock(); - braintreePaypalCheckoutCreatorMock = getPayPalCheckoutCreatorMock( - braintreePaypalCheckoutMock, - false, - ); - dataCollector = getDataCollectorMock(); - paypalSdkMock = getPaypalMock(); - eventEmitter = new EventEmitter(); - - store = createCheckoutStore(getCheckoutStoreState()); - checkoutActionCreator = new CheckoutActionCreator( - new CheckoutRequestSender(createRequestSender()), - new ConfigActionCreator(new ConfigRequestSender(createRequestSender())), - new FormFieldsActionCreator(new FormFieldsRequestSender(createRequestSender())), - ); - braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window); - braintreeSDKCreator = new BraintreeSDKCreator(braintreeScriptLoader); - formPoster = createFormPoster(); - - paymentMethodMock = { - ...getBraintree(), - clientToken: 'myToken', - }; - - googleRecaptchaMockWindow = { grecaptcha: {} } as GoogleRecaptchaWindow; - googleRecaptchaScriptLoader = new GoogleRecaptchaScriptLoader( - createScriptLoader(), - googleRecaptchaMockWindow, - ); - googleRecaptcha = new GoogleRecaptcha( - googleRecaptchaScriptLoader, - new MutationObserverFactory(), - ); - loadPaymentMethodAction = of( - createAction(PaymentMethodActionType.LoadPaymentMethodSucceeded, paymentMethodMock, { - methodId: paymentMethodMock.id, - }), - ); - - customerActionCreator = new CustomerActionCreator( - new CustomerRequestSender(createRequestSender()), - checkoutActionCreator, - new SpamProtectionActionCreator( - googleRecaptcha, - new SpamProtectionRequestSender(createRequestSender()), - ), - ); - paymentMethodActionCreator = {} as PaymentMethodActionCreator; - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - paymentMethodActionCreator.loadPaymentMethod = jest.fn(() => loadPaymentMethodAction); - - (window as PaypalHostWindow).paypal = paypalSdkMock; - - strategy = new BraintreePaypalCreditCustomerStrategy( - store, - checkoutActionCreator, - customerActionCreator, - paymentMethodActionCreator, - braintreeSDKCreator, - formPoster, - window, - ); - - paypalButtonElement = document.createElement('div'); - paypalButtonElement.id = defaultButtonContainerId; - document.body.appendChild(paypalButtonElement); - - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve(store.getState())); - jest.spyOn(store.getState().paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( - paymentMethodMock, - ); - jest.spyOn(store.getState().config, 'getStoreConfigOrThrow').mockReturnValue( - getConfig().storeConfig, - ); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(braintreeSDKCreator, 'getClient').mockReturnValue(paymentMethodMock.clientToken); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(braintreeSDKCreator, 'getDataCollector').mockReturnValue(dataCollector); - jest.spyOn(braintreeScriptLoader, 'loadPaypalCheckout').mockReturnValue( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreePaypalCheckoutCreatorMock, - ); - jest.spyOn(formPoster, 'postForm').mockImplementation(() => {}); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(checkoutActionCreator, 'loadDefaultCheckout').mockImplementation(() => {}); - - jest.spyOn(paypalSdkMock, 'Buttons').mockImplementation((options: PaypalButtonOptions) => { - eventEmitter.on('createOrder', () => { - if (options.createOrder) { - options.createOrder().catch(() => {}); - } - }); - - eventEmitter.on('approve', () => { - if (options.onApprove) { - options.onApprove({ payerId: 'PAYER_ID' }).catch(() => {}); - } - }); - - eventEmitter.on('click', () => { - if (options.onClick) { - options.onClick(); - } - }); - - return { - isEligible: jest.fn(() => true), - render: jest.fn(), - }; - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - - delete (window as PaypalHostWindow).paypal; - - if (document.getElementById(defaultButtonContainerId)) { - document.body.removeChild(paypalButtonElement); - } - }); - - it('creates an instance of the braintree paypal credit checkout customer strategy', () => { - expect(strategy).toBeInstanceOf(BraintreePaypalCreditCustomerStrategy); - }); - - describe('#initialize()', () => { - it('throws error if methodId is not provided', async () => { - const options = { - containerId: defaultButtonContainerId, - } as CustomerInitializeOptions; - - try { - await strategy.initialize(options); - } catch (error) { - expect(error).toBeInstanceOf(InvalidArgumentError); - } - }); - - it('throws an error if containerId is not provided', async () => { - const options = { - methodId: PaymentStrategyType.BRAINTREE_PAYPAL_CREDIT, - } as CustomerInitializeOptions; - - try { - await strategy.initialize(options); - } catch (error) { - expect(error).toBeInstanceOf(InvalidArgumentError); - } - }); - - it('throws an error if braintreepaypalcredit is not provided', async () => { - const options = { - containerId: defaultButtonContainerId, - methodId: PaymentStrategyType.BRAINTREE_PAYPAL_CREDIT, - } as CustomerInitializeOptions; - - try { - await strategy.initialize(options); - } catch (error) { - expect(error).toBeInstanceOf(InvalidArgumentError); - } - }); - - it('calls braintreeSdk with proper options', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); - paymentMethodMock.initializationData = { - ...paymentMethodMock.initializationData, - isCreditEnabled: true, - currency: 'USD', - intent: undefined, - }; - - await strategy.initialize(initializationOptions); - - expect(braintreeSDKCreator.getPaypalCheckout).toHaveBeenCalledWith( - { - currency: 'USD', - isCreditEnabled: true, - intent: undefined, - }, - expect.any(Function), - expect.any(Function), - ); - }); - - it('throws error if client token is missing', async () => { - paymentMethodMock.clientToken = undefined; - - try { - await strategy.initialize(initializationOptions); - } catch (error) { - expect(error).toBeInstanceOf(MissingDataError); - } - }); - - it('initializes braintree sdk creator', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); - - await strategy.initialize(initializationOptions); - - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( - paymentMethodMock.clientToken, - ); - }); - - it('initializes braintree paypal checkout', async () => { - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getPaypalCheckout = jest.fn(); - - await strategy.initialize(initializationOptions); - - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( - paymentMethodMock.clientToken, - ); - expect(braintreeSDKCreator.getPaypalCheckout).toHaveBeenCalled(); - }); - - it('calls braintree paypal checkout create method', async () => { - await strategy.initialize(initializationOptions); - - expect(braintreePaypalCheckoutCreatorMock.create).toHaveBeenCalled(); - }); - - it('calls onError callback option if the error was caught on paypal checkout creation', async () => { - braintreePaypalCheckoutCreatorMock = getPayPalCheckoutCreatorMock(undefined, true); - - jest.spyOn(braintreeScriptLoader, 'loadPaypalCheckout').mockReturnValue( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreePaypalCheckoutCreatorMock, - ); - - await strategy.initialize(initializationOptions); - - expect(initializationOptions.braintreepaypalcredit?.onError).toHaveBeenCalled(); - }); - - it('renders braintree paylater button', async () => { - await strategy.initialize(initializationOptions); - - expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ - createOrder: expect.any(Function), - env: 'sandbox', - fundingSource: paypalSdkMock.FUNDING.PAYLATER, - onApprove: expect.any(Function), - onClick: expect.any(Function), - style: { - height: DefaultCheckoutButtonHeight, - color: PaypalButtonStyleColorOption.GOLD, - }, - }); - - expect(paypalSdkMock.Buttons).not.toHaveBeenCalledWith({ - createOrder: expect.any(Function), - env: 'sandbox', - fundingSource: paypalSdkMock.FUNDING.CREDIT, - onApprove: expect.any(Function), - style: { - label: 'credit', - shape: 'rect', - height: 45, - }, - }); - }); - - it('renders braintree paylater button with a customized height', async () => { - await strategy.initialize({ - ...initializationOptions, - braintreepaypalcredit: { - ...braintreePaypalCreditOptions, - buttonHeight: 100, - }, - }); - - expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ - createOrder: expect.any(Function), - env: 'sandbox', - fundingSource: paypalSdkMock.FUNDING.PAYLATER, - onApprove: expect.any(Function), - onClick: expect.any(Function), - style: { - height: 100, - color: PaypalButtonStyleColorOption.GOLD, - }, - }); - }); - - it('renders braintree credit button if paylater is not eligible', async () => { - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(paypalSdkMock, 'Buttons').mockImplementationOnce(() => { - return { - isEligible: jest.fn(() => false), - }; - }); - - await strategy.initialize(initializationOptions); - - expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ - createOrder: expect.any(Function), - env: 'sandbox', - fundingSource: paypalSdkMock.FUNDING.PAYLATER, - onApprove: expect.any(Function), - onClick: expect.any(Function), - style: { - height: DefaultCheckoutButtonHeight, - color: PaypalButtonStyleColorOption.GOLD, - }, - }); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(paypalSdkMock.Buttons).toHaveBeenCalledWith({ - createOrder: expect.any(Function), - env: 'sandbox', - fundingSource: paypalSdkMock.FUNDING.CREDIT, - onApprove: expect.any(Function), - onClick: expect.any(Function), - style: { - height: DefaultCheckoutButtonHeight, - label: 'credit', - color: PaypalButtonStyleColorOption.GOLD, - }, - }); - }); - - it('renders braintree checkout button in production environment if payment method is in test mode', async () => { - paymentMethodMock.config.testMode = false; - - await strategy.initialize(initializationOptions); - - expect(paypalSdkMock.Buttons).toHaveBeenCalledWith( - expect.objectContaining({ env: 'production' }), - ); - }); - - it('loads checkout details when customer is ready to pay', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('createOrder'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(checkoutActionCreator.loadDefaultCheckout).toHaveBeenCalledTimes(1); - }); - - it('sets up PayPal payment flow with current checkout details when customer is ready to pay', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('createOrder'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(braintreePaypalCheckoutMock.createPayment).toHaveBeenCalledWith({ - amount: 190, - currency: 'USD', - enableShippingAddress: true, - flow: 'checkout', - offerCredit: true, - shippingAddressEditable: false, - shippingAddressOverride: { - city: 'Some City', - countryCode: 'US', - line1: '12345 Testing Way', - line2: '', - phone: '555-555-5555', - postalCode: '95555', - recipientName: 'Test Tester', - state: 'CA', - }, - }); - }); - - it('triggers error callback if unable to set up payment flow', async () => { - const expectedError = new Error('Unable to set up payment flow'); - - expectedError.name = 'BraintreeError'; - - jest.spyOn(braintreePaypalCheckoutMock, 'createPayment').mockImplementation(() => - Promise.reject(expectedError), - ); - - await strategy.initialize(initializationOptions); - - eventEmitter.emit('createOrder'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(braintreePaypalCreditOptions.onError).toHaveBeenCalledWith(expectedError); - }); - - it('tokenizes PayPal payment details when authorization event is triggered', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('approve'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(braintreePaypalCheckoutMock.tokenizePayment).toHaveBeenCalledWith({ - payerId: 'PAYER_ID', - }); - }); - - it('posts payment details to server to set checkout data when PayPal payment details are tokenized', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('approve'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(formPoster.postForm).toHaveBeenCalledWith( - '/checkout.php', - expect.objectContaining({ - payment_type: 'paypal', - provider: 'braintreepaypalcredit', - action: 'set_external_checkout', - device_data: dataCollector.deviceData, - nonce: 'NONCE', - billing_address: JSON.stringify({ - email: 'foo@bar.com', - first_name: 'Foo', - last_name: 'Bar', - address_line_1: '56789 Testing Way', - address_line_2: 'Level 2', - city: 'Some Other City', - state: 'Arizona', - country_code: 'US', - postal_code: '96666', - }), - shipping_address: JSON.stringify({ - email: 'foo@bar.com', - first_name: 'Hello', - last_name: 'World', - address_line_1: '12345 Testing Way', - address_line_2: 'Level 1', - city: 'Some City', - state: 'California', - country_code: 'US', - postal_code: '95555', - }), - }), - ); - }); - - it('triggers error callback if unable to tokenize payment', async () => { - const expectedError = new Error('Unable to tokenize'); - - expectedError.name = 'BraintreeError'; - - jest.spyOn(braintreePaypalCheckoutMock, 'tokenizePayment').mockReturnValue( - Promise.reject(expectedError), - ); - - await strategy.initialize(initializationOptions); - - eventEmitter.emit('approve'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(braintreePaypalCreditOptions.onError).toHaveBeenCalledWith(expectedError); - }); - - it('triggers click callback when onClick paypal callback get called', async () => { - await strategy.initialize(initializationOptions); - - eventEmitter.emit('click'); - - await new Promise((resolve) => process.nextTick(resolve)); - - expect(braintreePaypalCreditOptions.onClick).toHaveBeenCalled(); - }); - }); - - describe('#deinitialize()', () => { - it('teardowns braintree sdk creator on strategy deinitialize', async () => { - braintreeSDKCreator.teardown = jest.fn(); - - await strategy.initialize(initializationOptions); - await strategy.deinitialize(); - - expect(braintreeSDKCreator.teardown).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.ts b/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.ts deleted file mode 100644 index 3931a639c7..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-paypal-credit-customer-strategy.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { FormPoster } from '@bigcommerce/form-poster'; -import { noop } from 'lodash'; - -import { - DefaultCheckoutButtonHeight, - PaymentMethod, -} from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { CheckoutActionCreator, CheckoutStore, InternalCheckoutSelectors } from '../../../checkout'; -import mapToLegacyBillingAddress from '../../../checkout-buttons/strategies/braintree/map-to-legacy-billing-address'; -import mapToLegacyShippingAddress from '../../../checkout-buttons/strategies/braintree/map-to-legacy-shipping-address'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, -} from '../../../common/error/errors'; -import { PaymentMethodActionCreator } from '../../../payment'; -import { - BraintreeError, - BraintreePaypalCheckout, - BraintreePaypalSdkCreatorConfig, - BraintreeSDKCreator, - BraintreeTokenizePayload, - mapToBraintreeShippingAddressOverride, -} from '../../../payment/strategies/braintree'; -import isBraintreeError from '../../../payment/strategies/braintree/is-braintree-error'; -import { - PaypalAuthorizeData, - PaypalButtonStyleColorOption, - PaypalButtonStyleLabelOption, - PaypalHostWindow, -} from '../../../payment/strategies/paypal'; -import CustomerActionCreator from '../../customer-action-creator'; -import CustomerCredentials from '../../customer-credentials'; -import { - CustomerInitializeOptions, - CustomerRequestOptions, - ExecutePaymentMethodCheckoutOptions, -} from '../../customer-request-options'; -import CustomerStrategy from '../customer-strategy'; - -import BraintreePaypalCreditCustomerInitializeOptions from './braintree-paypal-credit-customer-options'; - -export default class BraintreePaypalCreditCustomerStrategy implements CustomerStrategy { - constructor( - private _store: CheckoutStore, - private _checkoutActionCreator: CheckoutActionCreator, - private _customerActionCreator: CustomerActionCreator, - private _paymentMethodActionCreator: PaymentMethodActionCreator, - private _braintreeSDKCreator: BraintreeSDKCreator, - private _formPoster: FormPoster, - private _window: PaypalHostWindow, - ) {} - - async initialize(options: CustomerInitializeOptions): Promise { - const { braintreepaypalcredit, methodId } = options; - - if (!methodId) { - throw new InvalidArgumentError( - 'Unable to initialize payment because "options.methodId" argument is not provided.', - ); - } - - if (!braintreepaypalcredit) { - throw new InvalidArgumentError( - `Unable to initialize payment because "options.braintreepaypalcredit" argument is not provided.`, - ); - } - - if (!braintreepaypalcredit.container) { - throw new InvalidArgumentError( - `Unable to initialize payment because "options.braintreepaypalcredit.container" argument is not provided.`, - ); - } - - let state = this._store.getState(); - let paymentMethod: PaymentMethod; - - try { - paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - } catch (_e) { - state = await this._store.dispatch( - this._paymentMethodActionCreator.loadPaymentMethod(methodId), - ); - paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - } - - const { clientToken, initializationData } = paymentMethod; - - if (!clientToken || !initializationData) { - throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); - } - - const currencyCode = state.cart.getCartOrThrow().currency.code; - const paypalCheckoutOptions: Partial = { - currency: currencyCode, - intent: initializationData.intent, - isCreditEnabled: initializationData.isCreditEnabled, - }; - - const paypalCheckoutCallback = (braintreePaypalCheckout: BraintreePaypalCheckout) => - this._renderPayPalButton( - braintreePaypalCheckout, - braintreepaypalcredit, - methodId, - Boolean(paymentMethod.config.testMode), - ); - const paypalCheckoutErrorCallback = (error: BraintreeError) => - this._handleError(error, braintreepaypalcredit); - - this._braintreeSDKCreator.initialize(clientToken); - await this._braintreeSDKCreator.getPaypalCheckout( - paypalCheckoutOptions, - paypalCheckoutCallback, - paypalCheckoutErrorCallback, - ); - - return this._store.getState(); - } - - deinitialize(): Promise { - this._braintreeSDKCreator.teardown(); - - return Promise.resolve(this._store.getState()); - } - - signIn( - credentials: CustomerCredentials, - options?: CustomerRequestOptions, - ): Promise { - return this._store.dispatch( - this._customerActionCreator.signInCustomer(credentials, options), - ); - } - - signOut(options?: CustomerRequestOptions): Promise { - return this._store.dispatch(this._customerActionCreator.signOutCustomer(options)); - } - - executePaymentMethodCheckout( - options?: ExecutePaymentMethodCheckoutOptions, - ): Promise { - options?.continueWithCheckoutCallback?.(); - - return Promise.resolve(this._store.getState()); - } - - private _renderPayPalButton( - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypalcredit: BraintreePaypalCreditCustomerInitializeOptions, - methodId: string, - testMode: boolean, - ): void { - const { - container, - buttonHeight = DefaultCheckoutButtonHeight, - onClick = noop, - } = braintreepaypalcredit; - const { paypal } = this._window; - - let hasRenderedSmartButton = false; - - if (paypal) { - const fundingSources = [paypal.FUNDING.PAYLATER, paypal.FUNDING.CREDIT]; - const commonButtonStyle = { - height: buttonHeight, - color: PaypalButtonStyleColorOption.GOLD, - }; - - fundingSources.forEach((fundingSource) => { - const buttonStyle = - fundingSource === paypal.FUNDING.CREDIT - ? { label: PaypalButtonStyleLabelOption.CREDIT, ...commonButtonStyle } - : commonButtonStyle; - - if (!hasRenderedSmartButton) { - const paypalButtonRender = paypal.Buttons({ - env: testMode ? 'sandbox' : 'production', - fundingSource, - style: buttonStyle, - createOrder: () => - this._setupPayment( - braintreePaypalCheckout, - braintreepaypalcredit, - methodId, - ), - onApprove: (authorizeData: PaypalAuthorizeData) => - this._tokenizePayment( - authorizeData, - braintreePaypalCheckout, - braintreepaypalcredit, - methodId, - ), - onClick, - }); - - if (paypalButtonRender.isEligible()) { - paypalButtonRender.render(`#${container}`); - hasRenderedSmartButton = true; - } - } - }); - } - - if (!paypal || !hasRenderedSmartButton) { - this._removeElement(container); - } - } - - private async _setupPayment( - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypalcredit: BraintreePaypalCreditCustomerInitializeOptions, - methodId: string, - ): Promise { - try { - const state = await this._store.dispatch( - this._checkoutActionCreator.loadDefaultCheckout(), - ); - - const customer = state.customer.getCustomer(); - const amount = state.checkout.getCheckoutOrThrow().outstandingBalance; - const currencyCode = state.cart.getCartOrThrow().currency.code; - const paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - const address = customer?.addresses[0]; - const shippingAddressOverride = address - ? mapToBraintreeShippingAddressOverride(address) - : undefined; - - return await braintreePaypalCheckout.createPayment({ - flow: 'checkout', - enableShippingAddress: true, - shippingAddressEditable: false, - shippingAddressOverride, - amount, - currency: currencyCode, - offerCredit: true, - intent: paymentMethod.initializationData?.intent, - }); - } catch (error) { - if (isBraintreeError(error)) { - this._handleError(error, braintreepaypalcredit); - } - } - } - - private async _tokenizePayment( - authorizeData: PaypalAuthorizeData, - braintreePaypalCheckout: BraintreePaypalCheckout, - braintreepaypalcredit: BraintreePaypalCreditCustomerInitializeOptions, - methodId: string, - ): Promise { - try { - const { deviceData } = await this._braintreeSDKCreator.getDataCollector({ - paypal: true, - }); - const tokenizePayload = await braintreePaypalCheckout.tokenizePayment(authorizeData); - const { details, nonce } = tokenizePayload; - - this._formPoster.postForm('/checkout.php', { - payment_type: 'paypal', - provider: methodId, - action: 'set_external_checkout', - nonce, - device_data: deviceData, - billing_address: JSON.stringify(mapToLegacyBillingAddress(details)), - shipping_address: JSON.stringify(mapToLegacyShippingAddress(details)), - }); - - return tokenizePayload; - } catch (error) { - if (isBraintreeError(error)) { - this._handleError(error, braintreepaypalcredit); - } - } - } - - private _handleError( - error: BraintreeError, - braintreepaypalcredit: BraintreePaypalCreditCustomerInitializeOptions, - ): void { - const { container, onError } = braintreepaypalcredit; - - this._removeElement(container); - - if (onError) { - onError(error); - } else { - throw error; - } - } - - private _removeElement(elementId?: string): void { - const element = elementId && document.getElementById(elementId); - - if (element) { - element.remove(); - } - } -} diff --git a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-initialize-options.ts b/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-initialize-options.ts deleted file mode 100644 index 424ea3aca7..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-initialize-options.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default interface BraintreeVisaCheckoutCustomerInitializeOptions { - container: string; - onError?(error: Error): void; -} diff --git a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.spec.ts b/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.spec.ts deleted file mode 100644 index f249b68b45..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.spec.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { createAction } from '@bigcommerce/data-store'; -import { createFormPoster, FormPoster } from '@bigcommerce/form-poster'; -import { createRequestSender } from '@bigcommerce/request-sender'; -import { createScriptLoader } from '@bigcommerce/script-loader'; -import { merge } from 'lodash'; -import { of } from 'rxjs'; - -import { getBillingAddress } from '../../../billing/billing-addresses.mock'; -import { - CheckoutActionCreator, - CheckoutRequestSender, - CheckoutStore, - createCheckoutStore, -} from '../../../checkout'; -import { getCheckoutStoreState } from '../../../checkout/checkouts.mock'; -import { ConfigActionCreator, ConfigRequestSender } from '../../../config'; -import { FormFieldsActionCreator, FormFieldsRequestSender } from '../../../form'; -import { - PaymentMethod, - PaymentMethodActionCreator, - PaymentMethodRequestSender, -} from '../../../payment'; -import { createPaymentIntegrationService } from '../../../payment-integration'; -import { getBraintreeVisaCheckout } from '../../../payment/payment-methods.mock'; -import { - BraintreeVisaCheckoutPaymentProcessor, - createBraintreeVisaCheckoutPaymentProcessor, - VisaCheckoutScriptLoader, - VisaCheckoutSDK, -} from '../../../payment/strategies/braintree'; -import { RemoteCheckoutActionCreator, RemoteCheckoutRequestSender } from '../../../remote-checkout'; -import { getShippingAddress } from '../../../shipping/shipping-addresses.mock'; -import createCustomerStrategyRegistry from '../../create-customer-strategy-registry'; -import createCustomerStrategyRegistryV2 from '../../create-customer-strategy-registry-v2'; -import { CustomerInitializeOptions } from '../../customer-request-options'; -import CustomerStrategyActionCreator from '../../customer-strategy-action-creator'; -import { CustomerStrategyActionType } from '../../customer-strategy-actions'; -import { getRemoteCustomer } from '../../internal-customers.mock'; - -import BraintreeVisaCheckoutCustomerStrategy from './braintree-visacheckout-customer-strategy'; - -describe('BraintreeVisaCheckoutCustomerStrategy', () => { - let braintreeVisaCheckoutPaymentProcessor: BraintreeVisaCheckoutPaymentProcessor; - let checkoutActionCreator: CheckoutActionCreator; - let container: HTMLDivElement; - let customerStrategyActionCreator: CustomerStrategyActionCreator; - let paymentMethodActionCreator: PaymentMethodActionCreator; - let paymentMethodMock: PaymentMethod; - let remoteCheckoutActionCreator: RemoteCheckoutActionCreator; - let store: CheckoutStore; - let strategy: BraintreeVisaCheckoutCustomerStrategy; - let visaCheckoutScriptLoader: VisaCheckoutScriptLoader; - let visaCheckoutSDK: VisaCheckoutSDK; - let formPoster: FormPoster; - - beforeEach(() => { - const scriptLoader = createScriptLoader(); - const requestSender = createRequestSender(); - - braintreeVisaCheckoutPaymentProcessor = createBraintreeVisaCheckoutPaymentProcessor( - scriptLoader, - requestSender, - ); - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreeVisaCheckoutPaymentProcessor.initialize = jest.fn(() => Promise.resolve()); - braintreeVisaCheckoutPaymentProcessor.handleSuccess = jest.fn(() => Promise.resolve()); - - paymentMethodMock = getBraintreeVisaCheckout(); - - store = createCheckoutStore(getCheckoutStoreState()); - - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve(store.getState())); - jest.spyOn(store.getState().paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( - paymentMethodMock, - ); - - const remoteCheckoutRequestSender = new RemoteCheckoutRequestSender(requestSender); - - remoteCheckoutActionCreator = new RemoteCheckoutActionCreator( - remoteCheckoutRequestSender, - checkoutActionCreator, - ); - - visaCheckoutSDK = {} as VisaCheckoutSDK; - visaCheckoutSDK.init = jest.fn(); - visaCheckoutSDK.on = jest.fn(); - - visaCheckoutScriptLoader = new VisaCheckoutScriptLoader(scriptLoader); - visaCheckoutScriptLoader.load = jest.fn(() => Promise.resolve(visaCheckoutSDK)); - - formPoster = createFormPoster(); - - const registry = createCustomerStrategyRegistry(store, requestSender, ''); - const paymentIntegrationService = createPaymentIntegrationService(store); - const customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService); - - checkoutActionCreator = new CheckoutActionCreator( - new CheckoutRequestSender(requestSender), - new ConfigActionCreator(new ConfigRequestSender(requestSender)), - new FormFieldsActionCreator(new FormFieldsRequestSender(requestSender)), - ); - - paymentMethodActionCreator = new PaymentMethodActionCreator( - new PaymentMethodRequestSender(requestSender), - ); - customerStrategyActionCreator = new CustomerStrategyActionCreator( - registry, - customerRegistryV2, - ); - - strategy = new BraintreeVisaCheckoutCustomerStrategy( - store, - checkoutActionCreator, - paymentMethodActionCreator, - customerStrategyActionCreator, - remoteCheckoutActionCreator, - braintreeVisaCheckoutPaymentProcessor, - visaCheckoutScriptLoader, - formPoster, - ); - - container = document.createElement('div'); - container.setAttribute('id', 'login'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - - it('creates an instance of BraintreeVisaCheckoutCustomerStrategy', () => { - expect(strategy).toBeInstanceOf(BraintreeVisaCheckoutCustomerStrategy); - }); - - describe('#initialize()', () => { - let visaCheckoutOptions: CustomerInitializeOptions; - - beforeEach(() => { - visaCheckoutOptions = { - methodId: 'braintreevisacheckout', - braintreevisacheckout: { container: 'login' }, - }; - }); - - it('loads visacheckout in test mode if enabled', async () => { - paymentMethodMock.config.testMode = true; - - await strategy.initialize(visaCheckoutOptions); - - expect(visaCheckoutScriptLoader.load).toHaveBeenLastCalledWith(true); - }); - - it('loads visacheckout without test mode if disabled', async () => { - paymentMethodMock.config.testMode = false; - - await strategy.initialize(visaCheckoutOptions); - - expect(visaCheckoutScriptLoader.load).toHaveBeenLastCalledWith(false); - }); - - it('throws if the container is not available', async () => { - try { - await strategy.initialize( - merge({}, visaCheckoutOptions, { - braintreevisacheckout: { container: 'non-existing' }, - }), - ); - } catch (error) { - expect(error).toBeInstanceOf(Error); - } - }); - - it('creates a visa checkout button', async () => { - await strategy.initialize(visaCheckoutOptions); - - expect(container.querySelector('.v-button')).not.toBeNull(); - }); - - it('initialises the visa checkout payment processor with the right data', async () => { - await strategy.initialize(visaCheckoutOptions); - - expect(braintreeVisaCheckoutPaymentProcessor.initialize).toHaveBeenCalledWith( - 'clientToken', - { - collectShipping: true, - currencyCode: 'USD', - locale: 'en_US', - subtotal: 190, - }, - ); - }); - - it('calls the visa checkout sdk init method with the processed options', async () => { - const options = { - settings: { - shipping: { collectShipping: true }, - }, - paymentRequest: {}, - }; - - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - braintreeVisaCheckoutPaymentProcessor.initialize = jest.fn(() => options); - - await strategy.initialize(visaCheckoutOptions); - - expect(visaCheckoutSDK.init).toHaveBeenCalledWith(options); - }); - - it('registers the error and success callbacks', async () => { - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - visaCheckoutSDK.on = jest.fn((_, callback) => callback()); - await strategy.initialize(visaCheckoutOptions); - - expect(visaCheckoutSDK.on).toHaveBeenCalledWith( - 'payment.success', - expect.any(Function), - ); - expect(visaCheckoutSDK.on).toHaveBeenCalledWith('payment.error', expect.any(Function)); - }); - - describe('when payment.success', () => { - beforeEach(() => { - visaCheckoutSDK.on = jest.fn((type, callback) => - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - type === 'payment.success' ? callback('data') : undefined, - ); - jest.spyOn(customerStrategyActionCreator, 'widgetInteraction').mockImplementation( - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (cb) => cb(), - ); - }); - - it('payment success triggers handle success in BraintreeVisaCheckoutPaymentProcessor', async () => { - await strategy.initialize(visaCheckoutOptions); - - expect(braintreeVisaCheckoutPaymentProcessor.handleSuccess).toHaveBeenCalledWith( - 'data', - getShippingAddress(), - getBillingAddress(), - ); - }); - - it('reloads quote and payment method', async () => { - jest.spyOn(checkoutActionCreator, 'loadCurrentCheckout'); - - await strategy.initialize(visaCheckoutOptions); - - expect(checkoutActionCreator.loadCurrentCheckout).toHaveBeenCalled(); - }); - - it('triggers a widgetInteraction action', async () => { - const widgetInteractionAction = of( - createAction(CustomerStrategyActionType.WidgetInteractionStarted), - ); - - jest.spyOn(customerStrategyActionCreator, 'widgetInteraction').mockImplementation( - () => widgetInteractionAction, - ); - - await strategy.initialize(visaCheckoutOptions); - - expect(store.dispatch).toHaveBeenCalledWith(widgetInteractionAction, { - queueId: 'widgetInteraction', - }); - expect(customerStrategyActionCreator.widgetInteraction).toHaveBeenCalledWith( - expect.any(Function), - { methodId: 'braintreevisacheckout' }, - ); - }); - }); - - it('payment error triggers onError from the options', async () => { - const onError = jest.fn(); - const errorMock = new Error(); - - visaCheckoutSDK.on = jest.fn((type, callback) => - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - type === 'payment.error' ? callback('data', errorMock) : undefined, - ); - - await strategy.initialize( - merge({}, visaCheckoutOptions, { - braintreevisacheckout: { onError }, - }), - ); - - expect(onError).toHaveBeenCalledWith(errorMock); - }); - }); - - describe('#signIn()', () => { - beforeEach(async () => { - await strategy.initialize({ - methodId: 'visaCheckout', - braintreevisacheckout: { container: 'login' }, - }); - }); - - it('throws error if trying to sign in programmatically', () => { - expect(() => strategy.signIn()).toThrow(); - }); - }); - - describe('#signOut()', () => { - beforeEach(async () => { - const remoteCustomer = merge({}, getRemoteCustomer(), { - remote: { provider: 'braintreevisacheckout' }, - }); - - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(store.getState().customer, 'getCustomer').mockReturnValue(remoteCustomer); - - // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - remoteCheckoutActionCreator.signOut = jest.fn(() => 'data'); - - await strategy.initialize({ - methodId: 'visaCheckout', - braintreevisacheckout: { container: 'login' }, - }); - }); - - it('throws error if trying to sign out programmatically', async () => { - const options = { - methodId: 'braintreevisacheckout', - }; - - await strategy.signOut(options); - - expect(remoteCheckoutActionCreator.signOut).toHaveBeenCalledWith( - 'braintreevisacheckout', - options, - ); - expect(store.dispatch).toHaveBeenCalledWith('data'); - }); - }); - - describe('#deinitialize()', () => { - beforeEach(async () => { - braintreeVisaCheckoutPaymentProcessor.deinitialize = jest.fn(() => Promise.resolve()); - await strategy.initialize({ - methodId: 'visaCheckout', - braintreevisacheckout: { container: 'login' }, - }); - }); - - it('deinitializes BraintreeVisaCheckoutPaymentProcessor', async () => { - await strategy.deinitialize(); - - expect(braintreeVisaCheckoutPaymentProcessor.deinitialize).toHaveBeenCalled(); - }); - }); - - describe('#executePaymentMethodCheckout', () => { - it('runs continue callback automatically on execute payment method checkout', async () => { - const mockCallback = jest.fn(); - - await strategy.executePaymentMethodCheckout({ - continueWithCheckoutCallback: mockCallback, - }); - - expect(mockCallback.mock.calls).toHaveLength(1); - }); - }); -}); diff --git a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.ts b/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.ts deleted file mode 100644 index 386112ab99..0000000000 --- a/packages/core/src/customer/strategies/braintree/braintree-visacheckout-customer-strategy.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { FormPoster } from '@bigcommerce/form-poster'; - -import { CheckoutActionCreator, CheckoutStore, InternalCheckoutSelectors } from '../../../checkout'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, - NotImplementedError, -} from '../../../common/error/errors'; -import { PaymentMethod, PaymentMethodActionCreator } from '../../../payment'; -import { - BraintreeVisaCheckoutPaymentProcessor, - VisaCheckoutPaymentSuccessPayload, - VisaCheckoutScriptLoader, -} from '../../../payment/strategies/braintree'; -import { RemoteCheckoutActionCreator } from '../../../remote-checkout'; -import { - CustomerInitializeOptions, - CustomerRequestOptions, - ExecutePaymentMethodCheckoutOptions, -} from '../../customer-request-options'; -import CustomerStrategyActionCreator from '../../customer-strategy-action-creator'; -import CustomerStrategy from '../customer-strategy'; - -export default class BraintreeVisaCheckoutCustomerStrategy implements CustomerStrategy { - private _paymentMethod?: PaymentMethod; - private _buttonClassName = 'visa-checkout-wrapper'; - - constructor( - private _store: CheckoutStore, - private _checkoutActionCreator: CheckoutActionCreator, - private _paymentMethodActionCreator: PaymentMethodActionCreator, - private _customerStrategyActionCreator: CustomerStrategyActionCreator, - private _remoteCheckoutActionCreator: RemoteCheckoutActionCreator, - private _braintreeVisaCheckoutPaymentProcessor: BraintreeVisaCheckoutPaymentProcessor, - private _visaCheckoutScriptLoader: VisaCheckoutScriptLoader, - private _formPoster: FormPoster, - ) {} - - initialize(options: CustomerInitializeOptions): Promise { - const { braintreevisacheckout: visaCheckoutOptions, methodId } = options; - - if (!visaCheckoutOptions || !methodId) { - throw new InvalidArgumentError( - 'Unable to proceed because "options.braintreevisacheckout" argument is not provided.', - ); - } - - return this._store - .dispatch(this._paymentMethodActionCreator.loadPaymentMethod(methodId)) - .then((state) => { - this._paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - - const { clientToken } = this._paymentMethod; - - const checkout = state.checkout.getCheckout(); - const storeConfig = state.config.getStoreConfig(); - - if (!checkout) { - throw new MissingDataError(MissingDataErrorType.MissingCheckout); - } - - if (!storeConfig) { - throw new MissingDataError(MissingDataErrorType.MissingCheckoutConfig); - } - - if (!clientToken) { - throw new MissingDataError(MissingDataErrorType.MissingPaymentMethod); - } - - const { container, onError = () => {} } = visaCheckoutOptions; - - const initOptions = { - locale: storeConfig.storeProfile.storeLanguage, - collectShipping: true, - subtotal: checkout.subtotal, - currencyCode: storeConfig.currency.code, - }; - - return Promise.all([ - this._visaCheckoutScriptLoader.load(this._paymentMethod.config.testMode), - this._braintreeVisaCheckoutPaymentProcessor.initialize( - clientToken, - initOptions, - ), - ]) - .then(([visaCheckout, initOptions]) => { - const signInButton = this._createSignInButton( - container, - this._buttonClassName, - ); - - visaCheckout.init(initOptions); - visaCheckout.on( - 'payment.success', - (paymentSuccessPayload: VisaCheckoutPaymentSuccessPayload) => - this._paymentInstrumentSelected(paymentSuccessPayload).catch( - (error) => onError(error), - ), - ); - visaCheckout.on('payment.error', (_, error) => onError(error)); - - return signInButton; - }) - .then((signInButton) => { - signInButton.style.visibility = 'visible'; - }); - }) - .then(() => this._store.getState()); - } - - signIn(): Promise { - throw new NotImplementedError( - 'In order to sign in via VisaCheckout, the shopper must click on "Visa Checkout" button.', - ); - } - - signOut(options?: CustomerRequestOptions): Promise { - return this._store.dispatch( - this._remoteCheckoutActionCreator.signOut('braintreevisacheckout', options), - ); - } - - executePaymentMethodCheckout( - options?: ExecutePaymentMethodCheckoutOptions, - ): Promise { - options?.continueWithCheckoutCallback?.(); - - return Promise.resolve(this._store.getState()); - } - - deinitialize(): Promise { - this._paymentMethod = undefined; - - return this._braintreeVisaCheckoutPaymentProcessor - .deinitialize() - .then(() => this._store.getState()); - } - - private _paymentInstrumentSelected(paymentSuccessPayload: VisaCheckoutPaymentSuccessPayload) { - const state = this._store.getState(); - - if (!this._paymentMethod) { - throw new Error('Payment method not initialized'); - } - - const { id: methodId } = this._paymentMethod; - - return this._store.dispatch( - this._customerStrategyActionCreator.widgetInteraction( - () => { - return this._braintreeVisaCheckoutPaymentProcessor - .handleSuccess( - paymentSuccessPayload, - state.shippingAddress.getShippingAddress(), - state.billingAddress.getBillingAddress(), - ) - .then(async () => { - await this._store.dispatch( - this._checkoutActionCreator.loadCurrentCheckout(), - ); - this._onPaymentSelectComplete(); - }); - }, - { methodId }, - ), - { queueId: 'widgetInteraction' }, - ); - } - - private _onPaymentSelectComplete(): void { - this._formPoster.postForm('/checkout.php', { - headers: { - Accept: 'text/html', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - } - - private _createSignInButton(containerId: string, buttonClass: string): HTMLElement { - const container = document.querySelector(`#${containerId}`); - - if (!container) { - throw new Error('Need a container to place the button'); - } - - return ( - (container.querySelector(`.${buttonClass}`) as HTMLElement) || - this._insertVisaCheckoutButton(container, buttonClass) - ); - } - - private _insertVisaCheckoutButton(container: Element, buttonClass: string): HTMLElement { - const buttonSource = - 'https://secure.checkout.visa.com/wallet-services-web/xo/button.png?acceptCanadianVisaDebit=false&cobrand=true&height=34&width=178'; - const buttonTemplate = ` - Visa Checkout - Tell Me More`; - - const visaCheckoutButton = document.createElement('div'); - - visaCheckoutButton.style.display = 'flex'; - visaCheckoutButton.style.flexDirection = 'column'; - visaCheckoutButton.style.visibility = 'hidden'; - visaCheckoutButton.className = buttonClass; - visaCheckoutButton.innerHTML = buttonTemplate; - - container.appendChild(visaCheckoutButton); - - return visaCheckoutButton; - } -} diff --git a/packages/core/src/customer/strategies/braintree/index.ts b/packages/core/src/customer/strategies/braintree/index.ts deleted file mode 100644 index 0a3221fc02..0000000000 --- a/packages/core/src/customer/strategies/braintree/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as BraintreeVisaCheckoutCustomerStrategy } from './braintree-visacheckout-customer-strategy'; -export { default as BraintreeVisaCheckoutCustomerInitializeOptions } from './braintree-visacheckout-customer-initialize-options'; -export { default as BraintreePaypalCreditCustomerStrategy } from './braintree-paypal-credit-customer-strategy'; -export { default as BraintreePaypalCreditCustomerInitializeOptions } from './braintree-paypal-credit-customer-options'; diff --git a/packages/payment-integration-api/src/is-resolvable-module.ts b/packages/payment-integration-api/src/is-resolvable-module.ts index 83a67e1682..d7adcac0f9 100644 --- a/packages/payment-integration-api/src/is-resolvable-module.ts +++ b/packages/payment-integration-api/src/is-resolvable-module.ts @@ -3,5 +3,5 @@ import ResolvableModule from './resolvable-module'; export default function isResolvableModule( module: TModule, ): module is ResolvableModule { - return 'resolveIds' in module; + return module && 'resolveIds' in module; }