diff --git a/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.tsx b/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.tsx index 0f93f50484f..41c95a6499a 100644 --- a/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.tsx +++ b/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.tsx @@ -15,6 +15,7 @@ import { useIntl } from 'react-intl'; import { AssetItemNotFound } from './AssetItemNotFound'; import { getNetworkByCoingeckoId, Network, NetworkSymbol } from '@suite-common/wallet-config'; import { getContractAddressForNetwork } from '@suite-common/wallet-utils'; +import { SendTokenTabs } from './SendTokenTabs'; export interface SelectAssetOptionCurrencyProps { type: 'currency'; @@ -49,9 +50,17 @@ export type SelectAssetSearchCategoryType = { coingeckoNativeId?: string; } | null; +export type TokenCategory = { + type: 'visibleWithBalance' | 'hiddenWithBalance'; + label: string; +}; + export interface SelectAssetModalProps { options: SelectAssetOptionProps[]; networkCategories?: SelectAssetNetworkProps[]; //optional if used for choosing token for swapping in account + searchPlaceholderText?: string; + setPickedSendTokenCategory: (category: TokenCategory['type']) => void; + pickedSendTokenCategory: TokenCategory['type']; onSelectAssetModal: (selectedAsset: SelectAssetOptionCurrencyProps) => void; onFavoriteClick?: (isFavorite: boolean) => void; onClose: () => void; @@ -112,6 +121,9 @@ const getNetworkCount = (options: SelectAssetOptionProps[]) => { export const SelectAssetModal = ({ options, networkCategories, + searchPlaceholderText, + setPickedSendTokenCategory, + pickedSendTokenCategory, onSelectAssetModal, onFavoriteClick, onClose, @@ -119,12 +131,36 @@ export const SelectAssetModal = ({ const intl = useIntl(); const [search, setSearch] = useState(''); const [searchCategory, setSearchCategory] = useState(null); // coingeckoNativeId as fallback for ex. polygon + + const sendTokenCategories = [ + { + type: 'visibleWithBalance', + label: intl.formatMessage({ + id: 'TR_TOKENS', + defaultMessage: 'Tokens', + }), + }, + { + type: 'hiddenWithBalance', + label: intl.formatMessage({ + id: 'TR_HIDDEN', + defaultMessage: 'Hidden', + }), + }, + ] as TokenCategory[]; const [end, setEnd] = useState(options.length); const data = useMemo(() => getData(options), [options]); const { scrollElementRef, onScroll, ShadowTop, ShadowBottom, ShadowContainer } = useScrollShadow(); const networkCount = getNetworkCount(options); + const searchPlaceholder = searchPlaceholderText + ? searchPlaceholderText + : intl.formatMessage({ + id: 'TR_SELECT_NAME_OR_ADDRESS', + defaultMessage: 'Search by name, symbol, network or contract address', + }); + const filteredData = data.filter(item => { const categoryFilter = searchCategory ? item.coingeckoId === searchCategory.coingeckoId || @@ -158,10 +194,7 @@ export const SelectAssetModal = ({ > setSearch(event.target.value)} autoFocus @@ -181,6 +214,13 @@ export const SelectAssetModal = ({ setSearchCategory={setSearchCategory} /> )} + {sendTokenCategories && ( + + )} {filteredData.length === 0 ? ( ` + margin-left: -${spacingsPx.md}; + width: calc(100% + ${spacings.md * 2}px); + padding: ${spacings.zero} ${spacingsPx.md} ${spacingsPx.lg}; + border-bottom: 1px solid + ${({ theme, $elevation }) => mapElevationToBorder({ $elevation, theme })}; +`; + +interface SendTokenTabsProps { + sendTokenCategories: TokenCategory[]; + pickedCategory: TokenCategory['type']; + setPickedCategory: (value: TokenCategory['type']) => void; +} + +export const SendTokenTabs = ({ + sendTokenCategories, + pickedCategory, + setPickedCategory, +}: SendTokenTabsProps) => { + const { elevation } = useElevation(); + + return ( + + + {sendTokenCategories.map(category => ( + { + setPickedCategory(category.type); + }} + key={category.type} + > + {category.label} + + ))} + + + ); +}; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 8273df3850b..d323ff16819 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -359,6 +359,10 @@ export default defineMessages({ defaultMessage: 'Search by name, symbol, network, or contract address', id: 'TR_SELECT_NAME_OR_ADDRESS', }, + TR_SEARCH_TOKEN_IN_SEND_FORM_MODAL: { + defaultMessage: 'Search by name, symbol, or contract address', + id: 'TR_SEARCH_TOKEN_IN_SEND_FORM_MODAL', + }, TR_TOKEN_NOT_FOUND: { defaultMessage: 'Token not found', id: 'TR_TOKEN_NOT_FOUND', diff --git a/packages/suite/src/views/wallet/send/Outputs/TokenSelect/TokenSelect.tsx b/packages/suite/src/views/wallet/send/Outputs/TokenSelect/TokenSelect.tsx index 25e8d8e0729..2480f75904a 100644 --- a/packages/suite/src/views/wallet/send/Outputs/TokenSelect/TokenSelect.tsx +++ b/packages/suite/src/views/wallet/send/Outputs/TokenSelect/TokenSelect.tsx @@ -3,7 +3,7 @@ import { Controller } from 'react-hook-form'; import { AssetLogo, Column, Row, Select } from '@trezor/components'; import { useSendFormContext } from 'src/hooks/wallet'; import { Account } from 'src/types/wallet'; -import { useDispatch, useSelector } from 'src/hooks/suite'; +import { useDispatch, useSelector, useTranslation } from 'src/hooks/suite'; import { updateFiatRatesThunk, selectCurrentFiatRates } from '@suite-common/wallet-core'; import { AddressType, Timestamp, TokenAddress } from '@suite-common/wallet-types'; import { networks, NetworkSymbol } from '@suite-common/wallet-config'; @@ -34,6 +34,7 @@ import { openModal } from 'src/actions/suite/modalActions'; import { copyToClipboard } from '@trezor/dom-utils'; import { notificationsActions } from '@suite-common/toast-notifications'; import { selectIsCopyAddressModalShown } from 'src/reducers/suite/suiteReducer'; +import { TokenCategory } from '@trezor/product-components/src/components/SelectAssetModal/SelectAssetModal'; export const buildTokenOptions = ( accountTokens: Account['tokens'], @@ -70,22 +71,22 @@ export const buildTokenOptions = ( }); }); - // Right now we dont want to show hidden or unverified tokens, left for the future use + if (tokens.hiddenWithBalance.length) { + tokens.hiddenWithBalance.forEach(token => { + result.push({ + type: 'currency', + symbol: token.symbol ?? symbol, + networkSymbol: symbol, + hidden: true, + coingeckoId: getCoingeckoId(symbol) ?? '', + contractAddress: token.contract, + cryptoName: token.name, + balance: token.balance, + }); + }); + } - // if (tokens.hiddenWithBalance.length) { - // tokens.hiddenWithBalance.forEach(token => { - // result.push({ - // type: 'currency', - // symbol: token.symbol ?? symbol, - // networkSymbol: symbol, - // hidden: true, - // coingeckoId: getCoingeckoId(symbol) ?? '', - // contractAddress: token.contract, - // cryptoName: token.name, - // balance: token.balance, - // }); - // }); - // } + // Right now we dont want to show unverified tokens, left for the future use // if (tokens.unverifiedWithBalance.length) { // tokens.unverifiedWithBalance.forEach(token => { @@ -128,6 +129,8 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => { setValue, setDraftSaveRequest, } = useSendFormContext(); + const [pickedSendTokenCategory, setPickedSendTokenCategory] = + useState('visibleWithBalance'); const shouldShowCopyAddressModal = useSelector(selectIsCopyAddressModalShown); const [isTokensModalActive, setIsTokensModalActive] = useState(false); const coinDefinitions = useSelector(state => selectCoinDefinitions(state, account.symbol)); @@ -141,6 +144,7 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => { fiatRates, ); const dispatch = useDispatch(); + const { translationString } = useTranslation(); const sortedTokens = useMemo(() => { return tokensWithRates.sort(sortTokensWithRates); @@ -160,6 +164,14 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => { account.formattedBalance, ); + let filteredOptions: SelectAssetOptionCurrencyProps[] = []; + + if (pickedSendTokenCategory === 'visibleWithBalance') { + filteredOptions = options.filter(option => !option.hidden); + } else { + filteredOptions = options.filter(option => option.hidden); + } + // Amount needs to be re-validated again AFTER token change propagation (decimal places, available balance) // watch token change and use "useSendFormFields.setAmount" util for validation (if amount is set) // if Amount is not valid 'react-hook-form' will set an error to it, and composeTransaction will be prevented @@ -185,14 +197,14 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => { } }, [sendFormPrefill, setValue, tokenInputName, setDraftSaveRequest, dispatch]); - const findOption = options.find(option => { + const findOption = filteredOptions.find(option => { return option.type === 'currency' && option.contractAddress === tokenContractAddress; }) as SelectAssetOptionCurrencyProps | undefined; const selectedOption = findOption; const handleSelectChange = async (selectedAsset: SelectAssetOptionCurrencyProps) => { - const selectedOption = options.find( + const selectedOption = filteredOptions.find( option => option.contractAddress === selectedAsset.contractAddress, ); if (!selectedOption) return; @@ -244,10 +256,13 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => { <> {isTokensModalActive && ( setIsTokensModalActive(false)} + searchPlaceholderText={translationString('TR_SEARCH_TOKEN_IN_SEND_FORM_MODAL')} + pickedSendTokenCategory={pickedSendTokenCategory} + setPickedSendTokenCategory={setPickedSendTokenCategory} /> )} { valueContainer: base => ({ ...base, justifyContent: 'flex-start !important', - zIndex: 1000, }), }} data-testid="@amount-select"