Skip to content

Commit

Permalink
[issue-1234] Sort the token by the balance on mobile app
Browse files Browse the repository at this point in the history
  • Loading branch information
dominhquang committed Dec 29, 2023
1 parent fea8f9d commit 5f966a4
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 51 deletions.
39 changes: 37 additions & 2 deletions src/components/Modal/common/TokenSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Keyboard, ListRenderItemInfo } from 'react-native';
import i18n from 'utils/i18n/i18n';
import { FullSizeSelectModal } from 'components/common/SelectModal';
Expand All @@ -8,12 +8,20 @@ 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 { useSelector } from 'react-redux';
import { RootState } from 'stores/index';
import { BN_ZERO } from 'utils/chainBalances';
import { getInputValuesFromString } from 'screens/Transaction/SendFundV2/Amount';

export type TokenItemType = {
name: string;
slug: string;
symbol: string;
originChain: string;
free?: BigN;
price?: number;
decimals?: number;
};

interface Props {
Expand All @@ -34,6 +42,23 @@ interface Props {
showAddBtn?: boolean;
}

const convertChainActivePriority = (active?: boolean) => (active ? 1 : 0);

const convertTokenBalance = (a: TokenItemType, b: TokenItemType) => {
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 0;
}

return Number(getInputValuesFromString(aResult.toFixed(), a.decimals || 0)) -
Number(getInputValuesFromString(bResult.toFixed(), b.decimals || 0)) >
0
? -1
: 1;
};

export const TokenSelector = ({
items,
selectedValueMap,
Expand All @@ -52,18 +77,28 @@ export const TokenSelector = ({
showAddBtn = true,
}: Props) => {
const navigation = useNavigation<RootNavigationProps>();
const { chainStateMap } = useSelector((state: RootState) => state.chainStore);
useEffect(() => {
setAdjustPan();
}, []);

const filteredItems = useMemo((): TokenItemType[] => {
return items.sort((a, b) => {
return (
convertChainActivePriority(chainStateMap[b.originChain]?.active) -
convertChainActivePriority(chainStateMap[a.originChain]?.active) || convertTokenBalance(a, b)
);
});
}, [chainStateMap, items]);

const _onSelectItem = (item: TokenItemType) => {
Keyboard.dismiss();
onSelectItem && onSelectItem(item);
};

return (
<FullSizeSelectModal
items={items}
items={filteredItems}
selectedValueMap={selectedValueMap}
onBackButtonPress={() => tokenSelectorRef?.current?.onCloseModal()}
selectModalType={'single'}
Expand Down
30 changes: 26 additions & 4 deletions src/components/TokenSelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import Text from 'components/Text';
import { ColorMap } from 'styles/color';
import { FontMedium, FontSemiBold } from 'styles/sharedStyles';
import { CheckCircle } from 'phosphor-react-native';
import { Icon, Typography } from 'components/design-system-ui';
import { Icon, Typography, Number } from 'components/design-system-ui';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';
import { ThemeTypes } from 'styles/themes';
import BigN from 'bignumber.js';
import { BN_ZERO } from 'utils/chainBalances';

interface Props extends TouchableOpacityProps {
symbol: string;
Expand All @@ -19,6 +21,9 @@ interface Props extends TouchableOpacityProps {
onSelectNetwork: () => void;
defaultItemKey?: string;
iconSize?: number;
free?: BigN;
decimals?: number;
price?: number;
}

export const TokenSelectItem = ({
Expand All @@ -31,6 +36,9 @@ export const TokenSelectItem = ({
onSelectNetwork,
defaultItemKey,
iconSize = 40,
free,
decimals,
price,
}: Props) => {
const theme = useSubWalletTheme().swThemes;
const styles = useMemo(() => createStyle(theme), [theme]);
Expand All @@ -52,11 +60,24 @@ export const TokenSelectItem = ({
</View>
</View>

{isSelected && (
<View style={styles.selectedIconWrapper}>
<Icon phosphorIcon={CheckCircle} weight={'fill'} size={'sm'} iconColor={theme.colorSuccess} />
{!!decimals && !!price && !new BigN(free || 0).eq(BN_ZERO) && (
<View style={{ alignItems: 'flex-end' }}>
<Number size={16} value={free || BN_ZERO} decimal={decimals} />
<Number
prefix={'$'}
size={12}
unitOpacity={0.45}
intOpacity={0.45}
decimalOpacity={0.45}
value={new BigN(free || 0).multipliedBy(new BigN(price))}
decimal={decimals}
/>
</View>
)}

<View style={styles.selectedIconWrapper}>
{isSelected && <Icon phosphorIcon={CheckCircle} weight={'fill'} size={'sm'} iconColor={theme.colorSuccess} />}
</View>
</View>
</TouchableOpacity>
);
Expand All @@ -81,6 +102,7 @@ function createStyle(theme: ThemeTypes) {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
paddingRight: theme.paddingXS,
},

itemTextStyle: {
Expand Down
6 changes: 5 additions & 1 deletion src/components/common/CancelUnstakeItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ export const CancelUnstakeItem = ({ item, isSelected, onPress }: Props) => {
size={theme.fontSize}
textStyle={{ ...FontSemiBold }}
/>
{isSelected && <Icon phosphorIcon={CheckCircle} weight={'fill'} size={'sm'} iconColor={theme.colorSuccess} />}
{isSelected ? (
<Icon phosphorIcon={CheckCircle} weight={'fill'} size={'sm'} iconColor={theme.colorSuccess} />
) : (
<View style={{ width: 20 }} />
)}
</View>
</TouchableOpacity>
);
Expand Down
6 changes: 5 additions & 1 deletion src/components/common/SelectModal/parts/TokenSelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ interface Props<T> {

export function _TokenSelectItem<T>({ item, selectedValueMap, onSelectItem, onCloseModal }: Props<T>) {
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 (
<TokenSelectItem
key={`${symbol}-${originChain}`}
Expand All @@ -23,10 +24,13 @@ export function _TokenSelectItem<T>({ item, selectedValueMap, onSelectItem, onCl
logoKey={slug.toLowerCase()}
subLogoKey={originChain}
isSelected={!!selectedValueMap[slug]}
free={free}
onSelectNetwork={() => {
onSelectItem && onSelectItem(item);
onCloseModal && onCloseModal();
}}
decimals={decimals}
price={price}
/>
);
}
51 changes: 27 additions & 24 deletions src/hooks/screen/Staking/useGetSupportedStakingTokens.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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 => {
Expand All @@ -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,
});
}
}
});
Expand All @@ -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]);
}
38 changes: 21 additions & 17 deletions src/screens/Transaction/SendFund/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AssetSetting, ExtrinsicType } from '@subwallet/extension-base/backgroun
import { AccountJson } from '@subwallet/extension-base/background/types';
import {
_getAssetDecimals,
_getAssetPriceId,
_getOriginChainOfAsset,
_isAssetFungibleToken,
_isChainEvmCompatible,
Expand Down Expand Up @@ -71,6 +72,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;
Expand All @@ -95,6 +97,8 @@ function getTokenItems(
assetRegistry: Record<string, _ChainAsset>,
assetSettingMap: Record<string, AssetSetting>,
multiChainAssetMap: Record<string, _MultiChainAsset>,
balanceMap: Record<string, BalanceItem>,
priceMap: Record<string, number>,
tokenGroupSlug?: string, // is ether a token slug or a multiChainAsset slug
): TokenItemType[] {
const account = findAccountByAddress(accounts, address);
Expand All @@ -109,6 +113,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)) {
Expand Down Expand Up @@ -141,10 +146,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) {
Expand All @@ -153,6 +160,9 @@ function getTokenItems(
slug: chainAsset.slug,
symbol: chainAsset.symbol,
originChain: chainAsset.originChain,
free: freeBalance,
price: priceMap[priceId] || 0,
decimals: chainAsset.decimals || undefined,
});
}
} else {
Expand All @@ -161,6 +171,9 @@ function getTokenItems(
slug: chainAsset.slug,
symbol: chainAsset.symbol,
originChain: chainAsset.originChain,
free: freeBalance,
price: priceMap[priceId] || 0,
decimals: chainAsset.decimals || undefined,
});
}
});
Expand Down Expand Up @@ -369,6 +382,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<string>('0');
Expand Down Expand Up @@ -466,30 +481,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,
]);

Expand Down
2 changes: 0 additions & 2 deletions src/screens/Transaction/Stake/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,7 @@ export const Stake = ({
selectedValueMap={{ [asset]: true }}
onSelectItem={onSelectToken}
disabled={stakingChain !== ALL_KEY || !from || loading}
defaultValue={asset}
showAddBtn={false}
acceptDefaultValue={true}
tokenSelectorRef={tokenSelectorRef}
renderSelected={() => <TokenSelectField logoKey={asset} subLogoKey={chain} value={symbol} showIcon />}
/>
Expand Down

0 comments on commit 5f966a4

Please sign in to comment.