From 6119fda80a4f09d0f5f822169fba4e9a8d121178 Mon Sep 17 00:00:00 2001 From: nguyentiendung Date: Wed, 29 May 2024 17:23:51 +0700 Subject: [PATCH 1/2] [Issue-64] Update Send Fund UX --- .../Popup/Transaction/variants/SendFund.tsx | 102 ++++++------------ 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index 26058892c08..724df72f1a0 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -8,13 +8,13 @@ import { _getAssetDecimals, _getOriginChainOfAsset, _getTokenMinAmount, _isAsset import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { BitcoinFeeDetail, ResponseSubscribeTransfer, TransactionFee } from '@subwallet/extension-base/types'; import { BN_ZERO, detectTranslate } from '@subwallet/extension-base/utils'; -import { AccountSelector, AddressInput, AlertBox, AlertModal, AmountInput, BitcoinFeeSelector, ChainSelector, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; +import { AccountSelector, AddressInput, AlertBox, AlertModal, AmountInput, BitcoinFeeSelector, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { BITCOIN_CHAINS, SUPPORT_CHAINS } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useFetchChainAssetInfo, useGetChainPrefixBySlug, useGetNativeTokenBasicInfo, useHandleSubmitTransaction, useInitValidateTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { cancelSubscription, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { FreeBalance } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { ChainItemType, FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; +import { FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { findAccountByAddress, formatBalance, noop, reformatAddress } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { KeypairType } from '@subwallet/keyring/types'; @@ -22,7 +22,7 @@ import { Button, Form, Icon, Number } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; import BigN from 'bignumber.js'; import CN from 'classnames'; -import { PaperPlaneRight, PaperPlaneTilt } from 'phosphor-react'; +import { PaperPlaneTilt } from 'phosphor-react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -52,10 +52,6 @@ function getTokenItems ( multiChainAssetMap: Record, tokenGroupSlug?: string // is ether a token slug or a multiChainAsset slug ): TokenItemType[] { - if (!address) { - return []; - } - const isSetTokenSlug = !!tokenGroupSlug && !!assetRegistry[tokenGroupSlug]; const isSetMultiChainAssetSlug = !!tokenGroupSlug && !!multiChainAssetMap[tokenGroupSlug]; const addressType = getKeypairTypeByAddress(address); @@ -94,9 +90,9 @@ function getTokenItems ( return; } - if (!checkValidBetweenAddressTypeAndChain(addressType, chainAsset.originChain)) { - return; - } + // if (!checkValidBetweenAddressTypeAndChain(addressType, chainAsset.originChain)) { + // return; + // } if (isSetMultiChainAssetSlug) { if (chainAsset.multiChainAsset === tokenGroupSlug) { @@ -128,34 +124,6 @@ function getTokenItems ( }); } -function getTokenAvailableDestinations (tokenSlug: string, xcmRefMap: Record, chainInfoMap: Record): ChainItemType[] { - if (!tokenSlug) { - return []; - } - - const result: ChainItemType[] = []; - const originChain = chainInfoMap[_getOriginChainOfAsset(tokenSlug)]; - - // Firstly, push the originChain of token - result.push({ - name: originChain.name, - slug: originChain.slug - }); - - Object.values(xcmRefMap).forEach((xcmRef) => { - if (xcmRef.srcAsset === tokenSlug) { - const destinationChain = chainInfoMap[xcmRef.destChain]; - - result.push({ - name: destinationChain.name, - slug: destinationChain.slug - }); - } - }); - - return result; -} - const hiddenFields: Array = ['chain', 'fromProxyId']; const validateFields: Array = ['value', 'to']; const alertModalId = 'confirmation-alert-modal'; @@ -187,7 +155,7 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { chainInfoMap, chainStatusMap } = useSelector((root) => root.chainStore); - const { assetRegistry, multiChainAssetMap, xcmRefMap } = useSelector((root) => root.assetRegistry); + const { assetRegistry, multiChainAssetMap } = useSelector((root) => root.assetRegistry); const { accounts } = useSelector((state: RootState) => state.accountState); const destChainNetworkPrefix = useGetChainPrefixBySlug(destChainValue); @@ -206,10 +174,6 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const chainStatus = useMemo(() => chainStatusMap[chainValue]?.connectionStatus, [chainValue, chainStatusMap]); - const destChainItems = useMemo(() => { - return getTokenAvailableDestinations(assetValue, xcmRefMap, chainInfoMap); - }, [chainInfoMap, assetValue, xcmRefMap]); - const currentChainAsset = useMemo(() => { const _asset = isFirstRender ? defaultData.asset : assetValue; @@ -504,8 +468,16 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const addressBookFilter = useCallback((addressJson: AbstractAddressJson): boolean => { const addressType = getKeypairTypeByAddress(addressJson.address); + if (chainValue === 'bitcoin') { + return 'bitcoin-84'.includes(addressType) && addressJson.address !== fromValue; + } else if (chainValue === 'bitcoinTestnet') { + return 'bittest-84'.includes(addressType) && addressJson.address !== fromValue; + } else if (chainValue === 'ethereum') { + return 'ethereum'.includes(addressType) && addressJson.address !== fromValue; + } + return ['ethereum', 'bitcoin-84', 'bittest-84'].includes(addressType); - }, []); + }, [chainValue, fromValue]); // TODO: Need to review // Auto fill logic @@ -605,8 +577,16 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const accountType = account.type || getKeypairTypeByAddress(account.address); + if (chainValue === 'bitcoin') { + return 'bitcoin-84'.includes(accountType); + } else if (chainValue === 'bitcoinTestnet') { + return 'bittest-84'.includes(accountType); + } else if (chainValue === 'ethereum') { + return 'ethereum'.includes(accountType); + } + return ['ethereum', 'bitcoin-84', 'bittest-84'].includes(accountType); - }, [fromProxyId]); + }, [chainValue, fromProxyId]); useEffect(() => { const bnTransferAmount = new BigN(transferAmountValue || '0'); @@ -634,15 +614,6 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { onFinish={onSubmit} onValuesChange={onValuesChange} > - - - -
=> { tooltip={t('Select token')} /> - - - - - -
+ + + From f1a8840c214916e3a6cc39da15abf262cfa138a0 Mon Sep 17 00:00:00 2001 From: nguyentiendung Date: Thu, 30 May 2024 15:47:39 +0700 Subject: [PATCH 2/2] [Issue-64] Update Send Fund UX --- .../src/Popup/Home/Tokens/DetailList.tsx | 3 +- .../Popup/Transaction/variants/SendFund.tsx | 141 ++++++++++-------- 2 files changed, 83 insertions(+), 61 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx index 17c20892bd2..b5dd4120a36 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailList.tsx @@ -245,11 +245,12 @@ function Component (): React.ReactElement { setStorage({ ...DEFAULT_TRANSFER_PARAMS, + defaultSlug: tokenGroupSlug || '', fromProxyId }); navigate('/transaction/send-fund'); }, - [currentAccountProxy, navigate, setStorage] + [currentAccountProxy, navigate, setStorage, tokenGroupSlug] ); const onOpenBuyTokens = useCallback(() => { diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index 724df72f1a0..c93811b4de0 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -1,15 +1,15 @@ // Copyright 2019-2022 @polkadot/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; +import { _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; import { ExtrinsicType, NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { AbstractAddressJson, AccountJson } from '@subwallet/extension-base/background/types'; -import { _getAssetDecimals, _getOriginChainOfAsset, _getTokenMinAmount, _isAssetFungibleToken, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getTokenMinAmount, _isNativeToken, _isPureBitcoinChain, _isPureEvmChain, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { BitcoinFeeDetail, ResponseSubscribeTransfer, TransactionFee } from '@subwallet/extension-base/types'; import { BN_ZERO, detectTranslate } from '@subwallet/extension-base/utils'; import { AccountSelector, AddressInput, AlertBox, AlertModal, AmountInput, BitcoinFeeSelector, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; -import { BITCOIN_CHAINS, SUPPORT_CHAINS } from '@subwallet/extension-koni-ui/constants'; +import { BITCOIN_CHAINS } from '@subwallet/extension-koni-ui/constants'; import { useAlert, useFetchChainAssetInfo, useGetChainPrefixBySlug, useGetNativeTokenBasicInfo, useHandleSubmitTransaction, useInitValidateTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import { cancelSubscription, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { FreeBalance } from '@subwallet/extension-koni-ui/Popup/Transaction/parts'; @@ -17,7 +17,6 @@ import { RootState } from '@subwallet/extension-koni-ui/stores'; import { FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { findAccountByAddress, formatBalance, noop, reformatAddress } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress } from '@subwallet/keyring'; -import { KeypairType } from '@subwallet/keyring/types'; import { Button, Form, Icon, Number } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; import BigN from 'bignumber.js'; @@ -34,27 +33,26 @@ import { TransactionContent, TransactionFooter } from '../parts'; type Props = ThemeProps; -function checkValidBetweenAddressTypeAndChain (addressType: KeypairType, chain: string): boolean { - if (chain === 'bitcoin') { - return ['bitcoin-44', 'bitcoin-84'].includes(addressType); - } - - if (chain === 'bitcoinTestnet') { - return ['bittest-44', 'bittest-84'].includes(addressType); - } - - return chain === 'ethereum' && addressType === 'ethereum'; -} +// function checkValidBetweenAddressTypeAndChain (addressType: KeypairType, chain: string): boolean { +// if (chain === 'bitcoin') { +// return ['bitcoin-44', 'bitcoin-84'].includes(addressType); +// } +// +// if (chain === 'bitcoinTestnet') { +// return ['bittest-44', 'bittest-84'].includes(addressType); +// } +// +// return chain === 'ethereum' && addressType === 'ethereum'; +// } function getTokenItems ( - address: string, + chainInfoMap: Record, assetRegistry: Record, multiChainAssetMap: Record, tokenGroupSlug?: string // is ether a token slug or a multiChainAsset slug ): TokenItemType[] { const isSetTokenSlug = !!tokenGroupSlug && !!assetRegistry[tokenGroupSlug]; const isSetMultiChainAssetSlug = !!tokenGroupSlug && !!multiChainAssetMap[tokenGroupSlug]; - const addressType = getKeypairTypeByAddress(address); if (tokenGroupSlug) { if (!(isSetTokenSlug || isSetMultiChainAssetSlug)) { @@ -66,10 +64,6 @@ function getTokenItems ( if (isSetTokenSlug && chainAsset) { const { name, originChain, slug, symbol } = chainAsset; - if (!checkValidBetweenAddressTypeAndChain(addressType, originChain)) { - return []; - } - return [ { name, @@ -84,18 +78,23 @@ function getTokenItems ( const items: TokenItemType[] = []; Object.values(assetRegistry).forEach((chainAsset) => { - const isTokenFungible = _isAssetFungibleToken(chainAsset); + const chainInfo = chainInfoMap[chainAsset.originChain]; - if (!(isTokenFungible && _isNativeToken(chainAsset) && SUPPORT_CHAINS.includes(chainAsset.originChain))) { + if (chainAsset.assetType === 'RUNE' || chainAsset.assetType === 'BRC20') { return; } - // if (!checkValidBetweenAddressTypeAndChain(addressType, chainAsset.originChain)) { - // return; - // } - - if (isSetMultiChainAssetSlug) { - if (chainAsset.multiChainAsset === tokenGroupSlug) { + if (_isPureBitcoinChain(chainInfo) || _isPureEvmChain(chainInfo)) { + if (isSetMultiChainAssetSlug) { + if (chainAsset.multiChainAsset === tokenGroupSlug) { + items.push({ + name: chainAsset.name, + slug: chainAsset.slug, + symbol: chainAsset.symbol, + originChain: chainAsset.originChain + }); + } + } else { items.push({ name: chainAsset.name, slug: chainAsset.slug, @@ -103,28 +102,42 @@ function getTokenItems ( originChain: chainAsset.originChain }); } - } else { - items.push({ - name: chainAsset.name, - slug: chainAsset.slug, - symbol: chainAsset.symbol, - originChain: chainAsset.originChain - }); } }); return items.sort((a, b) => { - if (a.originChain.toLowerCase() === 'bitcoin' || a.originChain.toLowerCase() === 'bitcoinTestnet') { - return -1; // Place 'bitcoin' or 'BitcoinTestnet' at the top - } else if (b.originChain.toLowerCase() === 'bitcoin' || b.originChain.toLowerCase() === 'bitcoinTestnet') { + const aChain = a.originChain.toLowerCase(); + const bChain = b.originChain.toLowerCase(); + + if (aChain === 'bitcoin') { + return -1; + } + + if (bChain === 'bitcoin') { return 1; - } else { - return a.originChain.localeCompare(b.originChain); } + + if (aChain === 'bitcointestnet') { + return -1; + } + + if (bChain === 'bitcointestnet') { + return 1; + } + + if (aChain === 'ethereum' && a.name.toLowerCase() === 'ethereum') { + return -1; + } + + if (bChain === 'ethereum' && b.name.toLowerCase() === 'ethereum') { + return 1; + } + + return a.originChain.localeCompare(b.originChain); }); } -const hiddenFields: Array = ['chain', 'fromProxyId']; +const hiddenFields: Array = ['chain', 'fromProxyId', 'destChain']; const validateFields: Array = ['value', 'to']; const alertModalId = 'confirmation-alert-modal'; @@ -153,7 +166,6 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const assetInfo = useFetchChainAssetInfo(assetValue); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); - const { chainInfoMap, chainStatusMap } = useSelector((root) => root.chainStore); const { assetRegistry, multiChainAssetMap } = useSelector((root) => root.assetRegistry); const { accounts } = useSelector((state: RootState) => state.accountState); @@ -169,7 +181,7 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const hideMaxButton = useMemo(() => { const chainInfo = chainInfoMap[chainValue]; - return !!chainInfo && !!assetInfo && _isChainEvmCompatible(chainInfo) && destChainValue === chainValue && _isNativeToken(assetInfo); + return !!chainInfo && !!assetInfo && _isPureEvmChain(chainInfo) && destChainValue === chainValue && _isNativeToken(assetInfo); }, [chainInfoMap, chainValue, destChainValue, assetInfo]); const chainStatus = useMemo(() => chainStatusMap[chainValue]?.connectionStatus, [chainValue, chainStatusMap]); @@ -202,12 +214,12 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const tokenItems = useMemo(() => { return getTokenItems( - fromValue, + chainInfoMap, assetRegistry, multiChainAssetMap, sendFundSlug ); - }, [assetRegistry, fromValue, multiChainAssetMap, sendFundSlug]); + }, [assetRegistry, chainInfoMap, multiChainAssetMap, sendFundSlug]); const [loading, setLoading] = useState(false); const [isTransferAll, setIsTransferAll] = useState(false); @@ -295,13 +307,6 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { } } - if (part.from) { - setForceUpdateMaxValue(undefined); - form.resetFields(['asset']); - // Because cache data, so next data may be same with default data - form.setFields([{ name: 'asset', value: '' }]); - } - if (part.destChain) { setForceUpdateMaxValue(isTransferAll ? {} : undefined); @@ -468,16 +473,18 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { const addressBookFilter = useCallback((addressJson: AbstractAddressJson): boolean => { const addressType = getKeypairTypeByAddress(addressJson.address); + const chainInfo = chainInfoMap[chainValue]; + if (chainValue === 'bitcoin') { return 'bitcoin-84'.includes(addressType) && addressJson.address !== fromValue; } else if (chainValue === 'bitcoinTestnet') { return 'bittest-84'.includes(addressType) && addressJson.address !== fromValue; - } else if (chainValue === 'ethereum') { + } else if (!!chainInfo && _isPureEvmChain(chainInfo)) { return 'ethereum'.includes(addressType) && addressJson.address !== fromValue; } - return ['ethereum', 'bitcoin-84', 'bittest-84'].includes(addressType); - }, [chainValue, fromValue]); + return false; + }, [chainInfoMap, chainValue, fromValue]); // TODO: Need to review // Auto fill logic @@ -576,17 +583,22 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { } const accountType = account.type || getKeypairTypeByAddress(account.address); + const chainInfo = chainInfoMap[chainValue]; if (chainValue === 'bitcoin') { return 'bitcoin-84'.includes(accountType); } else if (chainValue === 'bitcoinTestnet') { return 'bittest-84'.includes(accountType); - } else if (chainValue === 'ethereum') { + } else if (!!chainInfo && _isPureEvmChain(chainInfo)) { return 'ethereum'.includes(accountType); } - return ['ethereum', 'bitcoin-84', 'bittest-84'].includes(accountType); - }, [chainValue, fromProxyId]); + return false; + }, [chainInfoMap, chainValue, fromProxyId]); + + const accountList = useMemo(() => { + return accounts.filter(accountsFilter); + }, [accounts, accountsFilter]); useEffect(() => { const bnTransferAmount = new BigN(transferAmountValue || '0'); @@ -597,6 +609,14 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { } }, [transferInfo, transferAmountValue]); + useEffect(() => { + if (accountList.length === 1) { + form.setFieldsValue({ + from: accountList?.[0]?.address || '' + }); + } + }, [accountList, form]); + useRestoreTransaction(form); useInitValidateTransaction(validateFields, form, defaultData); @@ -629,7 +649,8 @@ const _SendFund = ({ className = '' }: Props): React.ReactElement => { name={'from'} >