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
Unsupported Method
+} diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 6d3b07608..00899daa9 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -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' @@ -319,6 +320,11 @@ export default function OnrampBankPage() { return } + // Show Mercado Pago flow for Argentina bank transfers + if (step === 'inputAmount' && selectedCountry.id === 'AR') { + return + } + if (step === 'inputAmount') { 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 && ( {`${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 +}