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
89 changes: 51 additions & 38 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, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { useForm, Controller } from 'react-hook-form'
import { useAuth } from '@/context/authContext'
import { Button } from '@/components/0_Bruddle/Button'
Expand All @@ -14,6 +14,7 @@ import PeanutActionDetailsCard, { PeanutActionDetailsCardProps } from '../Global
import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { getCountryFromIban, validateMXCLabeAccount, validateUSBankAccount } from '@/utils/withdraw.utils'
import { useDebounce } from '@/hooks/useDebounce'

const isIBANCountry = (country: string) => {
return countryCodeMap[country.toUpperCase()] !== undefined
Expand Down Expand Up @@ -44,6 +45,7 @@ interface DynamicBankAccountFormProps {
flow?: 'claim' | 'withdraw'
actionDetailsProps?: Partial<PeanutActionDetailsCardProps>
error: string | null
hideEmailInput?: boolean
}

export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, DynamicBankAccountFormProps>(
Expand All @@ -56,17 +58,21 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
actionDetailsProps,
countryName: countryNameFromProps,
error,
hideEmailInput = false,
},
ref
) => {
const isMx = country.toUpperCase() === 'MX'
const isUs = country.toUpperCase() === 'USA'
const isIban = isUs || isMx ? false : isIBANCountry(country)
const { user } = useAuth()
const [isSubmitting, setIsSubmitting] = useState(false)
const [submissionError, setSubmissionError] = useState<string | null>(null)
const [showBicField, setShowBicField] = useState(false)
const { country: countryNameParams } = useParams()
const { amountToWithdraw } = useWithdrawFlow()
const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
const lastName = lastNameParts.join(' ')
const [isCheckingBICValid, setisCheckingBICValid] = useState(false)

let selectedCountry = (countryNameFromProps ?? (countryNameParams as string)).toLowerCase()

Expand All @@ -75,6 +81,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
handleSubmit,
setValue,
getValues,
watch,
formState: { errors, isValid, isValidating, touchedFields },
} = useForm<IBankAccountDetails>({
defaultValues: {
Expand All @@ -95,10 +102,29 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
reValidateMode: 'onSubmit',
})

// Watch BIC field value for debouncing
const bicValue = watch('bic')
const debouncedBicValue = useDebounce(bicValue, 500) // 500ms delay

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

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

// Trigger BIC validation when debounced value changes
useEffect(() => {
if (isIban && debouncedBicValue && debouncedBicValue.trim().length > 0) {
// Trigger validation for the BIC field
setValue('bic', debouncedBicValue, { shouldValidate: true })
}
}, [debouncedBicValue, isIban, setValue])

const onSubmit = async (data: IBankAccountDetails) => {
// If validation is still running, don't proceed
if (isValidating) {
Expand Down Expand Up @@ -129,35 +155,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
let bic = data.bic || getValues('bic')
const iban = data.iban || getValues('iban')

// for IBAN countries, ensure BIC is available
if (isIban) {
// if BIC field is shown but empty, don't proceed
if (showBicField && !bic) {
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)
setSubmissionError('BIC is required')
return
}
} catch (error) {
setShowBicField(true)
setSubmissionError('BIC is required')
return
}
}
}

const payload: Partial<AddBankAccountPayload> = {
accountType,
accountNumber: accountNumber.replace(/\s/g, ''),
Expand Down Expand Up @@ -202,10 +199,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
}
}

const isMx = country.toUpperCase() === 'MX'
const isUs = country.toUpperCase() === 'USA'
const isIban = isUs || isMx ? false : isIBANCountry(country)

const renderInput = (
name: keyof IBankAccountDetails,
placeholder: string,
Expand Down Expand Up @@ -288,6 +281,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
{flow === 'claim' &&
user?.user.userId &&
!user.user.email &&
!hideEmailInput &&
renderInput('email', 'E-mail', {
required: 'Email is required',
})}
Expand All @@ -298,6 +292,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
</div>
)}
{flow !== 'claim' &&
!hideEmailInput &&
!user?.user?.email &&
renderInput('email', 'E-mail', {
required: 'Email is required',
Expand Down Expand Up @@ -332,6 +327,17 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
undefined,
async (field) => {
if (!field.value || field.value.trim().length === 0) return
const isValidIban = await validateIban(field.value)
if (isValidIban) {
try {
const autoBic = await getBicFromIban(field.value)
if (autoBic && !getValues('bic')) {
setValue('bic', autoBic, { shouldValidate: true })
}
} catch {
console.log('Could not fetch BIC automatically.')
}
}
}
)
: renderInput(
Expand All @@ -346,15 +352,22 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
)}

{isIban &&
showBicField &&
renderInput(
'bic',
'BIC',
{
required: 'BIC is required',
validate: async (value: string) => {
if (!value || value.trim().length === 0) return 'BIC is required'

// Only validate if the value matches the debounced value (to prevent API calls on every keystroke)
if (value.trim() !== debouncedBicValue?.trim()) {
return true // Skip validation until debounced value is ready
}

setisCheckingBICValid(true)
const isValid = await validateBic(value.trim())
setisCheckingBICValid(false)
return isValid || 'Invalid BIC code'
},
},
Expand Down Expand Up @@ -393,8 +406,8 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
variant="purple"
shadowSize="4"
className="!mt-4 w-full"
loading={isSubmitting}
disabled={isSubmitting || !isValid}
loading={isSubmitting || isCheckingBICValid}
disabled={isSubmitting || !isValid || isCheckingBICValid}
>
Review
</Button>
Expand Down
1 change: 1 addition & 0 deletions src/components/Claim/Link/views/BankFlowManager.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
countryName={selectedCountry?.title ?? ''}
onSuccess={handleSuccess}
flow={'claim'}
hideEmailInput={bankClaimType === BankClaimType.GuestBankClaim}
actionDetailsProps={{
transactionType: 'CLAIM_LINK_BANK_ACCOUNT',
recipientType: 'BANK_ACCOUNT',
Expand Down
4 changes: 2 additions & 2 deletions src/components/Global/TokenAmountInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const TokenAmountInput = ({
useEffect(() => {
if (inputRef.current) {
if (displayValue?.length !== 0) {
inputRef.current.style.width = `${(displayValue?.length ?? 0) + 1}ch`
inputRef.current.style.width = `${displayValue?.length ?? 0}ch`
} else {
inputRef.current.style.width = `4ch`
}
Expand Down Expand Up @@ -203,7 +203,7 @@ const TokenAmountInput = ({
<div className="flex h-14 w-full flex-row items-center justify-center gap-1">
<label className={`text-h1 ${displayValue ? 'text-black' : 'text-gray-2'}`}>{displaySymbol}</label>
<input
className={`h-12 w-[4ch] max-w-80 bg-transparent text-h1 outline-none transition-colors placeholder:text-h1 focus:border-primary-1 dark:border-white dark:bg-n-1 dark:text-white dark:placeholder:text-white/75 dark:focus:border-primary-1`}
className={`h-12 max-w-80 bg-transparent text-center text-h1 outline-none transition-colors placeholder:text-h1 focus:border-primary-1 dark:border-white dark:bg-n-1 dark:text-white dark:placeholder:text-white/75 dark:focus:border-primary-1`}
placeholder={'0.00'}
onChange={(e) => {
const value = formatAmountWithoutComma(e.target.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ const DirectRequestInitialView = ({ username }: DirectRequestInitialViewProps) =
disabled={isButtonDisabled || isButtonLoading}
loading={isButtonLoading}
icon="arrow-down-left"
iconSize={12}
>
{isButtonLoading ? loadingState : 'Request'}
</Button>
Expand Down
Loading