diff --git a/packages/suite/src/components/suite/FormattedNftAmount.tsx b/packages/suite/src/components/suite/FormattedNftAmount.tsx
index 90a36e95a2fd..c481f2e09911 100644
--- a/packages/suite/src/components/suite/FormattedNftAmount.tsx
+++ b/packages/suite/src/components/suite/FormattedNftAmount.tsx
@@ -63,7 +63,7 @@ export const FormattedNftAmount = ({
) : (
{token.value}x
-
+
)}
@@ -95,7 +95,7 @@ export const FormattedNftAmount = ({
{signValue ? : null}
-
+
{isWithLink ? (
`
+ transition: transform 0.2s ease-in-out;
+ transform: ${({ $isActive }) => ($isActive ? 'rotate(0)' : 'rotate(-90deg)')};
+`;
+
+const NftsRow = ({ nft, network, isShown, selectedAccount }: NftsRowProps) => {
+ const dispatch = useDispatch();
+
+ const [isCollectionOpen, setIsCollectionOpen] = useState(false);
+
+ const shouldShowCopyAddressModal = useSelector(selectIsCopyAddressModalShown);
+ const { account } = selectedAccount;
+
+ const getNftContractExplorerUrl = (network: Network, nft: EnhancedTokenInfo) => {
+ const explorerUrl = network.explorer.account;
+ const contractAddress = nft.contract;
+ const queryString = network.explorer.queryString ?? '';
+
+ return `${explorerUrl}${contractAddress}${queryString}`;
+ };
+
+ const nftItemsCount = nft.ids?.length || nft.multiTokenValues?.length || 0;
+
+ return (
+ <>
+ setIsCollectionOpen(!isCollectionOpen)}>
+
+
+
+
+
+
+ {nft.name}
+ {nftItemsCount}
+
+
+
+
+
+
+ ,
+ icon: 'hide',
+ onClick: () =>
+ dispatch(
+ tokenDefinitionsActions.setTokenStatus({
+ symbol: network.symbol,
+ contractAddress: nft.contract || '',
+ status: TokenManagementAction.HIDE,
+ type: DefinitionType.NFT,
+ }),
+ ),
+ isHidden: isShown === false,
+ },
+ {
+ label: ,
+ icon: 'newspaper',
+ onClick: () => {
+ dispatch({
+ type: SUITE.SET_TRANSACTION_HISTORY_PREFILL,
+ payload: nft.contract || '',
+ });
+ if (account) {
+ dispatch(
+ goto('wallet-index', {
+ params: {
+ symbol: account.symbol,
+ accountIndex: account.index,
+ accountType:
+ account.accountType,
+ },
+ }),
+ );
+ }
+ },
+ },
+ {
+ label: ,
+ icon: 'arrowUpRight',
+ onClick: () => {
+ window.open(
+ getNftContractExplorerUrl(network, nft),
+ '_blank',
+ );
+ },
+ },
+ ],
+ },
+ {
+ key: 'contract-address',
+ label: ,
+ options: [
+ {
+ label: (
+
+ {nft.contract}
+
+
+ ),
+ onClick: () =>
+ onCopyAddressWithModal(
+ nft.contract || '',
+ 'contract',
+ shouldShowCopyAddressModal,
+ ),
+ },
+ ],
+ },
+ ].filter(category => category) as GroupedMenuItems[]
+ }
+ />
+ {!isShown && (
+
+ )}
+
+
+
+ {nft.type === 'ERC721' && (
+ <>
+ {nft.ids?.map((id, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+ >
+ )}
+ {nft.type === 'ERC1155' && (
+ <>
+ {nft.multiTokenValues?.map((value, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+ >
+ )}
+ >
+ );
+};
+
+export default NftsRow;
diff --git a/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx
new file mode 100644
index 000000000000..ec1f68e2f9f5
--- /dev/null
+++ b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx
@@ -0,0 +1,76 @@
+import { SelectedAccountLoaded } from '@suite-common/wallet-types';
+import { Card, Column, Table } from '@trezor/components';
+import { getNetwork } from '@suite-common/wallet-config';
+import { spacings } from '@trezor/theme';
+
+import { GetTokensOutputType } from 'src/utils/wallet/tokenUtils';
+import { Translation } from 'src/components/suite/Translation';
+
+import NftsRow from './NftsRow';
+
+type NftsTableProps = {
+ selectedAccount: SelectedAccountLoaded;
+ isShown?: boolean;
+ verified?: boolean;
+ nfts: GetTokensOutputType;
+};
+
+const NftsTable = ({ selectedAccount, isShown, verified, nfts }: NftsTableProps) => {
+ const { account } = selectedAccount;
+ const network = getNetwork(account.symbol);
+
+ const getNftsToShow = () => {
+ if (isShown) {
+ return [...nfts.shownWithBalance, ...nfts.shownWithoutBalance];
+ }
+
+ return verified
+ ? [...nfts.hiddenWithBalance, ...nfts.hiddenWithoutBalance]
+ : [...nfts.unverifiedWithBalance, ...nfts.unverifiedWithoutBalance];
+ };
+
+ const nftsToShow = getNftsToShow();
+
+ return nftsToShow.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {nftsToShow.map(nft => (
+
+ ))}
+
+
+
+
+ ) : null;
+};
+
+export default NftsTable;
diff --git a/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx b/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx
new file mode 100644
index 000000000000..2becbf74ceb4
--- /dev/null
+++ b/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx
@@ -0,0 +1,94 @@
+import { Banner, H3, Column } from '@trezor/components';
+import { SelectedAccountLoaded } from '@suite-common/wallet-types';
+import { selectNftDefinitions } from '@suite-common/token-definitions';
+
+import { Translation } from 'src/components/suite';
+import { useSelector } from 'src/hooks/suite';
+import { getTokens } from 'src/utils/wallet/tokenUtils';
+
+import { NoTokens } from '../tokens/common/NoTokens';
+import NftsTable from './NftsTable/NftsTable';
+
+type EvmNftsTablesProps = {
+ selectedAccount: SelectedAccountLoaded;
+ searchQuery: string;
+ isShown: boolean;
+};
+
+export const NftsTablesSection = ({
+ selectedAccount,
+ searchQuery,
+ isShown = true,
+}: EvmNftsTablesProps) => {
+ const nftDefinitions = useSelector(state =>
+ selectNftDefinitions(state, selectedAccount.account.symbol),
+ );
+ const nfts = getTokens({
+ tokens: selectedAccount.account.tokens || [],
+ symbol: selectedAccount.account.symbol,
+ tokenDefinitions: nftDefinitions,
+ isNft: true,
+ searchQuery,
+ });
+
+ const areNoShownNfts = !nfts?.shownWithBalance.length && !nfts?.shownWithoutBalance.length;
+
+ const areNoHiddenNfts = !nfts?.hiddenWithBalance.length && !nfts?.hiddenWithoutBalance.length;
+
+ const areNoUnverifiedNfts =
+ !nfts?.unverifiedWithBalance.length && !nfts?.unverifiedWithoutBalance.length;
+
+ const hiddenEvmNfts = (
+
+
+
+
+
+
+
+
+
+
+ );
+
+ if (isShown) {
+ if (areNoShownNfts) {
+ return (
+
+ }
+ />
+ );
+ }
+
+ return (
+
+ );
+ } else {
+ if (areNoHiddenNfts && areNoUnverifiedNfts) {
+ return } />;
+ }
+
+ return hiddenEvmNfts;
+ }
+};
diff --git a/packages/suite/src/views/wallet/nfts/index.tsx b/packages/suite/src/views/wallet/nfts/index.tsx
new file mode 100644
index 000000000000..9ee8315edda9
--- /dev/null
+++ b/packages/suite/src/views/wallet/nfts/index.tsx
@@ -0,0 +1,59 @@
+import { useState, useEffect } from 'react';
+import { Route, Switch } from 'react-router-dom';
+
+import { WalletLayout } from 'src/components/wallet';
+import { useDispatch, useSelector } from 'src/hooks/suite';
+import { goto } from 'src/actions/suite/routerActions';
+
+import { TokensNavigation } from '../tokens/TokensNavigation';
+import { NftsTablesSection } from './NftsTablesSection';
+
+export const Nfts = () => {
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const { selectedAccount } = useSelector(state => state.wallet);
+
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (
+ selectedAccount.status === 'loaded' &&
+ !selectedAccount.network?.features.includes('nfts')
+ ) {
+ dispatch(goto('wallet-index', { preserveParams: true }));
+ }
+ }, [selectedAccount, dispatch]);
+
+ if (selectedAccount.status !== 'loaded') {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Nfts;