From 81634e3ac07dcd39bfad191c1aba6286ce21fb7c Mon Sep 17 00:00:00 2001 From: Lemonexe Date: Thu, 6 Jun 2024 13:44:38 +0200 Subject: [PATCH 1/6] feat: group pending transactions by date refactor groupTransactionsByDate to enable/disable grouping pending split DayHeader to create PendingHeader refactor TransactionList: split pending & confirmed, rather than lumping them together refactor useMemo maps to FCs rename DayHeader to GroupHeader which more properly reflects its purpose Revert "REVERT ME: mock data for development" This reverts commit 5f2cf76de6afa046a32bdae5bee4937b59a16542. CR: use arrayPartition CR: don't use FC GroupHeaders file reorganization rename Wrapper to HeaderWrapper extract TransactionGroupedList refactor groupTransactionsByDate monkey-patch original behavior in suite-native more test cases --- .../TransactionGroupedList.tsx | 62 ++++++++++ .../TransactionList/TransactionList.tsx | 91 +++++++-------- .../TransactionsGroup/CommonComponents.tsx | 47 ++++++++ .../TransactionsGroup/DayHeader.tsx | 110 +++++------------- .../TransactionsGroup/PendingGroupHeader.tsx | 14 +++ .../TransactionsGroup/TransactionsGroup.tsx | 1 - .../src/__tests__/transactionUtils.test.ts | 20 ++-- .../wallet-utils/src/transactionUtils.ts | 9 +- suite-native/transactions/package.json | 1 + .../TransactionsList/TransactionList.tsx | 13 ++- 10 files changed, 217 insertions(+), 151 deletions(-) create mode 100644 packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx create mode 100644 packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents.tsx create mode 100644 packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx new file mode 100644 index 00000000000..6a43c822761 --- /dev/null +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx @@ -0,0 +1,62 @@ +import { + getAccountNetwork, + GroupedTransactionsByDate, + groupJointTransactions, +} from '@suite-common/wallet-utils'; +import { CoinjoinBatchItem } from 'src/components/wallet/TransactionItem/CoinjoinBatchItem'; +import { useSelector } from 'src/hooks/suite'; +import { Account, WalletAccountTransaction } from 'src/types/wallet'; +import { TransactionItem } from 'src/components/wallet/TransactionItem/TransactionItem'; +import { TransactionsGroup } from './TransactionsGroup/TransactionsGroup'; +import { selectLabelingDataForAccount } from 'src/reducers/suite/metadataReducer'; +import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; + +interface TransactionGroupedListProps { + transactionGroups: GroupedTransactionsByDate; + symbol: WalletAccountTransaction['symbol']; + account: Account; + isPending: boolean; +} + +export const TransactionGroupedList = ({ + transactionGroups, + symbol, + account, + isPending, +}: TransactionGroupedListProps) => { + const localCurrency = useSelector(selectLocalCurrency); + const accountMetadata = useSelector(state => selectLabelingDataForAccount(state, account.key)); + const network = getAccountNetwork(account); + + return Object.entries(transactionGroups).map(([dateKey, value], groupIndex) => ( + + {groupJointTransactions(value).map((item, index) => + item.type === 'joint-batch' ? ( + + ) : ( + + ), + )} + + )); +}; diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionList.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionList.tsx index e68a9859ade..73e9eb0f618 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionList.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionList.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import useDebounce from 'react-use/lib/useDebounce'; @@ -6,21 +6,18 @@ import { fetchAllTransactionsForAccountThunk, fetchTransactionsPageThunk, } from '@suite-common/wallet-core'; +import { arrayPartition } from '@trezor/utils'; import { - groupTransactionsByDate, advancedSearchTransactions, - groupJointTransactions, - getAccountNetwork, + groupTransactionsByDate, + isPending, } from '@suite-common/wallet-utils'; -import { CoinjoinBatchItem } from 'src/components/wallet/TransactionItem/CoinjoinBatchItem'; import { Translation } from 'src/components/suite'; import { DashboardSection } from 'src/components/dashboard'; import { useDispatch, useSelector } from 'src/hooks/suite'; -import { WalletAccountTransaction, Account } from 'src/types/wallet'; +import { Account, WalletAccountTransaction } from 'src/types/wallet'; import { TransactionListActions } from './TransactionListActions/TransactionListActions'; -import { TransactionItem } from 'src/components/wallet/TransactionItem/TransactionItem'; import { Pagination } from 'src/components/wallet'; -import { TransactionsGroup } from './TransactionsGroup/TransactionsGroup'; import { SkeletonTransactionItem } from './SkeletonTransactionItem'; import { NoSearchResults } from './NoSearchResults'; import { findAnchorTransactionPage } from 'src/utils/suite/anchor'; @@ -28,7 +25,8 @@ import { TransactionCandidates } from './TransactionCandidates'; import { selectLabelingDataForAccount } from 'src/reducers/suite/metadataReducer'; import { getTxsPerPage } from '@suite-common/suite-utils'; import { SkeletonStack } from '@trezor/components'; -import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; +import { PendingGroupHeader } from './TransactionsGroup/PendingGroupHeader'; +import { TransactionGroupedList } from './TransactionGroupedList'; const StyledSection = styled(DashboardSection)` margin-bottom: 20px; @@ -55,11 +53,9 @@ export const TransactionList = ({ customTotalItems, isExportable = true, }: TransactionListProps) => { - const localCurrency = useSelector(selectLocalCurrency); const anchor = useSelector(state => state.router.anchor); const dispatch = useDispatch(); const accountMetadata = useSelector(state => selectLabelingDataForAccount(state, account.key)); - const network = getAccountNetwork(account); // Search const [searchQuery, setSearchQuery] = useState(''); @@ -123,49 +119,18 @@ export const TransactionList = ({ [searchedTransactions, startIndex, stopIndex], ); - const transactionsByDate = useMemo( - () => groupTransactionsByDate(slicedTransactions), + const [pendingTxs, confirmedTxs] = useMemo( + () => arrayPartition(slicedTransactions, isPending), [slicedTransactions], ); - const listItems = useMemo( - () => - Object.entries(transactionsByDate).map(([dateKey, value], groupIndex) => { - const isPending = dateKey === 'pending'; - - return ( - - {groupJointTransactions(value).map((item, index) => - item.type === 'joint-batch' ? ( - - ) : ( - - ), - )} - - ); - }), - [transactionsByDate, account.key, localCurrency, symbol, network, accountMetadata], + const pendingTxsByDate = useMemo( + () => groupTransactionsByDate(pendingTxs, 'day'), + [pendingTxs], + ); + const confirmedTxsByDate = useMemo( + () => groupTransactionsByDate(confirmedTxs, 'day'), + [confirmedTxs], ); // if total pages cannot be determined check current page and number of txs (XRP) @@ -204,7 +169,29 @@ export const TransactionList = ({ ) : ( - <>{areTransactionsAvailable ? : listItems} + <> + {areTransactionsAvailable ? ( + + ) : ( + <> + {pendingTxs.length > 0 && ( + + )} + + + + )} + )} {showPagination && ( diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents.tsx new file mode 100644 index 00000000000..2c663003d5a --- /dev/null +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents.tsx @@ -0,0 +1,47 @@ +import styled from 'styled-components'; +import { variables } from '@trezor/components'; +import { zIndices } from '@trezor/theme'; +import { HiddenPlaceholder } from 'src/components/suite'; +import { SUBPAGE_NAV_HEIGHT } from 'src/constants/suite/layout'; + +export const HeaderWrapper = styled.div` + display: flex; + position: sticky; + background: ${({ theme }) => theme.backgroundSurfaceElevation0}; + top: ${SUBPAGE_NAV_HEIGHT}; + align-items: center; + justify-content: space-between; + flex: 1; + padding-top: 8px; + padding-bottom: 8px; + padding-right: 24px; + z-index: ${zIndices.secondaryStickyBar}; +`; + +export const Col = styled(HiddenPlaceholder)` + font-size: ${variables.FONT_SIZE.SMALL}; + color: ${({ theme }) => theme.TYPE_LIGHT_GREY}; + font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD}; +`; + +export const ColDate = styled(Col)` + font-variant-numeric: tabular-nums; + flex: 1; +`; + +export const ColPending = styled(Col)` + color: ${({ theme }) => theme.TYPE_ORANGE}; + font-variant-numeric: tabular-nums; +`; + +export const ColAmount = styled(Col)<{ $isVisible?: boolean }>` + padding-left: 16px; + text-align: right; + opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)}; + transition: opacity 0.1s; +`; + +export const ColFiat = styled(Col)` + padding-left: 16px; + text-align: right; +`; diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx index 8c86c9fdcd2..59425427b7e 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx @@ -1,57 +1,10 @@ import { FormattedDate } from 'react-intl'; -import styled from 'styled-components'; import { BigNumber } from '@trezor/utils/src/bigNumber'; - -import { variables } from '@trezor/components'; -import { zIndices } from '@trezor/theme'; import { useFormatters } from '@suite-common/formatters'; -import { parseTransactionDateKey, isTestnet } from '@suite-common/wallet-utils'; - -import { Translation, HiddenPlaceholder, FormattedCryptoAmount } from 'src/components/suite'; +import { isTestnet, parseTransactionDateKey } from '@suite-common/wallet-utils'; +import { FormattedCryptoAmount, HiddenPlaceholder } from 'src/components/suite'; import { Network } from 'src/types/wallet'; -import { SUBPAGE_NAV_HEIGHT } from 'src/constants/suite/layout'; - -const Wrapper = styled.div` - display: flex; - position: sticky; - background: ${({ theme }) => theme.backgroundSurfaceElevation0}; - top: ${SUBPAGE_NAV_HEIGHT}; - align-items: center; - justify-content: space-between; - flex: 1; - padding-top: 8px; - padding-bottom: 8px; - padding-right: 24px; - z-index: ${zIndices.secondaryStickyBar}; -`; - -const Col = styled(HiddenPlaceholder)` - font-size: ${variables.FONT_SIZE.SMALL}; - color: ${({ theme }) => theme.TYPE_LIGHT_GREY}; - font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD}; -`; - -const ColDate = styled(Col)` - font-variant-numeric: tabular-nums; - flex: 1; -`; - -const ColPending = styled(Col)` - color: ${({ theme }) => theme.TYPE_ORANGE}; - font-variant-numeric: tabular-nums; -`; - -const ColAmount = styled(Col)<{ $isVisible?: boolean }>` - padding-left: 16px; - text-align: right; - opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)}; - transition: opacity 0.1s; -`; - -const ColFiat = styled(Col)` - padding-left: 16px; - text-align: right; -`; +import { ColAmount, ColDate, ColFiat, HeaderWrapper } from './CommonComponents'; interface DayHeaderProps { dateKey: string; @@ -59,7 +12,6 @@ interface DayHeaderProps { totalAmount: BigNumber; totalFiatAmountPerDay: BigNumber; localCurrency: string; - txsCount?: number; isMissingFiatRates?: boolean; isHovered?: boolean; } @@ -71,7 +23,6 @@ export const DayHeader = ({ totalAmount, totalFiatAmountPerDay, localCurrency, - txsCount, isMissingFiatRates, isHovered, }: DayHeaderProps) => { @@ -81,39 +32,30 @@ export const DayHeader = ({ const showFiatValue = !isTestnet(symbol); return ( - - {dateKey === 'pending' ? ( - - •{' '} - {txsCount} - - ) : ( - <> - - + + + + + {totalAmount.gt(0) && +} + + + {showFiatValue && !isMissingFiatRates && ( + + + {totalFiatAmountPerDay.gt(0) && +} + - - - {totalAmount.gt(0) && +} - - - {showFiatValue && !isMissingFiatRates && ( - - - {totalFiatAmountPerDay.gt(0) && +} - - - - )} - + + )} - + ); }; diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx new file mode 100644 index 00000000000..43f93adc548 --- /dev/null +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx @@ -0,0 +1,14 @@ +import { Translation } from 'src/components/suite'; +import { ColPending, HeaderWrapper } from './CommonComponents'; + +type PendingGroupHeaderProps = { txsCount: number }; + +export const PendingGroupHeader = ({ txsCount }: PendingGroupHeaderProps) => { + return ( + + + • {txsCount} + + + ); +}; diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx index a3ff539ccd4..287629090ab 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx @@ -98,7 +98,6 @@ export const TransactionsGroup = ({ isHovered={isHovered} totalAmount={totalAmountPerDay} totalFiatAmountPerDay={totalFiatAmountPerDay} - txsCount={transactions.length} localCurrency={localCurrency} isMissingFiatRates={isMissingFiatRates} /> diff --git a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts index 9bef546dd1f..d80f983df93 100644 --- a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts @@ -43,21 +43,23 @@ describe('transaction utils', () => { it('groupTransactionsByDate - groupBy day', () => { const groupedTxs = groupTransactionsByDate([ + getWalletTransaction({ blockTime: 1565792979, blockHeight: undefined }), getWalletTransaction({ blockTime: 1565792979, blockHeight: 5 }), getWalletTransaction({ blockTime: 1565792379, blockHeight: 4 }), - getWalletTransaction({ blockHeight: 0 }), + getWalletTransaction({ blockHeight: 0, blockTime: 0 }), getWalletTransaction({ blockTime: 1570147200, blockHeight: 2 }), getWalletTransaction({ blockTime: 1570127200, blockHeight: 3 }), - getWalletTransaction({ blockHeight: undefined }), + getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ]); expect(groupedTxs).toEqual({ pending: [ - getWalletTransaction({ blockHeight: 0 }), - getWalletTransaction({ blockHeight: undefined }), + getWalletTransaction({ blockHeight: 0, blockTime: 0 }), + getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ], '2019-10-4': [getWalletTransaction({ blockTime: 1570147200, blockHeight: 2 })], '2019-10-3': [getWalletTransaction({ blockTime: 1570127200, blockHeight: 3 })], '2019-8-14': [ + getWalletTransaction({ blockTime: 1565792979, blockHeight: undefined }), getWalletTransaction({ blockTime: 1565792979, blockHeight: 5 }), getWalletTransaction({ blockTime: 1565792379, blockHeight: 4 }), ], @@ -67,12 +69,13 @@ describe('transaction utils', () => { it('groupTransactionsByDate - groupBy month', () => { const groupedTxs = groupTransactionsByDate( [ + getWalletTransaction({ blockTime: 1565792979, blockHeight: undefined }), getWalletTransaction({ blockTime: 1565792979, blockHeight: 5 }), getWalletTransaction({ blockTime: 1565792379, blockHeight: 4 }), - getWalletTransaction({ blockHeight: 0 }), + getWalletTransaction({ blockHeight: 0, blockTime: 0 }), getWalletTransaction({ blockTime: 1570147200, blockHeight: 2 }), getWalletTransaction({ blockTime: 1570127200, blockHeight: 3 }), - getWalletTransaction({ blockHeight: undefined }), + getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ], 'month', ); @@ -83,14 +86,15 @@ describe('transaction utils', () => { const secondMonth = generateTransactionMonthKey(new Date(secondBlocktime * 1000)); expect(groupedTxs).toEqual({ pending: [ - getWalletTransaction({ blockHeight: 0 }), - getWalletTransaction({ blockHeight: undefined }), + getWalletTransaction({ blockHeight: 0, blockTime: 0 }), + getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ], [firstMonth]: [ getWalletTransaction({ blockTime: firstBlocktime, blockHeight: 3 }), getWalletTransaction({ blockTime: 1570147200, blockHeight: 2 }), ], [secondMonth]: [ + getWalletTransaction({ blockTime: secondBlocktime, blockHeight: undefined }), getWalletTransaction({ blockTime: secondBlocktime, blockHeight: 5 }), getWalletTransaction({ blockTime: 1565792379, blockHeight: 4 }), ], diff --git a/suite-common/wallet-utils/src/transactionUtils.ts b/suite-common/wallet-utils/src/transactionUtils.ts index a87a52c10f1..c022dc216ee 100644 --- a/suite-common/wallet-utils/src/transactionUtils.ts +++ b/suite-common/wallet-utils/src/transactionUtils.ts @@ -76,6 +76,7 @@ export const generateTransactionMonthKey = (d: Date): MonthKey => export const parseTransactionMonthKey = (key: MonthKey): Date => new Date(key); +export type GroupedTransactionsByDate = Record; /** * Returns object with transactions grouped by a date. Key is a string in YYYY-MM-DD format. * Pending txs are assigned to key 'pending'. @@ -85,7 +86,7 @@ export const parseTransactionMonthKey = (key: MonthKey): Date => new Date(key); export const groupTransactionsByDate = ( transactions: WalletAccountTransaction[], groupBy: 'day' | 'month' = 'day', -) => { +): GroupedTransactionsByDate => { // Note: We should use ts-belt for sorting this array but currently, there can be undefined inside // Built-in sort doesn't include undefined elements but ts-belt does so there will be some refactoring involved. const keyFormatter = @@ -96,10 +97,10 @@ export const groupTransactionsByDate = ( // There could be some undefined/null in array, not sure how it happens. Maybe related to pagination? .filter(transaction => !!transaction) .sort(sortByBlockHeight) - .reduce<{ [key: string]: WalletAccountTransaction[] }>((r, item) => { - const isTxPending = isPending(item); + .reduce((r, item) => { + // pending txs are grouped as 'pending' only if blockTime is unavailable (otherwise sorted by blockTime) const key = - !isTxPending && item.blockTime && item.blockTime > 0 + item.blockTime && item.blockTime > 0 ? keyFormatter(new Date(item.blockTime * 1000)) : 'pending'; const prev = r[key] ?? []; diff --git a/suite-native/transactions/package.json b/suite-native/transactions/package.json index 4d3ea578be6..2d9578d5a2c 100644 --- a/suite-native/transactions/package.json +++ b/suite-native/transactions/package.json @@ -37,6 +37,7 @@ "@trezor/blockchain-link-types": "workspace:*", "@trezor/styles": "workspace:*", "@trezor/theme": "workspace:*", + "@trezor/utils": "workspace:*", "proxy-memoize": "2.0.2", "react": "18.2.0", "react-native": "0.74.1", diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx index 0f1f8fb3940..f24bf1fc51e 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx @@ -12,7 +12,7 @@ import { TransactionsRootState, } from '@suite-common/wallet-core'; import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; -import { groupTransactionsByDate, MonthKey } from '@suite-common/wallet-utils'; +import { groupTransactionsByDate, isPending, MonthKey } from '@suite-common/wallet-utils'; import { Box, Loader } from '@suite-native/atoms'; import { EthereumTokenTransfer, @@ -21,6 +21,7 @@ import { } from '@suite-native/ethereum-tokens'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { SettingsSliceRootState } from '@suite-native/settings'; +import { arrayPartition } from '@trezor/utils'; import { TransactionsEmptyState } from '../TransactionsEmptyState'; import { TokenTransferListItem } from './TokenTransferListItem'; @@ -162,7 +163,15 @@ export const TransactionList = ({ }, [dispatch, accountKey, fetchTransactions]); const data = useMemo((): TransactionListItem[] => { - const accountTransactionsByMonth = groupTransactionsByDate(transactions, 'month'); + // groupTransactionsByDate now sorts also pending transactions, if they have blockTime set + // this is a temporary solution to emulate the original behavior here in suite-native + // TODO: display pending transactions in the correct order + const [pendingTxs, confirmedTxs] = arrayPartition(transactions, isPending); + const accountTransactionsByMonth = groupTransactionsByDate(confirmedTxs, 'month'); + accountTransactionsByMonth['pending'] = [ + ...accountTransactionsByMonth['pending'], + ...pendingTxs, + ]; const transactionMonthKeys = Object.keys(accountTransactionsByMonth) as MonthKey[]; if (tokenContract) { From a7512299672b98c5f6afca97897e0a454bf9e9fe Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Wed, 19 Jun 2024 16:29:04 +0200 Subject: [PATCH 2/6] chore: update lockfile --- suite-native/transactions/tsconfig.json | 3 ++- yarn.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/suite-native/transactions/tsconfig.json b/suite-native/transactions/tsconfig.json index 5ad5a0264b5..0bedbcb1246 100644 --- a/suite-native/transactions/tsconfig.json +++ b/suite-native/transactions/tsconfig.json @@ -37,6 +37,7 @@ "path": "../../packages/blockchain-link-types" }, { "path": "../../packages/styles" }, - { "path": "../../packages/theme" } + { "path": "../../packages/theme" }, + { "path": "../../packages/utils" } ] } diff --git a/yarn.lock b/yarn.lock index 012661585cc..4c92bc4ccbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10065,6 +10065,7 @@ __metadata: "@trezor/blockchain-link-types": "workspace:*" "@trezor/styles": "workspace:*" "@trezor/theme": "workspace:*" + "@trezor/utils": "workspace:*" proxy-memoize: "npm:2.0.2" react: "npm:18.2.0" react-native: "npm:0.74.1" From b5390ae114dcad7e2faf626179408aeec90e3ef0 Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Thu, 20 Jun 2024 18:49:18 +0200 Subject: [PATCH 3/6] fix: handle no-blocktime transaction edge case --- .../TransactionsGroup/DayHeader.tsx | 19 ++++++++++++------- .../src/__tests__/transactionUtils.test.ts | 4 ++-- .../wallet-utils/src/transactionUtils.ts | 5 ++--- .../TransactionsList/TransactionList.tsx | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx index 59425427b7e..b898b262c24 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/DayHeader.tsx @@ -16,7 +16,7 @@ interface DayHeaderProps { isHovered?: boolean; } -// TODO: Do not show FEE for sent but not mine transactions +// TODO: Do not show FEE for sent but not mined transactions export const DayHeader = ({ dateKey, symbol, @@ -31,15 +31,20 @@ export const DayHeader = ({ const parsedDate = parseTransactionDateKey(dateKey); const showFiatValue = !isTestnet(symbol); + // blockTime can be undefined according to types, although I don't know when that happens. + const isDateValid = !isNaN(parsedDate.getTime()); + return ( - + {isDateValid && ( + + )} {totalAmount.gt(0) && +} diff --git a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts index d80f983df93..9aa1a01dc57 100644 --- a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts @@ -52,7 +52,7 @@ describe('transaction utils', () => { getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ]); expect(groupedTxs).toEqual({ - pending: [ + 'no-blocktime': [ getWalletTransaction({ blockHeight: 0, blockTime: 0 }), getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ], @@ -85,7 +85,7 @@ describe('transaction utils', () => { const firstMonth = generateTransactionMonthKey(new Date(firstBlocktime * 1000)); const secondMonth = generateTransactionMonthKey(new Date(secondBlocktime * 1000)); expect(groupedTxs).toEqual({ - pending: [ + 'no-blocktime': [ getWalletTransaction({ blockHeight: 0, blockTime: 0 }), getWalletTransaction({ blockHeight: 0, blockTime: undefined }), ], diff --git a/suite-common/wallet-utils/src/transactionUtils.ts b/suite-common/wallet-utils/src/transactionUtils.ts index c022dc216ee..45c8cdae4c6 100644 --- a/suite-common/wallet-utils/src/transactionUtils.ts +++ b/suite-common/wallet-utils/src/transactionUtils.ts @@ -79,7 +79,6 @@ export const parseTransactionMonthKey = (key: MonthKey): Date => new Date(key); export type GroupedTransactionsByDate = Record; /** * Returns object with transactions grouped by a date. Key is a string in YYYY-MM-DD format. - * Pending txs are assigned to key 'pending'. * * @param {WalletAccountTransaction[]} transactions */ @@ -98,11 +97,11 @@ export const groupTransactionsByDate = ( .filter(transaction => !!transaction) .sort(sortByBlockHeight) .reduce((r, item) => { - // pending txs are grouped as 'pending' only if blockTime is unavailable (otherwise sorted by blockTime) + // pending txs are grouped as 'no-blocktime' only if blockTime is unavailable (otherwise sorted by blockTime) const key = item.blockTime && item.blockTime > 0 ? keyFormatter(new Date(item.blockTime * 1000)) - : 'pending'; + : 'no-blocktime'; const prev = r[key] ?? []; return { diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx index f24bf1fc51e..67ebfe8554c 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx @@ -169,7 +169,7 @@ export const TransactionList = ({ const [pendingTxs, confirmedTxs] = arrayPartition(transactions, isPending); const accountTransactionsByMonth = groupTransactionsByDate(confirmedTxs, 'month'); accountTransactionsByMonth['pending'] = [ - ...accountTransactionsByMonth['pending'], + ...accountTransactionsByMonth['no-blocktime'], ...pendingTxs, ]; const transactionMonthKeys = Object.keys(accountTransactionsByMonth) as MonthKey[]; From 9cfc0a06503d972b57eb265bde9cde621d7493cf Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Thu, 20 Jun 2024 18:50:04 +0200 Subject: [PATCH 4/6] test(suite): mark transaction groups as pending or confirmed --- .../tests/wallet/pending-transactions.test.ts | 22 +++++++++---------- .../tests/wallet/send-form-regtest.test.ts | 2 +- .../TransactionGroupedList.tsx | 1 + .../TransactionsGroup/TransactionsGroup.tsx | 4 +++- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/suite-web/e2e/tests/wallet/pending-transactions.test.ts b/packages/suite-web/e2e/tests/wallet/pending-transactions.test.ts index a3d5aded907..8e27512c4fd 100644 --- a/packages/suite-web/e2e/tests/wallet/pending-transactions.test.ts +++ b/packages/suite-web/e2e/tests/wallet/pending-transactions.test.ts @@ -55,16 +55,16 @@ describe('Use regtest to test pending transactions', () => { cy.task('pressYes'); cy.getTestElement('@modal/send').click(); - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { // pre-pending is immediately created and placed in "pending transactions group" cy.getTestElement('@transaction-item/0/prepending/heading'); // however, after a while it is replaced by a standard pending transaction cy.getTestElement(`@transaction-item/0/heading`, { timeout: 60000 }).click({ scrollBehavior: 'bottom', }); - // count has not changed - cy.getTestElement('@transaction-group/pending/count').contains(index + 1); }); + // count has not changed + cy.getTestElement('@transaction-group/pending/count').contains(index + 1); cy.getTestElement('@tx-detail/txid-value').then($el => { cy.task('set', { key: address, value: $el.attr('id') }); }); @@ -73,20 +73,20 @@ describe('Use regtest to test pending transactions', () => { }); // account 1 has 2 pending transactions (self and sent) - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Sending REGTEST'); cy.getTestElement('@transaction-item/1/heading').contains('Sending REGTEST to myself'); }); // account 2 has 1 pending transaction (receive) cy.getTestElement('@account-menu/regtest/normal/1').click(); - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Receiving REGTEST'); }); // while observing account 1, sent transaction is mined cy.getTestElement('@account-menu/regtest/normal/0').click(); - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Sending REGTEST'); cy.getTestElement('@transaction-item/1/heading').contains('Sending REGTEST to myself'); }); @@ -98,7 +98,7 @@ describe('Use regtest to test pending transactions', () => { }); cy.wait(2000); // wait for potential notification about mined txs // nothing has changed - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Sending REGTEST'); cy.getTestElement('@transaction-item/1/heading').contains('Sending REGTEST to myself'); }); @@ -114,18 +114,18 @@ describe('Use regtest to test pending transactions', () => { }); }); // which causes sent transaction to disappear, self transaction stays - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Sending REGTEST to myself'); - cy.getTestElement('@transaction-group/pending/count').contains(1); }); + cy.getTestElement('@transaction-group/pending/count').contains(1); // and new group of transactions appears with the previously pending transaction now confirmed - cy.getTestElement('@wallet/accounts/transaction-list/group/1').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/confirmed/group/0').within(() => { cy.getTestElement('@transaction-item/0/heading').contains('Sent REGTEST'); }); // receive pending transaction on account2 is now mined as well cy.getTestElement('@account-menu/regtest/normal/1').click(); - cy.getTestElement('@wallet/accounts/transaction-list/group/0').within(() => { + cy.getTestElement('@wallet/accounts/transaction-list/confirmed/group/0').within(() => { cy.getTestElement(`@transaction-item/0/heading`).contains('Received REGTEST'); }); }); diff --git a/packages/suite-web/e2e/tests/wallet/send-form-regtest.test.ts b/packages/suite-web/e2e/tests/wallet/send-form-regtest.test.ts index 3d6d2f47251..bf444f495ae 100644 --- a/packages/suite-web/e2e/tests/wallet/send-form-regtest.test.ts +++ b/packages/suite-web/e2e/tests/wallet/send-form-regtest.test.ts @@ -94,7 +94,7 @@ describe('Send form for bitcoin', () => { cy.task('pressYes'); cy.getTestElement('@modal/send').click(); - cy.getTestElement('@wallet/accounts/transaction-list/group/0').should( + cy.getTestElement('@wallet/accounts/transaction-list/pending/group/0').should( 'contain', 'OP_RETURN (meow)', ); diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx index 6a43c822761..8f9091e9f32 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx @@ -35,6 +35,7 @@ export const TransactionGroupedList = ({ symbol={symbol} transactions={value} localCurrency={localCurrency} + isPending={isPending} index={groupIndex} > {groupJointTransactions(value).map((item, index) => diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx index 287629090ab..a1d921e9ff1 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/TransactionsGroup.tsx @@ -35,6 +35,7 @@ interface TransactionsGroupProps { symbol: Network['symbol']; localCurrency: FiatCurrencyCode; index: number; + isPending: boolean; } export const TransactionsGroup = ({ @@ -42,6 +43,7 @@ export const TransactionsGroup = ({ symbol, transactions, localCurrency, + isPending, children, index, ...rest @@ -88,7 +90,7 @@ export const TransactionsGroup = ({ setIsHovered(true)} - data-test={`@wallet/accounts/transaction-list/group/${index}`} + data-test={`@wallet/accounts/transaction-list/${isPending ? 'pending' : 'confirmed'}/group/${index}`} onMouseLeave={() => setIsHovered(false)} {...rest} > From 45ad0e30223bcb447bd0c57ae2751d5a25de8358 Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Fri, 21 Jun 2024 17:03:56 +0200 Subject: [PATCH 5/6] fix(suite-native): add fallback to [] --- .../components/TransactionsList/TransactionList.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx index 67ebfe8554c..c5ac5006f47 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx @@ -168,10 +168,14 @@ export const TransactionList = ({ // TODO: display pending transactions in the correct order const [pendingTxs, confirmedTxs] = arrayPartition(transactions, isPending); const accountTransactionsByMonth = groupTransactionsByDate(confirmedTxs, 'month'); - accountTransactionsByMonth['pending'] = [ - ...accountTransactionsByMonth['no-blocktime'], - ...pendingTxs, - ]; + if (pendingTxs.length || accountTransactionsByMonth['no-blocktime']) { + accountTransactionsByMonth['pending'] = [ + ...(accountTransactionsByMonth['no-blocktime'] ?? []), + ...pendingTxs, + ]; + delete accountTransactionsByMonth['no-blocktime']; + } + const transactionMonthKeys = Object.keys(accountTransactionsByMonth) as MonthKey[]; if (tokenContract) { From 8c1ec92bd9f2e1d8cf6002961e5c92d3ed1dd144 Mon Sep 17 00:00:00 2001 From: Matej Kriz Date: Thu, 25 Jul 2024 15:17:27 +0200 Subject: [PATCH 6/6] fix(suite-native): pending transactions at the beginning of the list in one separate group as before --- .../TransactionsList/TransactionList.tsx | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx index c5ac5006f47..5c7254f9275 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx @@ -74,6 +74,25 @@ const sectionListContainerStyle = prepareNativeStyle(utils => ({ paddingVertical: utils.spacings.small, })); +const sortKeysPendingFirst = (a: string, b: string) => { + if (a === 'pending' && b === 'pending') return 0; + if (a === 'pending') return -1; + if (b === 'pending') return 1; + + const dateA = new Date(a); + const dateB = new Date(b); + + return dateA.getTime() - dateB.getTime(); +}; + +const sortPendingTransactions = (a: WalletAccountTransaction, b: WalletAccountTransaction) => { + if (a.blockTime === undefined && b.blockTime === undefined) return 0; + if (a.blockTime === undefined) return -1; + if (b.blockTime === undefined) return 1; + + return a.blockTime - b.blockTime; +}; + const renderTransactionItem = ({ item, isFirst, @@ -163,20 +182,22 @@ export const TransactionList = ({ }, [dispatch, accountKey, fetchTransactions]); const data = useMemo((): TransactionListItem[] => { - // groupTransactionsByDate now sorts also pending transactions, if they have blockTime set - // this is a temporary solution to emulate the original behavior here in suite-native - // TODO: display pending transactions in the correct order + // groupTransactionsByDate now sorts also pending transactions, if they have blockTime set. + // This is here to keep the original behavior of having pending transactions in one group + // at the beginning of the list. const [pendingTxs, confirmedTxs] = arrayPartition(transactions, isPending); const accountTransactionsByMonth = groupTransactionsByDate(confirmedTxs, 'month'); if (pendingTxs.length || accountTransactionsByMonth['no-blocktime']) { accountTransactionsByMonth['pending'] = [ ...(accountTransactionsByMonth['no-blocktime'] ?? []), - ...pendingTxs, + ...pendingTxs.sort(sortPendingTransactions), ]; delete accountTransactionsByMonth['no-blocktime']; } - const transactionMonthKeys = Object.keys(accountTransactionsByMonth) as MonthKey[]; + const transactionMonthKeys = Object.keys(accountTransactionsByMonth).sort( + sortKeysPendingFirst, + ) as MonthKey[]; if (tokenContract) { return transactionMonthKeys.flatMap(monthKey => [