diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index aad1f6bf2..d5973535f 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -16,6 +16,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { clearRedirectUrl, getRedirectUrl, isTxReverted } from '@/utils/general.utils' import ErrorAlert from '@/components/Global/ErrorAlert' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts' import { formatUnits, parseUnits } from 'viem' import { useTransactionDetailsDrawer } from '@/hooks/useTransactionDetailsDrawer' import { TransactionDetailsDrawer } from '@/components/TransactionDetails/TransactionDetailsDrawer' @@ -30,7 +31,6 @@ import ActionModal from '@/components/Global/ActionModal' import { saveRedirectUrl } from '@/utils/general.utils' import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' -const MANTECA_DEPOSIT_ADDRESS = '0x959e088a09f61aB01cb83b0eBCc74b2CF6d62053' const MAX_QR_PAYMENT_AMOUNT = '200' export default function QRPayPage() { diff --git a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx index 0c452f09c..c6565d5cd 100644 --- a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx @@ -41,10 +41,14 @@ export default function WithdrawBankPage() { )?.currencyCode useEffect(() => { - if (!bankAccount) { + if (!bankAccount && !amountToWithdraw) { + // If no bank account AND no amount, go back to main page router.replace('/withdraw') + } else if (!bankAccount && amountToWithdraw) { + // If amount is set but no bank account, go to country method selection + router.replace(`/withdraw/${country}`) } - }, [bankAccount, router]) + }, [bankAccount, router, amountToWithdraw, country]) const destinationDetails = (account: Account) => { let countryId: string diff --git a/src/app/(mobile-ui)/withdraw/crypto/page.tsx b/src/app/(mobile-ui)/withdraw/crypto/page.tsx index 891dbacaa..cf6486739 100644 --- a/src/app/(mobile-ui)/withdraw/crypto/page.tsx +++ b/src/app/(mobile-ui)/withdraw/crypto/page.tsx @@ -89,13 +89,10 @@ export default function WithdrawCryptoPage() { }, [dispatch, resetPaymentInitiator]) useEffect(() => { - if (!amountToWithdraw) { - console.error('Amount not available in WithdrawFlowContext for withdrawal, redirecting.') - router.push('/withdraw') - return + if (amountToWithdraw) { + clearErrors() + dispatch(paymentActions.setChargeDetails(null)) } - clearErrors() - dispatch(paymentActions.setChargeDetails(null)) }, [amountToWithdraw]) useEffect(() => { @@ -318,6 +315,8 @@ export default function WithdrawCryptoPage() { }, [xChainRoute]) if (!amountToWithdraw) { + // Redirect to main withdraw page for amount input + router.push('/withdraw') return } diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx new file mode 100644 index 000000000..14f3c453e --- /dev/null +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -0,0 +1,440 @@ +'use client' + +import { useWallet } from '@/hooks/wallet/useWallet' +import { useWithdrawFlow } from '@/context/WithdrawFlowContext' +import { useState, useMemo, useContext, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { Button } from '@/components/0_Bruddle/Button' +import { Card } from '@/components/0_Bruddle/Card' +import NavHeader from '@/components/Global/NavHeader' +import ErrorAlert from '@/components/Global/ErrorAlert' +import { Icon } from '@/components/Global/Icons/Icon' +import PeanutLoading from '@/components/Global/PeanutLoading' +import { mantecaApi } from '@/services/manteca' +import { MANTECA_DEPOSIT_ADDRESS } from '@/constants/manteca.consts' +import { useCurrency } from '@/hooks/useCurrency' +import { isTxReverted } from '@/utils/general.utils' +import { loadingStateContext } from '@/context' +import { countryData } from '@/components/AddMoney/consts' +import Image from 'next/image' +import { formatAmount } from '@/utils' +import { validateCbuCvuAlias } from '@/utils/withdraw.utils' +import ValidatedInput from '@/components/Global/ValidatedInput' +import TokenAmountInput from '@/components/Global/TokenAmountInput' +import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { formatUnits, parseUnits } from 'viem' +import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' +import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' +import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' +import { useAuth } from '@/context/authContext' +import { useWebSocket } from '@/hooks/useWebSocket' + +type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' + +interface MantecaBankDetails { + destinationAddress: string +} + +const MAX_WITHDRAW_AMOUNT = '500' + +export default function MantecaWithdrawFlow() { + const [amount, setAmount] = useState(undefined) + const [currencyAmount, setCurrencyAmount] = useState(undefined) + const [usdAmount, setUsdAmount] = useState(undefined) + const [step, setStep] = useState('amountInput') + const [balanceErrorMessage, setBalanceErrorMessage] = useState(null) + const [bankDetails, setBankDetails] = useState({ destinationAddress: '' }) + const [errorMessage, setErrorMessage] = useState(null) + const [isKycModalOpen, setIsKycModalOpen] = useState(false) + const [isDestinationAddressValid, setIsDestinationAddressValid] = useState(false) + const [isDestinationAddressChanging, setIsDestinationAddressChanging] = useState(false) + const router = useRouter() + const searchParams = useSearchParams() + const { resetWithdrawFlow } = useWithdrawFlow() + const { sendMoney, balance } = useWallet() + const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) + const { user, fetchUser } = useAuth() + + // Get method and country from URL parameters + const selectedMethodType = searchParams.get('method') // mercadopago, pix, bank-transfer, etc. + const countryFromUrl = searchParams.get('country') // argentina, brazil, etc. + + // Determine country and currency from URL params or context + const countryPath = countryFromUrl || 'argentina' + + // Map country path to CountryData for KYC + const selectedCountry = useMemo(() => { + return countryData.find((country) => country.type === 'country' && country.path === countryPath) + }, [countryPath]) + + const { + code: currencyCode, + symbol: currencySymbol, + price: currencyPrice, + isLoading: isCurrencyLoading, + } = useCurrency(selectedCountry?.currency!) + + // Initialize KYC flow hook + const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry }) + + // WebSocket listener for KYC status updates + useWebSocket({ + username: user?.user.username ?? undefined, + autoConnect: !!user?.user.username, + onMantecaKycStatusUpdate: (newStatus) => { + if (newStatus === 'ACTIVE' || newStatus === 'WIDGET_FINISHED') { + fetchUser() + setIsKycModalOpen(false) + setStep('review') // Proceed to review after successful KYC + } + }, + }) + + // Get country flag code + const countryFlagCode = useMemo(() => { + return selectedCountry?.iso2?.toLowerCase() + }, [selectedCountry]) + + // Get method display info + const methodDisplayInfo = useMemo(() => { + const methodNames: { [key: string]: string } = { + mercadopago: 'MercadoPago', + pix: 'Pix', + 'bank-transfer': 'Bank Transfer', + } + + return { + name: methodNames[selectedMethodType || 'bank-transfer'] || 'Bank Transfer', + } + }, [selectedMethodType]) + + const validateDestinationAddress = async (value: string) => { + value = value.trim() + if (!value) { + return false + } + + let isValid = false + switch (countryPath) { + case 'argentina': + isValid = validateCbuCvuAlias(value) + default: + isValid = true + break + } + + return isValid + } + + const handleBankDetailsSubmit = () => { + if (!bankDetails.destinationAddress.trim()) { + setErrorMessage('Please enter your account address') + return + } + setErrorMessage(null) + + // Check if we still need to determine KYC status + if (isMantecaKycRequired === null) { + // still loading/determining KYC status, don't proceed yet + return + } + + // Check KYC status before proceeding to review + if (isMantecaKycRequired === true) { + setIsKycModalOpen(true) + return + } + + setStep('review') + } + + const handleWithdraw = async () => { + if (!bankDetails.destinationAddress || !usdAmount || !currencyCode) return + + try { + setLoadingState('Preparing transaction') + + // Send crypto to Manteca address + const { userOpHash, receipt } = await sendMoney(MANTECA_DEPOSIT_ADDRESS, usdAmount) + + if (receipt !== null && isTxReverted(receipt)) { + setErrorMessage('Transaction reverted by the network.') + return + } + + const txHash = receipt?.transactionHash ?? userOpHash + setLoadingState('Withdrawing') + + // Call Manteca withdraw API + const result = await mantecaApi.withdraw({ + amount: usdAmount, + destinationAddress: bankDetails.destinationAddress, + txHash: txHash, + currency: currencyCode, + }) + + if (result.error) { + setErrorMessage(result.error) + return + } + + setStep('success') + } catch (error) { + console.error('Manteca withdraw error:', error) + setErrorMessage('An unexpected error occurred. Please contact support.') + } finally { + setLoadingState('Idle') + } + } + + const resetState = () => { + setStep('amountInput') + setBankDetails({ destinationAddress: '' }) + setErrorMessage(null) + resetWithdrawFlow() + setBalanceErrorMessage(null) + } + + useEffect(() => { + resetState() + }, []) + + useEffect(() => { + if (!usdAmount || balance === undefined) { + setBalanceErrorMessage(null) + return + } + const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) + if (paymentAmount > parseUnits(MAX_WITHDRAW_AMOUNT, PEANUT_WALLET_TOKEN_DECIMALS)) { + setBalanceErrorMessage(`Withdraw amount exceeds maximum limit of $${MAX_WITHDRAW_AMOUNT}`) + } else if (paymentAmount > balance) { + setBalanceErrorMessage('Not enough balance to complete withdrawal.') + } else { + setBalanceErrorMessage(null) + } + }, [usdAmount, balance]) + + if (isCurrencyLoading || !currencyPrice) { + return + } + + if (step === 'success') { + return ( +
+ +
+ +
+
+ +
+
+
+

You just withdrew

+
+ {currencyCode} {currencyAmount} +
+
≈ ${usdAmount} USD
+

to {bankDetails.destinationAddress}

+
+
+
+ +
+
+
+ ) + } + + return ( +
+ { + if (step === 'review') { + setStep('bankDetails') + } else if (step === 'bankDetails') { + setStep('amountInput') + } else { + router.push('/withdraw') + } + }} + /> + + {step === 'amountInput' && ( +
+
Amount to withdraw
+ + + {balanceErrorMessage && } +
+ )} + + {step === 'bankDetails' && ( +
+ {/* Amount Display Card */} + +
+
+ {`flag`} +
+ +
+
+
+

+ You're withdrawing +

+

+ {currencyCode} {currencyAmount} +

+
+
+
+ + {/* Bank Details Form */} +
+

Enter {methodDisplayInfo.name} details

+ +
+ { + setBankDetails({ destinationAddress: update.value }) + setIsDestinationAddressValid(update.isValid) + setIsDestinationAddressChanging(update.isChanging) + }} + validate={validateDestinationAddress} + /> + +
+ + You can only withdraw to accounts under your name. +
+
+ + + + {errorMessage && } +
+ + {/* KYC Modal */} + {isKycModalOpen && selectedCountry && ( + setIsKycModalOpen(false)} + onManualClose={() => setIsKycModalOpen(false)} + onKycSuccess={() => { + setIsKycModalOpen(false) + fetchUser() + setStep('review') + }} + country={selectedCountry} + /> + )} +
+ )} + + {step === 'review' && ( +
+ +
+
+ {`flag`} +
+ +
+
+
+

+ You're withdrawing +

+

+ {currencyCode} {currencyAmount} +

+
+
+
+ {/* Review Summary */} + + + + + + + + {errorMessage && } +
+ )} +
+ ) +} diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx index 3dd1d4a42..b2767c9a1 100644 --- a/src/app/(mobile-ui)/withdraw/page.tsx +++ b/src/app/(mobile-ui)/withdraw/page.tsx @@ -13,6 +13,7 @@ import { formatAmount } from '@/utils' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useMemo, useState, useRef, useContext } from 'react' import { formatUnits } from 'viem' +import { isMantecaCountry } from '@/constants/manteca.consts' type WithdrawStep = 'inputAmount' | 'selectMethod' @@ -27,11 +28,12 @@ export default function WithdrawPage() { error, setUsdAmount, resetWithdrawFlow, + selectedMethod, + setSelectedMethod, } = useWithdrawFlow() - // choose the first screen: if an amount already exists we jump straight to the method list - const initialStep: WithdrawStep = - amountFromContext && parseFloat(amountFromContext) > 0 ? 'selectMethod' : 'inputAmount' + // FIXED FLOW: Only crypto gets amount input on main page, countries route directly + const initialStep: WithdrawStep = selectedMethod ? 'inputAmount' : 'selectMethod' const [step, setStep] = useState(initialStep) @@ -66,26 +68,20 @@ export default function WithdrawPage() { }, [setError, amountFromContext]) useEffect(() => { - if (!amountFromContext) { - resetWithdrawFlow() - } - }, [amountFromContext, resetWithdrawFlow]) - - useEffect(() => { - if (amountFromContext && parseFloat(amountFromContext) > 0) { - setStep('selectMethod') - if (!rawTokenAmount) { + if (selectedMethod) { + setStep('inputAmount') + if (amountFromContext && !rawTokenAmount) { setRawTokenAmount(amountFromContext) } } else { - setStep('inputAmount') - // clear the raw token amount when switching back to input - if (step !== 'inputAmount') { + setStep('selectMethod') + // clear the raw token amount when switching back to method selection + if (step !== 'selectMethod') { setRawTokenAmount('') setTokenInputKey((k) => k + 1) } } - }, [amountFromContext, step]) + }, [selectedMethod, amountFromContext, step, rawTokenAmount]) const validateAmount = useCallback( (amountStr: string): boolean => { @@ -170,12 +166,26 @@ export default function WithdrawPage() { }, [rawTokenAmount, validateAmount, setError, step]) const handleAmountContinue = () => { - if (validateAmount(rawTokenAmount)) { + if (validateAmount(rawTokenAmount) && selectedMethod) { const cleanedAmount = rawTokenAmount.replace(/,/g, '') setAmountToWithdraw(cleanedAmount) const usdVal = (selectedTokenData?.price ?? 1) * parseFloat(cleanedAmount) setUsdAmount(usdVal.toString()) - // the step will automatically change to 'selectMethod' via the useEffect above + + // Route based on selected method type + if (selectedMethod.type === 'crypto') { + router.push('/withdraw/crypto') + } else if (selectedMethod.type === 'manteca') { + // Route directly to Manteca with method and country params + const methodParam = selectedMethod.title?.toLowerCase().replace(/\s+/g, '-') || 'bank-transfer' + router.push(`/withdraw/manteca?method=${methodParam}&country=${selectedMethod.countryPath}`) + } else if (selectedMethod.type === 'bridge' && selectedMethod.countryPath) { + // Bridge countries go to country page for bank account form + router.push(`/withdraw/${selectedMethod.countryPath}`) + } else if (selectedMethod.countryPath) { + // Other countries go to their country pages + router.push(`/withdraw/${selectedMethod.countryPath}`) + } } } @@ -194,11 +204,23 @@ export default function WithdrawPage() { }, [rawTokenAmount, maxDecimalAmount, error.showError, selectedTokenData?.price]) if (step === 'inputAmount') { + const methodTitle = selectedMethod?.title || selectedMethod?.countryPath || 'Selected method' + return (
- router.push('/home')} /> + { + // Go back to method selection + setSelectedMethod(null) + setStep('selectMethod') + }} + />
-
Amount to withdraw
+
+
Amount to withdraw
+
Withdrawing via {methodTitle}
+
{ - resetWithdrawFlow() + router.push('/home') }} /> ) diff --git a/src/components/AddMoney/consts/index.ts b/src/components/AddMoney/consts/index.ts index 365c99736..a8661ddf6 100644 --- a/src/components/AddMoney/consts/index.ts +++ b/src/components/AddMoney/consts/index.ts @@ -231,11 +231,18 @@ export const DEFAULT_WITHDRAW_METHODS: SpecificPaymentMethod[] = [ const countrySpecificWithdrawMethods: Record< string, - Array<{ title: string; description: string; icon?: IconName | string }> + Array<{ title: string; description: string; icon?: IconName | string; isSoon?: boolean }> > = { 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.' }], - Argentina: [{ title: 'MercadoPago', description: 'Dominant wallet in LATAM, supports QR and bank transfers.' }], + Argentina: [ + { + title: 'Mercado Pago', + description: 'Instant transfers', + icon: MERCADO_PAGO, + isSoon: false, + }, + ], Mexico: [{ title: 'CoDi', description: 'Central bank-backed RTP, adoption growing.' }], Kenya: [{ title: 'M-Pesa', description: 'Over 90% penetration, also in Tanzania, Mozambique, etc.' }], Portugal: [{ title: 'MB WAY', description: 'Popular for QR payments, instant transfers.' }], @@ -2511,7 +2518,7 @@ export const countryCodeMap: { [key: string]: string } = { USA: 'US', } -const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX', 'AR']) +const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX', 'AR', 'BR', 'BO']) // Helper function to check if a country code is enabled for bank transfers // Handles both 2-letter and 3-letter country codes @@ -2537,12 +2544,31 @@ countryData.forEach((country) => { const specificMethodDetails = countrySpecificWithdrawMethods[countryTitle] if (specificMethodDetails && specificMethodDetails.length > 0) { specificMethodDetails.forEach((method) => { + const methodId = `${countryCode.toLowerCase()}-${method.title.toLowerCase().replace(/\s+/g, '-')}-withdraw` + + // Check if this is a Manteca country to add appropriate routing + const isMantecaCountry = [ + 'argentina', + 'chile', + 'brazil', + 'colombia', + 'panama', + 'costa-rica', + 'guatemala', + 'philippines', + 'bolivia', + ].includes(country.path) + withdrawList.push({ - id: `${countryCode.toLowerCase()}-${method.title.toLowerCase().replace(/\s+/g, '-')}-withdraw`, + id: methodId, icon: method.icon ?? undefined, title: method.title, description: method.description, - isSoon: true, + isSoon: method.isSoon ?? true, + // Add path for Manteca countries to route to Manteca flow + path: isMantecaCountry + ? `/withdraw/manteca?method=${method.title.toLowerCase().replace(/\s+/g, '-')}&country=${country.path}` + : undefined, }) }) } @@ -2574,10 +2600,24 @@ countryData.forEach((country) => { // only add default bank if it doesn't already exist AND (SEPA was not added OR it's not considered redundant by SEPA) // for now, we simplify: if SEPA was added, we assume default bank is redundant. if (!genericBankExists && !sepaWasAdded) { + const isMantecaCountry = [ + 'argentina', + 'chile', + 'brazil', + 'colombia', + 'panama', + 'costa-rica', + 'guatemala', + 'philippines', + 'bolivia', + ].includes(country.path) + withdrawList.push({ ...DEFAULT_BANK_WITHDRAW_METHOD, id: `${countryCode.toLowerCase()}-default-bank-withdraw`, - path: `/withdraw/${countryCode.toLowerCase()}/bank`, + path: isMantecaCountry + ? `/withdraw/manteca?method=bank-transfer&country=${country.path}` + : `/withdraw/${countryCode.toLowerCase()}/bank`, isSoon: !isCountryEnabledForBankTransfer(countryCode), }) } diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index e67b73bc2..8ecaad753 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -21,6 +21,7 @@ import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { Account } from '@/interfaces' import PeanutLoading from '../Global/PeanutLoading' import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils' +import { isMantecaCountry } from '@/constants/manteca.consts' import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' import CryptoMethodDrawer from '../AddMoney/components/CryptoMethodDrawer' import { useAppDispatch } from '@/redux/hooks' @@ -38,11 +39,11 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { // hooks const { deviceType } = useDeviceType() const { user, fetchUser } = useAuth() - const { setSelectedBankAccount, amountToWithdraw } = useWithdrawFlow() + const { setSelectedBankAccount, amountToWithdraw, setSelectedMethod } = useWithdrawFlow() const dispatch = useAppDispatch() // component level states - const [view, setView] = useState<'list' | 'form'>('list') + const [view, setView] = useState<'list' | 'form'>(flow === 'withdraw' && amountToWithdraw ? 'form' : 'list') const [isKycModalOpen, setIsKycModalOpen] = useState(false) const formRef = useRef<{ handleSubmit: () => void }>(null) const [liveKycStatus, setLiveKycStatus] = useState( @@ -161,23 +162,31 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { } const handleWithdrawMethodClick = (method: SpecificPaymentMethod) => { - if (method.id.includes('default-bank-withdraw') || method.id.includes('sepa-instant-withdraw')) { - setView('form') - } else if (method.path) { + if (method.path && method.path.includes('/manteca')) { + // Manteca methods route directly (has own amount input) router.push(method.path) - } - } - - useEffect(() => { - if (flow !== 'withdraw') { - return - } - if (!amountToWithdraw) { - console.error('Amount not available in WithdrawFlowContext for withdrawal, redirecting.') + } else if (method.id.includes('default-bank-withdraw') || method.id.includes('sepa-instant-withdraw')) { + // Bridge methods: Set in context and navigate for amount input + setSelectedMethod({ + type: 'bridge', + countryPath: currentCountry?.path, + currency: currentCountry?.currency, + title: method.title, + }) router.push('/withdraw') return + } else if (method.id.includes('crypto-withdraw')) { + setSelectedMethod({ + type: 'crypto', + countryPath: 'crypto', + title: 'Crypto', + }) + router.push('/withdraw') + } else if (method.path) { + // Other methods with paths + router.push(method.path) } - }, [amountToWithdraw, router, flow]) + } const methods = useMemo(() => { if (!currentCountry) return undefined @@ -207,14 +216,6 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { } }, [currentCountry, flow, deviceType]) - if (!amountToWithdraw && flow === 'withdraw') { - return ( -
- -
- ) - } - if (!currentCountry) { return (
diff --git a/src/components/AddWithdraw/AddWithdrawRouterView.tsx b/src/components/AddWithdraw/AddWithdrawRouterView.tsx index 189747014..878848649 100644 --- a/src/components/AddWithdraw/AddWithdrawRouterView.tsx +++ b/src/components/AddWithdraw/AddWithdrawRouterView.tsx @@ -9,6 +9,7 @@ import { useUserStore } from '@/redux/hooks' import { AccountType, Account } from '@/interfaces' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useOnrampFlow } from '@/context/OnrampFlowContext' +import { isMantecaCountry } from '@/constants/manteca.consts' import Card from '@/components/Global/Card' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { CountryList } from '../Common/CountryList' @@ -33,7 +34,8 @@ export const AddWithdrawRouterView: FC = ({ }) => { const router = useRouter() const { user } = useUserStore() - const { setSelectedBankAccount, showAllWithdrawMethods, setShowAllWithdrawMethods } = useWithdrawFlow() + const { setSelectedBankAccount, showAllWithdrawMethods, setShowAllWithdrawMethods, setSelectedMethod } = + useWithdrawFlow() const onrampFlowContext = useOnrampFlow() const { setFromBankSelected } = onrampFlowContext const [recentMethodsState, setRecentMethodsState] = useState([]) @@ -90,6 +92,23 @@ export const AddWithdrawRouterView: FC = ({ return } + // NEW: For withdraw flow, set selected method in context instead of navigating + if (flow === 'withdraw') { + const methodType = + method.type === 'crypto' ? 'crypto' : isMantecaCountry(method.path) ? 'manteca' : 'bridge' + + setSelectedMethod({ + type: methodType, + countryPath: method.path, + currency: method.currency, + title: method.title, + }) + + // Don't navigate - let the main withdraw page handle the flow + return + } + + // Original add flow logic const newRecentMethod: RecentMethod = { id: method.id, type: method.type as 'crypto' | 'country', @@ -168,6 +187,14 @@ export const AddWithdrawRouterView: FC = ({ savedAccounts={savedAccounts} onAccountClick={(account, path) => { setSelectedBankAccount(account) + + // FIXED: For withdraw flow, route to saved account path + if (flow === 'withdraw') { + router.push(path) + return + } + + // Original add flow router.push(path) }} onSelectNewMethodClick={() => setShouldShowAllMethods(true)} @@ -233,6 +260,14 @@ export const AddWithdrawRouterView: FC = ({ inputTitle={mainHeading} viewMode="add-withdraw" onCountryClick={(country) => { + if (flow === 'withdraw') { + // FIXED: Route directly to country page for method selection + const countryPath = `${baseRoute}/${country.path}` + router.push(countryPath) + return + } + + // Original add flow const countryPath = `${baseRoute}/${country.path}` router.push(countryPath) }} @@ -240,8 +275,13 @@ export const AddWithdrawRouterView: FC = ({ if (flow === 'add') { setIsDrawerOpen(true) } else { - const cryptoPath = `${baseRoute}/crypto` - router.push(cryptoPath) + // Set crypto method and navigate to main page for amount input + setSelectedMethod({ + type: 'crypto', + countryPath: 'crypto', + title: 'Crypto', + }) + router.push('/withdraw') } }} flow={flow} diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index ed65cd4e9..fc491cdd6 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -248,6 +248,7 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact isPeerActuallyUser = false break case EHistoryEntryType.BRIDGE_OFFRAMP: + case EHistoryEntryType.MANTECA_OFFRAMP: direction = 'bank_withdraw' transactionCardType = 'bank_withdraw' nameForDetails = 'Bank Account' diff --git a/src/constants/loadingStates.consts.ts b/src/constants/loadingStates.consts.ts index ee502f0c5..e0cd66b68 100644 --- a/src/constants/loadingStates.consts.ts +++ b/src/constants/loadingStates.consts.ts @@ -27,3 +27,4 @@ export type LoadingStates = | 'Logging in' | 'Logging out' | 'Paying' + | 'Withdrawing' diff --git a/src/constants/manteca.consts.ts b/src/constants/manteca.consts.ts index 9330c63dd..ab2fb17ff 100644 --- a/src/constants/manteca.consts.ts +++ b/src/constants/manteca.consts.ts @@ -1 +1,23 @@ export const MANTECA_DEPOSIT_ADDRESS = '0x959e088a09f61aB01cb83b0eBCc74b2CF6d62053' + +// Countries that use Manteca for bank withdrawals instead of Bridge +export const MANTECA_COUNTRIES = [ + 'argentina', // ARS, USD, BRL (QR pix payments) + 'chile', // CLP + 'brazil', // BRL + 'colombia', // COP + 'panama', // PUSD + 'costa-rica', // CRC + 'guatemala', // GTQ + // 'mexico', // MXN - Keep as Bridge (CoDi disabled) + 'philippines', // PHP + 'bolivia', // BOB +] as const + +// Type for Manteca countries +export type MantecaCountry = (typeof MANTECA_COUNTRIES)[number] + +// Helper function to check if a country uses Manteca +export const isMantecaCountry = (countryPath: string): boolean => { + return MANTECA_COUNTRIES.includes(countryPath as MantecaCountry) +} diff --git a/src/context/WithdrawFlowContext.tsx b/src/context/WithdrawFlowContext.tsx index 2841df2f3..7b9ad71ce 100644 --- a/src/context/WithdrawFlowContext.tsx +++ b/src/context/WithdrawFlowContext.tsx @@ -4,6 +4,15 @@ import { ITokenPriceData, Account } from '@/interfaces' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import React, { createContext, ReactNode, useContext, useMemo, useState, useCallback } from 'react' +export interface WithdrawMethod { + type: 'bridge' | 'manteca' | 'crypto' + countryPath?: string + currency?: string + minimumAmount?: number + savedAccount?: Account + title?: string +} + export type WithdrawView = 'INITIAL' | 'CONFIRM' | 'STATUS' export interface WithdrawData { @@ -50,6 +59,8 @@ interface WithdrawFlowContextType { setSelectedBankAccount: (account: Account | null) => void showAllWithdrawMethods: boolean setShowAllWithdrawMethods: (show: boolean) => void + selectedMethod: WithdrawMethod | null + setSelectedMethod: (method: WithdrawMethod | null) => void resetWithdrawFlow: () => void } @@ -73,6 +84,7 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ }) const [selectedBankAccount, setSelectedBankAccount] = useState(null) const [showAllWithdrawMethods, setShowAllWithdrawMethods] = useState(false) + const [selectedMethod, setSelectedMethod] = useState(null) const resetWithdrawFlow = useCallback(() => { setAmountToWithdraw('') @@ -84,6 +96,7 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ setPaymentError(null) setShowAllWithdrawMethods(false) setUsdAmount('') + setSelectedMethod(null) }, []) const value = useMemo( @@ -114,6 +127,8 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ setSelectedBankAccount, showAllWithdrawMethods, setShowAllWithdrawMethods, + selectedMethod, + setSelectedMethod, resetWithdrawFlow, }), [ @@ -130,6 +145,7 @@ export const WithdrawFlowContextProvider: React.FC<{ children: ReactNode }> = ({ error, selectedBankAccount, showAllWithdrawMethods, + selectedMethod, setShowAllWithdrawMethods, resetWithdrawFlow, ] diff --git a/src/hooks/useTransactionHistory.ts b/src/hooks/useTransactionHistory.ts index 5a6229727..68f17dac2 100644 --- a/src/hooks/useTransactionHistory.ts +++ b/src/hooks/useTransactionHistory.ts @@ -20,6 +20,7 @@ export enum EHistoryEntryType { BRIDGE_ONRAMP = 'BRIDGE_ONRAMP', BANK_SEND_LINK_CLAIM = 'BANK_SEND_LINK_CLAIM', MANTECA_QR_PAYMENT = 'MANTECA_QR_PAYMENT', + MANTECA_OFFRAMP = 'MANTECA_OFFRAMP', } export enum EHistoryUserRole { diff --git a/src/utils/withdraw.utils.ts b/src/utils/withdraw.utils.ts index 16fb4d27a..53bfe0de7 100644 --- a/src/utils/withdraw.utils.ts +++ b/src/utils/withdraw.utils.ts @@ -145,3 +145,7 @@ export const getCountryCodeForWithdraw = (country: string) => { return threeDigitCode || country } + +export function validateCbuCvuAlias(value: string): boolean { + return true +}