Skip to content

Commit

Permalink
feat(suite): picker between Tokens and Hidden in modal
Browse files Browse the repository at this point in the history
  • Loading branch information
enjojoy committed Oct 25, 2024
1 parent 223373a commit 4ae812b
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -112,19 +121,46 @@ const getNetworkCount = (options: SelectAssetOptionProps[]) => {
export const SelectAssetModal = ({
options,
networkCategories,
searchPlaceholderText,
setPickedSendTokenCategory,
pickedSendTokenCategory,
onSelectAssetModal,
onFavoriteClick,
onClose,
}: SelectAssetModalProps) => {
const intl = useIntl();
const [search, setSearch] = useState('');
const [searchCategory, setSearchCategory] = useState<SelectAssetSearchCategoryType>(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 ||
Expand Down Expand Up @@ -158,10 +194,7 @@ export const SelectAssetModal = ({
>
<Column gap={spacings.md} alignItems="stretch">
<Input
placeholder={intl.formatMessage({
id: 'TR_SELECT_NAME_OR_ADDRESS',
defaultMessage: 'Search by name, symbol, network or contract address',
})}
placeholder={searchPlaceholder}
value={search}
onChange={event => setSearch(event.target.value)}
autoFocus
Expand All @@ -181,6 +214,13 @@ export const SelectAssetModal = ({
setSearchCategory={setSearchCategory}
/>
)}
{sendTokenCategories && (
<SendTokenTabs
sendTokenCategories={sendTokenCategories}
pickedCategory={pickedSendTokenCategory}
setPickedCategory={setPickedSendTokenCategory}
/>
)}

{filteredData.length === 0 ? (
<AssetItemNotFound
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Row, useElevation } from '@trezor/components';
import { Elevation, mapElevationToBorder, spacings, spacingsPx } from '@trezor/theme';
import { TokenCategory } from './SelectAssetModal';
import { useEffect } from 'react';
import styled from 'styled-components';
import { CheckableTag } from './CheckableTag';

interface NetworkTabsWrapperProps {
$elevation: Elevation;
}

const NetworkTabsWrapper = styled.div<NetworkTabsWrapperProps>`
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 (
<NetworkTabsWrapper $elevation={elevation}>
<Row gap={spacings.xs} flexWrap="wrap">
{sendTokenCategories.map(category => (
<CheckableTag
$elevation={elevation}
$variant={pickedCategory === category.type ? 'primary' : 'tertiary'}
onClick={() => {
setPickedCategory(category.type);
}}
key={category.type}
>
<Row gap={spacings.xxs}>{category.label}</Row>
</CheckableTag>
))}
</Row>
</NetworkTabsWrapper>
);
};
4 changes: 4 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -128,6 +129,8 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => {
setValue,
setDraftSaveRequest,
} = useSendFormContext();
const [pickedSendTokenCategory, setPickedSendTokenCategory] =
useState<TokenCategory['type']>('visibleWithBalance');
const shouldShowCopyAddressModal = useSelector(selectIsCopyAddressModalShown);
const [isTokensModalActive, setIsTokensModalActive] = useState(false);
const coinDefinitions = useSelector(state => selectCoinDefinitions(state, account.symbol));
Expand All @@ -141,6 +144,7 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => {
fiatRates,
);
const dispatch = useDispatch();
const { translationString } = useTranslation();

const sortedTokens = useMemo(() => {
return tokensWithRates.sort(sortTokensWithRates);
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -244,10 +256,13 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => {
<>
{isTokensModalActive && (
<SelectAssetModal
options={options}
options={filteredOptions}
// networkCategories={getNetworks()}
onSelectAssetModal={handleSelectChange}
onClose={() => setIsTokensModalActive(false)}
searchPlaceholderText={translationString('TR_SEARCH_TOKEN_IN_SEND_FORM_MODAL')}
pickedSendTokenCategory={pickedSendTokenCategory}
setPickedSendTokenCategory={setPickedSendTokenCategory}
/>
)}
<Controller
Expand Down Expand Up @@ -348,7 +363,6 @@ export const TokenSelect = ({ outputId }: TokenSelectProps) => {
valueContainer: base => ({
...base,
justifyContent: 'flex-start !important',
zIndex: 1000,
}),
}}
data-testid="@amount-select"
Expand Down

0 comments on commit 4ae812b

Please sign in to comment.