diff --git a/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx
new file mode 100644
index 000000000..204533ae2
--- /dev/null
+++ b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx
@@ -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
+ }
+
+ return
diff --git a/src/app/actions/onramp.ts b/src/app/actions/onramp.ts
index 4976b95f3..5a542494a 100644
--- a/src/app/actions/onramp.ts
+++ b/src/app/actions/onramp.ts
@@ -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'
@@ -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.' }
+ }
+}
diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx
new file mode 100644
index 000000000..d9229c64d
--- /dev/null
+++ b/src/components/AddMoney/components/InputAmountStep.tsx
@@ -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
>
+ setTokenUSDAmount: React.Dispatch>
+ 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
+ }
+
+ return (
+
+
router.back()} />
+
+
How much do you want to add?
+
+
setTokenAmount(e ?? '')}
+ walletBalance={undefined}
+ hideCurrencyToggle
+ setUsdValue={(e) => setTokenUSDAmount(e)}
+ currency={
+ currencyData
+ ? {
+ code: currencyData.code!,
+ symbol: currencyData.symbol!,
+ price: currencyData.price!,
+ }
+ : undefined
+ }
+ hideBalance={true}
+ />
+
+
+ This must exactly match what you send from your bank
+
+
+ {error && }
+
+
+ )
+}
+
+export default InputAmountStep
diff --git a/src/components/AddMoney/components/MantecaDepositCard.tsx b/src/components/AddMoney/components/MantecaDepositCard.tsx
new file mode 100644
index 000000000..25e26d825
--- /dev/null
+++ b/src/components/AddMoney/components/MantecaDepositCard.tsx
@@ -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 (
+
+
+
+
Account details
+
+ {cbu && }
+ {alias && }
+ {depositAddress && }
+ {pixKey && }
+
+
+ )
+}
+
+export default MantecaDepositCard
diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/MercadoPagoDepositDetails.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/MercadoPagoDepositDetails.tsx
new file mode 100644
index 000000000..2df420209
--- /dev/null
+++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/MercadoPagoDepositDetails.tsx
@@ -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 (
+
+ router.back()} />
+
+
+
+ generateShareText()}
+ title="Bank Transfer Details"
+ variant="purple"
+ className="w-full"
+ >
+ Share Details
+
+
+ )
+}
+
+export default MercadoPagoDepositDetails
diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx
new file mode 100644
index 000000000..d7e0ff9ee
--- /dev/null
+++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx
@@ -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 = ({ source }) => {
+ const params = useParams()
+ const [step, setStep] = useState('inputAmount')
+ const [isCreatingDeposit, setIsCreatingDeposit] = useState(false)
+ const [tokenAmount, setTokenAmount] = useState('')
+ const [tokenUSDAmount, setTokenUSDAmount] = useState('')
+ const [error, setError] = useState(null)
+ const [depositDetails, setDepositDetails] = useState()
+
+ 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 (
+
+ )
+ }
+
+ if (step === 'depositDetails' && depositDetails) {
+ return
+ }
+
+ return null
+}
+
+export default MercadoPago
diff --git a/src/components/AddMoney/consts/index.ts b/src/components/AddMoney/consts/index.ts
index c4100d0d4..ac27ca5c9 100644
--- a/src/components/AddMoney/consts/index.ts
+++ b/src/components/AddMoney/consts/index.ts
@@ -2497,7 +2497,7 @@ export const countryCodeMap: { [key: string]: string } = {
USA: 'US',
}
-const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX'])
+const enabledBankTransferCountries = new Set([...Object.values(countryCodeMap), 'US', 'MX', 'AR'])
// Helper function to check if a country code is enabled for bank transfers
// Handles both 2-letter and 3-letter country codes
@@ -2590,25 +2590,15 @@ countryData.forEach((country) => {
} else if (newMethod.id === 'crypto-add') {
newMethod.path = `/add-money/crypto`
newMethod.isSoon = false
+ } else if (newMethod.id === 'mercado-pago-add' && countryCode === 'AR') {
+ newMethod.isSoon = false
+ newMethod.path = `/add-money/${country.path}/mercadopago`
} else {
newMethod.isSoon = true
}
return newMethod
})
- // Add country-specific add methods (same as withdraw methods for consistency)
- if (specificMethodDetails && specificMethodDetails.length > 0) {
- specificMethodDetails.forEach((method) => {
- currentAddMethods.push({
- id: `${countryCode.toLowerCase()}-${method.title.toLowerCase().replace(/\s+/g, '-')}-add`,
- icon: method.icon ?? undefined,
- title: method.title,
- description: method.description,
- isSoon: true,
- })
- })
- }
-
COUNTRY_SPECIFIC_METHODS[countryCode] = {
add: currentAddMethods,
withdraw: withdrawList,
diff --git a/src/components/Global/PeanutActionDetailsCard/index.tsx b/src/components/Global/PeanutActionDetailsCard/index.tsx
index 51a5b9240..0f1c2db58 100644
--- a/src/components/Global/PeanutActionDetailsCard/index.tsx
+++ b/src/components/Global/PeanutActionDetailsCard/index.tsx
@@ -11,6 +11,7 @@ import { Icon, IconName } from '../Icons/Icon'
import RouteExpiryTimer from '../RouteExpiryTimer'
import Image from 'next/image'
import Loading from '../Loading'
+import { StaticImport } from 'next/dist/shared/lib/get-img-props'
export type PeanutActionDetailsCardTransactionType =
| 'REQUEST'
@@ -47,6 +48,7 @@ export interface PeanutActionDetailsCardProps {
disableTimerRefetch?: boolean
timerError?: string | null
isLoading?: boolean
+ logo?: StaticImport
}
export default function PeanutActionDetailsCard({
@@ -70,6 +72,7 @@ export default function PeanutActionDetailsCard({
countryCodeForFlag,
currencySymbol,
isLoading = false,
+ logo,
}: PeanutActionDetailsCardProps) {
const renderRecipient = () => {
if (recipientType === 'ADDRESS') return printableAddress(recipientName)
@@ -95,7 +98,7 @@ export default function PeanutActionDetailsCard({
if (viewType === 'SUCCESS') title = `You just claimed`
else title = `${renderRecipient()} sent you`
}
- if (transactionType === 'ADD_MONEY') title = `You're adding`
+ if (transactionType === 'ADD_MONEY' || transactionType === 'ADD_MONEY_BANK_ACCOUNT') title = `You're adding`
if (transactionType === 'WITHDRAW' || transactionType === 'WITHDRAW_BANK_ACCOUNT') title = `You're withdrawing`
if (transactionType === 'CLAIM_LINK_BANK_ACCOUNT') {
if (viewType === 'SUCCESS') {
@@ -158,12 +161,13 @@ export default function PeanutActionDetailsCard({
const isClaimLinkBankAccount = transactionType === 'CLAIM_LINK_BANK_ACCOUNT' && recipientType === 'BANK_ACCOUNT'
const withdrawBankIcon = () => {
+ const imgSrc = logo ? logo : `https://flagcdn.com/w320/${countryCodeForFlag}.png`
if (isWithdrawBankAccount || isAddBankAccount || isClaimLinkBankAccount)
return (
{countryCodeForFlag && (
{
const [code, setCode] = useState(currencyCode?.toUpperCase() ?? null)
const [symbol, setSymbol] = useState(null)
const [price, setPrice] = useState(null)
- const [isLoading, setIsLoading] = useState(false)
+ const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
if (!code) {
diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts
new file mode 100644
index 000000000..0e9be9c30
--- /dev/null
+++ b/src/types/manteca.types.ts
@@ -0,0 +1,5 @@
+export interface MantecaDepositDetails {
+ depositAddress: string
+ depositAlias: string
+ depositAmount: string
+}