From 23a7f027379b93bc23affab878a38ce623a8d3ea Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 8 Aug 2024 17:30:51 +0300 Subject: [PATCH] feat(payment): PAYPAL-2611 moved BT venmo button strategy inside packages --- .../braintree-venmo-button-strategy.spec.ts | 134 +++++++-------- .../braintree-venmo-button-strategy.ts | 161 +++++++++--------- .../braintree-venmo-initialize-options.ts | 45 +++++ ...te-braintree-venmo-button-strategy.spec.ts | 19 +++ .../create-braintree-venmo-button-strategy.ts | 36 ++++ packages/braintree-integration/src/index.ts | 5 + .../src/braintree-integration-service.ts | 40 +++++ packages/braintree-utils/src/braintree.ts | 2 +- .../src/buy-now-cart-request-body.ts | 19 +++ packages/braintree-utils/src/index.ts | 5 + .../src/map-to-legacy-billing-address.spec.ts | 71 ++++++++ .../src/map-to-legacy-billing-address.ts | 23 +++ .../map-to-legacy-shipping-address.spec.ts | 39 +++++ .../src/map-to-legacy-shipping-address.ts | 24 +++ packages/braintree-utils/src/paypal.ts | 1 + packages/braintree-utils/src/types.ts | 12 ++ .../src/unsupported-browser-error.ts | 14 ++ .../create-checkout-button-registry.spec.ts | 5 - .../create-checkout-button-registry.ts | 17 -- .../strategies/braintree/index.ts | 1 - 20 files changed, 495 insertions(+), 178 deletions(-) rename packages/{core/src/checkout-buttons/strategies/braintree => braintree-integration/src/braintree-venmo}/braintree-venmo-button-strategy.spec.ts (85%) rename packages/{core/src/checkout-buttons/strategies/braintree => braintree-integration/src/braintree-venmo}/braintree-venmo-button-strategy.ts (66%) create mode 100644 packages/braintree-integration/src/braintree-venmo/braintree-venmo-initialize-options.ts create mode 100644 packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.spec.ts create mode 100644 packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.ts create mode 100644 packages/braintree-utils/src/buy-now-cart-request-body.ts create mode 100644 packages/braintree-utils/src/map-to-legacy-billing-address.spec.ts create mode 100644 packages/braintree-utils/src/map-to-legacy-billing-address.ts create mode 100644 packages/braintree-utils/src/map-to-legacy-shipping-address.spec.ts create mode 100644 packages/braintree-utils/src/map-to-legacy-shipping-address.ts create mode 100644 packages/braintree-utils/src/unsupported-browser-error.ts diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.spec.ts b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.spec.ts similarity index 85% rename from packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.spec.ts rename to packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.spec.ts index b8f3b501aab..24b9dec1767 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.spec.ts +++ b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.spec.ts @@ -1,51 +1,49 @@ import { createFormPoster, FormPoster } from '@bigcommerce/form-poster'; -import { createRequestSender, RequestSender } from '@bigcommerce/request-sender'; import { getScriptLoader } from '@bigcommerce/script-loader'; -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 { CheckoutStore, createCheckoutStore } from '../../../checkout'; -import { getCheckoutStoreState } from '../../../checkout/checkouts.mock'; -import { InvalidArgumentError, MissingDataError } from '../../../common/error/errors'; -import { - PaymentMethod, - PaymentMethodActionCreator, - PaymentMethodRequestSender, -} from '../../../payment'; -import { getBraintree } from '../../../payment/payment-methods.mock'; import { - BraintreeSDKCreator, + BraintreeHostWindow, + BraintreeIntegrationService, + BraintreeScriptLoader, BraintreeVenmoCheckout, BraintreeVenmoCheckoutCreator, -} from '../../../payment/strategies/braintree'; -import { CheckoutButtonInitializeOptions } from '../../checkout-button-options'; -import CheckoutButtonMethodType from '../checkout-button-method-type'; + BuyNowCartRequestBody, + getBraintree, +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + Cart, + CartSource, + CheckoutButtonInitializeOptions, + InvalidArgumentError, + MissingDataError, + PaymentIntegrationService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import { + getCart, + PaymentIntegrationServiceMock, +} from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; import BraintreeVenmoButtonStrategy from './braintree-venmo-button-strategy'; describe('BraintreeVenmoButtonStrategy', () => { - let cartRequestSender: CartRequestSender; - let braintreeSDKCreator: BraintreeSDKCreator; let braintreeScriptLoader: BraintreeScriptLoader; + let braintreeIntegrationService: BraintreeIntegrationService; + let paymentIntegrationService: PaymentIntegrationService; let braintreeVenmoCheckoutMock: BraintreeVenmoCheckout; let braintreeVenmoCheckoutCreatorMock: BraintreeVenmoCheckoutCreator; let formPoster: FormPoster; - let requestSender: RequestSender; - let paymentMethodActionCreator: PaymentMethodActionCreator; + let mockWindow: BraintreeHostWindow; let paymentMethodMock: PaymentMethod; let venmoButtonElement: HTMLDivElement; - let store: CheckoutStore; let strategy: BraintreeVenmoButtonStrategy; const defaultContainerId = 'braintree-venmo-button-mock-id'; - const buyNowCartMock = { + const buyNowCartMock: Cart = { ...getCart(), - id: 999, + id: '999', source: CartSource.BuyNow, }; @@ -64,7 +62,7 @@ describe('BraintreeVenmoButtonStrategy', () => { }; const getBraintreeVenmoButtonOptionsMock = () => ({ - methodId: CheckoutButtonMethodType.BRAINTREE_VENMO, + methodId: 'braintreevenmo', containerId: defaultContainerId, braintreevenmo: { onError: jest.fn(), @@ -72,7 +70,7 @@ describe('BraintreeVenmoButtonStrategy', () => { }); const getBuyNowBraintreeVenmoButtonOptionsMock = () => ({ - methodId: CheckoutButtonMethodType.BRAINTREE_VENMO, + methodId: 'braintreevenmo', containerId: defaultContainerId, braintreevenmo: { onError: jest.fn(), @@ -111,22 +109,19 @@ describe('BraintreeVenmoButtonStrategy', () => { }; beforeEach(() => { - store = createCheckoutStore(getCheckoutStoreState()); - requestSender = createRequestSender(); - paymentMethodActionCreator = new PaymentMethodActionCreator( - new PaymentMethodRequestSender(createRequestSender()), - ); - braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window); - braintreeSDKCreator = new BraintreeSDKCreator(braintreeScriptLoader); + paymentIntegrationService = new PaymentIntegrationServiceMock(); formPoster = createFormPoster(); - cartRequestSender = new CartRequestSender(requestSender); + mockWindow = {} as BraintreeHostWindow; + braintreeScriptLoader = new BraintreeScriptLoader(getScriptLoader(), window); + braintreeIntegrationService = new BraintreeIntegrationService( + braintreeScriptLoader, + mockWindow, + ); strategy = new BraintreeVenmoButtonStrategy( - store, - paymentMethodActionCreator, - cartRequestSender, - braintreeSDKCreator, + paymentIntegrationService, formPoster, + braintreeIntegrationService, ); paymentMethodMock = { @@ -137,20 +132,22 @@ describe('BraintreeVenmoButtonStrategy', () => { }, }; - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve(store.getState())); - jest.spyOn(store.getState().paymentMethods, 'getPaymentMethodOrThrow').mockReturnValue( + jest.spyOn(paymentIntegrationService.getState(), 'getPaymentMethodOrThrow').mockReturnValue( paymentMethodMock, ); - // 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); - jest.spyOn(braintreeSDKCreator, 'getDataCollector').mockReturnValue({ + jest.spyOn(braintreeIntegrationService, 'getClient').mockReturnValue( + Promise.resolve({ request: jest.fn() }), + ); + jest.spyOn(paymentIntegrationService, 'createBuyNowCart').mockReturnValue( + Promise.resolve(buyNowCartMock), + ); + jest.spyOn(braintreeIntegrationService, 'getDataCollector').mockReturnValue({ // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore deviceData: { device: 'something' }, }); + jest.spyOn(formPoster, 'postForm').mockImplementation(() => {}); venmoButtonElement = document.createElement('div'); @@ -197,7 +194,7 @@ describe('BraintreeVenmoButtonStrategy', () => { it('throws an error if containerId is not provided', async () => { const options = { - methodId: CheckoutButtonMethodType.BRAINTREE_VENMO, + methodId: 'braintreevenmo', } as CheckoutButtonInitializeOptions; try { @@ -210,36 +207,36 @@ describe('BraintreeVenmoButtonStrategy', () => { it('initializes braintree sdk creator', async () => { const options = getBraintreeVenmoButtonOptionsMock(); - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getVenmoCheckout = jest.fn(); + braintreeIntegrationService.initialize = jest.fn(); + braintreeIntegrationService.getVenmoCheckout = jest.fn(); await strategy.initialize(options); - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( + expect(braintreeIntegrationService.initialize).toHaveBeenCalledWith( paymentMethodMock.clientToken, - store.getState().config.getStoreConfigOrThrow(), + paymentIntegrationService.getState().getStoreConfig(), ); }); it('initializes the braintree venmo checkout', async () => { const options = getBraintreeVenmoButtonOptionsMock(); - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getVenmoCheckout = jest.fn(); + braintreeIntegrationService.initialize = jest.fn(); + braintreeIntegrationService.getVenmoCheckout = jest.fn(); await strategy.initialize(options); - expect(braintreeSDKCreator.initialize).toHaveBeenCalledWith( + expect(braintreeIntegrationService.initialize).toHaveBeenCalledWith( paymentMethodMock.clientToken, - store.getState().config.getStoreConfigOrThrow(), + paymentIntegrationService.getState().getStoreConfig(), ); - expect(braintreeSDKCreator.getVenmoCheckout).toHaveBeenCalled(); + expect(braintreeIntegrationService.getVenmoCheckout).toHaveBeenCalled(); }); it('calls braintree venmo checkout create method', async () => { braintreeVenmoCheckoutCreatorMock = { create: jest.fn() }; - jest.spyOn(braintreeSDKCreator, 'getClient').mockReturnValue( + jest.spyOn(braintreeIntegrationService, 'getClient').mockReturnValue( // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -395,12 +392,6 @@ describe('BraintreeVenmoButtonStrategy', () => { // @ts-ignore braintreeVenmoCheckoutCreatorMock, ); - 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, - }); const venmoButton = document.getElementById(options.containerId); @@ -409,7 +400,7 @@ describe('BraintreeVenmoButtonStrategy', () => { if (venmoButton) { venmoButton.click(); - expect(cartRequestSender.createBuyNowCart).toHaveBeenCalled(); + expect(paymentIntegrationService.createBuyNowCart).toHaveBeenCalled(); } }); @@ -562,7 +553,7 @@ describe('BraintreeVenmoButtonStrategy', () => { // @ts-ignore braintreeVenmoCheckoutCreatorMock, ); - jest.spyOn(cartRequestSender, 'createBuyNowCart').mockReturnValue({ + jest.spyOn(paymentIntegrationService, '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 @@ -591,7 +582,6 @@ describe('BraintreeVenmoButtonStrategy', () => { provider: 'braintreevenmo', billing_address: JSON.stringify(expectedAddress), shipping_address: JSON.stringify(expectedAddress), - cart_id: buyNowCartMock.id, }); } }); @@ -670,14 +660,14 @@ describe('BraintreeVenmoButtonStrategy', () => { it('teardowns braintree sdk creator on strategy deinitialize', async () => { const options = getBraintreeVenmoButtonOptionsMock(); - braintreeSDKCreator.initialize = jest.fn(); - braintreeSDKCreator.getVenmoCheckout = jest.fn(); - braintreeSDKCreator.teardown = jest.fn(); + braintreeIntegrationService.initialize = jest.fn(); + braintreeIntegrationService.getVenmoCheckout = jest.fn(); + braintreeIntegrationService.teardown = jest.fn(); await strategy.initialize(options); await strategy.deinitialize(); - expect(braintreeSDKCreator.teardown).toHaveBeenCalled(); + expect(braintreeIntegrationService.teardown).toHaveBeenCalled(); }); }); }); diff --git a/packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.ts b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.ts similarity index 66% rename from packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.ts rename to packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.ts index 6a0aa002cb0..5809a86c3de 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/braintree-venmo-button-strategy.ts +++ b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-button-strategy.ts @@ -1,34 +1,30 @@ -import { FormPoster } from '@bigcommerce/form-poster'; -import { noop } from 'lodash'; - -import { DefaultCheckoutButtonHeight } from '@bigcommerce/checkout-sdk/payment-integration-api'; - -import { BuyNowCartRequestBody, CartRequestSender } from '../../../cart'; -import { BuyNowCartCreationError } from '../../../cart/errors'; -import { CheckoutStore } from '../../../checkout'; -import { - InvalidArgumentError, - MissingDataError, - MissingDataErrorType, - UnsupportedBrowserError, -} from '../../../common/error/errors'; -import { PaymentMethodActionCreator } from '../../../payment'; import { BraintreeError, - BraintreeSDKCreator, + BraintreeIntegrationService, BraintreeTokenizePayload, BraintreeVenmoCheckout, -} from '../../../payment/strategies/braintree'; -import { + BuyNowCartRequestBody, + mapToLegacyBillingAddress, + mapToLegacyShippingAddress, PaypalButtonStyleColorOption, PaypalStyleOptions, -} from '../../../payment/strategies/paypal'; -import { CheckoutButtonInitializeOptions } from '../../checkout-button-options'; -import CheckoutButtonStrategy from '../checkout-button-strategy'; -import { CheckoutButtonMethodType } from '../index'; - -import mapToLegacyBillingAddress from './map-to-legacy-billing-address'; -import mapToLegacyShippingAddress from './map-to-legacy-shipping-address'; + UnsupportedBrowserError, +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { + BuyNowCartCreationError, + Cart, + CheckoutButtonInitializeOptions, + CheckoutButtonStrategy, + DefaultCheckoutButtonHeight, + InvalidArgumentError, + MissingDataError, + MissingDataErrorType, + PaymentIntegrationService, + PaymentMethod, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { noop } from 'lodash'; +import { FormPoster } from '@bigcommerce/form-poster'; +import { WithBraintreeVenmoInitializeOptions } from './braintree-venmo-initialize-options'; const getVenmoButtonStyle = (styles: PaypalStyleOptions): Record => { const { color } = styles; @@ -71,17 +67,17 @@ interface BuyNowInitializeOptions { } export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrategy { - private _onError = noop; + private onError = noop; constructor( - private _store: CheckoutStore, - private _paymentMethodActionCreator: PaymentMethodActionCreator, - private _cartRequestSender: CartRequestSender, - private _braintreeSDKCreator: BraintreeSDKCreator, - private _formPoster: FormPoster, + private paymentIntegrationService: PaymentIntegrationService, + private formPoster: FormPoster, + private braintreeIntegrationService: BraintreeIntegrationService, ) {} - async initialize(options: CheckoutButtonInitializeOptions): Promise { + async initialize( + options: CheckoutButtonInitializeOptions & WithBraintreeVenmoInitializeOptions, + ): Promise { const { braintreevenmo, containerId, methodId } = options; if (!methodId) { @@ -89,15 +85,12 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat 'Unable to initialize payment because "options.methodId" argument is not provided.', ); } - - const state = await this._store.dispatch( - this._paymentMethodActionCreator.loadPaymentMethod(methodId), - ); + const state = this.paymentIntegrationService.getState(); // Info: does not use getStoreConfigOrThrow, because storeConfig is not available if // cart is empty, so it causes issues on Product Details Page - const storeConfig = state.config.getStoreConfig(); - const paymentMethod = state.paymentMethods.getPaymentMethodOrThrow(methodId); - const { clientToken, initializationData } = paymentMethod; + const storeConfig = state.getStoreConfig(); + const paymentMethod = state.getPaymentMethodOrThrow(methodId); + const { clientToken, initializationData }: PaymentMethod = paymentMethod; const { paymentButtonStyles } = initializationData; const { cartButtonStyles } = paymentButtonStyles || {}; const styles = braintreevenmo?.style || cartButtonStyles; @@ -112,38 +105,37 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat ); } - this._onError = braintreevenmo?.onError || this._handleError; - - this._braintreeSDKCreator.initialize(clientToken, storeConfig); - await this._braintreeSDKCreator.getVenmoCheckout( + this.onError = braintreevenmo?.onError || this.handleError; + this.braintreeIntegrationService.initialize(clientToken, storeConfig); + await this.braintreeIntegrationService.getVenmoCheckout( (braintreeVenmoCheckout) => - this._handleInitializationVenmoSuccess( + this.handleInitializationVenmoSuccess( braintreeVenmoCheckout, containerId, braintreevenmo?.buyNowInitializeOptions, styles, ), - (error) => this._handleInitializationVenmoError(error, containerId), + (error) => this.handleInitializationVenmoError(error, containerId), ); } - deinitialize(): Promise { - this._braintreeSDKCreator.teardown(); + async deinitialize(): Promise { + await this.braintreeIntegrationService.teardown(); return Promise.resolve(); } - private _handleError(error: BraintreeError) { + private handleError(error: BraintreeError) { throw new Error(error.message); } - private _handleInitializationVenmoSuccess( + private handleInitializationVenmoSuccess( braintreeVenmoCheckout: BraintreeVenmoCheckout, parentContainerId: string, buyNowInitializeOptions?: BuyNowInitializeOptions, buttonsStyles?: PaypalStyleOptions, ): void { - return this._renderVenmoButton( + return this.renderVenmoButton( braintreeVenmoCheckout, parentContainerId, buyNowInitializeOptions, @@ -151,16 +143,40 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat ); } - private _handleInitializationVenmoError( + private async createBuyNowCart( + buyNowInitializeOptions?: BuyNowInitializeOptions, + ): Promise { + if (typeof buyNowInitializeOptions?.getBuyNowCartRequestBody === 'function') { + const cartRequestBody = buyNowInitializeOptions?.getBuyNowCartRequestBody(); + + if (!cartRequestBody) { + throw new MissingDataError(MissingDataErrorType.MissingCart); + } + + try { + const buyNowCart = await this.paymentIntegrationService.createBuyNowCart( + cartRequestBody, + ); + + return buyNowCart; + } catch (error) { + throw new BuyNowCartCreationError(); + } + } + + return undefined; + } + + private handleInitializationVenmoError( error: BraintreeError | UnsupportedBrowserError, containerId: string, ): void { - this._removeVenmoContainer(containerId); + this.removeVenmoContainer(containerId); - return this._onError(error); + return this.onError(error); } - private _removeVenmoContainer(containerId: string): void { + private removeVenmoContainer(containerId: string): void { const buttonContainer = document.getElementById(containerId); if (buttonContainer) { @@ -168,7 +184,7 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat } } - private _renderVenmoButton( + private renderVenmoButton( braintreeVenmoCheckout: BraintreeVenmoCheckout, containerId: string, buyNowInitializeOptions?: BuyNowInitializeOptions, @@ -186,10 +202,11 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat venmoButton.setAttribute('aria-label', 'Venmo'); Object.assign(venmoButton.style, getVenmoButtonStyle(buttonStyles || {})); + // eslint-disable-next-line @typescript-eslint/no-misused-promises venmoButton.addEventListener('click', async () => { venmoButton.setAttribute('disabled', 'true'); - const buyBowCart = await this._createBuyNowCart(buyNowInitializeOptions); + const buyBowCart = await this.createBuyNowCart(buyNowInitializeOptions); if (braintreeVenmoCheckout.tokenize) { braintreeVenmoCheckout.tokenize( @@ -200,10 +217,10 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat venmoButton.removeAttribute('disabled'); if (error) { - return this._onError(error); + return this.onError(error); } - await this._handlePostForm(payload, buyBowCart?.id); + await this.handlePostForm(payload, buyBowCart?.id); }, ); } @@ -222,36 +239,16 @@ export default class BraintreeVenmoButtonStrategy implements CheckoutButtonStrat } } - private async _createBuyNowCart(buyNowInitializeOptions?: BuyNowInitializeOptions) { - if (typeof buyNowInitializeOptions?.getBuyNowCartRequestBody === 'function') { - const cartRequestBody = buyNowInitializeOptions.getBuyNowCartRequestBody(); - - if (!cartRequestBody) { - throw new MissingDataError(MissingDataErrorType.MissingCart); - } - - try { - const { body: buyNowCart } = await this._cartRequestSender.createBuyNowCart( - cartRequestBody, - ); - - return buyNowCart; - } catch (error) { - throw new BuyNowCartCreationError(); - } - } - } - - private async _handlePostForm( + private async handlePostForm( payload: BraintreeTokenizePayload, buyNowCartId?: string, ): Promise { - const { deviceData } = await this._braintreeSDKCreator.getDataCollector(); + const { deviceData } = await this.braintreeIntegrationService.getDataCollector(); const { nonce, details } = payload; - this._formPoster.postForm('/checkout.php', { + this.formPoster.postForm('/checkout.php', { nonce, - provider: CheckoutButtonMethodType.BRAINTREE_VENMO, + provider: 'braintreevenmo', payment_type: 'paypal', device_data: deviceData, action: 'set_external_checkout', diff --git a/packages/braintree-integration/src/braintree-venmo/braintree-venmo-initialize-options.ts b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-initialize-options.ts new file mode 100644 index 00000000000..06f33806589 --- /dev/null +++ b/packages/braintree-integration/src/braintree-venmo/braintree-venmo-initialize-options.ts @@ -0,0 +1,45 @@ +import { + BraintreeError, + BuyNowCartRequestBody, + PaypalStyleOptions, +} from '@bigcommerce/checkout-sdk/braintree-utils'; +import { StandardError } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +export interface BraintreeVenmoButtonInitializeOptions { + /** + * 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. + */ + buyNowInitializeOptions?: BuyNowInitializeOptions; + + /** + * A set of styling options for the checkout button. + */ + style?: Pick< + PaypalStyleOptions, + 'layout' | 'size' | 'color' | 'label' | 'shape' | 'tagline' | 'fundingicons' | 'height' + >; + + /** + * A callback that gets called on any error. + * + * @param error - The error object describing the failure. + */ + onError?(error: BraintreeError | StandardError): void; +} + +export interface BuyNowInitializeOptions { + getBuyNowCartRequestBody?(): BuyNowCartRequestBody | void; +} + +export interface WithBraintreeVenmoInitializeOptions { + /** + * The options that are required to facilitate Braintree Venmo. They can be + * omitted unless you need to support Braintree Venmo. + */ + braintreevenmo?: BraintreeVenmoButtonInitializeOptions; +} diff --git a/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.spec.ts b/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.spec.ts new file mode 100644 index 00000000000..aa428b77fe9 --- /dev/null +++ b/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-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 BraintreeVenmoButtonStrategy from './braintree-venmo-button-strategy'; +import createBraintreeVenmoButtonStrategy from './create-braintree-venmo-button-strategy'; + +describe('createBraintreeVenmoButtonStrategy', () => { + let paymentIntegrationService: PaymentIntegrationService; + + beforeEach(() => { + paymentIntegrationService = new PaymentIntegrationServiceMock(); + }); + + it('initializes braintree venmo button strategy', () => { + const strategy = createBraintreeVenmoButtonStrategy(paymentIntegrationService); + + expect(strategy).toBeInstanceOf(BraintreeVenmoButtonStrategy); + }); +}); diff --git a/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.ts b/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.ts new file mode 100644 index 00000000000..1f427d85311 --- /dev/null +++ b/packages/braintree-integration/src/braintree-venmo/create-braintree-venmo-button-strategy.ts @@ -0,0 +1,36 @@ +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 { Overlay } from '@bigcommerce/checkout-sdk/ui'; + +import BraintreeVenmoButtonStrategy from './braintree-venmo-button-strategy'; + +const createBraintreeVenmoButtonStrategy: CheckoutButtonStrategyFactory< + BraintreeVenmoButtonStrategy +> = (paymentIntegrationService) => { + const braintreeHostWindow: BraintreeHostWindow = window; + const overlay = new Overlay(); + + const braintreeIntegrationService = new BraintreeIntegrationService( + new BraintreeScriptLoader(getScriptLoader(), braintreeHostWindow), + braintreeHostWindow, + overlay, + ); + + return new BraintreeVenmoButtonStrategy( + paymentIntegrationService, + createFormPoster(), + braintreeIntegrationService, + ); +}; + +export default toResolvableModule(createBraintreeVenmoButtonStrategy, [{ id: 'braintreevenmo' }]); diff --git a/packages/braintree-integration/src/index.ts b/packages/braintree-integration/src/index.ts index ad66242cca7..571783a634d 100644 --- a/packages/braintree-integration/src/index.ts +++ b/packages/braintree-integration/src/index.ts @@ -35,3 +35,8 @@ export { WithBraintreeFastlanePaymentInitializeOptions } from './braintree-fastl */ export { default as createBraintreeVisaCheckoutButtonStrategy } from './braintree-visa-checkout/create-braintree-visa-checkout-button-strategy'; export { default as createBraintreeVisaCheckoutCustomerStrategy } from './braintree-visa-checkout/create-braintree-visa-checkout-customer-strategy'; + +/** + * Braintree Venmo + */ +export { default as createBraintreeVenmoButtonStrategy } from './braintree-venmo/create-braintree-venmo-button-strategy'; diff --git a/packages/braintree-utils/src/braintree-integration-service.ts b/packages/braintree-utils/src/braintree-integration-service.ts index e4c46135339..9e231bfd6d8 100644 --- a/packages/braintree-utils/src/braintree-integration-service.ts +++ b/packages/braintree-utils/src/braintree-integration-service.ts @@ -28,12 +28,14 @@ import { BraintreeThreeDSecure, BraintreeTokenizationDetails, BraintreeTokenizePayload, + BraintreeVenmoCheckout, GetLocalPaymentInstance, GooglePayBraintreeSDK, LocalPaymentInstance, PAYPAL_COMPONENTS, } from './types'; import isBraintreeError from './utils/is-braintree-error'; +import { UnsupportedBrowserError } from './index'; export interface PaypalConfig { amount: number; @@ -55,6 +57,7 @@ export default class BraintreeIntegrationService { private googlePay?: Promise; private threeDS?: Promise; private braintreePaypal?: Promise; + private braintreeVenmo?: Promise; constructor( private braintreeScriptLoader: BraintreeScriptLoader, @@ -271,6 +274,43 @@ export default class BraintreeIntegrationService { return cached; } + async getVenmoCheckout( + onSuccess: (braintreeVenmoCheckout: BraintreeVenmoCheckout) => void, + onError: (error: BraintreeError | UnsupportedBrowserError) => void, + ): Promise { + if (!this.braintreeVenmo) { + const client = await this.getClient(); + + const venmoCheckout = await this.braintreeScriptLoader.loadVenmoCheckout(); + + const venmoCheckoutConfig = { + client, + allowDesktop: true, + paymentMethodUsage: 'multi_use', + }; + + const venmoCheckoutCallback = ( + error?: BraintreeError, + braintreeVenmoCheckout?: BraintreeVenmoCheckout, + ): void => { + if (error) { + return onError(error); + } + if (braintreeVenmoCheckout) { + if (!braintreeVenmoCheckout.isBrowserSupported()) { + return onError(new UnsupportedBrowserError()); + } + + onSuccess(braintreeVenmoCheckout); + } + }; + + this.braintreeVenmo = venmoCheckout.create(venmoCheckoutConfig, venmoCheckoutCallback); + } + + return this.braintreeVenmo; + } + getGooglePaymentComponent(): Promise { if (!this.googlePay) { this.googlePay = Promise.all([ diff --git a/packages/braintree-utils/src/braintree.ts b/packages/braintree-utils/src/braintree.ts index 2d4b30ba9a8..3081753af15 100644 --- a/packages/braintree-utils/src/braintree.ts +++ b/packages/braintree-utils/src/braintree.ts @@ -546,7 +546,7 @@ export type BraintreeVenmoCheckoutCreator = BraintreeModuleCreator< >; export interface BraintreeVenmoCheckout extends BraintreeModule { - tokenize(callback: (error: BraintreeError, payload: BraintreeTokenizePayload) => void): void; + tokenize(callback: (error: BraintreeError, payload: BraintreeTokenizePayload) => unknown): void; isBrowserSupported(): boolean; } diff --git a/packages/braintree-utils/src/buy-now-cart-request-body.ts b/packages/braintree-utils/src/buy-now-cart-request-body.ts new file mode 100644 index 00000000000..46219fabe49 --- /dev/null +++ b/packages/braintree-utils/src/buy-now-cart-request-body.ts @@ -0,0 +1,19 @@ +import { CartSource } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +interface LineItem { + productId: number; + quantity: number; + variantId?: number; + optionSelections?: { + optionId: number; + optionValue: number | string; + }; +} + +/** + * An object that contains the information required for creating 'Buy now' cart. + */ +export default interface BuyNowCartRequestBody { + source: CartSource.BuyNow; + lineItems: LineItem[]; +} diff --git a/packages/braintree-utils/src/index.ts b/packages/braintree-utils/src/index.ts index 00b7363121f..1a3475a18ba 100644 --- a/packages/braintree-utils/src/index.ts +++ b/packages/braintree-utils/src/index.ts @@ -10,3 +10,8 @@ export { BRAINTREE_SDK_STABLE_VERSION, BRAINTREE_SDK_FASTLANE_COMPATIBLE_VERSION, } from './sdk-verison'; + +export { default as BuyNowCartRequestBody } from './buy-now-cart-request-body'; +export { default as UnsupportedBrowserError } from './unsupported-browser-error'; +export { default as mapToLegacyBillingAddress } from './map-to-legacy-billing-address'; +export { default as mapToLegacyShippingAddress } from './map-to-legacy-shipping-address'; diff --git a/packages/braintree-utils/src/map-to-legacy-billing-address.spec.ts b/packages/braintree-utils/src/map-to-legacy-billing-address.spec.ts new file mode 100644 index 00000000000..9029ef2aec0 --- /dev/null +++ b/packages/braintree-utils/src/map-to-legacy-billing-address.spec.ts @@ -0,0 +1,71 @@ +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/braintree-utils/src/map-to-legacy-billing-address.ts b/packages/braintree-utils/src/map-to-legacy-billing-address.ts new file mode 100644 index 00000000000..98f504c3a1a --- /dev/null +++ b/packages/braintree-utils/src/map-to-legacy-billing-address.ts @@ -0,0 +1,23 @@ +import { BraintreeDetails } from './types'; +import { LegacyAddress } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +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/braintree-utils/src/map-to-legacy-shipping-address.spec.ts b/packages/braintree-utils/src/map-to-legacy-shipping-address.spec.ts new file mode 100644 index 00000000000..3de93334a72 --- /dev/null +++ b/packages/braintree-utils/src/map-to-legacy-shipping-address.spec.ts @@ -0,0 +1,39 @@ +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/braintree-utils/src/map-to-legacy-shipping-address.ts b/packages/braintree-utils/src/map-to-legacy-shipping-address.ts new file mode 100644 index 00000000000..0fbe90b6ec8 --- /dev/null +++ b/packages/braintree-utils/src/map-to-legacy-shipping-address.ts @@ -0,0 +1,24 @@ +import { BraintreeDetails } from './index'; +import { LegacyAddress } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +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/braintree-utils/src/paypal.ts b/packages/braintree-utils/src/paypal.ts index 8b2651b6e71..eacd1a8e2aa 100644 --- a/packages/braintree-utils/src/paypal.ts +++ b/packages/braintree-utils/src/paypal.ts @@ -24,6 +24,7 @@ export enum PaypalButtonStyleColorOption { BLUE = 'blue', SIlVER = 'silver', BLACK = 'black', + WHITE = 'white', } export enum PaypalButtonStyleLabelOption { diff --git a/packages/braintree-utils/src/types.ts b/packages/braintree-utils/src/types.ts index c16951adf32..7172162aa38 100644 --- a/packages/braintree-utils/src/types.ts +++ b/packages/braintree-utils/src/types.ts @@ -48,6 +48,18 @@ export interface BraintreeClient { request(payload: BraintreeClientRequestPayload): Promise; } +export interface BraintreeDetails { + username?: string; + email?: string; + payerId?: string; + firstName?: string; + lastName?: string; + countryCode?: string; + phone?: string; + shippingAddress?: BraintreeShippingAddress; + billingAddress?: BraintreeAddress; +} + export interface BraintreeClientRequestPayload { data: { creditCard: { diff --git a/packages/braintree-utils/src/unsupported-browser-error.ts b/packages/braintree-utils/src/unsupported-browser-error.ts new file mode 100644 index 00000000000..65ddd94582e --- /dev/null +++ b/packages/braintree-utils/src/unsupported-browser-error.ts @@ -0,0 +1,14 @@ +import { StandardError } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +/** + * Throw this error if the shopper is using a browser version that is not + * supported by us or any third party provider we use. + */ +export default class UnsupportedBrowserError extends StandardError { + constructor(message?: string) { + super(message || 'Unsupported browser error'); + + this.name = 'UnsupportedBrowserError'; + this.type = 'unsupported_browser'; + } +} 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 c4912f66374..b37b0738795 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 @@ -10,7 +10,6 @@ import { AmazonPayV2ButtonStrategy } from './strategies/amazon-pay-v2'; import { BraintreePaypalButtonStrategy, BraintreePaypalCreditButtonStrategy, - BraintreeVenmoButtonStrategy, } from './strategies/braintree'; describe('createCheckoutButtonRegistry', () => { @@ -40,8 +39,4 @@ describe('createCheckoutButtonRegistry', () => { expect.any(BraintreePaypalCreditButtonStrategy), ); }); - - it('returns registry with Braintree Venmo registered', () => { - expect(registry.get('braintreevenmo')).toEqual(expect.any(BraintreeVenmoButtonStrategy)); - }); }); 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 157b0c55ec6..d6388314239 100644 --- a/packages/core/src/checkout-buttons/create-checkout-button-registry.ts +++ b/packages/core/src/checkout-buttons/create-checkout-button-registry.ts @@ -10,7 +10,6 @@ import { CheckoutActionCreator, CheckoutRequestSender, CheckoutStore } from '../ import { Registry } from '../common/registry'; import { ConfigActionCreator, ConfigRequestSender } from '../config'; import { FormFieldsActionCreator, FormFieldsRequestSender } from '../form'; -import { PaymentMethodActionCreator, PaymentMethodRequestSender } from '../payment'; import { BraintreeSDKCreator } from '../payment/strategies/braintree'; import { MasterpassScriptLoader } from '../payment/strategies/masterpass'; import { PaypalScriptLoader } from '../payment/strategies/paypal'; @@ -21,7 +20,6 @@ import AmazonPayV2RequestSender from './strategies/amazon-pay-v2/amazon-pay-v2-r import { BraintreePaypalButtonStrategy, BraintreePaypalCreditButtonStrategy, - BraintreeVenmoButtonStrategy, } from './strategies/braintree'; import { MasterpassButtonStrategy } from './strategies/masterpass'; import { PaypalButtonStrategy } from './strategies/paypal'; @@ -41,9 +39,6 @@ export default function createCheckoutButtonRegistry( new ConfigActionCreator(new ConfigRequestSender(requestSender)), new FormFieldsActionCreator(new FormFieldsRequestSender(requestSender)), ); - const paymentMethodActionCreator = new PaymentMethodActionCreator( - new PaymentMethodRequestSender(requestSender), - ); const braintreeSdkCreator = new BraintreeSDKCreator( new BraintreeScriptLoader(scriptLoader, window), @@ -89,18 +84,6 @@ export default function createCheckoutButtonRegistry( ), ); - registry.register( - CheckoutButtonMethodType.BRAINTREE_VENMO, - () => - new BraintreeVenmoButtonStrategy( - store, - paymentMethodActionCreator, - cartRequestSender, - braintreeSdkCreator, - formPoster, - ), - ); - registry.register( CheckoutButtonMethodType.MASTERPASS, () => diff --git a/packages/core/src/checkout-buttons/strategies/braintree/index.ts b/packages/core/src/checkout-buttons/strategies/braintree/index.ts index 50f88b95335..5543743cfa1 100644 --- a/packages/core/src/checkout-buttons/strategies/braintree/index.ts +++ b/packages/core/src/checkout-buttons/strategies/braintree/index.ts @@ -7,5 +7,4 @@ export { default as BraintreePaypalCreditButtonStrategy } from './braintree-payp export { BraintreePaypalCreditButtonInitializeOptions } from './braintree-paypal-credit-button-options'; // Braintree Venmo -export { default as BraintreeVenmoButtonStrategy } from './braintree-venmo-button-strategy'; export { BraintreeVenmoButtonInitializeOptions } from './braintree-venmo-button-options';