diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx index d7fd6d4fd..bececb135 100644 --- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx +++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx @@ -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' @@ -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 @@ -44,6 +45,7 @@ interface DynamicBankAccountFormProps { flow?: 'claim' | 'withdraw' actionDetailsProps?: Partial error: string | null + hideEmailInput?: boolean } export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, DynamicBankAccountFormProps>( @@ -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(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() @@ -75,6 +81,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D handleSubmit, setValue, getValues, + watch, formState: { errors, isValid, isValidating, touchedFields }, } = useForm({ defaultValues: { @@ -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) { @@ -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 = { accountType, accountNumber: accountNumber.replace(/\s/g, ''), @@ -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, @@ -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', })} @@ -298,6 +292,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D )} {flow !== 'claim' && + !hideEmailInput && !user?.user?.email && renderInput('email', 'E-mail', { required: 'Email is required', @@ -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( @@ -346,7 +352,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D )} {isIban && - showBicField && renderInput( 'bic', 'BIC', @@ -354,7 +359,15 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D 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' }, }, @@ -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 diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx index 33c7ca4a8..e2c09a7ee 100644 --- a/src/components/Claim/Link/views/BankFlowManager.view.tsx +++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx @@ -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', diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 39387c799..bc3833434 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -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` } @@ -203,7 +203,7 @@ const TokenAmountInput = ({
{ const value = formatAmountWithoutComma(e.target.value) diff --git a/src/components/Request/direct-request/views/Initial.direct.request.view.tsx b/src/components/Request/direct-request/views/Initial.direct.request.view.tsx index 9d5bdcff9..4ed7f08f4 100644 --- a/src/components/Request/direct-request/views/Initial.direct.request.view.tsx +++ b/src/components/Request/direct-request/views/Initial.direct.request.view.tsx @@ -279,6 +279,7 @@ const DirectRequestInitialView = ({ username }: DirectRequestInitialViewProps) = disabled={isButtonDisabled || isButtonLoading} loading={isButtonLoading} icon="arrow-down-left" + iconSize={12} > {isButtonLoading ? loadingState : 'Request'}