diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index 24ce2f325..0d0ad6365 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -1,6 +1,11 @@
'use client'
-import { COUNTRY_SPECIFIC_METHODS, countryData, SpecificPaymentMethod } from '@/components/AddMoney/consts'
+import {
+ COUNTRY_SPECIFIC_METHODS,
+ countryCodeMap,
+ countryData,
+ SpecificPaymentMethod,
+} from '@/components/AddMoney/consts'
import StatusBadge from '@/components/Global/Badges/StatusBadge'
import { IconName } from '@/components/Global/Icons/Icon'
import NavHeader from '@/components/Global/NavHeader'
@@ -11,7 +16,7 @@ import Image, { StaticImageData } from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import EmptyState from '../Global/EmptyStates/EmptyState'
import { useAuth } from '@/context/authContext'
-import { useEffect, useRef, useState } from 'react'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { InitiateKYCModal } from '@/components/Kyc'
import { DynamicBankAccountForm, IBankAccountDetails } from './DynamicBankAccountForm'
import { addBankAccount, updateUserById } from '@/app/actions/users'
@@ -23,6 +28,7 @@ import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useOnrampFlow } from '@/context/OnrampFlowContext'
import { Account } from '@/interfaces'
import PeanutLoading from '../Global/PeanutLoading'
+import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
interface AddWithdrawCountriesListProps {
flow: 'add' | 'withdraw'
@@ -212,7 +218,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
index 5cb050530..038ee3bed 100644
--- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx
+++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
@@ -13,6 +13,7 @@ import { getBicFromIban } from '@/app/actions/ibanToBic'
import PeanutActionDetailsCard, { PeanutActionDetailsCardProps } from '../Global/PeanutActionDetailsCard'
import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
+import { getCountryFromIban, validateMXCLabeAccount, validateUSBankAccount } from '@/utils/withdraw.utils'
const isIBANCountry = (country: string) => {
return countryCodeMap[country.toUpperCase()] !== undefined
@@ -41,19 +42,22 @@ interface DynamicBankAccountFormProps {
initialData?: Partial
flow?: 'claim' | 'withdraw'
actionDetailsProps?: Partial
+ countryName?: string
}
export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, DynamicBankAccountFormProps>(
- ({ country, onSuccess, initialData, flow = 'withdraw', actionDetailsProps }, ref) => {
+ ({ country, countryName, onSuccess, initialData, flow = 'withdraw', actionDetailsProps }, ref) => {
const { user } = useAuth()
const [isSubmitting, setIsSubmitting] = useState(false)
const [submissionError, setSubmissionError] = useState(null)
const [showBicField, setShowBicField] = useState(false)
- const { country: countryName } = useParams()
+ const { country: countryNameParams } = useParams()
const { amountToWithdraw } = useWithdrawFlow()
const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
const lastName = lastNameParts.join(' ')
+ let selectedCountry = (countryName ?? (countryNameParams as string)).toLowerCase()
+
const {
control,
handleSubmit,
@@ -85,9 +89,9 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
setIsSubmitting(true)
setSubmissionError(null)
try {
- const isIban = isIBANCountry(country)
- const isUs = country.toUpperCase() === 'US'
+ const isUs = country.toUpperCase() === 'USA'
const isMx = country.toUpperCase() === 'MX'
+ const isIban = isUs || isMx ? false : isIBANCountry(country)
let accountType: BridgeAccountType
if (isIban) accountType = BridgeAccountType.IBAN
@@ -121,7 +125,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
accountType,
accountNumber: accountNumber.replace(/\s/g, ''),
countryCode: isUs ? 'USA' : country.toUpperCase(),
- countryName: countryName as string,
+ countryName: selectedCountry,
accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL,
accountOwnerName: {
firstName: firstName.trim(),
@@ -159,9 +163,9 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
}
}
- const isIban = isIBANCountry(country)
- const isUs = country.toUpperCase() === 'US'
const isMx = country.toUpperCase() === 'MX'
+ const isUs = country.toUpperCase() === 'USA'
+ const isIban = isUs || isMx ? false : isIBANCountry(country)
const renderInput = (
name: keyof IBankAccountDetails,
@@ -246,14 +250,25 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
required: 'CLABE is required',
minLength: { value: 18, message: 'CLABE must be 18 digits' },
maxLength: { value: 18, message: 'CLABE must be 18 digits' },
+ validate: async (value: string) =>
+ validateMXCLabeAccount(value).isValid || 'Invalid CLABE',
})
: isIban
? renderInput(
'accountNumber',
'IBAN',
{
- required: 'Account number is required',
- validate: async (val: string) => (await validateIban(val)) || 'Invalid IBAN',
+ required: 'IBAN is required',
+ validate: async (val: string) => {
+ const isValidIban = await validateIban(val)
+ if (!isValidIban) return 'Invalid IBAN'
+
+ if (getCountryFromIban(val)?.toLowerCase() !== selectedCountry) {
+ return 'IBAN does not match the selected country'
+ }
+
+ return true
+ },
},
'text',
undefined,
@@ -267,7 +282,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
{
required: 'Account number is required',
validate: async (value: string) =>
- (await validateBankAccount(value)) || 'Invalid account number',
+ validateUSBankAccount(value).isValid || 'Invalid account number',
},
'text'
)}
diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx
index 7d98e6f3b..60afba58b 100644
--- a/src/components/Claim/Link/views/BankFlowManager.view.tsx
+++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx
@@ -20,6 +20,7 @@ import { getBridgeChainName, getBridgeTokenName } from '@/utils/bridge-accounts.
import peanut from '@squirrel-labs/peanut-sdk'
import { getUserById } from '@/app/actions/users'
import NavHeader from '@/components/Global/NavHeader'
+import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
export const BankFlowManager = (props: IClaimScreenProps) => {
const { onCustom, claimLinkData, setTransactionHash } = props
@@ -214,7 +215,8 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
{
+ // Remove spaces and convert to uppercase
+ const cleanIban = iban.replace(/\s/g, '').toUpperCase()
+
+ // Extract the first 2 characters as country code
+ const countryCode = cleanIban.substring(0, 2)
+
+ // Try to find country by 2-letter code directly in countryData
+ let country = countryData.find((c) => c.type === 'country' && c.id === countryCode)
+
+ // If not found, get the 3-letter code and try that
+ if (!country) {
+ const threeLetterCode = getCountryCodeForWithdraw(countryCode)
+ if (threeLetterCode !== countryCode) {
+ country = countryData.find((c) => c.type === 'country' && c.id === threeLetterCode)
+ }
+ }
+
+ return country ? country.title : null
+}
+
+/**
+ * Validates a US bank account number with comprehensive checks
+ * @param accountNumber - The bank account number to validate
+ * @returns Object with isValid boolean and error message if invalid
+ */
+export const validateUSBankAccount = (accountNumber: string) => {
+ // Remove spaces and hyphens for validation
+ const cleanAccountNumber = accountNumber.replace(/[\s-]/g, '')
+
+ // Check if contains only digits
+ if (!/^\d+$/.test(cleanAccountNumber)) {
+ return {
+ isValid: false,
+ error: 'Account number must contain only digits',
+ }
+ }
+
+ // Check minimum length (US bank accounts are typically 6-17 digits)
+ if (cleanAccountNumber.length < 6) {
+ return {
+ isValid: false,
+ error: 'Account number must be at least 6 digits',
+ }
+ }
+
+ // Check maximum length
+ if (cleanAccountNumber.length > 17) {
+ return {
+ isValid: false,
+ error: 'Account number cannot exceed 17 digits',
+ }
+ }
+
+ // Check for obviously invalid patterns
+ if (/^0+$/.test(cleanAccountNumber)) {
+ return {
+ isValid: false,
+ error: 'Account number cannot be all zeros',
+ }
+ }
+
+ return {
+ isValid: true,
+ error: null,
+ }
+}
+
+/**
+ * Validates a Mexican CLABE (Clave Bancaria Estandarizada) account number
+ * CLABE is exactly 18 digits with a specific structure and check digit validation
+ * @param accountNumber - The CLABE account number to validate
+ * @returns Object with isValid boolean and error message if invalid
+ */
+export const validateMXCLabeAccount = (accountNumber: string) => {
+ // Remove spaces and hyphens for validation
+ const cleanAccountNumber = accountNumber.replace(/[\s-]/g, '')
+
+ // Check if contains only digits
+ if (!/^\d+$/.test(cleanAccountNumber)) {
+ return {
+ isValid: false,
+ error: 'CLABE must contain only digits',
+ }
+ }
+
+ // CLABE must be exactly 18 digits
+ if (cleanAccountNumber.length !== 18) {
+ return {
+ isValid: false,
+ error: 'CLABE must be exactly 18 digits',
+ }
+ }
+
+ // Check for obviously invalid patterns
+ if (/^0+$/.test(cleanAccountNumber)) {
+ return {
+ isValid: false,
+ error: 'CLABE cannot be all zeros',
+ }
+ }
+
+ // Validate CLABE check digit using the official algorithm
+ const digits = cleanAccountNumber.split('').map(Number)
+ const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7]
+
+ let sum = 0
+ for (let i = 0; i < 17; i++) {
+ sum += digits[i] * weights[i]
+ }
+
+ const remainder = sum % 10
+ const calculatedCheckDigit = remainder === 0 ? 0 : 10 - remainder
+ const providedCheckDigit = digits[17]
+
+ if (calculatedCheckDigit !== providedCheckDigit) {
+ return {
+ isValid: false,
+ error: 'CLABE check digit is invalid',
+ }
+ }
+
+ return {
+ isValid: true,
+ error: null,
+ }
+}
+
+// Returns the 3-letter country code for the given country code
+export const getCountryCodeForWithdraw = (country: string) => {
+ // If the input is already a 3-digit code and exists in the map, return it
+ if (countryCodeMap[country]) {
+ return country
+ }
+
+ // If the input is a 2-digit code, find the corresponding 3-digit code
+ const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country)
+
+ return threeDigitCode || country
+}