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

Extract HeadingSubtitleWithUsdValue #6037

Merged
merged 1 commit into from
Dec 18, 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
75 changes: 10 additions & 65 deletions frontend/src/lib/components/accounts/WalletPageHeading.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
<script lang="ts">
import TooltipIcon from "$lib/components/ui/TooltipIcon.svelte";
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import { onIntersection } from "$lib/directives/intersection.directives";
import { ENABLE_USD_VALUES } from "$lib/stores/feature-flags.store";
import { i18n } from "$lib/stores/i18n";
import { layoutTitleStore } from "$lib/stores/layout.store";
import type { IntersectingDetail } from "$lib/types/intersection.types";
import { formatNumber } from "$lib/utils/format.utils";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { formatTokenV2 } from "$lib/utils/token.utils";
import HeadingSubtitle from "../common/HeadingSubtitle.svelte";
import HeadingSubtitleWithUsdValue from "../common/HeadingSubtitleWithUsdValue.svelte";
import PageHeading from "../common/PageHeading.svelte";
import AmountDisplay from "../ic/AmountDisplay.svelte";
import IdentifierHash from "../ui/IdentifierHash.svelte";
Expand All @@ -22,28 +20,6 @@
export let principal: Principal | undefined = undefined;
export let ledgerCanisterId: Principal | undefined;

let icpSwapHasError: boolean;
$: icpSwapHasError = $icpSwapUsdPricesStore === "error";

let tokenPrice: number | undefined;
$: tokenPrice =
nonNullish(ledgerCanisterId) &&
nonNullish($icpSwapUsdPricesStore) &&
$icpSwapUsdPricesStore !== "error"
? $icpSwapUsdPricesStore[ledgerCanisterId.toText()]
: undefined;

let balanceInUsd: number | undefined;
$: balanceInUsd =
nonNullish(balance) && nonNullish(tokenPrice)
? (tokenPrice * Number(balance.toE8s())) / 100_000_000
: undefined;

let formattedBalanceInUsd: string;
$: formattedBalanceInUsd = nonNullish(balanceInUsd)
? `$${formatNumber(balanceInUsd)}`
: "$-/-";

let detailedAccountBalance: string | undefined;
$: detailedAccountBalance = nonNullish(balance)
? formatTokenV2({
Expand Down Expand Up @@ -93,27 +69,15 @@
on:nnsIntersecting={updateLayoutTitle}
use:onIntersection
>
<HeadingSubtitle testId="wallet-page-heading-subtitle">
<div class="subtitle">
{#if $ENABLE_USD_VALUES}
<div class="usd-balance" class:icp-swap-has-error={icpSwapHasError}>
<span data-tid="usd-balance">
{formattedBalanceInUsd}
</span>
<TooltipIcon>
{#if icpSwapHasError}
{$i18n.accounts.token_price_error}
{:else}
{$i18n.accounts.token_price_source}
{/if}
</TooltipIcon>
</div>
<div class="vertical-divider"></div>
{/if}<div class="account-name">
{accountName}
</div>
</div>
</HeadingSubtitle>
{#if $ENABLE_USD_VALUES}
<HeadingSubtitleWithUsdValue amount={balance} {ledgerCanisterId}>
{accountName}
</HeadingSubtitleWithUsdValue>
{:else}
<HeadingSubtitle testId="wallet-page-heading-subtitle">
{accountName}
</HeadingSubtitle>
{/if}
{#if nonNullish(principal)}
<p class="description" data-tid="wallet-page-heading-principal">
{$i18n.core.principal}: <IdentifierHash
Expand All @@ -126,25 +90,6 @@
</PageHeading>

<style lang="scss">
.subtitle {
display: flex;
gap: var(--padding-2x);

.usd-balance {
display: flex;
gap: var(--padding-0_5x);
align-items: center;

&.icp-swap-has-error {
--tooltip-icon-color: var(--tag-failed-text);
}
}

.vertical-divider {
border-right: 1px solid var(--elements-divider);
}
}

.subtitles {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import TooltipIcon from "$lib/components/ui/TooltipIcon.svelte";
import { icpSwapUsdPricesStore } from "$lib/derived/icp-swap.derived";
import { i18n } from "$lib/stores/i18n";
import { formatNumber } from "$lib/utils/format.utils";
import HeadingSubtitle from "../common/HeadingSubtitle.svelte";
import type { Principal } from "@dfinity/principal";
import { nonNullish, type TokenAmountV2 } from "@dfinity/utils";

export let amount: TokenAmountV2 | undefined = undefined;
export let ledgerCanisterId: Principal | undefined;

let icpSwapHasError: boolean;
$: icpSwapHasError = $icpSwapUsdPricesStore === "error";

let tokenPrice: number | undefined;
$: tokenPrice =
nonNullish(ledgerCanisterId) &&
nonNullish($icpSwapUsdPricesStore) &&
$icpSwapUsdPricesStore !== "error"
? $icpSwapUsdPricesStore[ledgerCanisterId.toText()]
: undefined;

let amountInUsd: number | undefined;
$: amountInUsd =
nonNullish(amount) && nonNullish(tokenPrice)
? (tokenPrice * Number(amount.toE8s())) / 100_000_000
: undefined;

let formattedAmountInUsd: string;
$: formattedAmountInUsd = nonNullish(amountInUsd)
? `$${formatNumber(amountInUsd)}`
: "$-/-";
</script>

<HeadingSubtitle testId="heading-subtitle-with-usd-value-component">
<div class="subtitle">
<div class="usd-value" class:icp-swap-has-error={icpSwapHasError}>
<span data-tid="usd-value">
{formattedAmountInUsd}
</span>
<TooltipIcon>
{#if icpSwapHasError}
{$i18n.accounts.token_price_error}
{:else}
{$i18n.accounts.token_price_source}
{/if}
</TooltipIcon>
</div>
<div class="vertical-divider"></div>
<div>
<slot />
</div>
</div>
</HeadingSubtitle>

<style lang="scss">
.subtitle {
display: flex;
gap: var(--padding-2x);

.usd-value {
display: flex;
gap: var(--padding-0_5x);
align-items: center;

&.icp-swap-has-error {
--tooltip-icon-color: var(--tag-failed-text);
}
}

.vertical-divider {
border-right: 1px solid var(--elements-divider);
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import HeadingSubtitleWithUsdValue from "$lib/components/common/HeadingSubtitleWithUsdValue.svelte";
import { LEDGER_CANISTER_ID } from "$lib/constants/canister-ids.constants";
import { CKUSDC_UNIVERSE_CANISTER_ID } from "$lib/constants/ckusdc-canister-ids.constants";
import { icpSwapTickersStore } from "$lib/stores/icp-swap.store";
import { mockIcpSwapTicker } from "$tests/mocks/icp-swap.mock";
import { principal } from "$tests/mocks/sns-projects.mock";
import { HeadingSubtitleWithUsdValuePo } from "$tests/page-objects/HeadingSubtitleWithUsdValue.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { Principal } from "@dfinity/principal";
import { ICPToken, TokenAmountV2 } from "@dfinity/utils";
import { render } from "@testing-library/svelte";

describe("HeadingSubtitleWithUsdValue", () => {
const renderComponent = ({
amount,
ledgerCanisterId,
}: {
amount: TokenAmountV2 | undefined;
ledgerCanisterId: Principal | undefined;
}) => {
const { container } = render(HeadingSubtitleWithUsdValue, {
amount,
ledgerCanisterId,
});
return HeadingSubtitleWithUsdValuePo.under(
new JestPageObjectElement(container)
);
};

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

it("should render amount in USD", async () => {
const amount = TokenAmountV2.fromString({
amount: "3",
token: ICPToken,
}) as TokenAmountV2;

const po = renderComponent({
amount,
ledgerCanisterId: LEDGER_CANISTER_ID,
});

expect(await po.hasAmountInUsd()).toBe(true);
expect(await po.getAmountInUsd()).toBe("$30.00");
expect(await po.getTooltipIconPo().getTooltipText()).toBe(
"Token prices are in ckUSDC based on data provided by ICPSwap."
);
});

it("should show error state when token prices failed to load", async () => {
icpSwapTickersStore.set("error");

const amount = TokenAmountV2.fromString({
amount: "3",
token: ICPToken,
}) as TokenAmountV2;
const po = renderComponent({
amount,
ledgerCanisterId: LEDGER_CANISTER_ID,
});

expect(await po.hasAmountInUsd()).toBe(true);
expect(await po.getAmountInUsd()).toBe("$-/-");
expect(await po.getTooltipIconPo().getTooltipText()).toBe(
"ICPSwap API is currently unavailable, token prices cannot be fetched at the moment."
);
});

it("should get token price based on ledger canister ID", async () => {
const ledgerCanisterId = principal(3);

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

const amount = TokenAmountV2.fromString({
amount: "3",
token: ICPToken,
}) as TokenAmountV2;
const po = renderComponent({ amount, ledgerCanisterId });

expect(await po.getAmountInUsd()).toBe("$15.00");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BasePageObject } from "$tests/page-objects/base.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { TooltipIconPo } from "./TooltipIcon.page-object";

export class HeadingSubtitleWithUsdValuePo extends BasePageObject {
private static readonly TID = "heading-subtitle-with-usd-value-component";

static under(element: PageObjectElement): HeadingSubtitleWithUsdValuePo {
return new HeadingSubtitleWithUsdValuePo(
element.byTestId(HeadingSubtitleWithUsdValuePo.TID)
);
}

getTooltipIconPo(): TooltipIconPo {
return TooltipIconPo.under(this.root);
}

hasAmountInUsd(): Promise<boolean> {
return this.isPresent("usd-value");
}

getAmountInUsd(): Promise<string> {
return this.getText("usd-value");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BasePageObject } from "$tests/page-objects/base.page-object";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { AmountDisplayPo } from "./AmountDisplay.page-object";
import { HashPo } from "./Hash.page-object";
import { HeadingSubtitleWithUsdValuePo } from "./HeadingSubtitleWithUsdValue.page-object";
import { TooltipPo } from "./Tooltip.page-object";
import { TooltipIconPo } from "./TooltipIcon.page-object";

Expand All @@ -12,6 +13,10 @@ export class WalletPageHeadingPo extends BasePageObject {
return new WalletPageHeadingPo(element.byTestId(WalletPageHeadingPo.TID));
}

getHeadingSubtitleWithUsdValuePo(): HeadingSubtitleWithUsdValuePo {
return HeadingSubtitleWithUsdValuePo.under(this.root);
}

async getTitle(): Promise<string | null> {
if (await this.hasBalancePlaceholder()) {
return null;
Expand All @@ -32,15 +37,15 @@ export class WalletPageHeadingPo extends BasePageObject {
}

hasBalanceInUsd(): Promise<boolean> {
return this.isPresent("usd-balance");
return this.getHeadingSubtitleWithUsdValuePo().hasAmountInUsd();
}

getBalanceInUsd(): Promise<string> {
return this.getText("usd-balance");
return this.getHeadingSubtitleWithUsdValuePo().getAmountInUsd();
}

getTooltipIconPo(): TooltipIconPo {
return TooltipIconPo.under(this.root);
return this.getHeadingSubtitleWithUsdValuePo().getTooltipIconPo();
}

getPrincipal(): Promise<string> {
Expand Down
Loading