diff --git a/packages/extension-web-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-web-ui/src/Popup/Transaction/variants/SendFund.tsx index b57b5312e6b..cde711abd90 100644 --- a/packages/extension-web-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-web-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -24,7 +24,7 @@ import { approveSpending, getMaxTransfer, getOptimalTransferProcess, makeCrossCh import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from '@subwallet/extension-web-ui/reducer'; import { RootState } from '@subwallet/extension-web-ui/stores'; import { ChainItemType, FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-web-ui/types'; -import { findAccountByAddress, formatBalance, noop, reformatAddress, transactionDefaultFilterAccount } from '@subwallet/extension-web-ui/utils'; +import { findAccountByAddress, formatBalance, ledgerMustCheckNetwork, noop, reformatAddress, transactionDefaultFilterAccount } from '@subwallet/extension-web-ui/utils'; import { findNetworkJsonByGenesisHash } from '@subwallet/extension-web-ui/utils/chain/getNetworkJsonByGenesisHash'; import { Button, Form, Icon } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; @@ -248,7 +248,7 @@ const _SendFund = ({ className = '', modalContent }: Props): React.ReactElement< const assetInfo = useFetchChainAssetInfo(asset); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); - const { chainInfoMap, chainStatusMap } = useSelector((root) => root.chainStore); + const { chainInfoMap, chainStatusMap, ledgerGenericAllowNetworks } = useSelector((root) => root.chainStore); const { assetRegistry, assetSettingMap, multiChainAssetMap, xcmRefMap } = useSelector((root) => root.assetRegistry); const { accounts, isAllAccount } = useSelector((state: RootState) => state.accountState); const [maxTransfer, setMaxTransfer] = useState('0'); @@ -389,10 +389,16 @@ const _SendFund = ({ className = '', modalContent }: Props): React.ReactElement< return Promise.reject(t('Wrong network. Your Ledger account is not supported by {{network}}. Please choose another receiving account and try again.', { replace: { network: destChainName } })); } + + const ledgerCheck = ledgerMustCheckNetwork(account); + + if (ledgerCheck !== 'unnecessary' && !ledgerGenericAllowNetworks.includes(destChainInfo.slug)) { + return Promise.reject(t('Ledger {{ledgerApp}} address is not supported for this transfer', { replace: { ledgerApp: ledgerCheck === 'polkadot' ? 'Polkadot' : 'Migration' } })); + } } return Promise.resolve(); - }, [accounts, chainInfoMap, form, t]); + }, [accounts, chainInfoMap, form, ledgerGenericAllowNetworks, t]); const validateAmount = useCallback((rule: Rule, amount: string): Promise => { if (!amount) { diff --git a/packages/extension-web-ui/src/components/Modal/ReceiveModal/TokensSelectorModal.tsx b/packages/extension-web-ui/src/components/Modal/ReceiveModal/TokensSelectorModal.tsx index 53edfe366e0..bf6ce395d8d 100644 --- a/packages/extension-web-ui/src/components/Modal/ReceiveModal/TokensSelectorModal.tsx +++ b/packages/extension-web-ui/src/components/Modal/ReceiveModal/TokensSelectorModal.tsx @@ -3,20 +3,17 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; +import { TokenEmptyList, TokenSelectionItem } from '@subwallet/extension-web-ui/components'; import { BaseModal } from '@subwallet/extension-web-ui/components/Modal/BaseModal'; -import { TokenSelectionItem } from '@subwallet/extension-web-ui/components/TokenItem/TokenSelectionItem'; -import { RECEIVE_QR_MODAL, RECEIVE_TOKEN_SELECTOR_MODAL } from '@subwallet/extension-web-ui/constants/modal'; -import { useSelector } from '@subwallet/extension-web-ui/hooks'; -import { useGetZkAddress } from '@subwallet/extension-web-ui/hooks/account/useGetZkAddress'; -import useTranslation from '@subwallet/extension-web-ui/hooks/common/useTranslation'; +import { RECEIVE_QR_MODAL, RECEIVE_TOKEN_SELECTOR_MODAL, WARNING_LEDGER_RECEIVE_MODAL } from '@subwallet/extension-web-ui/constants'; +import { useConfirmModal, useGetAccountByAddress, useGetZkAddress, useSelector, useTranslation } from '@subwallet/extension-web-ui/hooks'; import { ThemeProps } from '@subwallet/extension-web-ui/types'; -import { ModalContext, SwList } from '@subwallet/react-ui'; +import { ledgerMustCheckNetwork } from '@subwallet/extension-web-ui/utils'; +import { ModalContext, SwList, SwModalFuncProps } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; -import React, { useCallback, useContext, useEffect, useRef } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; -import { TokenEmptyList } from '../../EmptyList'; - interface Props extends ThemeProps { onSelectItem?: (item: _ChainAsset) => void, address?: string, @@ -33,8 +30,29 @@ function Component ({ address, className = '', items, onSelectItem }: Props): Re const zkAddress = useGetZkAddress(address); - const { chainInfoMap } = useSelector((state) => state.chainStore); + const { chainInfoMap, ledgerGenericAllowNetworks } = useSelector((state) => state.chainStore); + + const account = useGetAccountByAddress(address); + + const ledgerCheck = useMemo(() => ledgerMustCheckNetwork(account), [account]); + + const confirmModalProps = useMemo((): SwModalFuncProps => ({ + id: WARNING_LEDGER_RECEIVE_MODAL, + title: t('Unsupported network'), + maskClosable: true, + closable: true, + subTitle: t('Do you still want to get the address?'), + okText: t('Get address'), + okCancel: true, + type: 'warn', + cancelButtonProps: { + children: t('Cancel'), + schema: 'secondary' + }, + className: 'ledger-warning-modal' + }), [t]); + const { handleSimpleConfirmModal } = useConfirmModal(confirmModalProps); const isActive = checkActive(modalId); const sectionRef = useRef(null); @@ -56,12 +74,35 @@ function Component ({ address, className = '', items, onSelectItem }: Props): Re const onClickQrBtn = useCallback((item: _ChainAsset) => { return () => { + if (ledgerCheck !== 'unnecessary' && !ledgerGenericAllowNetworks.includes(item.originChain)) { + handleSimpleConfirmModal({ + content: t( + 'Ledger {{ledgerApp}} accounts are NOT compatible with {{networkName}} network. Tokens will get stuck (i.e., can’t be transferred out or staked) when sent to this account type.', + { + replace: { + ledgerApp: ledgerCheck === 'polkadot' ? 'Polkadot' : 'Migration', + networkName: chainInfoMap[item.originChain]?.name + } + } + ) + }) + .then(() => { + onSelectItem && onSelectItem(item); + // checkAsset(item.slug); + inactiveModal(modalId); + activeModal(RECEIVE_QR_MODAL); + }) + .catch(console.error); + + return; + } + onSelectItem && onSelectItem(item); // checkAsset(item.slug); inactiveModal(modalId); activeModal(RECEIVE_QR_MODAL); }; - }, [activeModal, inactiveModal, onSelectItem]); + }, [activeModal, chainInfoMap, handleSimpleConfirmModal, inactiveModal, ledgerCheck, ledgerGenericAllowNetworks, onSelectItem, t]); useEffect(() => { if (!isActive) { @@ -70,9 +111,24 @@ function Component ({ address, className = '', items, onSelectItem }: Props): Re }, 100); } }, [isActive]); - + const onPreCopy = useCallback((item: _ChainAsset, ledgerCheck: string) => { + return () => { + return handleSimpleConfirmModal({ + content: t( + 'Ledger {{ledgerApp}} accounts are NOT compatible with {{networkName}} network. Tokens will get stuck (i.e., can’t be transferred out or staked) when sent to this account type.', + { + replace: { + ledgerApp: ledgerCheck === 'polkadot' ? 'Polkadot' : 'Migration', + networkName: chainInfoMap[item.originChain]?.name + } + } + ) + }); + }; + }, [chainInfoMap, handleSimpleConfirmModal, t]); const renderItem = useCallback((item: _ChainAsset) => { const isMantaZkAsset = _MANTA_ZK_CHAIN_GROUP.includes(item.originChain) && item.symbol.startsWith(_ZK_ASSET_PREFIX); + const needConfirm = ledgerCheck !== 'unnecessary' && !ledgerGenericAllowNetworks.includes(item.originChain); return ( ); - }, [address, onClickQrBtn, zkAddress]); + }, [address, ledgerCheck, ledgerGenericAllowNetworks, onClickQrBtn, onPreCopy, zkAddress]); + + useEffect(() => { + if (!isActive) { + setTimeout(() => { + sectionRef.current?.setSearchValue(''); + }, 100); + } + }, [isActive]); return ( void; onClickQrBtn?: () => void; + onPreCopy?: () => Promise; } const Component = (props: Props) => { - const { address, className, item, onClickCopyBtn, onClickQrBtn, onPressItem, ...restProps } = props; + const { address, className, item, onClickCopyBtn, onClickQrBtn, onPreCopy, onPressItem, ...restProps } = props; const { name, originChain: chain, slug, symbol } = item; const chainInfo = useFetchChainInfo(chain || ''); const notify = useNotification(); @@ -42,8 +44,22 @@ const Component = (props: Props) => { return reformatAddress(address || '', networkPrefix, isEvmChain); }, [address, chainInfo, symbol]); + const _onClickCopyBtnAsync = useCallback((e: React.SyntheticEvent) => { + e.stopPropagation(); + onPreCopy?.() + .then(() => { + notify({ + message: t('Copied to clipboard') + }); + copyToClipboard(formattedAddress); + onClickCopyBtn && onClickCopyBtn(); + }) + .catch(noop); + }, [formattedAddress, notify, onClickCopyBtn, onPreCopy, t]); + const _onCLickCopyBtn = useCallback((e: React.SyntheticEvent) => { e.stopPropagation(); + notify({ message: t('Copied to clipboard') }); @@ -81,20 +97,39 @@ const Component = (props: Props) => { rightItem={ ( <> - -