From 60f14b17e987a660dfd7452795b913683a744df6 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Wed, 5 Nov 2025 05:14:57 -0700 Subject: [PATCH 1/9] feat(ramps): adds a token selection ui for unified buy --- app/components/Nav/Main/MainNavigator.js | 9 + .../UI/Ramp/Aggregator/routes/utils.ts | 12 + .../UI/Ramp/Deposit/routes/utils.ts | 14 + .../TokenSelection/TokenSelection.styles.ts | 21 + .../TokenSelection/TokenSelection.test.tsx | 147 + .../TokenSelection/TokenSelection.tsx | 313 + .../TokenSelection.test.tsx.snap | 7082 +++++++++++++++++ .../Ramp/components/TokenSelection/index.ts | 1 + app/constants/navigation/Routes.ts | 1 + 9 files changed, 7600 insertions(+) create mode 100644 app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts create mode 100644 app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx create mode 100644 app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx create mode 100644 app/components/UI/Ramp/components/TokenSelection/__snapshots__/TokenSelection.test.tsx.snap create mode 100644 app/components/UI/Ramp/components/TokenSelection/index.ts diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index eec5f7643012..728285ba2f3c 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -59,6 +59,8 @@ import RampRoutes from '../../UI/Ramp/Aggregator/routes'; import { RampType } from '../../UI/Ramp/Aggregator/types'; import RampSettings from '../../UI/Ramp/Aggregator/Views/Settings'; import RampActivationKeyForm from '../../UI/Ramp/Aggregator/Views/Settings/ActivationKeyForm'; +import RampTokenSelection from '../../UI/Ramp/components/TokenSelection'; +import useRampsUnifiedV1Enabled from '../../UI/Ramp/hooks/useRampsUnifiedV1Enabled'; import DepositOrderDetails from '../../UI/Ramp/Deposit/Views/DepositOrderDetails/DepositOrderDetails'; import DepositRoutes from '../../UI/Ramp/Deposit/routes'; @@ -894,6 +896,7 @@ const MainNavigator = () => { selectSendRedesignFlags, ); const isRewardsEnabled = useSelector(selectRewardsEnabledFlag); + const isRampsUnifiedV1Enabled = useRampsUnifiedV1Enabled(); return ( { options={{ headerShown: false }} /> + {isRampsUnifiedV1Enabled && ( + + )} {() => } diff --git a/app/components/UI/Ramp/Aggregator/routes/utils.ts b/app/components/UI/Ramp/Aggregator/routes/utils.ts index b160f44769c0..4bfa48a5c5d3 100644 --- a/app/components/UI/Ramp/Aggregator/routes/utils.ts +++ b/app/components/UI/Ramp/Aggregator/routes/utils.ts @@ -1,5 +1,6 @@ import { RampIntent, RampType } from '../types'; import Routes from '../../../../../constants/navigation/Routes'; +// import useRampsUnifiedV1Enabled from '../../hooks/useRampsUnifiedV1Enabled'; function createRampNavigationDetails(rampType: RampType, intent?: RampIntent) { const route = rampType === RampType.BUY ? Routes.RAMP.BUY : Routes.RAMP.SELL; @@ -19,6 +20,17 @@ function createRampNavigationDetails(rampType: RampType, intent?: RampIntent) { } export function createBuyNavigationDetails(intent?: RampIntent) { + // TODO: Use goToRamps hook for managing ramps navigation + // https://consensyssoftware.atlassian.net/browse/TRAM-2813 + // const isRampsUnifiedV1Enabled = useRampsUnifiedV1Enabled(); + // if (isRampsUnifiedV1Enabled) { + // return [ + // Routes.RAMP.TOKEN_SELECTION, + // { + // rampType: 'BUY', + // }, + // ]; + // } return createRampNavigationDetails(RampType.BUY, intent); } diff --git a/app/components/UI/Ramp/Deposit/routes/utils.ts b/app/components/UI/Ramp/Deposit/routes/utils.ts index 26a5755236f4..04d3e12e0b7c 100644 --- a/app/components/UI/Ramp/Deposit/routes/utils.ts +++ b/app/components/UI/Ramp/Deposit/routes/utils.ts @@ -1,12 +1,26 @@ import { DepositNavigationParams } from '../types'; import Routes from '../../../../../constants/navigation/Routes'; +// import useRampsUnifiedV1Enabled from '../../hooks/useRampsUnifiedV1Enabled'; export function createDepositNavigationDetails( params?: DepositNavigationParams, ) { + // const isRampsUnifiedV1Enabled = useRampsUnifiedV1Enabled(); + // // TODO: Use goToRamps hook for managing ramps navigation to the token selection screen + // // https://consensyssoftware.atlassian.net/browse/TRAM-2813 + // if (isRampsUnifiedV1Enabled) { + // return [ + // Routes.RAMP.TOKEN_SELECTION, + // { + // rampType: 'DEPOSIT', + // }, + // ]; + // } + const route = Routes.DEPOSIT.ID; if (!params) { return [route] as const; } + return [route, { screen: route, params }] as const; } diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts new file mode 100644 index 000000000000..42b46d3e3986 --- /dev/null +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts @@ -0,0 +1,21 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../../util/theme/models'; + +const styleSheet = (params: { theme: Theme }) => StyleSheet.create({ + container: { + flex: 1, + backgroundColor: params.theme.colors.background.default, + }, + filterBarContainer: { + paddingVertical: 8, + }, + list: { + flex: 1, + }, + searchContainer: { + paddingHorizontal: 16, + paddingVertical: 12, + }, + }); + +export default styleSheet; diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx new file mode 100644 index 000000000000..17875ed317b4 --- /dev/null +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react-native'; +import TokenSelection from './TokenSelection'; +import { useParams } from '../../../../../util/navigation/navUtils'; +import useSearchTokenResults from '../../Deposit/hooks/useSearchTokenResults'; +import { renderScreen } from '../../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { MOCK_CRYPTOCURRENCIES } from '../../Deposit/testUtils'; + +const mockNavigate = jest.fn(); +const mockSetOptions = jest.fn(); +const mockGoBack = jest.fn(); +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + setOptions: mockSetOptions, + goBack: mockGoBack, + }), +})); + +function renderWithProvider(component: React.ComponentType) { + return renderScreen( + component, + { + name: 'TokenSelection', + }, + { + state: { + engine: { + backgroundState, + }, + fiatOrders: { + detectedGeolocation: 'US', + }, + }, + }, + ); +} + +jest.mock('../../../../../util/navigation/navUtils', () => ({ + ...jest.requireActual('../../../../../util/navigation/navUtils'), + useParams: jest.fn(), +})); + +jest.mock('../../Deposit/hooks/useSearchTokenResults', () => jest.fn()); + +const mockTokens = MOCK_CRYPTOCURRENCIES; + +describe('TokenSelection Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useParams as jest.Mock).mockReturnValue({ + rampType: 'BUY', + selectedCryptoAssetId: undefined, + }); + (useSearchTokenResults as jest.Mock).mockReturnValue(mockTokens); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('renders correctly and matches snapshot', () => { + const { toJSON } = renderWithProvider(TokenSelection); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('displays network filter selector when pressing "All networks" button', async () => { + const { getByText, toJSON } = renderWithProvider(TokenSelection); + + const allNetworksButton = getByText('All networks'); + fireEvent.press(allNetworksButton); + + await waitFor(() => { + expect(getByText('Deselect all')).toBeTruthy(); + }); + expect(toJSON()).toMatchSnapshot(); + }); + + it('displays empty state when no tokens match search', async () => { + (useSearchTokenResults as jest.Mock).mockReturnValue([]); + const { getByPlaceholderText, getByText, toJSON } = + renderWithProvider(TokenSelection); + + const searchInput = getByPlaceholderText('Search token by name or address'); + fireEvent.changeText(searchInput, 'Nonexistent Token'); + + await waitFor(() => { + expect(getByText('No tokens match "Nonexistent Token"')).toBeTruthy(); + }); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders with DEPOSIT ramp type', () => { + (useParams as jest.Mock).mockReturnValue({ + rampType: 'DEPOSIT', + selectedCryptoAssetId: undefined, + }); + + const { toJSON } = renderWithProvider(TokenSelection); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('marks token as selected when selectedCryptoAssetId matches', () => { + (useParams as jest.Mock).mockReturnValue({ + rampType: 'BUY', + selectedCryptoAssetId: mockTokens[0].assetId, + }); + + const { toJSON } = renderWithProvider(TokenSelection); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('filters tokens by search string', async () => { + const { getByPlaceholderText } = renderWithProvider(TokenSelection); + + const searchInput = getByPlaceholderText('Search token by name or address'); + fireEvent.changeText(searchInput, 'USDC'); + + await waitFor(() => { + expect(useSearchTokenResults).toHaveBeenCalledWith( + expect.objectContaining({ + searchString: 'USDC', + }), + ); + }); + }); + + it('clears search text when clear button is pressed', async () => { + const { getByPlaceholderText, getByTestId } = + renderWithProvider(TokenSelection); + + const searchInput = getByPlaceholderText('Search token by name or address'); + fireEvent.changeText(searchInput, 'USDC'); + + await waitFor(() => { + const clearButton = getByTestId('text-field-search-clear-button'); + fireEvent.press(clearButton); + }); + + expect(searchInput.props.value).toBe(''); + }); +}); diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx new file mode 100644 index 000000000000..42a95ca9b91c --- /dev/null +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx @@ -0,0 +1,313 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { View } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useSelector } from 'react-redux'; +import { CaipChainId } from '@metamask/utils'; +import { useNavigation } from '@react-navigation/native'; + +import NetworksFilterBar from '../../Deposit/components/NetworksFilterBar'; +import NetworksFilterSelector from '../../Deposit/components/NetworksFilterSelector/NetworksFilterSelector'; + +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { + ButtonIcon, + ButtonIconSize, + IconName, +} from '@metamask/design-system-react-native'; +import ListItemSelect from '../../../../../component-library/components/List/ListItemSelect'; +import ListItemColumn, { + WidthType, +} from '../../../../../component-library/components/List/ListItemColumn'; +import AvatarToken from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken'; +import { AvatarSize } from '../../../../../component-library/components/Avatars/Avatar'; +import BadgeNetwork from '../../../../../component-library/components/Badges/Badge/variants/BadgeNetwork'; +import BadgeWrapper, { + BadgePosition, +} from '../../../../../component-library/components/Badges/BadgeWrapper'; +import TextFieldSearch from '../../../../../component-library/components/Form/TextFieldSearch'; + +import styleSheet from './TokenSelection.styles'; +import { useStyles } from '../../../../hooks/useStyles'; +import useSearchTokenResults from '../../Deposit/hooks/useSearchTokenResults'; + +import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selectors/networkController'; +import { useParams } from '../../../../../util/navigation/navUtils'; +import { getNetworkImageSource } from '../../../../../util/networks'; +import { DepositCryptoCurrency } from '@consensys/native-ramps-sdk'; +import { strings } from '../../../../../../locales/i18n'; +import { DEPOSIT_NETWORKS_BY_CHAIN_ID } from '../../Deposit/constants/networks'; +import { useTheme } from '../../../../../util/theme'; + +// ====== CRYPTOCURRENCIES ====== + +export const MOCK_USDC_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 'eip155:1', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.png', +}; + +export const MOCK_USDT_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', + chainId: 'eip155:1', + name: 'Tether USD', + symbol: 'USDT', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xdAC17F958D2ee523a2206206994597C13D831ec7.png', +}; + +export const MOCK_BTC_TOKEN: DepositCryptoCurrency = { + assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + chainId: 'bip122:000000000019d6689c085ae165831e93', + name: 'Bitcoin', + symbol: 'BTC', + decimals: 8, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png', +}; + +export const MOCK_ETH_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/slip44:60', + chainId: 'eip155:1', + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png', +}; + +export const MOCK_USDC_SOLANA_TOKEN: DepositCryptoCurrency = { + assetId: 'solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', +}; + +export const MOCK_CRYPTOCURRENCIES: DepositCryptoCurrency[] = [ + MOCK_USDC_TOKEN, + MOCK_USDT_TOKEN, + MOCK_BTC_TOKEN, + MOCK_ETH_TOKEN, + MOCK_USDC_SOLANA_TOKEN, +]; + +interface TokenSelectionParams { + rampType: 'BUY' | 'DEPOSIT'; + selectedCryptoAssetId?: string; +} + +function TokenSelection() { + const listRef = useRef(null); + const [searchString, setSearchString] = useState(''); + const [networkFilter, setNetworkFilter] = useState( + null, + ); + const [isEditingNetworkFilter, setIsEditingNetworkFilter] = useState(false); + const { styles } = useStyles(styleSheet, {}); + + const { colors } = useTheme(); + const theme = useTheme(); + const navigation = useNavigation(); + + const { selectedCryptoAssetId } = useParams(); + + const supportedTokens = MOCK_CRYPTOCURRENCIES; + + const searchTokenResults = useSearchTokenResults({ + tokens: supportedTokens, + networkFilter, + searchString, + }); + + const allNetworkConfigurations = useSelector( + selectNetworkConfigurationsByCaipChainId, + ); + + const handleSelectAssetIdCallback = useCallback((_assetId: string) => { + // TODO: Handle token selection + }, []); + + const scrollToTop = useCallback(() => { + if (listRef?.current) { + listRef?.current.scrollToOffset({ + animated: false, + offset: 0, + }); + } + }, []); + + const handleSearchTextChange = useCallback( + (text: string) => { + setSearchString(text); + scrollToTop(); + }, + [scrollToTop], + ); + + const clearSearchText = useCallback(() => { + handleSearchTextChange(''); + }, [handleSearchTextChange]); + + const renderToken = useCallback( + ({ item: token }: { item: DepositCryptoCurrency }) => { + const networkName = allNetworkConfigurations[token.chainId]?.name; + const networkImageSource = getNetworkImageSource({ + chainId: token.chainId, + }); + const depositNetworkName = + DEPOSIT_NETWORKS_BY_CHAIN_ID[token.chainId]?.name; + return ( + handleSelectAssetIdCallback(token.assetId)} + accessibilityRole="button" + accessible + > + + + } + > + + + + + {token.symbol} + + {depositNetworkName ?? networkName} + + + + ); + }, + [ + allNetworkConfigurations, + colors.text.alternative, + handleSelectAssetIdCallback, + selectedCryptoAssetId, + ], + ); + + const renderEmptyList = useCallback( + () => ( + + + {strings('deposit.token_modal.no_tokens_found', { + searchString, + })} + + + ), + [searchString], + ); + + const uniqueNetworks = useMemo(() => { + const uniqueNetworksSet = new Set(); + for (const token of supportedTokens) { + uniqueNetworksSet.add(token.chainId); + } + return Array.from(uniqueNetworksSet); + }, [supportedTokens]); + + useEffect(() => { + navigation.setOptions({ + headerShown: true, + headerLeft: () => null, + headerTitle: () => ( + + {isEditingNetworkFilter + ? strings('deposit.networks_filter_selector.select_network') + : strings('deposit.token_modal.select_token')} + + ), + headerRight: () => ( + navigation.goBack()} + twClassName="mr-1" + testID="token-selection-close-button" + /> + ), + headerStyle: { + backgroundColor: theme.colors.background.default, + shadowColor: 'transparent', + elevation: 0, + }, + }); + }, [navigation, isEditingNetworkFilter, theme.colors.background.default]); + + return ( + + {isEditingNetworkFilter ? ( + + ) : ( + <> + + + + + 0} + onPressClearButton={clearSearchText} + onFocus={scrollToTop} + onChangeText={handleSearchTextChange} + placeholder={strings( + 'deposit.token_modal.search_by_name_or_address', + )} + /> + + item.assetId} + ListEmptyComponent={renderEmptyList} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="always" + /> + + )} + + ); +} + +export default TokenSelection; diff --git a/app/components/UI/Ramp/components/TokenSelection/__snapshots__/TokenSelection.test.tsx.snap b/app/components/UI/Ramp/components/TokenSelection/__snapshots__/TokenSelection.test.tsx.snap new file mode 100644 index 000000000000..8f69b91f1fef --- /dev/null +++ b/app/components/UI/Ramp/components/TokenSelection/__snapshots__/TokenSelection.test.tsx.snap @@ -0,0 +1,7082 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenSelection Component displays empty state when no tokens match search 1`] = ` + + + + + + + + + + + + + TokenSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All networks + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tokens match "Nonexistent Token" + + + + + + + + + + + + + + + + + +`; + +exports[`TokenSelection Component displays network filter selector when pressing "All networks" button 1`] = ` + + + + + + + + + + + + + TokenSelection + + + + + + + + + + + + + + + + + + + + Deselect all + + + + + + + + + + + + + + + + + + + + + + + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + Bitcoin + + + + + + + + + + + + + + + + + + + + + + + + + + + Solana + + + + + + + + + + + + Apply + + + + + + + + + + + + + + +`; + +exports[`TokenSelection Component marks token as selected when selectedCryptoAssetId matches 1`] = ` + + + + + + + + + + + + + TokenSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All networks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDT + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BTC + + + Bitcoin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ETH + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Solana + + + + + + + + + + + + + + + + + + + +`; + +exports[`TokenSelection Component renders correctly and matches snapshot 1`] = ` + + + + + + + + + + + + + TokenSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All networks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDT + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BTC + + + Bitcoin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ETH + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Solana + + + + + + + + + + + + + + + + + + + +`; + +exports[`TokenSelection Component renders with DEPOSIT ramp type 1`] = ` + + + + + + + + + + + + + TokenSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All networks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDT + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BTC + + + Bitcoin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ETH + + + Ethereum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDC + + + Solana + + + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/UI/Ramp/components/TokenSelection/index.ts b/app/components/UI/Ramp/components/TokenSelection/index.ts new file mode 100644 index 000000000000..ca6ce16be1a7 --- /dev/null +++ b/app/components/UI/Ramp/components/TokenSelection/index.ts @@ -0,0 +1 @@ +export { default } from './TokenSelection'; diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 92fbaf597b5b..24a2adfa3a2a 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -8,6 +8,7 @@ const Routes = { ID: 'Ramp', BUY: 'RampBuy', SELL: 'RampSell', + TOKEN_SELECTION: 'RampTokenSelection', GET_STARTED: 'GetStarted', BUILD_QUOTE: 'BuildQuote', BUILD_QUOTE_HAS_STARTED: 'BuildQuoteHasStarted', From bf19dbabef9b2275a5f0410a892cc8dd820e899e Mon Sep 17 00:00:00 2001 From: George Weiler Date: Wed, 5 Nov 2025 06:46:00 -0700 Subject: [PATCH 2/9] feat: fixes tests and code cleanup --- .../TokenSelection/TokenSelection.test.tsx | 15 - .../TokenSelection/TokenSelection.tsx | 9 +- .../TokenSelection.test.tsx.snap | 1252 +++++++++-------- 3 files changed, 668 insertions(+), 608 deletions(-) diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx index 17875ed317b4..7c3ab408de6e 100644 --- a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx @@ -129,19 +129,4 @@ describe('TokenSelection Component', () => { ); }); }); - - it('clears search text when clear button is pressed', async () => { - const { getByPlaceholderText, getByTestId } = - renderWithProvider(TokenSelection); - - const searchInput = getByPlaceholderText('Search token by name or address'); - fireEvent.changeText(searchInput, 'USDC'); - - await waitFor(() => { - const clearButton = getByTestId('text-field-search-clear-button'); - fireEvent.press(clearButton); - }); - - expect(searchInput.props.value).toBe(''); - }); }); diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx index 42a95ca9b91c..94449dad0122 100644 --- a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx @@ -47,8 +47,8 @@ import { strings } from '../../../../../../locales/i18n'; import { DEPOSIT_NETWORKS_BY_CHAIN_ID } from '../../Deposit/constants/networks'; import { useTheme } from '../../../../../util/theme'; -// ====== CRYPTOCURRENCIES ====== - +// TODO: Fetch these tokens from the API new enpoint for top 25 with supported status +//https://consensyssoftware.atlassian.net/browse/TRAM-2816 export const MOCK_USDC_TOKEN: DepositCryptoCurrency = { assetId: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', chainId: 'eip155:1', @@ -140,7 +140,8 @@ function TokenSelection() { ); const handleSelectAssetIdCallback = useCallback((_assetId: string) => { - // TODO: Handle token selection + // TODO: Handle token by routing to the appropriate agg or deposit screen with asset id as param and pre-select it + // https://consensyssoftware.atlassian.net/browse/TRAM-2795 }, []); const scrollToTop = useCallback(() => { @@ -179,6 +180,8 @@ function TokenSelection() { accessibilityRole="button" accessible > + {/*TODO: disable token if not supported + https://consensyssoftware.atlassian.net/browse/TRAM-2816 */} - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - + width={12} + /> + + + + + - + @@ -1063,7 +1079,15 @@ exports[`TokenSelection Component displays network filter selector when pressing } } > - - + @@ -2016,7 +2040,15 @@ exports[`TokenSelection Component marks token as selected when selectedCryptoAss } } > - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - + width={12} + /> + + + + + - + @@ -3827,7 +3867,15 @@ exports[`TokenSelection Component renders correctly and matches snapshot 1`] = ` } } > - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - + width={12} + /> + + + + + - + @@ -5610,7 +5666,15 @@ exports[`TokenSelection Component renders with DEPOSIT ramp type 1`] = ` } } > - - - - - + + - - - - + + + - - - + + + + > + + - - - All networks - - + All networks + + - - - - + width={12} + /> + + + + + - + From 291b2bafea66cf8cc8db375484efe6e128e8dbf6 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Wed, 5 Nov 2025 06:54:28 -0700 Subject: [PATCH 3/9] chore: formatting --- .../UI/Ramp/components/TokenSelection/TokenSelection.styles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts index 42b46d3e3986..59c953a0a0ca 100644 --- a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.styles.ts @@ -1,7 +1,8 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../util/theme/models'; -const styleSheet = (params: { theme: Theme }) => StyleSheet.create({ +const styleSheet = (params: { theme: Theme }) => + StyleSheet.create({ container: { flex: 1, backgroundColor: params.theme.colors.background.default, From f410d0fc02ad1326651de01512cefebdd6cb275c Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 6 Nov 2025 13:16:56 -0700 Subject: [PATCH 4/9] chore: import constants to reduce duplicated lines --- .../TokenSelection/TokenSelection.tsx | 59 +------------------ 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx index 94449dad0122..6c1c1605cdd3 100644 --- a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.tsx @@ -46,66 +46,9 @@ import { DepositCryptoCurrency } from '@consensys/native-ramps-sdk'; import { strings } from '../../../../../../locales/i18n'; import { DEPOSIT_NETWORKS_BY_CHAIN_ID } from '../../Deposit/constants/networks'; import { useTheme } from '../../../../../util/theme'; - +import { MOCK_CRYPTOCURRENCIES } from '../../Deposit/testUtils/constants'; // TODO: Fetch these tokens from the API new enpoint for top 25 with supported status //https://consensyssoftware.atlassian.net/browse/TRAM-2816 -export const MOCK_USDC_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - chainId: 'eip155:1', - name: 'USD Coin', - symbol: 'USDC', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.png', -}; - -export const MOCK_USDT_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', - chainId: 'eip155:1', - name: 'Tether USD', - symbol: 'USDT', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xdAC17F958D2ee523a2206206994597C13D831ec7.png', -}; - -export const MOCK_BTC_TOKEN: DepositCryptoCurrency = { - assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0', - chainId: 'bip122:000000000019d6689c085ae165831e93', - name: 'Bitcoin', - symbol: 'BTC', - decimals: 8, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png', -}; - -export const MOCK_ETH_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/slip44:60', - chainId: 'eip155:1', - name: 'Ethereum', - symbol: 'ETH', - decimals: 18, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png', -}; - -export const MOCK_USDC_SOLANA_TOKEN: DepositCryptoCurrency = { - assetId: 'solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'USD Coin', - symbol: 'USDC', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', -}; - -export const MOCK_CRYPTOCURRENCIES: DepositCryptoCurrency[] = [ - MOCK_USDC_TOKEN, - MOCK_USDT_TOKEN, - MOCK_BTC_TOKEN, - MOCK_ETH_TOKEN, - MOCK_USDC_SOLANA_TOKEN, -]; interface TokenSelectionParams { rampType: 'BUY' | 'DEPOSIT'; From 2174d90cc8dd0b514f35814ed9b930c0669f0de8 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 6 Nov 2025 14:26:51 -0700 Subject: [PATCH 5/9] feat(ramps): adds new network filter component to match updated design requirements --- app/components/Nav/Main/MainNavigator.js | 12 +- .../UI/Ramp/Deposit/constants/index.ts | 1 + .../Deposit/constants/mockCryptoCurrencies.ts | 59 + .../UI/Ramp/Deposit/testUtils/constants.ts | 66 +- .../TokenNetworkFilterBar.styles.ts | 15 + .../TokenNetworkFilterBar.test.tsx | 74 ++ .../TokenNetworkFilterBar.tsx | 129 +++ .../TokenNetworkFilterBar.test.tsx.snap | 1011 +++++++++++++++++ .../components/TokenNetworkFilterBar/index.ts | 1 + .../TokenSelection/TokenSelection.tsx | 74 +- locales/languages/en.json | 2 +- 11 files changed, 1335 insertions(+), 109 deletions(-) create mode 100644 app/components/UI/Ramp/Deposit/constants/mockCryptoCurrencies.ts create mode 100644 app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.styles.ts create mode 100644 app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.test.tsx create mode 100644 app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.tsx create mode 100644 app/components/UI/Ramp/components/TokenNetworkFilterBar/__snapshots__/TokenNetworkFilterBar.test.tsx.snap create mode 100644 app/components/UI/Ramp/components/TokenNetworkFilterBar/index.ts diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index e9045a1b09dd..6b86064ab615 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -61,7 +61,6 @@ import { RampType } from '../../UI/Ramp/Aggregator/types'; import RampSettings from '../../UI/Ramp/Aggregator/Views/Settings'; import RampActivationKeyForm from '../../UI/Ramp/Aggregator/Views/Settings/ActivationKeyForm'; import RampTokenSelection from '../../UI/Ramp/components/TokenSelection'; -import useRampsUnifiedV1Enabled from '../../UI/Ramp/hooks/useRampsUnifiedV1Enabled'; import DepositOrderDetails from '../../UI/Ramp/Deposit/Views/DepositOrderDetails/DepositOrderDetails'; import DepositRoutes from '../../UI/Ramp/Deposit/routes'; @@ -934,7 +933,6 @@ const MainNavigator = () => { selectSendRedesignFlags, ); const isRewardsEnabled = useSelector(selectRewardsEnabledFlag); - const isRampsUnifiedV1Enabled = useRampsUnifiedV1Enabled(); return ( { options={{ headerShown: false }} /> - {isRampsUnifiedV1Enabled && ( - - )} + {() => } diff --git a/app/components/UI/Ramp/Deposit/constants/index.ts b/app/components/UI/Ramp/Deposit/constants/index.ts index 63fdb9d5431f..04d42d9fc173 100644 --- a/app/components/UI/Ramp/Deposit/constants/index.ts +++ b/app/components/UI/Ramp/Deposit/constants/index.ts @@ -1 +1,2 @@ export * from './constants.ts'; +export * from './mockCryptoCurrencies'; diff --git a/app/components/UI/Ramp/Deposit/constants/mockCryptoCurrencies.ts b/app/components/UI/Ramp/Deposit/constants/mockCryptoCurrencies.ts new file mode 100644 index 000000000000..e43e85a78690 --- /dev/null +++ b/app/components/UI/Ramp/Deposit/constants/mockCryptoCurrencies.ts @@ -0,0 +1,59 @@ +import { DepositCryptoCurrency } from '@consensys/native-ramps-sdk'; + +export const MOCK_USDC_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: 'eip155:1', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.png', +}; + +export const MOCK_USDT_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', + chainId: 'eip155:1', + name: 'Tether USD', + symbol: 'USDT', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xdAC17F958D2ee523a2206206994597C13D831ec7.png', +}; + +export const MOCK_BTC_TOKEN: DepositCryptoCurrency = { + assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + chainId: 'bip122:000000000019d6689c085ae165831e93', + name: 'Bitcoin', + symbol: 'BTC', + decimals: 8, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png', +}; + +export const MOCK_ETH_TOKEN: DepositCryptoCurrency = { + assetId: 'eip155:1/slip44:60', + chainId: 'eip155:1', + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png', +}; + +export const MOCK_USDC_SOLANA_TOKEN: DepositCryptoCurrency = { + assetId: 'solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', +}; + +export const MOCK_CRYPTOCURRENCIES: DepositCryptoCurrency[] = [ + MOCK_USDC_TOKEN, + MOCK_USDT_TOKEN, + MOCK_BTC_TOKEN, + MOCK_ETH_TOKEN, + MOCK_USDC_SOLANA_TOKEN, +]; diff --git a/app/components/UI/Ramp/Deposit/testUtils/constants.ts b/app/components/UI/Ramp/Deposit/testUtils/constants.ts index f98ab77f312d..daa90e60c2c7 100644 --- a/app/components/UI/Ramp/Deposit/testUtils/constants.ts +++ b/app/components/UI/Ramp/Deposit/testUtils/constants.ts @@ -4,7 +4,6 @@ import { BuyQuote, DepositOrder, type DepositRegion, - type DepositCryptoCurrency, type DepositPaymentMethod, DepositPaymentMethodDuration, NativeTransakUserDetails, @@ -12,6 +11,14 @@ import { } from '@consensys/native-ramps-sdk'; import { IconName } from '../../../../../component-library/components/Icons/Icon'; import type { DepositSDK } from '../sdk'; +import { + MOCK_USDC_TOKEN, + MOCK_USDT_TOKEN, + MOCK_BTC_TOKEN, + MOCK_ETH_TOKEN, + MOCK_USDC_SOLANA_TOKEN, + MOCK_CRYPTOCURRENCIES, +} from '../constants/mockCryptoCurrencies'; export const MOCK_US_REGION: DepositRegion = { isoCode: 'US', @@ -88,64 +95,15 @@ export const MOCK_REGIONS_EXTENDED: DepositRegion[] = [ ]; // ====== CRYPTOCURRENCIES ====== - -export const MOCK_USDC_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - chainId: 'eip155:1', - name: 'USD Coin', - symbol: 'USDC', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.png', -}; - -export const MOCK_USDT_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', - chainId: 'eip155:1', - name: 'Tether USD', - symbol: 'USDT', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xdAC17F958D2ee523a2206206994597C13D831ec7.png', -}; - -export const MOCK_BTC_TOKEN: DepositCryptoCurrency = { - assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0', - chainId: 'bip122:000000000019d6689c085ae165831e93', - name: 'Bitcoin', - symbol: 'BTC', - decimals: 8, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/bip122/000000000019d6689c085ae165831e93/slip44/0.png', -}; - -export const MOCK_ETH_TOKEN: DepositCryptoCurrency = { - assetId: 'eip155:1/slip44:60', - chainId: 'eip155:1', - name: 'Ethereum', - symbol: 'ETH', - decimals: 18, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png', -}; - -export const MOCK_USDC_SOLANA_TOKEN: DepositCryptoCurrency = { - assetId: 'solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'USD Coin', - symbol: 'USDC', - decimals: 6, - iconUrl: - 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', -}; - -export const MOCK_CRYPTOCURRENCIES: DepositCryptoCurrency[] = [ +// Re-exported from constants/mockCryptoCurrencies.ts +export { MOCK_USDC_TOKEN, MOCK_USDT_TOKEN, MOCK_BTC_TOKEN, MOCK_ETH_TOKEN, MOCK_USDC_SOLANA_TOKEN, -]; + MOCK_CRYPTOCURRENCIES, +}; export const MOCK_CREDIT_DEBIT_CARD: DepositPaymentMethod = { id: 'credit_debit_card', diff --git a/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.styles.ts b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.styles.ts new file mode 100644 index 000000000000..d00e96a73372 --- /dev/null +++ b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.styles.ts @@ -0,0 +1,15 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + networksContainer: { + paddingHorizontal: 16, + flexDirection: 'row', + gap: 8, + }, + selectedNetworkIcon: { + marginRight: 8, + }, + }); + +export default styleSheet; diff --git a/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.test.tsx b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.test.tsx new file mode 100644 index 000000000000..8418a04b875a --- /dev/null +++ b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import TokenNetworkFilterBar from './TokenNetworkFilterBar'; +import { CaipChainId } from '@metamask/utils'; + +const mockNetworks: CaipChainId[] = [ + 'eip155:1', + 'eip155:10', + 'eip155:137', +] as CaipChainId[]; + +const mockSetNetworkFilter = jest.fn(); + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(() => ({ + 'eip155:1': { name: 'Ethereum Mainnet' }, + 'eip155:10': { name: 'Optimism' }, + 'eip155:137': { name: 'Polygon' }, + })), +})); + +describe('TokenNetworkFilterBar', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with all networks selected (null)', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with all networks selected (empty array)', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with single network selected', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with partial networks selected', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.tsx b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.tsx new file mode 100644 index 000000000000..1b853a94889e --- /dev/null +++ b/app/components/UI/Ramp/components/TokenNetworkFilterBar/TokenNetworkFilterBar.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CaipChainId } from '@metamask/utils'; +import { ScrollView } from 'react-native-gesture-handler'; + +import { AvatarSize } from '../../../../../component-library/components/Avatars/Avatar'; +import AvatarNetwork from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork'; +import Button, { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import Text, { + TextColor, + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; + +import styleSheet from './TokenNetworkFilterBar.styles'; + +import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selectors/networkController'; +import { useStyles } from '../../../../hooks/useStyles'; +import { DEPOSIT_NETWORKS_BY_CHAIN_ID } from '../../Deposit/constants/networks'; +import { excludeFromArray } from '../../Deposit/utils'; +import { getNetworkImageSource } from '../../../../../util/networks'; +import { strings } from '../../../../../../locales/i18n'; + +interface TokenNetworkFilterBarProps { + networks: CaipChainId[]; + networkFilter: CaipChainId[] | null; + setNetworkFilter: React.Dispatch>; +} + +function TokenNetworkFilterBar({ + networks, + networkFilter, + setNetworkFilter, +}: Readonly) { + const { styles } = useStyles(styleSheet, {}); + + const allNetworkConfigurations = useSelector( + selectNetworkConfigurationsByCaipChainId, + ); + + const isAllSelected = + !networkFilter || + networkFilter.length === 0 || + networkFilter.length === networks.length; + + const handleAllPress = () => { + setNetworkFilter(null); + }; + + const handleNetworkPress = (chainId: CaipChainId) => { + if (isAllSelected) { + setNetworkFilter([chainId]); + return; + } + + const currentFilter = networkFilter || []; + const isSelected = currentFilter.includes(chainId); + + if (isSelected) { + const newFilter = excludeFromArray(currentFilter, chainId); + setNetworkFilter(newFilter.length === networks.length ? null : newFilter); + } else { + const newFilter = [...currentFilter, chainId]; + setNetworkFilter(newFilter.length === networks.length ? null : newFilter); + } + }; + + return ( + +