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

Basic Imported Tokens Modal #5264

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import LinkIcon from "$lib/components/common/LinkIcon.svelte";

export let label: string;
export let testId: string = "import-token-canister-id-component";
export let canisterId: string | undefined = undefined;
export let canisterLinkHref: string | undefined = undefined;
export let canisterIdFallback: string | undefined = undefined;
</script>

<div class="container" data-tid="import-token-canister-id-component">
<div class="container" data-tid={testId}>
<span data-tid="label">{label}</span>
<div class="canister-id">
{#if nonNullish(canisterId)}
Expand Down
72 changes: 72 additions & 0 deletions frontend/src/lib/components/accounts/ImportTokenForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script lang="ts">
import type { Principal } from "@dfinity/principal";
import PrincipalInput from "$lib/components/ui/PrincipalInput.svelte";
import { i18n } from "$lib/stores/i18n";
import { Html } from "@dfinity/gix-components";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import { createEventDispatcher } from "svelte";
import { isNullish } from "@dfinity/utils";
import CalloutWarning from "$lib/components/common/CalloutWarning.svelte";

export let ledgerCanisterId: Principal | undefined = undefined;
export let indexCanisterId: Principal | undefined = undefined;

const dispatch = createEventDispatcher();

let disabled = true;
$: disabled = isNullish(ledgerCanisterId);
</script>

<TestIdWrapper testId="import-token-form-component">
<p class="description">{$i18n.import_token.description}</p>

<form on:submit|preventDefault={() => dispatch("nnsSubmit")}>
<PrincipalInput
bind:principal={ledgerCanisterId}
placeholderLabelKey="import_token.placeholder"
name="ledger-canister-id"
testId="ledger-canister-id"
>
<svelte:fragment slot="label"
>{$i18n.import_token.ledger_label}</svelte:fragment
>
</PrincipalInput>

<PrincipalInput
bind:principal={indexCanisterId}
required={false}
placeholderLabelKey="import_token.placeholder"
name="index-canister-id"
testId="index-canister-id"
>
<Html slot="label" text={$i18n.import_token.index_label_optional} />
</PrincipalInput>

<p class="description">
<Html text={$i18n.import_token.index_canister_description} />
</p>

<CalloutWarning htmlText={$i18n.import_token.warning} />

<div class="toolbar">
<button
class="secondary"
type="button"
data-tid="cancel-button"
on:click={() => dispatch("nnsCancel")}
>
{$i18n.core.cancel}
</button>

<button data-tid="next-button" class="primary" type="submit" {disabled}>
{$i18n.core.next}
</button>
</div>
</form>
</TestIdWrapper>

<style lang="scss">
p.description {
margin: 0 0 var(--padding-2x);
}
</style>
105 changes: 105 additions & 0 deletions frontend/src/lib/components/accounts/ImportTokenReview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import type { Principal } from "@dfinity/principal";
import { i18n } from "$lib/stores/i18n";
import { createEventDispatcher } from "svelte";
import type { IcrcTokenMetadata } from "$lib/types/icrc";
import Logo from "$lib/components/ui/Logo.svelte";
import ImportTokenCanisterId from "$lib/components/accounts/ImportTokenCanisterId.svelte";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { isNullish } from "@dfinity/utils";
import CalloutWarning from "$lib/components/common/CalloutWarning.svelte";

export let ledgerCanisterId: Principal;
export let indexCanisterId: Principal | undefined = undefined;
export let tokenMetaData: IcrcTokenMetadata;

const dispatch = createEventDispatcher();

let ledgerCanisterHref: string;
$: ledgerCanisterHref = replacePlaceholders(
$i18n.import_token.link_to_canister,
{
$canisterId: ledgerCanisterId.toText(),
}
);
let indexCanisterHref: string | undefined;
$: indexCanisterHref = isNullish(indexCanisterId)
? undefined
: replacePlaceholders($i18n.import_token.link_to_canister, {
$canisterId: ledgerCanisterId.toText(),
});
</script>

<div class="container" data-tid="import-token-review-component">
<div class="meta">
<Logo
testId="token-logo"
src={tokenMetaData?.logo ?? ""}
alt={tokenMetaData.name}
size="medium"
framed
/>
<div class="token-name">
<div data-tid="token-name">{tokenMetaData.name}</div>
<div data-tid="token-symbol" class="description">
{tokenMetaData.symbol}
</div>
</div>
</div>

<ImportTokenCanisterId
testId="ledger-canister-id"
label={$i18n.import_token.ledger_label}
canisterId={ledgerCanisterId.toText()}
canisterLinkHref={ledgerCanisterHref}
/>

<ImportTokenCanisterId
testId="index-canister-id"
label={$i18n.import_token.index_label}
canisterId={indexCanisterId?.toText()}
canisterLinkHref={indexCanisterHref}
canisterIdFallback={$i18n.import_token.index_fallback_label}
/>

<CalloutWarning htmlText={$i18n.import_token.warning} />

<div class="toolbar">
<button
class="secondary"
data-tid="back-button"
on:click={() => dispatch("nnsBack")}
>
{$i18n.core.back}
</button>

<button
data-tid="confirm-button"
class="primary"
on:click={() => dispatch("nnsConfirm")}
>
{$i18n.import_token.import_button}
</button>
</div>
</div>

<style lang="scss">
.container {
display: flex;
flex-direction: column;
gap: var(--padding-3x);
}

.meta {
display: flex;
align-items: center;
gap: var(--padding-1_5x);
padding: var(--padding) var(--padding-1_5x);
}

.token-name {
display: flex;
flex-direction: column;
gap: var(--padding-0_5x);
}
</style>
4 changes: 4 additions & 0 deletions frontend/src/lib/components/ui/PrincipalInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
export let placeholderLabelKey: string;
export let name: string;
export let principal: Principal | undefined = undefined;
export let required: boolean | undefined = undefined;
export let testId: string | undefined = undefined;

let address = principal?.toText() ?? "";
$: principal = getPrincipalFromString(address);
Expand All @@ -23,10 +25,12 @@
inputType="text"
{placeholderLabelKey}
{name}
{testId}
bind:value={address}
errorMessage={showError ? $i18n.error.principal_not_valid : undefined}
on:blur={showErrorIfAny}
showInfo={$$slots.label !== undefined}
{required}
>
<slot name="label" slot="label" />
</InputWithError>
17 changes: 16 additions & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1037,8 +1037,23 @@
"hide_zero_balances_toggle_label": "Switch between showing and hiding tokens with a balance of zero",
"zero_balance_hidden": "Tokens with 0 balances are hidden.",
"show_all": "Show all",
"import_token": "Import Token",
"add_imported_token_success": "New token has been successfully imported!",
"remove_imported_token_success": "The token has been successfully removed!"
},
"import_token": {
"import_token": "Import Token",
"description": "To import a new token to your NNS dapp wallet, you will need to find, and paste the ledger canister id of the token. If you want to see your transaction history, you need to import the token’s index canister.",
"ledger_label": "Ledger Canister ID",
"index_label_optional": "Index Canister ID <span class='description'>(Optional)</span>",
"index_label": "Index Canister ID",
"index_fallback_label": "Transaction history won’t be displayed.",
"placeholder": "00000-00000-00000-00000-000",
"index_canister_description": "Index Canister allows to display a token balance and transaction history. <strong>Note:</strong> not all tokens have index canisters.",
"warning": "<strong>Warning:</strong> Be careful what token you import! Anyone can create a token including one with the same name as existing tokens, such as ckBTC.",
"verifying": "Veryifying token details...",
"review_token_info": "Review token info",
"import_button": "Import",
"ledger_canister_loading_error": "Unable to load token details using the provided Ledger Canister ID.",
"link_to_canister": "https://dashboard.internetcomputer.org/canister/$canisterId"
}
}
110 changes: 110 additions & 0 deletions frontend/src/lib/modals/accounts/ImportTokenModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script lang="ts">
import {
WizardModal,
type WizardStep,
type WizardSteps,
} from "@dfinity/gix-components";
import { i18n } from "$lib/stores/i18n";
import type { Principal } from "@dfinity/principal";
import ImportTokenForm from "$lib/components/accounts/ImportTokenForm.svelte";
import type { IcrcTokenMetadata } from "$lib/types/icrc";
import { isNullish, nonNullish } from "@dfinity/utils";
import ImportTokenReview from "$lib/components/accounts/ImportTokenReview.svelte";
import { startBusy, stopBusy } from "$lib/stores/busy.store";
import { fetchIcrcTokenMetaData } from "$lib/services/icrc-accounts.services";
import { toastsError } from "$lib/stores/toasts.store";

let currentStep: WizardStep | undefined = undefined;

const STEP_FORM = "Form";
const STEP_REVIEW = "Review";

const steps: WizardSteps = [
{
name: STEP_FORM,
title: $i18n.import_token.import_token,
},
{
name: STEP_REVIEW,
title: $i18n.import_token.review_token_info,
},
];

let modal: WizardModal;
const next = () => {
modal?.next();
};
const back = () => {
modal?.back();
};

let ledgerCanisterId: Principal | undefined;
let indexCanisterId: Principal | undefined;
let tokenMetaData: IcrcTokenMetadata | undefined;

const updateTokenMetaData = async () => {
if (isNullish(ledgerCanisterId)) {
return;
}
const meta = await fetchIcrcTokenMetaData({ ledgerCanisterId });
if (isNullish(meta)) {
tokenMetaData = undefined;
toastsError({
labelKey: "import_token.ledger_canister_loading_error",
});
return;
}
tokenMetaData = meta;
};

const onUserInput = async () => {
if (isNullish(ledgerCanisterId)) {
return;
}

startBusy({
initiator: "import-token-validation",
labelKey: "import_token.verifying",
});
await updateTokenMetaData();
// TODO: validate index canister id here (if provided)
stopBusy("import-token-validation");

if (nonNullish(tokenMetaData)) {
next();
}
};

const onUserConfirm = async () => {
// TODO: save imported token to the backend canister
// TODO: navigate to imported token details page
};
</script>

<WizardModal
testId="import-token-modal-component"
{steps}
bind:currentStep
bind:this={modal}
on:nnsClose
>
<svelte:fragment slot="title">{currentStep?.title}</svelte:fragment>

{#if currentStep?.name === STEP_FORM}
<ImportTokenForm
bind:ledgerCanisterId
bind:indexCanisterId
on:nnsClose
on:nnsSubmit={onUserInput}
/>
{/if}
{#if currentStep?.name === STEP_REVIEW && nonNullish(ledgerCanisterId) && nonNullish(tokenMetaData)}
<ImportTokenReview
{ledgerCanisterId}
{indexCanisterId}
{tokenMetaData}
on:nnsBack={back}
on:nnsConfirm={onUserConfirm}
/>
{/if}
</WizardModal>
13 changes: 8 additions & 5 deletions frontend/src/lib/pages/Tokens.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Popover } from "@dfinity/gix-components";
import { TokenAmountV2 } from "@dfinity/utils";
import { ENABLE_IMPORT_TOKEN } from "$lib/stores/feature-flags.store";
import ImportTokenModal from "$lib/modals/accounts/ImportTokenModal.svelte";

export let userTokensData: UserToken[];

Expand Down Expand Up @@ -41,9 +42,7 @@
hideZeroBalancesStore.set("show");
};

const importToken = async () => {
// TBD: Implement import token.
};
let showImportTokenModal = false;

// TODO(Import token): After removing ENABLE_IMPORT_TOKEN combine divs -> <div slot="last-row" class="last-row">
</script>
Expand Down Expand Up @@ -82,9 +81,9 @@
<button
data-tid="import-token-button"
class="ghost with-icon import-token-button"
on:click={importToken}
on:click={() => (showImportTokenModal = true)}
>
<IconPlus />{$i18n.tokens.import_token}
<IconPlus />{$i18n.import_token.import_token}
</button>
</div>
{:else if shouldHideZeroBalances}
Expand Down Expand Up @@ -112,6 +111,10 @@
>
<HideZeroBalancesToggle />
</Popover>

{#if showImportTokenModal}
<ImportTokenModal on:nnsClose={() => (showImportTokenModal = false)} />
{/if}
</TestIdWrapper>

<style lang="scss">
Expand Down
Loading
Loading