Skip to content

Commit

Permalink
Add USD banner to accounts page (#5966)
Browse files Browse the repository at this point in the history
# Motivation

#5934 added the USD value banner
on the tokens page.
We want the same banner on the ICP accounts page.

# Changes

1. Move function to add up USD balances to utils.
2. Add banner to `NnsAccounts` component.

# Tests

1. Unit tests added.
2. Tested manually at
https://qsgjb-riaaa-aaaaa-aaaga-cai.dskloet-ingress.devenv.dfinity.network/accounts/?u=qsgjb-riaaa-aaaaa-aaaga-cai

# Todos

- [ ] Add entry to changelog (if necessary).
not yet
  • Loading branch information
dskloetd authored Dec 10, 2024
1 parent f91b779 commit 85da07d
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 16 deletions.
24 changes: 21 additions & 3 deletions frontend/src/lib/pages/NnsAccounts.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<script lang="ts">
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import TokensTable from "$lib/components/tokens/TokensTable/TokensTable.svelte";
import UsdValueBanner from "$lib/components/ui/UsdValueBanner.svelte";
import { nnsAccountsListStore } from "$lib/derived/accounts-list.derived";
import {
cancelPollAccounts,
loadBalance,
pollAccounts,
} from "$lib/services/icp-accounts.services";
import { ENABLE_USD_VALUES } from "$lib/stores/feature-flags.store";
import { i18n } from "$lib/stores/i18n";
import { toastsError } from "$lib/stores/toasts.store";
import { ActionType, type Action } from "$lib/types/actions";
import type { UserToken } from "$lib/types/tokens-page";
import { findAccount } from "$lib/utils/accounts.utils";
import { openAccountsModal } from "$lib/utils/modals.utils";
import { getTotalBalanceInUsd } from "$lib/utils/token.utils";
import { IconAccountsPage } from "@dfinity/gix-components";
import { IconAdd } from "@dfinity/gix-components";
import { isNullish } from "@dfinity/utils";
import { onDestroy, onMount } from "svelte";
Expand All @@ -27,6 +30,9 @@
export let userTokensData: UserToken[];
let totalBalanceInUsd: number;
$: totalBalanceInUsd = getTotalBalanceInUsd(userTokensData);
const openAddAccountModal = () => {
openAccountsModal({
type: "add-icp-account",
Expand Down Expand Up @@ -75,7 +81,13 @@
};
</script>

<TestIdWrapper testId="accounts-body">
<div class="wrapper" data-tid="accounts-body">
{#if $ENABLE_USD_VALUES}
<UsdValueBanner usdAmount={totalBalanceInUsd} hasUnpricedTokens={false}>
<IconAccountsPage slot="icon" />
</UsdValueBanner>
{/if}

<TokensTable
{userTokensData}
firstColumnHeader={$i18n.tokens.accounts_header}
Expand All @@ -97,11 +109,17 @@
>
</div>
</TokensTable>
</TestIdWrapper>
</div>

<style lang="scss">
@use "@dfinity/gix-components/dist/styles/mixins/interaction";
.wrapper {
display: flex;
flex-direction: column;
gap: var(--padding-2x);
}
.add-account-row {
@include interaction.tappable;
Expand Down
13 changes: 2 additions & 11 deletions frontend/src/lib/pages/Tokens.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,16 @@
import type { UserToken } from "$lib/types/tokens-page";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { isImportedToken } from "$lib/utils/imported-tokens.utils";
import { getTotalBalanceInUsd } from "$lib/utils/token.utils";
import { IconAccountsPage } from "@dfinity/gix-components";
import { IconPlus, IconSettings, Tooltip } from "@dfinity/gix-components";
import { Popover } from "@dfinity/gix-components";
import { TokenAmountV2, isNullish, nonNullish } from "@dfinity/utils";
export let userTokensData: UserToken[];
const getUsdBalance = (token: UserToken) => {
if (!("balanceInUsd" in token) || isNullish(token.balanceInUsd)) {
return 0;
}
return token.balanceInUsd;
};
let totalBalanceInUsd: number;
$: totalBalanceInUsd = userTokensData.reduce(
(acc, token) => acc + getUsdBalance(token),
0
);
$: totalBalanceInUsd = getTotalBalanceInUsd(userTokensData);
let hasUnpricedTokens: boolean;
$: hasUnpricedTokens = userTokensData.some(
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib/utils/token.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,13 @@ export const sortUserTokens = (tokens: UserToken[]): UserToken[] => [
// tokens without balance
...tokens.filter(({ balance }) => !(balance instanceof TokenAmountV2)),
];

const getUsdBalance = (token: UserToken) => {
if (!("balanceInUsd" in token) || isNullish(token.balanceInUsd)) {
return 0;
}
return token.balanceInUsd;
};

export const getTotalBalanceInUsd = (tokens: UserToken[]): number =>
tokens.reduce((acc, token) => acc + getUsdBalance(token), 0);
66 changes: 66 additions & 0 deletions frontend/src/tests/lib/pages/NnsAccounts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as ledgerApi from "$lib/api/icp-ledger.api";
import * as nnsDappApi from "$lib/api/nns-dapp.api";
import { SYNC_ACCOUNTS_RETRY_SECONDS } from "$lib/constants/accounts.constants";
import { CKUSDC_UNIVERSE_CANISTER_ID } from "$lib/constants/ckusdc-canister-ids.constants";
import { NNS_TOKEN_DATA } from "$lib/constants/tokens.constants";
import NnsAccounts from "$lib/pages/NnsAccounts.svelte";
import { cancelPollAccounts } from "$lib/services/icp-accounts.services";
import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store";
import { icpSwapTickersStore } from "$lib/stores/icp-swap.store";
import type { UserTokenData } from "$lib/types/tokens-page";
import { resetIdentity } from "$tests/mocks/auth.store.mock";
import { mockAccountDetails } from "$tests/mocks/icp-accounts.store.mock";
import { mockIcpSwapTicker } from "$tests/mocks/icp-swap.mock";
import { createUserToken } from "$tests/mocks/tokens-page.mock";
import { NnsAccountsPo } from "$tests/page-objects/NnsAccounts.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
Expand Down Expand Up @@ -35,6 +39,14 @@ describe("NnsAccounts", () => {
vi.spyOn(nnsDappApi, "queryAccount").mockImplementation(async () => {
return mockAccountDetails;
});

icpSwapTickersStore.set([
{
...mockIcpSwapTicker,
base_id: CKUSDC_UNIVERSE_CANISTER_ID.toText(),
last_price: "10.00",
},
]);
});

describe("when tokens flag is enabled", () => {
Expand Down Expand Up @@ -99,6 +111,60 @@ describe("NnsAccounts", () => {
const po = renderComponent([mainTokenData, subaccountTokenData]);
expect(await po.getAddAccountRowTabindex()).toBe("0");
});

it("should not show total USD value banner when feature flag is disabled", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_USD_VALUES", false);

const mainTokenData = createUserToken({
title: "Main",
balanceInUsd: 30.0,
rowHref: "/main",
domKey: "/main",
});

const po = renderComponent([mainTokenData]);

expect(await po.getUsdValueBannerPo().isPresent()).toBe(false);
});

it("should show total USD value banner when feature flag is enabled", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_USD_VALUES", true);

const mainTokenData = createUserToken({
title: "Main",
balanceInUsd: 30.0,
rowHref: "/main",
domKey: "/main",
});

const po = renderComponent([mainTokenData]);

expect(await po.getUsdValueBannerPo().isPresent()).toBe(true);
});

it("should show total USD value", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_USD_VALUES", true);

const mainTokenData = createUserToken({
title: "Main",
balanceInUsd: 30.0,
rowHref: "/main",
domKey: "/main",
});
const subaccountTokenData = createUserToken({
title: "Subaccount test",
balanceInUsd: 20.0,
rowHref: "/subaccount",
domKey: "/subaccount",
});
const po = renderComponent([mainTokenData, subaccountTokenData]);

expect(await po.getUsdValueBannerPo().isPresent()).toBe(true);
expect(await po.getUsdValueBannerPo().getPrimaryAmount()).toBe("$50.00");
expect(
await po.getUsdValueBannerPo().getTotalsTooltipIconPo().isPresent()
).toBe(false);
});
});

// TODO: Move the pollAccounts to Accounts route when universe selected is NNS instead of the child.
Expand Down
37 changes: 35 additions & 2 deletions frontend/src/tests/lib/utils/token.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
formatTokenV2,
formattedTransactionFeeICP,
getMaxTransactionAmount,
getTotalBalanceInUsd,
numberToE8s,
numberToUlps,
sortUserTokens,
Expand All @@ -22,8 +23,8 @@ import {
} from "$lib/utils/token.utils";
import { mockCkETHToken } from "$tests/mocks/cketh-accounts.mock";
import { mockSubAccount } from "$tests/mocks/icp-accounts.store.mock";
import { mockSnsToken } from "$tests/mocks/sns-projects.mock";
import { icpTokenBase } from "$tests/mocks/tokens-page.mock";
import { mockSnsToken, principal } from "$tests/mocks/sns-projects.mock";
import { createUserToken, icpTokenBase } from "$tests/mocks/tokens-page.mock";
import { nnsUniverseMock } from "$tests/mocks/universe.mock";
import { Principal } from "@dfinity/principal";
import { ICPToken, TokenAmount, TokenAmountV2 } from "@dfinity/utils";
Expand Down Expand Up @@ -883,4 +884,36 @@ describe("token-utils", () => {
).toEqual([token5, token3, token1, loadingUserToken, loadingUserToken]);
});
});

describe("getTotalBalanceInUsd", () => {
it("should add up USD balances", () => {
const token1 = createUserToken({
universeId: principal(1),
balanceInUsd: 2,
});
const token2 = createUserToken({
universeId: principal(2),
balanceInUsd: 3,
});

expect(getTotalBalanceInUsd([token1, token2])).toBe(5);
});

it("should ignore tokens with unknown balance in USD when adding up the total", () => {
const token1 = createUserToken({
universeId: principal(1),
balanceInUsd: 3,
});
const token2 = createUserToken({
universeId: principal(2),
balanceInUsd: undefined,
});
const token3 = createUserToken({
universeId: principal(3),
balanceInUsd: 5,
});

expect(getTotalBalanceInUsd([token1, token2, token3])).toBe(8);
});
});
});
5 changes: 5 additions & 0 deletions frontend/src/tests/page-objects/NnsAccounts.page-object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NnsAddAccountPo } from "$tests/page-objects/NnsAddAccount.page-object";
import { UsdValueBannerPo } from "$tests/page-objects/UsdValueBanner.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { TokensTablePo } from "./TokensTable.page-object";
import { BasePageObject } from "./base.page-object";
Expand All @@ -10,6 +11,10 @@ export class NnsAccountsPo extends BasePageObject {
return new NnsAccountsPo(element.byTestId(NnsAccountsPo.TID));
}

getUsdValueBannerPo(): UsdValueBannerPo {
return UsdValueBannerPo.under(this.root);
}

getTokensTablePo() {
return TokensTablePo.under(this.root);
}
Expand Down

0 comments on commit 85da07d

Please sign in to comment.