From af5baf18fe91b34768a3d892ecd8789fd11e5f84 Mon Sep 17 00:00:00 2001 From: dominhquang Date: Sat, 16 Dec 2023 17:57:25 +0700 Subject: [PATCH] [issue-1234] Sort the token by the balance on mobile app --- src/components/Modal/common/TokenSelector.tsx | 40 +++++++++++- src/components/TokenSelectItem.tsx | 32 +++++++-- .../common/CancelUnstakeItem/index.tsx | 6 +- src/components/common/SelectModal/index.tsx | 3 + .../SelectModal/parts/TokenSelectItem.tsx | 10 ++- .../Staking/useGetSupportedStakingTokens.ts | 51 ++++++++------- src/screens/Home/Crypto/BuyToken.tsx | 2 +- src/screens/Transaction/SendFund/index.tsx | 65 ++++++++++++------- src/screens/Transaction/Stake/index.tsx | 2 - 9 files changed, 150 insertions(+), 61 deletions(-) diff --git a/src/components/Modal/common/TokenSelector.tsx b/src/components/Modal/common/TokenSelector.tsx index a4939c68b..b5e7e675b 100644 --- a/src/components/Modal/common/TokenSelector.tsx +++ b/src/components/Modal/common/TokenSelector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { ListRenderItemInfo } from 'react-native'; import i18n from 'utils/i18n/i18n'; import { FullSizeSelectModal } from 'components/common/SelectModal'; @@ -8,12 +8,18 @@ import { EmptyList } from 'components/EmptyList'; import { MagnifyingGlass } from 'phosphor-react-native'; import { useNavigation } from '@react-navigation/native'; import { RootNavigationProps } from 'routes/index'; +import BigN from 'bignumber.js'; +import { BN_ZERO } from 'utils/chainBalances'; +import { getInputValuesFromString } from 'screens/Transaction/SendFund/Amount'; export type TokenItemType = { name: string; slug: string; symbol: string; originChain: string; + free?: BigN; + price?: number; + decimals?: number; }; interface Props { @@ -32,8 +38,30 @@ interface Props { acceptDefaultValue?: boolean; onCloseAccountSelector?: () => void; showAddBtn?: boolean; + isShowBalance?: boolean; } +const convertTokenBalance = (a: TokenItemType, b: TokenItemType) => { + const aFree = new BigN(a.free || BN_ZERO); + const bFree = new BigN(b.free || BN_ZERO); + const aResult = new BigN(a.free || BN_ZERO).multipliedBy(new BigN(a.price || BN_ZERO)); + const bResult = new BigN(b.free || BN_ZERO).multipliedBy(new BigN(b.price || BN_ZERO)); + + if (aResult.eq(bResult)) { + return Number(getInputValuesFromString(aFree.toFixed(), a.decimals || 0)) - + Number(getInputValuesFromString(bFree.toFixed(), b.decimals || 0)) > + 0 + ? -1 + : 1; + } + + return Number(getInputValuesFromString(aResult.toFixed(), a.decimals || 0)) - + Number(getInputValuesFromString(bResult.toFixed(), b.decimals || 0)) > + 0 + ? -1 + : 1; +}; + export const TokenSelector = ({ items, selectedValueMap, @@ -50,19 +78,26 @@ export const TokenSelector = ({ acceptDefaultValue, onCloseAccountSelector, showAddBtn = true, + isShowBalance, }: Props) => { const navigation = useNavigation(); useEffect(() => { setAdjustPan(); }, []); + const filteredItems = useMemo((): TokenItemType[] => { + return items.sort((a, b) => { + return convertTokenBalance(a, b); + }); + }, [items]); + const _onSelectItem = (item: TokenItemType) => { onSelectItem && onSelectItem(item); }; return ( tokenSelectorRef?.current?.onCloseModal()} selectModalType={'single'} @@ -78,6 +113,7 @@ export const TokenSelector = ({ renderCustomItem={renderCustomItem} defaultValue={defaultValue} acceptDefaultValue={acceptDefaultValue} + isShowBalance={isShowBalance} renderListEmptyComponent={() => { return ( void; defaultItemKey?: string; iconSize?: number; + free?: BigN; + decimals?: number; + price?: number; + isShowBalance?: boolean; } export const TokenSelectItem = ({ @@ -31,6 +37,10 @@ export const TokenSelectItem = ({ onSelectNetwork, defaultItemKey, iconSize = 40, + free, + decimals, + price, + isShowBalance = false, }: Props) => { const theme = useSubWalletTheme().swThemes; const styles = useMemo(() => createStyle(theme), [theme]); @@ -52,11 +62,24 @@ export const TokenSelectItem = ({ - {isSelected && ( - - + {typeof free !== undefined && isShowBalance && ( + + + )} + + + {isSelected && } + ); @@ -81,6 +104,7 @@ function createStyle(theme: ThemeTypes) { flexDirection: 'row', alignItems: 'center', flex: 1, + paddingRight: theme.paddingXS, }, itemTextStyle: { diff --git a/src/components/common/CancelUnstakeItem/index.tsx b/src/components/common/CancelUnstakeItem/index.tsx index a37f488f5..7cc314dfa 100644 --- a/src/components/common/CancelUnstakeItem/index.tsx +++ b/src/components/common/CancelUnstakeItem/index.tsx @@ -63,7 +63,11 @@ export const CancelUnstakeItem = ({ item, isSelected, onPress }: Props) => { size={theme.fontSize} textStyle={{ ...FontSemiBold }} /> - {isSelected && } + {isSelected ? ( + + ) : ( + + )} ); diff --git a/src/components/common/SelectModal/index.tsx b/src/components/common/SelectModal/index.tsx index 677508965..82888fbb9 100644 --- a/src/components/common/SelectModal/index.tsx +++ b/src/components/common/SelectModal/index.tsx @@ -56,6 +56,7 @@ interface Props { onCloseModal?: () => void; onModalOpened?: () => void; rightIconOption?: RightIconOpt; + isShowBalance?: boolean; level?: number; } const LOADING_TIMEOUT = Platform.OS === 'ios' ? 20 : 100; @@ -93,6 +94,7 @@ function _SelectModal(selectModalProps: Props, ref: ForwardedRef) { onCloseModal: _onCloseModal, onModalOpened, rightIconOption, + isShowBalance, level, } = selectModalProps; const chainInfoMap = useSelector((root: RootState) => root.chainStore.chainInfoMap); @@ -209,6 +211,7 @@ function _SelectModal(selectModalProps: Props, ref: ForwardedRef) { selectedValueMap={selectedValueMap} onSelectItem={_onSelectItem} onCloseModal={() => closeModalAfterSelect && modalBaseV2Ref?.current?.close()} + isShowBalance={isShowBalance} /> ); } else if (selectModalItemType === 'chain') { diff --git a/src/components/common/SelectModal/parts/TokenSelectItem.tsx b/src/components/common/SelectModal/parts/TokenSelectItem.tsx index c0582f624..82714004e 100644 --- a/src/components/common/SelectModal/parts/TokenSelectItem.tsx +++ b/src/components/common/SelectModal/parts/TokenSelectItem.tsx @@ -9,11 +9,13 @@ interface Props { selectedValueMap: Record; onSelectItem?: (item: T) => void; onCloseModal?: () => void; + isShowBalance?: boolean; } -export function _TokenSelectItem({ item, selectedValueMap, onSelectItem, onCloseModal }: Props) { +export function _TokenSelectItem({ item, selectedValueMap, onSelectItem, onCloseModal, isShowBalance }: Props) { const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); - const { symbol, originChain, slug, name } = item as TokenItemType; + const { symbol, originChain, slug, name, free, price, decimals } = item as TokenItemType; + return ( ({ item, selectedValueMap, onSelectItem, onCl logoKey={slug.toLowerCase()} subLogoKey={originChain} isSelected={!!selectedValueMap[slug]} + free={free} + isShowBalance={isShowBalance} onSelectNetwork={() => { onSelectItem && onSelectItem(item); onCloseModal && onCloseModal(); }} + decimals={decimals} + price={price} /> ); } diff --git a/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts b/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts index 9bd4e256d..6dc00c948 100644 --- a/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts +++ b/src/hooks/screen/Staking/useGetSupportedStakingTokens.ts @@ -1,4 +1,4 @@ -import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { _ChainInfo } from '@subwallet/chain-list/types'; import { StakingType } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson } from '@subwallet/extension-base/background/types'; import { _STAKING_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; @@ -15,7 +15,8 @@ import { ALL_KEY } from 'constants/index'; import { AccountAddressType } from 'types/index'; import { findAccountByAddress, getAccountAddressType } from 'utils/account'; import useChainAssets from 'hooks/chain/useChainAssets'; -import useChainChecker from 'hooks/chain/useChainChecker'; +import { BN_ZERO } from '@polkadot/util'; +import { TokenItemType } from 'components/Modal/common/TokenSelector'; const isChainTypeValid = (chainInfo: _ChainInfo, accounts: AccountJson[], address?: string): boolean => { const addressType = getAccountAddressType(address); @@ -43,13 +44,15 @@ export default function useGetSupportedStakingTokens( type: StakingType, address?: string, chain?: string, -): _ChainAsset[] { +): TokenItemType[] { const chainInfoMap = useSelector((state: RootState) => state.chainStore.chainInfoMap); const assetRegistryMap = useChainAssets().chainAssetRegistry; - const accounts = useSelector((state: RootState) => state.accountState.accounts); - const { checkChainConnected } = useChainChecker(); + const priceMap = useSelector((state: RootState) => state.price.priceMap); + const { balanceMap } = useSelector((root: RootState) => root.balance); + const { accounts, currentAccount } = useSelector((state: RootState) => state.accountState); return useMemo(() => { - const result: _ChainAsset[] = []; + const result: TokenItemType[] = []; + const accBalanceMap = currentAccount ? balanceMap[currentAccount.address] : undefined; if (type === StakingType.NOMINATED) { Object.values(chainInfoMap).forEach(chainInfo => { @@ -61,7 +64,14 @@ export default function useGetSupportedStakingTokens( isChainTypeValid(chainInfo, accounts, address) && (!chain || chain === ALL_KEY || chain === chainInfo.slug) ) { - result.push(assetRegistryMap[nativeTokenSlug]); + const item = assetRegistryMap[nativeTokenSlug]; + const freeBalance = accBalanceMap[item.slug]?.free || BN_ZERO; + result.push({ + ...item, + price: item.priceId ? priceMap[item.priceId] : 0, + free: accBalanceMap ? freeBalance : BN_ZERO, + decimals: item.decimals || undefined, + }); } } }); @@ -78,26 +88,19 @@ export default function useGetSupportedStakingTokens( isChainTypeValid(chainInfo, accounts, address) && (!chain || chain === ALL_KEY || chain === chainInfo.slug) ) { - result.push(assetRegistryMap[nativeTokenSlug]); + const item = assetRegistryMap[nativeTokenSlug]; + const freeBalance = accBalanceMap[item.slug]?.free || BN_ZERO; + result.push({ + ...item, + price: item.priceId ? priceMap[item.priceId] : 0, + free: accBalanceMap ? freeBalance : BN_ZERO, + decimals: item.decimals || undefined, + }); } } }); } - return result.sort((a, b) => { - if (checkChainConnected(a.originChain)) { - if (checkChainConnected(b.originChain)) { - return 0; - } else { - return -1; - } - } else { - if (checkChainConnected(b.originChain)) { - return 1; - } else { - return 0; - } - } - }); - }, [type, chainInfoMap, assetRegistryMap, accounts, address, chain, checkChainConnected]); + return result; + }, [currentAccount, balanceMap, type, chainInfoMap, assetRegistryMap, accounts, address, chain, priceMap]); } diff --git a/src/screens/Home/Crypto/BuyToken.tsx b/src/screens/Home/Crypto/BuyToken.tsx index fffccb7ca..f6317ae1e 100644 --- a/src/screens/Home/Crypto/BuyToken.tsx +++ b/src/screens/Home/Crypto/BuyToken.tsx @@ -4,7 +4,7 @@ import { Button, Icon, PageIcon, Typography } from 'components/design-system-ui' import { ShoppingCartSimple } from 'phosphor-react-native'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { AccountSelector } from 'components/Modal/common/AccountSelectorNew'; -import { TokenSelector } from 'components/Modal/common/TokenSelectorNew'; +import { TokenSelector } from 'components/Modal/common/TokenSelector'; import useBuyToken from 'hooks/screen/Home/Crypto/useBuyToken'; import { useSelector } from 'react-redux'; import { RootState } from 'stores/index'; diff --git a/src/screens/Transaction/SendFund/index.tsx b/src/screens/Transaction/SendFund/index.tsx index 377d2b9c0..127055ef2 100644 --- a/src/screens/Transaction/SendFund/index.tsx +++ b/src/screens/Transaction/SendFund/index.tsx @@ -6,6 +6,7 @@ import { AssetSetting, ExtrinsicType } from '@subwallet/extension-base/backgroun import { AccountJson } from '@subwallet/extension-base/background/types'; import { _getAssetDecimals, + _getAssetPriceId, _getOriginChainOfAsset, _getTokenMinAmount, _isAssetFungibleToken, @@ -23,14 +24,14 @@ import { SendFundProps } from 'routes/transaction/transactionAction'; import { useSelector } from 'react-redux'; import { RootState } from 'stores/index'; import { getMaxTransfer, makeCrossChainTransfer, makeTransfer, saveRecentAccountId } from 'messaging/index'; -import { findAccountByAddress } from 'utils/account'; +import { findAccountByAddress, getAccountAddressType } from 'utils/account'; import { findNetworkJsonByGenesisHash } from 'utils/getNetworkJsonByGenesisHash'; import { balanceFormatter, formatBalance, formatNumber } from 'utils/number'; import useGetChainPrefixBySlug from 'hooks/chain/useGetChainPrefixBySlug'; import { TokenItemType, TokenSelector } from 'components/Modal/common/TokenSelector'; import useHandleSubmitTransaction, { insufficientMessages } from 'hooks/transaction/useHandleSubmitTransaction'; import { isAccountAll } from 'utils/accountAll'; -import { ChainInfo, ChainItemType } from 'types/index'; +import { AccountAddressType, ChainInfo, ChainItemType } from 'types/index'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { useToast } from 'react-native-toast-notifications'; import usePreCheckAction from 'hooks/account/usePreCheckAction'; @@ -73,6 +74,7 @@ import { FreeBalanceDisplay } from 'screens/Transaction/parts/FreeBalanceDisplay import { ModalRef } from 'types/modalRef'; import useChainAssets from 'hooks/chain/useChainAssets'; import { TransactionDone } from 'screens/Transaction/TransactionDone'; +import { BalanceItem } from '@subwallet/extension-base/types'; interface TransferFormValues extends TransactionFormValues { to: string; @@ -97,6 +99,8 @@ function getTokenItems( assetRegistry: Record, assetSettingMap: Record, multiChainAssetMap: Record, + balanceMap: Record, + priceMap: Record, tokenGroupSlug?: string, // is ether a token slug or a multiChainAsset slug ): TokenItemType[] { const account = findAccountByAddress(accounts, address); @@ -111,6 +115,7 @@ function getTokenItems( const isAccountEthereum = isEthereumAddress(address); const isSetTokenSlug = !!tokenGroupSlug && !!assetRegistry[tokenGroupSlug]; const isSetMultiChainAssetSlug = !!tokenGroupSlug && !!multiChainAssetMap[tokenGroupSlug]; + const accBalanceMap = balanceMap[address]; if (tokenGroupSlug) { if (!(isSetTokenSlug || isSetMultiChainAssetSlug)) { @@ -122,14 +127,19 @@ function getTokenItems( if (isSetTokenSlug) { if (isAssetTypeValid(chainAsset, chainInfoMap, isAccountEthereum) && isValidLedger) { - const { name, originChain, slug, symbol } = assetRegistry[tokenGroupSlug]; - + const { name, originChain, slug, symbol, decimals } = assetRegistry[tokenGroupSlug]; + const priceId = _getAssetPriceId(assetRegistry[tokenGroupSlug]); + // @ts-ignore + const freeBalance = accBalanceMap[chainAsset.slug]?.free || BN_ZERO; return [ { name, slug, symbol, originChain, + free: freeBalance, + price: priceMap[priceId] || 0, + decimals: decimals || undefined, }, ]; } else { @@ -143,10 +153,12 @@ function getTokenItems( Object.values(assetRegistry).forEach(chainAsset => { const isValidLedger = isLedger ? isAccountEthereum || validLedgerNetwork.includes(chainAsset?.originChain) : true; const isTokenFungible = _isAssetFungibleToken(chainAsset); - + // @ts-ignore + const freeBalance = accBalanceMap[chainAsset.slug]?.free || BN_ZERO; if (!(isTokenFungible && isAssetTypeValid(chainAsset, chainInfoMap, isAccountEthereum) && isValidLedger)) { return; } + const priceId = _getAssetPriceId(chainAsset); if (isSetMultiChainAssetSlug) { if (chainAsset.multiChainAsset === tokenGroupSlug) { @@ -155,6 +167,9 @@ function getTokenItems( slug: chainAsset.slug, symbol: chainAsset.symbol, originChain: chainAsset.originChain, + free: freeBalance, + price: priceMap[priceId] || 0, + decimals: chainAsset.decimals || undefined, }); } } else { @@ -163,6 +178,9 @@ function getTokenItems( slug: chainAsset.slug, symbol: chainAsset.symbol, originChain: chainAsset.originChain, + free: freeBalance, + price: priceMap[priceId] || 0, + decimals: chainAsset.decimals || undefined, }); } }); @@ -382,6 +400,8 @@ export const SendFund = ({ const { chainInfoMap, chainStateMap } = useSelector((root: RootState) => root.chainStore); const { assetSettingMap, multiChainAssetMap, xcmRefMap } = useSelector((root: RootState) => root.assetRegistry); + const priceMap = useSelector((state: RootState) => state.price.priceMap); + const { balanceMap } = useSelector((root: RootState) => root.balance); const assetRegistry = useChainAssets().chainAssetRegistry; const { accounts, isAllAccount } = useSelector((state: RootState) => state.accountState); const [maxTransfer, setMaxTransfer] = useState('0'); @@ -466,30 +486,19 @@ export const SendFund = ({ assetRegistry, assetSettingMap, multiChainAssetMap, + balanceMap, + priceMap, tokenGroupSlug, - ).sort((a, b) => { - if (checkChainConnected(a.originChain)) { - if (checkChainConnected(b.originChain)) { - return 0; - } else { - return -1; - } - } else { - if (checkChainConnected(b.originChain)) { - return 1; - } else { - return 0; - } - } - }); + ); }, [ accounts, assetRegistry, assetSettingMap, + balanceMap, chainInfoMap, - checkChainConnected, fromValue, multiChainAssetMap, + priceMap, tokenGroupSlug, ]); @@ -963,10 +972,8 @@ export const SendFund = ({ if (tokenItems.length) { let isApplyDefaultAsset = true; - + const account = findAccountByAddress(accounts, from); if (!asset) { - const account = findAccountByAddress(accounts, from); - if (account?.originGenesisHash) { const network = findNetworkJsonByGenesisHash(chainInfoMap, account.originGenesisHash); @@ -985,7 +992,14 @@ export const SendFund = ({ } if (isApplyDefaultAsset) { - updateInfoWithTokenSlug(tokenItems[0].slug); + const a = getAccountAddressType(account?.address); + if (a === AccountAddressType.ETHEREUM) { + const _defaultItem = tokenItems.find(item => item.slug === 'ethereum-NATIVE-ETH'); + updateInfoWithTokenSlug(_defaultItem?.slug || tokenItems[0].slug); + } else { + const _defaultItem = tokenItems.find(item => item.slug === 'polkadot-NATIVE-DOT'); + updateInfoWithTokenSlug(_defaultItem?.slug || tokenItems[0].slug); + } } } }, [accounts, tokenItems, assetRegistry, setChain, chainInfoMap, getValues, setValue]); @@ -1122,6 +1136,7 @@ export const SendFund = ({ onSelectItem={_onChangeAsset} showAddBtn={false} tokenSelectorRef={tokenSelectorRef} + isShowBalance renderSelected={() => ( } />