diff --git a/src/frontend/src/btc/components/convert/BtcConvertFeeTotal.svelte b/src/frontend/src/btc/components/convert/BtcConvertFeeTotal.svelte index c96d9ca446..7ef607785b 100644 --- a/src/frontend/src/btc/components/convert/BtcConvertFeeTotal.svelte +++ b/src/frontend/src/btc/components/convert/BtcConvertFeeTotal.svelte @@ -1,4 +1,5 @@ - import { isNullish } from '@dfinity/utils'; import { getContext } from 'svelte'; import { BTC_CONVERT_FEE } from '$btc/constants/btc.constants'; import { UTXOS_FEE_CONTEXT_KEY, type UtxosFeeContext } from '$btc/stores/utxos-fee.store'; @@ -7,9 +6,6 @@ import ConvertFee from '$lib/components/convert/ConvertFee.svelte'; import { CONVERT_CONTEXT_KEY, type ConvertContext } from '$lib/stores/convert.store'; import { i18n } from '$lib/stores/i18n.store'; - import type { OptionAmount } from '$lib/types/send'; - - export let sendAmount: OptionAmount; const { sourceToken, sourceTokenExchangeRate, destinationToken } = getContext(CONVERT_CONTEXT_KEY); @@ -20,10 +16,7 @@ $: kytFee = $ckBtcMinterInfoStore?.[$destinationToken.id]?.data.kyt_fee; let satoshisFee: bigint | undefined; - $: satoshisFee = - isNullish(sendAmount) || Number(sendAmount) === 0 - ? 0n - : $storeUtxosFeeData?.utxosFee?.feeSatoshis; + $: satoshisFee = $storeUtxosFeeData?.utxosFee?.feeSatoshis; ; $: hasPendingTransactionsStore = initPendingSentTransactionsStatus(source); + let utxosFee: UtxosFee | undefined; + $: utxosFee = nonNullish(sendAmount) ? $storeUtxosFeeData?.utxosFee : undefined; + let invalid: boolean; $: invalid = insufficientFunds || @@ -57,16 +61,13 @@ {:else if nonNullish($hasPendingTransactionsStore)}
- +
{/if} - +
diff --git a/src/frontend/src/btc/components/convert/BtcConvertReview.svelte b/src/frontend/src/btc/components/convert/BtcConvertReview.svelte index b26c7ff0b2..6ea2603868 100644 --- a/src/frontend/src/btc/components/convert/BtcConvertReview.svelte +++ b/src/frontend/src/btc/components/convert/BtcConvertReview.svelte @@ -19,7 +19,7 @@ - + diff --git a/src/frontend/src/icp/components/transactions/IcTransactions.svelte b/src/frontend/src/icp/components/transactions/IcTransactions.svelte index 1b24639c30..da92e37f22 100644 --- a/src/frontend/src/icp/components/transactions/IcTransactions.svelte +++ b/src/frontend/src/icp/components/transactions/IcTransactions.svelte @@ -24,7 +24,7 @@ import { icTransactions } from '$icp/derived/ic-transactions.derived'; import { icTransactionsStore } from '$icp/stores/ic-transactions.store'; import type { IcTransactionUi } from '$icp/types/ic-transaction'; - import { isIcTokenCanistersStrict } from '$icp/validation/ic-token.validation'; + import { hasIndexCanister } from '$icp/validation/ic-token.validation'; import TransactionsPlaceholder from '$lib/components/transactions/TransactionsPlaceholder.svelte'; import Header from '$lib/components/ui/Header.svelte'; import { modalIcToken, modalIcTransaction } from '$lib/derived/modal.derived'; @@ -54,9 +54,6 @@ let noTransactions = false; $: noTransactions = nonNullish($token) && $icTransactionsStore?.[$token.id] === null; - - let hasIndexCanister = false; - $: hasIndexCanister = nonNullish($tokenAsIcToken) && isIcTokenCanistersStrict($tokenAsIcToken); @@ -86,7 +83,9 @@ {/if} {#if noTransactions} - + {:else if $icTransactions.length === 0} {/if} diff --git a/src/frontend/src/icp/validation/ic-token.validation.ts b/src/frontend/src/icp/validation/ic-token.validation.ts index 051d186bba..5135252378 100644 --- a/src/frontend/src/icp/validation/ic-token.validation.ts +++ b/src/frontend/src/icp/validation/ic-token.validation.ts @@ -5,6 +5,7 @@ import { } from '$icp/schema/ic-token.schema'; import type { IcCanistersStrict, IcCkToken, IcToken } from '$icp/types/ic-token'; import type { Token } from '$lib/types/token'; +import { nonNullish } from '@dfinity/utils'; export const isIcToken = (token: Token): token is IcToken => { const { success } = IcTokenSchema.safeParse(token); @@ -29,3 +30,8 @@ export const isIcCkToken = (token: Token): token is IcCkToken => { export const isNotIcCkToken = (token: Token): token is Exclude => !isIcCkToken(token); + +export const hasIndexCanister = (token: IcToken): boolean => + nonNullish(token) && isIcTokenCanistersStrict(token); + +export const hasNoIndexCanister = (token: IcToken): boolean => !hasIndexCanister(token); diff --git a/src/frontend/src/lib/components/core/SignOut.svelte b/src/frontend/src/lib/components/core/SignOut.svelte index 1ac2b0f3da..dd7fe07514 100644 --- a/src/frontend/src/lib/components/core/SignOut.svelte +++ b/src/frontend/src/lib/components/core/SignOut.svelte @@ -9,7 +9,7 @@ const logout = async () => { dispatch('icLogoutTriggered'); - await signOut(); + await signOut({ resetUrl: true }); }; diff --git a/src/frontend/src/lib/components/loaders/Loader.svelte b/src/frontend/src/lib/components/loaders/Loader.svelte index 16319fa970..e5efe2691c 100644 --- a/src/frontend/src/lib/components/loaders/Loader.svelte +++ b/src/frontend/src/lib/components/loaders/Loader.svelte @@ -116,7 +116,7 @@ ); if (!addressSuccess) { - await signOut(); + await signOut({}); return; } diff --git a/src/frontend/src/lib/components/transactions/AllTransactions.svelte b/src/frontend/src/lib/components/transactions/AllTransactions.svelte index 6c5452c618..bca6e85847 100644 --- a/src/frontend/src/lib/components/transactions/AllTransactions.svelte +++ b/src/frontend/src/lib/components/transactions/AllTransactions.svelte @@ -1,10 +1,29 @@
@@ -16,6 +35,14 @@
{/if} + {#if notEmptyString(tokenListWithoutCanister)} + + {replacePlaceholders($i18n.activity.warning.no_index_canister, { + $token_list: tokenListWithoutCanister + })} + + {/if} + {$i18n.activity.info.btc_transactions} diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 4100ac64ab..72243f83c4 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -828,6 +828,9 @@ }, "info": { "btc_transactions": "BTC transaction information is obtained from central third parties and should be independently verified." + }, + "warning": { + "no_index_canister": "Transaction for $token_list can not be displayed. No Index canister." } } } diff --git a/src/frontend/src/lib/services/auth.services.ts b/src/frontend/src/lib/services/auth.services.ts index 3d3fb594f0..7309d0c054 100644 --- a/src/frontend/src/lib/services/auth.services.ts +++ b/src/frontend/src/lib/services/auth.services.ts @@ -11,6 +11,7 @@ import { i18n } from '$lib/stores/i18n.store'; import { testnetsStore } from '$lib/stores/settings.store'; import { toastsClean, toastsError, toastsShow } from '$lib/stores/toasts.store'; import type { ToastMsg } from '$lib/types/toast'; +import { gotoReplaceRoot } from '$lib/utils/nav.utils'; import { replaceHistory } from '$lib/utils/route.utils'; import type { ToastLevel } from '@dfinity/gix-components'; import type { Principal } from '@dfinity/principal'; @@ -58,7 +59,8 @@ export const signIn = async ( } }; -export const signOut = (): Promise => logout({}); +export const signOut = ({ resetUrl = false }: { resetUrl?: boolean }): Promise => + logout({ resetUrl }); export const errorSignOut = (text: string): Promise => logout({ @@ -115,10 +117,12 @@ const clearTestnetsOption = async () => { const logout = async ({ msg = undefined, - clearStorages = true + clearStorages = true, + resetUrl = false }: { msg?: ToastMsg; clearStorages?: boolean; + resetUrl?: boolean; }) => { // To mask not operational UI (a side effect of sometimes slow JS loading after window.reload because of service worker and no cache). busy.start(); @@ -133,6 +137,10 @@ const logout = async ({ appendMsgToUrl(msg); } + if (resetUrl) { + await gotoReplaceRoot(); + } + // Auth: Delegation and identity are cleared from indexedDB by agent-js so, we do not need to clear these // Preferences: We do not clear local storage as well. It contains anonymous information such as the selected theme. diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 10f3837703..bda66a6b5d 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -702,6 +702,7 @@ interface I18nLicense_agreement { interface I18nActivity { text: { title: string }; info: { btc_transactions: string }; + warning: { no_index_canister: string }; } interface I18n { diff --git a/src/frontend/src/lib/utils/info.utils.ts b/src/frontend/src/lib/utils/info.utils.ts index f2a79f8893..3c9d569103 100644 --- a/src/frontend/src/lib/utils/info.utils.ts +++ b/src/frontend/src/lib/utils/info.utils.ts @@ -4,7 +4,8 @@ export type HideInfoKey = | 'oisy_ic_hide_bitcoin_info' | 'oisy_ic_hide_ethereum_info' | 'oisy_ic_hide_erc20_info' - | 'oisy_ic_hide_bitcoin_activity'; + | 'oisy_ic_hide_bitcoin_activity' + | 'oisy_ic_hide_transaction_no_canister'; export const saveHideInfo = (key: HideInfoKey) => { try { diff --git a/src/frontend/src/tests/btc/components/convert/BtcConvertFeeTotal.spec.ts b/src/frontend/src/tests/btc/components/convert/BtcConvertFeeTotal.spec.ts index f74cb27dab..e00a0f9c5c 100644 --- a/src/frontend/src/tests/btc/components/convert/BtcConvertFeeTotal.spec.ts +++ b/src/frontend/src/tests/btc/components/convert/BtcConvertFeeTotal.spec.ts @@ -44,31 +44,27 @@ describe('BtcConvertFeeTotal', () => { store.reset(); }); - it('should calculate totalFee correctly if only default fee is available', () => { + it('should not update totalFee if only default fee is available', () => { const { component } = render(BtcConvertFeeTotal, { context: mockContext({ utxosFeeStore: store }) }); - expect(component.$$.ctx[component.$$.props['totalFee']]).toBe(BTC_CONVERT_FEE); + expect(component.$$.ctx[component.$$.props['totalFee']]).toBeUndefined(); }); - it('should calculate totalFee correctly if default and utxos fees are available', () => { + it('should not update totalFee if only default and utxos fees are available', () => { store.setUtxosFee({ utxosFee: mockUtxosFee }); const { component } = render(BtcConvertFeeTotal, { context: mockContext({ utxosFeeStore: store }) }); - expect(component.$$.ctx[component.$$.props['totalFee']]).toBe( - BTC_CONVERT_FEE + mockUtxosFee.feeSatoshis - ); + expect(component.$$.ctx[component.$$.props['totalFee']]).toBeUndefined(); }); - it('should calculate totalFee correctly if default and ckBTC minter fees are available', () => { + it('should not update totalFee if only default and ckBTC minter fees are available', () => { const tokenId = setupCkBTCStores(); const { component } = render(BtcConvertFeeTotal, { context: mockContext({ utxosFeeStore: store, destinationTokenId: tokenId }) }); - expect(component.$$.ctx[component.$$.props['totalFee']]).toBe( - BTC_CONVERT_FEE + mockCkBtcMinterInfo.kyt_fee - ); + expect(component.$$.ctx[component.$$.props['totalFee']]).toBeUndefined(); }); it('should calculate totalFee correctly if all fees are available', () => { diff --git a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts index 1225849405..c3246330cc 100644 --- a/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts +++ b/src/frontend/src/tests/icp/validation/ic-token.validation.spec.ts @@ -1,6 +1,8 @@ import { IC_CKBTC_INDEX_CANISTER_ID } from '$env/networks.icrc.env'; import type { IcToken } from '$icp/types/ic-token'; import { + hasIndexCanister, + hasNoIndexCanister, isIcCkToken, isIcToken, isIcTokenCanistersStrict, @@ -92,4 +94,32 @@ describe('ic-token.validation', () => { expect(isNotIcCkToken(mockValidToken)).toBe(true); }); }); + + describe('hasIndexCanister', () => { + it('should return false for an IcToken without Index canister', () => { + expect(hasIndexCanister(mockValidIcToken)).toBe(false); + }); + + it('should return true for an IcToken with Index canister', () => { + expect(hasIndexCanister(mockValidIcTokenWithIndex)).toBe(true); + }); + + it('should return false for a token type casted to IcToken', () => { + expect(hasIndexCanister(mockValidToken as IcToken)).toBe(false); + }); + }); + + describe('hasNoIndexCanister', () => { + it('should return true for an IcToken without Index canister', () => { + expect(hasNoIndexCanister(mockValidIcToken)).toBe(true); + }); + + it('should return false for an IcToken with Index canister', () => { + expect(hasNoIndexCanister(mockValidIcTokenWithIndex)).toBe(false); + }); + + it('should return true for a token type casted to IcToken', () => { + expect(hasNoIndexCanister(mockValidToken as IcToken)).toBe(true); + }); + }); }); diff --git a/src/frontend/src/tests/lib/components/transactions/AllTransactions.spec.ts b/src/frontend/src/tests/lib/components/transactions/AllTransactions.spec.ts index ce7b8f7405..e18d3b9222 100644 --- a/src/frontend/src/tests/lib/components/transactions/AllTransactions.spec.ts +++ b/src/frontend/src/tests/lib/components/transactions/AllTransactions.spec.ts @@ -1,9 +1,21 @@ +import { icTransactionsStore } from '$icp/stores/ic-transactions.store'; +import { icrcCustomTokensStore } from '$icp/stores/icrc-custom-tokens.store'; +import type { IcrcCustomToken } from '$icp/types/icrc-custom-token'; import AllTransactions from '$lib/components/transactions/AllTransactions.svelte'; +import { replacePlaceholders } from '$lib/utils/i18n.utils'; import en from '$tests/mocks/i18n.mock'; +import { mockValidIcToken } from '$tests/mocks/ic-tokens.mock'; import { assertNonNullish } from '@dfinity/utils'; import { render } from '@testing-library/svelte'; +import { get } from 'svelte/store'; describe('Activity', () => { + const customIcrcToken: IcrcCustomToken = { + ...mockValidIcToken, + version: 1n, + enabled: true + }; + it('renders the title', () => { const { container } = render(AllTransactions); @@ -15,6 +27,27 @@ describe('Activity', () => { expect(title.textContent).toBe(en.activity.text.title); }); + it('renders the no Index canister warning box', () => { + const tokenWithoutIndexCanister: IcrcCustomToken = { + ...customIcrcToken, + symbol: 'UWT' + }; + + icrcCustomTokensStore.set({ data: tokenWithoutIndexCanister, certified: true }); + + const store = get(icrcCustomTokensStore); + const tokenId = store!.at(0)!.data.id; + icTransactionsStore.nullify(tokenId); + + const { getByText } = render(AllTransactions); + + const exceptedText = replacePlaceholders(en.activity.warning.no_index_canister, { + $token_list: '$UWT' + }); + + expect(getByText(exceptedText)).toBeInTheDocument(); + }); + it('renders the info box list', () => { const { getByText } = render(AllTransactions); diff --git a/src/frontend/src/tests/lib/services/auth.services.spec.ts b/src/frontend/src/tests/lib/services/auth.services.spec.ts new file mode 100644 index 0000000000..70f6385d93 --- /dev/null +++ b/src/frontend/src/tests/lib/services/auth.services.spec.ts @@ -0,0 +1,47 @@ +import { signOut } from '$lib/services/auth.services'; +import { authStore } from '$lib/stores/auth.store'; +import { vi } from 'vitest'; + +const rootLocation = 'https://oisy.com/'; +const activityLocation = 'https://oisy.com/activity'; + +const mockLocation = (url: string) => { + Object.defineProperty(window, 'location', { + writable: true, + value: { + href: url, + reload: vi.fn() + } + }); +}; + +describe('auth.services', () => { + describe('signOut', () => { + it('should call the signOut function of the authStore without resetting the url', async () => { + const signOutSpy = vi.spyOn(authStore, 'signOut'); + + mockLocation(activityLocation); + + await signOut({}); + + expect(signOutSpy).toHaveBeenCalled(); + expect(window.location.href).toEqual(activityLocation); + }); + + it('should call the signOut function of the authStore and resetting the url', async () => { + const signOutSpy = vi.spyOn(authStore, 'signOut'); + + vi.mock('$lib/utils/nav.utils', () => ({ + gotoReplaceRoot: () => mockLocation(rootLocation) + })); + + mockLocation(activityLocation); + + expect(window.location.href).toEqual(activityLocation); + await signOut({ resetUrl: true }); + + expect(signOutSpy).toHaveBeenCalled(); + expect(window.location.href).toEqual(rootLocation); + }); + }); +});