From 5f303a30be85a1d5a82c4400aff7ffdc05e474c2 Mon Sep 17 00:00:00 2001 From: David de Kloet <122978264+dskloetd@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:42:47 +0100 Subject: [PATCH] Support rendering pending transactions (#3839) # Motivation When converting BTC to and from ckBTC, it takes a while before the transaction on one network results in a transaction on the other network. We want to render this as a "pending" transaction, which should look like this: image # Changes 1. Add a field `isPending` to the type `UiTransaction`. 2. Make the field `timestamp` optional in `UiTransaction`. Since pending transaction haven't happened yet, they don't have a timestamp. 3. In `TransactionCard` make the icon orange, by adding class `pending`, when the transaction is pending. 4. In `TransactionCard` don't render a timestamp if it's absent, and render "Pending..." if it's pending. 5. Add `isPending: false` to existing instances. # Tests 1. Add a unit test for rendering a pending transaction. 2. Test that a non-pending transaction does not have a pending icon. 3. Add `hasPendingIcon` to page object. # Todos - [ ] Add entry to changelog (if necessary). will add when it's used --- .../accounts/TransactionCard.svelte | 34 +++++++++++++++---- frontend/src/lib/i18n/en.json | 3 +- frontend/src/lib/types/i18n.d.ts | 1 + frontend/src/lib/types/transaction.ts | 3 +- .../src/lib/utils/icrc-transactions.utils.ts | 1 + frontend/src/lib/utils/transactions.utils.ts | 1 + .../accounts/IcrcTransactionCard.spec.ts | 1 + .../accounts/TransactionCard.spec.ts | 13 ++++++- .../lib/utils/icrc-transactions.utils.spec.ts | 5 +++ .../lib/utils/transactions.utils.spec.ts | 1 + frontend/src/tests/mocks/transaction.mock.ts | 2 ++ .../TransactionCard.page-object.ts | 5 +++ 12 files changed, 61 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/accounts/TransactionCard.svelte b/frontend/src/lib/components/accounts/TransactionCard.svelte index c4dc283d33f..54c4e972ade 100644 --- a/frontend/src/lib/components/accounts/TransactionCard.svelte +++ b/frontend/src/lib/components/accounts/TransactionCard.svelte @@ -14,9 +14,10 @@ let headline: string; let tokenAmount: TokenAmount; let isIncoming: boolean; + let isPending: boolean; let otherParty: string | undefined; - let timestamp: Date; - $: ({ headline, tokenAmount, isIncoming, otherParty, timestamp } = + let timestamp: Date | undefined; + $: ({ headline, tokenAmount, isIncoming, isPending, otherParty, timestamp } = transaction); let label: string; @@ -24,12 +25,17 @@ ? $i18n.wallet.direction_from : $i18n.wallet.direction_to; - let seconds: number; - $: seconds = timestamp.getTime() / 1000; + let seconds: number | undefined; + $: seconds = timestamp && timestamp.getTime() / 1000;
-
+
{#if isIncoming} {:else} @@ -58,7 +64,13 @@
- + {#if nonNullish(seconds)} + + {:else if isPending} +

+ {$i18n.wallet.pending_transaction_timestamp} +

+ {/if}
@@ -97,6 +109,11 @@ min-width: fit-content; text-align: right; + .pending { + // Because DateSeconds also has margin-top: 0. + margin-top: 0; + } + @include media.min-width(small) { margin-top: var(--padding); } @@ -125,6 +142,11 @@ background: var(--background); color: var(--disable-contrast); } + + &.pending { + color: var(--pending-color); + background: var(--pending-background); + } } @include media.dark-theme { diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index df9f16d9d8d..894e1685573 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -491,7 +491,8 @@ "no_transactions": "No transactions", "icp_qrcode_aria_label": "A QR code that renders the address to receive ICP", "sns_qrcode_aria_label": "A QR code that renders the address to receive $tokenSymbol", - "token_address": "$tokenSymbol Address" + "token_address": "$tokenSymbol Address", + "pending_transaction_timestamp": "Pending..." }, "busy_screen": { "pending_approval_hw": "Please use your hardware wallet to approve.", diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index 048c66d414e..6b0985f5699 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -509,6 +509,7 @@ interface I18nWallet { icp_qrcode_aria_label: string; sns_qrcode_aria_label: string; token_address: string; + pending_transaction_timestamp: string; } interface I18nBusy_screen { diff --git a/frontend/src/lib/types/transaction.ts b/frontend/src/lib/types/transaction.ts index 94e5cae24a5..cc178cd93c5 100644 --- a/frontend/src/lib/types/transaction.ts +++ b/frontend/src/lib/types/transaction.ts @@ -84,12 +84,13 @@ export interface UiTransaction { // Used in forEach for consistent rendering. domKey: string; isIncoming: boolean; + isPending: boolean; headline: string; // Where the amount is going to or coming from. otherParty?: string; // Always positive. tokenAmount: TokenAmount; - timestamp: Date; + timestamp?: Date; } export enum TransactionNetwork { diff --git a/frontend/src/lib/utils/icrc-transactions.utils.ts b/frontend/src/lib/utils/icrc-transactions.utils.ts index 4690dfab1dd..a99a7ce7308 100644 --- a/frontend/src/lib/utils/icrc-transactions.utils.ts +++ b/frontend/src/lib/utils/icrc-transactions.utils.ts @@ -210,6 +210,7 @@ export const mapIcrcTransaction = ({ return { domKey: `${transaction.id}-${toSelfTransaction ? "0" : "1"}`, isIncoming: isReceive, + isPending: false, headline, otherParty, tokenAmount: TokenAmount.fromE8s({ diff --git a/frontend/src/lib/utils/transactions.utils.ts b/frontend/src/lib/utils/transactions.utils.ts index 98c0d63031b..3341cea80fa 100644 --- a/frontend/src/lib/utils/transactions.utils.ts +++ b/frontend/src/lib/utils/transactions.utils.ts @@ -168,6 +168,7 @@ export const toUiTransaction = ({ return { domKey: `${transactionId}-${toSelfTransaction ? "0" : "1"}`, isIncoming, + isPending: false, headline, otherParty, tokenAmount: TokenAmount.fromE8s({ diff --git a/frontend/src/tests/lib/components/accounts/IcrcTransactionCard.spec.ts b/frontend/src/tests/lib/components/accounts/IcrcTransactionCard.spec.ts index a69c39f6d20..e4cea1dde93 100644 --- a/frontend/src/tests/lib/components/accounts/IcrcTransactionCard.spec.ts +++ b/frontend/src/tests/lib/components/accounts/IcrcTransactionCard.spec.ts @@ -18,6 +18,7 @@ describe("IcrcTransactionCard", () => { const defaultTransaction = { domKey: "123-0", isIncoming: false, + isPending: false, icon: "outgoing", headline: "Sent", otherParty: "some-address", diff --git a/frontend/src/tests/lib/components/accounts/TransactionCard.spec.ts b/frontend/src/tests/lib/components/accounts/TransactionCard.spec.ts index d26d09c4589..1ddd3eb98ad 100644 --- a/frontend/src/tests/lib/components/accounts/TransactionCard.spec.ts +++ b/frontend/src/tests/lib/components/accounts/TransactionCard.spec.ts @@ -10,7 +10,7 @@ describe("TransactionCard", () => { const defaultTransaction = { domKey: "234-0", isIncoming: false, - icon: "outgoing", + isPending: false, headline: "Sent", otherParty: "some-address", tokenAmount: TokenAmount.fromE8s({ amount: 123_000_000n, token: ICPToken }), @@ -100,6 +100,17 @@ describe("TransactionCard", () => { expect(normalizeWhitespace(await po.getDate())).toBe( "Mar 14, 2021 12:00 AM" ); + expect(await po.hasPendingIcon()).toBe(false); + }); + + it("displays pending transaction", async () => { + const po = renderComponent({ + isPending: true, + timestamp: null, + }); + + expect(normalizeWhitespace(await po.getDate())).toBe("Pending..."); + expect(await po.hasPendingIcon()).toBe(true); }); it("displays identifier for received", async () => { diff --git a/frontend/src/tests/lib/utils/icrc-transactions.utils.spec.ts b/frontend/src/tests/lib/utils/icrc-transactions.utils.spec.ts index fccd2b29871..c7e94c1f001 100644 --- a/frontend/src/tests/lib/utils/icrc-transactions.utils.spec.ts +++ b/frontend/src/tests/lib/utils/icrc-transactions.utils.spec.ts @@ -145,6 +145,7 @@ describe("icrc-transaction utils", () => { domKey: "112-1", headline: "Sent", isIncoming: false, + isPending: false, otherParty: mockSnsSubAccount.identifier, timestamp: defaultTimestamp, tokenAmount: TokenAmount.fromE8s({ @@ -350,6 +351,7 @@ describe("icrc-transaction utils", () => { domKey: "1234-1", headline: "Sent", isIncoming: false, + isPending: false, otherParty: undefined, timestamp: new Date(0), tokenAmount: TokenAmount.fromE8s({ @@ -388,6 +390,7 @@ describe("icrc-transaction utils", () => { domKey: "1234-1", headline: "Sent", isIncoming: false, + isPending: false, otherParty: btcWithdrawalAddress, timestamp: new Date(0), tokenAmount: TokenAmount.fromE8s({ @@ -426,6 +429,7 @@ describe("icrc-transaction utils", () => { domKey: "1234-1", headline: "Sent", isIncoming: false, + isPending: false, otherParty: "BTC Network", timestamp: new Date(0), tokenAmount: TokenAmount.fromE8s({ @@ -457,6 +461,7 @@ describe("icrc-transaction utils", () => { domKey: "1234-1", headline: "Received", isIncoming: true, + isPending: false, otherParty: "BTC Network", timestamp: new Date(0), tokenAmount: TokenAmount.fromE8s({ diff --git a/frontend/src/tests/lib/utils/transactions.utils.spec.ts b/frontend/src/tests/lib/utils/transactions.utils.spec.ts index 208bc36a39f..70829c7f8f8 100644 --- a/frontend/src/tests/lib/utils/transactions.utils.spec.ts +++ b/frontend/src/tests/lib/utils/transactions.utils.spec.ts @@ -403,6 +403,7 @@ describe("transactions-utils", () => { const defaultExpectedUiTransaction: UiTransaction = { domKey: "123-1", isIncoming: false, + isPending: false, headline: "Sent", otherParty: defaultTo, tokenAmount: TokenAmount.fromE8s({ diff --git a/frontend/src/tests/mocks/transaction.mock.ts b/frontend/src/tests/mocks/transaction.mock.ts index c2080a12af6..5bc3868bd70 100644 --- a/frontend/src/tests/mocks/transaction.mock.ts +++ b/frontend/src/tests/mocks/transaction.mock.ts @@ -81,6 +81,7 @@ export const mockTransactionSendDataFromMain: Transaction = { export const createMockUiTransaction = ({ domKey = "123-1", isIncoming = false, + isPending = false, headline = "Sent", otherParty = "aaaaa-aa", tokenAmount = TokenAmount.fromE8s({ @@ -91,6 +92,7 @@ export const createMockUiTransaction = ({ }: Partial): UiTransaction => ({ domKey, isIncoming, + isPending, headline, otherParty, tokenAmount, diff --git a/frontend/src/tests/page-objects/TransactionCard.page-object.ts b/frontend/src/tests/page-objects/TransactionCard.page-object.ts index bd2b048bee2..4ba13719168 100644 --- a/frontend/src/tests/page-objects/TransactionCard.page-object.ts +++ b/frontend/src/tests/page-objects/TransactionCard.page-object.ts @@ -36,4 +36,9 @@ export class TransactionCardPo extends BasePageObject { getAmount(): Promise { return this.getAmountDisplayPo().getAmount(); } + + async hasPendingIcon(): Promise { + const classNames = await this.root.byTestId("icon").getClasses(); + return classNames.includes("pending"); + } }