diff --git a/packages/neuron-ui/src/components/DataSetting/hooks.ts b/packages/neuron-ui/src/components/DataSetting/hooks.ts index 57b0ff0680..586ea9858f 100644 --- a/packages/neuron-ui/src/components/DataSetting/hooks.ts +++ b/packages/neuron-ui/src/components/DataSetting/hooks.ts @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { getCkbNodeDataPath, @@ -7,11 +7,11 @@ import { stopProcessMonitor, setCkbNodeDataPath, } from 'services/remote' -import { isSuccessResponse, useDidMount } from 'utils' +import { isSuccessResponse } from 'utils' const type = 'ckb' -export const useDataPath = () => { +export const useDataPath = (network?: State.Network) => { const [t] = useTranslation() const [isSaving, setIsSaving] = useState(false) const [savingType, setSavingType] = useState() @@ -20,13 +20,13 @@ export const useDataPath = () => { const [isDialogOpen, setIsDialogOpen] = useState(false) const [faidMessage, setFaidMessage] = useState('') - useDidMount(() => { + useEffect(() => { getCkbNodeDataPath().then(res => { if (isSuccessResponse(res)) { setPrevPath(res.result!) } }) - }) + }, [network?.id]) const onSetting = useCallback(() => { invokeShowOpenDialog({ buttonLabel: t('settings.data.set', { lng: navigator.language }), diff --git a/packages/neuron-ui/src/components/DataSetting/index.tsx b/packages/neuron-ui/src/components/DataSetting/index.tsx index 7e83d99684..4b3d9709c6 100644 --- a/packages/neuron-ui/src/components/DataSetting/index.tsx +++ b/packages/neuron-ui/src/components/DataSetting/index.tsx @@ -1,15 +1,13 @@ -import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react' +import React, { useCallback, useRef, useMemo } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import ClearCache from 'components/ClearCache' import { useDispatch, useState as useGlobalState } from 'states' import { shell } from 'electron' -import { getIsCkbRunExternal } from 'services/remote' -import { isSuccessResponse } from 'utils' import Tooltip from 'widgets/Tooltip' import Dialog from 'widgets/Dialog' import AlertDialog from 'widgets/AlertDialog' -import { LIGHT_NETWORK_TYPE } from 'utils/const' +import { NetworkType } from 'utils/const' import { Attention } from 'widgets/Icons/icon' import { useDataPath } from './hooks' @@ -42,6 +40,12 @@ const PathItem = ({ const DataSetting = () => { const dispatch = useDispatch() const [t] = useTranslation() + const resyncRef = useRef(null) + const { + chain: { networkID }, + settings: { networks = [] }, + } = useGlobalState() + const network = useMemo(() => networks.find(n => n.id === networkID), [networkID, networks]) const { onSetting, prevPath, @@ -53,38 +57,20 @@ const DataSetting = () => { savingType, faidMessage, setFaidMessage, - } = useDataPath() - - const resyncRef = useRef(null) + } = useDataPath(network) const openPath = useCallback(() => { if (prevPath) { shell.openPath(prevPath!) } }, [prevPath]) - const [isCkbRunExternal, setIsCkbRunExternal] = useState() - useEffect(() => { - getIsCkbRunExternal().then(res => { - if (isSuccessResponse(res)) { - setIsCkbRunExternal(res.result ?? false) - } else { - // ignore - } - }) - }, []) - const { - chain: { networkID }, - settings: { networks = [] }, - } = useGlobalState() - const isLightClient = useMemo( - () => networks.find(n => n.id === networkID)?.type === LIGHT_NETWORK_TYPE, - [networkID, networks] - ) + const isLightClient = network?.type === NetworkType.Light + const hiddenDataPath = isLightClient || !network?.readonly return ( <>
- {isLightClient ? null : ( + {hiddenDataPath ? null : (
{t('settings.data.ckb-node-data')}
{
- {isLightClient ? null : ( - - )} + {hiddenDataPath ? null : } { const [t] = useTranslation() @@ -33,7 +33,7 @@ const HDWalletSign = ({ tx }: { tx: State.DetailedTransaction }) => { throw new Error('Cannot find current network in the network list') } - setIsMainnet(network.chain === MAINNET_TAG) + setIsMainnet(MAINNET_CLIENT_LIST.includes(network.chain)) } }) .catch(err => console.warn(err)) diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx index dd7da563f3..8e7297a42f 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx +++ b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx @@ -16,18 +16,16 @@ import Tooltip from 'widgets/Tooltip' import { ErrorCode, - CONSTANTS, localNumberFormatter, uniformTimeFormatter, shannonToCKBFormatter, isSuccessResponse, + isMainnet as isMainnetUtil, } from 'utils' import { HIDE_BALANCE } from 'utils/const' import styles from './historyDetailPage.module.scss' -const { MAINNET_TAG } = CONSTANTS - type InputOrOutputType = (State.DetailedInput | State.DetailedOutput) & { idx: number } const InfoItem = ({ label, value, className }: { label: string; value: React.ReactNode; className?: string }) => ( @@ -46,8 +44,7 @@ const HistoryDetailPage = () => { settings: { networks }, wallet: currentWallet, } = useGlobalState() - const network = networks.find(n => n.id === networkID) - const isMainnet = network != null && network.chain === MAINNET_TAG + const isMainnet = isMainnetUtil(networks, networkID) const [t] = useTranslation() const [transaction, setTransaction] = useState(transactionState) const [error, setError] = useState({ code: '', message: '' }) diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index 01119e2767..e8f61a250c 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -26,7 +26,7 @@ import { ReactComponent as Transfer } from 'widgets/Icons/Transfer.svg' import { ReactComponent as Upload } from 'widgets/Icons/Upload.svg' import { ReactComponent as Edit } from 'widgets/Icons/Edit.svg' import { Download, Search } from 'widgets/Icons/icon' -import { HIDE_BALANCE, LIGHT_NETWORK_TYPE } from 'utils/const' +import { HIDE_BALANCE, NetworkType } from 'utils/const' import { onEnter } from 'utils/inputDevice' import { useSearch, useConfigManage, useExportConfig, useActions, useSubscription } from './hooks' @@ -62,7 +62,7 @@ const MultisigAddress = () => { } = useGlobalState() const isMainnet = isMainnetUtil(networks, networkID) const isLightClient = useMemo( - () => networks.find(n => n.id === networkID)?.type === LIGHT_NETWORK_TYPE, + () => networks.find(n => n.id === networkID)?.type === NetworkType.Light, [networks, networkID] ) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) diff --git a/packages/neuron-ui/src/components/NervosDAODetail/CellsCard/index.tsx b/packages/neuron-ui/src/components/NervosDAODetail/CellsCard/index.tsx index 3ab5e8df22..d6f8041f8c 100644 --- a/packages/neuron-ui/src/components/NervosDAODetail/CellsCard/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAODetail/CellsCard/index.tsx @@ -1,11 +1,11 @@ import React, { FC, useMemo, useState } from 'react' import Tabs, { VariantProps } from 'widgets/Tabs' -import { clsx, localNumberFormatter, shannonToCKBFormatter } from 'utils' +import { clsx, localNumberFormatter, shannonToCKBFormatter, isMainnet as isMainnetUtils } from 'utils' import { useTranslation } from 'react-i18next' import { scriptToAddress } from '@nervosnetwork/ckb-sdk-utils' import { onEnter } from 'utils/inputDevice' import { EyesClose, EyesOpen } from 'widgets/Icons/icon' -import { HIDE_BALANCE, MAINNET_TAG } from 'utils/const' +import { HIDE_BALANCE } from 'utils/const' import ScriptTag from 'components/ScriptTag' import LockInfoDialog from 'components/LockInfoDialog' import { useState as useGlobalState } from 'states' @@ -24,8 +24,7 @@ const TabsVariantWithCellsCard: FC< chain: { networkID }, settings: { networks }, } = useGlobalState() - const network = networks.find(n => n.id === networkID) - const isMainnet = network != null && network.chain === MAINNET_TAG + const isMainnet = isMainnetUtils(networks, networkID) const [isPrivacyMode, setIsPrivacyMode] = useState(false) const [showingLockInfo, setShowingLockInfo] = useState(null) diff --git a/packages/neuron-ui/src/components/NetworkEditorDialog/index.tsx b/packages/neuron-ui/src/components/NetworkEditorDialog/index.tsx index 83dd80c5f9..a636923b7e 100644 --- a/packages/neuron-ui/src/components/NetworkEditorDialog/index.tsx +++ b/packages/neuron-ui/src/components/NetworkEditorDialog/index.tsx @@ -12,10 +12,12 @@ const NetworkEditorDialog = ({ onCancel, id, onSuccess, + url, }: { onCancel: () => void id: 'new' | string onSuccess: () => void + url?: string }) => { const { settings: { networks = [] }, @@ -31,7 +33,7 @@ const NetworkEditorDialog = ({ const [editor, setEditor] = useState({ name: '', nameError: '', - url: '', + url: url ?? '', urlError: '', }) const [isUpdating, setIsUpdating] = useState(false) @@ -115,6 +117,7 @@ const NetworkEditorDialog = ({ error={editor.urlError} placeholder={t('settings.network.edit-network.input-rpc')} autoFocus + disabled={!!url} /> { @@ -43,7 +46,6 @@ const NetworkSetting = ({ chain = chainState, settings: { networks = [] } }: Sta break } default: - // @ts-ignore } }, [setNetId, setShowEditorDialog, setShowDeleteDialog] @@ -75,20 +77,55 @@ const NetworkSetting = ({ chain = chainState, settings: { networks = [] } }: Sta } }, [setShowEditorDialog, setNotice, netId]) + const onSwitchNetworkType = useCallback>(() => { + const selectedNetwork = networks.find(v => v.id === currentId) + const switchNetwork = networks.find(v => v.type === selectedNetwork?.type && v.id !== currentId) + if (switchNetwork) { + setCurrentNetwork(switchNetwork.id) + lastShowInternalNodeIds.save(switchNetwork.type, switchNetwork.id) + } + }, [currentId, networks]) + + const showNetworks = useMemo(() => { + const internalLightNodeId = lastShowInternalNodeIds.get(NetworkType.Light) + return networks.filter(v => v.type !== NetworkType.Light || v.id === internalLightNodeId) + }, [currentId, networks]) + return (
({ + options={showNetworks.map(network => ({ value: network.id, - label: ( -
-

{`${network.name} (${network.remote})`}

-
{t(getNetworkLabelI18nkey(network.chain))}
-
- ), + label: + currentId === network.id && network.type === NetworkType.Light ? ( +
+

{`${network.name} (${network.remote})`}

+ + {t('settings.network.switch-network-type', { + type: network.chain === LIGHT_CLIENT_MAINNET ? 'testnet' : 'mainnet', + })} + + } + placement="top" + showTriangle + > +
+ {t(getNetworkLabelI18nkey(network.chain))} + +
+
+
+ ) : ( +
+

{`${network.name} (${network.remote})`}

+
{t(getNetworkLabelI18nkey(network.chain))}
+
+ ), suffix: (
{network.readonly ? null : ( diff --git a/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss b/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss index 3fbdf69edd..25024fd5ec 100644 --- a/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss +++ b/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss @@ -25,6 +25,18 @@ background: var(--table-head-background-color); border: 1px solid var(--divide-line-color); border-radius: 40px; + display: flex; + align-items: center; + + & > svg { + margin-left: 4px; + } + } + .switchBtn { + border: none; + background-color: transparent; + cursor: pointer; + color: var(--main-text-color); } } diff --git a/packages/neuron-ui/src/components/PageContainer/index.tsx b/packages/neuron-ui/src/components/PageContainer/index.tsx index 6237ba4ad4..b90cbc6575 100755 --- a/packages/neuron-ui/src/components/PageContainer/index.tsx +++ b/packages/neuron-ui/src/components/PageContainer/index.tsx @@ -15,7 +15,7 @@ import SyncStatusComponent from 'components/SyncStatus' import { AppActions, useDispatch, useState as useGlobalState } from 'states' import { openExternal } from 'services/remote' import Tooltip from 'widgets/Tooltip' -import { LIGHT_NETWORK_TYPE } from 'utils/const' +import { NetworkType } from 'utils/const' import Dialog from 'widgets/Dialog' import TextField from 'widgets/TextField' @@ -59,7 +59,7 @@ const PageContainer: React.FC = props => { const { theme, onSetTheme } = useTheme() const network = useMemo(() => networks.find(n => n.id === networkID), [networks, networkID]) const isLightClient = useMemo( - () => networks.find(n => n.id === networkID)?.type === LIGHT_NETWORK_TYPE, + () => networks.find(n => n.id === networkID)?.type === NetworkType.Light, [networkID, networks] ) const netWorkTypeLabel = useMemo(() => (network ? getNetworkLabelI18nkey(network.chain) : ''), [network]) diff --git a/packages/neuron-ui/src/components/Receive/hooks.ts b/packages/neuron-ui/src/components/Receive/hooks.ts index 5b2ddcbde1..416b695b4b 100644 --- a/packages/neuron-ui/src/components/Receive/hooks.ts +++ b/packages/neuron-ui/src/components/Receive/hooks.ts @@ -32,7 +32,7 @@ export const useCopyAndDownloadQrCode = () => { const toShortAddr = (addr: string) => { try { const script = addressToScript(addr) - const isMainnet = addr.startsWith('ckb') + const isMainnet = addr.startsWith(AddressPrefix.Mainnet) return bech32Address(script.args, { prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet }) } catch { return '' diff --git a/packages/neuron-ui/src/components/SUDTReceiveDialog/index.tsx b/packages/neuron-ui/src/components/SUDTReceiveDialog/index.tsx index ac2dddda05..371e2db970 100644 --- a/packages/neuron-ui/src/components/SUDTReceiveDialog/index.tsx +++ b/packages/neuron-ui/src/components/SUDTReceiveDialog/index.tsx @@ -16,7 +16,7 @@ const { DEFAULT_SUDT_FIELDS } = CONSTANTS const toShortAddr = (addr: string) => { try { const script = addressToScript(addr) - const isMainnet = addr.startsWith('ckb') + const isMainnet = addr.startsWith(AddressPrefix.Mainnet) return bech32Address(script.args, { prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, codeHashOrCodeHashIndex: '0x02', diff --git a/packages/neuron-ui/src/components/SpecialAssetList/index.tsx b/packages/neuron-ui/src/components/SpecialAssetList/index.tsx index 9b4c804427..136c5e43cb 100644 --- a/packages/neuron-ui/src/components/SpecialAssetList/index.tsx +++ b/packages/neuron-ui/src/components/SpecialAssetList/index.tsx @@ -26,7 +26,7 @@ import { getExplorerUrl, ConnectionStatus, } from 'utils' -import { LIGHT_NETWORK_TYPE, HIDE_BALANCE } from 'utils/const' +import { NetworkType, HIDE_BALANCE } from 'utils/const' import useGetCountDownAndFeeRateStats from 'utils/hooks/useGetCountDownAndFeeRateStats' import { ControllerResponse } from 'services/remote/remoteApiWrapper' import SUDTUpdateDialog, { SUDTUpdateDialogProps } from 'components/SUDTUpdateDialog' @@ -146,7 +146,7 @@ const SpecialAssetList = () => { const { suggestFeeRate } = useGetCountDownAndFeeRateStats() const isMainnet = isMainnetUtil(networks, networkID) const isLightClient = useMemo( - () => networks.find(n => n.id === networkID)?.type === LIGHT_NETWORK_TYPE, + () => networks.find(n => n.id === networkID)?.type === NetworkType.Light, [networkID, networks] ) const foundTokenInfo = tokenInfoList.find(token => token.tokenID === accountToClaim?.account.tokenID) diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts index 2049222fdd..b332e2ca9d 100644 --- a/packages/neuron-ui/src/containers/Main/hooks.ts +++ b/packages/neuron-ui/src/containers/Main/hooks.ts @@ -1,5 +1,5 @@ -import { useEffect, useCallback } from 'react' -import { useLocation, NavigateFunction } from 'react-router-dom' +import { useEffect, useCallback, useState } from 'react' +import { useLocation, NavigateFunction, useNavigate } from 'react-router-dom' import { NeuronWalletActions, StateDispatch, AppActions } from 'states/stateProvider/reducer' import { updateTransactionList, @@ -10,7 +10,7 @@ import { showGlobalAlertDialog, } from 'states/stateProvider/actionCreators' -import { getCurrentWallet, getWinID } from 'services/remote' +import { getCurrentWallet, getWinID, setCurrentNetwork, startNodeIgnoreExternal } from 'services/remote' import { DataUpdate as DataUpdateSubject, NetworkList as NetworkListSubject, @@ -105,6 +105,7 @@ export const useSubscription = ({ navigate, dispatch, location, + showSwitchNetwork, }: { walletID: string chain: State.Chain @@ -112,6 +113,7 @@ export const useSubscription = ({ navigate: NavigateFunction location: ReturnType dispatch: StateDispatch + showSwitchNetwork: () => void }) => { const { pageNo, pageSize, keywords } = chain.transactions @@ -203,6 +205,9 @@ export const useSubscription = ({ type: NeuronWalletActions.UpdateConnectionStatus, payload: getConnectionStatus({ ...status, isTimeout: Date.now() > CONNECTING_DEADLINE }), }) + if (status.connected && status.isBundledNode && !status.startedBundledNode) { + showSwitchNetwork() + } } }) @@ -308,7 +313,67 @@ export const useSubscription = ({ commandSubscription.unsubscribe() showGlobalDialogSubject.unsubscribe() } - }, [walletID, pageNo, pageSize, keywords, isAllowedToFetchList, navigate, dispatch, location.pathname]) + }, [ + walletID, + pageNo, + pageSize, + keywords, + isAllowedToFetchList, + navigate, + dispatch, + location.pathname, + showSwitchNetwork, + ]) +} + +export const useCheckNode = (networks: State.Network[], networkID: string) => { + const [isSwitchNetworkShow, setIsSwitchNetworkShow] = useState(false) + const [selectNetwork, setSelectNetwork] = useState(networks[0]?.id) + useEffect(() => { + if (isSwitchNetworkShow) { + setSelectNetwork(networks[0]?.id) + } + }, [networks, isSwitchNetworkShow]) + const [showEditorDialog, setShowEditorDialog] = useState(false) + const onConfirm = useCallback(() => { + if (selectNetwork) { + setCurrentNetwork(selectNetwork) + setIsSwitchNetworkShow(false) + } + }, [selectNetwork]) + const onCloseEditorDialog = useCallback(() => { + setShowEditorDialog(false) + }, []) + const onOpenEditorDialog = useCallback(() => { + setShowEditorDialog(true) + }, []) + const navigate = useNavigate() + const [networkIdWhenDialogShow, setNetworkIdWhenDialogShow] = useState() + useEffect(() => { + setNetworkIdWhenDialogShow(undefined) + }, [networkID]) + const showSwitchNetwork = useCallback(() => { + // if the use has not change network id, the dialog will only show once + if (networkIdWhenDialogShow !== networkID) { + setNetworkIdWhenDialogShow(networkID) + navigate(RoutePath.Settings) + setIsSwitchNetworkShow(true) + } + }, [networkID, networkIdWhenDialogShow]) + return { + selectNetwork, + onChangeSelected: setSelectNetwork, + isSwitchNetworkShow, + showSwitchNetwork, + onCancel: useCallback(() => { + setIsSwitchNetworkShow(false) + startNodeIgnoreExternal() + }, []), + onConfirm, + showEditorDialog, + onCloseEditorDialog, + onOpenEditorDialog, + } } export default { diff --git a/packages/neuron-ui/src/containers/Main/index.tsx b/packages/neuron-ui/src/containers/Main/index.tsx index 7d77ae8242..9d60f521aa 100644 --- a/packages/neuron-ui/src/containers/Main/index.tsx +++ b/packages/neuron-ui/src/containers/Main/index.tsx @@ -6,7 +6,11 @@ import { useMigrate, useOnDefaultContextMenu, useOnLocaleChange } from 'utils' import AlertDialog from 'widgets/AlertDialog' import Dialog from 'widgets/Dialog' import Button from 'widgets/Button' -import { useSubscription, useSyncChainData, useOnCurrentWalletChange } from './hooks' +import RadioGroup from 'widgets/RadioGroup' +import NetworkEditorDialog from 'components/NetworkEditorDialog' +import { AddSimple } from 'widgets/Icons/icon' +import styles from './main.module.scss' +import { useSubscription, useSyncChainData, useOnCurrentWalletChange, useCheckNode } from './hooks' const MainContent = () => { const navigate = useNavigate() @@ -21,6 +25,29 @@ const MainContent = () => { const { networkID } = chain const [t, i18n] = useTranslation() + const network = useMemo(() => networks.find(n => n.id === networkID), [networks, networkID]) + + const sameUrlNetworks = useMemo( + () => networks.filter(v => v.remote === network?.remote && !v.readonly), + [network, networks] + ) + + useSyncChainData({ + chainURL: network?.remote ?? '', + dispatch, + }) + + const { + isSwitchNetworkShow, + showSwitchNetwork, + onCancel: onCloseSwitchNetwork, + onConfirm: onSwitchNetwork, + onChangeSelected, + showEditorDialog, + onCloseEditorDialog, + onOpenEditorDialog, + } = useCheckNode(sameUrlNetworks, networkID) + useSubscription({ walletID, chain, @@ -28,16 +55,7 @@ const MainContent = () => { navigate, dispatch, location, - }) - - const chainURL = useMemo(() => { - const network = networks.find(n => n.id === networkID) - return network ? network.remote : '' - }, [networks, networkID]) - - useSyncChainData({ - chainURL, - dispatch, + showSwitchNetwork, }) useOnCurrentWalletChange({ @@ -76,6 +94,51 @@ const MainContent = () => {
+ + {sameUrlNetworks.length ? ( + + {t('main.external-node-detected-dialog.body-tips-with-network')} + + ) : ( + t('main.external-node-detected-dialog.body-tips-without-network') + )} + {sameUrlNetworks.length ? ( + <> +
+ ({ + value: v.id, + label: `${v.name} (${v.remote})`, + }))} + inputIdPrefix="main-switch" + /> +
+
+ +
+ + ) : null} +
+ {showEditorDialog ? ( + + ) : null}
) } diff --git a/packages/neuron-ui/src/containers/Main/main.module.scss b/packages/neuron-ui/src/containers/Main/main.module.scss new file mode 100644 index 0000000000..0e01fceaa4 --- /dev/null +++ b/packages/neuron-ui/src/containers/Main/main.module.scss @@ -0,0 +1,27 @@ +.networkDialog { + width: 680px; + + .chooseNetworkTip { + color: var(--secondary-text-color); + } + + .networks { + max-height: 124px; + border-radius: 8px; + margin-top: 16px; + border: 1px solid var(--divide-line-color); + overflow-y: auto; + } + + .addNetwork { + text-align: center; + + & > button { + font-weight: 400; + } + + svg { + margin-right: 4px; + } + } +} diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx index 8020a12b53..6c38bdbfe7 100644 --- a/packages/neuron-ui/src/containers/Navbar/index.tsx +++ b/packages/neuron-ui/src/containers/Navbar/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { useLocation, NavLink, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' @@ -63,8 +63,9 @@ const Navbar = () => { const dispatch = useDispatch() const neuronWallet = useGlobalState() const { + chain: { networkID }, wallet: { name }, - settings: { wallets = [] }, + settings: { wallets = [], networks = [] }, updater: { version, isUpdated }, } = neuronWallet const [t, i18n] = useTranslation() @@ -95,13 +96,16 @@ const Navbar = () => { const [verifyCkbResult, setVerifyCkbResult] = useState() + const network = useMemo(() => networks.find(n => n.id === networkID), [networkID, networks]) useEffect(() => { - verifyExternalCkbNode().then(res => { - if (isSuccessResponse(res) && res.result) { - setVerifyCkbResult(res.result) - } - }) - }, []) + if (!network?.readonly) { + verifyExternalCkbNode().then(res => { + if (isSuccessResponse(res) && res.result) { + setVerifyCkbResult(res.result) + } + }) + } + }, [network?.readonly]) useEffect(() => { // isUpdated is true or version is not empty means check update has return diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 4878419ed2..a60c6dee05 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -400,7 +400,9 @@ "mainnet": "Mainnet", "testnet": "Testnet", "lightTestnet": "Light Testnet", - "devnet": "Devnet" + "lightMainnet": "Light Mainnet", + "devnet": "Devnet", + "switch-network-type": "Switch to {{type}}" }, "locale": { "en": "English", @@ -1137,6 +1139,15 @@ "input-place-holder": "Please enter the start sync block number, eg: 10,100,101", "locate-first-tx": "Locate the first transaction", "view-block": "View the block" + }, + "main": { + "external-node-detected-dialog": { + "title": "External node detected", + "body-tips-without-network": "\"Internal Node\" is reserved for the built-in CKB node, please add a new network option for the external node.", + "body-tips-with-network": "\"Internal Node\" is reserved for the built-in CKB node, please select from the following network options or add a new one for the external node.", + "add-network": "Add Network", + "ignore-external-node": "Ignore external node" + } } } } diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index a1f2bde2c7..b8dc8731cf 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -394,7 +394,9 @@ "edit-success": "編輯網絡成功", "mainnet": "主網", "testnet": "測試網", - "devnet": "開發網" + "lightMainnet": "輕節點主網", + "devnet": "開發網", + "switch-network-type": "切換到 {{type}}" }, "locale": { "en": "English", @@ -1108,6 +1110,15 @@ "input-place-holder": "輸入同步起始區塊高度,例如:10,100,101", "locate-first-tx": "定位第一筆交易", "view-block": "查看區塊" + }, + "main": { + "external-node-detected-dialog": { + "title": "檢測到外部節點", + "body-tips-without-network": "\"Internal Node\" 僅用於連接內置節點,請添加新的網絡選項以連接外部節點。", + "body-tips-with-network": "\"Internal Node\" 僅用於連接內置節點,請選擇以下網絡選項或添加新的網絡選項以連接外部節點。", + "add-network": "添加網絡", + "ignore-external-node": "忽略外部節點" + } } } } diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 67d31df813..766a89bd10 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -393,7 +393,9 @@ "mainnet": "主网", "testnet": "测试网", "lightTestnet": "轻节点测试网", - "devnet": "开发网" + "lightMainnet": "轻节点主网", + "devnet": "开发网", + "switch-network-type": "切换到 {{type}}" }, "locale": { "en": "English", @@ -1129,6 +1131,15 @@ "input-place-holder": "输入同步起始区块高度,例如:10,100,101", "locate-first-tx": "定位第一笔交易", "view-block": "查看区块" + }, + "main": { + "external-node-detected-dialog": { + "title": "检测到外部节点", + "body-tips-without-network": "\"Internal Node\" 仅用于连接内置节点,请添加新的网络选项以连接外部节点。", + "body-tips-with-network": "\"Internal Node\" 仅用于连接内置节点,请选择以下网络选项或添加新的网络选项以连接外部节点。", + "add-network": "添加网络", + "ignore-external-node": "忽略外部节点" + } } } } diff --git a/packages/neuron-ui/src/services/localCache.ts b/packages/neuron-ui/src/services/localCache.ts index d134ce4934..02ae5b823e 100644 --- a/packages/neuron-ui/src/services/localCache.ts +++ b/packages/neuron-ui/src/services/localCache.ts @@ -1,4 +1,4 @@ -import { SYNC_REBUILD_SINCE_VERSION } from 'utils/const' +import { NetworkType, SYNC_REBUILD_SINCE_VERSION } from 'utils/const' export enum LocalCacheKey { Addresses = 'addresses', @@ -10,6 +10,7 @@ export enum LocalCacheKey { SyncRebuildNotification = 'syncRebuildNotification', LoadedWalletIDs = 'loadedWalletIDs', ImportedWallet = 'ImportedWallet', + ShownNodeId = 'ShownNodeId', } export const addresses = { @@ -154,3 +155,13 @@ export const importedWalletDialogShown = { } }, } + +export const lastShowInternalNodeIds = { + get: (type: NetworkType) => { + const savedNodeId = window.localStorage.getItem(`${type}_${LocalCacheKey.ShownNodeId}`) + return savedNodeId ?? networks.load().find(v => v.type === type)?.id + }, + save: (type: NetworkType, id: string) => { + window.localStorage.setItem(`${type}_${LocalCacheKey.ShownNodeId}`, id) + }, +} diff --git a/packages/neuron-ui/src/services/remote/app.ts b/packages/neuron-ui/src/services/remote/app.ts index 65e69fa9ca..5a0c65ae4a 100644 --- a/packages/neuron-ui/src/services/remote/app.ts +++ b/packages/neuron-ui/src/services/remote/app.ts @@ -22,7 +22,7 @@ export const setCkbNodeDataPath = remoteApi<{ dataPath: string; clearCache?: boo ) export const stopProcessMonitor = remoteApi<'ckb'>('stop-process-monitor') export const startProcessMonitor = remoteApi<'ckb'>('start-process-monitor') -export const getIsCkbRunExternal = remoteApi('is-ckb-run-external') +export const startNodeIgnoreExternal = remoteApi('start-node-ignore-external') export type VerifyExternalCkbNodeRes = | { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index d8f798b382..2bb198db01 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -48,8 +48,8 @@ type Action = | 'start-process-monitor' | 'is-dark' | 'set-theme' - | 'is-ckb-run-external' | 'verify-external-ckb-node' + | 'start-node-ignore-external' // Wallets | 'get-all-wallets' | 'get-current-wallet' diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts index bca989ac43..dd63438b57 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts @@ -9,6 +9,7 @@ import { currentWallet as currentWalletCache, currentNetworkID as currentNetworkIDCache, networks as networksCache, + lastShowInternalNodeIds, } from 'services/localCache' export const initAppState = () => (dispatch: StateDispatch, navigate: any) => { @@ -48,6 +49,10 @@ export const initAppState = () => (dispatch: StateDispatch, navigate: any) => { addressesCache.save(addresses) networksCache.save(networks) currentNetworkIDCache.save(currentNetworkID) + const currentNetwork = (networks as State.Network[]).find(v => v.id === currentNetworkID) + if (currentNetwork) { + lastShowInternalNodeIds.save(currentNetwork.type, currentNetworkID) + } } else { navigate(`${RoutePath.WalletWizard}${WalletWizardPath.Welcome}`) } diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index f9473e036b..043f51e3d1 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -175,7 +175,7 @@ declare namespace State { name: string remote: string chain: 'ckb' | 'ckb_testnet' | 'ckb_dev' | string - type: 0 | 1 | 2 + type: 0 | 1 | 2 // 0 for default node, 1 for full node, 2 for light client, ref: NetworkType in utils/const.ts genesisHash: string readonly: boolean } diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index abd90e982b..238c4be8b8 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -13,8 +13,10 @@ export const MAX_TIP_BLOCK_DELAY = 180000 export const BUFFER_BLOCK_NUMBER = 10 export const MAX_DECIMAL_DIGITS = 8 -export const MAINNET_TAG = 'ckb' -export const LIGHT_MAINNET_TAG = 'light_client_mainnet' +export const FULL_NODE_MAINNET = 'ckb' +export const LIGHT_CLIENT_MAINNET = 'light_client_mainnet' +export const LIGHT_CLIENT_TESTNET = 'light_client_testnet' +export const MAINNET_CLIENT_LIST = [FULL_NODE_MAINNET, LIGHT_CLIENT_MAINNET] export const MIN_DEPOSIT_AMOUNT = 102 export const TOKEN_ID_LENGTH = 66 @@ -67,8 +69,11 @@ export const DEPRECATED_CODE_HASH: Record = { } export const HIDE_BALANCE = '******' -export const LIGHT_CLIENT_TESTNET = 'light_client_testnet' -export const LIGHT_NETWORK_TYPE = 2 +export enum NetworkType { + Default, // internal full node + Normal, + Light, // internal Light node +} export const METHOD_NOT_FOUND = -32601 export const MAX_M_N_NUMBER = 255 diff --git a/packages/neuron-ui/src/utils/getNetworkLabel.ts b/packages/neuron-ui/src/utils/getNetworkLabel.ts index 29ac0d5e29..461adcb7fd 100644 --- a/packages/neuron-ui/src/utils/getNetworkLabel.ts +++ b/packages/neuron-ui/src/utils/getNetworkLabel.ts @@ -1,6 +1,6 @@ -import { LIGHT_CLIENT_TESTNET } from './const' +import { LIGHT_CLIENT_MAINNET, LIGHT_CLIENT_TESTNET } from './const' -export const getNetworkLabelI18nkey = (type: 'ckb' | 'ckb_testnet' | 'ckb_dev' | string) => { +export const getNetworkLabelI18nkey = (type: State.Network['chain']) => { switch (type) { case 'ckb': { return 'settings.network.mainnet' @@ -11,6 +11,9 @@ export const getNetworkLabelI18nkey = (type: 'ckb' | 'ckb_testnet' | 'ckb_dev' | case LIGHT_CLIENT_TESTNET: { return 'settings.network.lightTestnet' } + case LIGHT_CLIENT_MAINNET: { + return 'settings.network.lightMainnet' + } default: { return 'settings.network.devnet' } diff --git a/packages/neuron-ui/src/utils/is.ts b/packages/neuron-ui/src/utils/is.ts index 1975ddf3ba..f20ca58fad 100644 --- a/packages/neuron-ui/src/utils/is.ts +++ b/packages/neuron-ui/src/utils/is.ts @@ -8,11 +8,11 @@ import { PwAcpLockInfoOnMainNet, PwAcpLockInfoOnTestNet, } from 'utils/enums' -import { LIGHT_MAINNET_TAG, MAINNET_TAG } from './const' +import { MAINNET_CLIENT_LIST } from './const' export const isMainnet = (networks: Readonly, networkID: string) => { const network = networks.find(n => n.id === networkID) - return network?.chain === MAINNET_TAG || network?.chain === LIGHT_MAINNET_TAG + return MAINNET_CLIENT_LIST.includes(network?.chain ?? '') } export const isSuccessResponse = (res: Pick): res is SuccessFromController => { diff --git a/packages/neuron-ui/src/widgets/Icons/Switch.svg b/packages/neuron-ui/src/widgets/Icons/Switch.svg new file mode 100644 index 0000000000..d998334223 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/Switch.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/neuron-ui/src/widgets/Icons/icon.tsx b/packages/neuron-ui/src/widgets/Icons/icon.tsx index b05e00b7cf..542cb2843d 100644 --- a/packages/neuron-ui/src/widgets/Icons/icon.tsx +++ b/packages/neuron-ui/src/widgets/Icons/icon.tsx @@ -44,6 +44,8 @@ import { ReactComponent as OverviewSendSvg } from './OverviewSend.svg' import { ReactComponent as OverviewReceiveSvg } from './OverviewReceive.svg' import { ReactComponent as AddressbookSvg } from './Addressbook.svg' import { ReactComponent as AddSvg } from './Add.svg' +import { ReactComponent as AddSimpleSvg } from './AddSimple.svg' +import { ReactComponent as SwitchSvg } from './Switch.svg' import styles from './icon.module.scss' @@ -104,3 +106,5 @@ export const OverviewSend = WrapSvg(OverviewSendSvg) export const OverviewReceive = WrapSvg(OverviewReceiveSvg) export const Addressbook = WrapSvg(AddressbookSvg) export const Add = WrapSvg(AddSvg) +export const Switch = WrapSvg(SwitchSvg) +export const AddSimple = WrapSvg(AddSimpleSvg) diff --git a/packages/neuron-ui/src/widgets/RadioGroup/index.tsx b/packages/neuron-ui/src/widgets/RadioGroup/index.tsx index fb56c07757..1ca7197d1a 100644 --- a/packages/neuron-ui/src/widgets/RadioGroup/index.tsx +++ b/packages/neuron-ui/src/widgets/RadioGroup/index.tsx @@ -13,22 +13,32 @@ export interface RadioGroupProps { options: RadioGroupOptions[] onChange?: (arg: string | number) => void defaultValue?: string | number + value?: string | number itemClassName?: string className?: string + inputIdPrefix?: string } -const RadioGroup = ({ defaultValue, options, onChange, itemClassName = '', className = '' }: RadioGroupProps) => { - const [checkedValue, setCheckedValue] = useState(defaultValue || options[0]?.value) +const RadioGroup = ({ + defaultValue, + options, + onChange, + itemClassName = '', + className = '', + value, + inputIdPrefix = '', +}: RadioGroupProps) => { + const [checkedValue, setCheckedValue] = useState(defaultValue ?? options[0]?.value) const handleChange = useCallback( (e: React.SyntheticEvent) => { - const { value } = e.target as HTMLInputElement - if (value !== checkedValue) { - setCheckedValue(value) - onChange?.(value) + const { value: selectedValue } = e.target as HTMLInputElement + if (selectedValue !== value ?? checkedValue) { + setCheckedValue(selectedValue) + onChange?.(selectedValue) } }, - [onChange, checkedValue, setCheckedValue] + [onChange, checkedValue, setCheckedValue, value] ) return ( @@ -36,12 +46,12 @@ const RadioGroup = ({ defaultValue, options, onChange, itemClassName = '', class {options.map(item => (
-