diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index d80885d5e..35feb7f5c 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -161,6 +161,7 @@ const HistoryPage = () => { initials={transactionDetails.initials} transaction={transactionDetails} position={position} + haveSentMoneyToUser={transactionDetails.haveSentMoneyToUser} /> ) })() diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 4fcaf7dd3..444bf5605 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -216,12 +216,6 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern // determine card position for styling (first, middle, last, single) const position = getCardPosition(index, pendingRequests.length) - const haveSentMoneyToUser = - item.userRole === 'SENDER' - ? interactions[item.recipientAccount.userId] - : item.senderAccount?.userId - ? interactions[item.senderAccount.userId] - : false return ( ) })} diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 5f7c6411e..51d055b9f 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -6,13 +6,14 @@ import { TransactionDirection } from '@/components/TransactionDetails/Transactio import { TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' import { useTransactionDetailsDrawer } from '@/hooks/useTransactionDetailsDrawer' import { EHistoryEntryType, EHistoryUserRole } from '@/hooks/useTransactionHistory' -import { formatNumberForDisplay } from '@/utils' +import { formatNumberForDisplay, printableAddress } from '@/utils' import { getDisplayCurrencySymbol } from '@/utils/currency' import React from 'react' import { STABLE_COINS } from '@/constants' import Image from 'next/image' import StatusPill, { StatusPillType } from '../Global/StatusPill' import { VerifiedUserLabel } from '../UserHeader' +import { isAddress } from 'viem' export type TransactionType = | 'send' @@ -24,6 +25,7 @@ export type TransactionType = | 'bank_withdraw' | 'bank_deposit' | 'bank_request_fulfillment' + | 'claim_external' interface TransactionCardProps { type: TransactionType @@ -174,11 +176,10 @@ const TransactionCard: React.FC = ({ {isPending &&
}
- {/* */}
{/* display the action icon and type text */} @@ -229,6 +230,7 @@ function getActionIcon(type: TransactionType, direction: TransactionDirection): case 'withdraw': case 'bank_withdraw': case 'cashout': + case 'claim_external': iconName = 'arrow-up' iconSize = 8 break @@ -252,6 +254,9 @@ function getActionText(type: TransactionType): string { case 'bank_withdraw': actionText = 'Withdraw' break + case 'claim_external': + actionText = 'Claim' + break case 'bank_deposit': actionText = 'Add' break diff --git a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx index 54d3b6729..3396f0469 100644 --- a/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx +++ b/src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx @@ -22,6 +22,7 @@ export type TransactionDirection = | 'bank_claim' | 'bank_deposit' | 'bank_request_fulfillment' + | 'claim_external' interface TransactionDetailsHeaderCardProps { direction: TransactionDirection @@ -100,6 +101,15 @@ const getTitle = ( case 'bank_deposit': titleText = `${status === 'completed' ? 'Added' : 'Adding'} from ${displayName}` break + case 'claim_external': + if (status === 'completed') { + titleText = `Claimed to ${displayName}` + } else if (status === 'failed') { + titleText = `Claim to ${displayName}` + } else { + titleText = `Claiming to ${displayName}` + } + break default: titleText = displayName break @@ -124,6 +134,7 @@ const getIcon = (direction: TransactionDirection, isLinkTransaction?: boolean): return 'arrow-down-left' case 'withdraw': case 'bank_claim': + case 'claim_external': return 'arrow-up' case 'add': case 'bank_deposit': diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 5bd3548f9..3f9f0ccbf 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -8,7 +8,7 @@ import { useUserStore } from '@/redux/hooks' import { chargesApi } from '@/services/charges' import { sendLinksApi } from '@/services/sendLinks' import { formatAmount, formatDate, getInitialsFromName } from '@/utils' -import { formatIban, shortenAddress } from '@/utils/general.utils' +import { formatIban, printableAddress, shortenAddress, shortenAddressLong } from '@/utils/general.utils' import { getDisplayCurrencySymbol } from '@/utils/currency' import { cancelOnramp } from '@/app/actions/onramp' import { captureException } from '@sentry/nextjs' @@ -25,17 +25,8 @@ import CopyToClipboard from '../Global/CopyToClipboard' import MoreInfo from '../Global/MoreInfo' import CancelSendLinkModal from '../Global/CancelSendLinkModal' import { twMerge } from 'tailwind-merge' - -const getBankAccountLabel = (type: string) => { - switch (type.toLowerCase()) { - case 'iban': - return 'IBAN' - case 'clabe': - return 'CLABE' - default: - return 'Account Number' - } -} +import { isAddress } from 'viem' +import { getBankAccountLabel, TransactionDetailsRowKey, transactionDetailsRowKeys } from './transaction-details.utils' export const TransactionDetailsReceipt = ({ transaction, @@ -83,61 +74,71 @@ export const TransactionDetailsReceipt = ({ ) }, [transaction]) - const shouldHideCreatedRowBorder = useMemo(() => { - if (!transaction) return true - - // the border for the 'Created' row should be hidden if it is the last item in the details card. - // this logic checks for the presence of any other detail rows that might be displayed below it. - // if any of the following conditions are met, a row will be displayed, and the border should NOT be hidden. - - const hasTokenDetails = transaction.tokenDisplayDetails && transaction.sourceView === 'history' - const hasCancellationDate = transaction.status === 'cancelled' && transaction.cancelledDate - const hasClaimDate = transaction.status === 'completed' && transaction.claimedAt - const hasCompletionDate = - transaction.status === 'completed' && - transaction.completedAt && - transaction.extraDataForDrawer?.originalType !== EHistoryEntryType.DIRECT_SEND - const hasFee = transaction.fee !== undefined - const hasExchangeRateInfo = - transaction.direction === 'bank_deposit' && - transaction.status === 'completed' && - transaction.currency?.code && - transaction.currency.code.toUpperCase() !== 'USD' - const hasBankAccountDetails = transaction.bankAccountDetails && transaction.bankAccountDetails.identifier - const hasTransferId = - transaction.id && (transaction.direction === 'bank_withdraw' || transaction.direction === 'bank_claim') - const hasDepositInstructions = - (transaction.extraDataForDrawer?.originalType === EHistoryEntryType.BRIDGE_ONRAMP || - (isPendingBankRequest && - transaction.extraDataForDrawer?.originalUserRole === EHistoryUserRole.SENDER)) && - transaction.status === 'pending' && - transaction.extraDataForDrawer?.depositInstructions && - transaction.extraDataForDrawer.depositInstructions.bank_name - const hasMemo = !!transaction.memo?.trim() - const hasNetworkFee = transaction.networkFeeDetails && transaction.sourceView === 'status' - const hasAttachment = !!transaction.attachmentUrl - - const isNonPendingTransaction = transaction.status !== 'pending' - - // if any of these are true, it means there's another row below "Created", so we should NOT hide the border. - const hasFollowingRows = - hasTokenDetails || - hasCancellationDate || - hasClaimDate || - hasCompletionDate || - hasFee || - hasExchangeRateInfo || - hasBankAccountDetails || - hasTransferId || - hasDepositInstructions || - hasMemo || - hasNetworkFee || - hasAttachment || - isNonPendingTransaction - - return !hasFollowingRows + // config to determine which rows are visible in the receipt + // this helps in managing layout and borders without repeating code + const rowVisibilityConfig = useMemo((): Record => { + if (!transaction) { + // if no transaction, return all false + return transactionDetailsRowKeys.reduce( + (acc, key) => { + acc[key] = false + return acc + }, + {} as Record + ) + } + + // if transaction exists, calculate visibility for each row + return { + createdAt: !!transaction.createdAt, + to: transaction.direction === 'claim_external', + tokenAndNetwork: !!(transaction.tokenDisplayDetails && transaction.sourceView === 'history'), + txId: !!transaction.txHash, + cancelled: !!(transaction.status === 'cancelled' && transaction.cancelledDate), + claimed: !!(transaction.status === 'completed' && transaction.claimedAt), + completed: !!( + transaction.status === 'completed' && + transaction.completedAt && + transaction.extraDataForDrawer?.originalType !== EHistoryEntryType.DIRECT_SEND + ), + fee: transaction.fee !== undefined, + exchangeRate: !!( + transaction.direction === 'bank_deposit' && + transaction.status === 'completed' && + transaction.currency?.code && + transaction.currency.code.toUpperCase() !== 'USD' + ), + bankAccountDetails: !!(transaction.bankAccountDetails && transaction.bankAccountDetails.identifier), + transferId: !!( + transaction.id && + (transaction.direction === 'bank_withdraw' || transaction.direction === 'bank_claim') + ), + depositInstructions: !!( + (transaction.extraDataForDrawer?.originalType === EHistoryEntryType.BRIDGE_ONRAMP || + (isPendingBankRequest && + transaction.extraDataForDrawer?.originalUserRole === EHistoryUserRole.SENDER)) && + transaction.status === 'pending' && + transaction.extraDataForDrawer?.depositInstructions && + transaction.extraDataForDrawer.depositInstructions.bank_name + ), + peanutFee: transaction.status !== 'pending', + comment: !!transaction.memo?.trim(), + networkFee: !!(transaction.networkFeeDetails && transaction.sourceView === 'status'), + attachment: !!transaction.attachmentUrl, + } }, [transaction, isPendingBankRequest]) + const visibleRows = useMemo(() => { + // filter rowkeys to only include visible rows, maintaining the order + return transactionDetailsRowKeys.filter((key) => rowVisibilityConfig[key]) + }, [rowVisibilityConfig]) + + // helper to hide border for the last visible row + const shouldHideBorder = (rowKey: TransactionDetailsRowKey) => { + const lastVisibleRow = visibleRows[visibleRows.length - 1] + return rowKey === lastVisibleRow + } + const isPendingRequestee = useMemo(() => { if (!transaction) return false return ( @@ -252,15 +253,32 @@ export const TransactionDetailsReceipt = ({ {/* details card (date, fee, memo) and more */}
- {transaction.createdAt && ( + {rowVisibilityConfig.createdAt && ( + )} + + {rowVisibilityConfig.to && ( + + + {isAddress(transaction.userName) + ? printableAddress(transaction.userName) + : transaction.userName} + + +
+ } + hideBottomBorder={shouldHideBorder('to')} /> )} - {transaction.tokenDisplayDetails && transaction.sourceView === 'history' && ( + {rowVisibilityConfig.tokenAndNetwork && transaction.tokenDisplayDetails && ( } - hideBottomBorder={!transaction.networkFeeDetails && !transaction.peanutFeeDetails} + hideBottomBorder={shouldHideBorder('tokenAndNetwork')} + /> + )} + + {rowVisibilityConfig.txId && transaction.txHash && ( + + {shortenAddressLong(transaction.txHash)} + + + ) : ( +
+ {shortenAddressLong(transaction.txHash)} + +
+ ) + } + hideBottomBorder={shouldHideBorder('txId')} /> )} - {transaction.status === 'cancelled' && transaction.cancelledDate && ( + {rowVisibilityConfig.cancelled && ( <> {transaction.cancelledDate && ( )} )} - {transaction.status === 'completed' && transaction.claimedAt && ( + {rowVisibilityConfig.claimed && ( <> {transaction.claimedAt && ( - + )} )} - {transaction.status === 'completed' && transaction.completedAt && ( + {rowVisibilityConfig.completed && ( <> )} - {transaction.fee !== undefined && ( - + {rowVisibilityConfig.fee && ( + )} {/* Exchange rate and original currency for completed bank_deposit transactions */} - {transaction.direction === 'bank_deposit' && - transaction.status === 'completed' && - transaction.currency?.code && - transaction.currency.code.toUpperCase() !== 'USD' && ( - <> + {rowVisibilityConfig.exchangeRate && ( + <> + { + const currencyAmount = transaction.currency?.amount || transaction.amount.toString() + const currencySymbol = getDisplayCurrencySymbol(transaction.currency!.code) + return `${currencySymbol} ${formatAmount(Number(currencyAmount))}` + })()} + hideBottomBorder={false} + /> + {transaction.extraDataForDrawer?.receipt?.exchange_rate && ( { - const currencyAmount = - transaction.currency?.amount || transaction.amount.toString() - const currencySymbol = getDisplayCurrencySymbol(transaction.currency.code) - return `${currencySymbol} ${formatAmount(Number(currencyAmount))}` - })()} - hideBottomBorder={false} + label="Exchange rate" + value={`1 ${transaction.currency!.code?.toUpperCase()} = $${formatAmount(Number(transaction.extraDataForDrawer.receipt.exchange_rate))}`} + hideBottomBorder={shouldHideBorder('exchangeRate')} /> - {transaction.extraDataForDrawer?.receipt?.exchange_rate && ( - - )} - - )} + )} + + )} - {transaction.bankAccountDetails && transaction.bankAccountDetails.identifier && ( + {rowVisibilityConfig.bankAccountDetails && transaction.bankAccountDetails && ( @@ -385,212 +417,225 @@ export const TransactionDetailsReceipt = ({ )} } - hideBottomBorder={ - !( - transaction.id && - (transaction.direction === 'bank_withdraw' || - transaction.direction === 'bank_claim') - ) && - !(transaction.direction === 'bank_deposit' && transaction.status === 'pending') && - !transaction.memo && - !transaction.attachmentUrl && - !transaction.networkFeeDetails && - transaction.status === 'pending' + hideBottomBorder={shouldHideBorder('bankAccountDetails')} + /> + )} + {rowVisibilityConfig.transferId && ( + + {shortenAddress(transaction.id.toUpperCase(), 20)} + + } + hideBottomBorder={shouldHideBorder('transferId')} /> )} - {transaction.id && - (transaction.direction === 'bank_withdraw' || transaction.direction === 'bank_claim') && ( + + {/* Onramp deposit instructions for bridge_onramp transactions */} + {rowVisibilityConfig.depositInstructions && transaction.extraDataForDrawer?.depositInstructions && ( + <> + Deposit Message + + + } value={
- {shortenAddress(transaction.id.toUpperCase(), 20)} - + + {transaction.extraDataForDrawer.depositInstructions.deposit_message} + +
} - hideBottomBorder={ - !transaction.status || - (!transaction.memo && - !transaction.attachmentUrl && - !transaction.networkFeeDetails && - transaction.status === 'pending') - } + hideBottomBorder={shouldHideBorder('depositInstructions')} /> - )} - - {/* Onramp deposit instructions for bridge_onramp transactions */} - {(transaction.extraDataForDrawer?.originalType === EHistoryEntryType.BRIDGE_ONRAMP || - (isPendingBankRequest && - transaction.extraDataForDrawer?.originalUserRole === EHistoryUserRole.SENDER)) && - transaction.status === 'pending' && - transaction.extraDataForDrawer?.depositInstructions && - transaction.extraDataForDrawer.depositInstructions.bank_name && ( - <> - - Deposit Message - - - } - value={ -
- - {transaction.extraDataForDrawer.depositInstructions.deposit_message} - - -
- } - hideBottomBorder={false} - /> - {/* Toggle button for bank details */} -
- -
- - {/* Collapsible bank details */} - {showBankDetails && ( - <> - - - {transaction.extraDataForDrawer.depositInstructions.bank_name} - - - - } - hideBottomBorder={false} - /> - - - { - transaction.extraDataForDrawer.depositInstructions - .bank_address - } - - - - } - hideBottomBorder={false} - /> + {/* Toggle button for bank details */} +
+ +
- {/* European format (IBAN/BIC) */} - {transaction.extraDataForDrawer.depositInstructions.iban && - transaction.extraDataForDrawer.depositInstructions.bic ? ( - <> - - - {formatIban( - transaction.extraDataForDrawer.depositInstructions - .iban - )} - - - + {/* Collapsible bank details */} + {showBankDetails && ( + <> + + + {transaction.extraDataForDrawer.depositInstructions.bank_name} + + + + } + hideBottomBorder={false} + /> + + + {transaction.extraDataForDrawer.depositInstructions.bank_address} + + + + } + hideBottomBorder={false} + /> + + {/* European format (IBAN/BIC) */} + {transaction.extraDataForDrawer.depositInstructions.iban && + transaction.extraDataForDrawer.depositInstructions.bic ? ( + <> + + + {formatIban( + transaction.extraDataForDrawer.depositInstructions.iban + )} + + + + } + hideBottomBorder={false} + /> + + + {transaction.extraDataForDrawer.depositInstructions.bic} + + + + } + hideBottomBorder={false} + /> + {transaction.extraDataForDrawer.depositInstructions.account_holder_name && ( - {transaction.extraDataForDrawer.depositInstructions.bic} + { + transaction.extraDataForDrawer.depositInstructions + .account_holder_name + } } - hideBottomBorder={false} + hideBottomBorder={true} /> - {transaction.extraDataForDrawer.depositInstructions - .account_holder_name && ( - - - { - transaction.extraDataForDrawer - .depositInstructions.account_holder_name - } - - - - } - hideBottomBorder={true} - /> - )} - - ) : ( - /* US format (Account Number/Routing Number) */ - <> + )} + + ) : ( + /* US format (Account Number/Routing Number) */ + <> + + + { + transaction.extraDataForDrawer.depositInstructions + .bank_account_number + } + + + + } + hideBottomBorder={false} + /> + + + { + transaction.extraDataForDrawer.depositInstructions + .bank_routing_number + } + + + + } + hideBottomBorder={false} + /> + {transaction.extraDataForDrawer.depositInstructions + .bank_beneficiary_name && ( { transaction.extraDataForDrawer.depositInstructions - .bank_account_number + .bank_beneficiary_name } @@ -598,111 +643,63 @@ export const TransactionDetailsReceipt = ({ } hideBottomBorder={false} /> + )} + {transaction.extraDataForDrawer.depositInstructions + .bank_beneficiary_address && ( { transaction.extraDataForDrawer.depositInstructions - .bank_routing_number + .bank_beneficiary_address } } - hideBottomBorder={false} + hideBottomBorder={true} /> - {transaction.extraDataForDrawer.depositInstructions - .bank_beneficiary_name && ( - - - { - transaction.extraDataForDrawer - .depositInstructions.bank_beneficiary_name - } - - - - } - hideBottomBorder={false} - /> - )} - {transaction.extraDataForDrawer.depositInstructions - .bank_beneficiary_address && ( - - - { - transaction.extraDataForDrawer - .depositInstructions - .bank_beneficiary_address - } - - - - } - hideBottomBorder={true} - /> - )} - - )} - - )} - - )} + )} + + )} + + )} + + )} - {transaction.status !== 'pending' && ( + {rowVisibilityConfig.peanutFee && ( )} - {transaction.memo?.trim() && ( + {rowVisibilityConfig.comment && ( )} - {transaction.networkFeeDetails && transaction.sourceView === 'status' && ( + {rowVisibilityConfig.networkFee && ( )} - {transaction.attachmentUrl && ( + {rowVisibilityConfig.attachment && transaction.attachmentUrl && ( { + switch (type.toLowerCase()) { + case 'iban': + return 'IBAN' + case 'clabe': + return 'CLABE' + default: + return 'Account Number' + } +} diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 887d13d26..dbd4cd21c 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -173,15 +173,29 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact isPeerActuallyUser = !!entry.recipientAccount?.isUser isLinkTx = !isPeerActuallyUser } else if (entry.userRole === EHistoryUserRole.RECIPIENT) { - direction = 'receive' - transactionCardType = 'receive' - nameForDetails = entry.senderAccount?.username || entry.senderAccount?.identifier || 'Received via Link' - isPeerActuallyUser = !!entry.senderAccount?.isUser - isLinkTx = !isPeerActuallyUser + // if the recipient is not a peanut user, it's an external claim + if (entry.recipientAccount && !entry.recipientAccount.isUser) { + direction = 'claim_external' + transactionCardType = 'claim_external' + nameForDetails = entry.recipientAccount.identifier + isPeerActuallyUser = false + isLinkTx = true + } else { + direction = 'receive' + transactionCardType = 'receive' + nameForDetails = + entry.senderAccount?.username || entry.senderAccount?.identifier || 'Received via Link' + isPeerActuallyUser = !!entry.senderAccount?.isUser + isLinkTx = !isPeerActuallyUser + } } else if (entry.userRole === EHistoryUserRole.BOTH) { isPeerActuallyUser = true uiStatus = 'cancelled' nameForDetails = 'Sent via Link' + } else { + direction = 'claim_external' + transactionCardType = 'claim_external' + nameForDetails = entry.recipientAccount?.username || entry.recipientAccount?.identifier } break case EHistoryEntryType.REQUEST: @@ -307,7 +321,10 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact uiStatus = 'pending' break case 'COMPLETED': - uiStatus = EHistoryEntryType.SEND_LINK === entry.type ? 'pending' : 'completed' + uiStatus = + EHistoryEntryType.SEND_LINK === entry.type && direction !== 'claim_external' + ? 'pending' + : 'completed' break case 'SUCCESSFUL': case 'CLAIMED': @@ -389,7 +406,8 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact tokenSymbol: rewardData?.getSymbol(amount) ?? entry.tokenSymbol, initials: getInitialsFromName(nameForDetails), status: uiStatus, - isVerified: entry.isVerified, + isVerified: entry.isVerified && isPeerActuallyUser, + // only show verification badge if the other person is a peanut user date: new Date(entry.timestamp), fee: undefined, memo: entry.memo?.trim(),