From 001f1fcb4e23a7865264315e6053def06403f9c3 Mon Sep 17 00:00:00 2001 From: thibautbremand Date: Wed, 4 Oct 2023 09:47:38 +0200 Subject: [PATCH] Regroup transactions by date in Transaction history view --- .../TransactionListing/TransactionDetails.tsx | 121 +++++++ .../TransactionListing.test.tsx | 4 +- .../TransactionListing/TransactionListing.tsx | 318 +++++------------- .../TransactionListing/format.util.ts | 68 ++++ 4 files changed, 269 insertions(+), 242 deletions(-) create mode 100644 packages/extension/src/components/organisms/TransactionListing/TransactionDetails.tsx create mode 100644 packages/extension/src/components/organisms/TransactionListing/format.util.ts diff --git a/packages/extension/src/components/organisms/TransactionListing/TransactionDetails.tsx b/packages/extension/src/components/organisms/TransactionListing/TransactionDetails.tsx new file mode 100644 index 000000000..a24289fce --- /dev/null +++ b/packages/extension/src/components/organisms/TransactionListing/TransactionDetails.tsx @@ -0,0 +1,121 @@ +import { FC } from 'react'; + +import { Divider, List, ListItem, ListItemText } from '@mui/material'; +import { convertHexToString, dropsToXrp } from 'xrpl'; + +import { AccountTransaction } from '../../../types'; +import { formatFlagsToNumber } from '../../../utils'; +import { formatDate, formatTransaction } from './format.util'; + +export interface TransactionDetailsProps { + transaction: AccountTransaction | null; + publicAddress: string; +} + +const renderDestinationField = (transaction: AccountTransaction): JSX.Element | null => { + if (transaction.tx && 'Destination' in transaction.tx) { + return ( + + + + ); + } + return null; +}; + +export const TransactionDetails: FC = ({ transaction, publicAddress }) => { + if (!transaction) { + return null; + } + + return ( + <> + + + + + + {renderDestinationField(transaction)} + + + + + + + + + + + + + + {transaction.tx?.Memos?.[0]?.Memo?.MemoData ? ( + <> + + + + + + ) : null} + {transaction.meta && + typeof transaction.meta === 'object' && + 'nftoken_id' in transaction.meta ? ( + <> + + + + + + ) : null} + {transaction.meta && + typeof transaction.meta === 'object' && + 'offer_id' in transaction.meta ? ( + <> + + + + + + ) : null} + + + + + {transaction.tx && 'DestinationTag' in transaction.tx && transaction.tx?.DestinationTag ? ( + <> + + + + + + ) : null} + {transaction.tx && 'Flags' in transaction.tx && transaction.tx?.Flags ? ( + <> + + + + + + ) : null} + + + + + + + + + ); +}; diff --git a/packages/extension/src/components/organisms/TransactionListing/TransactionListing.test.tsx b/packages/extension/src/components/organisms/TransactionListing/TransactionListing.test.tsx index 1f1f6eeff..87928222e 100644 --- a/packages/extension/src/components/organisms/TransactionListing/TransactionListing.test.tsx +++ b/packages/extension/src/components/organisms/TransactionListing/TransactionListing.test.tsx @@ -33,9 +33,9 @@ describe('TransactionListing', () => { test('renders the list of transactions', async () => { const screen = render(); expect(screen.getByText('Payment sent - 20 XRP')).toBeInTheDocument(); - expect(screen.getByText('12 February 2023 - 17:31')).toBeInTheDocument(); + expect(screen.getByText('Feb 12, 2023 - 17:31')).toBeInTheDocument(); expect(screen.getByText('TrustLine transaction')).toBeInTheDocument(); - expect(screen.getByText('12 February 2023 - 06:48')).toBeInTheDocument(); + expect(screen.getByText('Feb 12, 2023 - 06:48')).toBeInTheDocument(); }); test('renders the transaction details when the transaction is clicked', async () => { diff --git a/packages/extension/src/components/organisms/TransactionListing/TransactionListing.tsx b/packages/extension/src/components/organisms/TransactionListing/TransactionListing.tsx index b6c4b7bc7..559531489 100644 --- a/packages/extension/src/components/organisms/TransactionListing/TransactionListing.tsx +++ b/packages/extension/src/components/organisms/TransactionListing/TransactionListing.tsx @@ -1,136 +1,52 @@ -import { FC, useCallback, useState } from 'react'; +import { FC, useCallback, useMemo, useState } from 'react'; import TransactionIcon from '@mui/icons-material/CompareArrows'; -import { Divider, List, ListItem, ListItemIcon, ListItemText, Paper } from '@mui/material'; +import { List, ListItem, ListItemIcon, ListItemText, Paper, Typography } from '@mui/material'; import { unix } from 'moment'; -import { convertHexToString, dropsToXrp } from 'xrpl'; import { useWallet } from '../../../contexts'; -import { AccountTransaction, TransactionTypes } from '../../../types'; -import { formatAmount, formatFlagsToNumber } from '../../../utils'; +import { AccountTransaction } from '../../../types'; import { InformationMessage } from '../../molecules'; import { DialogPage, PageWithSpinner } from '../../templates'; +import { formatDate, formatTransaction } from './format.util'; +import { TransactionDetails } from './TransactionDetails'; export interface TransactionListingProps { transactions: AccountTransaction[]; } -const formatTransaction = (transaction: AccountTransaction, publicAddress: string): string => { - switch (transaction.tx?.TransactionType) { - case TransactionTypes.Payment: - // Might need to handle more use case - const amount = formatAmount(transaction.tx.Amount); - if (transaction.tx.Destination === publicAddress) { - return `Payment received - ${amount}`; - } - return `Payment sent - ${amount}`; - case TransactionTypes.TrustSet: { - // Might need to handle more use case - return 'TrustLine transaction'; - } - case TransactionTypes.EscrowCreate: - return 'Create escrow'; - case TransactionTypes.EscrowFinish: - return 'Finish escrow'; - case TransactionTypes.EscrowCancel: - return 'Cancel escrow'; - case TransactionTypes.AccountSet: - // Might need to handle more use cases here - return 'Edit account'; - case TransactionTypes.SignerListSet: - return 'Set Signer List'; - case TransactionTypes.OfferCreate: - // Might need to handle more use cases here - return 'Create offer'; - case TransactionTypes.OfferCancel: - return 'Cancel offer'; - case TransactionTypes.AccountDelete: - return 'Delete Account'; - case TransactionTypes.SetRegularKey: - if (transaction.tx.RegularKey) { - return 'Set Regular Key'; - } - return 'Remove Regular Key'; - case TransactionTypes.DepositPreauth: - if (transaction.tx.Authorize) { - return 'Authorize deposit'; - } - return 'Unauthorize deposit'; - case TransactionTypes.CheckCreate: - return 'Create check'; - case TransactionTypes.CheckCash: - return 'Cash check'; - case TransactionTypes.CheckCancel: - return 'Cancel check'; - case TransactionTypes.TicketCreate: - return 'Create ticket'; - case TransactionTypes.PaymentChannelCreate: - return 'Create payment channel'; - case TransactionTypes.PaymentChannelClaim: - return 'Claim payment channel'; - case TransactionTypes.PaymentChannelFund: - return 'Fund payment channel'; - case TransactionTypes.NFTokenMint: - return 'Mint NFT'; - case TransactionTypes.NFTokenBurn: - return 'Burn NFT'; - case TransactionTypes.NFTokenCreateOffer: - return 'Create NFT offer'; - case TransactionTypes.NFTokenCancelOffer: - return 'Cancel NFT offer'; - case TransactionTypes.NFTokenAcceptOffer: - return 'Accept NFT offer'; - default: - return 'Unsupported transaction'; - } -}; - -const renderDestinationField = (transaction: AccountTransaction): JSX.Element | null => { - if (transaction.tx && 'Destination' in transaction.tx) { - return ( - - - - ); - } - return null; -}; - -const formatDate = (unixTimestamp: number): string => { - return unix(946684800 + unixTimestamp).format('DD MMMM YYYY - HH:mm'); -}; - export const TransactionListing: FC = ({ transactions }) => { - const { getCurrentWallet } = useWallet(); + const [openedTx, setOpenedTx] = useState(null); - const [tx, setTx] = useState( - transactions.length > 0 ? transactions.map((t) => ({ ...t, touched: false })) : [] - ); + const { getCurrentWallet } = useWallet(); const wallet = getCurrentWallet(); - const handleClick = useCallback( - (index: number) => { - const newTx = [...tx]; - newTx[index].touched = !newTx[index].touched; - setTx(newTx); - }, - [tx] - ); + const handleClick = useCallback((transaction: AccountTransaction) => { + setOpenedTx(transaction); + }, []); - const handleClose = useCallback( - (index: number) => { - const newTx = [...tx]; - newTx[index].touched = false; - setTx(newTx); - }, - [tx] - ); + const handleClose = useCallback(() => { + setOpenedTx(null); + }, []); + + const transactionsByDate = useMemo(() => { + const grouped = new Map(); + transactions.forEach((transaction) => { + const date = transaction.tx?.date + ? unix(946684800 + transaction.tx.date).format('MMM DD, YYYY') + : 'Unknown Date'; + const group = grouped.get(date) || []; + group.push(transaction); + grouped.set(date, group); + }); + return grouped; + }, [transactions]); if (!wallet) { return ; } - if (tx.length === 0) { + if (transactions.length === 0) { return (
@@ -141,134 +57,56 @@ export const TransactionListing: FC = ({ transactions } } return ( - - {tx.map((transaction, index) => ( -
- handleClick(index)} - > - - - - - - - - handleClose(index)} - open={transaction.touched} - data-testid="dialog" - > - - - - - - {renderDestinationField(transaction)} - - - - - - - - - - - - - - {transaction.tx?.Memos?.[0]?.Memo?.MemoData ? ( - <> - - - - - - ) : null} - {transaction.meta && - typeof transaction.meta === 'object' && - 'nftoken_id' in transaction.meta ? ( - <> - - - - - - ) : null} - {transaction.meta && - typeof transaction.meta === 'object' && - 'offer_id' in transaction.meta ? ( - <> - - - - - - ) : null} - - - - - {transaction.tx && - 'DestinationTag' in transaction.tx && - transaction.tx?.DestinationTag ? ( - <> - - - - - - ) : null} - {transaction.tx && 'Flags' in transaction.tx && transaction.tx?.Flags ? ( - <> - - - - - - ) : null} - - - - - - - - -
- ))} -
+ <> + + {Array.from(transactionsByDate).map(([date, transactionsForDate]) => ( +
+ + {date} + + {transactionsForDate.map((transaction, index) => ( + handleClick(transaction)} + > + + + + + + + + ))} +
+ ))} +
+ + + + ); }; diff --git a/packages/extension/src/components/organisms/TransactionListing/format.util.ts b/packages/extension/src/components/organisms/TransactionListing/format.util.ts new file mode 100644 index 000000000..b475f027d --- /dev/null +++ b/packages/extension/src/components/organisms/TransactionListing/format.util.ts @@ -0,0 +1,68 @@ +import { unix } from 'moment'; +import { DepositPreauth, Payment, SetRegularKey } from 'xrpl'; + +import { AccountTransaction, TransactionTypes } from '../../../types'; +import { formatAmount } from '../../../utils'; + +export const formatDate = (unixTimestamp: number): string => { + return unix(946684800 + unixTimestamp).format('MMM DD, YYYY - HH:mm'); +}; + +type TransactionFormatter = (transaction: AccountTransaction, publicAddress: string) => string; + +const transactionMappers: Record = { + [TransactionTypes.Payment]: (transaction, publicAddress) => { + const amount = formatAmount((transaction.tx as Payment).Amount); + return (transaction.tx as Payment).Destination === publicAddress + ? `Payment received - ${amount}` + : `Payment sent - ${amount}`; + }, + [TransactionTypes.TrustSet]: () => 'TrustLine transaction', + [TransactionTypes.EscrowCreate]: () => 'Create escrow', + [TransactionTypes.EscrowFinish]: () => 'Finish escrow', + [TransactionTypes.EscrowCancel]: () => 'Cancel escrow', + [TransactionTypes.AccountSet]: () => 'Edit account', + [TransactionTypes.SignerListSet]: () => 'Set Signer List', + [TransactionTypes.OfferCreate]: () => 'Create offer', + [TransactionTypes.OfferCancel]: () => 'Cancel offer', + [TransactionTypes.AccountDelete]: () => 'Delete Account', + [TransactionTypes.SetRegularKey]: (transaction) => { + if ((transaction.tx as SetRegularKey).RegularKey) { + return 'Set Regular Key'; + } + return 'Remove Regular Key'; + }, + [TransactionTypes.DepositPreauth]: (transaction) => { + if ((transaction.tx as DepositPreauth).Authorize) { + return 'Authorize Deposit'; + } + return 'Unauthorize Deposit'; + }, + [TransactionTypes.CheckCreate]: () => 'Create check', + [TransactionTypes.CheckCash]: () => 'Cash check', + [TransactionTypes.CheckCancel]: () => 'Cancel check', + [TransactionTypes.TicketCreate]: () => 'Create ticket', + [TransactionTypes.PaymentChannelCreate]: () => 'Create payment channel', + [TransactionTypes.PaymentChannelClaim]: () => 'Claim payment channel', + [TransactionTypes.PaymentChannelFund]: () => 'Fund payment channel', + [TransactionTypes.NFTokenMint]: () => 'Mint NFT', + [TransactionTypes.NFTokenBurn]: () => 'Burn NFT', + [TransactionTypes.NFTokenCreateOffer]: () => 'Create NFT offer', + [TransactionTypes.NFTokenCancelOffer]: () => 'Cancel NFT offer', + [TransactionTypes.NFTokenAcceptOffer]: () => 'Accept NFT offer' +}; + +export const formatTransaction = ( + transaction: AccountTransaction, + publicAddress: string +): string => { + if (!transaction.tx) { + return 'Unsupported transaction'; + } + + const txType = transaction.tx.TransactionType; + const formatter = transactionMappers[txType as keyof typeof transactionMappers]; + + // If formatter doesn't exist for this txType, return the txType as default message + return formatter ? formatter(transaction, publicAddress) : txType; +};