diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index ce4ee47e8..e4535d3c3 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -2,7 +2,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' -import { useState, useMemo, useContext, useEffect } from 'react' +import { useState, useMemo, useContext, useEffect, useCallback } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/0_Bruddle/Button' import { Card } from '@/components/0_Bruddle/Card' @@ -28,12 +28,11 @@ import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' +import { useSupportModalContext } from '@/context/SupportModalContext' +import { MantecaAccountType, MANTECA_COUNTRIES_CONFIG, MantecaBankCode } from '@/constants/manteca.consts' +import Select from '@/components/Global/Select' -type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' - -interface MantecaBankDetails { - destinationAddress: string -} +type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure' const MAX_WITHDRAW_AMOUNT = '500' @@ -43,7 +42,9 @@ export default function MantecaWithdrawFlow() { const [usdAmount, setUsdAmount] = useState(undefined) const [step, setStep] = useState('amountInput') const [balanceErrorMessage, setBalanceErrorMessage] = useState(null) - const [bankDetails, setBankDetails] = useState({ destinationAddress: '' }) + const [destinationAddress, setDestinationAddress] = useState('') + const [selectedBank, setSelectedBank] = useState(null) + const [accountType, setAccountType] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const [isKycModalOpen, setIsKycModalOpen] = useState(false) const [isDestinationAddressValid, setIsDestinationAddressValid] = useState(false) @@ -54,6 +55,7 @@ export default function MantecaWithdrawFlow() { const { sendMoney, balance } = useWallet() const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) const { user, fetchUser } = useAuth() + const { setIsSupportModalOpen } = useSupportModalContext() // Get method and country from URL parameters const selectedMethodType = searchParams.get('method') // mercadopago, pix, bank-transfer, etc. @@ -67,6 +69,11 @@ export default function MantecaWithdrawFlow() { return countryData.find((country) => country.type === 'country' && country.path === countryPath) }, [countryPath]) + const countryConfig = useMemo(() => { + if (!selectedCountry) return undefined + return MANTECA_COUNTRIES_CONFIG[selectedCountry.id] + }, [selectedCountry]) + const { code: currencyCode, symbol: currencySymbol, @@ -131,11 +138,23 @@ export default function MantecaWithdrawFlow() { return isValid } - const handleBankDetailsSubmit = () => { - if (!bankDetails.destinationAddress.trim()) { + const isCompleteBankDetails = useMemo(() => { + return ( + !!destinationAddress.trim() && + (!countryConfig?.needsBankCode || selectedBank != null) && + (!countryConfig?.needsAccountType || accountType != null) + ) + }, [selectedBank, accountType, countryConfig, destinationAddress]) + + const handleBankDetailsSubmit = useCallback(() => { + if (!destinationAddress.trim()) { setErrorMessage('Please enter your account address') return } + if ((countryConfig?.needsBankCode && !selectedBank) || (countryConfig?.needsAccountType && !accountType)) { + setErrorMessage('Please complete the bank details') + return + } setErrorMessage(null) // Check if we still need to determine KYC status @@ -151,10 +170,10 @@ export default function MantecaWithdrawFlow() { } setStep('review') - } + }, [selectedBank, accountType, destinationAddress, countryConfig?.needsBankCode, countryConfig?.needsAccountType]) const handleWithdraw = async () => { - if (!bankDetails.destinationAddress || !usdAmount || !currencyCode) return + if (!destinationAddress || !usdAmount || !currencyCode) return try { setLoadingState('Preparing transaction') @@ -173,20 +192,28 @@ export default function MantecaWithdrawFlow() { // Call Manteca withdraw API const result = await mantecaApi.withdraw({ amount: usdAmount, - destinationAddress: bankDetails.destinationAddress, - txHash: txHash, + destinationAddress, + bankCode: selectedBank?.code, + accountType: accountType ?? undefined, + txHash, currency: currencyCode, }) if (result.error) { - setErrorMessage(result.error) + if (result.error === 'Unexpected error') { + setErrorMessage('Withdraw failed unexpectedly. If problem persists contact support') + setStep('failure') + } else { + setErrorMessage(result.message ?? result.error) + } return } setStep('success') } catch (error) { console.error('Manteca withdraw error:', error) - setErrorMessage('An unexpected error occurred. Please contact support.') + setErrorMessage('Withdraw failed unexpectedly. If problem persists contact support') + setStep('failure') } finally { setLoadingState('Idle') } @@ -194,8 +221,16 @@ export default function MantecaWithdrawFlow() { const resetState = () => { setStep('amountInput') - setBankDetails({ destinationAddress: '' }) + setAmount(undefined) + setCurrencyAmount(undefined) + setUsdAmount(undefined) + setDestinationAddress('') + setSelectedBank(null) + setAccountType(null) setErrorMessage(null) + setIsKycModalOpen(false) + setIsDestinationAddressValid(false) + setIsDestinationAddressChanging(false) resetWithdrawFlow() setBalanceErrorMessage(null) } @@ -219,7 +254,7 @@ export default function MantecaWithdrawFlow() { } }, [usdAmount, balance]) - if (isCurrencyLoading || !currencyPrice) { + if (isCurrencyLoading || !currencyPrice || !selectedCountry) { return } @@ -240,7 +275,7 @@ export default function MantecaWithdrawFlow() { {currencyCode} {currencyAmount}
≈ ${usdAmount} USD
-

to {bankDetails.destinationAddress}

+

to {destinationAddress}

@@ -259,6 +294,33 @@ export default function MantecaWithdrawFlow() { ) } + if (step === 'failure') { + return ( +
+ +
+ + + Something went wrong! + {errorMessage} + + + + + + +
+
+ ) + } return (
{currencyCode} {currencyAmount}

+
≈ {usdAmount} USD
@@ -342,10 +405,10 @@ export default function MantecaWithdrawFlow() {
{ - setBankDetails({ destinationAddress: update.value }) + setDestinationAddress(update.value) setIsDestinationAddressValid(update.isValid) setIsDestinationAddressChanging(update.isChanging) if (update.isValid || update.value === '') { @@ -354,6 +417,31 @@ export default function MantecaWithdrawFlow() { }} validate={validateDestinationAddress} /> + {countryConfig?.needsAccountType && ( + { + setSelectedBank({ code: item.id, name: item.title }) + }} + items={countryConfig.validBankCodes.map((bank) => ({ + id: bank.code, + title: bank.name, + }))} + placeholder="Select bank" + className="w-full" + /> + )}
@@ -364,9 +452,7 @@ export default function MantecaWithdrawFlow() {
{/* Review Summary */} - + > = { India: [{ title: 'UPI', description: 'Unified Payments Interface, ~17B txns/month, 84% of digital payments.' }], - Brazil: [{ title: 'Pix', description: '75%+ population use it, 40% e-commerce share.' }], + Brazil: [{ title: 'Pix', description: 'Instant transfers', icon: PIX, isSoon: false }], Argentina: [ { title: 'Mercado Pago', @@ -2518,7 +2518,7 @@ export const countryCodeMap: { [key: string]: string } = { USA: 'US', } -const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX', 'AR', 'BR', 'BO']) +const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX', 'AR', 'BO']) // Helper function to check if a country code is enabled for bank transfers // Handles both 2-letter and 3-letter country codes diff --git a/src/components/Global/Select/index.tsx b/src/components/Global/Select/index.tsx index c8a2472a9..46a78c62d 100644 --- a/src/components/Global/Select/index.tsx +++ b/src/components/Global/Select/index.tsx @@ -4,6 +4,11 @@ import { useRef } from 'react' import { createPortal } from 'react-dom' import { twMerge } from 'tailwind-merge' +type SelectItem = { + id: string + title: string +} + type SelectProps = { label?: string className?: string @@ -12,9 +17,9 @@ type SelectProps = { classOptions?: string classOption?: string placeholder?: string - items: any - value: any - onChange: any + items: SelectItem[] + value: SelectItem | null + onChange: (item: SelectItem) => void up?: boolean small?: boolean classPlaceholder?: string @@ -33,7 +38,6 @@ const Select = ({ onChange, up, small, - classPlaceholder, }: SelectProps) => { const buttonRef = useRef(null) @@ -46,21 +50,19 @@ const Select = ({ - - {value ? ( - value - ) : ( - {placeholder} - )} - + {value ? ( + {value.title} + ) : ( + {placeholder} + )} - {items.map((item: any) => ( + {items.map((item) => ( - {item.name} + {item.title} ))} diff --git a/src/components/Global/ValidatedInput/index.tsx b/src/components/Global/ValidatedInput/index.tsx index 2cae05c10..a7a1f4c7d 100644 --- a/src/components/Global/ValidatedInput/index.tsx +++ b/src/components/Global/ValidatedInput/index.tsx @@ -147,7 +147,7 @@ const ValidatedInput = ({ return (
{ classButton="h-auto px-0 border-none bg-trasparent text-sm !font-normal" classOptions="-left-4 -right-3 w-auto py-1 overflow-auto max-h-36" classArrow="ml-1" - items={consts.supportedPeanutChains} + items={consts.supportedPeanutChains.map((chain) => ({ + id: chain.chainId, + title: chain.name, + }))} value={ - consts.supportedPeanutChains.find( - (chain) => chain.chainId === refundFormWatch.chainId - )?.name + consts.supportedPeanutChains + .map((c) => ({ id: c.chainId, title: c.name })) + .find((i) => i.id === refundFormWatch.chainId) ?? null } onChange={(chainId: any) => { refundForm.setValue('chainId', chainId.chainId) diff --git a/src/constants/manteca.consts.ts b/src/constants/manteca.consts.ts index ab2fb17ff..6da815ce5 100644 --- a/src/constants/manteca.consts.ts +++ b/src/constants/manteca.consts.ts @@ -21,3 +21,91 @@ export type MantecaCountry = (typeof MANTECA_COUNTRIES)[number] export const isMantecaCountry = (countryPath: string): boolean => { return MANTECA_COUNTRIES.includes(countryPath as MantecaCountry) } + +export enum MantecaAccountType { + SAVINGS = 'SAVINGS', + CHECKING = 'CHECKING', + DEBIT = 'DEBIT', + PHONE = 'PHONE', + VISTA = 'VISTA', + RUT = 'RUT', +} + +export type MantecaBankCode = { + code: string + name: string +} + +type MantecaCountryConfig = { + accountNumberLabel: string +} & ( + | { + needsBankCode: true + needsAccountType: true + validAccountTypes: MantecaAccountType[] + validBankCodes: MantecaBankCode[] + } + | { + needsBankCode: false + needsAccountType: false + } +) + +/** + * Configuration for each country that uses Manteca + * Some countries needs only account number but others need extra data, + * and that data like account type and bank code is different for each + * country and part of a list of valid values so we need to have a + * config for each country + * + * @see https://docs.manteca.dev/cripto/start-operating/manual-operation/requesting-a-withdraw/defining-the-destination + */ +export const MANTECA_COUNTRIES_CONFIG: Record = { + AR: { + accountNumberLabel: 'CBU, CVU or Alias', + needsBankCode: false, + needsAccountType: false, + }, + BR: { + accountNumberLabel: 'PIX Key', + needsBankCode: false, + needsAccountType: false, + }, + BO: { + accountNumberLabel: 'Account Number', + needsBankCode: true, + needsAccountType: true, + validAccountTypes: [MantecaAccountType.CHECKING, MantecaAccountType.SAVINGS], + validBankCodes: [ + { code: '001', name: 'BANCO MERCANTIL' }, + { code: '002', name: 'BANCO NACIONAL DE BOLIVIA' }, + { code: '003', name: 'BANCO DE CRÉDITO DE BOLIVIA' }, + { code: '005', name: 'BANCO BISA' }, + { code: '006', name: 'BANCO UNIÓN' }, + { code: '007', name: 'BANCO ECONÓMICO' }, + { code: '008', name: 'BANCO SOLIDARIO' }, + { code: '009', name: 'BANCO GANADERO' }, + { code: '011', name: 'LA PRIMERA ENTIDAD FINANCIERA DE VIVIENDA (EX MUTUAL LA PRIMERA)' }, + { code: '013', name: 'MUTUAL LA PROMOTORA' }, + { code: '014', name: 'EL PROGRESO ENTIDAD FINANCIERA DE VIVIENDA (EX MUTUAL EL PROGRESO)' }, + { code: '022', name: 'COOPERATIVA JESÚS NAZARENO' }, + { code: '023', name: 'COOPERATIVA SAN MARTIN' }, + { code: '024', name: 'COOPERATIVA FÁTIMA' }, + { code: '029', name: 'COOPERATIVA PIO X' }, + { code: '031', name: 'COOPERATIVA QUILLACOLLO' }, + { code: '033', name: 'COOPERATIVA TRINIDAD' }, + { code: '034', name: 'COOPERATIVA COMARAPA' }, + { code: '035', name: 'COOPERATIVA SAN MATEO' }, + { code: '036', name: 'COOPERATIVA EL CHOROLQUE' }, + { code: '038', name: 'COOPERATIVA CATEDRAL' }, + { code: '039', name: 'MAGISTERIO RURAL' }, + { code: '044', name: 'BANCO PYME DE LA COMUNIDAD (BANCOMUNIDAD)' }, + { code: '045', name: 'BANCO FIE' }, + { code: '047', name: 'BANCO ECOFUTURO' }, + { code: '049', name: 'BANCO FORTALEZA' }, + { code: '052', name: 'BANCO DE LA NACION ARGENTINA' }, + { code: '053', name: 'TIGO MONEY (BILLETERA MOVIL)' }, + { code: '054', name: 'BILLETERA MÓVIL DE ENTEL' }, + ], + }, +} diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 7ac881617..9bf4227ec 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -1,3 +1,5 @@ +import { MantecaAccountType } from '@/constants/manteca.consts' + export interface MantecaDepositDetails { depositAddress: string depositAlias: string @@ -13,6 +15,8 @@ export enum MercadoPagoStep { export type MantecaWithdrawData = { amount: string destinationAddress: string + bankCode?: string + accountType?: MantecaAccountType txHash: string currency: string } @@ -66,6 +70,7 @@ export type MantecaWithdrawResponseData = { export type MantecaWithdrawResponse = { data?: MantecaWithdrawResponseData error?: string + message?: string } export interface CreateMantecaOnrampParams {