diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index ba0b2c93ca5c..fb117d7790e7 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 NftFullView from '../../Views/NftFullView'; import TokensFullView from '../../Views/TokensFullView'; import SendLegacy from '../../Views/confirmations/legacy/Send'; import SendTo from '../../Views/confirmations/legacy/SendFlow/SendTo'; @@ -998,6 +999,11 @@ const MainNavigator = () => { name="NftDetailsFullImage" component={NftDetailsFullImageModeView} /> + {() => } diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap index a85374932888..516947632173 100644 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap @@ -110,6 +110,15 @@ exports[`MainNavigator matches rendered snapshot 1`] = ` component={[Function]} name="NftDetailsFullImage" /> + ; @@ -17,6 +22,7 @@ const mockUseSelector = useSelector as jest.MockedFunction; jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ navigate: mockNavigate, + push: mockPush, }), })); @@ -135,6 +141,7 @@ jest.mock('../../../../locales/i18n', () => ({ const strings: Record = { 'wallet.no_collectibles': 'No NFTs yet', 'wallet.add_collectibles': 'Import NFTs', + 'wallet.view_all_nfts': 'View all NFTs', }; return strings[key] || key; }, @@ -153,6 +160,33 @@ jest.mock('../CollectibleMedia', () => () => null); jest.mock('@metamask/design-system-react-native', () => ({ Text: ({ children }: { children: React.ReactNode }) => children, TextVariant: { BodyMd: 'BodyMd', BodySm: 'BodySm' }, + Box: ({ + children, + testID, + }: { + children: React.ReactNode; + testID?: string; + }) => { + const { View } = jest.requireActual('react-native'); + return {children}; + }, + Button: ({ + children, + onPress, + testID, + }: { + children: React.ReactNode; + onPress: () => void; + testID?: string; + }) => { + const { TouchableOpacity, Text } = jest.requireActual('react-native'); + return ( + + {children} + + ); + }, + ButtonVariant: { Secondary: 'Secondary' }, })); // Mock ButtonIcon and its enums @@ -224,6 +258,11 @@ jest.mock('../../../util/trace', () => ({ TraceName: { LoadCollectibles: 'LoadCollectibles' }, })); +// Mock useTailwind +jest.mock('@metamask/design-system-twrnc-preset', () => ({ + useTailwind: () => (className: string) => ({ className }), +})); + describe('NftGrid', () => { const mockNft: Nft = { address: '0x123', @@ -268,8 +307,100 @@ describe('NftGrid', () => { }); await waitFor(() => { - expect(getByTestId('collectible-Test NFT-456')).toBeDefined(); - expect(getByTestId('nft-grid-header')).toBeDefined(); + expect(getByTestId('collectible-Test NFT-456')).toBeOnTheScreen(); + expect(getByTestId('nft-grid-header')).toBeOnTheScreen(); + }); + }); + + it('renders control bar with add button', async () => { + const mockCollectibles = { '0x1': [mockNft] }; + mockUseSelector + .mockReturnValueOnce(false) // isNftFetchingProgress + .mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector + const store = mockStore(initialState); + + const { getByTestId } = render( + + + , + ); + + act(() => { + jest.advanceTimersByTime(100); + }); + + await waitFor(() => { + expect(getByTestId('base-control-bar')).toBeOnTheScreen(); + expect(getByTestId('import-token-button')).toBeOnTheScreen(); + }); + }); + + it('applies full view styling when isFullView is true', async () => { + const mockCollectibles = { '0x1': [mockNft] }; + mockUseSelector + .mockReturnValueOnce(false) // isNftFetchingProgress + .mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector + const store = mockStore(initialState); + + const { getByTestId } = render( + + + , + ); + + act(() => { + jest.advanceTimersByTime(100); + }); + + await waitFor(() => { + expect(getByTestId('base-control-bar')).toBeOnTheScreen(); + expect(getByTestId('import-token-button')).toBeOnTheScreen(); + }); + }); + + it('shows view all button when maxItems is exceeded', async () => { + const mockCollectibles = { + '0x1': [mockNft, { ...mockNft, tokenId: '789' }], + }; + mockUseSelector + .mockReturnValueOnce(false) // isNftFetchingProgress + .mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector + const store = mockStore(initialState); + + const { getByTestId } = render( + + + , + ); + + act(() => { + jest.advanceTimersByTime(100); + }); + + await waitFor(() => { + expect(getByTestId('view-all-nfts-button')).toBeOnTheScreen(); + }); + }); + + it('hides view all button when maxItems is not exceeded', async () => { + const mockCollectibles = { '0x1': [mockNft] }; + mockUseSelector + .mockReturnValueOnce(false) // isNftFetchingProgress + .mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector + const store = mockStore(initialState); + + const { queryByTestId } = render( + + + , + ); + + act(() => { + jest.advanceTimersByTime(100); + }); + + await waitFor(() => { + expect(queryByTestId('view-all-nfts-button')).toBeNull(); }); }); @@ -296,19 +427,21 @@ describe('NftGrid', () => { }); await waitFor(() => { - expect(getByTestId('collectible-Test NFT-456')).toBeDefined(); + expect(getByTestId('collectible-Test NFT-456')).toBeOnTheScreen(); expect(queryByTestId('collectible-Test NFT-789')).toBeNull(); }); }); - it('calls navigation when add collectible is triggered from empty state', async () => { - let callCount = 0; - mockUseSelector.mockImplementation(() => { - callCount++; - if (callCount % 2 === 1) { - return false; // isNftFetchingProgress + it('navigates to AddAsset when add collectible button is pressed', async () => { + const mockCollectibles = { '0x1': [mockNft] }; + mockUseSelector.mockImplementation((selector) => { + if (selector === isNftFetchingProgressSelector) { + return false; + } + if (selector === multichainCollectiblesByEnabledNetworksSelector) { + return mockCollectibles; } - return {}; // multichainCollectiblesByEnabledNetworksSelector + return {}; }); const store = mockStore(initialState); @@ -322,15 +455,10 @@ describe('NftGrid', () => { jest.advanceTimersByTime(100); }); - await waitFor(() => { - const emptyState = getByTestId('import-collectible-button'); - expect(emptyState).toBeDefined(); - }); - - const emptyState = getByTestId('import-collectible-button'); - fireEvent.press(emptyState); + const addButton = getByTestId('import-token-button'); + fireEvent.press(addButton); - expect(mockNavigate).toHaveBeenCalledWith('AddAsset', { + expect(mockPush).toHaveBeenCalledWith('AddAsset', { assetType: 'collectible', }); expect(mockTrackEvent).toHaveBeenCalled(); @@ -382,7 +510,7 @@ describe('NftGrid', () => { }); await waitFor(() => { - expect(getByTestId('collectible-null-456')).toBeDefined(); + expect(getByTestId('collectible-null-456')).toBeOnTheScreen(); }); }); @@ -404,7 +532,7 @@ describe('NftGrid', () => { }); await waitFor(() => { - expect(getByTestId('collectible-contracts-spinner')).toBeDefined(); + expect(getByTestId('collectible-contracts-spinner')).toBeOnTheScreen(); }); }); @@ -447,7 +575,7 @@ describe('NftGrid', () => { }); await waitFor(() => { - expect(getByTestId('collectibles-empty-state')).toBeDefined(); + expect(getByTestId('collectibles-empty-state')).toBeOnTheScreen(); }); }); @@ -472,47 +600,18 @@ describe('NftGrid', () => { }); }); - it('disables add NFT button when isAddNFTEnabled is false', async () => { - // Given a user with no collectibles - mockUseSelector - .mockReturnValueOnce(false) // isNftFetchingProgress - .mockReturnValueOnce({}); // multichainCollectiblesByEnabledNetworksSelector - const store = mockStore(initialState); - - // When the component renders - const { getByTestId } = render( - - - , - ); - - act(() => { - jest.advanceTimersByTime(100); - }); - - // When the add button is pressed - await waitFor(() => { - const addButton = getByTestId('import-token-button'); - fireEvent.press(addButton); - }); - - // Then it should be temporarily disabled during navigation - const addButton = getByTestId('import-token-button'); - expect(addButton.props.disabled).toBe(false); - }); - - it('calls navigation when add collectible button in control bar is pressed', async () => { - // Given a user with collectibles - const mockCollectibles = { '0x1': [mockNft] }; + it('navigates to full view when view all button is pressed', async () => { + const mockCollectibles = { + '0x1': [mockNft, { ...mockNft, tokenId: '789' }], + }; mockUseSelector .mockReturnValueOnce(false) // isNftFetchingProgress .mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector const store = mockStore(initialState); - // When the component renders const { getByTestId } = render( - + , ); @@ -520,16 +619,11 @@ describe('NftGrid', () => { jest.advanceTimersByTime(100); }); - // When the add button in control bar is pressed await waitFor(() => { - const addButton = getByTestId('import-token-button'); - fireEvent.press(addButton); + const viewAllButton = getByTestId('view-all-nfts-button'); + fireEvent.press(viewAllButton); }); - // Then it should navigate to AddAsset screen - expect(mockNavigate).toHaveBeenCalledWith('AddAsset', { - assetType: 'collectible', - }); - expect(mockTrackEvent).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith('NftFullView'); }); }); diff --git a/app/components/UI/NftGrid/NftGrid.tsx b/app/components/UI/NftGrid/NftGrid.tsx index 8b3f0604d65c..7c52a9ec5ed0 100644 --- a/app/components/UI/NftGrid/NftGrid.tsx +++ b/app/components/UI/NftGrid/NftGrid.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback, } from 'react'; -import { FlashList } from '@shopify/flash-list'; +import { FlashList, FlashListProps } from '@shopify/flash-list'; import { useSelector } from 'react-redux'; import { RefreshTestId, SpinnerTestId } from './constants'; import { endTrace, trace, TraceName } from '../../../util/trace'; @@ -20,31 +20,73 @@ import ActionSheet from '@metamask/react-native-actionsheet'; import NftGridItemActionSheet from './NftGridItemActionSheet'; import NftGridHeader from './NftGridHeader'; import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; import { CollectiblesEmptyState } from '../CollectiblesEmptyState'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; -import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { ActivityIndicator } from 'react-native'; +import { + Box, + Button, + ButtonVariant, +} from '@metamask/design-system-react-native'; +import Routes from '../../../constants/navigation/Routes'; +import { strings } from '../../../../locales/i18n'; import BaseControlBar from '../shared/BaseControlBar'; import ButtonIcon, { ButtonIconSizes, } from '../../../component-library/components/Buttons/ButtonIcon'; import { IconName } from '../../../component-library/components/Icons/Icon'; -import { useStyles } from '../../hooks/useStyles'; -import createControlBarStyles from '../shared/ControlBarStyles'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; + +interface NFTNavigationParamList { + AddAsset: { assetType: string }; + [key: string]: undefined | object; +} -const style = StyleSheet.create({ - container: { - flex: 1, - }, -}); +interface NftGridProps { + flashListProps?: Partial>; + maxItems?: number; + isFullView?: boolean; +} -const NftGrid = () => { - const navigation = useNavigation(); +const NftRow = ({ + items, + onLongPress, +}: { + items: Nft[]; + onLongPress: (nft: Nft) => void; +}) => ( + + {items.map((item, index) => { + // Create a truly unique key combining multiple identifiers + const uniqueKey = `${item.address}-${item.tokenId}-${item.chainId}-${index}`; + return ( + + + + ); + })} + {/* Fill remaining slots if less than 3 items */} + {items.length < 3 && + Array.from({ length: 3 - items.length }).map((_, index) => ( + + ))} + +); + +const NftGrid = ({ + flashListProps, + maxItems, + isFullView = false, +}: NftGridProps) => { + const navigation = + useNavigation>(); const { trackEvent, createEventBuilder } = useMetrics(); const [isAddNFTEnabled, setIsAddNFTEnabled] = useState(true); const [longPressedCollectible, setLongPressedCollectible] = useState(null); - const { styles } = useStyles(createControlBarStyles, undefined); + const tw = useTailwind(); const isNftFetchingProgress = useSelector(isNftFetchingProgressSelector); @@ -64,6 +106,18 @@ const NftGrid = () => { return owned; }, [collectiblesByEnabledNetworks]); + const groupedCollectibles: Nft[][] = useMemo(() => { + const groups: Nft[][] = []; + const itemsToProcess = maxItems + ? allFilteredCollectibles.slice(0, maxItems) + : allFilteredCollectibles; + + for (let i = 0; i < itemsToProcess.length; i += 3) { + groups.push(itemsToProcess.slice(i, i + 3)); + } + return groups; + }, [allFilteredCollectibles, maxItems]); + useEffect(() => { if (longPressedCollectible) { actionSheetRef.current.show(); @@ -72,7 +126,7 @@ const NftGrid = () => { const goToAddCollectible = useCallback(() => { setIsAddNFTEnabled(false); - navigation.navigate('AddAsset', { assetType: 'collectible' }); + navigation.push('AddAsset', { assetType: 'collectible' }); trackEvent( createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(), ); @@ -85,28 +139,51 @@ const NftGrid = () => { size={ButtonIconSizes.Lg} onPress={goToAddCollectible} iconName={IconName.Add} - disabled={!isAddNFTEnabled} - isDisabled={!isAddNFTEnabled} - style={styles.controlIconButton} /> ); + const handleViewAllNfts = useCallback(() => { + navigation.navigate(Routes.WALLET.NFTS_FULL_VIEW); + }, [navigation]); + + // Determine if we should show the "View all NFTs" button + const shouldShowViewAllButton = + maxItems && allFilteredCollectibles.length > maxItems; + + // Default flashListProps for full view + const defaultFullViewProps = useMemo( + () => ({ + contentContainerStyle: tw`px-4`, + scrollEnabled: true, + }), + [tw], + ); + + // Merge default props with passed props + const mergedFlashListProps = useMemo(() => { + if (isFullView) { + return { ...defaultFullViewProps, ...flashListProps }; + } + return flashListProps; + }, [isFullView, defaultFullViewProps, flashListProps]); + return ( - + <> } - data={allFilteredCollectibles} + data={groupedCollectibles} renderItem={({ item }) => ( - + )} - keyExtractor={(item, index) => `nft-${item.address}-${index}`} + keyExtractor={(_, index) => `nft-row-${index}`} testID={RefreshTestId} decelerationRate="fast" refreshControl={} @@ -130,14 +207,28 @@ const NftGrid = () => { )} } - numColumns={3} + {...mergedFlashListProps} /> - + + {/* View all NFTs button - shown when there are more items than maxItems */} + {shouldShowViewAllButton && ( + + + + )} + ); }; diff --git a/app/components/UI/NftGrid/NftGridItem.tsx b/app/components/UI/NftGrid/NftGridItem.tsx index 622da2137dd1..6b7fa4bdc2b3 100644 --- a/app/components/UI/NftGrid/NftGridItem.tsx +++ b/app/components/UI/NftGrid/NftGridItem.tsx @@ -9,7 +9,6 @@ import CollectibleMedia from '../CollectibleMedia'; const styles = StyleSheet.create({ container: { flex: 1, - padding: 5, }, collectible: { aspectRatio: 1, diff --git a/app/components/UI/shared/ControlBarStyles.ts b/app/components/UI/shared/ControlBarStyles.ts index 964ff8e3bfd0..d43dfe2b1347 100644 --- a/app/components/UI/shared/ControlBarStyles.ts +++ b/app/components/UI/shared/ControlBarStyles.ts @@ -28,6 +28,7 @@ const createControlBarStyles = (params: { theme: Theme }) => { controlButtonInnerWrapper: { flexDirection: 'row', gap: 12, + alignItems: 'center', }, controlButton: { backgroundColor: colors.background.default, diff --git a/app/components/Views/NftFullView/NftFullView.test.tsx b/app/components/Views/NftFullView/NftFullView.test.tsx new file mode 100644 index 000000000000..89d42929069c --- /dev/null +++ b/app/components/Views/NftFullView/NftFullView.test.tsx @@ -0,0 +1,161 @@ +import { renderScreen } from '../../../util/test/renderWithProvider'; +import NftFullView from './NftFullView'; +import { useNavigation } from '@react-navigation/native'; + +// Mock external dependencies that are not under test +jest.mock('@metamask/design-system-twrnc-preset', () => ({ + useTailwind: () => (className: string) => ({ className }), +})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(), +})); + +// Mock child components to avoid complex Redux state setup +jest.mock('../../UI/NftGrid/NftGrid', () => { + const React = jest.requireActual('react'); + const { View, Text } = jest.requireActual('react-native'); + + return function MockNftGrid({ isFullView }: { isFullView?: boolean }) { + return React.createElement( + View, + { testID: 'nft-grid' }, + React.createElement( + Text, + null, + `NftGrid ${isFullView ? 'Full View' : 'Tab View'}`, + ), + ); + }; +}); + +jest.mock( + '../../../component-library/components/BottomSheets/BottomSheetHeader', + () => { + const React = jest.requireActual('react'); + const { View, TouchableOpacity, Text } = jest.requireActual('react-native'); + + return function MockBottomSheetHeader({ + onBack, + children, + }: { + onBack: () => void; + children: string; + }) { + return React.createElement( + View, + { testID: 'bottom-sheet-header' }, + React.createElement( + TouchableOpacity, + { testID: 'back-button', onPress: onBack }, + React.createElement(Text, null, 'Back'), + ), + React.createElement(Text, { testID: 'header-title' }, children), + ); + }; + }, +); + +// Mock Box component +jest.mock('@metamask/design-system-react-native', () => ({ + Box: ({ + children, + testID, + }: { + children: React.ReactNode; + testID?: string; + }) => { + const React = jest.requireActual('react'); + const { View } = jest.requireActual('react-native'); + return React.createElement(View, { testID }, children); + }, +})); + +// Type the mocked functions +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; + +describe('NftFullView', () => { + const mockGoBack = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + mockUseNavigation.mockReturnValue({ + goBack: mockGoBack, + } as unknown as ReturnType); + }); + + it('renders header with title and back button', () => { + // Arrange + const { getByTestId } = renderScreen(NftFullView, { + name: 'NftFullView', + }); + + // Act + const header = getByTestId('bottom-sheet-header'); + const backButton = getByTestId('back-button'); + const headerTitle = getByTestId('header-title'); + + // Assert + expect(header).toBeOnTheScreen(); + expect(backButton).toBeOnTheScreen(); + expect(headerTitle).toBeOnTheScreen(); + }); + + it('renders NFT grid with isFullView prop', () => { + // Arrange + const { getByTestId } = renderScreen(NftFullView, { + name: 'NftFullView', + }); + + // Act + const nftGrid = getByTestId('nft-grid'); + + // Assert + expect(nftGrid).toBeOnTheScreen(); + }); + + it('calls goBack when back button is pressed', () => { + // Arrange + const { getByTestId } = renderScreen(NftFullView, { + name: 'NftFullView', + }); + + // Act + const backButton = getByTestId('back-button'); + backButton.props.onPress(); + + // Assert + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); + + it('displays correct header title', () => { + // Arrange + const { getByTestId } = renderScreen(NftFullView, { + name: 'NftFullView', + }); + + // Act + const headerTitle = getByTestId('header-title'); + + // Assert + expect(headerTitle).toBeOnTheScreen(); + }); + + it('renders with safe area view', () => { + // Arrange + const { getByTestId } = renderScreen(NftFullView, { + name: 'NftFullView', + }); + + // Act + const header = getByTestId('bottom-sheet-header'); + + // Assert + expect(header).toBeOnTheScreen(); + }); +}); diff --git a/app/components/Views/NftFullView/NftFullView.tsx b/app/components/Views/NftFullView/NftFullView.tsx new file mode 100644 index 000000000000..cf8bd72355fe --- /dev/null +++ b/app/components/Views/NftFullView/NftFullView.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; +import { strings } from '../../../../locales/i18n'; +import { Box } from '@metamask/design-system-react-native'; +import NftGrid from '../../UI/NftGrid/NftGrid'; + +interface NFTNavigationParamList { + AddAsset: { assetType: string }; + [key: string]: undefined | object; +} + +const NftFullView = () => { + const navigation = + useNavigation>(); + const tw = useTailwind(); + + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + return ( + + + {strings('wallet.collectibles')} + + + + + + ); +}; + +export default NftFullView; diff --git a/app/components/Views/NftFullView/index.ts b/app/components/Views/NftFullView/index.ts new file mode 100644 index 000000000000..7f5091ba3007 --- /dev/null +++ b/app/components/Views/NftFullView/index.ts @@ -0,0 +1 @@ +export { default } from './NftFullView'; diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 9937a57f58b4..82bbfe5920d0 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -184,7 +184,7 @@ import { EVM_SCOPE } from '../../UI/Earn/constants/networks'; import { useCurrentNetworkInfo } from '../../hooks/useCurrentNetworkInfo'; import { createAddressListNavigationDetails } from '../../Views/MultichainAccounts/AddressList'; import { useRewardsIntroModal } from '../../UI/Rewards/hooks/useRewardsIntroModal'; -import NftGrid from '../../UI/NftGrid'; +import NftGrid from '../../UI/NftGrid/NftGrid'; import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; import { selectDisplayCardButton } from '../../../core/redux/slices/card'; diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 784cd6716b73..9026fb8c0f05 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', + NFTS_FULL_VIEW: 'NftFullView', TOKENS_FULL_VIEW: 'TokensFullView', }, VAULT_RECOVERY: { diff --git a/locales/languages/en.json b/locales/languages/en.json index e32f751e6ca9..1bddeeddf3b4 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1844,6 +1844,7 @@ "wallet": { "title": "Wallet", "tokens": "Tokens", + "view_all_nfts": "View all NFTs", "view_all_tokens": "View all tokens", "collectible": "Collectible", "collectibles": "NFTs",