From db05011226157762df089e92e302f88ff356ff26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Fri, 19 Sep 2025 17:38:13 -0300 Subject: [PATCH 1/5] feat(manteca-withdraw): allow refund on case of withdrawal failure --- src/app/(mobile-ui)/qr-pay/page.tsx | 1 + src/app/(mobile-ui)/withdraw/manteca/page.tsx | 68 ++++++++++++++----- src/app/actions/manteca.ts | 46 ------------- .../Claim/Link/MantecaFlowManager.tsx | 2 +- .../Claim/Link/views/MantecaReviewStep.tsx | 14 +--- src/types/manteca.types.ts | 2 +- 6 files changed, 57 insertions(+), 76 deletions(-) delete mode 100644 src/app/actions/manteca.ts diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 31c6c8a4a..b8987b9bf 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -334,6 +334,7 @@ export default function QRPayPage() { id: qrPayment!.id, direction: 'qr_payment', userName: qrPayment!.details.merchant.name, + fullName: qrPayment!.details.merchant.name, amount: Number(usdAmount), currency: { amount: qrPayment!.details.paymentAssetAmount, diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index ce4ee47e8..cba517dc4 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -11,9 +11,7 @@ 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' @@ -28,8 +26,10 @@ import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' +import { useCreateLink } from '@/components/Create/useCreateLink' +import { useSupportModalContext } from '@/context/SupportModalContext' -type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' +type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure' interface MantecaBankDetails { destinationAddress: string @@ -51,9 +51,11 @@ export default function MantecaWithdrawFlow() { const router = useRouter() const searchParams = useSearchParams() const { resetWithdrawFlow } = useWithdrawFlow() - const { sendMoney, balance } = useWallet() + const { balance } = useWallet() + const { createLink } = useCreateLink() 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. @@ -159,34 +161,32 @@ export default function MantecaWithdrawFlow() { try { setLoadingState('Preparing transaction') - // Send crypto to Manteca address - const { userOpHash, receipt } = await sendMoney(MANTECA_DEPOSIT_ADDRESS, usdAmount) + const { link } = await createLink(parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS)) - 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, + sendLink: link, currency: currencyCode, }) if (result.error) { - setErrorMessage(result.error) + setErrorMessage( + 'Withdraw cancelled, please check that the destination address is correct. If problem persists contact support' + ) + setStep('failure') return } setStep('success') } catch (error) { console.error('Manteca withdraw error:', error) - setErrorMessage('An unexpected error occurred. Please contact support.') + setErrorMessage( + 'Withdraw cancelled, please check that the destination address is correct. If problem persists contact support' + ) + setStep('failure') } finally { setLoadingState('Idle') } @@ -194,10 +194,16 @@ export default function MantecaWithdrawFlow() { const resetState = () => { setStep('amountInput') + setAmount(undefined) + setCurrencyAmount(undefined) + setUsdAmount(undefined) setBankDetails({ destinationAddress: '' }) setErrorMessage(null) - resetWithdrawFlow() + setIsKycModalOpen(false) + setIsDestinationAddressValid(false) + setIsDestinationAddressChanging(false) setBalanceErrorMessage(null) + resetWithdrawFlow() } useEffect(() => { @@ -259,6 +265,34 @@ export default function MantecaWithdrawFlow() { ) } + if (step === 'failure') { + return ( +
+ +
+ + + Something went wrong! + {errorMessage} + + + + + + +
+
+ ) + } + return (
{ - const apiUrl = process.env.PEANUT_API_URL - const cookieStore = cookies() - const jwtToken = (await cookieStore).get('jwt-token')?.value - - if (!apiUrl || !API_KEY) { - console.error('API URL or API Key is not configured.') - return { error: 'Server configuration error.' } - } - - try { - const response = await fetchWithSentry(`${apiUrl}/manteca/withdraw`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwtToken}`, - 'api-key': API_KEY, - }, - body: JSON.stringify(params), - }) - - const data = await response.json() - - if (!response.ok) { - console.log('error', response) - return { error: data.error || 'Failed to create manteca withdraw.' } - } - - return { data } - } catch (error) { - console.log('error', error) - console.error('Error calling create manteca withdraw API:', error) - if (error instanceof Error) { - return { error: error.message } - } - return { error: 'An unexpected error occurred.' } - } -} diff --git a/src/components/Claim/Link/MantecaFlowManager.tsx b/src/components/Claim/Link/MantecaFlowManager.tsx index e412ab5a7..0efdf5a24 100644 --- a/src/components/Claim/Link/MantecaFlowManager.tsx +++ b/src/components/Claim/Link/MantecaFlowManager.tsx @@ -71,7 +71,7 @@ const MantecaFlowManager: FC = ({ claimLinkData, amount return ( > - claimLink: string + sendLink: string destinationAddress: string amount: string currency: string @@ -19,7 +17,7 @@ interface MantecaReviewStepProps { const MantecaReviewStep: FC = ({ setCurrentStep, - claimLink, + sendLink, destinationAddress, amount, currency, @@ -52,16 +50,10 @@ const MantecaReviewStep: FC = ({ try { setError(null) setIsSubmitting(true) - const claimResponse = await sendLinksApi.claim(MANTECA_DEPOSIT_ADDRESS, claimLink) - const txHash = claimResponse?.claim?.txHash - if (!txHash) { - setError('Claim failed: missing transaction hash.') - return - } const { data, error: withdrawError } = await mantecaApi.withdraw({ amount: amount.replace(/,/g, ''), destinationAddress, - txHash, + sendLink, currency, }) if (withdrawError || !data) { diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 7ac881617..c10074498 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -13,7 +13,7 @@ export enum MercadoPagoStep { export type MantecaWithdrawData = { amount: string destinationAddress: string - txHash: string + sendLink: string currency: string } From 9c6c6decf9ccfb28d1034e92916be217a536ddd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 22 Sep 2025 16:36:04 -0300 Subject: [PATCH 2/5] feat: enable withdraw for brazil and bolivia --- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 88 ++++++++++++++----- src/components/AddMoney/consts/index.ts | 6 +- src/components/Global/Select/index.tsx | 38 ++++---- .../Global/ValidatedInput/index.tsx | 4 +- src/components/Refund/index.tsx | 11 ++- src/constants/manteca.consts.ts | 79 +++++++++++++++++ src/types/manteca.types.ts | 4 + 7 files changed, 182 insertions(+), 48 deletions(-) diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index cba517dc4..0c087a472 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,13 +28,11 @@ import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' import { useCreateLink } from '@/components/Create/useCreateLink' 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' | 'failure' -interface MantecaBankDetails { - destinationAddress: string -} - const MAX_WITHDRAW_AMOUNT = '500' export default function MantecaWithdrawFlow() { @@ -43,7 +41,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) @@ -69,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, @@ -133,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 @@ -153,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') @@ -167,7 +184,9 @@ export default function MantecaWithdrawFlow() { // Call Manteca withdraw API const result = await mantecaApi.withdraw({ amount: usdAmount, - destinationAddress: bankDetails.destinationAddress, + destinationAddress, + bankCode: selectedBank?.code, + accountType: accountType ?? undefined, sendLink: link, currency: currencyCode, }) @@ -197,7 +216,9 @@ export default function MantecaWithdrawFlow() { setAmount(undefined) setCurrencyAmount(undefined) setUsdAmount(undefined) - setBankDetails({ destinationAddress: '' }) + setDestinationAddress('') + setSelectedBank(null) + setAccountType(null) setErrorMessage(null) setIsKycModalOpen(false) setIsDestinationAddressValid(false) @@ -225,7 +246,7 @@ export default function MantecaWithdrawFlow() { } }, [usdAmount, balance]) - if (isCurrencyLoading || !currencyPrice) { + if (isCurrencyLoading || !currencyPrice || !selectedCountry) { return } @@ -246,7 +267,7 @@ export default function MantecaWithdrawFlow() { {currencyCode} {currencyAmount}
≈ ${usdAmount} USD
-

to {bankDetails.destinationAddress}

+

to {destinationAddress}

@@ -366,6 +387,7 @@ export default function MantecaWithdrawFlow() {

{currencyCode} {currencyAmount}

+
≈ {usdAmount} USD
@@ -376,10 +398,10 @@ export default function MantecaWithdrawFlow() {
{ - setBankDetails({ destinationAddress: update.value }) + setDestinationAddress(update.value) setIsDestinationAddressValid(update.isValid) setIsDestinationAddressChanging(update.isChanging) if (update.isValid || update.value === '') { @@ -388,6 +410,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" + /> + )}
@@ -398,9 +445,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..d00427b20 100644 --- a/src/constants/manteca.consts.ts +++ b/src/constants/manteca.consts.ts @@ -21,3 +21,82 @@ 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 + } +) + +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 c10074498..99428c02a 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 sendLink: string currency: string } From 50af8da1d43f85746d0ceda0b7d9952bebddd627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Tue, 23 Sep 2025 09:47:59 -0300 Subject: [PATCH 3/5] Revert "feat(manteca-withdraw): allow refund on case of withdrawal failure" This reverts commit db05011226157762df089e92e302f88ff356ff26. --- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 23 ++++++---- src/app/actions/manteca.ts | 46 +++++++++++++++++++ .../Claim/Link/MantecaFlowManager.tsx | 2 +- .../Claim/Link/views/MantecaReviewStep.tsx | 14 ++++-- src/types/manteca.types.ts | 2 +- 5 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 src/app/actions/manteca.ts diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index 0c087a472..bd6925a44 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -11,7 +11,9 @@ 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' @@ -26,7 +28,6 @@ import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' -import { useCreateLink } from '@/components/Create/useCreateLink' import { useSupportModalContext } from '@/context/SupportModalContext' import { MantecaAccountType, MANTECA_COUNTRIES_CONFIG, MantecaBankCode } from '@/constants/manteca.consts' import Select from '@/components/Global/Select' @@ -51,8 +52,7 @@ export default function MantecaWithdrawFlow() { const router = useRouter() const searchParams = useSearchParams() const { resetWithdrawFlow } = useWithdrawFlow() - const { balance } = useWallet() - const { createLink } = useCreateLink() + const { sendMoney, balance } = useWallet() const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) const { user, fetchUser } = useAuth() const { setIsSupportModalOpen } = useSupportModalContext() @@ -178,16 +178,24 @@ export default function MantecaWithdrawFlow() { try { setLoadingState('Preparing transaction') - const { link } = await createLink(parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS)) + // 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, bankCode: selectedBank?.code, accountType: accountType ?? undefined, - sendLink: link, + txHash, currency: currencyCode, }) @@ -223,8 +231,8 @@ export default function MantecaWithdrawFlow() { setIsKycModalOpen(false) setIsDestinationAddressValid(false) setIsDestinationAddressChanging(false) - setBalanceErrorMessage(null) resetWithdrawFlow() + setBalanceErrorMessage(null) } useEffect(() => { @@ -313,7 +321,6 @@ export default function MantecaWithdrawFlow() {
) } - return (
{/* Review Summary */} - + { + const apiUrl = process.env.PEANUT_API_URL + const cookieStore = cookies() + const jwtToken = (await cookieStore).get('jwt-token')?.value + + if (!apiUrl || !API_KEY) { + console.error('API URL or API Key is not configured.') + return { error: 'Server configuration error.' } + } + + try { + const response = await fetchWithSentry(`${apiUrl}/manteca/withdraw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwtToken}`, + 'api-key': API_KEY, + }, + body: JSON.stringify(params), + }) + + const data = await response.json() + + if (!response.ok) { + console.log('error', response) + return { error: data.error || 'Failed to create manteca withdraw.' } + } + + return { data } + } catch (error) { + console.log('error', error) + console.error('Error calling create manteca withdraw API:', error) + if (error instanceof Error) { + return { error: error.message } + } + return { error: 'An unexpected error occurred.' } + } +} diff --git a/src/components/Claim/Link/MantecaFlowManager.tsx b/src/components/Claim/Link/MantecaFlowManager.tsx index 0efdf5a24..e412ab5a7 100644 --- a/src/components/Claim/Link/MantecaFlowManager.tsx +++ b/src/components/Claim/Link/MantecaFlowManager.tsx @@ -71,7 +71,7 @@ const MantecaFlowManager: FC = ({ claimLinkData, amount return ( > - sendLink: string + claimLink: string destinationAddress: string amount: string currency: string @@ -17,7 +19,7 @@ interface MantecaReviewStepProps { const MantecaReviewStep: FC = ({ setCurrentStep, - sendLink, + claimLink, destinationAddress, amount, currency, @@ -50,10 +52,16 @@ const MantecaReviewStep: FC = ({ try { setError(null) setIsSubmitting(true) + const claimResponse = await sendLinksApi.claim(MANTECA_DEPOSIT_ADDRESS, claimLink) + const txHash = claimResponse?.claim?.txHash + if (!txHash) { + setError('Claim failed: missing transaction hash.') + return + } const { data, error: withdrawError } = await mantecaApi.withdraw({ amount: amount.replace(/,/g, ''), destinationAddress, - sendLink, + txHash, currency, }) if (withdrawError || !data) { diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 99428c02a..5c86646e8 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -17,7 +17,7 @@ export type MantecaWithdrawData = { destinationAddress: string bankCode?: string accountType?: MantecaAccountType - sendLink: string + txHash: string currency: string } From 4912aaad3c483ac81cbdcfd9fbca7895086f8f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Tue, 23 Sep 2025 10:00:03 -0300 Subject: [PATCH 4/5] refactor: better error messaging for manteca withdraw --- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 14 +++++++------- src/types/manteca.types.ts | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index bd6925a44..e4535d3c3 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -200,19 +200,19 @@ export default function MantecaWithdrawFlow() { }) if (result.error) { - setErrorMessage( - 'Withdraw cancelled, please check that the destination address is correct. If problem persists contact support' - ) - setStep('failure') + 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( - 'Withdraw cancelled, please check that the destination address is correct. If problem persists contact support' - ) + setErrorMessage('Withdraw failed unexpectedly. If problem persists contact support') setStep('failure') } finally { setLoadingState('Idle') diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 5c86646e8..9bf4227ec 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -70,6 +70,7 @@ export type MantecaWithdrawResponseData = { export type MantecaWithdrawResponse = { data?: MantecaWithdrawResponseData error?: string + message?: string } export interface CreateMantecaOnrampParams { From 0c3ca7cef38abbeaf0f89f1a09937cc42f13ddde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Tue, 23 Sep 2025 12:08:59 -0300 Subject: [PATCH 5/5] docs: documment manteca country config --- src/constants/manteca.consts.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/constants/manteca.consts.ts b/src/constants/manteca.consts.ts index d00427b20..6da815ce5 100644 --- a/src/constants/manteca.consts.ts +++ b/src/constants/manteca.consts.ts @@ -51,6 +51,15 @@ type MantecaCountryConfig = { } ) +/** + * 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',