From 1b9c660b8fad800cb6e9e80d2a16132b2a697b41 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 21 Sep 2026 17:35:16 +0800 Subject: [PATCH 01/11] Implement watch only account, #6920 --- .../components/common/tx/submitButton.jsx | 17 +++++ .../common/tx/txSubmissionButton.js | 36 ++++++---- .../components/common/tx/useTxSubmission.js | 9 +++ .../fellowship/applications/induct.js | 5 +- .../header/connectedAccountDisplay.jsx | 31 +++++++++ .../next-common/components/header/drawer.jsx | 11 ++- .../components/header/headerAccount.js | 5 +- .../hydrationCrossChainPopup/index.js | 6 +- .../components/login/web3/index.jsx | 5 ++ .../components/login/web3/watchOnly.jsx | 31 +++++++++ .../multisigs/actions/removePopup.jsx | 6 +- .../multisigs/actions/renamePopup.jsx | 6 +- .../signField/signApprovePopup/popup.jsx | 6 +- .../signField/signCancelPopup/popup.jsx | 6 +- .../myProxies/operations/removeProxy.jsx | 7 +- .../index.js | 7 ++ .../createPromotionReferendumAndVoteButton.js | 10 ++- .../createRetentionReferendumAndVoteButton.js | 10 ++- .../voteButtons/voteButton.js | 8 +++ .../components/gov2/votePopup/popupContent.js | 6 +- .../referenda/popup/popupContent.js | 5 +- .../paraChainTeleportPopup/index.js | 6 +- .../paraChainTeleportPopup/teleport.jsx | 10 +-- .../identity/directIdentityActions.jsx | 12 +++- .../judgements/cancelRequestJudgement.jsx | 7 +- .../components/popup/voteButton.js | 37 ++++++---- .../popupWithSigner/context/signer.js | 24 +++++-- .../popupWithSigner/maybeSignerConnected.js | 4 ++ packages/next-common/components/signer.js | 21 ++++-- .../components/signerPopup/index.js | 15 +++- .../staking/pools/actions/unbondingPopup.jsx | 6 +- .../components/switchAccount/index.jsx | 41 ++++++++--- .../components/switchAccountButton.js | 5 +- .../components/switchSignerPopup/index.js | 40 +++-------- .../components/terminateApplicationPopup.jsx | 6 +- .../wallet/options/substrate/singleSig.jsx | 14 ++++ .../components/wallet/watchOnly.jsx | 18 +++++ .../components/watchOnly/addressForm.jsx | 69 +++++++++++++++++++ .../next-common/components/watchOnly/hint.jsx | 18 +++++ .../components/watchOnly/tooltip.jsx | 29 ++++++++ .../context/connectedAccount/index.js | 6 ++ .../hooks/connect/useSubstrateWallets.js | 2 + .../hooks/connect/useWeb3WalletView.js | 3 + .../next-common/hooks/useAccountByAddress.js | 33 +++++++++ packages/next-common/hooks/useEnsureLogin.js | 9 +++ .../next-common/hooks/useSendTransaction.js | 9 +++ packages/next-common/hooks/useSignMessage.js | 8 +++ .../next-common/utils/consts/connect/index.js | 8 +++ .../consts/settings/collectives/index.js | 1 + .../utils/consts/settings/polkadot/index.js | 1 + .../next-common/utils/consts/walletTypes.js | 1 + packages/next-common/utils/watchOnly.js | 14 ++++ 52 files changed, 576 insertions(+), 134 deletions(-) create mode 100644 packages/next-common/components/common/tx/submitButton.jsx create mode 100644 packages/next-common/components/header/connectedAccountDisplay.jsx create mode 100644 packages/next-common/components/login/web3/watchOnly.jsx create mode 100644 packages/next-common/components/wallet/watchOnly.jsx create mode 100644 packages/next-common/components/watchOnly/addressForm.jsx create mode 100644 packages/next-common/components/watchOnly/hint.jsx create mode 100644 packages/next-common/components/watchOnly/tooltip.jsx create mode 100644 packages/next-common/hooks/useAccountByAddress.js create mode 100644 packages/next-common/utils/watchOnly.js diff --git a/packages/next-common/components/common/tx/submitButton.jsx b/packages/next-common/components/common/tx/submitButton.jsx new file mode 100644 index 0000000000..ea6691156b --- /dev/null +++ b/packages/next-common/components/common/tx/submitButton.jsx @@ -0,0 +1,17 @@ +import PrimaryButton from "next-common/lib/button/primary"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; + +// Primary button for tx submissions: disabled, with a hint tooltip, when the +// connected account is watch-only. +export default function SubmitButton({ disabled = false, children, ...props }) { + const isWatchOnly = useIsWatchOnly(); + + return ( + + + {children} + + + ); +} diff --git a/packages/next-common/components/common/tx/txSubmissionButton.js b/packages/next-common/components/common/tx/txSubmissionButton.js index c49e92fb1b..da71528b51 100644 --- a/packages/next-common/components/common/tx/txSubmissionButton.js +++ b/packages/next-common/components/common/tx/txSubmissionButton.js @@ -4,6 +4,8 @@ import PrimaryButton from "next-common/lib/button/primary"; import LoadingButton from "next-common/lib/button/loading"; import useTxSubmission from "./useTxSubmission"; import { usePopupOnClose } from "next-common/context/popup"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; export default function TxSubmissionButton({ api, @@ -21,6 +23,7 @@ export default function TxSubmissionButton({ autoClose = true, }) { const onClose = usePopupOnClose(); + const isWatchOnly = useIsWatchOnly(); const { isSubmitting, isWrapping, doSubmit } = useTxSubmission({ api, getTxFunc, @@ -44,13 +47,15 @@ export default function TxSubmissionButton({ {isLoading && loadingText ? ( {loadingText} ) : ( - - {title} - + + + {title} + + )} ); @@ -68,6 +73,7 @@ export function useTxSubmissionButton({ onTxError = noop, onTxStart = noop, }) { + const isWatchOnly = useIsWatchOnly(); const { isSubmitting, isWrapping, doSubmit } = useTxSubmission({ getTxFunc, onFinalized, @@ -86,13 +92,15 @@ export function useTxSubmissionButton({ {isSubmitting && loadingText ? ( {loadingText} ) : ( - - {title} - + + + {title} + + )} ), diff --git a/packages/next-common/components/common/tx/useTxSubmission.js b/packages/next-common/components/common/tx/useTxSubmission.js index c162198cd0..a38f8ed2cb 100644 --- a/packages/next-common/components/common/tx/useTxSubmission.js +++ b/packages/next-common/components/common/tx/useTxSubmission.js @@ -7,6 +7,8 @@ import { wrapTransaction } from "next-common/utils/sendTransaction"; import { useContextApi } from "next-common/context/api"; import { useSendTransaction } from "next-common/hooks/useSendTransaction"; import { useMaybeMultisigCallback } from "./useMaybeMultisigCallback"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function useTxSubmission({ api, @@ -22,6 +24,7 @@ export default function useTxSubmission({ const defaultApi = useContextApi(); const apiToUse = api || defaultApi; const signerAccount = useSignerAccount(); + const isWatchOnly = useIsWatchOnly(); const { sendTxFunc, isSubmitting } = useSendTransaction(); const { onInBlock: maybeMultisigOnInBlock, @@ -51,6 +54,11 @@ export default function useTxSubmission({ const doSubmit = useCallback( async (...args) => { + if (isWatchOnly) { + dispatch(newErrorToast(WATCH_ONLY_TOOLTIP_TEXT)); + return; + } + if (!apiToUse) { dispatch(newErrorToast("Chain RPC is not connected yet")); return; @@ -87,6 +95,7 @@ export default function useTxSubmission({ [ apiToUse, dispatch, + isWatchOnly, signerAccount, getTx, sendTxFunc, diff --git a/packages/next-common/components/fellowship/applications/induct.js b/packages/next-common/components/fellowship/applications/induct.js index 27d92d1495..22261c6d92 100644 --- a/packages/next-common/components/fellowship/applications/induct.js +++ b/packages/next-common/components/fellowship/applications/induct.js @@ -1,5 +1,6 @@ import { useCallback } from "react"; import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import { useCoreFellowshipPallet } from "next-common/context/collectives/collectives"; import Tooltip from "next-common/components/tooltip"; import { useFellowshipCanInductMember } from "next-common/hooks/fellowship/useFellowshipCanInductMember"; @@ -48,13 +49,13 @@ function InductButton({ address }) { = 3"} > - Induct - + ); diff --git a/packages/next-common/components/header/connectedAccountDisplay.jsx b/packages/next-common/components/header/connectedAccountDisplay.jsx new file mode 100644 index 0000000000..0e12c3a0dc --- /dev/null +++ b/packages/next-common/components/header/connectedAccountDisplay.jsx @@ -0,0 +1,31 @@ +import { SystemEye } from "@osn/icons/subsquare"; +import { cn } from "next-common/utils"; +import { AddressUser } from "next-common/components/user"; +import Tooltip from "next-common/components/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; + +// Connected account shown in the header. A watch-only account gets an eye badge +// on its avatar: it is absolutely positioned, so the display size is unchanged. +export default function ConnectedAccountDisplay({ address, className }) { + const isWatchOnly = useIsWatchOnly(); + const account = ; + + if (!isWatchOnly) { + return account; + } + + return ( + +
+ {account} + +
+
+ ); +} diff --git a/packages/next-common/components/header/drawer.jsx b/packages/next-common/components/header/drawer.jsx index 7ca5ce5886..91cc068325 100644 --- a/packages/next-common/components/header/drawer.jsx +++ b/packages/next-common/components/header/drawer.jsx @@ -15,7 +15,8 @@ import { walletConnect } from "next-common/utils/consts/connect/index.js"; import { useAccountMenu } from "./useAccountMenu"; import Divider from "next-common/components/styled/layout/divider"; import SwitchAccount from "next-common/components/switchAccount"; -import AddressUser from "next-common/components/user/addressUser"; +import ConnectedAccountDisplay from "./connectedAccountDisplay.jsx"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; import SearchInputWithPopup from "./searchInputWithPopup"; import { useWalletConnect } from "next-common/context/walletconnect"; import Loading from "next-common/components/loading"; @@ -66,14 +67,18 @@ function ProfileMenuItem({ onClick }) { } function ConnectedAccount({ user }) { + const isWatchOnly = useIsWatchOnly(); + if (!user?.address) { return null; } return (
- - Connected + + + {isWatchOnly ? "Watch-only" : "Connected"} +
); } diff --git a/packages/next-common/components/header/headerAccount.js b/packages/next-common/components/header/headerAccount.js index 585d879600..971f6cb358 100644 --- a/packages/next-common/components/header/headerAccount.js +++ b/packages/next-common/components/header/headerAccount.js @@ -11,7 +11,8 @@ import { useLoginPopup } from "next-common/hooks/useLoginPopup.js"; import SecondaryButton from "next-common/lib/button/secondary"; import { SystemProfile } from "@osn/icons/subsquare"; import { useConnectedAccountContext } from "next-common/context/connectedAccount/index.js"; -import { AddressUser, SystemUser } from "../user"; +import { SystemUser } from "../user"; +import ConnectedAccountDisplay from "./connectedAccountDisplay.jsx"; import { useAccountMenu } from "./useAccountMenu.js"; import { walletConnect } from "next-common/utils/consts/connect/index.js"; import { useWalletConnect } from "next-common/context/walletconnect/index.jsx"; @@ -121,7 +122,7 @@ export default function HeaderAccount() { } else { connectBtn = ( setShow(!show)}> - + ); } diff --git a/packages/next-common/components/hydrationCrossChainPopup/index.js b/packages/next-common/components/hydrationCrossChainPopup/index.js index 2d03c02dd4..a5dad4d8ca 100644 --- a/packages/next-common/components/hydrationCrossChainPopup/index.js +++ b/packages/next-common/components/hydrationCrossChainPopup/index.js @@ -6,7 +6,7 @@ import useAddressComboField from "next-common/components/preImages/createPreimag import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; import { useUser } from "next-common/context/user"; import { useSendTransaction } from "next-common/hooks/useSendTransaction"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import { newErrorToast, newSuccessToast, @@ -153,9 +153,9 @@ function PopupContent() { />
- + Submit - +
); diff --git a/packages/next-common/components/login/web3/index.jsx b/packages/next-common/components/login/web3/index.jsx index 14f07ad827..85e2275cae 100644 --- a/packages/next-common/components/login/web3/index.jsx +++ b/packages/next-common/components/login/web3/index.jsx @@ -5,6 +5,7 @@ import LoginWeb3Substrate from "./substrate"; import { useUnmount } from "react-use"; import LoginWeb3WalletConnect from "./walletconnect"; import LoginWeb3PolkadotVault from "./polkadotVault"; +import LoginWeb3WatchOnly from "./watchOnly"; import { useChainSettings } from "next-common/context/chain"; export default function LoginWeb3({ setIsWeb3 = noop }) { @@ -14,6 +15,7 @@ export default function LoginWeb3({ setIsWeb3 = noop }) { isEVMView, isWalletConnectView, isPolkadotVaultView, + isWatchOnlyView, resetView, } = useWeb3WalletView(); @@ -27,6 +29,9 @@ export default function LoginWeb3({ setIsWeb3 = noop }) { {isWalletConnectView && } {isPolkadotVaultView && } + {isWatchOnlyView && chainSettings?.supportWatchOnly && ( + + )} {chainSettings?.allowWeb2Login && (
diff --git a/packages/next-common/components/login/web3/watchOnly.jsx b/packages/next-common/components/login/web3/watchOnly.jsx new file mode 100644 index 0000000000..87f497eddf --- /dev/null +++ b/packages/next-common/components/login/web3/watchOnly.jsx @@ -0,0 +1,31 @@ +import { ArrowCircleLeft } from "@osn/icons/subsquare"; +import { + WalletGroupTitle, + WalletOptionsWrapper, +} from "next-common/components/wallet/options/styled"; +import WalletOption from "next-common/components/wallet/walletOption"; +import { useWeb3WalletView } from "next-common/hooks/connect/useWeb3WalletView"; +import WatchOnlyAddressForm from "next-common/components/watchOnly/addressForm"; + +export default function LoginWeb3WatchOnly() { + const { setView } = useWeb3WalletView(); + + return ( +
+ + } + title="Back to Substrate" + onClick={() => { + setView("substrate"); + }} + /> + + + Watch-only Address + + +
+ ); +} diff --git a/packages/next-common/components/multisigs/actions/removePopup.jsx b/packages/next-common/components/multisigs/actions/removePopup.jsx index 654d2aca39..cb184d7c8f 100644 --- a/packages/next-common/components/multisigs/actions/removePopup.jsx +++ b/packages/next-common/components/multisigs/actions/removePopup.jsx @@ -6,7 +6,7 @@ import { } from "next-common/store/reducers/toastSlice"; import { useDispatch } from "react-redux"; import SecondaryButton from "next-common/lib/button/secondary"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import Popup from "next-common/components/popup/wrapper/Popup"; import { useMultisigAccounts } from "../context/multisigAccountsContext"; import { getRealField } from "next-common/sima/actions/common"; @@ -88,14 +88,14 @@ export default function RemovePopup({ onClose, multisigAddress }) {
Cancel - Confirm - +
); diff --git a/packages/next-common/components/multisigs/actions/renamePopup.jsx b/packages/next-common/components/multisigs/actions/renamePopup.jsx index f67240d610..b4a8058858 100644 --- a/packages/next-common/components/multisigs/actions/renamePopup.jsx +++ b/packages/next-common/components/multisigs/actions/renamePopup.jsx @@ -5,7 +5,7 @@ import { newSuccessToast, } from "next-common/store/reducers/toastSlice"; import { useDispatch } from "react-redux"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import Popup from "next-common/components/popup/wrapper/Popup"; import TextInputField from "next-common/components/popup/fields/textInputField"; import { useMultisigAccounts } from "../context/multisigAccountsContext"; @@ -106,13 +106,13 @@ export default function RemovePopup({ onClose, multisig }) { {ERROR_MESSAGE.NAME_EXIST} )}
- Save Changes - +
); diff --git a/packages/next-common/components/multisigs/signField/signApprovePopup/popup.jsx b/packages/next-common/components/multisigs/signField/signApprovePopup/popup.jsx index a3ef64fe5e..3fa4801e1e 100644 --- a/packages/next-common/components/multisigs/signField/signApprovePopup/popup.jsx +++ b/packages/next-common/components/multisigs/signField/signApprovePopup/popup.jsx @@ -2,7 +2,7 @@ import SignerWithBalance from "next-common/components/signerPopup/signerWithBala import ProposeTree from "next-common/components/multisigs/signField/signSubmitPopup/proposeTree"; import Popup from "next-common/components/popup/wrapper/Popup"; import { useSignApprove } from "../signApprove"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; function SignApproveInnerPopup({ onClose, multisig }) { const { doSubmit, isDisabled } = useSignApprove(multisig); @@ -12,9 +12,9 @@ function SignApproveInnerPopup({ onClose, multisig }) {
- + Submit - +
); diff --git a/packages/next-common/components/multisigs/signField/signCancelPopup/popup.jsx b/packages/next-common/components/multisigs/signField/signCancelPopup/popup.jsx index 7e0df5137b..afcbf7a0ae 100644 --- a/packages/next-common/components/multisigs/signField/signCancelPopup/popup.jsx +++ b/packages/next-common/components/multisigs/signField/signCancelPopup/popup.jsx @@ -2,7 +2,7 @@ import SignerWithBalance from "next-common/components/signerPopup/signerWithBala import ProposeTree from "next-common/components/multisigs/signField/signSubmitPopup/proposeTree"; import Popup from "next-common/components/popup/wrapper/Popup"; import { useSignCancel } from "../signCancel"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; function SignCancelInnerPopup({ onClose, multisig }) { const { doSubmit, isDisabled } = useSignCancel(multisig); @@ -12,9 +12,9 @@ function SignCancelInnerPopup({ onClose, multisig }) {
- + Cancel - +
); diff --git a/packages/next-common/components/myProxies/operations/removeProxy.jsx b/packages/next-common/components/myProxies/operations/removeProxy.jsx index 5bd04e2f8a..96610a2900 100644 --- a/packages/next-common/components/myProxies/operations/removeProxy.jsx +++ b/packages/next-common/components/myProxies/operations/removeProxy.jsx @@ -7,11 +7,14 @@ import Tooltip from "next-common/components/tooltip"; import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { noop } from "lodash-es"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function RemoveProxy({ data, onSubmitted = noop }) { const api = useContextApi(); const address = useRealAddress(); const dispatch = useDispatch(); + const isWatchOnly = useIsWatchOnly(); const [isDisabled, setIsDisabled] = useState(false); const getTxFunc = useCallback(() => { @@ -42,8 +45,8 @@ export default function RemoveProxy({ data, onSubmitted = noop }) { }, [isSubmitting]); return ( - - + + ); } diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js index ace0cc80d6..3e7138551b 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js @@ -16,6 +16,8 @@ import useRequiredRankToPromoteMember from "./useRequiredRankToPromoteMember"; import RankField from "./rankField"; import useMemberRank from "../memberPromotionPopup/voteButtons/useMemberRank"; import { isNil } from "lodash-es"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function CreatePromotionReferendaAndVotePopup({ who, @@ -34,6 +36,7 @@ export default function CreatePromotionReferendaAndVotePopup({ const { rank: evidenceOwnerRank } = useMemberRank(who); const [toRank, setToRank] = useState(evidenceOwnerRank + 1); const [enactment] = useState({ after: 100 }); + const isWatchOnly = useIsWatchOnly(); const requiredRank = useRequiredRankToPromoteMember( evidenceOwnerRank, @@ -74,6 +77,10 @@ export default function CreatePromotionReferendaAndVotePopup({ disabled = true; tooltipContent = `Only rank >= ${requiredRank} can create a referendum and then vote`; } + if (isWatchOnly) { + disabled = true; + tooltipContent = WATCH_ONLY_TOOLTIP_TEXT; + } return ( diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js index 56107569f9..073bd533e5 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js @@ -12,6 +12,8 @@ import { getPromoteTrackNameFromRank } from "next-common/components/fellowship/c import useMemberRank from "./useMemberRank"; import { useMyVotesChangedContext } from "../../../context/myVotesChanged"; import SecondaryButton from "next-common/lib/button/secondary"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const CreatePromotionReferendaAndVotePopup = dynamicPopup(() => import("../../createPromotionReferendaAndVotePopup"), @@ -29,6 +31,7 @@ function CreateReferendumAndVoteButtonImpl({ useState(false); const dispatch = useDispatch(); const { rank: evidenceOwnerRank } = useMemberRank(who); + const isWatchOnly = useIsWatchOnly(); const chain = useChain(); const trackName = getPromoteTrackNameFromRank(chain, evidenceOwnerRank + 1); @@ -67,8 +70,11 @@ function CreateReferendumAndVoteButtonImpl({ return ( <> - - + + {children} diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createRetentionReferendumAndVoteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createRetentionReferendumAndVoteButton.js index ba8a3ef8e0..750f003a75 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createRetentionReferendumAndVoteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createRetentionReferendumAndVoteButton.js @@ -11,6 +11,8 @@ import { getRetainTrackNameFromRank } from "next-common/components/fellowship/co import useMemberRank from "./useMemberRank"; import { useMyVotesChangedContext } from "../../../context/myVotesChanged"; import SecondaryButton from "next-common/lib/button/secondary"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; function CreateReferendumAndVoteButtonImpl({ who, @@ -24,6 +26,7 @@ function CreateReferendumAndVoteButtonImpl({ const { rank: evidenceOwnerRank } = useMemberRank(who); const chain = useChain(); const trackName = getRetainTrackNameFromRank(chain, evidenceOwnerRank); + const isWatchOnly = useIsWatchOnly(); const [enactment] = useState({ after: 100 }); const { fetch: fetchActiveReferenda } = useActiveReferendaContext(); @@ -50,8 +53,11 @@ function CreateReferendumAndVoteButtonImpl({ }); return ( - - + + {children} diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js index 3292be1ccf..b51c8e37a3 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js @@ -10,6 +10,8 @@ import useSubFellowshipReferendum from "next-common/hooks/collectives/useSubFell import Tooltip from "next-common/components/tooltip"; import { getMinRankOfClass } from "next-common/context/post/fellowship/useMaxVoters"; import { isNil, noop } from "lodash-es"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; function VoteButtonImpl({ referendumIndex, @@ -27,6 +29,7 @@ function VoteButtonImpl({ ); const { result: referendumInfo, loading: isReferendumInfoLoading } = useSubFellowshipReferendum(referendumIndex); + const isWatchOnly = useIsWatchOnly(); const { onInBlock = noop, onFinalized = noop } = callbacks || {}; @@ -61,6 +64,11 @@ function VoteButtonImpl({ } } + if (isWatchOnly) { + disabled = true; + tooltipContent = WATCH_ONLY_TOOLTIP_TEXT; + } + return ( diff --git a/packages/next-common/components/pages/components/gov2/votePopup/popupContent.js b/packages/next-common/components/pages/components/gov2/votePopup/popupContent.js index 189fedba66..ad1dfe9d3b 100644 --- a/packages/next-common/components/pages/components/gov2/votePopup/popupContent.js +++ b/packages/next-common/components/pages/components/gov2/votePopup/popupContent.js @@ -10,7 +10,7 @@ import { WarningMessage } from "next-common/components/popup/styled"; import SplitAbstainVoteStatus from "./splitAbstainVoteStatus"; import VStack from "next-common/components/styled/vStack"; import VoteTypeTab, { Aye, Nay, Split, SplitAbstain } from "./tab"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import useSubAddressReferendaVote, { getReferendaDirectVote, } from "next-common/hooks/referenda/useSubMyReferendaVote"; @@ -139,9 +139,9 @@ function VotePanel({ {!isDelegated && ( // Address is not allow to vote directly when it is in delegate mode
- + Submit - +
)} diff --git a/packages/next-common/components/pages/components/referenda/popup/popupContent.js b/packages/next-common/components/pages/components/referenda/popup/popupContent.js index 1d2c26761a..1af569b6f8 100644 --- a/packages/next-common/components/pages/components/referenda/popup/popupContent.js +++ b/packages/next-common/components/pages/components/referenda/popup/popupContent.js @@ -23,6 +23,7 @@ import AyeNaySplitTab from "./ayeNaySplitTab"; import useStandardVote from "./voteHooks/useStandardVote"; import useSplitVote from "./voteHooks/useSplitVote"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; +import SubmitButton from "next-common/components/common/tx/submitButton"; export function LoadingPanel() { return ( @@ -118,9 +119,9 @@ function VotePanel({ {!isDelegated && ( // Address is not allow to vote directly when it is in delegate mode
- + Confirm - +
)} diff --git a/packages/next-common/components/paraChainTeleportPopup/index.js b/packages/next-common/components/paraChainTeleportPopup/index.js index f4ab04ffb1..6e246104ef 100644 --- a/packages/next-common/components/paraChainTeleportPopup/index.js +++ b/packages/next-common/components/paraChainTeleportPopup/index.js @@ -6,7 +6,7 @@ import useAddressComboField from "next-common/components/preImages/createPreimag import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; import { useUser } from "next-common/context/user"; import { useSendTransaction } from "next-common/hooks/useSendTransaction"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import { newErrorToast, newSuccessToast, @@ -93,9 +93,9 @@ function PopupContent() {
- + Submit - +
); diff --git a/packages/next-common/components/paraChainTeleportPopup/teleport.jsx b/packages/next-common/components/paraChainTeleportPopup/teleport.jsx index 0a1ba4f8f1..bd3f5b68c0 100644 --- a/packages/next-common/components/paraChainTeleportPopup/teleport.jsx +++ b/packages/next-common/components/paraChainTeleportPopup/teleport.jsx @@ -6,7 +6,8 @@ import useAddressComboField from "next-common/components/preImages/createPreimag import AdvanceSettings from "next-common/components/summary/newProposalQuickStart/common/advanceSettings"; import { useUser } from "next-common/context/user"; import { useSendTransaction } from "next-common/hooks/useSendTransaction"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; import { newErrorToast, newSuccessToast, @@ -46,6 +47,7 @@ function PopupContent() { destinationChain, }); const { sendTxFunc, isSubmitting } = useSendTransaction(); + const isWatchOnly = useIsWatchOnly(); const user = useUser(); const address = user?.address; @@ -109,14 +111,14 @@ function PopupContent() {
- - + Submit - +
diff --git a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx index 1e16d0019f..1f68735cdb 100644 --- a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx +++ b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx @@ -14,6 +14,8 @@ import getChainSettings from "next-common/utils/consts/settings"; import { useCallback, useState } from "react"; import dynamicPopup from "next-common/lib/dynamic/popup"; import CheckJudgement from "./checkJudgement"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const SetIdentityPopup = dynamicPopup( () => import("next-common/components/setIdentityPopup"), @@ -26,6 +28,7 @@ export function DirectIdentityActions() { const chain = useChain(); const { identity: identityChain } = getChainSettings(chain); const [showSetIdentityPopup, setShowSetIdentityPopup] = useState(false); + const isWatchOnly = useIsWatchOnly(); const extensionAccounts = useExtensionAccounts(); const api = useContextApi(); @@ -79,8 +82,13 @@ export function DirectIdentityActions() {
- - + + {showSetIdentityPopup && ( diff --git a/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx b/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx index 9277423f76..64d69cbd8e 100644 --- a/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx +++ b/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx @@ -6,11 +6,14 @@ import { useContextApi } from "next-common/context/api"; import { useCallback } from "react"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function CancelRequestJudgement({ registrarIndex }) { const api = useContextApi(); const dispatch = useDispatch(); const address = useRealAddress(); + const isWatchOnly = useIsWatchOnly(); const getTxFunc = useCallback(() => { if (!api || !api?.tx?.identity || !address) { @@ -30,8 +33,8 @@ export default function CancelRequestJudgement({ registrarIndex }) { }); return ( - - + + ); } diff --git a/packages/next-common/components/popup/voteButton.js b/packages/next-common/components/popup/voteButton.js index 407f2c8d8b..5c8c3c8b5a 100644 --- a/packages/next-common/components/popup/voteButton.js +++ b/packages/next-common/components/popup/voteButton.js @@ -3,6 +3,8 @@ import { VoteEnum } from "../../utils/voteEnum"; import { ButtonWrapper } from "./styled"; import SuccessButton from "next-common/lib/button/success"; import DangerButton from "next-common/lib/button/danger"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; export default function VoteButton({ disabled, @@ -10,22 +12,29 @@ export default function VoteButton({ isLoading, doVote, }) { + const isWatchOnly = useIsWatchOnly(); + const isDisabled = disabled || isWatchOnly; + return ( - doVote(false)} - loading={isLoading && loadingState === VoteEnum.Nay} - disabled={disabled || (isLoading && loadingState === VoteEnum.Aye)} - > - Nay - - doVote(true)} - loading={isLoading && loadingState === VoteEnum.Aye} - disabled={disabled || (isLoading && loadingState === VoteEnum.Nay)} - > - Aye - + + doVote(false)} + loading={isLoading && loadingState === VoteEnum.Nay} + disabled={isDisabled || (isLoading && loadingState === VoteEnum.Aye)} + > + Nay + + + + doVote(true)} + loading={isLoading && loadingState === VoteEnum.Aye} + disabled={isDisabled || (isLoading && loadingState === VoteEnum.Nay)} + > + Aye + + ); } diff --git a/packages/next-common/components/popupWithSigner/context/signer.js b/packages/next-common/components/popupWithSigner/context/signer.js index 18e70ac498..bdc7cd7108 100644 --- a/packages/next-common/components/popupWithSigner/context/signer.js +++ b/packages/next-common/components/popupWithSigner/context/signer.js @@ -10,6 +10,8 @@ import useInjectedWeb3 from "next-common/hooks/connect/useInjectedWeb3"; import { useUser } from "next-common/context/user"; import { isSameAddress } from "next-common/utils"; import { findInjectedExtension } from "next-common/hooks/connect/useInjectedWeb3Extension"; +import { useConnectedAccount } from "next-common/context/connectedAccount"; +import { isWatchOnlyAccount } from "next-common/utils/watchOnly"; export const SignerContext = createContext(); @@ -51,6 +53,7 @@ export function useSetSigner() { export function SignerContextProvider({ children, extensionAccounts }) { const user = useUser(); + const connectedAccount = useConnectedAccount(); const userAddress = user?.address; const proxyAddress = user?.proxyAddress; const [selectedProxyAddress, setSelectedProxyAddress] = useState(); @@ -73,23 +76,34 @@ export function SignerContextProvider({ children, extensionAccounts }) { if (!userAddress) { return; } + + const isWatchOnly = isWatchOnlyAccount(connectedAccount); const account = extensionAccounts?.find((item) => isSameAddress(item.address, userAddress), ); - if (!account) { - return; - } + // The connected address may not be owned by any loaded wallet, e.g. a + // watch-only address, a mock account, or an account of a wallet that is no + // longer available. It is still the origin of the transaction. + const connectedAccountInfo = account + ? account + : { + address: userAddress, + meta: { source: connectedAccount?.wallet }, + }; + return { - ...account, - name: account.meta?.name, + ...connectedAccountInfo, + name: connectedAccountInfo.meta?.name, proxyAddress, selectedProxyAddress, multisig, realAddress, + isWatchOnly, }; }, [ userAddress, extensionAccounts, + connectedAccount, proxyAddress, selectedProxyAddress, multisig, diff --git a/packages/next-common/components/popupWithSigner/maybeSignerConnected.js b/packages/next-common/components/popupWithSigner/maybeSignerConnected.js index 4c2682749b..637b496ee5 100644 --- a/packages/next-common/components/popupWithSigner/maybeSignerConnected.js +++ b/packages/next-common/components/popupWithSigner/maybeSignerConnected.js @@ -4,14 +4,18 @@ import { isSameAddress } from "../../utils"; import { SignerContextProvider, usePopupParams } from "./context"; import LoginPopup from "next-common/components/login/popup"; import { isMockAccountAddress } from "next-common/utils/mockAccount"; +import { useConnectedAccount } from "next-common/context/connectedAccount"; +import { isWatchOnlyAccount } from "next-common/utils/watchOnly"; export default function MaybeSignerConnected({ children, extensionAccounts }) { const user = useUser(); + const connectedAccount = useConnectedAccount(); const { onClose } = usePopupParams(); if ( !user?.address || (!isMockAccountAddress(user?.address) && + !isWatchOnlyAccount(connectedAccount) && !extensionAccounts?.find((acc) => isSameAddress(acc.address, user?.address), )) diff --git a/packages/next-common/components/signer.js b/packages/next-common/components/signer.js index 1e2356454d..0556d0c0b8 100644 --- a/packages/next-common/components/signer.js +++ b/packages/next-common/components/signer.js @@ -14,6 +14,8 @@ import useOnChainProxyInfo from "next-common/hooks/useOnChainProxy"; import Tooltip from "./tooltip"; import { MultisigAccount } from "./multisigs/styled"; import SwitchButtonWrapper from "./switchAccountButton"; +import WatchOnlyHint from "next-common/components/watchOnly/hint"; +import useAccountByAddress from "next-common/hooks/useAccountByAddress"; const Wrapper = styled(GreyPanel)` padding: 12px 16px; @@ -106,6 +108,14 @@ function ProxyHintForAddress({ address }) { return ; } +function WatchOnlyHintRow() { + return ( +
+ +
+ ); +} + function MaybeMultisigAccount({ signerAccount }) { const originAccount = useOriginAccount(); @@ -145,7 +155,9 @@ export default function MaybeProxySigner({ )} - {signerAccount?.multisig && supportedMultisig ? ( + {signerAccount?.isWatchOnly ? ( + + ) : signerAccount?.multisig && supportedMultisig ? ( ) : signerAccount?.selectedProxyAddress ? ( @@ -159,10 +171,9 @@ export default function MaybeProxySigner({ export function ConnectedAccountSigner({ extra = null }) { const user = useUser(); - const extensionAccounts = useExtensionAccounts(); - const originAccount = extensionAccounts.find((item) => - isSameAddress(item.address, user?.address), - ); + const signerAccount = useSignerAccount(); + const address = signerAccount?.address || user?.address; + const originAccount = useAccountByAddress(address); return ( diff --git a/packages/next-common/components/signerPopup/index.js b/packages/next-common/components/signerPopup/index.js index 81c913673f..f8eae853a4 100644 --- a/packages/next-common/components/signerPopup/index.js +++ b/packages/next-common/components/signerPopup/index.js @@ -5,6 +5,8 @@ import { PopupButtonWrapper } from "../popup/wrapper"; import { useSignerAccount } from "../popupWithSigner/context"; import SignerWithBalance from "./signerWithBalance"; import { noop } from "lodash-es"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; function PopupContent({ children, @@ -14,6 +16,7 @@ function PopupContent({ }) { const [isLoading, setIsLoading] = useState(false); const signerAccount = useSignerAccount(); + const isWatchOnly = useIsWatchOnly(); const onConfirm = useCallback(async () => { setIsLoading(true); @@ -29,9 +32,15 @@ function PopupContent({ {children} - - {confirmText} - + + + {confirmText} + + ); diff --git a/packages/next-common/components/staking/pools/actions/unbondingPopup.jsx b/packages/next-common/components/staking/pools/actions/unbondingPopup.jsx index 9d654337a6..148a96ce41 100644 --- a/packages/next-common/components/staking/pools/actions/unbondingPopup.jsx +++ b/packages/next-common/components/staking/pools/actions/unbondingPopup.jsx @@ -19,7 +19,7 @@ import { SystemTransfer } from "@osn/icons/subsquare"; import { TitleContainer } from "next-common/components/styled/containers/titleContainer"; import { isNumber } from "lodash-es"; import Signer from "next-common/components/popup/fields/signerField"; -import PrimaryButton from "next-common/lib/button/primary"; +import SubmitButton from "next-common/components/common/tx/submitButton"; function UnbondingPopupContent() { const { unbondingEras = {}, activeEra } = usePopupParams(); @@ -118,7 +118,7 @@ function WithdrawButtonImpl({ era }) { }); return (
- - +
); } diff --git a/packages/next-common/components/switchAccount/index.jsx b/packages/next-common/components/switchAccount/index.jsx index 66c291599e..6327e477c7 100644 --- a/packages/next-common/components/switchAccount/index.jsx +++ b/packages/next-common/components/switchAccount/index.jsx @@ -13,6 +13,8 @@ import { useConnectedAccountContext } from "next-common/context/connectedAccount import { useWeb3Login } from "next-common/hooks/connect/useWeb3Login"; import NoData from "next-common/components/noData"; import { useIsMobile } from "next-common/components/overview/accountInfo/components/accountBalances"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import WatchOnlyAddressForm from "next-common/components/watchOnly/addressForm"; function PopupTitle() { return ( @@ -23,11 +25,9 @@ function PopupTitle() { ); } -function SubTitle() { +function SubTitle({ text = "Select Account" }) { return ( - - Select Account - + {text} ); } @@ -139,15 +139,34 @@ function PopupContent({ onClose }) { } export default function SwitchAccount({ onClose, onOpenLogin }) { + const isWatchOnly = useIsWatchOnly(); + + const popupProps = { + title: , + showCloseIcon: false, + onClose, + className: "flex flex-col space-y-6 p-12 !mb-0 max-sm:!p-6 max-h-[76vh]", + style: { maxWidth: "76vh" }, + }; + + // A watch-only account is not owned by any wallet, so there is no account + // list to pick from. Ask for the address to watch instead. + if (isWatchOnly) { + return ( + +
+ + +
+ + +
+ ); + } + return ( - } - showCloseIcon={false} - onClose={onClose} - className="flex flex-col space-y-6 p-12 !mb-0 max-sm:!p-6 max-h-[76vh]" - style={{ maxWidth: "76vh" }} - > + diff --git a/packages/next-common/components/switchAccountButton.js b/packages/next-common/components/switchAccountButton.js index 0eb7991ca1..215cdc3443 100644 --- a/packages/next-common/components/switchAccountButton.js +++ b/packages/next-common/components/switchAccountButton.js @@ -5,6 +5,7 @@ import { MultisigAccountsProvider } from "./multisigs/context/multisigAccountsCo import SwitchSignerPopup from "./switchSignerPopup"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import { useContextApi } from "next-common/context/api"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; function SwitchButton() { const user = useUser(); @@ -40,8 +41,10 @@ function SwitchButtonContent({ supportedMultisig }) { export default function SwitchButtonWrapper({ supportedMultisig = true }) { const realAddress = useRealAddress(); const api = useContextApi(); + const isWatchOnly = useIsWatchOnly(); - if (!api) { + // A watch-only account has no signer to switch between. + if (isWatchOnly || !api) { return null; } diff --git a/packages/next-common/components/switchSignerPopup/index.js b/packages/next-common/components/switchSignerPopup/index.js index 7ff66afdd7..ce2dc3d569 100644 --- a/packages/next-common/components/switchSignerPopup/index.js +++ b/packages/next-common/components/switchSignerPopup/index.js @@ -1,9 +1,5 @@ import Popup from "../popup/wrapper/Popup"; -import { - useCallerAddress, - useExtensionAccounts, - useSignerContext, -} from "../popupWithSigner/context"; +import { useCallerAddress, useSignerContext } from "../popupWithSigner/context"; import Account from "../account"; import { ArrowRight } from "@osn/icons/subsquare"; import { usePopupOnClose } from "next-common/context/popup"; @@ -11,11 +7,13 @@ import { useMyProxied } from "next-common/context/proxy"; import Tooltip from "../tooltip"; import tw from "tailwind-styled-components"; import Loading from "../loading"; -import { addressEllipsis, cn, isSameAddress } from "next-common/utils"; -import { useMemo } from "react"; +import { cn, isSameAddress } from "next-common/utils"; import { noop } from "lodash-es"; import MultiSignerAccounts from "./multiSignerAccounts"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; +import useAccountByAddress from "next-common/hooks/useAccountByAddress"; +import WatchOnlyHint from "next-common/components/watchOnly/hint"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; const DisabledAccountItemWrapper = tw.div` flex flex-col gap-[12px] p-[12px] pr-[16px] @@ -101,14 +99,8 @@ function AccountItem({ disabled, account, onClick }) { export function OriginAddress({ selected, onSelect = noop }) { const onClose = usePopupOnClose(); const realAddress = useRealAddress(); - const extensionAccounts = useExtensionAccounts(); - const account = useMemo( - () => - extensionAccounts.find((item) => - isSameAddress(item.address, realAddress), - ), - [extensionAccounts, realAddress], - ); + const account = useAccountByAddress(realAddress); + const isWatchOnly = useIsWatchOnly(); const disabled = isSameAddress(selected, realAddress); @@ -123,27 +115,13 @@ export function OriginAddress({ selected, onSelect = noop }) { onClose(); }} /> + {isWatchOnly && } ); } function ProxyAddress({ disabled, proxyInfo, onClick = noop }) { - const { signerAccount } = useSignerContext(); - const extensionAccounts = useExtensionAccounts(); - - const account = useMemo(() => { - const extensionAccount = extensionAccounts.find((item) => - isSameAddress(item.address, proxyInfo.delegator), - ); - if (extensionAccount) { - return extensionAccount; - } - return { - address: proxyInfo.delegator, - name: addressEllipsis(proxyInfo.delegator), - meta: signerAccount.meta, - }; - }, [proxyInfo, extensionAccounts, signerAccount]); + const account = useAccountByAddress(proxyInfo.delegator); return (
Cancel - Confirm - +
); diff --git a/packages/next-common/components/wallet/options/substrate/singleSig.jsx b/packages/next-common/components/wallet/options/substrate/singleSig.jsx index f6c9bba033..ac5abfad5a 100644 --- a/packages/next-common/components/wallet/options/substrate/singleSig.jsx +++ b/packages/next-common/components/wallet/options/substrate/singleSig.jsx @@ -11,6 +11,7 @@ import { WalletOptionsWrapper } from "../styled"; import shouldEnableEvmWallets from "next-common/utils/shouldEnableEvmWallets"; import WalletConnectWallet from "../../walletConnectWallet"; import PolkadotVault from "../../polkadotVault"; +import WatchOnly from "../../watchOnly"; export default function WalletSubstrateSingleSigOptions({ selectedWallet, @@ -81,6 +82,19 @@ export default function WalletSubstrateSingleSigOptions({ ); } + if (wallet.extensionName === WalletTypes.WATCH_ONLY) { + return ( + { + onSelect(wallet); + }} + selected={selected} + /> + ); + } + return ( } + title={wallet.title} + onClick={() => { + setView("watch-only"); + }} + /> + ); +} diff --git a/packages/next-common/components/watchOnly/addressForm.jsx b/packages/next-common/components/watchOnly/addressForm.jsx new file mode 100644 index 0000000000..bd53c09d56 --- /dev/null +++ b/packages/next-common/components/watchOnly/addressForm.jsx @@ -0,0 +1,69 @@ +import { isAddress } from "@polkadot/util-crypto"; +import { useState } from "react"; +import Input from "next-common/lib/input"; +import PrimaryButton from "next-common/lib/button/primary"; +import { Label } from "next-common/components/login/styled"; +import { useChain } from "next-common/context/chain"; +import { encodeAddressToChain } from "next-common/services/address"; +import { useWeb3Login } from "next-common/hooks/connect/useWeb3Login"; +import WalletTypes from "next-common/utils/consts/walletTypes"; + +export default function WatchOnlyAddressForm({ + buttonText = "Next", + onConnected, +}) { + const chain = useChain(); + const [web3Login, web3Loading] = useWeb3Login(); + const [address, setAddress] = useState(""); + const [error, setError] = useState(""); + + const onConfirm = async () => { + const input = address.trim(); + + if (!input) { + setError("Please fill an address"); + return; + } + + if (!isAddress(input)) { + setError("Invalid address"); + return; + } + + setError(""); + await web3Login({ + account: { address: encodeAddressToChain(input, chain) }, + wallet: WalletTypes.WATCH_ONLY, + }); + onConnected?.(); + }; + + return ( +
+
+ + { + setAddress(e.target.value); + setError(""); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + onConfirm(); + } + }} + /> +
+ + {buttonText} + +
+ ); +} diff --git a/packages/next-common/components/watchOnly/hint.jsx b/packages/next-common/components/watchOnly/hint.jsx new file mode 100644 index 0000000000..7c2308adbf --- /dev/null +++ b/packages/next-common/components/watchOnly/hint.jsx @@ -0,0 +1,18 @@ +import { SystemEye } from "@osn/icons/subsquare"; +import { cn } from "next-common/utils"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; + +export default function WatchOnlyHint({ className }) { + return ( +
+ + {WATCH_ONLY_TOOLTIP_TEXT} +
+ ); +} diff --git a/packages/next-common/components/watchOnly/tooltip.jsx b/packages/next-common/components/watchOnly/tooltip.jsx new file mode 100644 index 0000000000..99613de747 --- /dev/null +++ b/packages/next-common/components/watchOnly/tooltip.jsx @@ -0,0 +1,29 @@ +import styled from "styled-components"; +import Tooltip from "next-common/components/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; + +// Disabled buttons do not emit pointer events, so let the wrapper be the hover +// target of the tooltip. +const TriggerWrapper = styled.div` + display: inline-block; + cursor: not-allowed; + + button { + pointer-events: none; + } +`; + +export default function WatchOnlyTooltip({ children, content }) { + const isWatchOnly = useIsWatchOnly(); + + if (!isWatchOnly) { + return children; + } + + return ( + + {children} + + ); +} diff --git a/packages/next-common/context/connectedAccount/index.js b/packages/next-common/context/connectedAccount/index.js index c324be7869..35a92bceec 100644 --- a/packages/next-common/context/connectedAccount/index.js +++ b/packages/next-common/context/connectedAccount/index.js @@ -15,6 +15,7 @@ import { clearMyMultisigsData } from "next-common/store/reducers/multisigSlice"; import { useDispatch } from "react-redux"; import { usePageLoading } from "next-common/context/pageLoading"; import { getMockAccountAddress } from "next-common/utils/mockAccount"; +import { isWatchOnlyAccount } from "next-common/utils/watchOnly"; const ConnectedAccountContext = createContext(null); @@ -171,6 +172,11 @@ export function useConnectedAccount() { return connectedAccount; } +export function useIsWatchOnly() { + const connectedAccount = useConnectedAccount(); + return isWatchOnlyAccount(connectedAccount); +} + export function getContextConnectedAccount() { return savedConnectedAccount || ssrConnectedAccount; } diff --git a/packages/next-common/hooks/connect/useSubstrateWallets.js b/packages/next-common/hooks/connect/useSubstrateWallets.js index e5e777ad57..cd48e8c486 100644 --- a/packages/next-common/hooks/connect/useSubstrateWallets.js +++ b/packages/next-common/hooks/connect/useSubstrateWallets.js @@ -10,6 +10,7 @@ import { signet, walletConnect, polkadotVaultWallet, + watchOnlyWallet, } from "next-common/utils/consts/connect"; import isEvmChain, { isSupportSubstrateThroughEthereumAddress, @@ -29,6 +30,7 @@ export function useSubstrateWallets() { !isSubstrateThroughEvm && nova, chainSettings?.supportWalletconnect && walletConnect, chainSettings?.supportPolkadotVault && polkadotVaultWallet, + chainSettings?.supportWatchOnly && watchOnlyWallet, ].filter(Boolean); let multiSigWallets = []; diff --git a/packages/next-common/hooks/connect/useWeb3WalletView.js b/packages/next-common/hooks/connect/useWeb3WalletView.js index 75d52043b0..6ea2fae879 100644 --- a/packages/next-common/hooks/connect/useWeb3WalletView.js +++ b/packages/next-common/hooks/connect/useWeb3WalletView.js @@ -6,6 +6,7 @@ const VIEWS = { EVM: "evm", WALLETCONNECT: "walletconnect", POLKADOT_VAULT: "polkadot-vault", + WATCH_ONLY: "watch-only", }; const DEFAULT_VIEW = shouldEnableSubstrateWallets() ? VIEWS.SUBSTRATE @@ -20,6 +21,7 @@ export function useWeb3WalletView() { const isEVMView = view === VIEWS.EVM; const isWalletConnectView = view === VIEWS.WALLETCONNECT; const isPolkadotVaultView = view === VIEWS.POLKADOT_VAULT; + const isWatchOnlyView = view === VIEWS.WATCH_ONLY; function resetView() { setView(DEFAULT_VIEW); @@ -30,6 +32,7 @@ export function useWeb3WalletView() { isEVMView, isWalletConnectView, isPolkadotVaultView, + isWatchOnlyView, setView, resetView, }; diff --git a/packages/next-common/hooks/useAccountByAddress.js b/packages/next-common/hooks/useAccountByAddress.js new file mode 100644 index 0000000000..a85ab632c5 --- /dev/null +++ b/packages/next-common/hooks/useAccountByAddress.js @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { + useExtensionAccounts, + useSignerAccount, +} from "next-common/components/popupWithSigner/context"; +import { addressEllipsis, isSameAddress } from "next-common/utils"; + +// Resolve an address to the wallet account that owns it. Addresses owned by no +// loaded wallet, e.g. a watch-only or a mock account, fall back to a plain +// account so they can still be displayed and used as an origin. +export default function useAccountByAddress(address) { + const extensionAccounts = useExtensionAccounts(); + const signerAccount = useSignerAccount(); + const meta = signerAccount?.meta; + + return useMemo(() => { + if (!address) { + return null; + } + + const account = extensionAccounts?.find((item) => + isSameAddress(item.address, address), + ); + + return ( + account || { + address, + name: addressEllipsis(address), + meta, + } + ); + }, [address, extensionAccounts, meta]); +} diff --git a/packages/next-common/hooks/useEnsureLogin.js b/packages/next-common/hooks/useEnsureLogin.js index f92986864e..c98537da1a 100644 --- a/packages/next-common/hooks/useEnsureLogin.js +++ b/packages/next-common/hooks/useEnsureLogin.js @@ -14,6 +14,10 @@ import { import { useChain } from "next-common/context/chain"; import { useLoginPopup } from "./useLoginPopup"; import { getCookieConnectedAccount } from "next-common/utils/getCookieConnectedAccount"; +import { + isWatchOnlyAccount, + WATCH_ONLY_LOGIN_REJECTED_TEXT, +} from "next-common/utils/watchOnly"; import { useSignMessage } from "./useSignMessage"; import isShibuya from "next-common/utils/isShibuya"; @@ -31,6 +35,11 @@ export function useEnsureLogin() { const login = useCallback(async () => { const connectedAccount = getCookieConnectedAccount(); + if (isWatchOnlyAccount(connectedAccount)) { + dispatch(newErrorToast(WATCH_ONLY_LOGIN_REJECTED_TEXT)); + return false; + } + setLoading(true); try { let address; diff --git a/packages/next-common/hooks/useSendTransaction.js b/packages/next-common/hooks/useSendTransaction.js index 4424b40121..ff010264d8 100644 --- a/packages/next-common/hooks/useSendTransaction.js +++ b/packages/next-common/hooks/useSendTransaction.js @@ -32,6 +32,8 @@ import { sendWalletConnectTx } from "next-common/utils/sendTransaction/sendWalle import { useWalletConnectBuildPayload } from "next-common/hooks/useWalletConnectBuildPayload"; import { useVaultSigner } from "next-common/context/polkadotVault/vaultSignerProvider"; import { useWalletConnect } from "next-common/context/walletconnect"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; function shouldSendSignetTx(signerAccount) { return signerAccount?.meta?.source === WalletTypes.SIGNET; @@ -76,6 +78,7 @@ export function useSendTransaction() { const [isSubmitting, setIsSubmitting] = useState(false); const dispatch = useDispatch(); const signerAccount = useSignerAccount(); + const isWatchOnly = useIsWatchOnly(); const setSigner = useSetSigner(); const { sdk: signetSdk } = useSignetSdk(); const { sendVaultTx } = useVaultSigner(); @@ -105,6 +108,11 @@ export function useSendTransaction() { return; } + if (isWatchOnly) { + dispatch(newErrorToast(WATCH_ONLY_TOOLTIP_TEXT)); + return; + } + try { await setSigner(api, signerAccount); } catch (e) { @@ -295,6 +303,7 @@ export function useSendTransaction() { [ buildPayload, dispatch, + isWatchOnly, signerAccount, signetSdk, setSigner, diff --git a/packages/next-common/hooks/useSignMessage.js b/packages/next-common/hooks/useSignMessage.js index 5348403d77..fa9b370a36 100644 --- a/packages/next-common/hooks/useSignMessage.js +++ b/packages/next-common/hooks/useSignMessage.js @@ -12,6 +12,10 @@ import { useWalletConnect } from "next-common/context/walletconnect"; import WalletTypes from "next-common/utils/consts/walletTypes"; import { useVaultSigner } from "next-common/context/polkadotVault/vaultSignerProvider"; import { isMockAccountAddress } from "next-common/utils/mockAccount"; +import { + isWatchOnlyAccount, + WATCH_ONLY_MESSAGE_SIGN_REJECTED_TEXT, +} from "next-common/utils/watchOnly"; // Fixed fake signature returned for mock accounts (bypass real wallet signing) const MOCK_SIGNATURE = "0x" + "00".repeat(64); @@ -28,6 +32,10 @@ export function useSignMessage() { return MOCK_SIGNATURE; } + if (isWatchOnlyAccount({ wallet: walletName })) { + throw new Error(WATCH_ONLY_MESSAGE_SIGN_REJECTED_TEXT); + } + const shouldUseEVMSign = walletName === metamask.extensionName; if (shouldUseEVMSign) { diff --git a/packages/next-common/utils/consts/connect/index.js b/packages/next-common/utils/consts/connect/index.js index 722c1cf9fb..cab93ef184 100644 --- a/packages/next-common/utils/consts/connect/index.js +++ b/packages/next-common/utils/consts/connect/index.js @@ -14,6 +14,7 @@ import { WalletCoinbase, WalletWalletconnect, WalletPolkadotVaultLight, + SystemEye, } from "@osn/icons/subsquare"; export const polkadotJs = { @@ -119,6 +120,12 @@ export const polkadotVaultWallet = { logo: WalletPolkadotVaultLight, }; +export const watchOnlyWallet = { + extensionName: WalletTypes.WATCH_ONLY, + title: "Watch-only", + logo: SystemEye, +}; + export const allWallets = [ polkadotJs, subWallet, @@ -134,4 +141,5 @@ export const allWallets = [ coinbaseWallet, walletConnect, polkadotVaultWallet, + watchOnlyWallet, ]; diff --git a/packages/next-common/utils/consts/settings/collectives/index.js b/packages/next-common/utils/consts/settings/collectives/index.js index 6d3e923f21..07e8d07606 100644 --- a/packages/next-common/utils/consts/settings/collectives/index.js +++ b/packages/next-common/utils/consts/settings/collectives/index.js @@ -58,6 +58,7 @@ const collectives = { sima: true, allowWeb2Login: false, supportPolkadotVault: true, + supportWatchOnly: true, enablePapi: true, ...collectivesThemes, }; diff --git a/packages/next-common/utils/consts/settings/polkadot/index.js b/packages/next-common/utils/consts/settings/polkadot/index.js index 72e0f4d728..ace6b9db6b 100644 --- a/packages/next-common/utils/consts/settings/polkadot/index.js +++ b/packages/next-common/utils/consts/settings/polkadot/index.js @@ -128,6 +128,7 @@ const polkadot = { }, supportWalletconnect: true, supportPolkadotVault: true, + supportWatchOnly: true, openSquare: { voting: "polkadot", }, diff --git a/packages/next-common/utils/consts/walletTypes.js b/packages/next-common/utils/consts/walletTypes.js index 91c0720fa7..82359fa5e1 100644 --- a/packages/next-common/utils/consts/walletTypes.js +++ b/packages/next-common/utils/consts/walletTypes.js @@ -13,6 +13,7 @@ const WalletTypes = { COINBASE_WALLET: "coinbase wallet", WALLETCONNECT: "walletconnect", POLKADOT_VAULT: "polkadot-vault", + WATCH_ONLY: "watch-only", }; export default WalletTypes; diff --git a/packages/next-common/utils/watchOnly.js b/packages/next-common/utils/watchOnly.js new file mode 100644 index 0000000000..867d7e6bd2 --- /dev/null +++ b/packages/next-common/utils/watchOnly.js @@ -0,0 +1,14 @@ +import WalletTypes from "./consts/walletTypes"; + +export const WATCH_ONLY_TOOLTIP_TEXT = + "Watch-only accounts cannot sign transactions."; + +export const WATCH_ONLY_LOGIN_REJECTED_TEXT = + "Watch-only account cannot sign in."; + +export const WATCH_ONLY_MESSAGE_SIGN_REJECTED_TEXT = + "Watch-only account cannot sign messages."; + +export function isWatchOnlyAccount(account) { + return account?.wallet === WalletTypes.WATCH_ONLY; +} From 431fab4a65cc44183f3ef76fe7902280a2ed2f1c Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 21 Sep 2026 18:24:51 +0800 Subject: [PATCH 02/11] refactor, #6920 --- .../components/common/tx/submitButton.jsx | 13 +++-- .../myProxies/operations/removeProxy.jsx | 8 +-- .../index.js | 27 ++++------ .../createPromotionReferendumAndVoteButton.js | 21 ++++---- .../createRetentionReferendumAndVoteButton.js | 21 ++++---- .../voteButtons/voteButton.js | 23 ++++----- .../paraChainTeleportPopup/teleport.jsx | 34 +++++-------- .../identity/directIdentityActions.jsx | 16 ++---- .../judgements/cancelRequestJudgement.jsx | 8 +-- .../next-common/components/removeButton.js | 50 +++++++++---------- .../removeDelegation.js | 12 +++-- .../components/watchOnly/tooltip.jsx | 30 ++++++++--- 12 files changed, 118 insertions(+), 145 deletions(-) diff --git a/packages/next-common/components/common/tx/submitButton.jsx b/packages/next-common/components/common/tx/submitButton.jsx index ea6691156b..abd92655bd 100644 --- a/packages/next-common/components/common/tx/submitButton.jsx +++ b/packages/next-common/components/common/tx/submitButton.jsx @@ -2,13 +2,18 @@ import PrimaryButton from "next-common/lib/button/primary"; import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; import { useIsWatchOnly } from "next-common/context/connectedAccount"; -// Primary button for tx submissions: disabled, with a hint tooltip, when the -// connected account is watch-only. -export default function SubmitButton({ disabled = false, children, ...props }) { +// Tx submit button. Watch-only accounts are handled here: the button is +// disabled and its tooltip becomes the watch-only hint. +export default function SubmitButton({ + tooltip, + disabled = false, + children, + ...props +}) { const isWatchOnly = useIsWatchOnly(); return ( - + {children} diff --git a/packages/next-common/components/myProxies/operations/removeProxy.jsx b/packages/next-common/components/myProxies/operations/removeProxy.jsx index 96610a2900..1896e54926 100644 --- a/packages/next-common/components/myProxies/operations/removeProxy.jsx +++ b/packages/next-common/components/myProxies/operations/removeProxy.jsx @@ -3,18 +3,14 @@ import { useContextApi } from "next-common/context/api"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; import RemoveButton from "next-common/components/removeButton"; -import Tooltip from "next-common/components/tooltip"; import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { noop } from "lodash-es"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function RemoveProxy({ data, onSubmitted = noop }) { const api = useContextApi(); const address = useRealAddress(); const dispatch = useDispatch(); - const isWatchOnly = useIsWatchOnly(); const [isDisabled, setIsDisabled] = useState(false); const getTxFunc = useCallback(() => { @@ -45,8 +41,6 @@ export default function RemoveProxy({ data, onSubmitted = noop }) { }, [isSubmitting]); return ( - - - + ); } diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js index 3e7138551b..1f3ac75508 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/createPromotionReferendaAndVotePopup/index.js @@ -6,8 +6,7 @@ import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { useActiveReferendaContext } from "next-common/context/activeReferenda"; -import PrimaryButton from "next-common/lib/button/primary"; -import Tooltip from "next-common/components/tooltip"; +import SubmitButton from "next-common/components/common/tx/submitButton"; import useMyRank from "../memberPromotionPopup/voteButtons/useMyRank"; import { useChain } from "next-common/context/chain"; import { getPromoteTrackNameFromRank } from "next-common/components/fellowship/core/members/actions/promote/popup"; @@ -16,8 +15,6 @@ import useRequiredRankToPromoteMember from "./useRequiredRankToPromoteMember"; import RankField from "./rankField"; import useMemberRank from "../memberPromotionPopup/voteButtons/useMemberRank"; import { isNil } from "lodash-es"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function CreatePromotionReferendaAndVotePopup({ who, @@ -36,7 +33,6 @@ export default function CreatePromotionReferendaAndVotePopup({ const { rank: evidenceOwnerRank } = useMemberRank(who); const [toRank, setToRank] = useState(evidenceOwnerRank + 1); const [enactment] = useState({ after: 100 }); - const isWatchOnly = useIsWatchOnly(); const requiredRank = useRequiredRankToPromoteMember( evidenceOwnerRank, @@ -77,10 +73,6 @@ export default function CreatePromotionReferendaAndVotePopup({ disabled = true; tooltipContent = `Only rank >= ${requiredRank} can create a referendum and then vote`; } - if (isWatchOnly) { - disabled = true; - tooltipContent = WATCH_ONLY_TOOLTIP_TEXT; - } return ( @@ -92,15 +84,14 @@ export default function CreatePromotionReferendaAndVotePopup({ setSelectedRank={setToRank} />
- - - Create & Vote - - + + Create & Vote +
); diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js index 073bd533e5..1c20baff66 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js @@ -6,14 +6,12 @@ import { useFellowshipProposalSubmissionTxFunc } from "next-common/hooks/fellows import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { useActiveReferendaContext } from "next-common/context/activeReferenda"; import dynamicPopup from "next-common/lib/dynamic/popup"; -import Tooltip from "next-common/components/tooltip"; import { useChain } from "next-common/context/chain"; import { getPromoteTrackNameFromRank } from "next-common/components/fellowship/core/members/actions/promote/popup"; import useMemberRank from "./useMemberRank"; import { useMyVotesChangedContext } from "../../../context/myVotesChanged"; import SecondaryButton from "next-common/lib/button/secondary"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; +import SubmitButton from "next-common/components/common/tx/submitButton"; const CreatePromotionReferendaAndVotePopup = dynamicPopup(() => import("../../createPromotionReferendaAndVotePopup"), @@ -31,7 +29,6 @@ function CreateReferendumAndVoteButtonImpl({ useState(false); const dispatch = useDispatch(); const { rank: evidenceOwnerRank } = useMemberRank(who); - const isWatchOnly = useIsWatchOnly(); const chain = useChain(); const trackName = getPromoteTrackNameFromRank(chain, evidenceOwnerRank + 1); @@ -70,14 +67,14 @@ function CreateReferendumAndVoteButtonImpl({ return ( <> - - - {children} - - + + {children} + {showMaybeFastPromotePopup && ( - - {children} - -
+ + {children} + ); } diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js index b51c8e37a3..5525dee13d 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js @@ -7,11 +7,9 @@ import { useRankedCollectivePallet } from "next-common/context/collectives/colle import { useFellowshipMemberRank } from "next-common/hooks/fellowship/useFellowshipMemberRank"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useSubFellowshipReferendum from "next-common/hooks/collectives/useSubFellowshipReferendum"; -import Tooltip from "next-common/components/tooltip"; import { getMinRankOfClass } from "next-common/context/post/fellowship/useMaxVoters"; import { isNil, noop } from "lodash-es"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; +import SubmitButton from "next-common/components/common/tx/submitButton"; function VoteButtonImpl({ referendumIndex, @@ -29,7 +27,6 @@ function VoteButtonImpl({ ); const { result: referendumInfo, loading: isReferendumInfoLoading } = useSubFellowshipReferendum(referendumIndex); - const isWatchOnly = useIsWatchOnly(); const { onInBlock = noop, onFinalized = noop } = callbacks || {}; @@ -64,17 +61,15 @@ function VoteButtonImpl({ } } - if (isWatchOnly) { - disabled = true; - tooltipContent = WATCH_ONLY_TOOLTIP_TEXT; - } - return ( - - - {children} - - + + {children} + ); } diff --git a/packages/next-common/components/paraChainTeleportPopup/teleport.jsx b/packages/next-common/components/paraChainTeleportPopup/teleport.jsx index bd3f5b68c0..8193147f14 100644 --- a/packages/next-common/components/paraChainTeleportPopup/teleport.jsx +++ b/packages/next-common/components/paraChainTeleportPopup/teleport.jsx @@ -7,7 +7,6 @@ import AdvanceSettings from "next-common/components/summary/newProposalQuickStar import { useUser } from "next-common/context/user"; import { useSendTransaction } from "next-common/hooks/useSendTransaction"; import SubmitButton from "next-common/components/common/tx/submitButton"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; import { newErrorToast, newSuccessToast, @@ -20,17 +19,6 @@ import useNativeTransferAmount from "./useNativeTransferAmount"; import PeopleApiProvider from "next-common/context/people/api"; import CoretimeApiProvider from "next-common/context/coretime/api"; import { CollectivesApiProvider } from "next-common/context/collectives/api"; -import Tooltip from "next-common/components/tooltip"; - -function TooltipDisabledGuard({ disabled, children }) { - return disabled ? ( - - {children} - - ) : ( - children - ); -} function PopupContent() { const { onClose } = usePopupParams(); @@ -47,7 +35,6 @@ function PopupContent() { destinationChain, }); const { sendTxFunc, isSubmitting } = useSendTransaction(); - const isWatchOnly = useIsWatchOnly(); const user = useUser(); const address = user?.address; @@ -111,15 +98,18 @@ function PopupContent() {
- - - Submit - - + + Submit +
); diff --git a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx index 1f68735cdb..8e333b10ed 100644 --- a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx +++ b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx @@ -14,8 +14,6 @@ import getChainSettings from "next-common/utils/consts/settings"; import { useCallback, useState } from "react"; import dynamicPopup from "next-common/lib/dynamic/popup"; import CheckJudgement from "./checkJudgement"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const SetIdentityPopup = dynamicPopup( () => import("next-common/components/setIdentityPopup"), @@ -28,7 +26,6 @@ export function DirectIdentityActions() { const chain = useChain(); const { identity: identityChain } = getChainSettings(chain); const [showSetIdentityPopup, setShowSetIdentityPopup] = useState(false); - const isWatchOnly = useIsWatchOnly(); const extensionAccounts = useExtensionAccounts(); const api = useContextApi(); @@ -82,14 +79,11 @@ export function DirectIdentityActions() {
- - - + {showSetIdentityPopup && ( setShowSetIdentityPopup(false)} /> diff --git a/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx b/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx index 64d69cbd8e..b10645638a 100644 --- a/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx +++ b/packages/next-common/components/people/overview/judgements/cancelRequestJudgement.jsx @@ -1,19 +1,15 @@ import RemoveButton from "next-common/components/removeButton"; -import Tooltip from "next-common/components/tooltip"; import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { useContextApi } from "next-common/context/api"; import { useCallback } from "react"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; -import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; export default function CancelRequestJudgement({ registrarIndex }) { const api = useContextApi(); const dispatch = useDispatch(); const address = useRealAddress(); - const isWatchOnly = useIsWatchOnly(); const getTxFunc = useCallback(() => { if (!api || !api?.tx?.identity || !address) { @@ -33,8 +29,6 @@ export default function CancelRequestJudgement({ registrarIndex }) { }); return ( - - - + ); } diff --git a/packages/next-common/components/removeButton.js b/packages/next-common/components/removeButton.js index 6e9b422e3d..96da6526f5 100644 --- a/packages/next-common/components/removeButton.js +++ b/packages/next-common/components/removeButton.js @@ -1,34 +1,30 @@ import { SystemClose } from "@osn/icons/subsquare"; import { noop } from "lodash-es"; -import styled, { css } from "styled-components"; +import { cn } from "next-common/utils"; +import SubmitButton from "next-common/components/common/tx/submitButton"; -const Wrapper = styled.div` - display: inline-flex; - cursor: pointer; - padding: 6px; - border-radius: 4px; - border: 1px solid var(--neutral400); - ${(p) => - p.disabled && - css` - pointer-events: none; - `} - svg path { - ${(p) => - p.disabled - ? css` - fill: var(--textDisabled); - ` - : css` - fill: var(--textPrimary); - `} - } -`; - -export default function RemoveButton({ disabled, onClick = noop }) { +// Compact square submit button, built on SubmitButton so it inherits the +// watch-only handling. +export default function RemoveButton({ + tooltip, + disabled = false, + onClick = noop, + className, +}) { return ( - + - + ); } diff --git a/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js b/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js index 3070cf39d8..8a57690b6b 100644 --- a/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js +++ b/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js @@ -16,8 +16,10 @@ export default function DemocracyRemoveDelegation({ ButtonComponent }) { const [isLoading, setIsLoading] = useState(false); const [showUndelegatePopup, setShowUndelegatePopup] = useState(false); + // A passed button handles its own tooltip, see RemoveButton. const button = ButtonComponent ? ( setShowUndelegatePopup(true)} /> @@ -32,9 +34,13 @@ export default function DemocracyRemoveDelegation({ ButtonComponent }) { return ( <> - -
{button}
-
+ {ButtonComponent ? ( + button + ) : ( + +
{button}
+
+ )} {showUndelegatePopup && ( setShowUndelegatePopup(false)} diff --git a/packages/next-common/components/watchOnly/tooltip.jsx b/packages/next-common/components/watchOnly/tooltip.jsx index 99613de747..e1d8ab5626 100644 --- a/packages/next-common/components/watchOnly/tooltip.jsx +++ b/packages/next-common/components/watchOnly/tooltip.jsx @@ -1,29 +1,43 @@ -import styled from "styled-components"; +import styled, { css } from "styled-components"; import Tooltip from "next-common/components/tooltip"; import { useIsWatchOnly } from "next-common/context/connectedAccount"; import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; -// Disabled buttons do not emit pointer events, so let the wrapper be the hover +// Disabled buttons do not emit pointer events, so the wrapper is the hover // target of the tooltip. const TriggerWrapper = styled.div` display: inline-block; - cursor: not-allowed; - button { + button:disabled, + [disabled] { pointer-events: none; } + + ${(p) => + p.watchOnly && + css` + cursor: not-allowed; + `} `; +// Shows `content`, or the watch-only hint when the connected account is +// watch-only. export default function WatchOnlyTooltip({ children, content }) { const isWatchOnly = useIsWatchOnly(); - if (!isWatchOnly) { - return children; + if (isWatchOnly) { + return ( + + {children} + + ); } - return ( - + return content ? ( + {children} + ) : ( + children ); } From f562d449d3e8d587835e1e19943242cfc1427e37 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 09:59:02 +0800 Subject: [PATCH 03/11] fix: refactor, #6920 --- .../common/tx/submitRemoveButton.jsx | 29 +++++++++++ .../myProxies/operations/removeProxy.jsx | 8 ++- .../identity/directIdentityActions.jsx | 4 +- .../judgements/cancelRequestJudgement.jsx | 8 ++- .../next-common/components/removeButton.js | 52 +++++++++++-------- .../removeDelegation.js | 12 ++--- 6 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 packages/next-common/components/common/tx/submitRemoveButton.jsx diff --git a/packages/next-common/components/common/tx/submitRemoveButton.jsx b/packages/next-common/components/common/tx/submitRemoveButton.jsx new file mode 100644 index 0000000000..1d21ed13bb --- /dev/null +++ b/packages/next-common/components/common/tx/submitRemoveButton.jsx @@ -0,0 +1,29 @@ +import { SystemClose } from "@osn/icons/subsquare"; +import { noop } from "lodash-es"; +import { cn } from "next-common/utils"; +import SubmitButton from "next-common/components/common/tx/submitButton"; + +// ✕ variant of SubmitButton, for buttons that submit a tx. +export default function SubmitRemoveButton({ + tooltip, + disabled = false, + onClick = noop, + className, +}) { + return ( + + + + ); +} diff --git a/packages/next-common/components/myProxies/operations/removeProxy.jsx b/packages/next-common/components/myProxies/operations/removeProxy.jsx index 1896e54926..7b175fe366 100644 --- a/packages/next-common/components/myProxies/operations/removeProxy.jsx +++ b/packages/next-common/components/myProxies/operations/removeProxy.jsx @@ -2,7 +2,7 @@ import { useCallback, useState, useEffect } from "react"; import { useContextApi } from "next-common/context/api"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; -import RemoveButton from "next-common/components/removeButton"; +import SubmitRemoveButton from "next-common/components/common/tx/submitRemoveButton"; import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import { noop } from "lodash-es"; @@ -41,6 +41,10 @@ export default function RemoveProxy({ data, onSubmitted = noop }) { }, [isSubmitting]); return ( - + ); } diff --git a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx index 8e333b10ed..583feb34b0 100644 --- a/packages/next-common/components/people/overview/identity/directIdentityActions.jsx +++ b/packages/next-common/components/people/overview/identity/directIdentityActions.jsx @@ -5,7 +5,7 @@ import { useDispatch } from "react-redux"; import { newSuccessToast } from "next-common/store/reducers/toastSlice"; import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useTxSubmission from "next-common/components/common/tx/useTxSubmission"; -import RemoveButton from "next-common/components/removeButton"; +import SubmitRemoveButton from "next-common/components/common/tx/submitRemoveButton"; import Tooltip from "next-common/components/tooltip"; import { clearCachedIdentitys } from "next-common/services/identity"; import { useChain } from "next-common/context/chain"; @@ -79,7 +79,7 @@ export function DirectIdentityActions() { - + ); } diff --git a/packages/next-common/components/removeButton.js b/packages/next-common/components/removeButton.js index 96da6526f5..ed334fde7d 100644 --- a/packages/next-common/components/removeButton.js +++ b/packages/next-common/components/removeButton.js @@ -1,30 +1,36 @@ import { SystemClose } from "@osn/icons/subsquare"; import { noop } from "lodash-es"; -import { cn } from "next-common/utils"; -import SubmitButton from "next-common/components/common/tx/submitButton"; +import styled, { css } from "styled-components"; -// Compact square submit button, built on SubmitButton so it inherits the -// watch-only handling. -export default function RemoveButton({ - tooltip, - disabled = false, - onClick = noop, - className, -}) { +const Wrapper = styled.div` + display: inline-flex; + cursor: pointer; + padding: 6px; + border-radius: 4px; + border: 1px solid var(--neutral400); + ${(p) => + p.disabled && + css` + pointer-events: none; + `} + svg path { + ${(p) => + p.disabled + ? css` + fill: var(--textDisabled); + ` + : css` + fill: var(--textPrimary); + `} + } +`; + +// Plain ✕ button, for buttons that only trigger a dialog. Use SubmitRemoveButton +// instead when it submits a tx, so watch-only accounts are handled for you. +export default function RemoveButton({ disabled, onClick = noop }) { return ( - + - + ); } diff --git a/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js b/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js index 8a57690b6b..3070cf39d8 100644 --- a/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js +++ b/packages/next-common/components/summary/democracySummaryDelegation/removeDelegation.js @@ -16,10 +16,8 @@ export default function DemocracyRemoveDelegation({ ButtonComponent }) { const [isLoading, setIsLoading] = useState(false); const [showUndelegatePopup, setShowUndelegatePopup] = useState(false); - // A passed button handles its own tooltip, see RemoveButton. const button = ButtonComponent ? ( setShowUndelegatePopup(true)} /> @@ -34,13 +32,9 @@ export default function DemocracyRemoveDelegation({ ButtonComponent }) { return ( <> - {ButtonComponent ? ( - button - ) : ( - -
{button}
-
- )} + +
{button}
+
{showUndelegatePopup && ( setShowUndelegatePopup(false)} From 72ff41ad5f4729a8b69aeed1f7582b91a328d676 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 11:51:19 +0800 Subject: [PATCH 04/11] fix: button layout --- packages/next-common/components/watchOnly/tooltip.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/next-common/components/watchOnly/tooltip.jsx b/packages/next-common/components/watchOnly/tooltip.jsx index e1d8ab5626..d86d015ace 100644 --- a/packages/next-common/components/watchOnly/tooltip.jsx +++ b/packages/next-common/components/watchOnly/tooltip.jsx @@ -4,9 +4,10 @@ import { useIsWatchOnly } from "next-common/context/connectedAccount"; import { WATCH_ONLY_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; // Disabled buttons do not emit pointer events, so the wrapper is the hover -// target of the tooltip. +// target of the tooltip. `display: grid` keeps the wrapper block-wide with its +// child stretched, so it does not shrink buttons inside flex layouts. const TriggerWrapper = styled.div` - display: inline-block; + display: grid; button:disabled, [disabled] { From b34028a134ce3cc9d0d9a081538a96a895f33fa9 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 11:51:41 +0800 Subject: [PATCH 05/11] Improve create post button state, #6920 --- .../next-common/components/post/postCreate.js | 20 ++++++++++++----- .../components/watchOnly/tooltip.jsx | 10 ++++++--- .../sima/components/post/postCreate.js | 22 +++++++++++++------ packages/next-common/utils/watchOnly.js | 6 +++++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/next-common/components/post/postCreate.js b/packages/next-common/components/post/postCreate.js index 2ce3ec9302..85f135ba26 100644 --- a/packages/next-common/components/post/postCreate.js +++ b/packages/next-common/components/post/postCreate.js @@ -18,6 +18,9 @@ import { NeutralPanel } from "../styled/containers/neutralPanel"; import PostLabel from "./postLabel"; import Editor, { useEditorUploading } from "../editor"; import { useEnsureLogin } from "next-common/hooks/useEnsureLogin"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_CREATE_POST_LOGIN_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const Wrapper = styled(NeutralPanel)` color: var(--textPrimary); @@ -77,6 +80,7 @@ export default function PostCreate() { const [selectedLabels, setSelectedLabels] = useState([]); const { ensureLogin } = useEnsureLogin(); const [editorUploading] = useEditorUploading(); + const isWatchOnly = useIsWatchOnly(); const createPost = async () => { setCreating(true); @@ -193,13 +197,17 @@ export default function PostCreate() { /> - - Create - + + Create + + ); diff --git a/packages/next-common/components/watchOnly/tooltip.jsx b/packages/next-common/components/watchOnly/tooltip.jsx index d86d015ace..65f465af32 100644 --- a/packages/next-common/components/watchOnly/tooltip.jsx +++ b/packages/next-common/components/watchOnly/tooltip.jsx @@ -22,13 +22,17 @@ const TriggerWrapper = styled.div` `; // Shows `content`, or the watch-only hint when the connected account is -// watch-only. -export default function WatchOnlyTooltip({ children, content }) { +// watch-only; `watchOnlyContent` replaces the hint for non-tx buttons. +export default function WatchOnlyTooltip({ + children, + content, + watchOnlyContent = WATCH_ONLY_TOOLTIP_TEXT, +}) { const isWatchOnly = useIsWatchOnly(); if (isWatchOnly) { return ( - + {children} ); diff --git a/packages/next-common/sima/components/post/postCreate.js b/packages/next-common/sima/components/post/postCreate.js index 2f0cf28e06..14c2eea251 100644 --- a/packages/next-common/sima/components/post/postCreate.js +++ b/packages/next-common/sima/components/post/postCreate.js @@ -16,6 +16,9 @@ import { getContentField } from "next-common/utils/sima/utils"; import useSignSimaMessage from "next-common/utils/sima/useSignSimaMessage"; import AdvancedForm from "next-common/components/post/advanced/form"; import { getRealField } from "next-common/sima/actions/common"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_CREATE_POST_MESSAGE_SIGN_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const Wrapper = styled(NeutralPanel)` color: var(--textPrimary); @@ -68,6 +71,7 @@ export default function SimaPostCreate() { const advancedForm = useRef(); const [isAdvanced, setIsAdvanced] = useState(false); const [formValue, setFormValue] = useState({}); + const isWatchOnly = useIsWatchOnly(); const createPost = async (proxyAddress) => { setCreating(true); @@ -164,13 +168,17 @@ export default function SimaPostCreate() { /> - + + + ); diff --git a/packages/next-common/utils/watchOnly.js b/packages/next-common/utils/watchOnly.js index 867d7e6bd2..5f5e424d17 100644 --- a/packages/next-common/utils/watchOnly.js +++ b/packages/next-common/utils/watchOnly.js @@ -9,6 +9,12 @@ export const WATCH_ONLY_LOGIN_REJECTED_TEXT = export const WATCH_ONLY_MESSAGE_SIGN_REJECTED_TEXT = "Watch-only account cannot sign messages."; +export const WATCH_ONLY_CREATE_POST_LOGIN_TOOLTIP_TEXT = + "Watch-only account cannot sign in to create a post."; + +export const WATCH_ONLY_CREATE_POST_MESSAGE_SIGN_TOOLTIP_TEXT = + "Watch-only account cannot sign messages to create a post."; + export function isWatchOnlyAccount(account) { return account?.wallet === WalletTypes.WATCH_ONLY; } From 1af57985ff94652bbb07e10d4691fc3ee1b53251 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 11:56:13 +0800 Subject: [PATCH 06/11] Improve application create button state, #6920 --- .../fellowship/applications/create.js | 18 +++++++++++++++--- packages/next-common/utils/watchOnly.js | 3 +++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/next-common/components/fellowship/applications/create.js b/packages/next-common/components/fellowship/applications/create.js index 9011a745b0..c891d20060 100644 --- a/packages/next-common/components/fellowship/applications/create.js +++ b/packages/next-common/components/fellowship/applications/create.js @@ -11,6 +11,9 @@ import { useRouter } from "next/router"; import { useExtensionAccounts } from "next-common/components/popupWithSigner/context"; import AddressCombo from "next-common/components/addressCombo"; import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_CREATE_APPLICATION_LOGIN_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; function PageTitle() { return ( @@ -109,6 +112,7 @@ function CreateFellowshipApplicationImpl() { const [contentType, setContentType] = useState("markdown"); const [loading, setLoading] = useState(false); const { ensureLogin } = useEnsureLogin(); + const isWatchOnly = useIsWatchOnly(); const createApplication = async () => { setLoading(true); @@ -154,9 +158,17 @@ function CreateFellowshipApplicationImpl() { setContentType={setContentType} />
- - Create - + + + Create + +
); diff --git a/packages/next-common/utils/watchOnly.js b/packages/next-common/utils/watchOnly.js index 5f5e424d17..76d9426a68 100644 --- a/packages/next-common/utils/watchOnly.js +++ b/packages/next-common/utils/watchOnly.js @@ -15,6 +15,9 @@ export const WATCH_ONLY_CREATE_POST_LOGIN_TOOLTIP_TEXT = export const WATCH_ONLY_CREATE_POST_MESSAGE_SIGN_TOOLTIP_TEXT = "Watch-only account cannot sign messages to create a post."; +export const WATCH_ONLY_CREATE_APPLICATION_LOGIN_TOOLTIP_TEXT = + "Watch-only account cannot sign in to create an application."; + export function isWatchOnlyAccount(account) { return account?.wallet === WalletTypes.WATCH_ONLY; } From 3c4a1f571cce9f96e8f1c98a82bc9e5e7624a753 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 12:06:12 +0800 Subject: [PATCH 07/11] Improve watch only comment button state, #6920 --- packages/next-common/components/comment/editor.js | 14 ++++++++++---- .../components/fellowship/applications/create.js | 4 ++-- packages/next-common/components/post/postCreate.js | 4 ++-- .../next-common/sima/components/post/postCreate.js | 4 ++-- packages/next-common/utils/watchOnly.js | 12 ++++++------ 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/next-common/components/comment/editor.js b/packages/next-common/components/comment/editor.js index 8df663e9b8..d88883f714 100644 --- a/packages/next-common/components/comment/editor.js +++ b/packages/next-common/components/comment/editor.js @@ -14,10 +14,12 @@ import { usePost } from "next-common/context/post"; import { useCommentActions } from "next-common/sima/context/commentActions"; import { newErrorToast } from "next-common/store/reducers/toastSlice"; import { useDispatch } from "react-redux"; -import Tooltip from "../tooltip"; import { useDetailType } from "next-common/context/page"; import { detailPageCategory } from "next-common/utils/consts/business/category"; import { getRealField } from "next-common/sima/actions/common"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WATCH_ONLY_COMMENT_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; const Wrapper = styled.div` margin-top: 48px; @@ -91,6 +93,7 @@ function CommentEditor( const { createPostComment, createCommentReply } = useCommentActions(); const shouldUseSima = useShouldUseSima(replyToComment); + const isWatchOnly = useIsWatchOnly(); const createComment = async (realAddress) => { if (!isMounted()) { @@ -191,18 +194,21 @@ function CommentEditor( Cancel )} - + createComment()} onClickCommentAsProxy={(realAddress) => { createComment(realAddress); }} /> - + ); diff --git a/packages/next-common/components/fellowship/applications/create.js b/packages/next-common/components/fellowship/applications/create.js index c891d20060..ff9823ca17 100644 --- a/packages/next-common/components/fellowship/applications/create.js +++ b/packages/next-common/components/fellowship/applications/create.js @@ -13,7 +13,7 @@ import AddressCombo from "next-common/components/addressCombo"; import SignerPopupWrapper from "next-common/components/popupWithSigner/signerPopupWrapper"; import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WATCH_ONLY_CREATE_APPLICATION_LOGIN_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; +import { WATCH_ONLY_CREATE_APPLICATION_TOOLTIP_TEXT } from "next-common/utils/watchOnly"; function PageTitle() { return ( @@ -159,7 +159,7 @@ function CreateFellowshipApplicationImpl() { />
Date: Tue, 22 Sep 2026 12:18:40 +0800 Subject: [PATCH 08/11] fix: watch only vote button, #6920 --- .../createPromotionReferendumAndVoteButton.js | 20 ++++++++++--------- .../createRetentionReferendumAndVoteButton.js | 20 ++++++++++--------- .../voteButtons/voteButton.js | 20 ++++++++++--------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js index 1c20baff66..bac1a1d88c 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/createPromotionReferendumAndVoteButton.js @@ -11,7 +11,8 @@ import { getPromoteTrackNameFromRank } from "next-common/components/fellowship/c import useMemberRank from "./useMemberRank"; import { useMyVotesChangedContext } from "../../../context/myVotesChanged"; import SecondaryButton from "next-common/lib/button/secondary"; -import SubmitButton from "next-common/components/common/tx/submitButton"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; const CreatePromotionReferendaAndVotePopup = dynamicPopup(() => import("../../createPromotionReferendaAndVotePopup"), @@ -29,6 +30,7 @@ function CreateReferendumAndVoteButtonImpl({ useState(false); const dispatch = useDispatch(); const { rank: evidenceOwnerRank } = useMemberRank(who); + const isWatchOnly = useIsWatchOnly(); const chain = useChain(); const trackName = getPromoteTrackNameFromRank(chain, evidenceOwnerRank + 1); @@ -67,14 +69,14 @@ function CreateReferendumAndVoteButtonImpl({ return ( <> - - {children} - + + + {children} + + {showMaybeFastPromotePopup && ( - {children} - + + + {children} + + ); } diff --git a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js index 5525dee13d..78be405d94 100644 --- a/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js +++ b/packages/next-common/components/overview/accountInfo/components/fellowshipTodoList/todoList/memberPromotionPopup/voteButtons/voteButton.js @@ -9,7 +9,8 @@ import useRealAddress from "next-common/utils/hooks/useRealAddress"; import useSubFellowshipReferendum from "next-common/hooks/collectives/useSubFellowshipReferendum"; import { getMinRankOfClass } from "next-common/context/post/fellowship/useMaxVoters"; import { isNil, noop } from "lodash-es"; -import SubmitButton from "next-common/components/common/tx/submitButton"; +import WatchOnlyTooltip from "next-common/components/watchOnly/tooltip"; +import { useIsWatchOnly } from "next-common/context/connectedAccount"; function VoteButtonImpl({ referendumIndex, @@ -21,6 +22,7 @@ function VoteButtonImpl({ const api = useContextApi(); const collectivePallet = useRankedCollectivePallet(); const realAddress = useRealAddress(); + const isWatchOnly = useIsWatchOnly(); const { rank: myRank, isLoading: isMyRankLoading } = useFellowshipMemberRank( realAddress, collectivePallet, @@ -62,14 +64,14 @@ function VoteButtonImpl({ } return ( - - {children} - + + + {children} + + ); } From 74515dcc77e6b3ff6587275fb91142779c349dd2 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 14:39:11 +0800 Subject: [PATCH 09/11] Enable watch-only feature to all chains, #6920 --- packages/next-common/components/login/web3/index.jsx | 4 +--- packages/next-common/hooks/connect/useSubstrateWallets.js | 2 +- .../next-common/utils/consts/settings/collectives/index.js | 1 - packages/next-common/utils/consts/settings/polkadot/index.js | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/next-common/components/login/web3/index.jsx b/packages/next-common/components/login/web3/index.jsx index 85e2275cae..4f036e5694 100644 --- a/packages/next-common/components/login/web3/index.jsx +++ b/packages/next-common/components/login/web3/index.jsx @@ -29,9 +29,7 @@ export default function LoginWeb3({ setIsWeb3 = noop }) { {isWalletConnectView && } {isPolkadotVaultView && } - {isWatchOnlyView && chainSettings?.supportWatchOnly && ( - - )} + {isWatchOnlyView && } {chainSettings?.allowWeb2Login && (
diff --git a/packages/next-common/hooks/connect/useSubstrateWallets.js b/packages/next-common/hooks/connect/useSubstrateWallets.js index cd48e8c486..314f5af57d 100644 --- a/packages/next-common/hooks/connect/useSubstrateWallets.js +++ b/packages/next-common/hooks/connect/useSubstrateWallets.js @@ -30,7 +30,7 @@ export function useSubstrateWallets() { !isSubstrateThroughEvm && nova, chainSettings?.supportWalletconnect && walletConnect, chainSettings?.supportPolkadotVault && polkadotVaultWallet, - chainSettings?.supportWatchOnly && watchOnlyWallet, + watchOnlyWallet, ].filter(Boolean); let multiSigWallets = []; diff --git a/packages/next-common/utils/consts/settings/collectives/index.js b/packages/next-common/utils/consts/settings/collectives/index.js index 07e8d07606..6d3e923f21 100644 --- a/packages/next-common/utils/consts/settings/collectives/index.js +++ b/packages/next-common/utils/consts/settings/collectives/index.js @@ -58,7 +58,6 @@ const collectives = { sima: true, allowWeb2Login: false, supportPolkadotVault: true, - supportWatchOnly: true, enablePapi: true, ...collectivesThemes, }; diff --git a/packages/next-common/utils/consts/settings/polkadot/index.js b/packages/next-common/utils/consts/settings/polkadot/index.js index ace6b9db6b..72e0f4d728 100644 --- a/packages/next-common/utils/consts/settings/polkadot/index.js +++ b/packages/next-common/utils/consts/settings/polkadot/index.js @@ -128,7 +128,6 @@ const polkadot = { }, supportWalletconnect: true, supportPolkadotVault: true, - supportWatchOnly: true, openSquare: { voting: "polkadot", }, From ab0143d4bfc8052d4639972ae632db0a0c44fc67 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 14:52:49 +0800 Subject: [PATCH 10/11] Improve watch-only icon display, #6920 --- .../header/connectedAccountDisplay.jsx | 21 +++++++------------ .../next-common/utils/consts/connect/index.js | 4 ++-- .../utils/consts/connect/walletIcons.jsx | 8 +++++++ 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 packages/next-common/utils/consts/connect/walletIcons.jsx diff --git a/packages/next-common/components/header/connectedAccountDisplay.jsx b/packages/next-common/components/header/connectedAccountDisplay.jsx index 0e12c3a0dc..b8fed47760 100644 --- a/packages/next-common/components/header/connectedAccountDisplay.jsx +++ b/packages/next-common/components/header/connectedAccountDisplay.jsx @@ -1,30 +1,23 @@ -import { SystemEye } from "@osn/icons/subsquare"; import { cn } from "next-common/utils"; import { AddressUser } from "next-common/components/user"; import Tooltip from "next-common/components/tooltip"; import { useIsWatchOnly } from "next-common/context/connectedAccount"; +import { WalletWatchOnly } from "next-common/utils/consts/connect/walletIcons"; -// Connected account shown in the header. A watch-only account gets an eye badge -// on its avatar: it is absolutely positioned, so the display size is unchanged. +// Connected account shown in the header. A watch-only account hides the avatar +// and shows an eye icon in its place. export default function ConnectedAccountDisplay({ address, className }) { const isWatchOnly = useIsWatchOnly(); - const account = ; if (!isWatchOnly) { - return account; + return ; } return ( -
- {account} - +
+ +
); diff --git a/packages/next-common/utils/consts/connect/index.js b/packages/next-common/utils/consts/connect/index.js index cab93ef184..d563af827e 100644 --- a/packages/next-common/utils/consts/connect/index.js +++ b/packages/next-common/utils/consts/connect/index.js @@ -14,8 +14,8 @@ import { WalletCoinbase, WalletWalletconnect, WalletPolkadotVaultLight, - SystemEye, } from "@osn/icons/subsquare"; +import { WalletWatchOnly } from "./walletIcons"; export const polkadotJs = { extensionName: WalletTypes.POLKADOT_JS, @@ -123,7 +123,7 @@ export const polkadotVaultWallet = { export const watchOnlyWallet = { extensionName: WalletTypes.WATCH_ONLY, title: "Watch-only", - logo: SystemEye, + logo: WalletWatchOnly, }; export const allWallets = [ diff --git a/packages/next-common/utils/consts/connect/walletIcons.jsx b/packages/next-common/utils/consts/connect/walletIcons.jsx new file mode 100644 index 0000000000..7c6f0601d0 --- /dev/null +++ b/packages/next-common/utils/consts/connect/walletIcons.jsx @@ -0,0 +1,8 @@ +import { SystemEye } from "@osn/icons/subsquare"; +import { cn } from "next-common/utils"; + +export function WalletWatchOnly({ className, ...props }) { + return ( + + ); +} From ffcc35d3efb281beca5100173be5f9803c26cc00 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 22 Sep 2026 14:58:12 +0800 Subject: [PATCH 11/11] revert, #6920 --- .../header/connectedAccountDisplay.jsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/next-common/components/header/connectedAccountDisplay.jsx b/packages/next-common/components/header/connectedAccountDisplay.jsx index b8fed47760..0e12c3a0dc 100644 --- a/packages/next-common/components/header/connectedAccountDisplay.jsx +++ b/packages/next-common/components/header/connectedAccountDisplay.jsx @@ -1,23 +1,30 @@ +import { SystemEye } from "@osn/icons/subsquare"; import { cn } from "next-common/utils"; import { AddressUser } from "next-common/components/user"; import Tooltip from "next-common/components/tooltip"; import { useIsWatchOnly } from "next-common/context/connectedAccount"; -import { WalletWatchOnly } from "next-common/utils/consts/connect/walletIcons"; -// Connected account shown in the header. A watch-only account hides the avatar -// and shows an eye icon in its place. +// Connected account shown in the header. A watch-only account gets an eye badge +// on its avatar: it is absolutely positioned, so the display size is unchanged. export default function ConnectedAccountDisplay({ address, className }) { const isWatchOnly = useIsWatchOnly(); + const account = ; if (!isWatchOnly) { - return ; + return account; } return ( -
- - +
+ {account} +
);