Skip to content

refactor(frontend): Manage tokens modal #5882

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

Merged
merged 48 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e540fdd
refactor(e2e): remove unnecessary matrix in E2E tests final step
AntonioVentilii Apr 10, 2025
9ed6d11
Merge branch 'main' into refactor(e2e)/remove-unnecessary-matrix-in-E…
bitdivine Apr 10, 2025
cedaae7
Merge branch 'main' of https://github.com/dfinity/oisy-wallet
daviddecentage Apr 10, 2025
2acf8a7
Merge branch 'main' of https://github.com/dfinity/oisy-wallet
daviddecentage Apr 10, 2025
ee1d80c
Merge branch 'main' of https://github.com/dfinity/oisy-wallet
daviddecentage Apr 11, 2025
d1d8249
Merge branch 'main' of https://github.com/dfinity/oisy-wallet
daviddecentage Apr 15, 2025
43a9b2b
update
daviddecentage Apr 15, 2025
b0c7f5d
Merge branch 'main' of https://github.com/dfinity/oisy-wallet into re…
daviddecentage Apr 15, 2025
168bb9c
update
daviddecentage Apr 15, 2025
60d8f5a
🤖 Updated i18n files
github-actions[bot] Apr 15, 2025
ba1e21d
update
daviddecentage Apr 15, 2025
50f0142
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 15, 2025
b6e49e2
🤖 Updated i18n files
github-actions[bot] Apr 15, 2025
1dad33e
Merge branch 'main' of https://github.com/dfinity/oisy-wallet into re…
daviddecentage Apr 16, 2025
c955070
update
daviddecentage Apr 16, 2025
77a8aa4
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 16, 2025
4903bdb
update
daviddecentage Apr 16, 2025
4ba3e0a
update
daviddecentage Apr 16, 2025
082157e
update
daviddecentage Apr 16, 2025
81e9fa2
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 16, 2025
6928515
Merge branch 'main' of https://github.com/dfinity/oisy-wallet into re…
daviddecentage Apr 17, 2025
590fadb
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 17, 2025
763926e
Merge branch 'main' of https://github.com/dfinity/oisy-wallet into re…
daviddecentage Apr 17, 2025
f744f54
update
daviddecentage Apr 17, 2025
bca5954
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 17, 2025
6c2366e
update
daviddecentage Apr 17, 2025
1d78260
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 17, 2025
c0c9bde
🤖 Apply formatting changes
github-actions[bot] Apr 17, 2025
e33757c
update
daviddecentage Apr 17, 2025
5fd3784
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 17, 2025
4f64be9
update
daviddecentage Apr 17, 2025
0988c88
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 17, 2025
cb69541
Update src/frontend/src/lib/components/manage/ManageTokens.svelte
daviddecentage Apr 17, 2025
2e9fdce
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 17, 2025
4fbe884
update
daviddecentage Apr 17, 2025
13862a2
🤖 Update E2E snapshots
github-actions[bot] Apr 17, 2025
aff195b
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 17, 2025
f2b48ff
update
daviddecentage Apr 17, 2025
a57d815
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
f1b4fc6
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
774e7b9
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
951b9eb
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
5bdc039
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
a63825f
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
db3a043
update
daviddecentage Apr 22, 2025
7385bb2
Merge remote-tracking branch 'origin/refactor(frontend)/manage-tokens…
daviddecentage Apr 22, 2025
e097baf
Merge branch 'main' into refactor(frontend)/manage-tokens
daviddecentage Apr 22, 2025
e52429a
Merge branch 'main' into refactor(frontend)/manage-tokens
AntonioVentilii Apr 22, 2025
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
189 changes: 73 additions & 116 deletions src/frontend/src/lib/components/manage/ManageTokens.svelte
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
<script lang="ts">
import { debounce, nonNullish, notEmptyString } from '@dfinity/utils';
import { createEventDispatcher, onMount, type Snippet } from 'svelte';
import { fade } from 'svelte/transition';
import { nonNullish } from '@dfinity/utils';
import { createEventDispatcher, getContext, onMount, setContext, type Snippet } from 'svelte';
import BtcManageTokenToggle from '$btc/components/tokens/BtcManageTokenToggle.svelte';
import { isBitcoinToken } from '$btc/utils/token.utils';
import { erc20UserTokensNotInitialized } from '$eth/derived/erc20.derived';
import type { Erc20UserToken } from '$eth/types/erc20-user-token';
import { icTokenErc20UserToken, icTokenEthereumUserToken } from '$eth/utils/erc20.utils';
import IcManageTokenToggle from '$icp/components/tokens/IcManageTokenToggle.svelte';
import type { IcrcCustomToken } from '$icp/types/icrc-custom-token';
import { icTokenIcrcCustomToken } from '$icp/utils/icrc.utils';
import IconPlus from '$lib/components/icons/lucide/IconPlus.svelte';
import ManageTokenToggle from '$lib/components/tokens/ManageTokenToggle.svelte';
import ModalNetworksFilter from '$lib/components/tokens/ModalNetworksFilter.svelte';
import ModalTokensList from '$lib/components/tokens/ModalTokensList.svelte';
import TokenLogo from '$lib/components/tokens/TokenLogo.svelte';
import TokenName from '$lib/components/tokens/TokenName.svelte';
import Button from '$lib/components/ui/Button.svelte';
import ButtonCancel from '$lib/components/ui/ButtonCancel.svelte';
import ButtonGroup from '$lib/components/ui/ButtonGroup.svelte';
import Card from '$lib/components/ui/Card.svelte';
import InputSearch from '$lib/components/ui/InputSearch.svelte';
import {
MANAGE_TOKENS_MODAL_CLOSE,
MANAGE_TOKENS_MODAL_SAVE
} from '$lib/constants/test-ids.constants';
import LogoButton from '$lib/components/ui/LogoButton.svelte';
import { MANAGE_TOKENS_MODAL_SAVE } from '$lib/constants/test-ids.constants';
import { allTokens } from '$lib/derived/all-tokens.derived';
import { exchanges } from '$lib/derived/exchange.derived';
import { pseudoNetworkChainFusion, selectedNetwork } from '$lib/derived/network.derived';
import { selectedNetwork } from '$lib/derived/network.derived';
import { tokensToPin } from '$lib/derived/tokens.derived';
import { i18n } from '$lib/stores/i18n.store';
import {
initModalTokensListContext,
MODAL_TOKENS_LIST_CONTEXT_KEY,
type ModalTokensListContext
} from '$lib/stores/modal-tokens-list.store';
import type { ExchangesData } from '$lib/types/exchange';
import type { Token } from '$lib/types/token';
import type { TokenToggleable } from '$lib/types/token-toggleable';
import { isDesktop } from '$lib/utils/device.utils';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
import { filterTokensForSelectedNetwork } from '$lib/utils/network.utils';
import { filterTokens, pinEnabledTokensAtTop, sortTokens } from '$lib/utils/tokens.utils';
import { pinEnabledTokensAtTop, sortTokens } from '$lib/utils/tokens.utils';
import SolManageTokenToggle from '$sol/components/tokens/SolManageTokenToggle.svelte';
import type { SplTokenToggleable } from '$sol/types/spl-token-toggleable';
import { isTokenSplToggleable } from '$sol/utils/spl.utils';
import { isSolanaToken } from '$sol/utils/token.utils';

let { initialSearch, infoElement }: { initialSearch?: string; infoElement?: Snippet } = $props();
let { initialSearch, infoElement }: { initialSearch: string | undefined; infoElement?: Snippet } =
$props();

const dispatch = createEventDispatcher();

Expand All @@ -51,55 +50,42 @@
exchangesStaticData = nonNullish($exchanges) ? { ...$exchanges } : undefined;
});

let allTokensForSelectedNetwork: TokenToggleable<Token>[] = $derived(
filterTokensForSelectedNetwork([$allTokens, $selectedNetwork, $pseudoNetworkChainFusion])
);

let allTokensSorted: Token[] = $derived(
let allTokensSorted = $derived(
nonNullish(exchangesStaticData)
? pinEnabledTokensAtTop(
sortTokens({
$tokens: allTokensForSelectedNetwork,
$tokens: $allTokens,
$exchanges: exchangesStaticData,
$tokensToPin
})
)
: []
);

let tokensFilter = $state('');
const updateFilter = () => (tokensFilter = filter);
const debounceUpdateFilter = debounce(updateFilter);

let filter = $state(initialSearch ?? '');
setContext<ModalTokensListContext>(
MODAL_TOKENS_LIST_CONTEXT_KEY,
initModalTokensListContext({
tokens: [],
filterZeroBalance: false,
filterNetwork: $selectedNetwork,
filterQuery: nonNullish(initialSearch) ? initialSearch : ''
})
);

$effect(() => {
debounceUpdateFilter(filter);
getContext<ModalTokensListContext>(MODAL_TOKENS_LIST_CONTEXT_KEY).setTokens(allTokensSorted);
});

let filteredTokens: Token[] = $derived(
filterTokens({ tokens: allTokensSorted, filter: tokensFilter })
);
let loading = $erc20UserTokensNotInitialized;

let tokens: Token[] = $derived(
filteredTokens.map((token) => {
const modifiedToken =
modifiedTokens[`${token.network.id.description}-${token.id.description}`];

return {
...token,
...(icTokenIcrcCustomToken(token)
? {
enabled: (modifiedToken as IcrcCustomToken)?.enabled ?? token.enabled
}
: {})
};
})
);
let showNetworks = $state(false);

let noTokensMatch = $derived(tokens.length === 0);
const onSelectNetwork = () => {
showNetworks = !showNetworks;
};

let modifiedTokens: Record<string, Token> = $state({});

const onToggle = ({ detail: { id, network, ...rest } }: CustomEvent<Token>) => {
const { id: networkId } = network;
const { [`${networkId.description}-${id.description}`]: current, ...tokens } = modifiedTokens;
Expand Down Expand Up @@ -140,77 +126,48 @@
const save = () => dispatch('icSave', groupModifiedTokens);
</script>

<div class="mb-4">
<InputSearch
bind:filter
showResetButton={notEmptyString(filter)}
placeholder={$i18n.tokens.placeholder.search_token}
autofocus={isDesktop()}
/>
</div>

{#if nonNullish($selectedNetwork)}
<p class="mb-4 pb-2 pt-1 text-tertiary">
{replacePlaceholders($i18n.tokens.manage.text.manage_for_network, {
$network: $selectedNetwork.name
})}
</p>
{/if}

{#if nonNullish(infoElement)}
{@render infoElement()}
{/if}

{#if noTokensMatch}
<button
class="flex w-full flex-col items-center justify-center py-16"
in:fade
onclick={() => dispatch('icAddToken')}
>
<span class="text-7xl">🤔</span>

<span class="py-4 text-center font-bold text-brand-primary-alt no-underline"
>+ {$i18n.tokens.manage.text.do_not_see_import}</span
>
</button>
{#if showNetworks}
<ModalNetworksFilter on:icNetworkFilter={() => (showNetworks = false)} />
{:else}
<div class="flex flex-col overflow-y-hidden py-3 sm:max-h-[26rem]">
<div class="my-3 overflow-y-auto overscroll-contain">
{#each tokens as token (`${token.network.id.description}-${token.id.description}`)}
<Card>
<TokenName data={token} />

<TokenLogo slot="icon" color="white" data={token} badge={{ type: 'network' }} />

<span class="break-all" slot="description">
{token.symbol}
</span>

<svelte:fragment slot="action">
{#if icTokenIcrcCustomToken(token)}
<IcManageTokenToggle {token} on:icToken={onToggle} />
{:else if icTokenEthereumUserToken(token) || isTokenSplToggleable(token)}
<ManageTokenToggle {token} on:icShowOrHideToken={onToggle} />
{:else if isBitcoinToken(token)}
<BtcManageTokenToggle />
{:else if isSolanaToken(token)}
<SolManageTokenToggle />
{/if}
</svelte:fragment>
</Card>
{/each}
</div>
</div>

<button
class="mb-4 flex w-full justify-center pt-4 text-center font-bold text-brand-primary-alt no-underline"
onclick={() => dispatch('icAddToken')}>+ {$i18n.tokens.manage.text.do_not_see_import}</button
<ModalTokensList
{loading}
on:icSelectNetworkFilter={onSelectNetwork}
networkSelectorViewOnly={nonNullish($selectedNetwork)}
>

<ButtonGroup>
<ButtonCancel testId={MANAGE_TOKENS_MODAL_CLOSE} on:click={() => dispatch('icClose')} />
<Button testId={MANAGE_TOKENS_MODAL_SAVE} disabled={saveDisabled} on:click={save}>
{$i18n.core.text.save}
</Button>
</ButtonGroup>
{#snippet tokenListItem(token)}
<LogoButton dividers hover={false}>
<TokenName slot="title" data={token} />

<TokenLogo slot="logo" color="white" data={token} badge={{ type: 'network' }} />

<span class="break-all" slot="description">
{token.symbol}
</span>

<svelte:fragment slot="action">
{#if icTokenIcrcCustomToken(token)}
<IcManageTokenToggle {token} on:icToken={onToggle} />
{:else if icTokenEthereumUserToken(token) || isTokenSplToggleable(token)}
<ManageTokenToggle {token} on:icShowOrHideToken={onToggle} />
{:else if isBitcoinToken(token)}
<BtcManageTokenToggle />
{:else if isSolanaToken(token)}
<SolManageTokenToggle />
{/if}
</svelte:fragment>
</LogoButton>
{/snippet}
{#snippet toolbar()}
<Button colorStyle="secondary-light" on:click={() => dispatch('icAddToken')}
><IconPlus /> {$i18n.tokens.manage.text.import_token}</Button
>
<Button testId={MANAGE_TOKENS_MODAL_SAVE} disabled={saveDisabled} on:click={save}>
{$i18n.core.text.save}
</Button>
{/snippet}
</ModalTokensList>
{/if}
16 changes: 7 additions & 9 deletions src/frontend/src/lib/components/tokens/ModalTokensList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,13 @@
<div class="gap-6 overflow-y-auto overscroll-contain">
<TokensSkeletons {loading}>
{#if noTokensMatch}
<p class="text-primary">
{#if noResults}
{@render noResults()}
{:else}
<p class="text-primary">
{$i18n.core.text.no_results}
</p>
{/if}
</p>
{#if noResults}
{@render noResults()}
{:else}
<p class="text-primary">
{$i18n.core.text.no_results}
</p>
{/if}
{:else}
<ul class="list-none">
{#each $filteredTokens as token (token.id)}
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/lib/constants/test-ids.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const LOGIN_BUTTON = 'login-button';
export const MAX_BUTTON = 'max-button';

export const LOADER_MODAL = 'loader-modal';
export const BUTTON_MODAL_CLOSE = 'close-modal';

export const TOKEN_CARD = 'token-card';
export const TOKEN_GROUP = 'token-group';
Expand Down Expand Up @@ -117,7 +118,6 @@ export const SWAP_TOKENS_MODAL = 'swap-tokens-modal';
export const MANAGE_TOKENS_MODAL = 'manage-tokens-modal';
export const MANAGE_TOKENS_MODAL_BUTTON = 'manage-tokens-modal-button';
export const MANAGE_TOKENS_MODAL_SAVE = 'manage-tokens-modal-save';
export const MANAGE_TOKENS_MODAL_CLOSE = 'manage-tokens-modal-close';
export const MANAGE_TOKENS_MODAL_TOKEN_TOGGLE = 'manage-tokens-modal-token-toggle';

export const NETWORKS_SWITCHER_SELECTOR = 'networks-switcher-selector';
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@
"manage_list": "Manage tokens",
"list_settings": "List settings",
"do_not_see_import": "Don’t see your token? Import",
"import_token": "Import token",
"manage_for_network": "Managing tokens for $network",
"network": "Network",
"all_tokens_zero_balance": "All tokens have zero balance."
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ interface I18nTokens {
manage_list: string;
list_settings: string;
do_not_see_import: string;
import_token: string;
manage_for_network: string;
network: string;
all_tokens_zero_balance: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as appNavigation from '$app/navigation';
import { ICP_NETWORK_SYMBOL } from '$env/networks/networks.icp.env';
import { ICP_TOKEN } from '$env/tokens/tokens.icp.env';
import Transactions from '$lib/components/transactions/Transactions.svelte';
import { MANAGE_TOKENS_MODAL_CLOSE } from '$lib/constants/test-ids.constants';
import { BUTTON_MODAL_CLOSE } from '$lib/constants/test-ids.constants';
import { modalStore } from '$lib/stores/modal.store';
import { mockPage } from '$tests/mocks/page.store.mock';
import { render, waitFor } from '@testing-library/svelte';
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('Transactions', () => {
expect(get(modalStore)?.type).toBe('manage-tokens');

const button: HTMLButtonElement | null = container.querySelector(
`button[data-tid='${MANAGE_TOKENS_MODAL_CLOSE}']`
`button[data-tid='${BUTTON_MODAL_CLOSE}']`
);

button?.click();
Expand All @@ -110,7 +110,7 @@ describe('Transactions', () => {
expect(get(modalStore)?.type).toBe('manage-tokens');

const button: HTMLButtonElement | null = container.querySelector(
`button[data-tid='${MANAGE_TOKENS_MODAL_CLOSE}']`
`button[data-tid='${BUTTON_MODAL_CLOSE}']`
);

mockPage.mock({ token: ICP_TOKEN.name, network: ICP_NETWORK_SYMBOL });
Expand Down
Loading