Skip to content

Commit

Permalink
- use braintree-web from node_modules instead of cdn in edit payment …
Browse files Browse the repository at this point in the history
…details

- refactor and improve braintree.js wrapper
  • Loading branch information
josemigallas committed Nov 15, 2022
1 parent 77c2fce commit cd50a02
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 262 deletions.
55 changes: 32 additions & 23 deletions app/javascript/packs/braintree_customer_form.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
import { client } from 'braintree-web'

import { createBraintreeClient } from 'PaymentGateways/braintree/braintree'
import { BraintreeFormWrapper } from 'PaymentGateways/braintree/BraintreeForm'
import { getClient } from 'PaymentGateways/braintree/braintree'
import { safeFromJsonString } from 'utilities/json-utils'

import type { Client } from 'braintree-web'
import type { BraintreeError } from 'braintree-web'
import type { BillingAddressData } from 'PaymentGateways/braintree/types'

const CONTAINER_ID = 'braintree-form-wrapper'

// eslint-disable-next-line @typescript-eslint/no-misused-promises
document.addEventListener('DOMContentLoaded', async () => {
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById(CONTAINER_ID)

if (!container) {
throw new Error('The target ID was not found: ' + CONTAINER_ID)
}

const { clientToken, threeDSecureEnabled, formActionPath, countriesList, selectedCountryCode } = container.dataset as Record<string, string>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- FIXME: too much assumption here
const billingAddress = safeFromJsonString<BillingAddressData>(container.dataset.billingAddress)!
for (const key in billingAddress) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- FIXME
if (billingAddress[key as keyof BillingAddressData] === null) {
billingAddress[key as keyof BillingAddressData] = ''
}
const { clientToken, threeDSecureEnabled, formActionPath, countriesList, selectedCountryCode, billingAddress } = container.dataset as Record<string, string>

const billingAddressData: BillingAddressData = {
address: '',
address1: '',
address2: '',
city: '',
company: '',
country: '',
// eslint-disable-next-line @typescript-eslint/naming-convention
phone_number: '',
state: '',
zip: '',
...safeFromJsonString<Partial<BillingAddressData>>(billingAddress)
}
const braintreeClient = await createBraintreeClient(client, clientToken) as Client

BraintreeFormWrapper({
braintreeClient,
formActionPath: formActionPath,
countriesList: countriesList,
selectedCountryCode: selectedCountryCode,
billingAddress,
threeDSecureEnabled: threeDSecureEnabled === 'true'
}, CONTAINER_ID)
void getClient(clientToken)
.then(braintreeClient => {
BraintreeFormWrapper({
braintreeClient,
formActionPath: formActionPath,
countriesList: countriesList,
selectedCountryCode: selectedCountryCode,
billingAddress: billingAddressData,
threeDSecureEnabled: safeFromJsonString<boolean>(threeDSecureEnabled)
}, CONTAINER_ID)
})
.catch((e: BraintreeError) => {
console.error('Something went wrong with Braintree', e.message)
})
})
40 changes: 40 additions & 0 deletions app/javascript/packs/braintree_edit_form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// TODO: we can probably re-use BraintreeForm here

import { error } from 'utilities/flash'
import { getClient, getHostedFields } from 'PaymentGateways/braintree/braintree'

import type { BraintreeError } from 'braintree-web'

document.addEventListener('DOMContentLoaded', () => {
const submit = document.querySelector<HTMLButtonElement>('button[type="submit"]')
const data = document.querySelector<HTMLSpanElement>('#braintree_data')
const form = document.querySelector<HTMLFormElement>('form#new_customer')

if (!(submit && data && form)) {
throw new Error('Required Element not found')
}

const authorization = data.dataset.authorization ?? ''

void getClient(authorization)
.then(getHostedFields)
.then(hostedFieldsInstance => {
submit.removeAttribute('disabled')

form.addEventListener('submit', (event: Event) => {
event.preventDefault()
event.stopPropagation()
hostedFieldsInstance.tokenize((tokenizeErr, payload) => {
if (tokenizeErr) {
error('Some of the credit card details are wrong. Please update them.')
} else {
document.querySelector('input#braintree_nonce')?.setAttribute('value', payload?.nonce ?? '')
form.submit()
}
})
})
})
.catch((e: BraintreeError) => {
console.error('Something is wrong with Braintree', e.message)
})
})
50 changes: 24 additions & 26 deletions app/javascript/src/PaymentGateways/braintree/BraintreeForm.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import { useEffect, useRef, useState } from 'react'
import validate from 'validate.js'
import { hostedFields, threeDSecure } from 'braintree-web'

import { CSRFToken } from 'utilities/CSRFToken'
import {
validationConstraints,
createHostedFieldsInstance,
hostedFieldOptions,
create3DSecureInstance,
veryfyCard as verifyCard
} from 'PaymentGateways/braintree/braintree'
import { getHostedFields, create3DSecureInstance, verifyCard } from 'PaymentGateways/braintree/braintree'
import { createReactWrapper } from 'utilities/createReactWrapper'
import { BraintreeBillingAddressFields } from 'PaymentGateways/braintree/BraintreeBillingAddressFields'
import { validateForm } from 'PaymentGateways/braintree/utils/formValidation'
import { BraintreeCardFields } from 'PaymentGateways/braintree/BraintreeCardFields'
import { BraintreeSubmitFields } from 'PaymentGateways/braintree/BraintreeSubmitFields'
import { BraintreeUserFields } from 'PaymentGateways/braintree/BraintreeUserFields'
import { createReactWrapper } from 'utilities/createReactWrapper'

import type { HostedFieldsFieldDataFields } from 'braintree-web/modules/hosted-fields'
import type { FunctionComponent } from 'react'
import type { Client, HostedFields, HostedFieldsTokenizePayload, ThreeDSecure, ThreeDSecureVerifyPayload } from 'braintree-web'
import type { Client, HostedFields, HostedFieldsTokenizePayload, ThreeDSecureVerifyPayload } from 'braintree-web'
import type { ThreeDSecureInfo } from 'braintree-web/modules/three-d-secure'
import type { BillingAddressData } from 'PaymentGateways/braintree/types'

Expand All @@ -28,7 +22,7 @@ const CC_ERROR_MESSAGE = 'An error occurred, please review your CC details or tr
interface Props {
braintreeClient: Client;
billingAddress: BillingAddressData;
threeDSecureEnabled: boolean;
threeDSecureEnabled?: boolean;
formActionPath: string;
countriesList: string;
selectedCountryCode: string;
Expand All @@ -37,7 +31,7 @@ interface Props {
const BraintreeForm: FunctionComponent<Props> = ({
braintreeClient,
billingAddress,
threeDSecureEnabled,
threeDSecureEnabled = false,
formActionPath,
countriesList,
selectedCountryCode
Expand All @@ -47,14 +41,22 @@ const BraintreeForm: FunctionComponent<Props> = ({
const [braintreeNonceValue, setBraintreeNonceValue] = useState<string | null>('')
const [billingAddressData, setBillingAddressData] = useState<BillingAddressData>(billingAddress)
const [isCardValid, setIsCardValid] = useState(false)
const [formErrors, setFormErrors] = useState<unknown>(validate(formRef, validationConstraints))
const [formErrors, setFormErrors] = useState<Record<string, string[]> | undefined>(validateForm(formRef))
const [cardError, setCardError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const isFormValid = isCardValid && !formErrors && !isLoading

useEffect(() => {
const getHostedFieldsInstance = async () => {
setHostedFieldsInstance(await createHostedFieldsInstance(hostedFields, braintreeClient, hostedFieldOptions, setIsCardValid, setCardError) as HostedFields)
const hostedFields = await getHostedFields(braintreeClient)
hostedFields.on('validityChange', () => {
const state = hostedFields.getState()
const cardValid = Object.keys(state.fields).every((key) => state.fields[key as keyof HostedFieldsFieldDataFields].isValid)
setIsCardValid(cardValid)
})
hostedFields.on('focus', () => { setCardError(null) })

setHostedFieldsInstance(hostedFields)
}
void getHostedFieldsInstance()
}, [])
Expand All @@ -72,8 +74,8 @@ const BraintreeForm: FunctionComponent<Props> = ({
}

const get3DSecureNonce = async (payload: HostedFieldsTokenizePayload) => {
const threeDSecureInstance = await create3DSecureInstance(threeDSecure, braintreeClient) as ThreeDSecure
const response = await verifyCard(threeDSecureInstance, payload, billingAddressData) as ThreeDSecureVerifyPayload
const threeDSecureInstance = await create3DSecureInstance(braintreeClient)
const response = await verifyCard(threeDSecureInstance, payload, billingAddressData)

const error = get3DSecureError(response)
const nonce = response.nonce || null
Expand All @@ -92,10 +94,6 @@ const BraintreeForm: FunctionComponent<Props> = ({
}
}

const validateForm = (event: React.SyntheticEvent<HTMLFormElement>) => {
setFormErrors(validate(event.currentTarget, validationConstraints))
}

const handleCardError = (error: string) => {
setCardError(error)
clearHostedFields()
Expand All @@ -110,13 +108,13 @@ const BraintreeForm: FunctionComponent<Props> = ({
.catch(error => { console.error(error) })

// @ts-expect-error tokenize() expects an error, yet payload is assumed to be present. TODO: Fix this mess.
const response3Dsecure = threeDSecureEnabled ? await get3DSecureNonce(payload) : null
if (response3Dsecure?.error) {
handleCardError(response3Dsecure.error)
const response3DSecure = threeDSecureEnabled ? await get3DSecureNonce(payload) : null
if (response3DSecure?.error) {
handleCardError(response3DSecure.error)
return
}
// @ts-expect-error cascade previous TODO.
const nonce = (response3Dsecure ? response3Dsecure.nonce : payload.nonce) as string | null
const nonce = (response3DSecure ? response3DSecure.nonce : payload.nonce) as string | null
setBraintreeNonceValue(nonce)
setIsLoading(false)
}
Expand All @@ -128,7 +126,7 @@ const BraintreeForm: FunctionComponent<Props> = ({
className="form-horizontal customer"
id="customer_form"
ref={formRef}
onChange={validateForm}
onChange={() => { setFormErrors(validateForm(formRef)) }}
>
<input name="utf8" type="hidden" value="✓" />
<CSRFToken />
Expand Down

0 comments on commit cd50a02

Please sign in to comment.