diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index a43daffd15e3..1746d61de6b4 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -26,6 +26,7 @@ import Asset from '../../Views/Asset';
import AssetDetails from '../../Views/AssetDetails';
import AddAsset from '../../Views/AddAsset';
import Collectible from '../../Views/Collectible';
+import TokensFullView from '../../Views/TokensFullView';
import SendLegacy from '../../Views/confirmations/legacy/Send';
import SendTo from '../../Views/confirmations/legacy/SendFlow/SendTo';
import { RevealPrivateCredential } from '../../Views/RevealPrivateCredential';
@@ -196,11 +197,6 @@ const WalletTabStackFlow = () => (
component={WalletModalFlow}
options={{ headerShown: false }}
/>
-
{
}}
/>
+
+
{isRewardsEnabled && (
+
+
-
- Fund your wallet to get started in web3
-
-
-
- Add funds
-
-
-
-`;
diff --git a/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx b/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx
deleted file mode 100644
index addb199517b9..000000000000
--- a/app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React from 'react';
-import { render, fireEvent, waitFor } from '@testing-library/react-native';
-import { TokenListFooter } from '.';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { strings } from '../../../../../../locales/i18n';
-import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork';
-import { MetaMetricsEvents } from '../../../../../components/hooks/useMetrics';
-import { mockNetworkState } from '../../../../../util/test/network';
-import { backgroundState } from '../../../../../util/test/initial-root-state';
-import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMultichainBalances';
-
-jest.mock('../../../Ramp/Aggregator/hooks/useRampNetwork', () => jest.fn());
-jest.mock('../../../../hooks/useMultichainBalances', () => ({
- useSelectedAccountMultichainBalances: jest.fn(),
-}));
-jest.mock('../../../../../core/Engine', () => ({
- context: {
- PreferencesController: {
- setPrivacyMode: jest.fn(),
- },
- },
- getTotalEvmFiatAccountBalance: jest.fn(() => ({
- ethFiat: 0,
- tokenFiat: 0,
- tokenFiat1dAgo: 0,
- ethFiat1dAgo: 0,
- totalNativeTokenBalance: '0',
- ticker: 'ETH',
- })),
-}));
-jest.mock('../../../../../components/hooks/useMetrics', () => ({
- MetaMetricsEvents: {
- CARD_ADD_FUNDS_DEPOSIT_CLICKED: 'CARD_ADD_FUNDS_DEPOSIT_CLICKED',
- RAMPS_BUTTON_CLICKED: 'RAMPS_BUTTON_CLICKED',
- },
- useMetrics: jest.fn(() => ({
- trackEvent: jest.fn(),
- createEventBuilder: jest.fn(() => ({
- addProperties: jest.fn().mockReturnThis(),
- build: jest.fn(),
- })),
- })),
-}));
-
-jest.mock('../../../../../util/trace', () => ({
- trace: jest.fn(),
- TraceName: {
- LoadDepositExperience: 'Load Deposit Experience',
- },
-}));
-
-jest.mock('@react-navigation/native', () => {
- const actualNav = jest.requireActual('@react-navigation/native');
- return {
- ...actualNav,
- useNavigation: () => ({
- navigate: jest.fn(),
- }),
- };
-});
-
-const mockStore = configureMockStore();
-
-const initialState = {
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: '0x1',
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- },
- },
- settings: {
- primaryCurrency: 'usd',
- },
-};
-
-const store = mockStore(initialState);
-
-describe('TokenListFooter', () => {
- beforeEach(() => {
- (useRampNetwork as jest.Mock).mockReturnValue([true, true]);
- (useSelectedAccountMultichainBalances as jest.Mock).mockReturnValue({
- selectedAccountMultichainBalance: {
- totalFiatBalance: 0,
- },
- });
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- const renderComponent = (initialStore = store) =>
- render(
-
-
- ,
- );
-
- it('renders correctly', () => {
- const { toJSON } = renderComponent();
- expect(toJSON()).toMatchSnapshot();
- });
-
- it('does not render the deposit button when total account balance is greater than zero', () => {
- (useSelectedAccountMultichainBalances as jest.Mock).mockReturnValue({
- selectedAccountMultichainBalance: {
- totalFiatBalance: 100,
- },
- });
- const { queryByText } = renderComponent();
- expect(queryByText(strings('wallet.add_funds'))).toBeNull();
- expect(
- queryByText(strings('wallet.fund_your_wallet_to_get_started')),
- ).toBeNull();
- });
-
- it('tracks the CARD_ADD_FUNDS_DEPOSIT_CLICKED and RAMPS_BUTTON_CLICKED events when the deposit button is pressed', async () => {
- const { getByText } = renderComponent();
-
- fireEvent.press(getByText(strings('wallet.add_funds')));
-
- await waitFor(() => {
- expect(MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED).toBeDefined();
- expect(MetaMetricsEvents.RAMPS_BUTTON_CLICKED).toBeDefined();
- });
- });
-});
diff --git a/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx b/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx
deleted file mode 100644
index 6c936386bf47..000000000000
--- a/app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import createStyles from '../../styles';
-import { useTheme } from '../../../../../util/theme';
-import { View } from 'react-native';
-import TextComponent, {
- TextVariant,
-} from '../../../../../component-library/components/Texts/Text';
-import { strings } from '../../../../../../locales/i18n';
-import { useSelector } from 'react-redux';
-import Button, {
- ButtonVariants,
- ButtonSize,
- ButtonWidthTypes,
-} from '../../../../../component-library/components/Buttons/Button';
-import { useNavigation } from '@react-navigation/native';
-import {
- MetaMetricsEvents,
- useMetrics,
-} from '../../../../../components/hooks/useMetrics';
-import { getDecimalChainId } from '../../../../../util/networks';
-import { selectChainId } from '../../../../../selectors/networkController';
-import { trace, TraceName } from '../../../../../util/trace';
-import { useSelectedAccountMultichainBalances } from '../../../../hooks/useMultichainBalances';
-import { createDepositNavigationDetails } from '../../../Ramp/Deposit/routes/utils';
-
-export const TokenListFooter = () => {
- const chainId = useSelector(selectChainId);
- const navigation = useNavigation();
- const { colors } = useTheme();
- const styles = createStyles(colors);
- const { trackEvent, createEventBuilder } = useMetrics();
- const { selectedAccountMultichainBalance } =
- useSelectedAccountMultichainBalances();
-
- const shouldShowFooter =
- selectedAccountMultichainBalance?.totalFiatBalance === 0;
-
- const goToDeposit = () => {
- navigation.navigate(...createDepositNavigationDetails());
-
- trackEvent(
- createEventBuilder(
- MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED,
- ).build(),
- );
-
- trackEvent(
- createEventBuilder(MetaMetricsEvents.RAMPS_BUTTON_CLICKED)
- .addProperties({
- text: 'Deposit',
- location: 'TokenListFooter',
- chain_id_destination: getDecimalChainId(chainId),
- ramp_type: 'DEPOSIT',
- })
- .build(),
- );
-
- trace({
- name: TraceName.LoadDepositExperience,
- });
- };
-
- return (
- <>
- {/* render buy button */}
- {shouldShowFooter && (
-
-
- {strings('wallet.fund_your_wallet_to_get_started')}
-
-
-
- )}
- >
- );
-};
diff --git a/app/components/UI/Tokens/TokenList/index.tsx b/app/components/UI/Tokens/TokenList/index.tsx
index b5c14752d91a..1be0a5b3b8dd 100644
--- a/app/components/UI/Tokens/TokenList/index.tsx
+++ b/app/components/UI/Tokens/TokenList/index.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useLayoutEffect, useRef } from 'react';
import { View, RefreshControl } from 'react-native';
-import { FlashList, FlashListRef } from '@shopify/flash-list';
+import { FlashList, FlashListRef, FlashListProps } from '@shopify/flash-list';
import { useSelector } from 'react-redux';
import { useTheme } from '../../../../util/theme';
import {
@@ -13,7 +13,6 @@ import TextComponent, {
} from '../../../../component-library/components/Texts/Text';
import { TokenI } from '../types';
import { strings } from '../../../../../locales/i18n';
-import { TokenListFooter } from './TokenListFooter';
import { TokenListItem, TokenListItemBip44 } from './TokenListItem';
import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors';
import { useNavigation } from '@react-navigation/native';
@@ -33,6 +32,7 @@ interface TokenListProps {
showRemoveMenu: (arg: TokenI) => void;
showPercentageChange?: boolean;
setShowScamWarningModal: () => void;
+ flashListProps?: Partial>;
}
const TokenListComponent = ({
@@ -42,6 +42,7 @@ const TokenListComponent = ({
showRemoveMenu,
showPercentageChange = true,
setShowScamWarningModal,
+ flashListProps,
}: TokenListProps) => {
const { colors } = useTheme();
const privacyMode = useSelector(selectPrivacyMode);
@@ -107,7 +108,6 @@ const TokenListComponent = ({
return `${item.address}-${item.chainId}-${staked}-${idx}`;
}}
decelerationRate="fast"
- ListFooterComponent={}
refreshControl={
}
extraData={{ isTokenNetworkFilterEqualCurrentNetwork }}
- scrollEnabled
+ {...flashListProps}
/>
) : (
diff --git a/app/components/UI/Tokens/TokenListControlBar/TokenListControlBar.tsx b/app/components/UI/Tokens/TokenListControlBar/TokenListControlBar.tsx
index 127b16f3f98a..de98be28b375 100644
--- a/app/components/UI/Tokens/TokenListControlBar/TokenListControlBar.tsx
+++ b/app/components/UI/Tokens/TokenListControlBar/TokenListControlBar.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { selectIsEvmNetworkSelected } from '../../../../selectors/multichainNetworkController';
import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors';
@@ -12,10 +13,12 @@ import createControlBarStyles from '../../shared/ControlBarStyles';
interface TokenListControlBarProps {
goToAddToken: () => void;
+ style?: ViewStyle;
}
export const TokenListControlBar = ({
goToAddToken,
+ style,
}: TokenListControlBarProps) => {
const { styles } = useStyles(createControlBarStyles, undefined);
const isEvmSelected = useSelector(selectIsEvmNetworkSelected);
@@ -38,6 +41,7 @@ export const TokenListControlBar = ({
additionalButtons={additionalButtons}
useEvmSelectionLogic={isEvmSelected}
customWrapper="outer"
+ style={style}
/>
);
};
diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx
index a41ef1164168..c36cb5b046ad 100644
--- a/app/components/UI/Tokens/index.tsx
+++ b/app/components/UI/Tokens/index.tsx
@@ -26,6 +26,9 @@ import { strings } from '../../../../locales/i18n';
import { refreshTokens, removeEvmToken, goToAddEvmToken } from './util';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { Button, ButtonVariant } from '@metamask/design-system-react-native';
+import Routes from '../../../constants/navigation/Routes';
import { selectIsEvmNetworkSelected } from '../../../selectors/multichainNetworkController';
import { TokenListControlBar } from './TokenListControlBar';
import { selectSelectedInternalAccountId } from '../../../selectors/accountsController';
@@ -39,6 +42,7 @@ import { SolScope } from '@metamask/keyring-api';
interface TokenListNavigationParamList {
AddAsset: { assetType: string };
+ TokensFullView: undefined;
[key: string]: undefined | object;
}
@@ -247,12 +251,26 @@ const Tokens = memo(() => {
setShowScamWarningModal((prev) => !prev);
}, []);
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const navigateToTokensFullView = useCallback(() => {
+ navigation.navigate(Routes.WALLET.TOKENS_FULL_VIEW);
+ }, [navigation]);
+
return (
+ {/* Uncomment these lines to review TokensFullView */}
+ {/*
+
+ */}
{!isTokensLoading &&
renderedTokenKeys.length === 0 &&
progressiveTokens.length === 0 ? (
@@ -271,6 +289,7 @@ const Tokens = memo(() => {
onRefresh={onRefresh}
showRemoveMenu={showRemoveMenu}
setShowScamWarningModal={handleScamWarningModal}
+ flashListProps={{ scrollEnabled: true }}
/>
)}
>
diff --git a/app/components/UI/Tokens/styles.ts b/app/components/UI/Tokens/styles.ts
index 3372e10ff6b4..d7d045257845 100644
--- a/app/components/UI/Tokens/styles.ts
+++ b/app/components/UI/Tokens/styles.ts
@@ -238,6 +238,10 @@ const createStyles = (colors: Colors) =>
badge: {
marginTop: 8,
},
+ viewAllTokensButton: {
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
});
export default createStyles;
diff --git a/app/components/UI/shared/BaseControlBar/BaseControlBar.tsx b/app/components/UI/shared/BaseControlBar/BaseControlBar.tsx
index f0410d921b31..ee4b9ae3d130 100644
--- a/app/components/UI/shared/BaseControlBar/BaseControlBar.tsx
+++ b/app/components/UI/shared/BaseControlBar/BaseControlBar.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, ReactNode, useMemo, useEffect } from 'react';
-import { View } from 'react-native';
+import { View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { SolScope } from '@metamask/keyring-api';
@@ -77,6 +77,10 @@ export interface BaseControlBarProps {
* Custom wrapper component for the control buttons
*/
customWrapper?: 'outer' | 'none';
+ /**
+ * Custom style to apply to the action bar wrapper
+ */
+ style?: ViewStyle;
}
const BaseControlBar: React.FC = ({
@@ -88,6 +92,7 @@ const BaseControlBar: React.FC = ({
additionalButtons,
useEvmSelectionLogic = false,
customWrapper = 'outer',
+ style,
}) => {
const { styles } = useStyles(createControlBarStyles, undefined);
const navigation = useNavigation();
@@ -256,7 +261,7 @@ const BaseControlBar: React.FC = ({
if (customWrapper === 'none') {
return (
-
+
{networkButton}
{sortButton}
{additionalButtons}
@@ -265,7 +270,7 @@ const BaseControlBar: React.FC = ({
}
return (
-
+
{networkButton}
diff --git a/app/components/Views/AddAsset/AddAsset.tsx b/app/components/Views/AddAsset/AddAsset.tsx
index 572624fcf6c1..7c137af9b604 100644
--- a/app/components/Views/AddAsset/AddAsset.tsx
+++ b/app/components/Views/AddAsset/AddAsset.tsx
@@ -15,7 +15,7 @@ import ScrollableTabView, {
} from '@tommasini/react-native-scrollable-tab-view';
import { strings } from '../../../../locales/i18n';
import AddCustomCollectible from '../../UI/AddCustomCollectible';
-import { getImportTokenNavbarOptions } from '../../UI/Navbar';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
import { isTokenDetectionSupportedForNetwork } from '@metamask/assets-controllers';
import {
selectEvmChainId,
@@ -104,22 +104,9 @@ const AddAsset = () => {
return tokensData && Object.keys(tokensData).length > 0;
}, [selectedNetwork, tokenListForAllChains]);
- const updateNavBar = useCallback(() => {
- navigation.setOptions(
- getImportTokenNavbarOptions(
- `add_asset.${assetType === TOKEN ? TOKEN_TITLE : NFT_TITLE}`,
- true,
- navigation,
- colors,
- true,
- 0,
- ),
- );
- }, [assetType, colors, navigation]);
-
- useEffect(() => {
- updateNavBar();
- }, [updateNavBar]);
+ const handleBackPress = useCallback(() => {
+ navigation.goBack();
+ }, [navigation]);
const goToSecuritySettings = () => {
navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
@@ -147,6 +134,9 @@ const AddAsset = () => {
return (
+
+ {strings(`add_asset.${assetType === TOKEN ? TOKEN_TITLE : NFT_TITLE}`)}
+
{assetType !== 'token' && (
({
+ useTailwind: () => (className: string) => ({ className }),
+}));
+
+jest.mock('../../../components/hooks/useMetrics', () => ({
+ useMetrics: () => ({
+ trackEvent: jest.fn(),
+ createEventBuilder: jest.fn(),
+ }),
+}));
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: () => ({
+ goBack: jest.fn(),
+ navigate: jest.fn(),
+ }),
+}));
+
+describe('TokensFullView', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders header with title and back button', () => {
+ const { getByTestId } = renderScreen(TokensFullView, {
+ name: 'TokensFullView',
+ });
+
+ expect(getByTestId('bottom-sheet-header')).toBeOnTheScreen();
+ expect(getByTestId('header-title')).toBeOnTheScreen();
+ expect(getByTestId('back-button')).toBeOnTheScreen();
+ });
+
+ it('renders token list control bar with add button', () => {
+ const { getByTestId } = renderScreen(TokensFullView, {
+ name: 'TokensFullView',
+ });
+
+ expect(getByTestId('token-list-control-bar')).toBeOnTheScreen();
+ expect(getByTestId('add-token-button')).toBeOnTheScreen();
+ });
+
+ it('renders token list component', () => {
+ const { getByTestId } = renderScreen(TokensFullView, {
+ name: 'TokensFullView',
+ });
+
+ expect(getByTestId('token-list')).toBeOnTheScreen();
+ });
+
+ it('displays token count in token list', () => {
+ const { getByTestId } = renderScreen(TokensFullView, {
+ name: 'TokensFullView',
+ });
+
+ expect(getByTestId('token-count')).toBeOnTheScreen();
+ });
+
+ it('shows loader when tokens are loading', () => {
+ const { getByTestId } = renderScreen(TokensFullView, {
+ name: 'TokensFullView',
+ });
+
+ expect(getByTestId('loader')).toBeOnTheScreen();
+ });
+});
diff --git a/app/components/Views/TokensFullView/TokensFullView.tsx b/app/components/Views/TokensFullView/TokensFullView.tsx
new file mode 100644
index 000000000000..0bc71315b2d0
--- /dev/null
+++ b/app/components/Views/TokensFullView/TokensFullView.tsx
@@ -0,0 +1,323 @@
+import React, {
+ useRef,
+ useState,
+ LegacyRef,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+} from 'react';
+import { InteractionManager } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import ActionSheet from '@metamask/react-native-actionsheet';
+import { useSelector } from 'react-redux';
+import { useMetrics } from '../../../components/hooks/useMetrics';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+import {
+ selectChainId,
+ selectEvmNetworkConfigurationsByChainId,
+ selectNativeNetworkCurrencies,
+} from '../../../selectors/networkController';
+import { getDecimalChainId } from '../../../util/networks';
+import { TokenList } from '../../UI/Tokens/TokenList';
+import { TokenI } from '../../UI/Tokens/types';
+import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
+import { strings } from '../../../../locales/i18n';
+import {
+ refreshTokens,
+ removeEvmToken,
+ goToAddEvmToken,
+} from '../../UI/Tokens/util';
+import { useNavigation } from '@react-navigation/native';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { selectIsEvmNetworkSelected } from '../../../selectors/multichainNetworkController';
+import { TokenListControlBar } from '../../UI/Tokens/TokenListControlBar';
+import { selectSelectedInternalAccountId } from '../../../selectors/accountsController';
+import { ScamWarningModal } from '../../UI/Tokens/TokenList/ScamWarningModal';
+import { selectSortedTokenKeys } from '../../../selectors/tokenList';
+import { selectMultichainAccountsState2Enabled } from '../../../selectors/featureFlagController/multichainAccounts';
+import { selectSortedAssetsBySelectedAccountGroup } from '../../../selectors/assets/assets-list';
+import Loader from '../../../component-library/components-temp/Loader';
+import { selectSelectedInternalAccountByScope } from '../../../selectors/multichainAccounts/accounts';
+import { SolScope } from '@metamask/keyring-api';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import { Box } from '@metamask/design-system-react-native';
+
+interface TokenListNavigationParamList {
+ AddAsset: { assetType: string };
+ [key: string]: undefined | object;
+}
+
+const TokensFullView = memo(() => {
+ const navigation =
+ useNavigation<
+ StackNavigationProp
+ >();
+ const { trackEvent, createEventBuilder } = useMetrics();
+ const tw = useTailwind();
+
+ // evm
+ const evmNetworkConfigurationsByChainId = useSelector(
+ selectEvmNetworkConfigurationsByChainId,
+ );
+ const currentChainId = useSelector(selectChainId);
+ const nativeCurrencies = useSelector(selectNativeNetworkCurrencies);
+ const isEvmSelected = useSelector(selectIsEvmNetworkSelected);
+
+ const actionSheet = useRef();
+ const [tokenToRemove, setTokenToRemove] = useState();
+ const [refreshing, setRefreshing] = useState(false);
+ const selectedAccountId = useSelector(selectSelectedInternalAccountId);
+
+ const selectedSolanaAccount =
+ useSelector(selectSelectedInternalAccountByScope)(SolScope.Mainnet) || null;
+ const isSolanaSelected = selectedSolanaAccount !== null;
+
+ const [showScamWarningModal, setShowScamWarningModal] = useState(false);
+ const [isTokensLoading, setIsTokensLoading] = useState(true);
+ const [renderedTokenKeys, setRenderedTokenKeys] = useState<
+ typeof sortedTokenKeys
+ >([]);
+ const [progressiveTokens, setProgressiveTokens] = useState<
+ typeof sortedTokenKeys
+ >([]);
+ const lastTokenDataRef = useRef();
+
+ // BIP44 MAINTENANCE: Once stable, only use selectSortedAssetsBySelectedAccountGroup
+ const isMultichainAccountsState2Enabled = useSelector(
+ selectMultichainAccountsState2Enabled,
+ );
+
+ // Memoize selector computation for better performance
+ const sortedTokenKeys = useSelector(
+ useMemo(
+ () =>
+ isMultichainAccountsState2Enabled
+ ? selectSortedAssetsBySelectedAccountGroup
+ : selectSortedTokenKeys,
+ [isMultichainAccountsState2Enabled],
+ ),
+ );
+
+ // High-performance async rendering with progressive loading
+ useEffect(() => {
+ // Debounce rapid data changes
+ if (
+ JSON.stringify(sortedTokenKeys) ===
+ JSON.stringify(lastTokenDataRef.current)
+ ) {
+ return;
+ }
+ lastTokenDataRef.current = sortedTokenKeys;
+
+ if (sortedTokenKeys?.length) {
+ setIsTokensLoading(true);
+ setProgressiveTokens([]);
+
+ // Use InteractionManager for better performance than setTimeout
+ InteractionManager.runAfterInteractions(() => {
+ const CHUNK_SIZE = 20; // Process 20 tokens at a time
+ const chunks: (typeof sortedTokenKeys)[] = [];
+
+ for (let i = 0; i < sortedTokenKeys.length; i += CHUNK_SIZE) {
+ chunks.push(sortedTokenKeys.slice(i, i + CHUNK_SIZE));
+ }
+
+ // Progressive loading for better perceived performance
+ let currentChunkIndex = 0;
+ let accumulatedTokens: typeof sortedTokenKeys = [];
+
+ const processChunk = () => {
+ if (currentChunkIndex < chunks.length) {
+ accumulatedTokens = [
+ ...accumulatedTokens,
+ ...chunks[currentChunkIndex],
+ ];
+ setProgressiveTokens([...accumulatedTokens]);
+ currentChunkIndex++;
+
+ // Process next chunk after allowing UI to update
+ requestAnimationFrame(() => {
+ if (currentChunkIndex < chunks.length) {
+ setTimeout(processChunk, 0);
+ } else {
+ // All chunks processed
+ const tokenMap = new Map();
+ accumulatedTokens.forEach((item) => {
+ const staked = item.isStaked ? 'staked' : 'unstaked';
+ const key = `${item.address}-${item.chainId}-${staked}`;
+ tokenMap.set(key, item);
+ });
+ const deduped = Array.from(tokenMap.values());
+ setRenderedTokenKeys(deduped);
+ setIsTokensLoading(false);
+ }
+ });
+ }
+ };
+
+ processChunk();
+ });
+
+ return;
+ }
+
+ // No tokens to render
+ setRenderedTokenKeys([]);
+ setProgressiveTokens([]);
+ setIsTokensLoading(false);
+ }, [sortedTokenKeys]);
+
+ const showRemoveMenu = useCallback(
+ (token: TokenI) => {
+ // remove token currently only supported on evm
+ if (isEvmSelected && actionSheet.current) {
+ setTokenToRemove(token);
+ actionSheet.current.show();
+ }
+ },
+ [isEvmSelected],
+ );
+
+ const onRefresh = useCallback(async () => {
+ setRefreshing(true);
+
+ // Use InteractionManager for better performance during refresh
+ InteractionManager.runAfterInteractions(() => {
+ refreshTokens({
+ isSolanaSelected,
+ evmNetworkConfigurationsByChainId,
+ nativeCurrencies,
+ selectedAccountId,
+ });
+ setRefreshing(false);
+ });
+ }, [
+ isSolanaSelected,
+ evmNetworkConfigurationsByChainId,
+ nativeCurrencies,
+ selectedAccountId,
+ ]);
+
+ const removeToken = useCallback(async () => {
+ // remove token currently only supported on evm
+ if (isEvmSelected && tokenToRemove) {
+ await removeEvmToken({
+ tokenToRemove,
+ currentChainId,
+ trackEvent,
+ strings,
+ getDecimalChainId,
+ createEventBuilder, // Now passed as a prop
+ });
+ }
+ }, [
+ isEvmSelected,
+ tokenToRemove,
+ currentChainId,
+ trackEvent,
+ createEventBuilder,
+ ]);
+
+ const goToAddToken = useCallback(() => {
+ // add token currently only support on evm
+ if (isEvmSelected) {
+ goToAddEvmToken({
+ navigation,
+ trackEvent,
+ createEventBuilder,
+ getDecimalChainId,
+ currentChainId,
+ });
+ }
+ }, [
+ isEvmSelected,
+ navigation,
+ trackEvent,
+ createEventBuilder,
+ currentChainId,
+ ]);
+
+ const onActionSheetPress = useCallback(
+ (index: number) => {
+ if (index === 0) {
+ removeToken();
+ }
+ },
+ [removeToken],
+ );
+
+ const handleScamWarningModal = useCallback(() => {
+ setShowScamWarningModal((prev) => !prev);
+ }, []);
+
+ const handleBackPress = useCallback(() => {
+ navigation.goBack();
+ }, [navigation]);
+
+ return (
+
+
+
+ {strings('wallet.tokens')}
+
+
+
+
+
+ {isTokensLoading && progressiveTokens.length === 0 && (
+
+ )}
+ {(progressiveTokens.length > 0 || renderedTokenKeys.length > 0) && (
+
+ )}
+
+ {showScamWarningModal && (
+
+ )}
+ }
+ title={strings('wallet.remove_token_title')}
+ options={[strings('wallet.remove'), strings('wallet.cancel')]}
+ cancelButtonIndex={1}
+ destructiveButtonIndex={0}
+ onPress={onActionSheetPress}
+ />
+ }
+ title={strings('wallet.remove_token_title')}
+ options={[strings('wallet.remove'), strings('wallet.cancel')]}
+ cancelButtonIndex={1}
+ destructiveButtonIndex={0}
+ onPress={onActionSheetPress}
+ />
+
+
+
+ );
+});
+
+TokensFullView.displayName = 'TokensFullView';
+
+export default TokensFullView;
diff --git a/app/components/Views/TokensFullView/index.ts b/app/components/Views/TokensFullView/index.ts
new file mode 100644
index 000000000000..b97b65c0ca35
--- /dev/null
+++ b/app/components/Views/TokensFullView/index.ts
@@ -0,0 +1 @@
+export { default } from './TokensFullView';
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 9ceba06f4689..0d4f52a0498e 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -214,6 +214,7 @@ const Routes = {
HOME: 'WalletTabHome',
TAB_STACK_FLOW: 'WalletTabStackFlow',
WALLET_CONNECT_SESSIONS_VIEW: 'WalletConnectSessionsView',
+ TOKENS_FULL_VIEW: 'TokensFullView',
},
VAULT_RECOVERY: {
RESTORE_WALLET: 'RestoreWallet',
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 4c1618e2bac7..9f631f57b97f 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1819,6 +1819,7 @@
"wallet": {
"title": "Wallet",
"tokens": "Tokens",
+ "view_all_tokens": "View all tokens",
"collectible": "Collectible",
"collectibles": "NFTs",
"defi": "DeFi",