diff --git a/src/app/[...recipient]/client.tsx b/src/app/[...recipient]/client.tsx index 2aa748abb..aef205f4e 100644 --- a/src/app/[...recipient]/client.tsx +++ b/src/app/[...recipient]/client.tsx @@ -68,6 +68,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) showRequestFulfilmentBankFlowManager, setShowRequestFulfilmentBankFlowManager, setFlowStep: setRequestFulfilmentBankFlowStep, + fulfillUsingManteca, } = useRequestFulfillmentFlow() const { requestType } = useDetermineBankRequestType(chargeDetails?.requestLink.recipientAccount.userId ?? '') @@ -392,6 +393,10 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) } }, [transactionForDrawer, currentView, dispatch, openTransactionDetails, isExternalWalletFlow, chargeId]) + const showActionList = + flow !== 'direct_pay' || // Always show for non-direct-pay flows + (flow === 'direct_pay' && !user) || // Show for direct-pay when user is not logged in + !fulfillUsingManteca // Show when not fulfilling using Manteca // Send to bank step if its mentioned in the URL and guest KYC is not needed useEffect(() => { const stepFromURL = searchParams.get('step') @@ -407,12 +412,6 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props) } }, [searchParams, parsedPaymentData, chargeDetails, requestType]) - let showActionList = flow !== 'direct_pay' - - if (flow === 'direct_pay' && !user) { - showActionList = true - } - if (error) { return (
diff --git a/src/app/actions/manteca.ts b/src/app/actions/manteca.ts new file mode 100644 index 000000000..b4e3f5cc4 --- /dev/null +++ b/src/app/actions/manteca.ts @@ -0,0 +1,46 @@ +'use server' + +import { MantecaWithdrawData, MantecaWithdrawResponse } from '@/types/manteca.types' +import { fetchWithSentry } from '@/utils' +import { cookies } from 'next/headers' + +const API_KEY = process.env.PEANUT_API_KEY! + +export async function mantecaWithdraw(params: MantecaWithdrawData): Promise { + 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/app/actions/onramp.ts b/src/app/actions/onramp.ts index ad37f9776..053e3f2e3 100644 --- a/src/app/actions/onramp.ts +++ b/src/app/actions/onramp.ts @@ -3,7 +3,6 @@ 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' @@ -113,51 +112,3 @@ 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/MantecaDepositCard.tsx b/src/components/AddMoney/components/MantecaDepositCard.tsx index 25e26d825..cef3af572 100644 --- a/src/components/AddMoney/components/MantecaDepositCard.tsx +++ b/src/components/AddMoney/components/MantecaDepositCard.tsx @@ -1,7 +1,6 @@ import { MERCADO_PAGO } from '@/assets' -import Card from '@/components/Global/Card' +import MantecaDetailsCard from '@/components/Global/MantecaDetailsCard' import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' -import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' interface MantecaDepositCardProps { @@ -40,12 +39,22 @@ const MantecaDepositCard = ({ />

Account details

- - {cbu && } - {alias && } - {depositAddress && } - {pixKey && } - + {(() => { + const rows = [ + ...(cbu + ? [{ key: 'cbu', label: 'CBU', value: cbu, allowCopy: true, hideBottomBorder: false }] + : []), + ...(alias ? [{ key: 'alias', label: 'Alias', value: alias, hideBottomBorder: false }] : []), + ...(depositAddress + ? [{ key: 'deposit', label: 'Deposit Address', value: depositAddress, hideBottomBorder: false }] + : []), + ...(pixKey ? [{ key: 'pix', label: 'Pix Key', value: pixKey, hideBottomBorder: false }] : []), + ] + if (rows.length) { + rows[rows.length - 1].hideBottomBorder = true + } + return + })()}
) } diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx index d7e0ff9ee..d5e2e0493 100644 --- a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx +++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx @@ -1,10 +1,10 @@ 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' +import { mantecaApi } from '@/services/manteca' interface MercadoPagoProps { source: 'bank' | 'regionalMethod' @@ -33,7 +33,7 @@ const MercadoPago: FC = ({ source }) => { try { setError(null) setIsCreatingDeposit(true) - const depositData = await createMantecaOnramp({ + const depositData = await mantecaApi.deposit({ usdAmount: tokenUSDAmount.replace(/,/g, ''), currency: selectedCountry.currency, }) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index d1de4a4b2..471a9cb46 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -51,6 +51,7 @@ import { Button } from '@/components/0_Bruddle' import Image from 'next/image' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' import { GuestVerificationModal } from '@/components/Global/GuestVerificationModal' +import MantecaFlowManager from './MantecaFlowManager' export const InitialClaimLinkView = (props: IClaimScreenProps) => { const { @@ -91,6 +92,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { setShowVerificationModal, setClaimToExternalWallet, resetFlow: resetClaimBankFlow, + claimToMercadoPago, } = useClaimBankFlow() const { setLoadingState, isLoading } = useContext(loadingStateContext) const { @@ -638,6 +640,22 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { return } + if (claimToMercadoPago) { + return ( + + ) + } + return (
{!!user?.user.userId || claimBankFlowStep || claimToExternalWallet ? ( diff --git a/src/components/Claim/Link/MantecaFlowManager.tsx b/src/components/Claim/Link/MantecaFlowManager.tsx new file mode 100644 index 000000000..ead46215f --- /dev/null +++ b/src/components/Claim/Link/MantecaFlowManager.tsx @@ -0,0 +1,100 @@ +'use client' + +import { MERCADO_PAGO } from '@/assets' +import NavHeader from '@/components/Global/NavHeader' +import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' +import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' +import { ClaimLinkData } from '@/services/sendLinks' +import { FC, useState } from 'react' +import MantecaDetailsStep from './views/MantecaDetailsStep.view' +import { MercadoPagoStep } from '@/types/manteca.types' +import MantecaReviewStep from './views/MantecaReviewStep' +import { Button } from '@/components/0_Bruddle' +import { useRouter } from 'next/navigation' + +interface MantecaFlowManagerProps { + claimLinkData: ClaimLinkData + amount: string + attachment: { message: string | undefined; attachmentUrl: string | undefined } +} + +const MantecaFlowManager: FC = ({ claimLinkData, amount, attachment }) => { + const { setClaimToMercadoPago } = useClaimBankFlow() + const [currentStep, setCurrentStep] = useState(MercadoPagoStep.DETAILS) + const router = useRouter() + const [destinationAddress, setDestinationAddress] = useState('') + + const isSuccess = currentStep === MercadoPagoStep.SUCCESS + + const renderStepDetails = () => { + if (currentStep === MercadoPagoStep.DETAILS) { + return ( + + ) + } + if (currentStep === MercadoPagoStep.REVIEW) { + return ( + + ) + } + + if (currentStep === MercadoPagoStep.SUCCESS) { + return ( + + ) + } + return null + } + + const onPrev = () => { + if (currentStep === MercadoPagoStep.DETAILS) { + setClaimToMercadoPago(false) + return + } + + if (currentStep === MercadoPagoStep.REVIEW) { + setCurrentStep(MercadoPagoStep.DETAILS) + return + } + if (currentStep === MercadoPagoStep.SUCCESS) { + router.push('/home') + return + } + } + + return ( +
+ + +
+ + + {renderStepDetails()} +
+
+ ) +} + +export default MantecaFlowManager diff --git a/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx b/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx new file mode 100644 index 000000000..199c27ed9 --- /dev/null +++ b/src/components/Claim/Link/views/MantecaDetailsStep.view.tsx @@ -0,0 +1,45 @@ +'use client' + +import { Button } from '@/components/0_Bruddle' +import BaseInput from '@/components/0_Bruddle/BaseInput' +import { Icon } from '@/components/Global/Icons/Icon' +import { MercadoPagoStep } from '@/types/manteca.types' +import { Dispatch, FC, SetStateAction } from 'react' + +interface MantecaDetailsStepProps { + setCurrentStep: Dispatch> + destinationAddress: string + setDestinationAddress: Dispatch> +} + +const MantecaDetailsStep: FC = ({ + setCurrentStep, + destinationAddress, + setDestinationAddress, +}) => { + const handleOnClick = async () => { + setCurrentStep(MercadoPagoStep.REVIEW) + } + + return ( + <> +

Enter Mercado Pago account details

+ + setDestinationAddress(e.target.value)} + placeholder="CVU or Alias" + /> +
+ + You can only withdraw to accounts under your name. +
+ + + + ) +} + +export default MantecaDetailsStep diff --git a/src/components/Claim/Link/views/MantecaReviewStep.tsx b/src/components/Claim/Link/views/MantecaReviewStep.tsx new file mode 100644 index 000000000..2ea5bb388 --- /dev/null +++ b/src/components/Claim/Link/views/MantecaReviewStep.tsx @@ -0,0 +1,90 @@ +import { Button } from '@/components/0_Bruddle' +import ErrorAlert from '@/components/Global/ErrorAlert' +import MantecaDetailsCard, { MantecaCardRow } from '@/components/Global/MantecaDetailsCard' +import PeanutLoading from '@/components/Global/PeanutLoading' +import { MANTECA_DEPOSIT_ADDRESS } from '@/constants' +import { useCurrency } from '@/hooks/useCurrency' +import { mantecaApi } from '@/services/manteca' +import { sendLinksApi } from '@/services/sendLinks' +import { MercadoPagoStep } from '@/types/manteca.types' +import { Dispatch, FC, SetStateAction, useState } from 'react' + +interface MantecaReviewStepProps { + setCurrentStep: Dispatch> + claimLink: string + destinationAddress: string + amount: string +} + +const MantecaReviewStep: FC = ({ setCurrentStep, claimLink, destinationAddress, amount }) => { + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + const { price, isLoading } = useCurrency('ARS') // TODO: change to the currency of the selected Method + + const detailsCardRows: MantecaCardRow[] = [ + { + key: 'alias', + label: 'Alias', + value: destinationAddress, + allowCopy: true, + }, + { + key: 'exchangeRate', + label: 'Exchange Rate', + value: `1 USD = ${price?.buy} ARS`, + }, + { + key: 'fee', + label: 'Fee', + value: 'Sponsored by Peanut', + hideBottomBorder: true, + }, + ] + const handleWithdraw = async () => { + if (destinationAddress) { + 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, + currency: 'ARS', // TODO: source-selected currency + }) + if (withdrawError || !data) { + setError(withdrawError || 'Something went wrong. Please contact Support') + return + } + setCurrentStep(MercadoPagoStep.SUCCESS) + } catch (error) { + setError(error instanceof Error ? error.message : 'Something went wrong. Please contact Support') + console.error('Error claiming link:', error) + } finally { + setIsSubmitting(false) + } + } + } + + if (isLoading) { + return + } + + return ( + <> + + + {error && } + + + ) +} + +export default MantecaReviewStep diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 0233f0bf0..b706135a9 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -42,7 +42,12 @@ interface IActionListProps { */ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLinkData }: IActionListProps) { const router = useRouter() - const { setClaimToExternalWallet, setFlowStep: setClaimBankFlowStep, setShowVerificationModal } = useClaimBankFlow() + const { + setClaimToExternalWallet, + setFlowStep: setClaimBankFlowStep, + setShowVerificationModal, + setClaimToMercadoPago, + } = useClaimBankFlow() const [showMinAmountError, setShowMinAmountError] = useState(false) const { claimType } = useDetermineBankClaimType(claimLinkData?.sender?.userId ?? '') const { chargeDetails } = usePaymentStore() @@ -55,6 +60,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin setShowRequestFulfilmentBankFlowManager, setShowExternalWalletFulfilMethods, setFlowStep: setRequestFulfilmentBankFlowStep, + setFulfillUsingManteca, } = useRequestFulfillmentFlow() const [isGuestVerificationModalOpen, setIsGuestVerificationModalOpen] = useState(false) @@ -81,7 +87,8 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin } break case 'mercadopago': - break // soon tag, so no action needed + setClaimToMercadoPago(true) + break case 'exchange-or-wallet': setClaimToExternalWallet(true) break @@ -103,7 +110,8 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin } break case 'mercadopago': - break // soon tag, so no action needed + setFulfillUsingManteca(true) + break case 'exchange-or-wallet': setShowExternalWalletFulfilMethods(true) break diff --git a/src/components/Global/CopyToClipboard/index.tsx b/src/components/Global/CopyToClipboard/index.tsx index 3e25058dc..ca6e27410 100644 --- a/src/components/Global/CopyToClipboard/index.tsx +++ b/src/components/Global/CopyToClipboard/index.tsx @@ -23,7 +23,7 @@ const CopyToClipboard = ({ textToCopy, fill, className, iconSize = '6' }: Props) return ( diff --git a/src/components/Global/MantecaDetailsCard/index.tsx b/src/components/Global/MantecaDetailsCard/index.tsx new file mode 100644 index 000000000..346d46413 --- /dev/null +++ b/src/components/Global/MantecaDetailsCard/index.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react' +import Card from '../Card' +import { PaymentInfoRow, PaymentInfoRowProps } from '@/components/Payment/PaymentInfoRow' + +export interface MantecaCardRow extends PaymentInfoRowProps { + key: React.Key +} + +interface MantecaDetailsCardProps { + rows: MantecaCardRow[] +} + +const MantecaDetailsCard: FC = ({ rows }) => { + return ( + + {rows.map(({ key, ...row }) => ( + + ))} + + ) +} + +export default MantecaDetailsCard diff --git a/src/components/Global/PeanutActionDetailsCard/index.tsx b/src/components/Global/PeanutActionDetailsCard/index.tsx index 0f1c2db58..3b1e875c7 100644 --- a/src/components/Global/PeanutActionDetailsCard/index.tsx +++ b/src/components/Global/PeanutActionDetailsCard/index.tsx @@ -9,9 +9,8 @@ import Attachment from '../Attachment' import Card from '../Card' import { Icon, IconName } from '../Icons/Icon' import RouteExpiryTimer from '../RouteExpiryTimer' -import Image from 'next/image' +import Image, { type StaticImageData } from 'next/image' import Loading from '../Loading' -import { StaticImport } from 'next/dist/shared/lib/get-img-props' export type PeanutActionDetailsCardTransactionType = | 'REQUEST' @@ -23,6 +22,7 @@ export type PeanutActionDetailsCardTransactionType = | 'WITHDRAW' | 'WITHDRAW_BANK_ACCOUNT' | 'ADD_MONEY_BANK_ACCOUNT' + | 'REGIONAL_METHOD_CLAIM' export type PeanutActionDetailsCardRecipientType = RecipientType | 'BANK_ACCOUNT' @@ -48,7 +48,7 @@ export interface PeanutActionDetailsCardProps { disableTimerRefetch?: boolean timerError?: string | null isLoading?: boolean - logo?: StaticImport + logo?: StaticImageData } export default function PeanutActionDetailsCard({ @@ -85,7 +85,12 @@ export default function PeanutActionDetailsCard({ if (transactionType === 'ADD_MONEY' || transactionType === 'CLAIM_LINK_BANK_ACCOUNT') return 'arrow-down' if (transactionType === 'REQUEST' || transactionType === 'RECEIVED_LINK') return 'arrow-down-left' if (transactionType === 'CLAIM_LINK') return viewType !== 'SUCCESS' ? 'arrow-down' : undefined - if (transactionType === 'WITHDRAW' || transactionType === 'WITHDRAW_BANK_ACCOUNT') return 'arrow-up' + if ( + transactionType === 'WITHDRAW' || + transactionType === 'WITHDRAW_BANK_ACCOUNT' || + transactionType === 'REGIONAL_METHOD_CLAIM' + ) + return 'arrow-up' } const getTitle = () => { @@ -107,6 +112,7 @@ export default function PeanutActionDetailsCard({ title = `You're about to receive` } } + if (transactionType === 'REGIONAL_METHOD_CLAIM') title = recipientName // Render the string as is for regional method return (

{icon && } {title} @@ -197,6 +203,7 @@ export default function PeanutActionDetailsCard({ backgroundColor: getAvatarBackgroundColor(), color: getAvatarTextColor(), }} + logo={logo} /> )}

diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index f97e615cf..b1163a147 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -36,6 +36,7 @@ import { useAccount } from 'wagmi' import { useUserInteractions } from '@/hooks/useUserInteractions' import { useUserByUsername } from '@/hooks/useUserByUsername' import { PaymentFlow } from '@/app/[...recipient]/client' +import MantecaFulfillment from '../Views/MantecaFulfillment.view' export type PaymentFlowProps = { isPintaReq?: boolean @@ -80,7 +81,8 @@ export const PaymentForm = ({ error: paymentStoreError, attachmentOptions, } = usePaymentStore() - const { setShowExternalWalletFulfilMethods, setExternalWalletFulfilMethod } = useRequestFulfillmentFlow() + const { setShowExternalWalletFulfilMethods, setExternalWalletFulfilMethod, fulfillUsingManteca } = + useRequestFulfillmentFlow() const recipientUsername = !chargeDetails && recipient?.recipientType === 'USERNAME' ? recipient.identifier : null const { user: recipientUser } = useUserByUsername(recipientUsername) @@ -639,6 +641,10 @@ export const PaymentForm = ({ } } + if (fulfillUsingManteca) { + return + } + return (
diff --git a/src/components/Payment/PaymentInfoRow.tsx b/src/components/Payment/PaymentInfoRow.tsx index ce671684e..be0dc2d6e 100644 --- a/src/components/Payment/PaymentInfoRow.tsx +++ b/src/components/Payment/PaymentInfoRow.tsx @@ -4,6 +4,16 @@ import { Icon } from '../Global/Icons/Icon' import Loading from '../Global/Loading' import CopyToClipboard from '../Global/CopyToClipboard' +export interface PaymentInfoRowProps { + label: string | React.ReactNode + value: number | string | React.ReactNode + moreInfoText?: string + loading?: boolean + hideBottomBorder?: boolean + allowCopy?: boolean + copyValue?: string +} + export const PaymentInfoRow = ({ label, value, @@ -12,15 +22,7 @@ export const PaymentInfoRow = ({ hideBottomBorder, allowCopy, copyValue, -}: { - label: string | React.ReactNode - value: number | string | React.ReactNode - moreInfoText?: string - loading?: boolean - hideBottomBorder?: boolean - allowCopy?: boolean - copyValue?: string -}) => { +}: PaymentInfoRowProps) => { const [showMoreInfo, setShowMoreInfo] = useState(false) const tooltipId = useId() diff --git a/src/components/Payment/Views/MantecaFulfillment.view.tsx b/src/components/Payment/Views/MantecaFulfillment.view.tsx new file mode 100644 index 000000000..7f5618669 --- /dev/null +++ b/src/components/Payment/Views/MantecaFulfillment.view.tsx @@ -0,0 +1,110 @@ +import { MERCADO_PAGO } from '@/assets' +import ErrorAlert from '@/components/Global/ErrorAlert' +import MantecaDetailsCard from '@/components/Global/MantecaDetailsCard' +import NavHeader from '@/components/Global/NavHeader' +import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard' +import PeanutLoading from '@/components/Global/PeanutLoading' +import ShareButton from '@/components/Global/ShareButton' +import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' +import { usePaymentStore } from '@/redux/hooks' +import { mantecaApi } from '@/services/manteca' +import { useQuery } from '@tanstack/react-query' +import React from 'react' + +const MantecaFulfillment = () => { + const { setFulfillUsingManteca } = useRequestFulfillmentFlow() + const { requestDetails, chargeDetails } = usePaymentStore() + const { data: depositData, isLoading: isLoadingDeposit } = useQuery({ + queryKey: ['manteca-deposit', chargeDetails?.uuid], + queryFn: () => + mantecaApi.deposit({ + usdAmount: requestDetails?.tokenAmount || chargeDetails?.tokenAmount || '0', + currency: 'ARS', + chargeId: chargeDetails?.uuid, + }), + refetchOnWindowFocus: false, + staleTime: Infinity, // don't refetch the data + enabled: Boolean(chargeDetails?.uuid), + }) + + const generateShareText = () => { + const textParts = [] + textParts.push(`CBU: ${depositData?.data?.depositAddress}`) + textParts.push(`Alias: ${depositData?.data?.depositAlias}`) + + return textParts.join('\n') + } + + if (isLoadingDeposit) { + return + } + + return ( +
+ { + setFulfillUsingManteca(false) + }} + /> + +
+ + + {depositData?.error && } + + {depositData?.data && ( + <> +

Account details

+ + + + generateShareText()} + title="Account Details" + variant="purple" + className="w-full" + > + Share Details + + + )} +
+
+ ) +} + +export default MantecaFulfillment diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx index 7c3cbeef2..410249741 100644 --- a/src/components/Profile/AvatarWithBadge.tsx +++ b/src/components/Profile/AvatarWithBadge.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from 'react' import { twMerge } from 'tailwind-merge' import { Icon, IconName } from '../Global/Icons/Icon' import StatusPill, { StatusPillType } from '../Global/StatusPill' +import Image, { type StaticImageData } from 'next/image' export type AvatarSize = 'extra-small' | 'small' | 'medium' | 'large' @@ -20,6 +21,7 @@ interface AvatarWithBadgeProps { iconFillColor?: string showStatusPill?: boolean statusPillStatus?: StatusPillType + logo?: StaticImageData } /** @@ -36,6 +38,7 @@ const AvatarWithBadge: React.FC = ({ iconFillColor, showStatusPill, statusPillStatus, + logo, }) => { const sizeClasses: Record = { 'extra-small': 'h-8 w-8 text-xs', @@ -58,6 +61,28 @@ const AvatarWithBadge: React.FC = ({ return '' }, [name]) + if (logo) { + return ( +
+
+ {name +
+
+ ) + } + return (
{/* the main avatar circle */} @@ -76,6 +101,15 @@ const AvatarWithBadge: React.FC = ({ ...inlineStyle, }} > + {logo && ( + {''} + )} {/* display icon if provided, otherwise display initials */} {icon ? ( diff --git a/src/constants/actionlist.consts.ts b/src/constants/actionlist.consts.ts index bb22d0a5b..7ed112d8d 100644 --- a/src/constants/actionlist.consts.ts +++ b/src/constants/actionlist.consts.ts @@ -27,7 +27,7 @@ export const ACTION_METHODS: PaymentMethod[] = [ title: 'Mercado Pago', description: 'Instant transfers', icons: [MERCADO_PAGO], - soon: true, + soon: false, }, { id: 'exchange-or-wallet', diff --git a/src/constants/index.ts b/src/constants/index.ts index 5b2a7124c..3214ac1ff 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,3 +4,4 @@ export * from './general.consts' export * from './loadingStates.consts' export * from './query.consts' export * from './zerodev.consts' +export * from './manteca.consts' diff --git a/src/constants/manteca.consts.ts b/src/constants/manteca.consts.ts new file mode 100644 index 000000000..9330c63dd --- /dev/null +++ b/src/constants/manteca.consts.ts @@ -0,0 +1 @@ +export const MANTECA_DEPOSIT_ADDRESS = '0x959e088a09f61aB01cb83b0eBCc74b2CF6d62053' diff --git a/src/context/ClaimBankFlowContext.tsx b/src/context/ClaimBankFlowContext.tsx index b6f351fc6..deda0810f 100644 --- a/src/context/ClaimBankFlowContext.tsx +++ b/src/context/ClaimBankFlowContext.tsx @@ -42,6 +42,8 @@ interface ClaimBankFlowContextType { setSenderKycStatus: (status?: BridgeKycStatus) => void justCompletedKyc: boolean setJustCompletedKyc: (status: boolean) => void + claimToMercadoPago: boolean + setClaimToMercadoPago: (claimToMercadoPago: boolean) => void } const ClaimBankFlowContext = createContext(undefined) @@ -60,6 +62,7 @@ export const ClaimBankFlowContextProvider: React.FC<{ children: ReactNode }> = ( const [selectedBankAccount, setSelectedBankAccount] = useState(null) const [senderKycStatus, setSenderKycStatus] = useState() const [justCompletedKyc, setJustCompletedKyc] = useState(false) + const [claimToMercadoPago, setClaimToMercadoPago] = useState(false) const resetFlow = useCallback(() => { setClaimToExternalWallet(false) @@ -75,6 +78,7 @@ export const ClaimBankFlowContextProvider: React.FC<{ children: ReactNode }> = ( setSelectedBankAccount(null) setSenderKycStatus(undefined) setJustCompletedKyc(false) + setClaimToMercadoPago(false) }, []) const value = useMemo( @@ -106,6 +110,8 @@ export const ClaimBankFlowContextProvider: React.FC<{ children: ReactNode }> = ( setSenderKycStatus, justCompletedKyc, setJustCompletedKyc, + claimToMercadoPago, + setClaimToMercadoPago, }), [ claimToExternalWallet, @@ -122,6 +128,8 @@ export const ClaimBankFlowContextProvider: React.FC<{ children: ReactNode }> = ( selectedBankAccount, senderKycStatus, justCompletedKyc, + claimToMercadoPago, + setClaimToMercadoPago, ] ) diff --git a/src/context/RequestFulfillmentFlowContext.tsx b/src/context/RequestFulfillmentFlowContext.tsx index b742712ce..0fe808925 100644 --- a/src/context/RequestFulfillmentFlowContext.tsx +++ b/src/context/RequestFulfillmentFlowContext.tsx @@ -32,6 +32,8 @@ interface RequestFulfillmentFlowContextType { setShowVerificationModal: (show: boolean) => void requesterDetails: User | null setRequesterDetails: (details: User | null) => void + fulfillUsingManteca: boolean + setFulfillUsingManteca: (fulfillUsingManteca: boolean) => void } const RequestFulfillmentFlowContext = createContext(undefined) @@ -47,6 +49,7 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod const [onrampData, setOnrampData] = useState(null) const [showVerificationModal, setShowVerificationModal] = useState(false) const [requesterDetails, setRequesterDetails] = useState(null) + const [fulfillUsingManteca, setFulfillUsingManteca] = useState(false) const resetFlow = useCallback(() => { setExternalWalletFulfilMethod(null) @@ -57,6 +60,7 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod setOnrampData(null) setShowVerificationModal(false) setRequesterDetails(null) + setFulfillUsingManteca(false) }, []) const value = useMemo( @@ -78,6 +82,8 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod setShowVerificationModal, requesterDetails, setRequesterDetails, + fulfillUsingManteca, + setFulfillUsingManteca, }), [ resetFlow, @@ -89,6 +95,7 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod onrampData, showVerificationModal, requesterDetails, + fulfillUsingManteca, ] ) diff --git a/src/services/manteca.ts b/src/services/manteca.ts index 4b18f254c..7a6204a4b 100644 --- a/src/services/manteca.ts +++ b/src/services/manteca.ts @@ -1,4 +1,10 @@ import { PEANUT_API_URL, PEANUT_API_KEY } from '@/constants' +import { + MantecaDepositDetails, + MantecaWithdrawData, + MantecaWithdrawResponse, + CreateMantecaOnrampParams, +} from '@/types/manteca.types' import { fetchWithSentry } from '@/utils' import Cookies from 'js-cookie' import type { Address, Hash } from 'viem' @@ -142,4 +148,64 @@ export const mantecaApi = { return response.json() }, + + deposit: async (params: CreateMantecaOnrampParams): Promise<{ data?: MantecaDepositDetails; error?: string }> => { + try { + const response = await fetchWithSentry(`${PEANUT_API_URL}/manteca/deposit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: JSON.stringify({ + usdAmount: params.usdAmount, + currency: params.currency, + chargeId: params.chargeId, + }), + }) + + 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.log('error', 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.' } + } + }, + + withdraw: async (data: MantecaWithdrawData): Promise => { + try { + const response = await fetchWithSentry(`${PEANUT_API_URL}/manteca/withdraw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: JSON.stringify(data), + }) + + const result = await response.json() + if (!response.ok) { + return { error: result.error || 'Failed to create manteca withdraw.' } + } + + return { data: result } + } 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/services/sendLinks.ts b/src/services/sendLinks.ts index 04b72d56e..97c4f9aed 100644 --- a/src/services/sendLinks.ts +++ b/src/services/sendLinks.ts @@ -197,6 +197,7 @@ export const sendLinksApi = { * * @param recipient - The recipient's address or username * @param link - The link to claim + * @param destinationAddress - The destination address to claim the link to (for manteca claims) * @returns The claim link data */ claim: async (recipient: string, link: string): Promise => { diff --git a/src/types/manteca.types.ts b/src/types/manteca.types.ts index 0e9be9c30..7ac881617 100644 --- a/src/types/manteca.types.ts +++ b/src/types/manteca.types.ts @@ -3,3 +3,73 @@ export interface MantecaDepositDetails { depositAlias: string depositAmount: string } + +export enum MercadoPagoStep { + DETAILS = 'details', + REVIEW = 'review', + SUCCESS = 'success', +} + +export type MantecaWithdrawData = { + amount: string + destinationAddress: string + txHash: string + currency: string +} + +export type MantecaWithdrawResponseData = { + id: string + numberId: string + userId: string + userNumberId: string + userExternalId: string + status: string + type: string + details: { + depositAddresses: { + ARBITRUM: string + } + depositAddress: string + depositAvailableNetworks: string[] + withdrawCostInAgainst: string + withdrawCostInAsset: string + price: string + priceExpireAt: string + } + currentStage: number + stages: { + 1: { + stageType: string + side: string + type: string + asset: string + against: string + assetAmount: string + price: string + priceCode: string + } + 2: { + stageType: string + asset: string + amount: string + to: string + destination: { + address: string + bankCode: string + } + } + } + creationTime: string + updatedAt: string +} + +export type MantecaWithdrawResponse = { + data?: MantecaWithdrawResponseData + error?: string +} + +export interface CreateMantecaOnrampParams { + usdAmount: string + currency: string + chargeId?: string +}