Skip to content

Commit

Permalink
Support rendering pending transactions (#3839)
Browse files Browse the repository at this point in the history
# 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:

<img width="750" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/122978264/e15b1f29-90b3-4333-a1fd-a9d45d7f3528">

# 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
  • Loading branch information
dskloetd authored Nov 24, 2023
1 parent 538e69e commit 5f303a3
Show file tree
Hide file tree
Showing 12 changed files with 61 additions and 9 deletions.
34 changes: 28 additions & 6 deletions frontend/src/lib/components/accounts/TransactionCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,28 @@
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;
$: label = isIncoming
? $i18n.wallet.direction_from
: $i18n.wallet.direction_to;
let seconds: number;
$: seconds = timestamp.getTime() / 1000;
let seconds: number | undefined;
$: seconds = timestamp && timestamp.getTime() / 1000;
</script>

<article data-tid="transaction-card" transition:fade|global>
<div class="icon" class:send={!isIncoming}>
<div
class="icon"
data-tid="icon"
class:send={!isIncoming}
class:pending={isPending}
>
{#if isIncoming}
<IconDown size="24px" />
{:else}
Expand Down Expand Up @@ -58,7 +64,13 @@
</div>

<div slot="end" class="date label" data-tid="transaction-date">
<DateSeconds {seconds} />
{#if nonNullish(seconds)}
<DateSeconds {seconds} />
{:else if isPending}
<p class="value pending">
{$i18n.wallet.pending_transaction_timestamp}
</p>
{/if}
</div>
</ColumnRow>
</div>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -125,6 +142,11 @@
background: var(--background);
color: var(--disable-contrast);
}
&.pending {
color: var(--pending-color);
background: var(--pending-background);
}
}
@include media.dark-theme {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/types/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/utils/icrc-transactions.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export const mapIcrcTransaction = ({
return {
domKey: `${transaction.id}-${toSelfTransaction ? "0" : "1"}`,
isIncoming: isReceive,
isPending: false,
headline,
otherParty,
tokenAmount: TokenAmount.fromE8s({
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/utils/transactions.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const toUiTransaction = ({
return {
domKey: `${transactionId}-${toSelfTransaction ? "0" : "1"}`,
isIncoming,
isPending: false,
headline,
otherParty,
tokenAmount: TokenAmount.fromE8s({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("IcrcTransactionCard", () => {
const defaultTransaction = {
domKey: "123-0",
isIncoming: false,
isPending: false,
icon: "outgoing",
headline: "Sent",
otherParty: "some-address",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down Expand Up @@ -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 () => {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/tests/lib/utils/icrc-transactions.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions frontend/src/tests/lib/utils/transactions.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ describe("transactions-utils", () => {
const defaultExpectedUiTransaction: UiTransaction = {
domKey: "123-1",
isIncoming: false,
isPending: false,
headline: "Sent",
otherParty: defaultTo,
tokenAmount: TokenAmount.fromE8s({
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/tests/mocks/transaction.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -91,6 +92,7 @@ export const createMockUiTransaction = ({
}: Partial<UiTransaction>): UiTransaction => ({
domKey,
isIncoming,
isPending,
headline,
otherParty,
tokenAmount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ export class TransactionCardPo extends BasePageObject {
getAmount(): Promise<string> {
return this.getAmountDisplayPo().getAmount();
}

async hasPendingIcon(): Promise<boolean> {
const classNames = await this.root.byTestId("icon").getClasses();
return classNames.includes("pending");
}
}

0 comments on commit 5f303a3

Please sign in to comment.