From 76223340220d832869f90ff0bfd274fc962f0fb4 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Fri, 24 Oct 2025 13:55:36 -0700 Subject: [PATCH 01/15] Added NFTsFullView page --- app/components/Nav/Main/MainNavigator.js | 25 +- .../UI/AddCustomCollectible/index.tsx | 229 ++++++++++-------- .../CollectibleMedia.styles.ts | 2 +- app/components/UI/NFTsTabView/index.tsx | 89 +++++++ app/components/UI/NftGrid/NftGrid.tsx | 115 +++++---- app/components/UI/NftGrid/NftGridItem.tsx | 1 - .../shared/BaseControlBar/BaseControlBar.tsx | 11 +- app/components/UI/shared/ControlBarStyles.ts | 1 + app/components/Views/AddAsset/AddAsset.tsx | 24 +- .../Views/NftsFullView/NftsFullView.test.tsx | 179 ++++++++++++++ .../Views/NftsFullView/NftsFullView.tsx | 83 +++++++ app/components/Views/NftsFullView/index.ts | 1 + app/components/Views/Wallet/index.tsx | 4 +- app/constants/navigation/Routes.ts | 2 + locales/languages/en.json | 2 + 15 files changed, 592 insertions(+), 176 deletions(-) create mode 100644 app/components/UI/NFTsTabView/index.tsx create mode 100644 app/components/Views/NftsFullView/NftsFullView.test.tsx create mode 100644 app/components/Views/NftsFullView/NftsFullView.tsx create mode 100644 app/components/Views/NftsFullView/index.ts diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index a43daffd15e3..6d805a495e8f 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 NftsFullView from '../../Views/NftsFullView'; 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 }} /> - { return null; } + // Hide tab bar for AddAsset screen + if ( + currentRoute.name === 'AddAsset' || + currentRoute.name?.includes('AddAsset') || + currentRoute.params?.assetType + ) { + return null; + } + // Hide tab bar when browser is in fullscreen mode if ( isBrowserFullscreen && @@ -943,6 +948,11 @@ const MainNavigator = () => { }} /> + {isRewardsEnabled && ( { name="NftDetailsFullImage" component={NftDetailsFullImageModeView} /> + {() => } diff --git a/app/components/UI/AddCustomCollectible/index.tsx b/app/components/UI/AddCustomCollectible/index.tsx index 1312b0db0903..28b924a7cbc0 100644 --- a/app/components/UI/AddCustomCollectible/index.tsx +++ b/app/components/UI/AddCustomCollectible/index.tsx @@ -5,14 +5,17 @@ import { Text, TextInput, View, - StyleSheet, TouchableOpacity, + StyleSheet, } from 'react-native'; import { fontStyles } from '../../../styles/common'; import Engine from '../../../core/Engine'; import { strings } from '../../../../locales/i18n'; import { isValidAddress } from 'ethereumjs-util'; -import ActionView from '../ActionView'; +import Button, { + ButtonVariants, + ButtonSize, +} from '../../../component-library/components/Buttons/Button'; import { isSmartContractAddress } from '../../../util/transactions'; import Device from '../../../util/device'; import { MetaMetricsEvents } from '../../../core/Analytics'; @@ -51,7 +54,7 @@ const createStyles = (colors: any) => flex: 1, }, rowWrapper: { - padding: 20, + marginBottom: 20, }, rowTitleText: { paddingBottom: 3, @@ -100,6 +103,25 @@ const createStyles = (colors: any) => buttonIcon: { marginLeft: 16, }, + contentContainer: { + flex: 1, + }, + scrollView: { + flex: 1, + padding: 20, + }, + buttonContainer: { + flexDirection: 'row', + paddingVertical: 16, + paddingHorizontal: 16, + gap: 16, + backgroundColor: colors.background.default, + borderTopWidth: 1, + borderTopColor: colors.border.muted, + }, + button: { + flex: 1, + }, }); interface AddCustomCollectibleProps { @@ -292,105 +314,120 @@ const AddCustomCollectible = ({ return ( - - - - setOpenNetworkSelector(true)} - onLongPress={() => setOpenNetworkSelector(true)} - > - - {selectedNetwork || strings('networks.select_network')} - + + + + + setOpenNetworkSelector(true)} + onLongPress={() => setOpenNetworkSelector(true)} + > + + {selectedNetwork || strings('networks.select_network')} + - - {selectedNetwork ? ( - + {selectedNetwork ? ( + + ) : null} + + setOpenNetworkSelector(true)} + accessibilityRole="button" + style={styles.buttonIcon} /> - ) : null} - - setOpenNetworkSelector(true)} - accessibilityRole="button" - style={styles.buttonIcon} - /> - - + + - - {strings('collectible.collectible_address')} - - - - {warningAddress} - - - - - {strings('collectible.collectible_token_id')} - - - - {warningTokenId} - + + {strings('collectible.collectible_address')} + + + + {warningAddress} + + + + + {strings('collectible.collectible_token_id')} + + + + {warningTokenId} + + - + + + {/* CTA Buttons at bottom */} + + + */} + + + ); +}; + +export default NFTsTabView; diff --git a/app/components/UI/NftGrid/NftGrid.tsx b/app/components/UI/NftGrid/NftGrid.tsx index 8b3f0604d65c..d3879f6cb339 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,51 @@ 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 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'; - -const style = StyleSheet.create({ - container: { - flex: 1, - }, -}); - -const NftGrid = () => { - const navigation = useNavigation(); +import { ActivityIndicator } from 'react-native'; +import { Box } from '@metamask/design-system-react-native'; + +interface NFTNavigationParamList { + AddAsset: { assetType: string }; + [key: string]: undefined | object; +} + +interface NftGridProps { + onAddCollectible?: () => void; + flashListProps?: Partial>; +} + +const NftRow = ({ + items, + onLongPress, +}: { + items: Nft[]; + onLongPress: (nft: Nft) => void; +}) => ( + + {items.map((item, index) => ( + + + + ))} + {/* Fill remaining slots if less than 3 items */} + {items.length < 3 && + Array.from({ length: 3 - items.length }).map((_, index) => ( + + ))} + +); + +const NftGrid = ({ onAddCollectible, flashListProps }: NftGridProps) => { + const navigation = + useNavigation>(); const { trackEvent, createEventBuilder } = useMetrics(); const [isAddNFTEnabled, setIsAddNFTEnabled] = useState(true); const [longPressedCollectible, setLongPressedCollectible] = useState(null); - const { styles } = useStyles(createControlBarStyles, undefined); const isNftFetchingProgress = useSelector(isNftFetchingProgressSelector); @@ -64,6 +84,14 @@ const NftGrid = () => { return owned; }, [collectiblesByEnabledNetworks]); + const groupedCollectibles: Nft[][] = useMemo(() => { + const groups: Nft[][] = []; + for (let i = 0; i < allFilteredCollectibles.length; i += 3) { + groups.push(allFilteredCollectibles.slice(i, i + 3)); + } + return groups; + }, [allFilteredCollectibles]); + useEffect(() => { if (longPressedCollectible) { actionSheetRef.current.show(); @@ -71,42 +99,27 @@ const NftGrid = () => { }, [longPressedCollectible]); const goToAddCollectible = useCallback(() => { - setIsAddNFTEnabled(false); - navigation.navigate('AddAsset', { assetType: 'collectible' }); - trackEvent( - createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(), - ); - setIsAddNFTEnabled(true); - }, [navigation, trackEvent, createEventBuilder]); - - const additionalButtons = ( - - ); + if (onAddCollectible) { + onAddCollectible(); + } else { + setIsAddNFTEnabled(false); + navigation.push('AddAsset', { assetType: 'collectible' }); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(), + ); + setIsAddNFTEnabled(true); + } + }, [onAddCollectible, navigation, trackEvent, createEventBuilder]); 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 +143,14 @@ const NftGrid = () => { )} } - numColumns={3} + {...flashListProps} /> - + ); }; 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/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/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/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('../../hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: jest.fn(), + createEventBuilder: jest.fn(), + }), +})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + push: jest.fn(), + goBack: jest.fn(), + }), +})); + +describe('NftsFullView', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders header with title and back button', () => { + // Arrange + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // 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 control bar with add collectible button', () => { + // Arrange + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const controlBar = getByTestId('base-control-bar'); + const addButton = getByTestId('import-token-button'); + + // Assert + expect(controlBar).toBeOnTheScreen(); + expect(addButton).toBeOnTheScreen(); + }); + + it('renders NFT grid', () => { + // Arrange + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const nftGrid = getByTestId('nft-grid'); + + // Assert + expect(nftGrid).toBeOnTheScreen(); + }); + + it('handles back button press', () => { + // Arrange + const mockGoBack = jest.fn(); + jest.doMock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + push: jest.fn(), + goBack: mockGoBack, + }), + })); + + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const backButton = getByTestId('back-button'); + backButton.props.onPress(); + + // Assert + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); + + it('handles add collectible button press', () => { + // Arrange + const mockPush = jest.fn(); + const mockTrackEvent = jest.fn(); + const mockCreateEventBuilder = jest.fn(() => ({ + build: jest.fn(), + })); + + jest.doMock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + push: mockPush, + goBack: jest.fn(), + }), + })); + + jest.doMock('../../hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), + })); + + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const addButton = getByTestId('import-token-button'); + addButton.props.onPress(); + + // Assert + expect(mockPush).toHaveBeenCalledWith('AddAsset', { + assetType: 'collectible', + }); + expect(mockCreateEventBuilder).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalled(); + }); + + it('applies custom flashListProps when provided', () => { + // Arrange + const customFlashListProps = { + contentContainerStyle: { padding: 20 }, + scrollEnabled: false, + }; + + // Act + renderScreen(NftsFullView, { + name: 'NftsFullView', + initialParams: { flashListProps: customFlashListProps }, + }); + + // Assert - The component should render without errors with custom props + // The actual prop passing is tested through the NftGrid component + expect(true).toBe(true); + }); + + it('renders with safe area view', () => { + // Arrange + const { getByTestId } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const safeAreaView = getByTestId('safe-area-view'); + + // Assert + expect(safeAreaView).toBeOnTheScreen(); + }); + + it('displays correct header title', () => { + // Arrange + const { getByText } = renderScreen(NftsFullView, { + name: 'NftsFullView', + }); + + // Act + const headerTitle = getByText('wallet.collectibles'); + + // Assert + expect(headerTitle).toBeOnTheScreen(); + }); +}); diff --git a/app/components/Views/NftsFullView/NftsFullView.tsx b/app/components/Views/NftsFullView/NftsFullView.tsx new file mode 100644 index 000000000000..f24a4715f884 --- /dev/null +++ b/app/components/Views/NftsFullView/NftsFullView.tsx @@ -0,0 +1,83 @@ +import React, { useCallback } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; +import BaseControlBar from '../../UI/shared/BaseControlBar'; +import ButtonIcon, { + ButtonIconSizes, +} from '../../../component-library/components/Buttons/ButtonIcon'; +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { Box } from '@metamask/design-system-react-native'; +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 NftGrid from '../../UI/NftGrid/NftGrid'; +import { FlashListProps } from '@shopify/flash-list'; +import { Nft } from '@metamask/assets-controllers'; + +interface NFTNavigationParamList { + AddAsset: { assetType: string }; + [key: string]: undefined | object; +} + +interface NftsFullViewProps { + flashListProps?: Partial>; +} + +const NftsFullView = ({ + flashListProps: _flashListProps, +}: NftsFullViewProps) => { + const navigation = + useNavigation>(); + const tw = useTailwind(); + const { trackEvent, createEventBuilder } = useMetrics(); + + const goToAddCollectible = useCallback(() => { + navigation.push('AddAsset', { assetType: 'collectible' }); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(), + ); + }, [navigation, trackEvent, createEventBuilder]); + + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + const additionalButtons = ( + + ); + + return ( + + + {strings('wallet.collectibles')} + + + + + + + ); +}; + +export default NftsFullView; diff --git a/app/components/Views/NftsFullView/index.ts b/app/components/Views/NftsFullView/index.ts new file mode 100644 index 000000000000..34211aa07b7d --- /dev/null +++ b/app/components/Views/NftsFullView/index.ts @@ -0,0 +1 @@ +export { default } from './NftsFullView'; diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 9937a57f58b4..221466c11659 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 NFTsTabView from '../../UI/NFTsTabView'; import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; import { selectDisplayCardButton } from '../../../core/redux/slices/card'; @@ -427,7 +427,7 @@ const WalletTokensTabView = React.memo((props: WalletTokensTabViewProps) => { } if (collectiblesEnabled && isRemoveGlobalNetworkSelectorEnabled()) { - tabs.push(); + tabs.push(); } return tabs; diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 9ceba06f4689..5d8948c166bd 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -214,6 +214,8 @@ const Routes = { HOME: 'WalletTabHome', TAB_STACK_FLOW: 'WalletTabStackFlow', WALLET_CONNECT_SESSIONS_VIEW: 'WalletConnectSessionsView', + TOKENS_FULL_VIEW: 'TokensFullView', + NFTS_FULL_VIEW: 'NftsFullView', }, VAULT_RECOVERY: { RESTORE_WALLET: 'RestoreWallet', diff --git a/locales/languages/en.json b/locales/languages/en.json index 4c1618e2bac7..29360127a912 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1819,6 +1819,8 @@ "wallet": { "title": "Wallet", "tokens": "Tokens", + "view_all_tokens": "View all tokens", + "view_all_nfts": "View all NFTs", "collectible": "Collectible", "collectibles": "NFTs", "defi": "DeFi", From 96875e55e5036656ffebd17e78e2f5527c0d244c Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Fri, 24 Oct 2025 14:09:54 -0700 Subject: [PATCH 02/15] Removed unnecessary additions --- app/components/Nav/Main/MainNavigator.js | 9 --------- .../Views/NftsFullView/NftsFullView.test.tsx | 18 ------------------ .../Views/NftsFullView/NftsFullView.tsx | 11 +---------- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 6d805a495e8f..c3a2ceb8655b 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -622,15 +622,6 @@ const HomeTabs = () => { return null; } - // Hide tab bar for AddAsset screen - if ( - currentRoute.name === 'AddAsset' || - currentRoute.name?.includes('AddAsset') || - currentRoute.params?.assetType - ) { - return null; - } - // Hide tab bar when browser is in fullscreen mode if ( isBrowserFullscreen && diff --git a/app/components/Views/NftsFullView/NftsFullView.test.tsx b/app/components/Views/NftsFullView/NftsFullView.test.tsx index dd698bec7b9a..7af246369323 100644 --- a/app/components/Views/NftsFullView/NftsFullView.test.tsx +++ b/app/components/Views/NftsFullView/NftsFullView.test.tsx @@ -133,24 +133,6 @@ describe('NftsFullView', () => { expect(mockTrackEvent).toHaveBeenCalled(); }); - it('applies custom flashListProps when provided', () => { - // Arrange - const customFlashListProps = { - contentContainerStyle: { padding: 20 }, - scrollEnabled: false, - }; - - // Act - renderScreen(NftsFullView, { - name: 'NftsFullView', - initialParams: { flashListProps: customFlashListProps }, - }); - - // Assert - The component should render without errors with custom props - // The actual prop passing is tested through the NftGrid component - expect(true).toBe(true); - }); - it('renders with safe area view', () => { // Arrange const { getByTestId } = renderScreen(NftsFullView, { diff --git a/app/components/Views/NftsFullView/NftsFullView.tsx b/app/components/Views/NftsFullView/NftsFullView.tsx index f24a4715f884..4bd98ea24f23 100644 --- a/app/components/Views/NftsFullView/NftsFullView.tsx +++ b/app/components/Views/NftsFullView/NftsFullView.tsx @@ -14,21 +14,12 @@ import { useTailwind } from '@metamask/design-system-twrnc-preset'; import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; import { strings } from '../../../../locales/i18n'; import NftGrid from '../../UI/NftGrid/NftGrid'; -import { FlashListProps } from '@shopify/flash-list'; -import { Nft } from '@metamask/assets-controllers'; - interface NFTNavigationParamList { AddAsset: { assetType: string }; [key: string]: undefined | object; } -interface NftsFullViewProps { - flashListProps?: Partial>; -} - -const NftsFullView = ({ - flashListProps: _flashListProps, -}: NftsFullViewProps) => { +const NftsFullView = () => { const navigation = useNavigation>(); const tw = useTailwind(); From 4a5167a8db504aae8483ae944bf3a901e1644caf Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Sun, 26 Oct 2025 09:34:17 -0700 Subject: [PATCH 03/15] Added maxitems to nftgrid --- app/components/UI/NFTsTabView/index.tsx | 27 ++------------- app/components/UI/NftGrid/NftGrid.tsx | 46 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/app/components/UI/NFTsTabView/index.tsx b/app/components/UI/NFTsTabView/index.tsx index 9b8e2aea29ac..8a08065057d9 100644 --- a/app/components/UI/NFTsTabView/index.tsx +++ b/app/components/UI/NFTsTabView/index.tsx @@ -8,16 +8,7 @@ import ButtonIcon, { ButtonIconSizes, } from '../../../component-library/components/Buttons/ButtonIcon'; import { IconName } from '../../../component-library/components/Icons/Icon'; -import { - Box, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - Button, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ButtonVariant, -} from '@metamask/design-system-react-native'; -import Routes from '../../../constants/navigation/Routes'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { strings } from '../../../../locales/i18n'; +import { Box } from '@metamask/design-system-react-native'; import NftGrid from '../NftGrid/NftGrid'; import { FlashListProps } from '@shopify/flash-list'; import { Nft } from '@metamask/assets-controllers'; @@ -45,11 +36,6 @@ const NFTsTabView = ({ flashListProps }: NFTsProps) => { }, [navigation, trackEvent, createEventBuilder]); const tw = useTailwind(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const navigateToNftsFullView = useCallback(() => { - navigation.navigate(Routes.WALLET.NFTS_FULL_VIEW); - }, [navigation]); - const additionalButtons = ( { ); return ( - + { hideSort style={tw`pb-3`} /> - {/* Uncomment this to review NftsFullView */} - {/* - - */} void; flashListProps?: Partial>; + maxItems?: number; } const NftRow = ({ @@ -58,7 +65,11 @@ const NftRow = ({ ); -const NftGrid = ({ onAddCollectible, flashListProps }: NftGridProps) => { +const NftGrid = ({ + onAddCollectible, + flashListProps, + maxItems, +}: NftGridProps) => { const navigation = useNavigation>(); const { trackEvent, createEventBuilder } = useMetrics(); @@ -86,11 +97,15 @@ const NftGrid = ({ onAddCollectible, flashListProps }: NftGridProps) => { const groupedCollectibles: Nft[][] = useMemo(() => { const groups: Nft[][] = []; - for (let i = 0; i < allFilteredCollectibles.length; i += 3) { - groups.push(allFilteredCollectibles.slice(i, i + 3)); + 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]); + }, [allFilteredCollectibles, maxItems]); useEffect(() => { if (longPressedCollectible) { @@ -111,6 +126,14 @@ const NftGrid = ({ onAddCollectible, flashListProps }: NftGridProps) => { } }, [onAddCollectible, navigation, trackEvent, createEventBuilder]); + 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; + return ( <> { actionSheetRef={actionSheetRef} longPressedCollectible={longPressedCollectible} /> + + {/* View all NFTs button - shown when there are more items than maxItems */} + {shouldShowViewAllButton && ( + + + + )} ); }; From 83a52a96b6de68e2d1da9b154b2835a69b5fb33a Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Sun, 26 Oct 2025 10:32:10 -0700 Subject: [PATCH 04/15] Reorgs nft folder structures --- app/components/Nav/Main/MainNavigator.js | 4 +- app/components/UI/NftGrid/NftGrid.tsx | 2 +- .../NftFullView.test.tsx} | 32 +++--- .../NftFullView.tsx} | 4 +- app/components/Views/NftFullView/index.ts | 1 + .../Views/NftTabView/NftTabView.test.tsx | 100 ++++++++++++++++++ .../NftTabView/NftTabView.tsx} | 10 +- app/components/Views/NftTabView/index.ts | 1 + app/components/Views/NftsFullView/index.ts | 1 - app/components/Views/Wallet/index.tsx | 4 +- app/constants/navigation/Routes.ts | 2 +- 11 files changed, 131 insertions(+), 30 deletions(-) rename app/components/Views/{NftsFullView/NftsFullView.test.tsx => NftFullView/NftFullView.test.tsx} (83%) rename app/components/Views/{NftsFullView/NftsFullView.tsx => NftFullView/NftFullView.tsx} (97%) create mode 100644 app/components/Views/NftFullView/index.ts create mode 100644 app/components/Views/NftTabView/NftTabView.test.tsx rename app/components/{UI/NFTsTabView/index.tsx => Views/NftTabView/NftTabView.tsx} (89%) create mode 100644 app/components/Views/NftTabView/index.ts delete mode 100644 app/components/Views/NftsFullView/index.ts diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index c3a2ceb8655b..9731c86cdb8e 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -26,7 +26,7 @@ import Asset from '../../Views/Asset'; import AssetDetails from '../../Views/AssetDetails'; import AddAsset from '../../Views/AddAsset'; import Collectible from '../../Views/Collectible'; -import NftsFullView from '../../Views/NftsFullView'; +import NftFullView from '../../Views/NftFullView'; import SendLegacy from '../../Views/confirmations/legacy/Send'; import SendTo from '../../Views/confirmations/legacy/SendFlow/SendTo'; import { RevealPrivateCredential } from '../../Views/RevealPrivateCredential'; @@ -995,7 +995,7 @@ const MainNavigator = () => { /> diff --git a/app/components/UI/NftGrid/NftGrid.tsx b/app/components/UI/NftGrid/NftGrid.tsx index 59976894612c..3ebe013c665c 100644 --- a/app/components/UI/NftGrid/NftGrid.tsx +++ b/app/components/UI/NftGrid/NftGrid.tsx @@ -176,7 +176,7 @@ const NftGrid = ({ {/* View all NFTs button - shown when there are more items than maxItems */} {shouldShowViewAllButton && ( - + diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 2072792b08d5..82bbfe5920d0 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -16,7 +16,6 @@ import { View, } from 'react-native'; import { connect, useDispatch, useSelector } from 'react-redux'; -import { Box } from '@metamask/design-system-react-native'; import { strings } from '../../../../locales/i18n'; import { TabsList,