Skip to content

Commit

Permalink
Merge pull request #3836 from 3scale/braintree-3ds-improvements
Browse files Browse the repository at this point in the history
THREESCALE-11131: Braintree 3DS improvements
  • Loading branch information
mayorova authored Jul 23, 2024
2 parents 2f7feba + b26f786 commit 95540ea
Show file tree
Hide file tree
Showing 14 changed files with 88 additions and 85 deletions.
7 changes: 6 additions & 1 deletion app/helpers/payment_details_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def braintree_form_data
threeDSecureEnabled: site_account.payment_gateway_options[:three_ds_enabled],
clientToken: braintree_authorization,
countriesList: merchant_countries,
billingAddress: billing_address_data
billingAddress: billing_address_data,
ipAddress: ip_address
}
end

Expand All @@ -98,4 +99,8 @@ def country_code_for(country_name)

merchant_countries.find { |country| country[0] == country_name }&.dig(1)
end

def ip_address
request&.remote_ip
end
end
5 changes: 3 additions & 2 deletions app/javascript/packs/braintree_customer_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error('Braintree data was not provided')
}

const { billingAddress, clientToken, countriesList, formActionPath, threeDSecureEnabled } = data
const { billingAddress, clientToken, countriesList, formActionPath, threeDSecureEnabled, ipAddress } = data

BraintreeFormWrapper({
billingAddress,
clientToken,
countriesList,
formActionPath,
threeDSecureEnabled
threeDSecureEnabled,
ipAddress
}, CONTAINER_ID)
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const BraintreeForm: FunctionComponent<Props> = ({
clientToken,
countriesList,
formActionPath,
ipAddress,
threeDSecureEnabled = false
}) => {
const [billingAddress, setBillingAddress] = useState<BillingAddress>(defaultBillingAddress)
Expand Down Expand Up @@ -45,7 +46,7 @@ const BraintreeForm: FunctionComponent<Props> = ({
setSubmitting(true)
setSubmitError(undefined)

hostedFields.getNonce(billingAddress)
hostedFields.getNonce({ billingAddress, ipAddress })
.then(nonce => {
const input = form.elements.namedItem('braintree[nonce]') as HTMLInputElement
input.value = nonce
Expand Down
1 change: 1 addition & 0 deletions app/javascript/src/PaymentGateways/braintree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface BraintreeFormDataset {
countriesList: [string, string][];
formActionPath: string;
threeDSecureEnabled: boolean;
ipAddress?: string;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { client, hostedFields } from 'braintree-web'

import type { HostedFields } from 'braintree-web'
import type { HostedFieldFieldOptions } from 'braintree-web/modules/hosted-fields'
import type { HostedFieldFieldOptions } from 'braintree-web/hosted-fields'

/* eslint-disable @typescript-eslint/naming-convention */
const styles = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import { useEffect, useState } from 'react'

import { createHostedFields } from 'PaymentGateways/braintree/utils/createHostedFields'
import { createThreeDSecure } from 'PaymentGateways/braintree/utils/createThreeDSecure'
import { verifyCard } from 'PaymentGateways/braintree/utils/verifyCard'

import type { BraintreeError } from 'braintree-web'
import type { BillingAddress } from 'PaymentGateways/braintree/types'
import type { HostedFields, HostedFieldsFieldDataFields } from 'braintree-web/modules/hosted-fields'
import type { HostedFields, HostedFieldsFieldDataFields } from 'braintree-web/hosted-fields'

interface GetNonceParams {
billingAddress: BillingAddress;
ipAddress: string | undefined;
}

type CustomHostedFields = HostedFields & {
getNonce: (BillingAddress: BillingAddress) => Promise<string>;
getNonce: (getNonceParams: GetNonceParams) => Promise<string>;
}

const CC_ERROR_MESSAGE = 'An error occurred, please review your CC details or try later.'
Expand Down Expand Up @@ -64,17 +68,18 @@ const useBraintreeHostedFields = (

const customHostedFields = {
...hostedFieldsInstance,
getNonce: async (billingAddress: BillingAddress): Promise<string> => {
const hostedFieldsTokenizePayload = await hostedFieldsInstance.tokenize()
getNonce: async ({ billingAddress, ipAddress }: GetNonceParams): Promise<string> => {
const { firstName, lastName } = billingAddress
const cardholderName = [firstName, lastName].join(' ')
const hostedFieldsTokenizePayload = await hostedFieldsInstance.tokenize({ cardholderName })

if (!threeDSecureEnabled) {
return hostedFieldsTokenizePayload.nonce
}

const threeDSecureVerifyPayload = await verifyCard(threeDSecureInstance, {
const threeDSecureVerifyPayload = await threeDSecureInstance.verifyCard({
nonce: hostedFieldsTokenizePayload.nonce,
bin: hostedFieldsTokenizePayload.details.bin,
// @ts-expect-error Outdated types. {amount} is a string: https://braintree.github.io/braintree-web/current/ThreeDSecure.html#verifyCard
amount: '0.00',
billingAddress: {
givenName: billingAddress.firstName,
Expand All @@ -86,7 +91,11 @@ const useBraintreeHostedFields = (
postalCode: billingAddress.zip,
countryCodeAlpha2: billingAddress.countryCode
},
challengeRequested: true
collectDeviceData: true,
challengeRequested: true,
additionalInformation: {
ipAddress
}
}).catch((verifyCardError: BraintreeError) => {
console.error({ verifyCardError })
throw { message: CC_ERROR_MESSAGE }
Expand Down
19 changes: 0 additions & 19 deletions app/javascript/src/PaymentGateways/braintree/utils/verifyCard.ts

This file was deleted.

3 changes: 0 additions & 3 deletions app/javascript/src/Types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,4 @@ declare module '*.yaml' {
export default data
}

// We don't care about missing types from this package. Used only in spec/javascripts/PaymentGateways/braintree/BraintreeForm.spec.tsx
declare module 'braintree-web/hosted-fields';

declare let __webpack_public_path__: string
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@rails/webpacker": "5.4.4",
"@stripe/react-stripe-js": "1.16.5",
"@stripe/stripe-js": "1.50.0",
"@types/braintree-web": "3.75.22",
"@types/braintree-web": "3.96.11",
"@types/c3": "^0.7.8",
"@types/codemirror": "^5.60.5",
"@types/enzyme": "^3.10.13",
Expand All @@ -74,7 +74,7 @@
"@types/virtual-dom": "^2.1.1",
"babel-loader": "8.2.5",
"bootstrap-sass": "3.4.3",
"braintree-web": "3.90.0",
"braintree-web": "3.102.0",
"c3": "^0.4.11",
"classnames": "^2.2.6",
"codemirror": "5.65.9",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const defaultProps: Props = {
},
threeDSecureEnabled: false,
formActionPath: '',
countriesList: COUNTRIES_LIST
countriesList: COUNTRIES_LIST,
ipAddress: '123.123.123.123'
}

const mountWrapper = (props: Partial<Props> = {}) => mount(<BraintreeForm {...{ ...defaultProps, ...props }} />)
Expand Down Expand Up @@ -202,7 +203,7 @@ describe('after hosted fields instantiated', () => {

wrapper.find('button[type="submit"]').simulate('submit')

expect(getNonce).toHaveBeenCalledWith(expect.objectContaining({ ...billingAddress, countryCode: 'ES' }))
expect(getNonce).toHaveBeenCalledWith(expect.objectContaining({ billingAddress: { ...billingAddress, country: 'Spain', countryCode: 'ES' }, ipAddress: '123.123.123.123' }))

await waitForPromises(wrapper)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as createHostedFields from 'PaymentGateways/braintree/utils/createHoste

import type { BillingAddress } from 'PaymentGateways/braintree/types'
import type { CustomHostedFields } from 'PaymentGateways/braintree/utils/useBraintreeHostedFields'
import type { HostedFields } from 'braintree-web/modules/hosted-fields'
import type { HostedFields } from 'braintree-web/hosted-fields'
import type { FunctionComponent } from 'react'

interface Props {
Expand Down Expand Up @@ -94,7 +94,19 @@ describe('when hosted fields are created', () => {

await waitForPromises()

const nonce = await hostedFields!.getNonce({} as BillingAddress)
const nonce = await hostedFields!.getNonce({ billingAddress: {} as BillingAddress, ipAddress: '' })
expect(nonce).toEqual('This is non-cense')
})

it('should pass cardholder name to tokenize', async () => {
let hostedFields: CustomHostedFields | undefined

mountWrapper({ setHostedFields: (hf) => { hostedFields = hf }, threeDSecureEnabled: false })

await waitForPromises()

const nonce = await hostedFields!.getNonce({ billingAddress: { firstName: 'First', lastName: 'Last' } as BillingAddress, ipAddress: '' })
expect(hostedFields!.tokenize).toHaveBeenCalledWith({ cardholderName: 'First Last' })
expect(nonce).toEqual('This is non-cense')
})

Expand All @@ -105,7 +117,7 @@ describe('when hosted fields are created', () => {

await waitForPromises()

const nonce = await hostedFields!.getNonce({} as BillingAddress)
const nonce = await hostedFields!.getNonce({ billingAddress: {} as BillingAddress, ipAddress: '' })
expect(nonce).toEqual('This is a 3DS verified transaction')
})
})
Expand Down
21 changes: 9 additions & 12 deletions spec/javascripts/__mocks__/braintree-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,25 @@ module.exports = {
if (event === 'validityChange') { cb() }
if (event === 'lookup-complete') { /* do nothing? */ }
},
tokenize: () => Promise.resolve({
tokenize: jest.fn(() => Promise.resolve({
details: {
bin: 'bin'
},
nonce: 'This is non-cense'
})
}))
})
},
threeDSecure: {
create: () => Promise.resolve({
on: jest.fn(),
verifyCard: (_options, cb) => {
const payload = {
nonce: 'This is a 3DS verified transaction',
threeDSecureInfo: {
liabilityShifted: true,
liabilityShiftPossible: true,
status: 'authenticate_successful'
}
verifyCard: jest.fn((_options) => Promise.resolve({
nonce: 'This is a 3DS verified transaction',
threeDSecureInfo: {
liabilityShifted: true,
liabilityShiftPossible: true,
status: 'authenticate_successful'
}
cb(undefined, payload)
}
}))
})
},
}
8 changes: 6 additions & 2 deletions test/helpers/payment_details_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,15 @@ class PaymentDetailsHelperTest < DeveloperPortal::ActionView::TestCase
stubs(:merchant_countries).returns([])
stubs(:braintree_authorization).returns('token')
stubs(:billing_address).returns({})
stubs(:request).returns(ActionDispatch::Request.new('action_dispatch.remote_ip' => '123.123.123.123'))

expected = {
formActionPath: '/admin/account/braintree_blue/hosted_success',
threeDSecureEnabled: true,
clientToken: 'token',
countriesList: [],
billingAddress: empty_billing_address_data
billingAddress: empty_billing_address_data,
ipAddress: '123.123.123.123'
}

assert_equal expected, braintree_form_data
Expand All @@ -128,6 +130,7 @@ class PaymentDetailsHelperTest < DeveloperPortal::ActionView::TestCase
stubs(:merchant_countries).returns([])
stubs(:braintree_authorization).returns('token')
stubs(:has_billing_address?).returns(true)
stubs(:request).returns(ActionDispatch::Request.new('action_dispatch.remote_ip' => '123.123.123.123'))

account = FactoryBot.build(:account)
stubs(:current_account).returns(account)
Expand All @@ -137,7 +140,8 @@ class PaymentDetailsHelperTest < DeveloperPortal::ActionView::TestCase
threeDSecureEnabled: true,
clientToken: 'token',
countriesList: [],
billingAddress: billing_address_data
billingAddress: billing_address_data,
ipAddress: '123.123.123.123'
}

assert_equal expected, braintree_form_data
Expand Down
Loading

0 comments on commit 95540ea

Please sign in to comment.