diff --git a/@shared/api/internal.ts b/@shared/api/internal.ts index 174a030a7f..c33162cfe9 100644 --- a/@shared/api/internal.ts +++ b/@shared/api/internal.ts @@ -42,6 +42,8 @@ import { IndexerSettings, SettingsState, ExperimentalFeatures, + AssetKey, + AssetVisibility, } from "./types"; import { MAINNET_NETWORK_DETAILS, @@ -1208,6 +1210,7 @@ export const saveSettings = async ({ isNonSSLEnabled: false, isHideDustEnabled: true, error: "", + hiddenAssets: {}, }; try { @@ -1529,3 +1532,39 @@ export const simulateTransaction = async (args: { response, }; }; + +export const getHiddenAssets = async () => { + let response = { + error: "", + hiddenAssets: {} as Record, + }; + + response = await sendMessageToBackground({ + type: SERVICE_TYPES.GET_HIDDEN_ASSETS, + }); + + return { hiddenAssets: response.hiddenAssets, error: response.error }; +}; + +export const changeAssetVisibility = async ({ + assetIssuer, + assetVisibility, +}: { + assetIssuer: AssetKey; + assetVisibility: AssetVisibility; +}) => { + let response = { + error: "", + hiddenAssets: {} as Record, + }; + + response = await sendMessageToBackground({ + type: SERVICE_TYPES.CHANGE_ASSET_VISIBILITY, + assetVisibility: { + issuer: assetIssuer, + visibility: assetVisibility, + }, + }); + + return { hiddenAssets: response.hiddenAssets, error: response.error }; +}; diff --git a/@shared/api/types.ts b/@shared/api/types.ts index f560816d08..167cb0d45e 100644 --- a/@shared/api/types.ts +++ b/@shared/api/types.ts @@ -21,6 +21,9 @@ export interface UserInfo { export type MigratableAccount = Account & { keyIdIndex: number }; +export type AssetKey = string; // {assetCode}:{issuer/contract ID} issuer pub key for classic, contract ID for tokens +export type AssetVisibility = "visible" | "hidden"; + export interface Response { error: string; apiError: FreighterApiError; @@ -93,6 +96,11 @@ export interface Response { recommendedFee: string; isNonSSLEnabled: boolean; isHideDustEnabled: boolean; + assetVisibility: { + issuer: AssetKey; + visibility: AssetVisibility; + }; + hiddenAssets: Record; } export interface MemoRequiredAccount { @@ -182,6 +190,7 @@ export type Settings = { networkDetails: NetworkDetails; networksList: NetworkDetails[]; error: string; + hiddenAssets: Record; } & Preferences; export interface AssetIcons { diff --git a/@shared/constants/services.ts b/@shared/constants/services.ts index 3a8e1f0aa6..0975cc598f 100644 --- a/@shared/constants/services.ts +++ b/@shared/constants/services.ts @@ -48,6 +48,8 @@ export enum SERVICE_TYPES { MIGRATE_ACCOUNTS = "MIGRATE_ACCOUNTS", ADD_ASSETS_LIST = "ADD_ASSETS_LIST", MODIFY_ASSETS_LIST = "MODIFY_ASSETS_LIST", + CHANGE_ASSET_VISIBILITY = "CHANGE_ASSET_VISIBILITY", + GET_HIDDEN_ASSETS = "GET_HIDDEN_ASSETS", } export enum EXTERNAL_SERVICE_TYPES { diff --git a/extension/e2e-tests/addAsset.test.ts b/extension/e2e-tests/addAsset.test.ts index 5e8319407c..9a1a2433bb 100644 --- a/extension/e2e-tests/addAsset.test.ts +++ b/extension/e2e-tests/addAsset.test.ts @@ -8,7 +8,7 @@ test("Adding unverified Soroban token", async ({ page, extensionId }) => { await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage assets").click({ force: true }); - await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByText("Manage assets")).toBeVisible(); await expectPageToHaveScreenshot({ page, screenshot: "manage-assets-page.png", @@ -40,7 +40,7 @@ test("Adding Soroban verified token", async ({ page, extensionId }) => { await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage Assets").click({ force: true }); - await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByText("Manage assets")).toBeVisible(); await page.getByText("Add an asset").click({ force: true }); await page.getByText("Add manually").click({ force: true }); await page.getByTestId("search-token-input").fill(USDC_TOKEN_ADDRESS); diff --git a/extension/e2e-tests/loadAccount.test.ts b/extension/e2e-tests/loadAccount.test.ts index 136e2ee17c..3fbaeedee3 100644 --- a/extension/e2e-tests/loadAccount.test.ts +++ b/extension/e2e-tests/loadAccount.test.ts @@ -40,5 +40,5 @@ test("Switches account without password prompt", async ({ await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage Assets").click({ force: true }); - await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByText("Manage assets")).toBeVisible(); }); diff --git a/extension/e2e-tests/sendPayment.test.ts b/extension/e2e-tests/sendPayment.test.ts index bf86725133..4fddb62ffe 100644 --- a/extension/e2e-tests/sendPayment.test.ts +++ b/extension/e2e-tests/sendPayment.test.ts @@ -249,7 +249,7 @@ test("Send token payment to C address", async ({ page, extensionId }) => { // add E2E token await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage Assets").click({ force: true }); - await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByText("Manage assets")).toBeVisible(); await page.getByText("Add an asset").click({ force: true }); await page.getByText("Add manually").click({ force: true }); await page.getByTestId("search-token-input").fill(TEST_TOKEN_ADDRESS); diff --git a/extension/src/background/messageListener/popupMessageListener.ts b/extension/src/background/messageListener/popupMessageListener.ts index 34b2f5ce8b..acb32e38d7 100644 --- a/extension/src/background/messageListener/popupMessageListener.ts +++ b/extension/src/background/messageListener/popupMessageListener.ts @@ -53,6 +53,7 @@ import { IS_HASH_SIGNING_ENABLED_ID, IS_NON_SSL_ENABLED_ID, IS_HIDE_DUST_ENABLED_ID, + HIDDEN_ASSETS, } from "constants/localStorageTypes"; import { FUTURENET_NETWORK_DETAILS, @@ -1316,6 +1317,7 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { const assetsLists = await getAssetsLists(); const isNonSSLEnabled = await getIsNonSSLEnabled(); const isHideDustEnabled = await getIsHideDustEnabled(); + const { hiddenAssets } = await getHiddenAssets(); return { allowList: await getAllowList(), @@ -1331,6 +1333,7 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { assetsLists, isNonSSLEnabled, isHideDustEnabled, + hiddenAssets, }; }; @@ -1766,6 +1769,21 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { return { assetsLists: await getAssetsLists() }; }; + const changeAssetVisibility = async () => { + const { assetVisibility } = request; + + const { hiddenAssets } = await getHiddenAssets(); + hiddenAssets[assetVisibility.issuer] = assetVisibility.visibility; + + await localStore.setItem(HIDDEN_ASSETS, hiddenAssets); + return { hiddenAssets }; + }; + + const getHiddenAssets = async () => { + const hiddenAssets = (await localStore.getItem(HIDDEN_ASSETS)) || {}; + return { hiddenAssets }; + }; + const messageResponder: MessageResponder = { [SERVICE_TYPES.CREATE_ACCOUNT]: createAccount, [SERVICE_TYPES.FUND_ACCOUNT]: fundAccount, @@ -1818,6 +1836,8 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { [SERVICE_TYPES.MIGRATE_ACCOUNTS]: migrateAccounts, [SERVICE_TYPES.ADD_ASSETS_LIST]: addAssetsList, [SERVICE_TYPES.MODIFY_ASSETS_LIST]: modifyAssetsList, + [SERVICE_TYPES.CHANGE_ASSET_VISIBILITY]: changeAssetVisibility, + [SERVICE_TYPES.GET_HIDDEN_ASSETS]: getHiddenAssets, }; return messageResponder[request.type](); diff --git a/extension/src/constants/localStorageTypes.ts b/extension/src/constants/localStorageTypes.ts index eca894aefc..6d36a6e42d 100644 --- a/extension/src/constants/localStorageTypes.ts +++ b/extension/src/constants/localStorageTypes.ts @@ -23,3 +23,4 @@ export const IS_HASH_SIGNING_ENABLED_ID = "isHashSigningEnabled"; export const IS_NON_SSL_ENABLED_ID = "isNonSSLEnabled"; export const IS_BLOCKAID_ANNOUNCED_ID = "isBlockaidAnnounced"; export const IS_HIDE_DUST_ENABLED_ID = "isHideDustEnabled"; +export const HIDDEN_ASSETS = "hiddenAssets"; diff --git a/extension/src/popup/components/manageAssets/AssetVisibility/index.tsx b/extension/src/popup/components/manageAssets/AssetVisibility/index.tsx new file mode 100644 index 0000000000..a0ae95a904 --- /dev/null +++ b/extension/src/popup/components/manageAssets/AssetVisibility/index.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useRef } from "react"; +import { useHistory } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { Loader } from "@stellar/design-system"; + +import { View } from "popup/basics/layout/View"; +import { SubviewHeader } from "popup/components/SubviewHeader"; +import { + getAccountBalances, + resetSubmission, +} from "popup/ducks/transactionSubmission"; +import { + settingsNetworkDetailsSelector, + settingsSorobanSupportedSelector, +} from "popup/ducks/settings"; +import { publicKeySelector } from "popup/ducks/accountServices"; +import { useFetchDomains } from "popup/helpers/useFetchDomains"; +import { ToggleAssetRows } from "../ToggleAssetRows"; + +import "./styles.scss"; + +export const AssetVisibility = () => { + const { t } = useTranslation(); + const history = useHistory(); + const isSorobanSuported = useSelector(settingsSorobanSupportedSelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + const dispatch = useDispatch(); + const publicKey = useSelector(publicKeySelector); + + const ManageAssetRowsWrapperRef = useRef(null); + + const { assets, isManagingAssets } = useFetchDomains(); + + useEffect(() => { + dispatch( + getAccountBalances({ + publicKey, + networkDetails, + showHidden: true, + }), + ); + return () => { + dispatch(resetSubmission()); + }; + }, [publicKey, dispatch, networkDetails]); + + const goBack = () => { + dispatch(resetSubmission()); + history.goBack(); + }; + + return ( + + + + {assets.isLoading ? ( +
+ +
+ ) : ( +
+
+ +
+
+ )} +
+
+ ); +}; diff --git a/extension/src/popup/components/manageAssets/AssetVisibility/styles.scss b/extension/src/popup/components/manageAssets/AssetVisibility/styles.scss new file mode 100644 index 0000000000..8aecfcdf69 --- /dev/null +++ b/extension/src/popup/components/manageAssets/AssetVisibility/styles.scss @@ -0,0 +1,50 @@ +.ToggleAsset { + &__close-btn { + color: var(--sds-clr-gray-10); + } + + &__hide-btn { + background-color: transparent; + border: none; + padding: 0; + align-items: center; + cursor: pointer; + display: flex; + width: var(--back--button-dimension); + height: var(--back--button-dimension); + color: var(--sds-clr-gray-10); + } + + &__loader { + height: 100%; + width: 100%; + z-index: calc(var(--back--button-z-index) + 1); + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 0; + left: 0; + } + + &__wrapper { + display: flex; + flex-direction: column; + height: 100%; + } + + &__assets { + flex-grow: 1; + } + + &__button { + a { + color: var(--sds-clr-gray-12); + } + + button { + text-wrap: nowrap; + } + } +} diff --git a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx index 3bf6883bd7..b2296eae41 100644 --- a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx +++ b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx @@ -1,15 +1,13 @@ -import React, { useEffect, useRef, useState } from "react"; -import { useSelector } from "react-redux"; -import { Link } from "react-router-dom"; +import React, { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link, useHistory } from "react-router-dom"; import { Button, Icon, Loader } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; import { ROUTES } from "popup/constants/routes"; -import { sortBalances } from "popup/helpers/account"; -import { useIsSoroswapEnabled, useIsSwap } from "popup/helpers/useIsSwap"; import { - transactionSubmissionSelector, - AssetSelectType, + getAccountBalances, + resetSubmission, } from "popup/ducks/transactionSubmission"; import { settingsNetworkDetailsSelector, @@ -17,162 +15,72 @@ import { } from "popup/ducks/settings"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { View } from "popup/basics/layout/View"; -import { getCanonicalFromAsset } from "helpers/stellar"; -import { getAssetDomain } from "popup/helpers/getAssetDomain"; -import { getNativeContractDetails } from "popup/helpers/searchAsset"; -import { isAssetSuspicious } from "popup/helpers/blockaid"; +import { publicKeySelector } from "popup/ducks/accountServices"; +import { useFetchDomains } from "popup/helpers/useFetchDomains"; -import { Balances } from "@shared/api/types"; - -import { ManageAssetCurrency, ManageAssetRows } from "../ManageAssetRows"; +import { ManageAssetRows } from "../ManageAssetRows"; import { SelectAssetRows } from "../SelectAssetRows"; import "./styles.scss"; -interface ChooseAssetProps { - balances: Balances; -} - -export const ChooseAsset = ({ balances }: ChooseAssetProps) => { +export const ChooseAsset = () => { + const history = useHistory(); const { t } = useTranslation(); - const { assetIcons, assetSelect, soroswapTokens } = useSelector( - transactionSubmissionSelector, - ); const isSorobanSuported = useSelector(settingsSorobanSupportedSelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); + const dispatch = useDispatch(); + const publicKey = useSelector(publicKeySelector); - const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]); const ManageAssetRowsWrapperRef = useRef(null); - const [isLoading, setIsLoading] = useState(false); - const isSwap = useIsSwap(); - const isSoroswapEnabled = useIsSoroswapEnabled(); - - const isManagingAssets = assetSelect.type === AssetSelectType.MANAGE; useEffect(() => { - const fetchDomains = async () => { - setIsLoading(true); - const collection = [] as ManageAssetCurrency[]; - const sortedBalances = sortBalances(balances); - - // TODO: cache home domain when getting asset icon - // https://github.com/stellar/freighter/issues/410 - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < sortedBalances.length; i += 1) { - if (sortedBalances[i].liquidityPoolId) { - // eslint-disable-next-line - continue; - } - - const { token, contractId, blockaidData } = sortedBalances[i]; + dispatch( + getAccountBalances({ + publicKey, + networkDetails, + }), + ); + }, [publicKey, dispatch, networkDetails]); - const code = token.code || ""; - let issuer = { - key: "", - }; + const { assets, isManagingAssets } = useFetchDomains(); - if ("issuer" in token) { - issuer = token.issuer; - } - - // If we are in the swap flow and the asset has decimals (is a token), we skip it if Soroswap is not enabled - if ("decimals" in sortedBalances[i] && isSwap && !isSoroswapEnabled) { - // eslint-disable-next-line - continue; - } - - if (code !== "XLM") { - let domain = ""; - - if (issuer.key) { - try { - // eslint-disable-next-line no-await-in-loop - domain = await getAssetDomain( - issuer.key, - networkDetails.networkUrl, - networkDetails.networkPassphrase, - ); - } catch (e) { - console.error(e); - } - } - - collection.push({ - code, - issuer: issuer.key, - image: assetIcons[getCanonicalFromAsset(code, issuer.key)], - domain, - contract: contractId, - isSuspicious: isAssetSuspicious(blockaidData), - }); - // include native asset for asset dropdown selection - } else if (!isManagingAssets) { - collection.push({ - code, - issuer: "", - image: "", - domain: "", - isSuspicious: false, - }); - } - } - - if (isSoroswapEnabled && isSwap && !assetSelect.isSource) { - soroswapTokens.forEach((token) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const canonical = getCanonicalFromAsset(token.code, token.contract); - const nativeContractDetails = - getNativeContractDetails(networkDetails); - - // if we have a balance for a token, it will have been handled above. - // This is designed to populate tokens available from Soroswap that the user does not already have - if ( - balances && - !balances[canonical] && - token.contract !== nativeContractDetails.contract - ) { - collection.push({ - code: token.code, - issuer: token.contract, - image: token.icon, - domain: "", - icon: token.icon, - }); - } - }); - } - - setAssetRows(collection); - setIsLoading(false); - }; - - fetchDomains(); - }, [ - assetIcons, - balances, - isManagingAssets, - isSorobanSuported, - isSwap, - isSoroswapEnabled, - assetSelect.isSource, - soroswapTokens, - networkDetails, - ]); + const goBack = () => { + dispatch(resetSubmission()); + history.goBack(); + }; return ( : undefined} + title={t("Manage assets")} + customBackIcon={} + customBackAction={goBack} + rightButton={ + + + + } /> - {isLoading ? ( + {assets.isLoading ? (
) : ( -
- {!assetRows.length ? ( +
+ {!assets.assetRows.length ? (

You have no assets added. Get started by adding an asset @@ -187,9 +95,9 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { ref={ManageAssetRowsWrapperRef} > {isManagingAssets ? ( - + ) : ( - + )}

)} diff --git a/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss b/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss index f97f58555d..871f1443c2 100644 --- a/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss +++ b/extension/src/popup/components/manageAssets/ChooseAsset/styles.scss @@ -1,4 +1,20 @@ .ChooseAsset { + &__close-btn { + color: var(--sds-clr-gray-10); + } + + &__hide-btn { + background-color: transparent; + border: none; + padding: 0; + align-items: center; + cursor: pointer; + display: flex; + width: var(--back--button-dimension); + height: var(--back--button-dimension); + color: var(--sds-clr-gray-10); + } + &__loader { height: 100%; width: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index 5349ea9813..0ce4e44222 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -244,7 +244,7 @@ export const ManageAssetRows = ({ ); }; -interface AssetRowData { +export interface AssetRowData { code?: string; issuer?: string; image?: string; diff --git a/extension/src/popup/components/manageAssets/ToggleAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ToggleAssetRows/index.tsx new file mode 100644 index 0000000000..b3b798acc3 --- /dev/null +++ b/extension/src/popup/components/manageAssets/ToggleAssetRows/index.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import { Toggle } from "@stellar/design-system"; +import { useDispatch, useSelector } from "react-redux"; + +import { AssetKey } from "@shared/api/types"; +import { + formatDomain, + getCanonicalFromAsset, + truncateString, +} from "helpers/stellar"; +import { AssetIcon } from "popup/components/account/AccountAssets"; +import { changeAssetVisibility, settingsSelector } from "popup/ducks/settings"; +import { isAssetVisible } from "popup/helpers/settings"; +import { AssetRowData, ManageAssetCurrency } from "../ManageAssetRows"; + +import "./styles.scss"; + +interface ToggleAssetRowsProps { + assetRows: ManageAssetCurrency[]; +} + +export const ToggleAssetRows = ({ assetRows }: ToggleAssetRowsProps) => { + const dispatch = useDispatch(); + const { hiddenAssets } = useSelector(settingsSelector); + + const handleIsVisibleChange = (issuer: AssetKey) => { + const visibility = isAssetVisible(hiddenAssets, issuer) + ? "hidden" + : "visible"; + dispatch( + changeAssetVisibility({ + issuer, + visibility, + }), + ); + }; + + return ( + <> +
+
+ {assetRows.map( + ({ + code = "", + domain, + image = "", + issuer = "", + name = "", + isSuspicious, + }) => { + const canonicalAsset = getCanonicalFromAsset(code, issuer); + return ( +
+ + ) => + handleIsVisibleChange(canonicalAsset) + } + /> +
+ ); + }, + )} +
+
+ + ); +}; + +export const ToggleAssetRow = ({ + code = "", + issuer = "", + image = "", + domain, + name, + isSuspicious = false, +}: AssetRowData) => { + const canonicalAsset = getCanonicalFromAsset(code, issuer); + const assetCode = name || code; + const truncatedAssetCode = + assetCode.length > 20 ? truncateString(assetCode) : assetCode; + + return ( + <> + +
+
+ {truncatedAssetCode} +
+
+ {formatDomain(domain)} +
+
+ + ); +}; diff --git a/extension/src/popup/components/manageAssets/ToggleAssetRows/styles.scss b/extension/src/popup/components/manageAssets/ToggleAssetRows/styles.scss new file mode 100644 index 0000000000..a3e8b43d3d --- /dev/null +++ b/extension/src/popup/components/manageAssets/ToggleAssetRows/styles.scss @@ -0,0 +1,69 @@ +@use "../../../styles/utils.scss" as *; + +.ToggleAssetRows { + &__empty { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + + .EmptyMessage { + display: flex; + align-items: center; + color: var(--sds-clr-gray-09); + + svg { + margin-right: pxToRem(10px); + stroke: var(--sds-clr-gray-09); + } + } + } + &__scrollbar { + height: 100%; + } + &__content { + display: flex; + flex-direction: column; + gap: pxToRem(24px); + justify-content: space-between; + } + + &--scrolling { + padding-right: pxToRem(10px); + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + + &:last-child { + padding-bottom: pxToRem(64px); + } + + &__info { + color: var(--sds-clr-gray-12); + flex-grow: 1; + line-height: pxToRem(24px); + &__header { + display: flex; + } + } + } + + &__icon { + width: pxToRem(32px); + } + + &__bullet { + background: var(--color-purple-60); + height: pxToRem(24px); + width: pxToRem(24px); + } + + &__domain { + color: var(--sds-clr-gray-09); + font-size: var(--sds-fs-secondary); + line-height: pxToRem(22px); + } +} diff --git a/extension/src/popup/constants/metricsNames.ts b/extension/src/popup/constants/metricsNames.ts index 187f3e95b6..d249f5b85b 100644 --- a/extension/src/popup/constants/metricsNames.ts +++ b/extension/src/popup/constants/metricsNames.ts @@ -73,6 +73,7 @@ export const METRIC_NAMES = { viewManageAssets: "loaded screen: manage assets", viewSearchAsset: "loaded screen: search asset", + viewAssetVisibility: "loaded screen: asset visibility", viewTrustlineError: "loaded screen: trustline error", viewAddAsset: "loaded screen: add asset manually", diff --git a/extension/src/popup/constants/routes.ts b/extension/src/popup/constants/routes.ts index 5076788c75..f654a45580 100644 --- a/extension/src/popup/constants/routes.ts +++ b/extension/src/popup/constants/routes.ts @@ -52,6 +52,7 @@ export enum ROUTES { manageAssets = "/manage-assets", searchAsset = "/manage-assets/search-asset", + assetVisibility = "/manage-assets/asset-visibility", addAsset = "/manage-assets/add-asset", manageNetwork = "/manage-network", addNetwork = "/manage-network/add-network", diff --git a/extension/src/popup/ducks/accountServices.ts b/extension/src/popup/ducks/accountServices.ts index 2de999b091..ecc26ee6d3 100644 --- a/extension/src/popup/ducks/accountServices.ts +++ b/extension/src/popup/ducks/accountServices.ts @@ -503,6 +503,9 @@ const authSlice = createSlice({ name: "auth", initialState, reducers: { + resetAccountStatus: (state) => { + state.accountStatus = initialState.accountStatus; + }, clearApiError(state) { state.error = ""; }, @@ -888,6 +891,7 @@ export const accountStatusSelector = createSelector( (auth: InitialState) => auth.accountStatus, ); -export const { clearApiError, setConnectingWalletType } = authSlice.actions; +export const { clearApiError, setConnectingWalletType, resetAccountStatus } = + authSlice.actions; export { reducer }; diff --git a/extension/src/popup/ducks/settings.ts b/extension/src/popup/ducks/settings.ts index 558749d955..4ccc79f670 100644 --- a/extension/src/popup/ducks/settings.ts +++ b/extension/src/popup/ducks/settings.ts @@ -16,6 +16,8 @@ import { editCustomNetwork as editCustomNetworkService, addAssetsList as addAssetsListService, modifyAssetsList as modifyAssetsListService, + getHiddenAssets as getHiddenAssetsService, + changeAssetVisibility as changeAssetVisibilityService, } from "@shared/api/internal"; import { NETWORKS, @@ -34,6 +36,8 @@ import { IndexerSettings, SettingsState, ExperimentalFeatures, + AssetKey, + AssetVisibility, } from "@shared/api/types"; import { isMainnet } from "helpers/stellar"; @@ -56,6 +60,7 @@ const settingsInitialState: Settings = { isMemoValidationEnabled: true, isHideDustEnabled: true, error: "", + hiddenAssets: {}, }; const experimentalFeaturesInitialState = { @@ -272,6 +277,43 @@ export const modifyAssetsList = createAsyncThunk< }, ); +export const getHiddenAssets = createAsyncThunk< + { hiddenAssets: Record; error: string }, + { rejectValue: ErrorMessage } +>("settings/getHiddenAssets", async (_, thunkApi) => { + const res = await getHiddenAssetsService(); + + if (res.error) { + return thunkApi.rejectWithValue({ + errorMessage: res.error || "Unable to get hidden assets", + }); + } + + return res; +}); + +export const changeAssetVisibility = createAsyncThunk< + { hiddenAssets: Record; error: string }, + { issuer: AssetKey; visibility: AssetVisibility }, + { rejectValue: ErrorMessage } +>( + "settings/changeAssetVisibility", + async ({ issuer, visibility }, thunkApi) => { + const res = await changeAssetVisibilityService({ + assetIssuer: issuer, + assetVisibility: visibility, + }); + + if (res.error) { + return thunkApi.rejectWithValue({ + errorMessage: res.error || "Unable to toggle asset visibility", + }); + } + + return res; + }, +); + const settingsSlice = createSlice({ name: "settings", initialState, @@ -370,6 +412,7 @@ const settingsSlice = createSlice({ isRpcHealthy, userNotification, assetsLists, + hiddenAssets, isNonSSLEnabled, isHideDustEnabled, } = action?.payload || { @@ -389,6 +432,7 @@ const settingsSlice = createSlice({ isRpcHealthy, userNotification, assetsLists, + hiddenAssets, isNonSSLEnabled, isHideDustEnabled, settingsState: SettingsState.SUCCESS, @@ -535,6 +579,47 @@ const settingsSlice = createSlice({ }; }, ); + builder.addCase( + getHiddenAssets.fulfilled, + ( + state, + action: PayloadAction<{ + hiddenAssets: Record; + }>, + ) => { + const { hiddenAssets } = action?.payload; + + return { + ...state, + hiddenAssets, + settingsState: SettingsState.SUCCESS, + }; + }, + ); + builder.addCase(getHiddenAssets.pending, (state) => ({ + ...state, + settingsState: SettingsState.LOADING, + })); + builder.addCase(getHiddenAssets.rejected, (state) => ({ + ...state, + settingsState: SettingsState.ERROR, + })); + builder.addCase( + changeAssetVisibility.fulfilled, + ( + state, + action: PayloadAction<{ + hiddenAssets: Record; + }>, + ) => { + const { hiddenAssets } = action.payload; + + return { + ...state, + hiddenAssets, + }; + }, + ); }, }); diff --git a/extension/src/popup/ducks/transactionSubmission.ts b/extension/src/popup/ducks/transactionSubmission.ts index a86365b2c3..206a0644e5 100644 --- a/extension/src/popup/ducks/transactionSubmission.ts +++ b/extension/src/popup/ducks/transactionSubmission.ts @@ -20,6 +20,7 @@ import { removeTokenId as internalRemoveTokenId, submitFreighterTransaction as internalSubmitFreighterTransaction, submitFreighterSorobanTransaction as internalSubmitFreighterSorobanTransaction, + getHiddenAssets as internalGetHiddenAssets, } from "@shared/api/internal"; import { @@ -53,6 +54,7 @@ import { getSoroswapTokens as getSoroswapTokensService, } from "popup/helpers/sorobanSwap"; import { hardwareSign, hardwareSignAuth } from "popup/helpers/hardwareConnect"; +import { filterHiddenBalances } from "popup/helpers/account"; export const signFreighterTransaction = createAsyncThunk< { signedTransaction: string }, @@ -373,33 +375,45 @@ export const getAccountBalances = createAsyncThunk< { publicKey: string; networkDetails: NetworkDetails; + showHidden?: boolean; }, { rejectValue: ErrorMessage } ->("getAccountBalances", async ({ publicKey, networkDetails }, thunkApi) => { - try { - let balances; +>( + "getAccountBalances", + async ({ publicKey, networkDetails, showHidden = false }, thunkApi) => { + try { + let balances; - const isMainnet = isMainnetHelper(networkDetails); + const isMainnet = isMainnetHelper(networkDetails); + const hiddenAssets = await internalGetHiddenAssets(); - if (isCustomNetwork(networkDetails)) { - balances = await internalGetAccountBalancesStandalone({ - publicKey, - networkDetails, - isMainnet, - }); - } else { - balances = await internalgetAccountIndexerBalances( - publicKey, - networkDetails, - ); - } + if (isCustomNetwork(networkDetails)) { + balances = await internalGetAccountBalancesStandalone({ + publicKey, + networkDetails, + isMainnet, + }); + } else { + balances = await internalgetAccountIndexerBalances( + publicKey, + networkDetails, + ); + } - storeBalanceMetricData(publicKey, balances.isFunded || false); - return balances; - } catch (e) { - return thunkApi.rejectWithValue({ errorMessage: e as string }); - } -}); + storeBalanceMetricData(publicKey, balances.isFunded || false); + const filteredBalances = showHidden + ? balances.balances + : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + filterHiddenBalances(balances.balances, hiddenAssets.hiddenAssets); + return { + ...balances, + balances: filteredBalances, + }; + } catch (e) { + return thunkApi.rejectWithValue({ errorMessage: e as string }); + } + }, +); export const getDestinationBalances = createAsyncThunk< AccountBalancesInterface, @@ -676,6 +690,7 @@ const transactionSubmissionSlice = createSlice({ resetSubmission: () => initialState, resetAccountBalanceStatus: (state) => { state.accountBalanceStatus = initialState.accountBalanceStatus; + state.accountBalances = initialState.accountBalances; }, resetDestinationAmount: (state) => { state.transactionData.destinationAmount = diff --git a/extension/src/popup/helpers/account.ts b/extension/src/popup/helpers/account.ts index 2aa4437952..a808d35fd1 100644 --- a/extension/src/popup/helpers/account.ts +++ b/extension/src/popup/helpers/account.ts @@ -2,8 +2,11 @@ import { Horizon } from "stellar-sdk"; import { BigNumber } from "bignumber.js"; import { AssetType, + AssetVisibility, + BalanceMap, Balances, HorizonOperation, + AssetKey, SorobanBalance, TokenBalances, } from "@shared/api/types"; @@ -16,6 +19,7 @@ import { isTestnet, } from "helpers/stellar"; import { getAttrsFromSorobanHorizonOp } from "./soroban"; +import { isAssetVisible } from "./settings"; export const LP_IDENTIFIER = ":lp"; @@ -270,3 +274,24 @@ export const displaySorobanId = ( }; export const isSorobanIssuer = (issuer: string) => !issuer.startsWith("G"); + +export const filterHiddenBalances = ( + balances: BalanceMap, + hiddenAssets: Record, +) => { + const balanceKeys = Object.keys(balances); + const hiddenKeys = balanceKeys.filter((key) => { + if (key === "native") { + return false; + } + const [code, issuer] = key.split(":"); + if (!issuer) { + return true; + } + return !isAssetVisible(hiddenAssets, getCanonicalFromAsset(code, issuer)); + }); + + return Object.fromEntries( + Object.entries(balances).filter(([key]) => !hiddenKeys.includes(key)), + ) as BalanceMap; +}; diff --git a/extension/src/popup/helpers/settings.ts b/extension/src/popup/helpers/settings.ts new file mode 100644 index 0000000000..3f03ed7a50 --- /dev/null +++ b/extension/src/popup/helpers/settings.ts @@ -0,0 +1,6 @@ +import { AssetVisibility, AssetKey } from "@shared/api/types"; + +export const isAssetVisible = ( + hiddenAssets: Record, + key: AssetKey, +) => !hiddenAssets[key] || hiddenAssets[key] === "visible"; diff --git a/extension/src/popup/helpers/useFetchDomains.ts b/extension/src/popup/helpers/useFetchDomains.ts new file mode 100644 index 0000000000..a13e7fa3c9 --- /dev/null +++ b/extension/src/popup/helpers/useFetchDomains.ts @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +import { ManageAssetCurrency } from "popup/components/manageAssets/ManageAssetRows"; +import { + AssetSelectType, + transactionSubmissionSelector, +} from "popup/ducks/transactionSubmission"; +import { + settingsNetworkDetailsSelector, + settingsSorobanSupportedSelector, +} from "popup/ducks/settings"; +import { getCanonicalFromAsset } from "helpers/stellar"; +import { isAssetSuspicious } from "./blockaid"; +import { getNativeContractDetails } from "./searchAsset"; +import { useIsSoroswapEnabled, useIsSwap } from "./useIsSwap"; +import { sortBalances } from "./account"; +import { getAssetDomain } from "./getAssetDomain"; + +export const useFetchDomains = () => { + const isSwap = useIsSwap(); + const isSoroswapEnabled = useIsSoroswapEnabled(); + const { assetIcons, assetSelect, soroswapTokens, accountBalances } = + useSelector(transactionSubmissionSelector); + const isSorobanSuported = useSelector(settingsSorobanSupportedSelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + + const isManagingAssets = assetSelect.type === AssetSelectType.MANAGE; + const { balances } = accountBalances; + + const [assets, setAssets] = useState({ + assetRows: [] as ManageAssetCurrency[], + // TODO: REFACTOR getAccountBalances to a hook + // This loading state should be derived from the lifecycle of fetching account balances + // but the shared store state can cause this to be out of sync, and glitch the empty state. + isLoading: true, + }); + + useEffect(() => { + const fetchDomains = async () => { + if (!balances || assets.assetRows.length) { + return; + } + + setAssets({ + assetRows: [], + isLoading: true, + }); + + const collection = [] as ManageAssetCurrency[]; + const sortedBalances = sortBalances(balances); + + // TODO: cache home domain when getting asset icon + // https://github.com/stellar/freighter/issues/410 + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < sortedBalances.length; i += 1) { + if (sortedBalances[i].liquidityPoolId) { + // eslint-disable-next-line + continue; + } + + const { token, contractId, blockaidData } = sortedBalances[i]; + + const code = token.code || ""; + let issuer = { + key: "", + }; + + if ("issuer" in token) { + issuer = token.issuer; + } + + // If we are in the swap flow and the asset has decimals (is a token), we skip it if Soroswap is not enabled + if ("decimals" in sortedBalances[i] && isSwap && !isSoroswapEnabled) { + // eslint-disable-next-line + continue; + } + + if (code !== "XLM") { + let domain = ""; + + if (issuer.key) { + try { + // eslint-disable-next-line no-await-in-loop + domain = await getAssetDomain( + issuer.key, + networkDetails.networkUrl, + networkDetails.networkPassphrase, + ); + } catch (e) { + console.error(e); + } + } + + collection.push({ + code, + issuer: issuer.key, + image: assetIcons[getCanonicalFromAsset(code, issuer.key)], + domain, + contract: contractId, + isSuspicious: isAssetSuspicious(blockaidData), + }); + // include native asset for asset dropdown selection + } else if (!isManagingAssets) { + collection.push({ + code, + issuer: "", + image: "", + domain: "", + isSuspicious: false, + }); + } + } + + if (isSoroswapEnabled && isSwap && !assetSelect.isSource) { + soroswapTokens.forEach((token) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const canonical = getCanonicalFromAsset(token.code, token.contract); + const nativeContractDetails = + getNativeContractDetails(networkDetails); + + // if we have a balance for a token, it will have been handled above. + // This is designed to populate tokens available from Soroswap that the user does not already have + if ( + balances && + !balances[canonical] && + token.contract !== nativeContractDetails.contract + ) { + collection.push({ + code: token.code, + issuer: token.contract, + image: token.icon, + domain: "", + icon: token.icon, + }); + } + }); + } + + setAssets({ + assetRows: collection, + isLoading: false, + }); + }; + fetchDomains(); + }, [ + assets.assetRows, + assetIcons, + balances, + isManagingAssets, + isSorobanSuported, + isSwap, + isSoroswapEnabled, + assetSelect.isSource, + soroswapTokens, + networkDetails, + ]); + + return { + assets, + isManagingAssets, + }; +}; diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index 71c10f42dc..c6f8993d56 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -51,6 +51,7 @@ const routeToEventName = { [ROUTES.sendPaymentConfirm]: METRIC_NAMES.sendPaymentConfirm, [ROUTES.manageAssets]: METRIC_NAMES.viewManageAssets, [ROUTES.searchAsset]: METRIC_NAMES.viewSearchAsset, + [ROUTES.assetVisibility]: METRIC_NAMES.viewAssetVisibility, [ROUTES.addAsset]: METRIC_NAMES.viewAddAsset, [ROUTES.swap]: METRIC_NAMES.viewSwap, [ROUTES.swapAmount]: METRIC_NAMES.swapAmount, diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index ed874b3ab5..205527b6da 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -9,11 +9,7 @@ import { import { useTranslation } from "react-i18next"; import { getAccountHistory } from "@shared/api/internal"; -import { - AccountBalancesInterface, - ActionStatus, - AssetType, -} from "@shared/api/types"; +import { ActionStatus, AssetType } from "@shared/api/types"; import { settingsNetworkDetailsSelector, @@ -53,14 +49,8 @@ import { Loading } from "popup/components/Loading"; import { NotFundedMessage } from "popup/components/account/NotFundedMessage"; import "popup/metrics/authServices"; - import "./styles.scss"; -export const defaultAccountBalances = { - balances: null, - isFunded: null, -} as AccountBalancesInterface; - export const Account = () => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -175,7 +165,6 @@ export const Account = () => { return ( <> { - const { accountBalances, destinationBalances, assetSelect } = useSelector( - transactionSubmissionSelector, - ); - - let balances; - // path payment destAsset is the only time we use recipient trustlines - if ( - assetSelect.type === AssetSelectType.PATH_PAY && - assetSelect.isSource === false - ) { - balances = destinationBalances.balances; - } else { - balances = accountBalances.balances; - } - - if (!balances) { - return ( - - ); - } - - return ( - <> - - - - - - - - - - - - - ); -}; +export const ManageAssets = () => ( + <> + + + + + + + + + + + + + + + +); diff --git a/extension/src/popup/views/__tests__/Account.test.tsx b/extension/src/popup/views/__tests__/Account.test.tsx index 942646c1d2..9418d9b15e 100644 --- a/extension/src/popup/views/__tests__/Account.test.tsx +++ b/extension/src/popup/views/__tests__/Account.test.tsx @@ -73,6 +73,10 @@ jest.spyOn(global, "fetch").mockImplementation((url) => { } as any); }); +jest + .spyOn(ApiInternal, "getHiddenAssets") + .mockImplementation(() => Promise.resolve({ hiddenAssets: {}, error: "" })); + jest .spyOn(ApiInternal, "getAccountIndexerBalances") .mockImplementation(() => Promise.resolve(mockBalances)); @@ -187,6 +191,7 @@ describe("Account view", () => { settings: { networkDetails: TESTNET_NETWORK_DETAILS, networksList: DEFAULT_NETWORKS, + hiddenAssets: {}, }, }} > diff --git a/extension/src/popup/views/__tests__/ManageAssets.test.tsx b/extension/src/popup/views/__tests__/ManageAssets.test.tsx index 2fa433a19c..f260a43d97 100644 --- a/extension/src/popup/views/__tests__/ManageAssets.test.tsx +++ b/extension/src/popup/views/__tests__/ManageAssets.test.tsx @@ -75,6 +75,10 @@ const manageAssetsMockBalances = { subentryCount: 1, }; +jest + .spyOn(ApiInternal, "getHiddenAssets") + .mockImplementation(() => Promise.resolve({ hiddenAssets: {}, error: "" })); + jest .spyOn(ApiInternal, "getAccountIndexerBalances") .mockImplementation(() => Promise.resolve(manageAssetsMockBalances)); @@ -308,6 +312,7 @@ const initView = async ( networksList: DEFAULT_NETWORKS, isSorobanPublicEnabled: true, isRpcHealthy: true, + hiddenAssets: {}, }, transactionSubmission: { ...transactionSubmissionInitialState, @@ -347,9 +352,12 @@ describe("Manage assets", () => { await waitFor(() => { expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); }); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addedTrustlines = screen.queryAllByTestId("ManageAssetRow"); @@ -375,8 +383,11 @@ describe("Manage assets", () => { await initView(); expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addButton = screen.getByTestId("ChooseAssetAddAssetButton"); expect(addButton).toBeEnabled(); @@ -422,9 +433,12 @@ describe("Manage assets", () => { await waitFor(() => { expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); }); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addedTrustlines = screen.queryAllByTestId("ManageAssetRow"); const ellipsisButton = screen.getByTestId( @@ -463,9 +477,12 @@ describe("Manage assets", () => { await waitFor(() => { expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); }); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addedTrustlines = screen.queryAllByTestId("ManageAssetRow"); const ellipsisButton = screen.getByTestId( @@ -504,84 +521,42 @@ describe("Manage assets", () => { } as any), ); - await initView(true, false, { - balances: { - "USDC:GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56": { - token: { - code: "USDC", - issuer: { - key: "GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56", + jest + .spyOn(ApiInternal, "getAccountIndexerBalances") + .mockImplementation(() => + Promise.resolve({ + balances: { + "USDC:GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56": { + token: { + code: "USDC", + issuer: { + key: "GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56", + }, + }, + total: new BigNumber("111"), + available: new BigNumber("111"), + buyingLiabilities: "1", }, - }, - total: new BigNumber("111"), - available: new BigNumber("111"), - buyingLiabilities: "1", - }, - native: { - token: { type: "native", code: "XLM" }, - total: new BigNumber("222"), - available: new BigNumber("222"), - }, - "SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B": { - token: { - code: "SRT", - issuer: { - key: "GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", + native: { + token: { type: "native", code: "XLM" }, + total: new BigNumber("222"), + available: new BigNumber("222"), }, - }, - total: new BigNumber("0"), - available: new BigNumber("0"), - }, - } as any as Balances, - isFunded: true, - subentryCount: 1, - }); - - await waitFor(() => { - expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", - ); - }); - - const addedTrustlines = screen.queryAllByTestId("ManageAssetRow"); - const ellipsisButton = screen.getByTestId( - "ManageAssetRowButton__ellipsis-SRT", - ); - - await waitFor(async () => { - fireEvent.click(ellipsisButton); - const removeButton = within(addedTrustlines[1]).getByTestId( - "ManageAssetRowButton", - ); - expect(removeButton).toHaveTextContent("Remove asset"); - expect(removeButton).toBeEnabled(); - fireEvent.click(removeButton); - }); - - await waitFor(() => { - screen.getByTestId("TrustlineError__error"); - expect(screen.getByTestId("TrustlineError__error")).toHaveTextContent( - "This asset has buying liabilities", - ); - expect(screen.getByTestId("TrustlineError__body")).toHaveTextContent( - "You still have a buying liability of 1", - ); - }); - }); - - it("show error view when removing asset with buying liabilities", async () => { - jest.spyOn(global, "fetch").mockImplementation(() => - Promise.resolve({ - ok: false, - json: async () => ({ - extras: { - envelope_xdr: - "AAAAAgAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAPQkAAAYjdAAAA9gAAAAEAAAAAAAAAAAAAAABmXjffAAAAAAAAAAEAAAAAAAAABgAAAAFVU0RDAAAAACYFzNOyHT8GgwiyzcOOhwLtCctwM/RiSnrFp7JOe8xeAAAAAAAAAAAAAAAAAAAAAcskg+QAAABAA/rRMU+KKsxCX1pDBuCvYDz+eQTCsY9bzgPU4J+Xe3vOWUa8YOzWlL3N3zlxHVx9hsB7a8dpSXMSAINjjsY4Dg==", - result_codes: { operations: ["op_invalid_limit"] }, - }, + "SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B": { + token: { + code: "SRT", + issuer: { + key: "GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", + }, + }, + total: new BigNumber("0"), + available: new BigNumber("0"), + }, + } as any as Balances, + isFunded: true, + subentryCount: 1, }), - } as any), - ); + ); await initView(true, false, { balances: { @@ -618,9 +593,12 @@ describe("Manage assets", () => { await waitFor(() => { expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); }); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addedTrustlines = screen.queryAllByTestId("ManageAssetRow"); const ellipsisButton = screen.getByTestId( @@ -652,8 +630,11 @@ describe("Manage assets", () => { await initView(); expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addButton = screen.getByTestId("ChooseAssetAddAssetButton"); expect(addButton).toBeEnabled(); @@ -707,12 +688,16 @@ describe("Manage assets", () => { const lastRoute = history.entries.pop(); expect(lastRoute?.pathname).toBe("/account"); }); + it("show warning when adding an asset with Blockaid warning on Mainnet", async () => { await initView(true); expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addButton = screen.getByTestId("ChooseAssetAddAssetButton"); expect(addButton).toBeEnabled(); @@ -766,13 +751,17 @@ describe("Manage assets", () => { const lastRoute = history.entries.pop(); expect(lastRoute?.pathname).toBe("/account"); }); + it("add soroban token on asset list", async () => { // init Mainnet view await initView(false, true); expect(screen.getByTestId("AppHeaderPageTitle")).toHaveTextContent( - "Your assets", + "Manage assets", ); + await waitFor(() => { + expect(screen.getByTestId("ChooseAssetWrapper")).toBeDefined(); + }); const addTokenButton = screen.getByTestId("ChooseAssetAddAssetButton"); expect(addTokenButton).toBeEnabled(); @@ -815,6 +804,7 @@ describe("Manage assets", () => { await fireEvent.click(addAssetButton); }); }); + it("add soroban token not on asset list", async () => { // init Mainnet view await initView(false, true); @@ -864,4 +854,28 @@ describe("Manage assets", () => { await fireEvent.click(addAssetButton); }); }); + + it("hide a token", async () => { + await initView(); + + const navigateToHideBtn = screen.getByTestId("ChooseAssetHideAssetBtn"); + expect(navigateToHideBtn).toBeEnabled(); + await fireEvent.click(navigateToHideBtn); + + await waitFor(() => { + expect(screen.getByTestId("ToggleAssetContent")).toBeDefined(); + }); + + const toggleUsdcRow = screen.getByTestId("Toggle-USDC"); + const childInput = toggleUsdcRow.querySelector("input"); + expect(childInput).toBeEnabled(); + + if (!childInput) { + throw new Error("toggle not found"); + } + + expect(childInput).toBeChecked(); + fireEvent.change(childInput, { target: { checked: false } }); + expect(childInput).not.toBeChecked(); + }); }); diff --git a/extension/src/popup/views/__tests__/SignTransaction.test.tsx b/extension/src/popup/views/__tests__/SignTransaction.test.tsx index f880dbe15f..9fe7385b51 100644 --- a/extension/src/popup/views/__tests__/SignTransaction.test.tsx +++ b/extension/src/popup/views/__tests__/SignTransaction.test.tsx @@ -22,6 +22,10 @@ import { Balances } from "@shared/api/types"; jest.mock("stellar-identicon-js"); jest.setTimeout(20000); +jest + .spyOn(ApiInternal, "getHiddenAssets") + .mockImplementation(() => Promise.resolve({ hiddenAssets: {}, error: "" })); + jest .spyOn(ApiInternal, "getAccountIndexerBalances") .mockImplementation(() => Promise.resolve(mockBalances)); @@ -144,6 +148,7 @@ describe("SignTransactions", () => { ...defaultSettingsState.networkDetails, networkPassphrase: "Test SDF Future Network ; October 2022", }, + hiddenAssets: {}, }, }} > diff --git a/extension/src/popup/views/__tests__/Swap.test.tsx b/extension/src/popup/views/__tests__/Swap.test.tsx index c0856fdc47..6881102905 100644 --- a/extension/src/popup/views/__tests__/Swap.test.tsx +++ b/extension/src/popup/views/__tests__/Swap.test.tsx @@ -92,6 +92,10 @@ const swapMaliciousMockBalances = { subentryCount: 1, }; +jest + .spyOn(ApiInternal, "getHiddenAssets") + .mockImplementation(() => Promise.resolve({ hiddenAssets: {}, error: "" })); + jest .spyOn(ApiInternal, "getAccountIndexerBalances") .mockImplementation(() => Promise.resolve(swapMockBalances)); @@ -210,6 +214,7 @@ describe("Swap", () => { settings: { networkDetails: TESTNET_NETWORK_DETAILS, networksList: DEFAULT_NETWORKS, + hiddenAssets: {}, }, }} >