diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index edce3df1e..646304733 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -21,6 +21,7 @@ import { useWebSocket } from '@/hooks/useWebSocket'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { Account } from '@/interfaces'
import PeanutLoading from '../Global/PeanutLoading'
+import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
interface AddWithdrawCountriesListProps {
flow: 'add' | 'withdraw'
@@ -195,7 +196,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
/>
diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
index 8401d8dce..5133348d3 100644
--- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx
+++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
@@ -7,12 +7,13 @@ import { AddBankAccountPayload, BridgeAccountOwnerType, BridgeAccountType } from
import BaseInput from '@/components/0_Bruddle/BaseInput'
import { countryCodeMap } from '@/components/AddMoney/consts'
import { useParams } from 'next/navigation'
-import { validateBankAccount, validateIban, validateBic, isValidRoutingNumber } from '@/utils/bridge-accounts.utils'
+import { validateIban, validateBic, isValidRoutingNumber } from '@/utils/bridge-accounts.utils'
import ErrorAlert from '@/components/Global/ErrorAlert'
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
@@ -58,6 +59,8 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
const lastName = lastNameParts.join(' ')
+ let selectedCountry = (countryNameFromProps ?? (countryName as string)).toLowerCase()
+
const {
control,
handleSubmit,
@@ -89,9 +92,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
@@ -125,7 +128,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
accountType,
accountNumber: accountNumber.replace(/\s/g, ''),
countryCode: isUs ? 'USA' : country.toUpperCase(),
- countryName: (countryName ?? countryNameFromProps) as string,
+ countryName: selectedCountry,
accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL,
accountOwnerName: {
firstName: firstName.trim(),
@@ -164,9 +167,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,
@@ -265,14 +268,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,
@@ -286,7 +300,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 aed3f45e9..be27c9a2b 100644
--- a/src/components/Claim/Link/views/BankFlowManager.view.tsx
+++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx
@@ -27,6 +27,7 @@ import NavHeader from '@/components/Global/NavHeader'
import { InitiateKYCModal } from '@/components/Kyc'
import { useWebSocket } from '@/hooks/useWebSocket'
import { KYCStatus } from '@/utils/bridge-accounts.utils'
+import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
/**
* @name BankFlowManager
@@ -410,7 +411,7 @@ 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
+}