Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/group pending transactions by date #12975

Merged
merged 6 commits into from
Jul 31, 2024
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
22 changes: 11 additions & 11 deletions packages/suite-web/e2e/tests/wallet/pending-transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') });
});
Expand All @@ -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');
});
Expand All @@ -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');
});
Expand All @@ -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');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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) => (
<TransactionsGroup
key={dateKey}
dateKey={dateKey}
symbol={symbol}
transactions={value}
localCurrency={localCurrency}
isPending={isPending}
index={groupIndex}
>
{groupJointTransactions(value).map((item, index) =>
item.type === 'joint-batch' ? (
<CoinjoinBatchItem
key={item.rounds[0].txid}
transactions={item.rounds}
isPending={isPending}
localCurrency={localCurrency}
/>
) : (
<TransactionItem
key={item.tx.txid}
transaction={item.tx}
isPending={isPending}
accountMetadata={accountMetadata}
accountKey={account.key}
network={network!}
index={index}
/>
),
)}
</TransactionsGroup>
));
};
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
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';

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';
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;
Expand All @@ -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('');
Expand Down Expand Up @@ -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 (
<TransactionsGroup
key={dateKey}
dateKey={dateKey}
symbol={symbol}
transactions={value}
localCurrency={localCurrency}
index={groupIndex}
>
{groupJointTransactions(value).map((item, index) =>
item.type === 'joint-batch' ? (
<CoinjoinBatchItem
key={item.rounds[0].txid}
transactions={item.rounds}
isPending={isPending}
localCurrency={localCurrency}
/>
) : (
<TransactionItem
key={item.tx.txid}
transaction={item.tx}
isPending={isPending}
accountMetadata={accountMetadata}
accountKey={account.key}
network={network!}
index={index}
/>
),
)}
</TransactionsGroup>
);
}),
[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)
Expand Down Expand Up @@ -204,7 +169,29 @@ export const TransactionList = ({
<SkeletonTransactionItem />
</SkeletonStack>
) : (
<>{areTransactionsAvailable ? <NoSearchResults /> : listItems}</>
<>
{areTransactionsAvailable ? (
<NoSearchResults />
) : (
<>
{pendingTxs.length > 0 && (
<PendingGroupHeader txsCount={pendingTxs.length} />
)}
<TransactionGroupedList
transactionGroups={pendingTxsByDate}
symbol={symbol}
account={account}
isPending={true}
/>
<TransactionGroupedList
transactionGroups={confirmedTxsByDate}
symbol={symbol}
account={account}
isPending={false}
/>
</>
)}
</>
)}

{showPagination && (
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
`;
Loading
Loading