Skip to content

Commit

Permalink
feat(suite): nft section
Browse files Browse the repository at this point in the history
  • Loading branch information
enjojoy committed Nov 21, 2024
1 parent 4cfca17 commit 4b72ec9
Show file tree
Hide file tree
Showing 13 changed files with 462 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/suite-desktop-ui/src/support/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SettingsCoins } from 'src/views/settings/SettingsCoins/SettingsCoins';
import { SettingsDebug } from 'src/views/settings/SettingsDebug/SettingsDebug';
import { SettingsDevice } from 'src/views/settings/SettingsDevice/SettingsDevice';
import { Tokens } from 'src/views/wallet/tokens';
import { Nfts } from 'src/views/wallet/nfts';
import PasswordManager from 'src/views/password-manager';

const components: { [key: string]: ComponentType<any> } = {
Expand All @@ -47,6 +48,7 @@ const components: { [key: string]: ComponentType<any> } = {
'wallet-sign-verify': WalletSignVerify,
'wallet-anonymize': WalletAnonymize,
'wallet-tokens': Tokens,
'wallet-nfts': Nfts,
'wallet-coinmarket-buy': CoinmarketBuyForm,
'wallet-coinmarket-buy-detail': CoinmarketBuyDetail,
'wallet-coinmarket-buy-offers': CoinmarketBuyOffers,
Expand Down
1 change: 1 addition & 0 deletions packages/suite-web/src/support/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const components: Record<PageName, LazyExoticComponent<ComponentType<any>>> = {
() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/details'),
),
'wallet-tokens': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/tokens')),
'wallet-nfts': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/nfts')),
'wallet-send': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/send')),
'wallet-staking': lazy(() =>
import(/* webpackChunkName: "wallet" */ 'src/views/wallet/staking/WalletStaking').then(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const ACCOUNT_TABS = [
'wallet-index',
'wallet-details',
'wallet-tokens',
'wallet-nfts',
'wallet-nfts-hidden',
'wallet-tokens-hidden',
'wallet-staking',
];
Expand Down Expand Up @@ -54,6 +56,16 @@ export const AccountNavigation = () => {
activeRoutes: ['wallet-tokens', 'wallet-tokens-hidden'],
'data-testid': '@wallet/menu/wallet-tokens',
},
{
id: 'wallet-nfts',
callback: () => {
goToWithAnalytics('wallet-nfts', { preserveParams: true });
},
title: <Translation id="TR_NAV_NFTS" />,
isHidden: !['ethereum'].includes(networkType),
activeRoutes: ['wallet-nfts', 'wallet-nfts-hidden'],
'data-testid': '@wallet/menu/wallet-nfts',
},
{
id: 'wallet-staking',
callback: () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2721,6 +2721,10 @@ const messages = defineMessagesWithTypeCheck({
defaultMessage: 'Tokens',
id: 'TR_NAV_TOKENS',
},
TR_NAV_NFTS: {
defaultMessage: 'NFT',
id: 'TR_NAV_NFTS',
},
TR_NAV_SIGN_AND_VERIFY: {
defaultMessage: 'Sign & Verify',
description:
Expand Down Expand Up @@ -5281,6 +5285,14 @@ const messages = defineMessagesWithTypeCheck({
id: 'TR_TOKENS',
defaultMessage: 'Tokens',
},
TR_COLLECTION: {
id: 'TR_COLLECTION',
defaultMessage: 'Collection',
},
TR_NFT: {
id: 'TR_NFT',
defaultMessage: 'NFT',
},
TR_TOKENS_EMPTY: {
id: 'TR_TOKENS_EMPTY',
defaultMessage: 'No tokens... yet.',
Expand Down
60 changes: 60 additions & 0 deletions packages/suite/src/utils/wallet/nftUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isTokenDefinitionKnown, TokenDefinition } from '@suite-common/token-definitions';
import { isNftMatchesSearch, filterNftTokens } from '@suite-common/wallet-utils';
import { NetworkSymbol, getNetworkFeatures } from '@suite-common/wallet-config';
import { Token } from '@trezor/blockchain-link-types/src/blockbook-api';

type GetNfts = {
tokens: Token[];
symbol: NetworkSymbol;
nftDefinitions?: TokenDefinition;
searchQuery?: string;
};

export type NftType = 'ERC721' | 'ERC1155';

export const getNfts = ({ tokens, symbol, nftDefinitions, searchQuery }: GetNfts) => {
// filter out NFT tokens until we implement them
const nfts = filterNftTokens(tokens);

const hasNftDefinitions = getNetworkFeatures(symbol).includes('nft-definitions');

const shownVerified: Token[] = [];
const shownUnverified: Token[] = [];
const hiddenVerified: Token[] = [];
const hiddenUnverified: Token[] = [];

nfts.forEach(token => {
const isKnown = isTokenDefinitionKnown(nftDefinitions?.data, symbol, token.contract || '');
const isHidden = nftDefinitions?.hide.includes(token.contract || '');
const isShown = nftDefinitions?.show.includes(token.contract || '');

const query = searchQuery ? searchQuery.trim().toLowerCase() : '';

if (searchQuery && !isNftMatchesSearch(token, query)) return;

const pushToArray = (arrayVerified: Token[], arrayUnverified: Token[]) => {
if (isKnown) {
arrayVerified.push(token);
} else {
arrayUnverified.push(token);
}
};

if (isShown) {
pushToArray(shownVerified, shownUnverified);
} else if (hasNftDefinitions && !isKnown) {
pushToArray(hiddenVerified, hiddenUnverified);
} else if (isHidden) {
pushToArray(hiddenVerified, hiddenUnverified);
} else {
pushToArray(shownVerified, shownUnverified);
}
});

return {
shownVerified,
shownUnverified,
hiddenVerified,
hiddenUnverified,
};
};
85 changes: 85 additions & 0 deletions packages/suite/src/views/wallet/nfts/NftsNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';

import { SelectedAccountLoaded } from '@suite-common/wallet-types';
import { selectNftDefinitions } from '@suite-common/token-definitions';
import { spacings } from '@trezor/theme';
import { Row } from '@trezor/components';

import { useSelector } from 'src/hooks/suite';
import { NavigationItem } from 'src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem';
import { getNfts } from 'src/utils/wallet/nftUtils';
import { selectRouteName } from 'src/reducers/suite/routerReducer';
import { SearchAction } from 'src/components/wallet/SearchAction';

Check warning on line 12 in packages/suite/src/views/wallet/nfts/NftsNavigation.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import { filterNftTokens } from '@suite-common/wallet-utils';

Check warning on line 13 in packages/suite/src/views/wallet/nfts/NftsNavigation.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

`@suite-common/wallet-utils` import should occur before import of `src/hooks/suite`

interface NftsNavigationProps {
selectedAccount: SelectedAccountLoaded;
searchQuery: string;
setSearchQuery: Dispatch<SetStateAction<string>>;
}

export const NftsNavigation = ({
selectedAccount,
searchQuery,
setSearchQuery,
}: NftsNavigationProps) => {
const { account } = selectedAccount;

const [isExpanded, setExpanded] = useState(false);

const routeName = useSelector(selectRouteName);

const nftDefinitions = useSelector(state =>
selectNftDefinitions(state, selectedAccount.account.symbol),
);

const filteredTokens = filterNftTokens(account.tokens || []);

const nfts = getNfts({ tokens: filteredTokens, symbol: account.symbol, nftDefinitions });

useEffect(() => {
setSearchQuery('');
setExpanded(false);
}, [account.symbol, account.index, account.accountType, setSearchQuery]);

return (
<Row alignItems="center" justifyContent="space-between" margin={{ bottom: spacings.md }}>
<Row alignItems="center" gap={spacings.xxs}>
<NavigationItem
nameId="TR_NAV_NFTS"
isActive={routeName === 'wallet-nfts'}
icon="tokens"
goToRoute="wallet-nfts"
preserveParams
iconSize="mediumLarge"
itemsCount={nfts.shownVerified.length || undefined}
isRounded
typographyStyle="hint"
/>
<NavigationItem
nameId="TR_HIDDEN"
isActive={routeName === 'wallet-nfts-hidden'}
icon="hide"
goToRoute="wallet-nfts-hidden"
preserveParams
iconSize="mediumLarge"
itemsCount={nfts.hiddenVerified.length || undefined}
isRounded
typographyStyle="hint"
/>
</Row>
<Row>
<SearchAction
tooltipText="TR_TOKENS_SEARCH_TOOLTIP"
placeholder="TR_SEARCH_TOKENS"
isExpanded={isExpanded}
searchQuery={searchQuery}
setExpanded={setExpanded}
setSearch={setSearchQuery}
onSearch={setSearchQuery}
data-testid="@wallet/accounts/search-icon"
/>
</Row>
</Row>
);
};
58 changes: 58 additions & 0 deletions packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Card, Table } from '@trezor/components';
import { SelectedAccountLoaded } from '@suite-common/wallet-types';

Check warning on line 2 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import { Translation } from 'src/components/suite';

Check warning on line 3 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups

Check warning on line 3 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

`src/components/suite` import should occur after import of `@suite-common/wallet-config`
import { filterNftTokens } from '@suite-common/wallet-utils';
import { selectNftDefinitions } from '@suite-common/token-definitions';

Check warning on line 5 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import NftsRow from './NftsRow';

Check warning on line 6 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups

Check warning on line 6 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

`./NftsRow` import should occur after import of `@suite-common/wallet-config`
import { useSelector } from 'src/hooks/suite';

Check warning on line 7 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

`src/hooks/suite` import should occur after import of `@suite-common/wallet-config`
import { getNfts, NftType } from 'src/utils/wallet/nftUtils';

Check warning on line 8 in packages/suite/src/views/wallet/nfts/NftsTable/HiddenNftsTable.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import { getNetwork } from '@suite-common/wallet-config';

type NftsTableProps = {
selectedAccount: SelectedAccountLoaded;
searchQuery: string;
type?: NftType;
verified?: boolean;
};

const HiddenNftsTable = ({ selectedAccount, searchQuery, type, verified }: NftsTableProps) => {
const { account } = selectedAccount;
const network = getNetwork(account.symbol);
const nftDefinitions = useSelector(state => selectNftDefinitions(state, account.symbol));
const filteredTokens = filterNftTokens(account?.tokens || []);
const nfts = getNfts({ tokens: filteredTokens, symbol: account.symbol, nftDefinitions });

const hiddenNfts = verified
? nfts.hiddenVerified
: [...nfts.hiddenVerified, ...nfts.hiddenUnverified];

return (
<Card paddingType="none" overflow="hidden">
<Table>
<Table.Header>
<Table.Row>
<Table.Cell>
<Translation id="TR_COLLECTION" />
</Table.Cell>
<Table.Cell colSpan={2}>
<Translation id="TR_TOKENS" />
</Table.Cell>
</Table.Row>
</Table.Header>
<Table.Body>
{hiddenNfts.map(nft => (
<NftsRow
nft={nft}
key={nft.contract}
type="ERC721"
hidden
network={network}
/>
))}
</Table.Body>
</Table>
</Card>
);
};

export default HiddenNftsTable;
74 changes: 74 additions & 0 deletions packages/suite/src/views/wallet/nfts/NftsTable/NftsRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Network } from '@suite-common/wallet-config';
import { DefinitionType, tokenDefinitionsActions } from '@suite-common/token-definitions';
import { TokenManagementAction } from '@suite-common/token-definitions';
import { Token } from '@trezor/blockchain-link-types/src/blockbook-api';
import { Button, Table } from '@trezor/components';
import { Translation } from 'src/components/suite';

import { NftType } from 'src/utils/wallet/nftUtils';

import { useDispatch } from 'src/hooks/suite';

type NftsRowProps = {
nft: Token;
type: NftType;
hidden?: boolean;
network: Network;
};

const NftsRow = ({ nft, type, hidden, network }: NftsRowProps) => {
const dispatch = useDispatch();

return (
<Table.Row>
<Table.Cell colSpan={1}>{nft.name}</Table.Cell>
<Table.Cell align="left">
{(type === 'ERC721' && nft.ids?.map(id => id.toString()).join(', ')) || ''}
{(type === 'ERC1155' &&
nft.multiTokenValues?.map(value => value.id?.toString()).join(', ')) ||
''}
</Table.Cell>
<Table.Cell colSpan={1} align="right">
{hidden ? (
<Button
icon="show"
onClick={() => {
dispatch(
tokenDefinitionsActions.setTokenStatus({
networkSymbol: network.symbol,
contractAddress: nft.contract || '',
status: TokenManagementAction.SHOW,
type: DefinitionType.NFT,
}),
);
}}
variant="tertiary"
size="small"
>
<Translation id="TR_UNHIDE_TOKEN" />
</Button>
) : (
<Button
icon="eyeSlash"
onClick={() => {
dispatch(
tokenDefinitionsActions.setTokenStatus({
networkSymbol: network.symbol,
contractAddress: nft.contract || '',
status: TokenManagementAction.HIDE,
type: DefinitionType.NFT,
}),
);
}}
variant="tertiary"
size="small"
>
<Translation id="TR_HIDE_TOKEN" />
</Button>
)}
</Table.Cell>
</Table.Row>
);
};

export default NftsRow;
Loading

0 comments on commit 4b72ec9

Please sign in to comment.