Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 112 additions & 25 deletions src/app/(mobile-ui)/withdraw/manteca/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand All @@ -43,7 +42,9 @@ export default function MantecaWithdrawFlow() {
const [usdAmount, setUsdAmount] = useState<string | undefined>(undefined)
const [step, setStep] = useState<MantecaWithdrawStep>('amountInput')
const [balanceErrorMessage, setBalanceErrorMessage] = useState<string | null>(null)
const [bankDetails, setBankDetails] = useState<MantecaBankDetails>({ destinationAddress: '' })
const [destinationAddress, setDestinationAddress] = useState<string>('')
const [selectedBank, setSelectedBank] = useState<MantecaBankCode | null>(null)
const [accountType, setAccountType] = useState<MantecaAccountType | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const [isDestinationAddressValid, setIsDestinationAddressValid] = useState(false)
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -131,11 +138,23 @@ export default function MantecaWithdrawFlow() {
return isValid
}

const handleBankDetailsSubmit = () => {
if (!bankDetails.destinationAddress.trim()) {
const isCompleteBankDetails = useMemo<boolean>(() => {
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
Expand All @@ -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')
Expand All @@ -173,29 +192,45 @@ 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')
}
}

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)
}
Expand All @@ -219,7 +254,7 @@ export default function MantecaWithdrawFlow() {
}
}, [usdAmount, balance])

if (isCurrencyLoading || !currencyPrice) {
if (isCurrencyLoading || !currencyPrice || !selectedCountry) {
return <PeanutLoading />
}

Expand All @@ -240,7 +275,7 @@ export default function MantecaWithdrawFlow() {
{currencyCode} {currencyAmount}
</div>
<div className="text-lg font-bold">≈ ${usdAmount} USD</div>
<h1 className="text-sm font-normal text-grey-1">to {bankDetails.destinationAddress}</h1>
<h1 className="text-sm font-normal text-grey-1">to {destinationAddress}</h1>
</div>
</Card>
<div className="w-full space-y-5">
Expand All @@ -259,6 +294,33 @@ export default function MantecaWithdrawFlow() {
)
}

if (step === 'failure') {
return (
<div className="flex min-h-[inherit] flex-col gap-8">
<NavHeader title="Withdraw" />
<div className="my-auto flex h-full flex-col justify-center space-y-4">
<Card className="shadow-4">
<Card.Header>
<Card.Title>Something went wrong!</Card.Title>
<Card.Description>{errorMessage}</Card.Description>
</Card.Header>
<Card.Content className="flex flex-col gap-3">
<Button onClick={resetState} variant="purple">
Try again
</Button>
<Button
onClick={() => setIsSupportModalOpen(true)}
variant="transparent"
className="text-sm underline"
>
Contact Support
</Button>
</Card.Content>
</Card>
</div>
</div>
)
}
return (
<div className="flex min-h-[inherit] flex-col gap-8">
<NavHeader
Expand Down Expand Up @@ -332,6 +394,7 @@ export default function MantecaWithdrawFlow() {
<p className="text-2xl font-bold">
{currencyCode} {currencyAmount}
</p>
<div className="text-lg font-bold">≈ {usdAmount} USD</div>
</div>
</div>
</Card>
Expand All @@ -342,10 +405,10 @@ export default function MantecaWithdrawFlow() {

<div className="space-y-2">
<ValidatedInput
value={bankDetails.destinationAddress}
placeholder={countryPath === 'argentina' ? 'CBU, CVU or Alias' : 'Account Address'}
value={destinationAddress}
placeholder={countryConfig!.accountNumberLabel}
onUpdate={(update) => {
setBankDetails({ destinationAddress: update.value })
setDestinationAddress(update.value)
setIsDestinationAddressValid(update.isValid)
setIsDestinationAddressChanging(update.isChanging)
if (update.isValid || update.value === '') {
Expand All @@ -354,6 +417,31 @@ export default function MantecaWithdrawFlow() {
}}
validate={validateDestinationAddress}
/>
{countryConfig?.needsAccountType && (
<Select
value={accountType ? { id: accountType, title: accountType } : null}
onChange={(item) => {
setAccountType(MantecaAccountType[item.id as keyof typeof MantecaAccountType])
}}
items={countryConfig.validAccountTypes.map((type) => ({ id: type, title: type }))}
placeholder="Select account type"
className="w-full"
/>
)}
{countryConfig?.needsBankCode && (
<Select
value={selectedBank ? { id: selectedBank.code, title: selectedBank.name } : null}
onChange={(item) => {
setSelectedBank({ code: item.id, name: item.title })
}}
items={countryConfig.validBankCodes.map((bank) => ({
id: bank.code,
title: bank.name,
}))}
placeholder="Select bank"
className="w-full"
/>
)}

<div className="flex items-center gap-2 text-sm text-gray-600">
<Icon name="info" size={16} />
Expand All @@ -364,9 +452,7 @@ export default function MantecaWithdrawFlow() {
<Button
onClick={handleBankDetailsSubmit}
disabled={
!bankDetails.destinationAddress.trim() ||
isDestinationAddressChanging ||
!isDestinationAddressValid
!isCompleteBankDetails || isDestinationAddressChanging || !isDestinationAddressValid
}
loading={isDestinationAddressChanging}
className="w-full"
Expand Down Expand Up @@ -418,12 +504,13 @@ export default function MantecaWithdrawFlow() {
<p className="text-2xl font-bold">
{currencyCode} {currencyAmount}
</p>
<div className="text-lg font-bold">≈ {usdAmount} USD</div>
</div>
</div>
</Card>
{/* Review Summary */}
<Card className="space-y-0 px-4">
<PaymentInfoRow label="CBU, CVU or Alias" value={bankDetails.destinationAddress} />
<PaymentInfoRow label={countryConfig!.accountNumberLabel} value={destinationAddress} />
<PaymentInfoRow
label="Exchange Rate"
value={`1 USD = ${currencyPrice!.sell} ${currencyCode!.toUpperCase()}`}
Expand Down
6 changes: 3 additions & 3 deletions src/components/AddMoney/consts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { APPLE_PAY, GOOGLE_PAY, MERCADO_PAGO, SOLANA_ICON, TRON_ICON } from '@/assets'
import { APPLE_PAY, GOOGLE_PAY, MERCADO_PAGO, SOLANA_ICON, TRON_ICON, PIX } from '@/assets'
import { BINANCE_LOGO, LEMON_LOGO, RIPIO_LOGO } from '@/assets/exchanges'
import { METAMASK_LOGO, RAINBOW_LOGO, TRUST_WALLET_LOGO } from '@/assets/wallets'
import { IconName } from '@/components/Global/Icons/Icon'
Expand Down Expand Up @@ -234,7 +234,7 @@ const countrySpecificWithdrawMethods: Record<
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.' }],
Brazil: [{ title: 'Pix', description: 'Instant transfers', icon: PIX, isSoon: false }],
Argentina: [
{
title: 'Mercado Pago',
Expand Down Expand Up @@ -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
Expand Down
38 changes: 20 additions & 18 deletions src/components/Global/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -33,7 +38,6 @@ const Select = ({
onChange,
up,
small,
classPlaceholder,
}: SelectProps) => {
const buttonRef = useRef<HTMLButtonElement>(null)

Expand All @@ -46,21 +50,19 @@ const Select = ({
<Listbox.Button
ref={buttonRef}
className={twMerge(
`border-rounded flex h-16 w-full items-center bg-white px-5 text-sm font-bold text-n-1 outline-none transition-colors tap-highlight-color dark:bg-n-1 dark:text-white ${
`notranslate flex h-12 w-full justify-between rounded-sm border border-n-1 bg-white text-sm ${
small ? 'h-6 px-4 text-xs' : ''
} ${open ? 'border-primary-1 dark:border-primary-1' : ''} ${classButton}`
)}
>
<span className="mr-auto truncate text-black">
{value ? (
value
) : (
<span className={` dark:text-white/75 ${classPlaceholder}`}>{placeholder}</span>
)}
</span>
{value ? (
<span className="my-auto ml-4 text-black">{value.title}</span>
) : (
<span className="text-gray my-auto ml-4">{placeholder}</span>
)}
<Icon
className={twMerge(
`icon-20 -mr-0.5 ml-6 shrink-0 transition-transform dark:fill-white ${
`icon-20 my-auto mr-2 shrink-0 transition-transform dark:fill-white ${
small ? '-mr-2 ml-2' : ''
} ${open ? 'rotate-180' : ''} ${classArrow}`
)}
Expand All @@ -73,7 +75,7 @@ const Select = ({
<Transition leave="transition duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
<Listbox.Options
className={twMerge(
`absolute left-0 right-0 mt-1 w-full rounded-md border-2 border-grey-1 bg-white p-2 shadow-lg dark:border-white dark:bg-n-1 ${
`absolute left-0 right-0 mt-1 w-full rounded-sm border border-n-1 bg-white p-2 shadow-lg dark:border-white dark:bg-n-1 ${
small ? 'p-0' : ''
} ${up ? 'bottom-full top-auto mb-1 mt-0' : ''} z-50 ${classOptions}`
)}
Expand All @@ -84,17 +86,17 @@ const Select = ({
width: buttonRef.current.offsetWidth,
}}
>
{items.map((item: any) => (
{items.map((item) => (
<Listbox.Option
className={twMerge(
`flex cursor-pointer items-start rounded-sm px-3 py-2 text-start text-sm font-bold text-grey-1 transition-colors tap-highlight-color ui-selected:!bg-grey-1/20 ui-selected:!text-n-1 hover:text-n-1 dark:text-white/50 dark:ui-selected:!text-white dark:hover:text-white ${
small ? '!py-1 !pl-4 text-xs' : ''
} ${classOption}`
)}
key={item.chainId ?? item.code ?? item.id}
value={item.chainId ? item : item.code}
key={item.id}
value={item}
>
{item.name}
{item.title}
</Listbox.Option>
))}
</Listbox.Options>
Expand Down
Loading
Loading