Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/app/actions/history.ts
Original file line number Diff line number Diff line change
@@ -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<HistoryEntry | null> {
let response: Awaited<ReturnType<typeof fetchWithSentry>>
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
}
21 changes: 21 additions & 0 deletions src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageContainer className="min-h-[100dvh]">
<div className="my-auto flex h-full flex-col justify-center space-y-4">
<div className="shadow-4 flex w-full flex-col items-center space-y-2 bg-white px-4 py-2">
<h1 className="text-3xl font-extrabold">Not found</h1>
<Image src={PEANUTMAN_CRY.src} className="" alt="Peanutman crying 😭" width={96} height={96} />
<p>Woah there buddy, you're not supposed to be here.</p>
<Link href="/" className="btn btn-purple">
Take me home, I'm scared
</Link>
</div>
</div>
</PageContainer>
)
}
34 changes: 34 additions & 0 deletions src/app/receipt/[entryId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, string | string[] | undefined>>
}) {
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 (
<div className="flex min-h-[100dvh] flex-col items-center justify-center">
<TransactionDetailsReceipt className="w-full px-5" transaction={transactionDetails!} />
</div>
)
}
19 changes: 9 additions & 10 deletions src/components/AddMoney/components/MantecaDepositShareDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
import { Icon } from '@/components/Global/Icons/Icon'
import Image from 'next/image'
import { Card } from '@/components/0_Bruddle/Card'
import {
MANTECA_ARG_DEPOSIT_CUIT,
MANTECA_ARG_DEPOSIT_NAME,
MANTECA_COUNTRIES_CONFIG,
} from '@/constants/manteca.consts'
import { shortenStringLong, formatNumberForDisplay } from '@/utils'

const MantecaDepositShareDetails = ({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -119,8 +118,8 @@ const MantecaDepositShareDetails = ({
{depositAlias && <PaymentInfoRow label="Alias" value={depositAlias} allowCopy />}
{currentCountryDetails?.id === 'AR' && (
<>
<PaymentInfoRow label="Razón Social" value="Sixalime Sas" />
<PaymentInfoRow label="CUIT" value="30-71678845-3" />
<PaymentInfoRow label="Razón Social" value={MANTECA_ARG_DEPOSIT_NAME} />
<PaymentInfoRow label="CUIT" value={MANTECA_ARG_DEPOSIT_CUIT} />
</>
)}
<PaymentInfoRow label="Exchange Rate" value={`1 USD = ${exchangeRate} ${currencySymbol}`} />
Expand Down
143 changes: 105 additions & 38 deletions src/components/TransactionDetails/TransactionDetailsReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand All @@ -43,6 +50,7 @@ export const TransactionDetailsReceipt = ({
isModalOpen = false,
setIsModalOpen,
avatarUrl,
isPublic = false,
}: {
transaction: TransactionDetails | null
onClose?: () => void
Expand All @@ -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()
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
/>

Expand Down Expand Up @@ -460,20 +458,51 @@ export const TransactionDetailsReceipt = ({
<PaymentInfoRow label="Fee" value={feeDisplay} hideBottomBorder={shouldHideBorder('fee')} />
)}

{rowVisibilityConfig.mantecaDepositInfo && (
<>
{transaction.extraDataForDrawer?.receipt?.depositDetails?.depositAddress && (
<PaymentInfoRow
label={
country
? (MANTECA_COUNTRIES_CONFIG[country.id]?.depositAddressLabel ??
'Deposit Address')
: 'Deposit Address'
}
value={transaction.extraDataForDrawer.receipt.depositDetails.depositAddress}
allowCopy
/>
)}

{transaction.extraDataForDrawer?.receipt?.depositDetails?.depositAlias && (
<PaymentInfoRow
label="Alias"
value={transaction.extraDataForDrawer.receipt.depositDetails.depositAlias}
allowCopy
/>
)}
{country?.id === 'AR' && (
<>
<PaymentInfoRow label="Razón Social" value={MANTECA_ARG_DEPOSIT_NAME} />
<PaymentInfoRow label="CUIT" value={MANTECA_ARG_DEPOSIT_CUIT} />
</>
)}
</>
)}

{/* Exchange rate and original currency for completed bank_deposit transactions */}
{rowVisibilityConfig.exchangeRate && (
<>
{transaction.extraDataForDrawer?.receipt?.exchange_rate && (
<PaymentInfoRow
label={`Value in ${transaction.currency!.code}`}
value={`${transaction.currency!.code} ${formatNumberForDisplay(transaction.currency!.amount, { maxDecimals: 2 })}`}
value={`${transaction.currency!.code} ${formatCurrency(transaction.currency!.amount)}`}
/>
)}
{/* TODO: stop using snake_case!!!!! */}
{transaction.extraDataForDrawer?.receipt?.exchange_rate && (
<PaymentInfoRow
label="Exchange rate"
value={`1 ${transaction.currency!.code?.toUpperCase()} = $${formatAmount(Number(transaction.extraDataForDrawer.receipt.exchange_rate))}`}
value={`1 USD = ${transaction.currency!.code?.toUpperCase()} ${formatCurrency(transaction.extraDataForDrawer.receipt.exchange_rate)}`}
hideBottomBorder={shouldHideBorder('exchangeRate')}
/>
)}
Expand Down Expand Up @@ -926,9 +955,9 @@ export const TransactionDetailsReceipt = ({
</div>
)}

{shouldShowShareReceipt && transaction.extraDataForDrawer?.link && (
{shouldShowShareReceipt && !!getReceiptUrl(transaction) && (
<div className="pr-1">
<ShareButton url={transaction.extraDataForDrawer.link}>Share Receipt</ShareButton>
<ShareButton url={getReceiptUrl(transaction)!}>Share Receipt</ShareButton>
</div>
)}

Expand Down Expand Up @@ -991,6 +1020,44 @@ export const TransactionDetailsReceipt = ({
<span>Cancel deposit</span>
</Button>
)}
{transaction.extraDataForDrawer?.originalType === EHistoryEntryType.MANTECA_ONRAMP &&
transaction.status === 'pending' &&
setIsLoading &&
onClose && (
<Button
disabled={isLoading}
onClick={async () => {
setIsLoading(true)
try {
const result = await mantecaApi.cancelDeposit(transaction.id)
if (result.error) {
throw new Error(result.error)
}
// Invalidate queries and close drawer
queryClient
.invalidateQueries({
queryKey: [TRANSACTIONS],
})
.then(() => {
setIsLoading(false)
onClose()
})
} catch (error) {
captureException(error)
console.error('Error canceling deposit:', error)
setIsLoading(false)
}
}}
variant={'primary-soft'}
className="flex w-full items-center gap-1"
shadowSize="4"
>
<div className="flex items-center">
<Icon name="cancel" className="mr-0.5 min-w-3 rounded-full border border-black p-0.5" />
</div>
<span>Cancel deposit</span>
</Button>
)}

{isPendingBankRequest &&
transaction.extraDataForDrawer?.originalUserRole === EHistoryUserRole.SENDER &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type TransactionDetailsRowKey =
| 'peanutFee'
| 'comment'
| 'attachment'
| 'mantecaDepositInfo'

// rder of the rows in the receipt
export const transactionDetailsRowKeys: TransactionDetailsRowKey[] = [
Expand Down
4 changes: 4 additions & 0 deletions src/components/TransactionDetails/transactionTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading