Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/actions/external-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import { fetchWithSentry } from '@/utils'
import { AddBankAccountPayload } from './types/users.types'
import { IBridgeAccount } from '@/interfaces'

const API_KEY = process.env.PEANUT_API_KEY!
const API_URL = process.env.PEANUT_API_URL!

export async function createBridgeExternalAccountForGuest(
customerId: string,
accountDetails: AddBankAccountPayload
): Promise<{ id: string } | { error: string }> {
): Promise<IBridgeAccount | { error: string }> {
try {
const response = await fetchWithSentry(`${API_URL}/bridge/customers/${customerId}/external-accounts`, {
method: 'POST',
Expand Down
2 changes: 2 additions & 0 deletions src/components/AddMoney/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface CountryData {
currency?: string
description?: string
path: string
iso2?: string
}

export interface DepositMethods extends CountryData {
Expand Down Expand Up @@ -777,6 +778,7 @@ export const countryData: CountryData[] = [
title: 'United Kingdom',
currency: 'GBP',
path: 'united-kingdom',
iso2: 'GB',
},
{
id: 'GD',
Expand Down
1 change: 1 addition & 0 deletions src/components/AddWithdraw/AddWithdrawCountriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
country={getCountryCodeForWithdraw(currentCountry.id)}
onSuccess={handleFormSubmit}
initialData={{}}
error={null}
/>
<InitiateKYCModal
isOpen={isKycModalOpen}
Expand Down
103 changes: 81 additions & 22 deletions src/components/AddWithdraw/DynamicBankAccountForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client'
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
import { forwardRef, useImperativeHandle, useMemo, useState, useEffect } from 'react'
import { useForm, Controller } from 'react-hook-form'
import { useAuth } from '@/context/authContext'
import { Button } from '@/components/0_Bruddle/Button'
Expand Down Expand Up @@ -32,7 +32,7 @@ export type IBankAccountDetails = {
city: string
state: string
postalCode: string
iban?: string
iban: string
country: string
}

Expand All @@ -43,11 +43,20 @@ interface DynamicBankAccountFormProps {
initialData?: Partial<IBankAccountDetails>
flow?: 'claim' | 'withdraw'
actionDetailsProps?: Partial<PeanutActionDetailsCardProps>
error: string | null
}

export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, DynamicBankAccountFormProps>(
(
{ country, onSuccess, initialData, flow = 'withdraw', actionDetailsProps, countryName: countryNameFromProps },
{
country,
onSuccess,
initialData,
flow = 'withdraw',
actionDetailsProps,
countryName: countryNameFromProps,
error,
},
ref
) => {
const { user } = useAuth()
Expand All @@ -65,6 +74,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
control,
handleSubmit,
setValue,
getValues,
formState: { errors, isValid, isValidating, touchedFields },
} = useForm<IBankAccountDetails>({
defaultValues: {
Expand All @@ -82,15 +92,33 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
...initialData,
},
mode: 'onBlur',
reValidateMode: 'onSubmit',
})

useImperativeHandle(ref, () => ({
handleSubmit: handleSubmit(onSubmit),
}))

// Clear submission error when form becomes valid and BIC field is filled (if shown)
useEffect(() => {
if (submissionError && isValid && (!showBicField || getValues('bic'))) {
setSubmissionError(null)
}
}, [isValid, submissionError, showBicField, getValues])

const onSubmit = async (data: IBankAccountDetails) => {
// If validation is still running, don't proceed
if (isValidating) {
console.log('Validation still checking, skipping submission')
return
}

// Clear any existing submission errors before starting
if (submissionError) {
setSubmissionError(null)
}

setIsSubmitting(true)
setSubmissionError(null)
try {
const isUs = country.toUpperCase() === 'USA'
const isMx = country.toUpperCase() === 'MX'
Expand All @@ -105,22 +133,38 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
const accountNumber = isMx ? data.clabe : data.accountNumber

const { firstName, lastName } = data
let bic = data.bic
let bic = data.bic || getValues('bic')
const iban = data.iban || getValues('iban')

if (isIban && !bic) {
try {
bic = await getBicFromIban(accountNumber)
if (!bic) {
// for IBAN countries, ensure BIC is available
if (isIban) {
// if BIC field is shown but empty, don't proceed
if (showBicField && !bic) {
setIsSubmitting(false)
setSubmissionError('BIC is required')
return
}

// if BIC field is not shown and no BIC available, try to get it automatically
if (!showBicField && !bic) {
try {
const autoBic = await getBicFromIban(accountNumber)
if (autoBic) {
bic = autoBic
// set the BIC value in the form without showing the field
setValue('bic', autoBic, { shouldValidate: false })
} else {
setShowBicField(true)
setIsSubmitting(false)
setSubmissionError('BIC is required')
return
}
} catch (error) {
setShowBicField(true)
setIsSubmitting(false)
setSubmissionError('BIC is required')
return
}
} catch (error) {
setShowBicField(true)
setIsSubmitting(false)
setSubmissionError('BIC is required')
return
}
}

Expand Down Expand Up @@ -150,7 +194,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D

const result = await onSuccess(payload as AddBankAccountPayload, {
...data,
iban: isIban ? data.accountNumber : undefined,
iban: isIban ? data.accountNumber || iban || '' : '',
accountNumber: isIban ? '' : data.accountNumber,
bic: bic,
country,
Expand All @@ -164,7 +208,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
}
} catch (error: any) {
setSubmissionError(error.message)
} finally {
setIsSubmitting(false)
}
}
Expand Down Expand Up @@ -314,10 +357,25 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D

{isIban &&
showBicField &&
renderInput('bic', 'BIC', {
required: 'BIC is required',
validate: async (value: string) => (await validateBic(value)) || 'Invalid BIC code',
})}
renderInput(
'bic',
'BIC',
{
required: 'BIC is required',
validate: async (value: string) => {
if (!value || value.trim().length === 0) return 'BIC is required'
const isValid = await validateBic(value.trim())
return isValid || 'Invalid BIC code'
},
},
'text',
undefined,
(field) => {
if (field.value && field.value.trim().length > 0 && submissionError) {
setSubmissionError(null)
}
}
)}
{isUs &&
renderInput('routingNumber', 'Routing Number', {
required: 'Routing number is required',
Expand Down Expand Up @@ -345,12 +403,13 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
variant="purple"
shadowSize="4"
className="!mt-4 w-full"
loading={isSubmitting || isValidating}
disabled={isSubmitting || !isValid || isValidating}
loading={isSubmitting}
disabled={isSubmitting || !isValid}
>
Review
</Button>
{submissionError && <ErrorAlert description={submissionError} />}
{error && <ErrorAlert description={error} />}
</form>
</div>
</div>
Expand Down
63 changes: 46 additions & 17 deletions src/components/Claim/Link/views/BankFlowManager.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ import { useWebSocket } from '@/hooks/useWebSocket'
import { KYCStatus } from '@/utils/bridge-accounts.utils'
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'

type BankAccountWithId = IBankAccountDetails &
(
| { id: string; bridgeAccountId: string }
| { id: string; bridgeAccountId?: string }
| { id?: string; bridgeAccountId: string }
)

/**
* @name BankFlowManager
* @description This component manages the entire bank claim flow, acting as a state machine.
Expand Down Expand Up @@ -60,7 +67,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
const { claimLink } = useClaimLink()

// local states for this component
const [localBankDetails, setLocalBankDetails] = useState<IBankAccountDetails | null>(null)
const [localBankDetails, setLocalBankDetails] = useState<BankAccountWithId | null>(null)
const [receiverFullName, setReceiverFullName] = useState<string>('')
const [error, setError] = useState<string | null>(null)
const formRef = useRef<{ handleSubmit: () => void }>(null)
Expand Down Expand Up @@ -113,9 +120,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
* @name handleCreateOfframpAndClaim
* @description creates an off-ramp transfer for the user, either as a guest or a logged-in user.
*/
const handleCreateOfframpAndClaim = async (
account: IBankAccountDetails & { id?: string; bridgeAccountId?: string }
) => {
const handleCreateOfframpAndClaim = async (account: BankAccountWithId) => {
try {
setLoadingState('Executing transaction')
setError(null)
Expand Down Expand Up @@ -150,9 +155,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
const contractVersion = params.contractVersion
const peanutContractAddress = peanut.getContractAddress(chainId, contractVersion) as Address

const externalAccountId = account?.bridgeAccountId ?? account?.id

if (!externalAccountId) throw new Error('External account ID not found')
const externalAccountId = (account.bridgeAccountId ?? account.id) as string

const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id)

Expand Down Expand Up @@ -206,6 +209,9 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
payload: AddBankAccountPayload,
rawData: IBankAccountDetails
): Promise<{ error?: string }> => {
//clean any error from previous step
setError(null)

// scenario 1: receiver needs KYC
if (bankClaimType === BankClaimType.ReceiverKycNeeded && !justCompletedKyc) {
// update user's name and email if they are not present
Expand Down Expand Up @@ -242,16 +248,20 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
return { error: addBankAccountResponse.error }
}
if (addBankAccountResponse.data?.id) {
const bankDetails: IBankAccountDetails & { id?: string; bridgeAccountId?: string } = {
const bankDetails = {
name: addBankAccountResponse.data.details.accountOwnerName || user?.user.fullName || '',
iban:
addBankAccountResponse.data.type === 'iban'
? addBankAccountResponse.data.identifier
: undefined,
? addBankAccountResponse.data.identifier || ''
: '',
clabe:
addBankAccountResponse.data.type === 'clabe' ? addBankAccountResponse.data.identifier : '',
addBankAccountResponse.data.type === 'clabe'
? addBankAccountResponse.data.identifier || ''
: '',
accountNumber:
addBankAccountResponse.data.type === 'us' ? addBankAccountResponse.data.identifier : '',
addBankAccountResponse.data.type === 'us'
? addBankAccountResponse.data.identifier || ''
: '',
country: addBankAccountResponse.data.details.countryCode,
id: addBankAccountResponse.data.id,
bridgeAccountId: addBankAccountResponse.data.bridgeAccountId,
Expand Down Expand Up @@ -311,7 +321,25 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
throw new Error('Failed to create external account')
}

const finalBankDetails = { ...rawData, ...(externalAccountResponse as object) }
// merge the external account details with the user's details
const finalBankDetails = {
id: externalAccountResponse.id,
bridgeAccountId: externalAccountResponse.id,
name: externalAccountResponse.bank_name ?? rawData.name,
firstName: externalAccountResponse.first_name ?? rawData.firstName,
lastName: externalAccountResponse.last_name ?? rawData.lastName,
email: rawData.email,
accountNumber: externalAccountResponse.account_number ?? rawData.accountNumber,
bic: externalAccountResponse?.iban?.bic ?? rawData.bic,
routingNumber: externalAccountResponse?.account?.routing_number ?? rawData.routingNumber,
clabe: externalAccountResponse?.clabe?.account_number ?? rawData.clabe,
street: externalAccountResponse?.address?.street_line_1 ?? rawData.street,
city: externalAccountResponse?.address?.city ?? rawData.city,
state: externalAccountResponse?.address?.state ?? rawData.state,
postalCode: externalAccountResponse?.address?.postal_code ?? rawData.postalCode,
iban: externalAccountResponse?.iban?.account_number ?? rawData.iban,
country: externalAccountResponse?.iban?.country ?? rawData.country,
}
setLocalBankDetails(finalBankDetails)
setBankDetails(finalBankDetails)
setReceiverFullName(payload.accountOwnerName.firstName + ' ' + payload.accountOwnerName.lastName)
Expand Down Expand Up @@ -358,11 +386,11 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
).split(' ')
const lastName = lastNameParts.join(' ')

const bankDetails: IBankAccountDetails & { id?: string; bridgeAccountId?: string } = {
const bankDetails = {
name: account.details.accountOwnerName || user?.user.fullName || '',
iban: account.type === 'iban' ? account.identifier : undefined,
clabe: account.type === 'clabe' ? account.identifier : '',
accountNumber: account.type === 'us' ? account.identifier : '',
iban: account.type === 'iban' ? account.identifier || '' : '',
clabe: account.type === 'clabe' ? account.identifier || '' : '',
accountNumber: account.type === 'us' ? account.identifier || '' : '',
country: account.details.countryCode,
id: account.id,
bridgeAccountId: account.bridgeAccountId,
Expand Down Expand Up @@ -432,6 +460,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
tokenSymbol: claimLinkData.tokenSymbol,
}}
initialData={{}}
error={error}
/>
<InitiateKYCModal
isOpen={isKycModalOpen}
Expand Down
1 change: 0 additions & 1 deletion src/components/Common/CountryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export const CountryList = ({ inputTitle, viewMode, onCountryClick, onCryptoClic
['US', 'MX', ...Object.keys(countryCodeMap), ...Object.values(countryCodeMap)].includes(
country.id
)

return (
<SearchResultCard
key={country.id}
Expand Down
Loading
Loading