Skip to content
15 changes: 15 additions & 0 deletions src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client'
import MercadoPago from '@/components/AddMoney/components/RegionalMethods/MercadoPago'
import { useParams } from 'next/navigation'

export default function AddMoneyRegionalMethodPage() {
const params = useParams()
const country = params.country as string
const method = params['regional-method'] as string

if (country === 'argentina' && method === 'mercadopago') {
return <MercadoPago source="regionalMethod" />
}

return <div>Unsupported Method</div>
}
6 changes: 6 additions & 0 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { updateUserById } from '@/app/actions/users'
import AddMoneyBankDetails from '@/components/AddMoney/components/AddMoneyBankDetails'
import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/bridge.utils'
import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal'
import MercadoPago from '@/components/AddMoney/components/RegionalMethods/MercadoPago'

type AddStep = 'inputAmount' | 'kyc' | 'loading' | 'collectUserDetails' | 'showDetails'

Expand Down Expand Up @@ -319,6 +320,11 @@ export default function OnrampBankPage() {
return <AddMoneyBankDetails />
}

// Show Mercado Pago flow for Argentina bank transfers
if (step === 'inputAmount' && selectedCountry.id === 'AR') {
return <MercadoPago source="bank" />
}

if (step === 'inputAmount') {
return (
<div className="flex flex-col justify-start space-y-8">
Expand Down
49 changes: 49 additions & 0 deletions src/app/actions/onramp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { cookies } from 'next/headers'
import { fetchWithSentry } from '@/utils'
import { CountryData } from '@/components/AddMoney/consts'
import { MantecaDepositDetails } from '@/types/manteca.types'
import { getCurrencyConfig } from '@/utils/bridge.utils'
import { getCurrencyPrice } from '@/app/actions/currency'

Expand Down Expand Up @@ -112,3 +113,51 @@ export async function createOnrampForGuest(
return { error: 'An unexpected error occurred.' }
}
}

interface CreateMantecaOnrampParams {
usdAmount: string
currency: string
}

export async function createMantecaOnramp(
params: CreateMantecaOnrampParams
): Promise<{ data?: MantecaDepositDetails; error?: string }> {
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/deposit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
'api-key': API_KEY,
},
body: JSON.stringify({
usdAmount: params.usdAmount,
currency: params.currency,
}),
})

const data = await response.json()

if (!response.ok) {
console.log('error', response)
return { error: data.error || 'Failed to create on-ramp transfer for guest.' }
}

return { data }
} catch (error) {
console.error('Error calling create manteca on-ramp API:', error)
if (error instanceof Error) {
return { error: error.message }
}
return { error: 'An unexpected error occurred.' }
}
}
82 changes: 82 additions & 0 deletions src/components/AddMoney/components/InputAmountStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client'

import { Button } from '@/components/0_Bruddle'
import { Icon } from '@/components/Global/Icons/Icon'
import NavHeader from '@/components/Global/NavHeader'
import TokenAmountInput from '@/components/Global/TokenAmountInput'
import { useRouter } from 'next/navigation'
import { CountryData } from '../consts'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { useCurrency } from '@/hooks/useCurrency'
import PeanutLoading from '@/components/Global/PeanutLoading'

interface InputAmountStepProps {
onSubmit: () => void
selectedCountry: CountryData
isLoading: boolean
tokenAmount: string
setTokenAmount: React.Dispatch<React.SetStateAction<string>>
setTokenUSDAmount: React.Dispatch<React.SetStateAction<string>>
error: string | null
}

const InputAmountStep = ({
tokenAmount,
setTokenAmount,
onSubmit,
selectedCountry,
isLoading,
error,
setTokenUSDAmount,
}: InputAmountStepProps) => {
const router = useRouter()
const currencyData = useCurrency(selectedCountry.currency ?? 'ARS')

if (currencyData.isLoading) {
return <PeanutLoading />
}

return (
<div className="flex min-h-[inherit] flex-col justify-start space-y-8">
<NavHeader title="Add Money" onPrev={() => router.back()} />
<div className="my-auto flex flex-grow flex-col justify-center gap-4 md:my-0">
<div className="text-sm font-bold">How much do you want to add?</div>

<TokenAmountInput
tokenValue={tokenAmount}
setTokenValue={(e) => setTokenAmount(e ?? '')}
walletBalance={undefined}
hideCurrencyToggle
setUsdValue={(e) => setTokenUSDAmount(e)}
currency={
currencyData
? {
code: currencyData.code!,
symbol: currencyData.symbol!,
price: currencyData.price!,
}
: undefined
}
hideBalance={true}
/>
<div className="flex items-center gap-2 text-xs text-grey-1">
<Icon name="info" width={16} height={16} />
<span>This must exactly match what you send from your bank</span>
</div>
<Button
variant="purple"
shadowSize="4"
onClick={onSubmit}
disabled={isLoading || !parseFloat(tokenAmount.replace(/,/g, ''))}
className="w-full"
loading={isLoading}
>
Continue
</Button>
{error && <ErrorAlert description={error} />}
</div>
</div>
)
}

export default InputAmountStep
53 changes: 53 additions & 0 deletions src/components/AddMoney/components/MantecaDepositCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MERCADO_PAGO } from '@/assets'
import Card from '@/components/Global/Card'
import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'

interface MantecaDepositCardProps {
countryCodeForFlag: string
currencySymbol: string
amount: string
cbu?: string
alias?: string
depositAddress?: string
pixKey?: string
isMercadoPago: boolean
}

const MantecaDepositCard = ({
countryCodeForFlag,
currencySymbol,
amount,
cbu,
alias,
depositAddress,
pixKey,
isMercadoPago,
}: MantecaDepositCardProps) => {
return (
<div className="my-auto flex h-full w-full flex-col justify-center space-y-4">
<PeanutActionDetailsCard
avatarSize="small"
transactionType="ADD_MONEY_BANK_ACCOUNT"
recipientType="BANK_ACCOUNT"
recipientName="Your Bank Account"
amount={amount}
tokenSymbol={PEANUT_WALLET_TOKEN_SYMBOL}
countryCodeForFlag={countryCodeForFlag}
currencySymbol={currencySymbol}
logo={isMercadoPago ? MERCADO_PAGO : undefined}
/>

<h2 className="font-bold">Account details</h2>
<Card className="rounded-sm">
{cbu && <PaymentInfoRow label={'CBU'} value={cbu} allowCopy={true} />}
{alias && <PaymentInfoRow label={'Alias'} value={alias} hideBottomBorder />}
{depositAddress && <PaymentInfoRow label={'Deposit Address'} value={depositAddress} hideBottomBorder />}
{pixKey && <PaymentInfoRow label={'Pix Key'} value={pixKey} hideBottomBorder />}
</Card>
</div>
)
}

export default MantecaDepositCard
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client'

import NavHeader from '@/components/Global/NavHeader'
import { useParams, useRouter } from 'next/navigation'
import React, { useMemo } from 'react'
import { countryCodeMap, countryData } from '../../../consts'
import MantecaDepositCard from '../../MantecaDepositCard'
import ShareButton from '@/components/Global/ShareButton'
import { MantecaDepositDetails } from '@/types/manteca.types'

const MercadoPagoDepositDetails = ({
depositDetails,
source,
}: {
depositDetails: MantecaDepositDetails
source: 'bank' | 'regionalMethod'
}) => {
const router = useRouter()
const params = useParams()
const currentCountryName = params.country as string

const currentCountryDetails = useMemo(() => {
// check if we have country params (from dynamic route)
if (currentCountryName) {
return countryData.find(
(country) => country.type === 'country' && country.path === currentCountryName.toLowerCase()
)
}
// Default to Argentina
return countryData.find((c) => c.id === 'AR')
}, [currentCountryName])

const countryCodeForFlag = useMemo(() => {
const countryId = currentCountryDetails?.id || 'AR'
return countryId.toLowerCase()
}, [currentCountryDetails])

const generateShareText = () => {
const textParts = []
const currencySymbol = currentCountryDetails?.currency || 'ARS'

textParts.push(`Amount: ${currencySymbol} ${depositDetails.depositAmount}`)

if (depositDetails.depositAddress) {
textParts.push(`CBU: ${depositDetails.depositAddress}`)
}
if (depositDetails.depositAlias) {
textParts.push(`Alias: ${depositDetails.depositAlias}`)
}

return textParts.join('\n')
}

return (
<div className="flex h-full w-full flex-col justify-start gap-8 self-start">
<NavHeader title={'Add Money'} onPrev={() => router.back()} />

<MantecaDepositCard
countryCodeForFlag={countryCodeForFlag}
currencySymbol={currentCountryDetails?.currency || 'ARS'}
amount={depositDetails.depositAmount}
cbu={depositDetails.depositAddress}
alias={depositDetails.depositAlias}
isMercadoPago={source === 'regionalMethod'}
/>

<ShareButton
generateText={async () => generateShareText()}
title="Bank Transfer Details"
variant="purple"
className="w-full"
>
Share Details
</ShareButton>
</div>
)
}

export default MercadoPagoDepositDetails
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { FC, useMemo, useState } from 'react'
import MercadoPagoDepositDetails from './MercadoPagoDepositDetails'
import InputAmountStep from '../../InputAmountStep'
import { createMantecaOnramp } from '@/app/actions/onramp'
import { useParams } from 'next/navigation'
import { countryData } from '@/components/AddMoney/consts'
import { MantecaDepositDetails } from '@/types/manteca.types'

interface MercadoPagoProps {
source: 'bank' | 'regionalMethod'
}

type stepType = 'inputAmount' | 'depositDetails'

const MercadoPago: FC<MercadoPagoProps> = ({ source }) => {
const params = useParams()
const [step, setStep] = useState<stepType>('inputAmount')
const [isCreatingDeposit, setIsCreatingDeposit] = useState(false)
const [tokenAmount, setTokenAmount] = useState('')
const [tokenUSDAmount, setTokenUSDAmount] = useState('')
const [error, setError] = useState<string | null>(null)
const [depositDetails, setDepositDetails] = useState<MantecaDepositDetails>()

const selectedCountryPath = params.country as string
const selectedCountry = useMemo(() => {
return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath)
}, [selectedCountryPath])

const handleAmountSubmit = async () => {
if (!selectedCountry?.currency) return
if (isCreatingDeposit) return

try {
setError(null)
setIsCreatingDeposit(true)
const depositData = await createMantecaOnramp({
usdAmount: tokenUSDAmount.replace(/,/g, ''),
currency: selectedCountry.currency,
})
if (depositData.error) {
setError(depositData.error)
return
}
setDepositDetails(depositData.data)
setStep('depositDetails')
} catch (error) {
console.log(error)
setError(error instanceof Error ? error.message : String(error))
} finally {
setIsCreatingDeposit(false)
}
}

if (!selectedCountry) return null

if (step === 'inputAmount') {
return (
<InputAmountStep
tokenAmount={tokenAmount}
setTokenAmount={setTokenAmount}
onSubmit={handleAmountSubmit}
selectedCountry={selectedCountry}
isLoading={isCreatingDeposit}
error={error}
setTokenUSDAmount={setTokenUSDAmount}
/>
)
}

if (step === 'depositDetails' && depositDetails) {
return <MercadoPagoDepositDetails source={source} depositDetails={depositDetails} />
}

return null
}

export default MercadoPago
Loading
Loading