Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions app/components/Nav/Main/MainNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -196,11 +197,6 @@ const WalletTabStackFlow = () => (
component={WalletModalFlow}
options={{ headerShown: false }}
/>
<Stack.Screen
name="AddAsset"
component={AddAsset}
options={AddAsset.navigationOptions}
/>
<Stack.Screen
name="Collectible"
component={Collectible}
Expand Down Expand Up @@ -943,6 +939,16 @@ const MainNavigator = () => {
}}
/>
<Stack.Screen name="Home" component={HomeTabs} />
<Stack.Screen
name={Routes.WALLET.TOKENS_FULL_VIEW}
component={TokensFullView}
options={{ headerShown: false }}
/>
<Stack.Screen
name="AddAsset"
component={AddAsset}
options={{ headerShown: false }}
/>
{isRewardsEnabled && (
<Stack.Screen
name={Routes.SETTINGS_VIEW}
Expand Down
59 changes: 59 additions & 0 deletions app/components/UI/Tokens/TokenList/TokenListView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useTheme } from '../../../../util/theme';
import { TokenI } from '../types';
import { TokenListItem } from './TokenListItem';
import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors';
import createStyles from '../styles';

interface TokenListViewProps {
tokenKeys: TokenI[];
showRemoveMenu: (token: TokenI) => void;
setShowScamWarningModal: () => void;
}

const TokenListView = ({
tokenKeys,
showRemoveMenu,
setShowScamWarningModal,
}: TokenListViewProps) => {
const { colors } = useTheme();
const styles = createStyles(colors);

const renderTokenListItem = useCallback(
(token: TokenI, index: number) => {
const assetKey = {
address: token.address,
chainId: token.chainId,
isStaked: token.isStaked,
};

return (
<TokenListItem
key={`${token.address}-${token.chainId}-${
token.isStaked ? 'staked' : 'unstaked'
}-${index}`}
assetKey={assetKey}
showRemoveMenu={showRemoveMenu}
setShowScamWarningModal={setShowScamWarningModal}
privacyMode={false}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Token List Ignores User Privacy Settings

The TokenListView component hardcodes the privacyMode prop to false for TokenListItem, preventing it from respecting the user's privacy settings.

Fix in Cursor Fix in Web

showPercentageChange
/>
);
},
[showRemoveMenu, setShowScamWarningModal],
);

return (
<View
style={styles.wrapper}
testID={WalletViewSelectorsIDs.TOKENS_CONTAINER_LIST}
>
<View style={styles.wrapper}>
{tokenKeys.map((token, index) => renderTokenListItem(token, index))}
</View>
</View>
);
};

export default TokenListView;
8 changes: 4 additions & 4 deletions app/components/UI/Tokens/TokenList/index.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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';
Expand All @@ -33,6 +32,7 @@ interface TokenListProps {
showRemoveMenu: (arg: TokenI) => void;
showPercentageChange?: boolean;
setShowScamWarningModal: () => void;
flashListProps?: Partial<FlashListProps<FlashListAssetKey>>;
}

const TokenListComponent = ({
Expand All @@ -42,6 +42,7 @@ const TokenListComponent = ({
showRemoveMenu,
showPercentageChange = true,
setShowScamWarningModal,
flashListProps,
}: TokenListProps) => {
const { colors } = useTheme();
const privacyMode = useSelector(selectPrivacyMode);
Expand Down Expand Up @@ -107,7 +108,6 @@ const TokenListComponent = ({
return `${item.address}-${item.chainId}-${staked}-${idx}`;
}}
decelerationRate="fast"
ListFooterComponent={<TokenListFooter />}
refreshControl={
<RefreshControl
colors={[colors.primary.default]}
Expand All @@ -117,7 +117,7 @@ const TokenListComponent = ({
/>
}
extraData={{ isTokenNetworkFilterEqualCurrentNetwork }}
scrollEnabled
{...flashListProps}
/>
) : (
<View style={styles.emptyView}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -38,6 +41,7 @@ export const TokenListControlBar = ({
additionalButtons={additionalButtons}
useEvmSelectionLogic={isEvmSelected}
customWrapper="outer"
style={style}
/>
);
};
Expand Down
18 changes: 18 additions & 0 deletions app/components/UI/Tokens/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { strings } from '../../../../locales/i18n';
import { refreshTokens, removeEvmToken, goToAddEvmToken } from './util';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
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';
Expand All @@ -39,6 +41,7 @@ import { SolScope } from '@metamask/keyring-api';

interface TokenListNavigationParamList {
AddAsset: { assetType: string };
TokensFullView: undefined;
[key: string]: undefined | object;
}

Expand Down Expand Up @@ -247,12 +250,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 (
<View
style={styles.wrapper}
testID={WalletViewSelectorsIDs.TOKENS_CONTAINER}
>
<TokenListControlBar goToAddToken={goToAddToken} />
{/* Uncomment these lines to review TokensFullView */}
{/* <View style={styles.viewAllTokensButton}>
<Button
variant={ButtonVariant.Secondary}
onPress={navigateToTokensFullView}
>
{strings('wallet.view_all_tokens')}
</Button>
</View> */}
{!isTokensLoading &&
renderedTokenKeys.length === 0 &&
progressiveTokens.length === 0 ? (
Expand All @@ -271,6 +288,7 @@ const Tokens = memo(() => {
onRefresh={onRefresh}
showRemoveMenu={showRemoveMenu}
setShowScamWarningModal={handleScamWarningModal}
flashListProps={{ scrollEnabled: true }}
/>
)}
</>
Expand Down
4 changes: 4 additions & 0 deletions app/components/UI/Tokens/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ const createStyles = (colors: Colors) =>
badge: {
marginTop: 8,
},
viewAllTokensButton: {
paddingHorizontal: 16,
paddingVertical: 8,
},
});

export default createStyles;
11 changes: 8 additions & 3 deletions app/components/UI/shared/BaseControlBar/BaseControlBar.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<BaseControlBarProps> = ({
Expand All @@ -88,6 +92,7 @@ const BaseControlBar: React.FC<BaseControlBarProps> = ({
additionalButtons,
useEvmSelectionLogic = false,
customWrapper = 'outer',
style,
}) => {
const { styles } = useStyles(createControlBarStyles, undefined);
const navigation = useNavigation();
Expand Down Expand Up @@ -256,7 +261,7 @@ const BaseControlBar: React.FC<BaseControlBarProps> = ({

if (customWrapper === 'none') {
return (
<View style={styles.actionBarWrapper}>
<View style={[styles.actionBarWrapper, style]}>
{networkButton}
{sortButton}
{additionalButtons}
Expand All @@ -265,7 +270,7 @@ const BaseControlBar: React.FC<BaseControlBarProps> = ({
}

return (
<View style={styles.actionBarWrapper}>
<View style={[styles.actionBarWrapper, style]}>
<View style={styles.controlButtonOuterWrapper}>
{networkButton}
<View style={styles.controlButtonInnerWrapper}>
Expand Down
24 changes: 7 additions & 17 deletions app/components/Views/AddAsset/AddAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Import assets page was moved to the main stack to work with both the Tokens tab and tokens full page, so the header needs to be refactored to go inside the page


const goToSecuritySettings = () => {
navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
Expand Down Expand Up @@ -147,6 +134,9 @@ const AddAsset = () => {

return (
<SafeAreaView style={styles.wrapper} testID={`add-${assetType}-screen`}>
<BottomSheetHeader onBack={handleBackPress}>
{strings(`add_asset.${assetType === TOKEN ? TOKEN_TITLE : NFT_TITLE}`)}
</BottomSheetHeader>
{assetType !== 'token' && (
<View style={styles.infoWrapper}>
<Banner
Expand Down
71 changes: 71 additions & 0 deletions app/components/Views/TokensFullView/TokensFullView.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { renderScreen } from '../../../util/test/renderWithProvider';
import TokensFullView from './TokensFullView';

// Mock only external dependencies that are not under test
jest.mock('@metamask/design-system-twrnc-preset', () => ({
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();
});
});
Loading
Loading