From 33970f05d41175cb163a1a18c8c5129d4e732036 Mon Sep 17 00:00:00 2001 From: Mykhailo Pavlenko Date: Fri, 7 Jul 2023 10:46:43 +0300 Subject: [PATCH] fix(payment): PAYPAL-2502 bump braintree sdk version --- .../src/braintree-script-loader.spec.ts | 2 +- .../src/braintree-script-loader.ts | 2 +- ...ntree-credit-card-payment-strategy.spec.ts | 8 ++- .../braintree-credit-card-payment-strategy.ts | 15 +++++- .../braintree/braintree-hosted-form.ts | 20 ++++--- .../braintree-payment-processor.spec.ts | 52 ++++++++++++++++--- .../braintree/braintree-payment-processor.ts | 28 ++++++---- .../braintree/braintree-script-loader.spec.ts | 2 +- .../braintree/braintree-script-loader.ts | 2 +- .../strategies/braintree/braintree.mock.ts | 27 +++++++++- .../payment/strategies/braintree/braintree.ts | 8 ++- 11 files changed, 134 insertions(+), 32 deletions(-) diff --git a/packages/braintree-integration/src/braintree-script-loader.spec.ts b/packages/braintree-integration/src/braintree-script-loader.spec.ts index 914de1e699..de7b30696d 100644 --- a/packages/braintree-integration/src/braintree-script-loader.spec.ts +++ b/packages/braintree-integration/src/braintree-script-loader.spec.ts @@ -19,7 +19,7 @@ import { getPaypalCheckoutMock, } from './braintree.mock'; -const VERSION = '3.81.0'; +const VERSION = '3.95.0'; describe('BraintreeScriptLoader', () => { let scriptLoader: ScriptLoader; diff --git a/packages/braintree-integration/src/braintree-script-loader.ts b/packages/braintree-integration/src/braintree-script-loader.ts index 3e36daeb51..4dee4bcb38 100644 --- a/packages/braintree-integration/src/braintree-script-loader.ts +++ b/packages/braintree-integration/src/braintree-script-loader.ts @@ -10,7 +10,7 @@ import { BraintreePaypalCheckoutCreator, } from './braintree'; -const VERSION = '3.81.0'; +const VERSION = '3.95.0'; export default class BraintreeScriptLoader { constructor( diff --git a/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.spec.ts b/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.spec.ts index e80997864e..5faf264f41 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.spec.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.spec.ts @@ -40,6 +40,7 @@ import PaymentRequestTransformer from '../../payment-request-transformer'; import BraintreeCreditCardPaymentStrategy from './braintree-credit-card-payment-strategy'; import { BraintreePaymentInitializeOptions } from './braintree-payment-options'; import BraintreePaymentProcessor from './braintree-payment-processor'; +import { getTokenizeResponseBody } from './braintree.mock'; describe('BraintreeCreditCardPaymentStrategy', () => { let order: Order; @@ -587,6 +588,11 @@ describe('BraintreeCreditCardPaymentStrategy', () => { }, }; + jest.spyOn( + store.getState().instruments, + 'getCardInstrumentOrThrow', + ).mockImplementation(jest.fn(() => getTokenizeResponseBody())); + await braintreeCreditCardPaymentStrategy.initialize({ methodId: paymentMethodMock.id, }); @@ -594,7 +600,7 @@ describe('BraintreeCreditCardPaymentStrategy', () => { expect(paymentActionCreator.submitPayment).toHaveBeenCalledTimes(2); expect(braintreePaymentProcessorMock.challenge3DSVerification).toHaveBeenCalledWith( - threeDSecureNonce, + { nonce: threeDSecureNonce }, getOrder().orderAmount, ); }); diff --git a/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.ts b/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.ts index d2106586a8..aa7424c8f2 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-credit-card-payment-strategy.ts @@ -216,9 +216,22 @@ export default class BraintreeCreditCardPaymentStrategy implements PaymentStrate } try { + const { + instruments: { getCardInstrumentOrThrow }, + } = this._store.getState(); const { payer_auth_request: storedCreditCardNonce } = error.body.three_ds_result || {}; + const { paymentData } = payment; + + if (!paymentData || !isVaultedInstrument(paymentData)) { + throw new PaymentArgumentInvalidError(['instrumentId']); + } + + const instrument = getCardInstrumentOrThrow(paymentData.instrumentId); const { nonce } = await this._braintreePaymentProcessor.challenge3DSVerification( - storedCreditCardNonce, + { + nonce: storedCreditCardNonce, + bin: instrument.iin, + }, orderAmount, ); diff --git a/packages/core/src/payment/strategies/braintree/braintree-hosted-form.ts b/packages/core/src/payment/strategies/braintree/braintree-hosted-form.ts index d6869f5396..7b41e8112a 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-hosted-form.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-hosted-form.ts @@ -3,7 +3,6 @@ import { Dictionary, isEmpty, isNil, omitBy } from 'lodash'; import { Address } from '../../../address'; import { NotInitializedError, NotInitializedErrorType } from '../../../common/error/errors'; import { PaymentInvalidFormError, PaymentInvalidFormErrorDetails } from '../../errors'; -import { NonceInstrument } from '../../payment'; import { BraintreeBillingAddressRequestData, @@ -13,6 +12,7 @@ import { BraintreeHostedFieldsCreatorConfig, BraintreeHostedFieldsState, BraintreeHostedFormError, + TokenizationPayload, } from './braintree'; import { BraintreeFormFieldsMap, @@ -92,13 +92,13 @@ export default class BraintreeHostedForm { this._cardNameField?.detach(); } - async tokenize(billingAddress: Address): Promise { + async tokenize(billingAddress: Address): Promise { if (!this._cardFields) { throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); } try { - const { nonce } = await this._cardFields.tokenize( + const tokenizationPayload = await this._cardFields.tokenize( omitBy( { billingAddress: billingAddress && this._mapBillingAddress(billingAddress), @@ -113,7 +113,10 @@ export default class BraintreeHostedForm { errors: {}, }); - return { nonce }; + return { + nonce: tokenizationPayload.nonce, + bin: tokenizationPayload.details?.bin, + }; } catch (error) { const errors = this._mapTokenizeError(error); @@ -130,13 +133,13 @@ export default class BraintreeHostedForm { } } - async tokenizeForStoredCardVerification(): Promise { + async tokenizeForStoredCardVerification(): Promise { if (!this._cardFields) { throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); } try { - const { nonce } = await this._cardFields.tokenize( + const tokenizationPayload = await this._cardFields.tokenize( omitBy( { cardholderName: this._cardNameField?.getValue(), @@ -150,7 +153,10 @@ export default class BraintreeHostedForm { errors: {}, }); - return { nonce }; + return { + nonce: tokenizationPayload.nonce, + bin: tokenizationPayload.details?.bin, + }; } catch (error) { const errors = this._mapTokenizeError(error, true); diff --git a/packages/core/src/payment/strategies/braintree/braintree-payment-processor.spec.ts b/packages/core/src/payment/strategies/braintree/braintree-payment-processor.spec.ts index e583b2486c..23d3e1a0c1 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-payment-processor.spec.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-payment-processor.spec.ts @@ -119,7 +119,10 @@ describe('BraintreePaymentProcessor', () => { getBillingAddress(), ); - expect(tokenizedCard).toEqual({ nonce: 'demo_nonce' }); + expect(tokenizedCard).toEqual({ + nonce: 'demo_nonce', + bin: 'demo_bin', + }); }); it('calls the braintree client request with the correct information', async () => { @@ -548,17 +551,28 @@ describe('BraintreePaymentProcessor', () => { braintreePaymentProcessor.initialize('clientToken', {}); return expect( - braintreePaymentProcessor.challenge3DSVerification('tokenization_nonce', 122), + braintreePaymentProcessor.challenge3DSVerification( + { + nonce: 'tokenization_nonce', + bin: '123456', + }, + 122, + ), ).rejects.toThrow(NotInitializedError); }); it('challenges 3DS verifies the card using 3DS', async () => { jest.spyOn(threeDSecureMock, 'verifyCard').mockReturnValue( - Promise.resolve({ nonce: 'three_ds_nonce' }), + Promise.resolve({ + nonce: 'three_ds_nonce', + }), ); const verifiedCard = await braintreePaymentProcessor.challenge3DSVerification( - 'tokenization_nonce', + { + nonce: 'tokenization_nonce', + bin: '123456', + }, 122, ); @@ -566,13 +580,20 @@ describe('BraintreePaymentProcessor', () => { }); it('calls the verification service with the right values', async () => { - await braintreePaymentProcessor.challenge3DSVerification('tokenization_nonce', 122); + await braintreePaymentProcessor.challenge3DSVerification( + { + nonce: 'tokenization_nonce', + bin: '123456', + }, + 122, + ); expect(threeDSecureMock.verifyCard).toHaveBeenCalledWith({ addFrame: expect.any(Function), removeFrame: expect.any(Function), challengeRequested: true, amount: 122, + bin: '123456', nonce: 'tokenization_nonce', onLookupComplete: expect.any(Function), }); @@ -593,7 +614,13 @@ describe('BraintreePaymentProcessor', () => { it('cancels card verification', async () => { braintreePaymentProcessor - .challenge3DSVerification('tokenization_nonce', 122) + .challenge3DSVerification( + { + nonce: 'tokenization_nonce', + bin: '123456', + }, + 122, + ) .catch(noop); await new Promise((resolve) => process.nextTick(resolve)); @@ -604,7 +631,10 @@ describe('BraintreePaymentProcessor', () => { it('rejects the return promise', async () => { const promise = braintreePaymentProcessor.challenge3DSVerification( - 'tokenization_nonce', + { + nonce: 'tokenization_nonce', + bin: '123456', + }, 122, ); @@ -615,7 +645,13 @@ describe('BraintreePaymentProcessor', () => { }); it('resolves with verify payload', async () => { - braintreePaymentProcessor.challenge3DSVerification('tokenization_nonce', 122); + braintreePaymentProcessor.challenge3DSVerification( + { + nonce: 'tokenization_nonce', + bin: '123456', + }, + 122, + ); await new Promise((resolve) => process.nextTick(resolve)); diff --git a/packages/core/src/payment/strategies/braintree/braintree-payment-processor.ts b/packages/core/src/payment/strategies/braintree/braintree-payment-processor.ts index cf1862075e..5e7f913760 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-payment-processor.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-payment-processor.ts @@ -23,6 +23,7 @@ import { BraintreeTokenizePayload, BraintreeVenmoCheckout, BraintreeVerifyPayload, + TokenizationPayload, } from './braintree'; import BraintreeHostedForm from './braintree-hosted-form'; import { @@ -68,7 +69,7 @@ export default class BraintreePaymentProcessor { async tokenizeCard( payment: OrderPaymentRequestBody, billingAddress: Address, - ): Promise { + ): Promise { const { paymentData } = payment; if (!isCreditCardInstrumentLike(paymentData)) { @@ -85,7 +86,10 @@ export default class BraintreePaymentProcessor { const client = await this._braintreeSDKCreator.getClient(); const { creditCards } = await client.request(requestData); - return { nonce: creditCards[0].nonce }; + return { + nonce: creditCards[0].nonce, + bin: creditCards[0].details?.bin, + }; } async verifyCard( @@ -93,9 +97,9 @@ export default class BraintreePaymentProcessor { billingAddress: Address, amount: number, ): Promise { - const { nonce } = await this.tokenizeCard(payment, billingAddress); + const tokenizationPayload = await this.tokenizeCard(payment, billingAddress); - return this.challenge3DSVerification(nonce, amount); + return this.challenge3DSVerification(tokenizationPayload, amount); } paypal({ shouldSaveInstrument, ...config }: PaypalConfig): Promise { @@ -171,15 +175,18 @@ export default class BraintreePaymentProcessor { billingAddress: Address, amount: number, ): Promise { - const { nonce } = await this._braintreeHostedForm.tokenize(billingAddress); + const tokenizationPayload = await this._braintreeHostedForm.tokenize(billingAddress); - return this.challenge3DSVerification(nonce, amount); + return this.challenge3DSVerification(tokenizationPayload, amount); } - async challenge3DSVerification(nonce: string, amount: number): Promise { + async challenge3DSVerification( + tokenizationPayload: TokenizationPayload, + amount: number, + ): Promise { const threeDSecure = await this._braintreeSDKCreator.get3DS(); - return this._present3DSChallenge(threeDSecure, amount, nonce); + return this._present3DSChallenge(threeDSecure, amount, tokenizationPayload); } async getVenmoCheckout(): Promise { @@ -218,8 +225,10 @@ export default class BraintreePaymentProcessor { private _present3DSChallenge( threeDSecure: BraintreeThreeDSecure, amount: number, - nonce: string, + tokenizationPayload: TokenizationPayload, ): Promise { + const { nonce, bin } = tokenizationPayload; + if (!this._threeDSecureOptions || !nonce) { throw new NotInitializedError(NotInitializedErrorType.PaymentNotInitialized); } @@ -241,6 +250,7 @@ export default class BraintreePaymentProcessor { addFrame(error, iframe, cancelVerifyCard); }, amount: Number(roundedAmount), + bin, challengeRequested: true, nonce, removeFrame, diff --git a/packages/core/src/payment/strategies/braintree/braintree-script-loader.spec.ts b/packages/core/src/payment/strategies/braintree/braintree-script-loader.spec.ts index 0348ed9242..34a6ce3643 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-script-loader.spec.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-script-loader.spec.ts @@ -23,7 +23,7 @@ import { getVisaCheckoutMock, } from './braintree.mock'; -const version = '3.81.0'; +const version = '3.95.0'; describe('BraintreeScriptLoader', () => { let braintreeScriptLoader: BraintreeScriptLoader; diff --git a/packages/core/src/payment/strategies/braintree/braintree-script-loader.ts b/packages/core/src/payment/strategies/braintree/braintree-script-loader.ts index 6fc3fa4124..97466274df 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-script-loader.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-script-loader.ts @@ -15,7 +15,7 @@ import { BraintreeVisaCheckoutCreator, } from './braintree'; -const version = '3.81.0'; +const version = '3.95.0'; export default class BraintreeScriptLoader { constructor( diff --git a/packages/core/src/payment/strategies/braintree/braintree.mock.ts b/packages/core/src/payment/strategies/braintree/braintree.mock.ts index 8e839dbe23..7102ce5606 100644 --- a/packages/core/src/payment/strategies/braintree/braintree.mock.ts +++ b/packages/core/src/payment/strategies/braintree/braintree.mock.ts @@ -109,7 +109,32 @@ export function getModuleCreatorNewMock( export function getTokenizeResponseBody(): BraintreeTokenizeResponse { return { - creditCards: [{ nonce: 'demo_nonce' }], + creditCards: [ + { + nonce: 'demo_nonce', + details: { + bin: 'demo_bin', + cardType: 'Visa', + expirationMonth: '01', + expirationYear: '2025', + lastFour: '0001', + lastTwo: '01', + }, + description: 'ending in 01', + type: 'CreditCard', + binData: { + commercial: 'bin_data_commercial', + countryOfIssuance: 'bin_data_country_of_issuance', + debit: 'bin_data_debit', + durbinRegulated: 'bin_data_durbin_regulated', + healthcare: 'bin_data_healthcare', + issuingBank: 'bin_data_issuing_bank', + payroll: 'bin_data_payroll', + prepaid: 'bin_data_prepaid', + productId: 'bin_data_product_id', + }, + }, + ], }; } diff --git a/packages/core/src/payment/strategies/braintree/braintree.ts b/packages/core/src/payment/strategies/braintree/braintree.ts index 6966405bdb..742e8b4603 100644 --- a/packages/core/src/payment/strategies/braintree/braintree.ts +++ b/packages/core/src/payment/strategies/braintree/braintree.ts @@ -187,7 +187,7 @@ export interface BraintreeRequestData { } export interface BraintreeTokenizeResponse { - creditCards: Array<{ nonce: string }>; + creditCards: BraintreeHostedFieldsTokenizePayload[]; } /** @@ -359,6 +359,11 @@ export interface BraintreeHostedFieldsTokenizePayload { }; } +export interface TokenizationPayload { + nonce: string; + bin: string; +} + export interface BraintreeBillingAddressRequestData { postalCode?: string; firstName?: string; @@ -402,6 +407,7 @@ export interface BraintreeThreeDSecureCreatorConfig extends BraintreeModuleCreat export interface BraintreeThreeDSecureOptions { nonce: string; amount: number; + bin?: string; challengeRequested: boolean; showLoader?: boolean; addFrame(error: Error | undefined, iframe: HTMLIFrameElement): void;