From cd4e5ab74b71740d96e383cb883cfc6a83592890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Sat, 27 Sep 2025 01:05:26 -0300 Subject: [PATCH 1/2] feat(manteca): add receipts to all manteca transactions --- src/app/actions/history.ts | 42 ++++ src/app/not-found.tsx | 21 ++ src/app/receipt/[entryId]/page.tsx | 34 +++ .../components/MantecaDepositShareDetails.tsx | 19 +- .../TransactionDetailsReceipt.tsx | 143 +++++++++--- .../transaction-details.utils.ts | 1 + .../transactionTransformer.ts | 4 + src/constants/manteca.consts.ts | 7 + src/hooks/useTransactionHistory.ts | 155 +------------ src/services/manteca.ts | 27 +++ src/utils/general.utils.ts | 4 + src/utils/history.utils.ts | 219 +++++++++++++++++- 12 files changed, 481 insertions(+), 195 deletions(-) create mode 100644 src/app/actions/history.ts create mode 100644 src/app/not-found.tsx create mode 100644 src/app/receipt/[entryId]/page.tsx diff --git a/src/app/actions/history.ts b/src/app/actions/history.ts new file mode 100644 index 000000000..afecc4b2f --- /dev/null +++ b/src/app/actions/history.ts @@ -0,0 +1,42 @@ +'use server' + +import { EHistoryEntryType, completeHistoryEntry } from '@/utils/history.utils' +import type { HistoryEntry } from '@/utils/history.utils' +import { PEANUT_API_URL } from '@/constants' +import { fetchWithSentry } from '@/utils' + +/** + * Fetches a single history entry from the API. This is used for receipts + * + * We want to cache the response for final states, that way we have less + * calls to the backend when sharing the receipt. + * For intermediate states, we want to avoid caching, so we can show the + * latest state whenever called. + * + * @param entryId The id of the entry to fetch + * @param entryType The type of the entry to fetch + * @returns The fetched history entry + */ +export async function getHistoryEntry(entryId: string, entryType: EHistoryEntryType): Promise { + let response: Awaited> + try { + response = await fetchWithSentry(`${PEANUT_API_URL}/history/${entryId}?entryType=${entryType}`) + } catch (error) { + throw new Error(`Unexpected error fetching history entry: ${error}`) + } + + if (!response.ok) { + if (response.status === 404) { + return null + } + if (response.status === 400) { + const errorData = await response.json() + throw new Error(`Sent invalid params when fetching history entry: ${errorData.message ?? errorData.error}`) + } + throw new Error(`Failed to fetch history entry: ${response.statusText}`) + } + + const data = await response.json() + const entry = completeHistoryEntry(data) + return entry +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 000000000..5ceecfdc9 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,21 @@ +import Link from 'next/link' +import PageContainer from '@/components/0_Bruddle/PageContainer' +import PEANUTMAN_CRY from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_05.gif' +import Image from 'next/image' + +export default function NotFound() { + return ( + +
+
+

Not found

+ Peanutman crying 😭 +

Woah there buddy, you're not supposed to be here.

+ + Take me home, I'm scared + +
+
+
+ ) +} diff --git a/src/app/receipt/[entryId]/page.tsx b/src/app/receipt/[entryId]/page.tsx new file mode 100644 index 000000000..1cf10c7ae --- /dev/null +++ b/src/app/receipt/[entryId]/page.tsx @@ -0,0 +1,34 @@ +import { connection } from 'next/server' +import { notFound } from 'next/navigation' +import { EHistoryEntryType, isFinalState, historyTypeFromNumber } from '@/utils/history.utils' +import { getHistoryEntry } from '@/app/actions/history' +import { mapTransactionDataForDrawer } from '@/components/TransactionDetails/transactionTransformer' +import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt' + +export default async function ReceiptPage({ + params, + searchParams, +}: { + params: Promise<{ entryId: string }> + searchParams: Promise> +}) { + const { entryId } = await params + let entryTypeId = (await searchParams).t + if (!entryId || !entryTypeId || typeof entryTypeId !== 'string' || !historyTypeFromNumber(Number(entryTypeId))) { + notFound() + } + const entryType = historyTypeFromNumber(Number(entryTypeId)) + const entry = await getHistoryEntry(entryId, entryType) + if (!entry) { + notFound() + } + if (!isFinalState(entry)) { + await connection() + } + const { transactionDetails } = mapTransactionDataForDrawer(entry) + return ( +
+ +
+ ) +} diff --git a/src/components/AddMoney/components/MantecaDepositShareDetails.tsx b/src/components/AddMoney/components/MantecaDepositShareDetails.tsx index a4744824e..357db8920 100644 --- a/src/components/AddMoney/components/MantecaDepositShareDetails.tsx +++ b/src/components/AddMoney/components/MantecaDepositShareDetails.tsx @@ -11,6 +11,11 @@ import { Icon } from '@/components/Global/Icons/Icon' import Image from 'next/image' import { Card } from '@/components/0_Bruddle/Card' import { shortenStringLong } from '@/utils' +import { + MANTECA_ARG_DEPOSIT_CUIT, + MANTECA_ARG_DEPOSIT_NAME, + MANTECA_COUNTRIES_CONFIG, +} from '@/constants/manteca.consts' const MantecaDepositShareDetails = ({ depositDetails, @@ -40,14 +45,8 @@ const MantecaDepositShareDetails = ({ }, [currentCountryDetails]) const depositAddressLabel = useMemo(() => { - switch (currentCountryDetails?.id) { - case 'AR': - return 'CBU' - case 'BR': - return 'Pix Key' - default: - return 'Deposit Address' - } + if (!currentCountryDetails) return 'Deposit Address' + return MANTECA_COUNTRIES_CONFIG[currentCountryDetails.id]?.depositAddressLabel ?? 'Deposit Address' }, [currentCountryDetails]) const depositAddress = depositDetails.details.depositAddress @@ -117,8 +116,8 @@ const MantecaDepositShareDetails = ({ {depositAlias && } {currentCountryDetails?.id === 'AR' && ( <> - - + + )} diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 1d2d79eab..d5f04d337 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -9,9 +9,8 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore } from '@/redux/hooks' import { chargesApi } from '@/services/charges' import { sendLinksApi } from '@/services/sendLinks' -import { formatAmount, formatDate, getInitialsFromName, formatNumberForDisplay, isStableCoin } from '@/utils' +import { formatAmount, formatDate, getInitialsFromName, isStableCoin, formatCurrency, getAvatarUrl } from '@/utils' import { formatIban, printableAddress, shortenAddress, shortenStringLong, slugify } from '@/utils/general.utils' -import { getDisplayCurrencySymbol } from '@/utils/currency' import { cancelOnramp } from '@/app/actions/onramp' import { captureException } from '@sentry/nextjs' import { useQueryClient } from '@tanstack/react-query' @@ -31,6 +30,14 @@ import { isAddress } from 'viem' import { getBankAccountLabel, TransactionDetailsRowKey, transactionDetailsRowKeys } from './transaction-details.utils' import { useSupportModalContext } from '@/context/SupportModalContext' import { useRouter } from 'next/navigation' +import { countryData } from '@/components/AddMoney/consts' +import { + MANTECA_COUNTRIES_CONFIG, + MANTECA_ARG_DEPOSIT_CUIT, + MANTECA_ARG_DEPOSIT_NAME, +} from '@/constants/manteca.consts' +import { mantecaApi } from '@/services/manteca' +import { getReceiptUrl } from '@/utils/history.utils' export const TransactionDetailsReceipt = ({ transaction, @@ -43,6 +50,7 @@ export const TransactionDetailsReceipt = ({ isModalOpen = false, setIsModalOpen, avatarUrl, + isPublic = false, }: { transaction: TransactionDetails | null onClose?: () => void @@ -54,6 +62,7 @@ export const TransactionDetailsReceipt = ({ isModalOpen?: boolean setIsModalOpen?: (isModalOpen: boolean) => void avatarUrl?: string + isPublic?: boolean }) => { // ref for the main content area to calculate dynamic height const { user } = useUserStore() @@ -145,9 +154,18 @@ export const TransactionDetailsReceipt = ({ comment: !!transaction.memo?.trim(), networkFee: !!(transaction.networkFeeDetails && transaction.sourceView === 'status'), attachment: !!transaction.attachmentUrl, + mantecaDepositInfo: + !isPublic && + transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP && + transaction.status === 'pending', } }, [transaction, isPendingBankRequest]) + const country = useMemo(() => { + if (!transaction?.currency?.code) return undefined + return countryData.find((c) => c.currency === transaction.currency?.code) + }, [transaction?.currency?.code]) + const visibleRows = useMemo(() => { // filter rowkeys to only include visible rows, maintaining the order return transactionDetailsRowKeys.filter((key) => rowVisibilityConfig[key]) @@ -188,9 +206,18 @@ export const TransactionDetailsReceipt = ({ }, [transaction]) const shouldShowShareReceipt = useMemo(() => { + if (isPublic) return false if (!transaction || isPendingSentLink || isPendingRequester || isPendingRequestee) return false if (transaction?.txHash && transaction.direction !== 'receive' && transaction.direction !== 'request_sent') return true + if ( + [ + EHistoryEntryType.MANTECA_QR_PAYMENT, + EHistoryEntryType.MANTECA_OFFRAMP, + EHistoryEntryType.MANTECA_ONRAMP, + ].includes(transaction.extraDataForDrawer!.originalType) + ) + return true return false }, [transaction, isPendingSentLink, isPendingRequester, isPendingRequestee]) @@ -233,37 +260,8 @@ export const TransactionDetailsReceipt = ({ if (!transaction) return null - // format data for display - let amountDisplay = '' - - if (transactionAmount) { - amountDisplay = transactionAmount.replace(/[+-]/g, '').replace(/\$/, '$ ') - } else if (transaction.extraDataForDrawer?.rewardData) { - amountDisplay = transaction.extraDataForDrawer.rewardData.formatAmount(transaction.amount) - } else if ( - (transaction.direction === 'bank_deposit' || transaction.direction === 'bank_request_fulfillment') && - transaction.currency?.code && - transaction.currency.code.toUpperCase() !== 'USD' - ) { - const isCompleted = transaction.status === 'completed' - - if (isCompleted) { - // For completed bank_deposit: show USD amount (amount is already in USD) - amountDisplay = `$ ${formatAmount(transaction.amount as number)}` - } else { - // For non-completed bank_deposit: show original currency - const currencyAmount = transaction.currency?.amount || transaction.amount.toString() - const currencySymbol = getDisplayCurrencySymbol(transaction.currency.code) - amountDisplay = `${currencySymbol} ${formatAmount(Number(currencyAmount))}` - } - } else { - // default: use currency amount if provided, otherwise fallback to raw amount - never show token value, only USD - if (transaction.currency?.amount) { - amountDisplay = `${transaction.currency.code} ${formatAmount(Number(transaction.currency.amount))}` - } else { - amountDisplay = `$ ${formatAmount(transaction.amount as number)}` - } - } + const usdAmount = transactionAmount?.replace(/[\+\-\$]/g, '') ?? transaction.amount + const amountDisplay = `$ ${formatCurrency(usdAmount.toString())}` const feeDisplay = transaction.fee !== undefined ? formatAmount(transaction.fee as number) : 'N/A' // determine if the qr code and sharing section should be shown @@ -313,7 +311,7 @@ export const TransactionDetailsReceipt = ({ isVerified={transaction.isVerified} isLinkTransaction={transaction.extraDataForDrawer?.isLinkTransaction} transactionType={transaction.extraDataForDrawer?.transactionCardType} - avatarUrl={avatarUrl ?? transaction.extraDataForDrawer?.avatarUrl} + avatarUrl={avatarUrl ?? getAvatarUrl(transaction)} haveSentMoneyToUser={transaction.haveSentMoneyToUser} /> @@ -460,20 +458,51 @@ export const TransactionDetailsReceipt = ({ )} + {rowVisibilityConfig.mantecaDepositInfo && ( + <> + {transaction.extraDataForDrawer?.receipt?.depositDetails?.depositAddress && ( + + )} + + {transaction.extraDataForDrawer?.receipt?.depositDetails?.depositAlias && ( + + )} + {country?.id === 'AR' && ( + <> + + + + )} + + )} + {/* Exchange rate and original currency for completed bank_deposit transactions */} {rowVisibilityConfig.exchangeRate && ( <> {transaction.extraDataForDrawer?.receipt?.exchange_rate && ( )} {/* TODO: stop using snake_case!!!!! */} {transaction.extraDataForDrawer?.receipt?.exchange_rate && ( )} @@ -926,9 +955,9 @@ export const TransactionDetailsReceipt = ({ )} - {shouldShowShareReceipt && transaction.extraDataForDrawer?.link && ( + {shouldShowShareReceipt && !!getReceiptUrl(transaction) && (
- Share Receipt + Share Receipt
)} @@ -991,6 +1020,44 @@ export const TransactionDetailsReceipt = ({ Cancel deposit )} + {transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP && + transaction.status === 'pending' && + setIsLoading && + onClose && ( + + )} {isPendingBankRequest && transaction.extraDataForDrawer?.originalUserRole === EHistoryUserRole.SENDER && diff --git a/src/components/TransactionDetails/transaction-details.utils.ts b/src/components/TransactionDetails/transaction-details.utils.ts index 332c61fca..43ae1c093 100644 --- a/src/components/TransactionDetails/transaction-details.utils.ts +++ b/src/components/TransactionDetails/transaction-details.utils.ts @@ -16,6 +16,7 @@ export type TransactionDetailsRowKey = | 'peanutFee' | 'comment' | 'attachment' + | 'mantecaDepositInfo' // rder of the rows in the receipt export const transactionDetailsRowKeys: TransactionDetailsRowKey[] = [ diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 112546915..e8a1e6d07 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -84,6 +84,10 @@ export interface TransactionDetails { account_holder_name?: string } receipt?: { + depositDetails?: { + depositAddress: string + depositAlias: string + } initial_amount?: string developer_fee?: string exchange_fee?: string diff --git a/src/constants/manteca.consts.ts b/src/constants/manteca.consts.ts index 2466228f2..a48c76849 100644 --- a/src/constants/manteca.consts.ts +++ b/src/constants/manteca.consts.ts @@ -1,5 +1,8 @@ export const MANTECA_DEPOSIT_ADDRESS = '0x959e088a09f61aB01cb83b0eBCc74b2CF6d62053' +export const MANTECA_ARG_DEPOSIT_NAME = 'Sixalime Sas' +export const MANTECA_ARG_DEPOSIT_CUIT = '30-71678845-3' + // Countries that use Manteca for bank withdrawals instead of Bridge export const MANTECA_COUNTRIES = [ 'argentina', // ARS, USD, BRL (QR pix payments) @@ -38,6 +41,7 @@ export type MantecaBankCode = { type MantecaCountryConfig = { accountNumberLabel: string + depositAddressLabel: string } & ( | { needsBankCode: true @@ -63,16 +67,19 @@ type MantecaCountryConfig = { export const MANTECA_COUNTRIES_CONFIG: Record = { AR: { accountNumberLabel: 'CBU, CVU or Alias', + depositAddressLabel: 'CBU', needsBankCode: false, needsAccountType: false, }, BR: { accountNumberLabel: 'PIX Key', + depositAddressLabel: 'PIX Key', needsBankCode: false, needsAccountType: false, }, BO: { accountNumberLabel: 'Account Number', + depositAddressLabel: 'Deposit Address', needsBankCode: true, needsAccountType: true, validAccountTypes: [MantecaAccountType.CHECKING, MantecaAccountType.SAVINGS], diff --git a/src/hooks/useTransactionHistory.ts b/src/hooks/useTransactionHistory.ts index f20ccf22c..a0cadc391 100644 --- a/src/hooks/useTransactionHistory.ts +++ b/src/hooks/useTransactionHistory.ts @@ -1,82 +1,19 @@ -import { BASE_URL, PEANUT_API_URL, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants' +import { PEANUT_API_URL } from '@/constants' import { TRANSACTIONS } from '@/constants/query.consts' -import { fetchWithSentry, formatAmount, getFromLocalStorage, getTokenDetails } from '@/utils' +import { fetchWithSentry } from '@/utils' import type { InfiniteData, InfiniteQueryObserverResult, QueryObserverResult } from '@tanstack/react-query' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import Cookies from 'js-cookie' -import { formatUnits, type Hash } from 'viem' +import { completeHistoryEntry } from '@/utils/history.utils' +import type { HistoryEntry } from '@/utils/history.utils' + +//TODO: remove and import all from utils everywhere +export { EHistoryEntryType, EHistoryUserRole } from '@/utils/history.utils' +export type { HistoryEntry, HistoryEntryType, HistoryUserRole } from '@/utils/history.utils' type LatestHistoryResult = QueryObserverResult type InfiniteHistoryResult = InfiniteQueryObserverResult> -export enum EHistoryEntryType { - REQUEST = 'REQUEST', - CASHOUT = 'CASHOUT', - DEPOSIT = 'DEPOSIT', - SEND_LINK = 'SEND_LINK', - DIRECT_SEND = 'DIRECT_SEND', - WITHDRAW = 'WITHDRAW', - BRIDGE_OFFRAMP = 'BRIDGE_OFFRAMP', - BRIDGE_ONRAMP = 'BRIDGE_ONRAMP', - BANK_SEND_LINK_CLAIM = 'BANK_SEND_LINK_CLAIM', - MANTECA_QR_PAYMENT = 'MANTECA_QR_PAYMENT', - MANTECA_OFFRAMP = 'MANTECA_OFFRAMP', - MANTECA_ONRAMP = 'MANTECA_ONRAMP', -} - -export enum EHistoryUserRole { - SENDER = 'SENDER', - RECIPIENT = 'RECIPIENT', - BOTH = 'BOTH', - NONE = 'NONE', -} - -export type HistoryEntryType = `${EHistoryEntryType}` -export type HistoryUserRole = `${EHistoryUserRole}` - -export type HistoryEntry = { - uuid: string - type: HistoryEntryType - timestamp: Date - amount: string - currency?: { - amount: string - code: string - } - txHash?: string - chainId: string - tokenSymbol: string - tokenAddress: string - status: string - userRole: HistoryUserRole - attachmentUrl?: string - memo?: string - cancelledAt?: Date | string - senderAccount?: - | { - identifier: string - type: string - isUser: boolean - username?: string | undefined - fullName?: string - userId?: string - } - | undefined - recipientAccount: { - identifier: string - type: string - isUser: boolean - username?: string | undefined - fullName?: string - userId?: string - } - extraData?: Record - claimedAt?: string | Date - createdAt?: string | Date - completedAt?: string | Date - isVerified?: boolean -} - export type HistoryResponse = { entries: HistoryEntry[] cursor?: string @@ -154,83 +91,9 @@ export function useTransactionHistory({ const data = await response.json() - // Convert ISO strings to Date objects for timestamps return { ...data, - entries: data.entries.map((entry: HistoryEntry) => { - const extraData = entry.extraData ?? {} - let link: string = '' - let tokenSymbol: string = '' - let usdAmount: string = '' - switch (entry.type) { - case EHistoryEntryType.SEND_LINK: { - const password = getFromLocalStorage(`sendLink::password::${entry.uuid}`) - const { contractVersion, depositIdx } = extraData - if (password) { - link = `${BASE_URL}/claim?c=${entry.chainId}&v=${contractVersion}&i=${depositIdx}#p=${password}` - } - const tokenDetails = getTokenDetails({ - tokenAddress: entry.tokenAddress as Hash, - chainId: entry.chainId, - }) - usdAmount = formatUnits(BigInt(entry.amount), tokenDetails?.decimals ?? 6) - tokenSymbol = tokenDetails?.symbol ?? '' - break - } - case EHistoryEntryType.REQUEST: { - link = `${BASE_URL}/${entry.recipientAccount.username || entry.recipientAccount.identifier}?chargeId=${entry.uuid}` - tokenSymbol = entry.tokenSymbol - usdAmount = entry.amount.toString() - break - } - case EHistoryEntryType.DIRECT_SEND: { - link = `${BASE_URL}/${entry.recipientAccount.username || entry.recipientAccount.identifier}?chargeId=${entry.uuid}` - tokenSymbol = entry.tokenSymbol - usdAmount = entry.amount.toString() - break - } - case EHistoryEntryType.DEPOSIT: { - const details = getTokenDetails({ - tokenAddress: entry.tokenAddress as Hash, - chainId: entry.chainId, - }) - tokenSymbol = details?.symbol ?? entry.tokenSymbol - - if (entry.extraData?.blockNumber) { - // direct deposits are always in wei - usdAmount = formatUnits(BigInt(entry.amount), PEANUT_WALLET_TOKEN_DECIMALS) - } else { - usdAmount = entry.amount.toString() - } - break - } - case EHistoryEntryType.WITHDRAW: - case EHistoryEntryType.BRIDGE_OFFRAMP: - case EHistoryEntryType.BRIDGE_ONRAMP: - case EHistoryEntryType.BANK_SEND_LINK_CLAIM: { - tokenSymbol = entry.tokenSymbol - usdAmount = entry.amount.toString() - break - } - default: { - if (entry.amount && !usdAmount) { - usdAmount = entry.amount.toString() - } - tokenSymbol = entry.tokenSymbol - } - } - return { - ...entry, - tokenSymbol, - timestamp: new Date(entry.timestamp), - cancelledAt: entry.cancelledAt ? new Date(entry.cancelledAt) : undefined, - extraData: { - ...extraData, - link, - usdAmount: `$${formatAmount(usdAmount)}`, - }, - } - }), + entries: data.entries.map(completeHistoryEntry), } } diff --git a/src/services/manteca.ts b/src/services/manteca.ts index 11cf3dca1..7a4401d35 100644 --- a/src/services/manteca.ts +++ b/src/services/manteca.ts @@ -205,6 +205,33 @@ export const mantecaApi = { } }, + cancelDeposit: async (depositId: string): Promise<{ data?: MantecaDepositResponseData; error?: string }> => { + try { + const response = await fetchWithSentry(`${PEANUT_API_URL}/manteca/deposit/${depositId}/cancel`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + }) + + const data = await response.json() + + if (!response.ok) { + console.log('error', response) + return { error: data.error || 'Failed to cancel manteca deposit.' } + } + + return { data } + } catch (error) { + console.log('error', error) + console.error('Error calling cancel manteca deposit 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`, { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 893a16bb3..d6261a886 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -375,6 +375,10 @@ export const formatNumberForDisplay = (valueStr: string | undefined, options?: { }) } +export function formatCurrency(valueStr: string | undefined): string { + return formatNumberForDisplay(valueStr, { maxDecimals: 2 }) +} + /** * formats a number by: * - displaying 2 significant digits for small numbers (<0.01) diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 5de2f331e..dbbd6eaa9 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -1,6 +1,147 @@ import { MERCADO_PAGO, PIX } from '@/assets/payment-apps' -import { EHistoryEntryType } from '@/hooks/useTransactionHistory' import { TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' +import { getFromLocalStorage, formatAmount } from '@/utils' +import { PEANUT_WALLET_TOKEN_DECIMALS, BASE_URL } from '@/constants' +import { formatUnits } from 'viem' +import { Hash } from 'viem' +import { getTokenDetails } from '@/utils' + +export enum EHistoryEntryType { + REQUEST = 'REQUEST', + CASHOUT = 'CASHOUT', + DEPOSIT = 'DEPOSIT', + SEND_LINK = 'SEND_LINK', + DIRECT_SEND = 'DIRECT_SEND', + WITHDRAW = 'WITHDRAW', + BRIDGE_OFFRAMP = 'BRIDGE_OFFRAMP', + BRIDGE_ONRAMP = 'BRIDGE_ONRAMP', + BANK_SEND_LINK_CLAIM = 'BANK_SEND_LINK_CLAIM', + MANTECA_QR_PAYMENT = 'MANTECA_QR_PAYMENT', + MANTECA_OFFRAMP = 'MANTECA_OFFRAMP', + MANTECA_ONRAMP = 'MANTECA_ONRAMP', +} +export function historyTypeToNumber(type: EHistoryEntryType): number { + return Object.values(EHistoryEntryType).indexOf(type) +} +export function historyTypeFromNumber(type: number): EHistoryEntryType { + return Object.values(EHistoryEntryType)[type] +} + +export enum EHistoryUserRole { + SENDER = 'SENDER', + RECIPIENT = 'RECIPIENT', + BOTH = 'BOTH', + NONE = 'NONE', +} + +// This comes from the backend, only add if added in the backend +// Merge of statuses of charges, sendlinks and manteca and bridge transfers +export enum EHistoryStatus { + STARTING = 'STARTING', + ACTIVE = 'ACTIVE', + WAITING = 'WAITING', + PAUSED = 'PAUSED', + COMPLETED = 'COMPLETED', + CANCELLED = 'CANCELLED', + EXPIRED = 'EXPIRED', + FAILED = 'FAILED', + NEW = 'NEW', + PENDING = 'PENDING', + SIGNED = 'SIGNED', + creating = 'creating', + completed = 'completed', + CLAIMING = 'CLAIMING', + CLAIMED = 'CLAIMED', + AWAITING_FUNDS = 'AWAITING_FUNDS', + IN_REVIEW = 'IN_REVIEW', + FUNDS_RECEIVED = 'FUNDS_RECEIVED', + PAYMENT_SUBMITTED = 'PAYMENT_SUBMITTED', + PAYMENT_PROCESSED = 'PAYMENT_PROCESSED', + UNDELIVERABLE = 'UNDELIVERABLE', + RETURNED = 'RETURNED', + REFUNDED = 'REFUNDED', + CANCELED = 'CANCELED', + ERROR = 'ERROR', +} + +export const FINAL_STATES: HistoryStatus[] = [ + EHistoryStatus.COMPLETED, + EHistoryStatus.EXPIRED, + EHistoryStatus.CLAIMED, + EHistoryStatus.PAYMENT_PROCESSED, + EHistoryStatus.REFUNDED, + EHistoryStatus.CANCELED, + EHistoryStatus.ERROR, +] + +export type HistoryEntryType = `${EHistoryEntryType}` +export type HistoryUserRole = `${EHistoryUserRole}` +export type HistoryStatus = `${EHistoryStatus}` + +export type HistoryEntry = { + uuid: string + type: HistoryEntryType + timestamp: Date + amount: string + currency?: { + amount: string + code: string + } + txHash?: string + chainId: string + tokenSymbol: string + tokenAddress: string + status: HistoryStatus + userRole: HistoryUserRole + attachmentUrl?: string + memo?: string + cancelledAt?: Date | string + senderAccount?: + | { + identifier: string + type: string + isUser: boolean + username?: string | undefined + fullName?: string + userId?: string + } + | undefined + recipientAccount: { + identifier: string + type: string + isUser: boolean + username?: string | undefined + fullName?: string + userId?: string + } + extraData?: Record + claimedAt?: string | Date + createdAt?: string | Date + completedAt?: string | Date + isVerified?: boolean +} + +export function isFinalState(transaction: Pick): boolean { + return FINAL_STATES.includes(transaction.status) +} + +export function getReceiptUrl(transaction: TransactionDetails): string | undefined { + if (transaction.extraDataForDrawer?.link) { + return transaction.extraDataForDrawer.link + } + if (!transaction.extraDataForDrawer?.originalType) { + return + } + const isManteca = [ + EHistoryEntryType.MANTECA_QR_PAYMENT, + EHistoryEntryType.MANTECA_OFFRAMP, + EHistoryEntryType.MANTECA_ONRAMP, + ].includes(transaction.extraDataForDrawer.originalType) + if (isManteca) { + const typeId = historyTypeToNumber(transaction.extraDataForDrawer.originalType) + return `${BASE_URL}/receipt/${transaction.id}?t=${typeId}` + } +} export function getAvatarUrl(transaction: TransactionDetails): string | undefined { if (transaction.extraDataForDrawer?.rewardData?.avatarUrl) { @@ -40,3 +181,79 @@ export function getTransactionSign(transaction: Pick Date: Mon, 29 Sep 2025 14:36:56 -0300 Subject: [PATCH 2/2] fix: receipt page padding --- src/app/receipt/[entryId]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/receipt/[entryId]/page.tsx b/src/app/receipt/[entryId]/page.tsx index 1cf10c7ae..e987eb908 100644 --- a/src/app/receipt/[entryId]/page.tsx +++ b/src/app/receipt/[entryId]/page.tsx @@ -27,8 +27,8 @@ export default async function ReceiptPage({ } const { transactionDetails } = mapTransactionDataForDrawer(entry) return ( -
- +
+
) }