diff --git a/.agents/shared/metrics/hits.jsonl b/.agents/shared/metrics/hits.jsonl
index eec593656..d11857e79 100644
--- a/.agents/shared/metrics/hits.jsonl
+++ b/.agents/shared/metrics/hits.jsonl
@@ -106,6 +106,10 @@
{"ts":"2026-07-07T08:25:58.831Z","tool":"Edit","file":"src/shared/components/transactionDialog/transactionDialog.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":2,"adapter":"claude"}
{"ts":"2026-07-07T08:26:08.144Z","tool":"Edit","file":"src/shared/components/transactionDialog/transactionDialog.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":1,"adapter":"claude"}
{"ts":"2026-07-07T08:27:04.958Z","tool":"Edit","file":"src/shared/components/transactionDialog/transactionDialog.test.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":1,"adapter":"claude"}
+{"ts":"2026-07-08T11:05:20.251Z","tool":"Edit","file":"src/modules/application/components/errorBoundary/errorBoundaryClass.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":29,"adapter":"claude"}
+{"ts":"2026-07-08T11:05:45.029Z","tool":"Edit","file":"src/modules/application/components/errorBoundary/errorBoundaryClass.tsx","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":6,"adapter":"claude"}
+{"ts":"2026-07-08T11:06:24.791Z","tool":"Edit","file":"src/shared/utils/monitoringUtils/monitoringUtils.ts","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":3,"adapter":"claude"}
+{"ts":"2026-07-08T11:06:45.503Z","tool":"Edit","file":"src/shared/utils/monitoringUtils/monitoringUtils.test.ts","rule":"error-and-monitoring","bytes":2525,"elapsed_ms":4,"adapter":"claude"}
{"ts":"2026-07-07T13:45:43.994Z","tool":"Edit","file":"src/modules/governance/dialogs/selectPluginDialog/selectPluginDialog.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":2,"adapter":"claude"}
{"ts":"2026-07-07T15:31:34.162Z","tool":"Write","file":"src/modules/governance/dialogs/selectPluginDialog/selectPluginDialogItem.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":2,"adapter":"claude"}
{"ts":"2026-07-07T15:31:49.364Z","tool":"Edit","file":"src/modules/governance/dialogs/selectPluginDialog/selectPluginDialog.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":2,"adapter":"claude"}
@@ -119,6 +123,8 @@
{"ts":"2026-07-08T16:29:42.858Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialog.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":3,"adapter":"claude"}
{"ts":"2026-07-08T16:29:55.658Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialog.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":11,"adapter":"claude"}
{"ts":"2026-07-08T16:30:49.523Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialogProcessListItem.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":2,"adapter":"claude"}
+{"ts":"2026-07-14T13:48:20.570Z","tool":"Edit","file":".agents/shared/skills/rules/dialog-conventions.md","rule":"rule-authoring","bytes":2672,"elapsed_ms":1,"adapter":"claude"}
+{"ts":"2026-07-14T13:49:10.198Z","tool":"Edit","file":".agents/shared/skills/rules/dialog-conventions.md","rule":"rule-authoring","bytes":2672,"elapsed_ms":4,"adapter":"claude"}
{"ts":"2026-07-09T11:51:56.300Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialogProcessListItem.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":3,"adapter":"claude"}
{"ts":"2026-07-09T12:00:44.020Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialogProcessListItem.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":3,"adapter":"claude"}
{"ts":"2026-07-13T10:09:35.950Z","tool":"Edit","file":"apps/app/src/modules/governance/dialogs/selectPluginDialog/selectPluginDialogProcessListItem.tsx","rule":"plugin-visibility","bytes":2379,"elapsed_ms":3,"adapter":"claude"}
diff --git a/.agents/shared/skills/rules/dialog-conventions.md b/.agents/shared/skills/rules/dialog-conventions.md
index 7e31a9848..097b910fa 100644
--- a/.agents/shared/skills/rules/dialog-conventions.md
+++ b/.agents/shared/skills/rules/dialog-conventions.md
@@ -1,7 +1,7 @@
---
name: dialog-conventions
description: Dialog barrel + definitions wiring — dynamic-import the component, static-export the types, register by id through the dialogProvider.
-globs: apps/app/src/**/dialogs/*/index.ts, apps/app/src/**/constants/*DialogsDefinitions.ts
+globs: apps/app/src/**/dialogs/*/index.ts, apps/app/src/**/*DialogsDefinitions.ts, apps/app/src/**/*DialogDefinitions.ts
kind: rule
---
@@ -49,6 +49,7 @@ export const financeDialogsDefinitions: Record', () => {
useWalletAccountSpy.mockReturnValue({
address: WALLET,
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
useEnsAvatarSpy.mockReturnValue({
diff --git a/apps/app/src/modules/application/dialogs/aragonProfileRenameDialog/aragonProfileRenameDialog.test.tsx b/apps/app/src/modules/application/dialogs/aragonProfileRenameDialog/aragonProfileRenameDialog.test.tsx
index d38fd8b99..bf3ba0897 100644
--- a/apps/app/src/modules/application/dialogs/aragonProfileRenameDialog/aragonProfileRenameDialog.test.tsx
+++ b/apps/app/src/modules/application/dialogs/aragonProfileRenameDialog/aragonProfileRenameDialog.test.tsx
@@ -87,6 +87,7 @@ describe('', () => {
useWalletAccountSpy.mockReturnValue({
address: '0x1111111111111111111111111111111111111111',
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
useEnsSubdomainFieldSpy.mockReturnValue({
diff --git a/apps/app/src/modules/application/dialogs/aragonProfileRenameTransactionDialog/aragonProfileRenameTransactionDialog.test.tsx b/apps/app/src/modules/application/dialogs/aragonProfileRenameTransactionDialog/aragonProfileRenameTransactionDialog.test.tsx
index a95835080..9efb3f5ef 100644
--- a/apps/app/src/modules/application/dialogs/aragonProfileRenameTransactionDialog/aragonProfileRenameTransactionDialog.test.tsx
+++ b/apps/app/src/modules/application/dialogs/aragonProfileRenameTransactionDialog/aragonProfileRenameTransactionDialog.test.tsx
@@ -61,6 +61,7 @@ describe('', () => {
useWalletAccountSpy.mockReturnValue({
address: WALLET,
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
});
diff --git a/apps/app/src/modules/application/hooks/useWalletAccount/useWalletAccount.ts b/apps/app/src/modules/application/hooks/useWalletAccount/useWalletAccount.ts
index 9e7d4c405..18b38bfd0 100644
--- a/apps/app/src/modules/application/hooks/useWalletAccount/useWalletAccount.ts
+++ b/apps/app/src/modules/application/hooks/useWalletAccount/useWalletAccount.ts
@@ -3,13 +3,13 @@ import { useConnection } from 'wagmi';
/**
* Returns the wagmi-sourced account fields the app actually needs:
- * `address`, `chainId`, `isReconnecting`. Intentionally omits `isConnected` —
- * use `useWalletConnected` (AppKit-sourced) for any UI gate that must agree
- * with the connect modal. `DesyncWatcher` is the only sanctioned consumer of
- * wagmi's raw `useConnection`.
+ * `address`, `chainId`, `isConnecting`, `isReconnecting`. Intentionally omits
+ * `isConnected` — use `useWalletConnected` (AppKit-sourced) for any UI gate that
+ * must agree with the connect modal. `DesyncWatcher` is the only sanctioned
+ * consumer of wagmi's raw `useConnection`.
*/
export const useWalletAccount = () => {
- const { address, chainId, isReconnecting } = useConnection();
+ const { address, chainId, isConnecting, isReconnecting } = useConnection();
- return { address, chainId, isReconnecting };
+ return { address, chainId, isConnecting, isReconnecting };
};
diff --git a/apps/app/src/modules/capitalFlow/constants/capitalFlowDialogsDefinitions.ts b/apps/app/src/modules/capitalFlow/constants/capitalFlowDialogsDefinitions.ts
index 9418991f3..47f99ca8a 100644
--- a/apps/app/src/modules/capitalFlow/constants/capitalFlowDialogsDefinitions.ts
+++ b/apps/app/src/modules/capitalFlow/constants/capitalFlowDialogsDefinitions.ts
@@ -24,7 +24,10 @@ export const capitalFlowDialogsDefinitions: Record<
hiddenDescription:
'app.capitalFlow.setupStrategyDialog.a11y.description',
},
- [CapitalFlowDialogId.PREPARE_POLICY]: { Component: PreparePolicyDialog },
+ [CapitalFlowDialogId.PREPARE_POLICY]: {
+ Component: PreparePolicyDialog,
+ requiresWallet: true,
+ },
[CapitalFlowDialogId.ROUTER_SELECTOR]: {
Component: RouterSelectorDialog,
size: 'lg',
@@ -36,6 +39,7 @@ export const capitalFlowDialogsDefinitions: Record<
},
[CapitalFlowDialogId.DISPATCH_TRANSACTION]: {
Component: DispatchTransactionDialog,
+ requiresWallet: true,
size: 'lg',
},
};
diff --git a/apps/app/src/modules/createDao/constants/createDaoDialogsDefinitions.ts b/apps/app/src/modules/createDao/constants/createDaoDialogsDefinitions.ts
index 76b6bd93b..f2f622b0d 100644
--- a/apps/app/src/modules/createDao/constants/createDaoDialogsDefinitions.ts
+++ b/apps/app/src/modules/createDao/constants/createDaoDialogsDefinitions.ts
@@ -11,7 +11,10 @@ export const createDaoDialogsDefinitions: Record<
CreateDaoDialogId,
IDialogComponentDefinitions
> = {
- [CreateDaoDialogId.PUBLISH_DAO]: { Component: PublishDaoDialog },
+ [CreateDaoDialogId.PUBLISH_DAO]: {
+ Component: PublishDaoDialog,
+ requiresWallet: true,
+ },
[CreateDaoDialogId.CREATE_DAO_DETAILS]: {
Component: CreateDaoDetailsDialog,
size: 'lg',
@@ -20,7 +23,10 @@ export const createDaoDialogsDefinitions: Record<
Component: CreateProcessDetailsDialog,
size: 'lg',
},
- [CreateDaoDialogId.PREPARE_PROCESS]: { Component: PrepareProcessDialog },
+ [CreateDaoDialogId.PREPARE_PROCESS]: {
+ Component: PrepareProcessDialog,
+ requiresWallet: true,
+ },
[CreateDaoDialogId.SETUP_BODY]: {
Component: SetupBodyDialog,
size: 'lg',
diff --git a/apps/app/src/modules/governance/constants/governanceDialogsDefinitions.ts b/apps/app/src/modules/governance/constants/governanceDialogsDefinitions.ts
index 3b512f4f9..33f753863 100644
--- a/apps/app/src/modules/governance/constants/governanceDialogsDefinitions.ts
+++ b/apps/app/src/modules/governance/constants/governanceDialogsDefinitions.ts
@@ -18,7 +18,10 @@ export const governanceDialogsDefinitions: Record<
GovernanceDialogId,
IDialogComponentDefinitions
> = {
- [GovernanceDialogId.PUBLISH_PROPOSAL]: { Component: PublishProposalDialog },
+ [GovernanceDialogId.PUBLISH_PROPOSAL]: {
+ Component: PublishProposalDialog,
+ requiresWallet: true,
+ },
[GovernanceDialogId.DUPLICATE_PROPOSAL_WARNING]: {
Component: DuplicateProposalAlertDialog,
variant: 'warning',
@@ -26,9 +29,18 @@ export const governanceDialogsDefinitions: Record<
hiddenDescription:
'app.governance.duplicateProposalAlertDialog.a11y.description',
},
- [GovernanceDialogId.EXECUTE_ACTIONS]: { Component: ExecuteActionsDialog },
- [GovernanceDialogId.EXECUTE]: { Component: ExecuteDialog },
- [GovernanceDialogId.VOTE]: { Component: VoteDialog },
+ [GovernanceDialogId.EXECUTE_ACTIONS]: {
+ Component: ExecuteActionsDialog,
+ requiresWallet: true,
+ },
+ [GovernanceDialogId.EXECUTE]: {
+ Component: ExecuteDialog,
+ requiresWallet: true,
+ },
+ [GovernanceDialogId.VOTE]: {
+ Component: VoteDialog,
+ requiresWallet: true,
+ },
[GovernanceDialogId.SELECT_PLUGIN]: { Component: SelectPluginDialog },
[GovernanceDialogId.VERIFY_SMART_CONTRACT]: {
Component: VerifySmartContractDialog,
@@ -56,5 +68,6 @@ export const governanceDialogsDefinitions: Record<
},
[GovernanceDialogId.DELEGATE_STATEMENT_TRANSACTION]: {
Component: DelegateStatementTransactionDialog,
+ requiresWallet: true,
},
};
diff --git a/apps/app/src/modules/governance/hooks/useExecutePermissionCheckGuard/useExecutePermissionCheckGuard.test.tsx b/apps/app/src/modules/governance/hooks/useExecutePermissionCheckGuard/useExecutePermissionCheckGuard.test.tsx
index bc8c079ae..41ca743ec 100644
--- a/apps/app/src/modules/governance/hooks/useExecutePermissionCheckGuard/useExecutePermissionCheckGuard.test.tsx
+++ b/apps/app/src/modules/governance/hooks/useExecutePermissionCheckGuard/useExecutePermissionCheckGuard.test.tsx
@@ -41,6 +41,7 @@ describe('useExecutePermissionCheckGuard hook', () => {
useWalletAccountSpy.mockReturnValue({
address: '0xabc',
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
useConnectedWalletGuardSpy.mockImplementation((params) => ({
diff --git a/apps/app/src/modules/governance/hooks/useSimulateProposal/useSimulateProposalCreation.test.ts b/apps/app/src/modules/governance/hooks/useSimulateProposal/useSimulateProposalCreation.test.ts
index e016cb322..d657aba15 100644
--- a/apps/app/src/modules/governance/hooks/useSimulateProposal/useSimulateProposalCreation.test.ts
+++ b/apps/app/src/modules/governance/hooks/useSimulateProposal/useSimulateProposalCreation.test.ts
@@ -35,6 +35,7 @@ describe('useSimulateProposalCreation hook', () => {
useWalletAccountSpy.mockReturnValue({
address: walletAddress,
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
buildTransactionSpy.mockReturnValue({
@@ -114,6 +115,7 @@ describe('useSimulateProposalCreation hook', () => {
useWalletAccountSpy.mockReturnValue({
address: undefined,
chainId: undefined,
+ isConnecting: false,
isReconnecting: false,
});
diff --git a/apps/app/src/modules/governance/pages/createExecuteActionsPage/createExecuteActionsPageClientSteps.test.tsx b/apps/app/src/modules/governance/pages/createExecuteActionsPage/createExecuteActionsPageClientSteps.test.tsx
index a35616ab0..1767edf71 100644
--- a/apps/app/src/modules/governance/pages/createExecuteActionsPage/createExecuteActionsPageClientSteps.test.tsx
+++ b/apps/app/src/modules/governance/pages/createExecuteActionsPage/createExecuteActionsPageClientSteps.test.tsx
@@ -54,6 +54,7 @@ describe(' component', () => {
useWalletAccountSpy.mockReturnValue({
address: '0xwallet',
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
});
diff --git a/apps/app/src/modules/settings/constants/settingsDialogDefinitions.ts b/apps/app/src/modules/settings/constants/settingsDialogDefinitions.ts
index 0411211c8..b68fbe2aa 100644
--- a/apps/app/src/modules/settings/constants/settingsDialogDefinitions.ts
+++ b/apps/app/src/modules/settings/constants/settingsDialogDefinitions.ts
@@ -16,6 +16,7 @@ export const settingsDialogDefinitions: Record<
},
[SettingsDialogId.PREPARE_DAO_CONTRACTS_UPDATE]: {
Component: PrepareDaoContractsUpdateDialog,
+ requiresWallet: true,
},
[SettingsDialogId.GOVERNANCE_PROCESS_REQUIRED]: {
Component: GovernanceProcessRequiredDialog,
@@ -32,5 +33,6 @@ export const settingsDialogDefinitions: Record<
},
[SettingsDialogId.PREPARE_PLUGIN_UNINSTALLATION]: {
Component: PreparePluginUninstallationDialog,
+ requiresWallet: true,
},
};
diff --git a/apps/app/src/plugins/capitalDistributorPlugin/constants/capitalDistributorPluginDialogsDefinitions.ts b/apps/app/src/plugins/capitalDistributorPlugin/constants/capitalDistributorPluginDialogsDefinitions.ts
index 6dc9aa86c..f5c4ea416 100644
--- a/apps/app/src/plugins/capitalDistributorPlugin/constants/capitalDistributorPluginDialogsDefinitions.ts
+++ b/apps/app/src/plugins/capitalDistributorPlugin/constants/capitalDistributorPluginDialogsDefinitions.ts
@@ -12,5 +12,6 @@ export const capitalDistributorPluginDialogsDefinitions: Record<
},
[CapitalDistributorPluginDialogId.CLAIM_TRANSACTION]: {
Component: CapitalDistributorClaimTransactionDialog,
+ requiresWallet: true,
},
};
diff --git a/apps/app/src/plugins/gaugeVoterPlugin/constants/gaugeVoterPluginDialogsDefinitions.ts b/apps/app/src/plugins/gaugeVoterPlugin/constants/gaugeVoterPluginDialogsDefinitions.ts
index dbd95aafb..b3cf1f0da 100644
--- a/apps/app/src/plugins/gaugeVoterPlugin/constants/gaugeVoterPluginDialogsDefinitions.ts
+++ b/apps/app/src/plugins/gaugeVoterPlugin/constants/gaugeVoterPluginDialogsDefinitions.ts
@@ -30,9 +30,11 @@ export const gaugeVoterPluginDialogsDefinitions: Record<
[GaugeVoterPluginDialogId.VOTE_GAUGES_TRANSACTION]: {
Component: GaugeVoterVoteTransactionDialog,
size: 'md',
+ requiresWallet: true,
},
[GaugeVoterPluginDialogId.LOCK_UNLOCK]: {
Component: GaugeVoterLockUnlockDialog,
+ requiresWallet: true,
},
[GaugeVoterPluginDialogId.VIEW_LOCKS]: {
Component: GaugeVoterLocksDialog,
@@ -42,6 +44,7 @@ export const gaugeVoterPluginDialogsDefinitions: Record<
},
[GaugeVoterPluginDialogId.EXIT_QUEUE_WITHDRAW_TRANSACTION]: {
Component: GaugeVoterExitQueueWithdrawTransactionDialog,
+ requiresWallet: true,
},
[GaugeVoterPluginDialogId.LOCK_ONBOARDING_INTRO]: {
Component: GaugeVoterLockOnboardingIntroDialog,
diff --git a/apps/app/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts b/apps/app/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts
index 5c9763586..efe905072 100644
--- a/apps/app/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts
+++ b/apps/app/src/plugins/lockToVotePlugin/constants/lockToVotePluginDialogsDefinitions.ts
@@ -25,6 +25,7 @@ export const lockToVotePluginDialogsDefinitions: Record<
},
[LockToVotePluginDialogId.LOCK_UNLOCK_L2V]: {
Component: LockToVoteLockUnlockDialog,
+ requiresWallet: true,
},
[LockToVotePluginDialogId.UNLOCK_BLOCKED_INFO]: {
Component: UnlockBlockedInfoDialog,
diff --git a/apps/app/src/plugins/sppPlugin/constants/sppPluginDialogsDefinitions.ts b/apps/app/src/plugins/sppPlugin/constants/sppPluginDialogsDefinitions.ts
index fe3e4cf0f..097b55d27 100644
--- a/apps/app/src/plugins/sppPlugin/constants/sppPluginDialogsDefinitions.ts
+++ b/apps/app/src/plugins/sppPlugin/constants/sppPluginDialogsDefinitions.ts
@@ -8,9 +8,13 @@ export const sppPluginDialogsDefinitions: Record<
SppPluginDialogId,
IDialogComponentDefinitions
> = {
- [SppPluginDialogId.ADVANCE_STAGE]: { Component: SppAdvanceStageDialog },
+ [SppPluginDialogId.ADVANCE_STAGE]: {
+ Component: SppAdvanceStageDialog,
+ requiresWallet: true,
+ },
[SppPluginDialogId.REPORT_PROPOSAL_RESULT]: {
Component: SppReportProposalResultDialog,
+ requiresWallet: true,
},
[SppPluginDialogId.INVALID_ADDRESS_CONNECTED]: {
Component: SppInvalidAddressConnectedDialog,
diff --git a/apps/app/src/plugins/tokenPlugin/constants/tokenPluginDialogsDefinitions.ts b/apps/app/src/plugins/tokenPlugin/constants/tokenPluginDialogsDefinitions.ts
index 40e692fcf..310ed215e 100644
--- a/apps/app/src/plugins/tokenPlugin/constants/tokenPluginDialogsDefinitions.ts
+++ b/apps/app/src/plugins/tokenPlugin/constants/tokenPluginDialogsDefinitions.ts
@@ -13,12 +13,22 @@ export const tokenPluginDialogsDefinitions: Record<
TokenPluginDialogId,
IDialogComponentDefinitions
> = {
- [TokenPluginDialogId.DELEGATE]: { Component: TokenDelegationDialog },
+ [TokenPluginDialogId.DELEGATE]: {
+ Component: TokenDelegationDialog,
+ requiresWallet: true,
+ },
[TokenPluginDialogId.APPROVE_TOKENS]: {
Component: TokenApproveTokensDialog,
+ requiresWallet: true,
+ },
+ [TokenPluginDialogId.APPROVE_NFT]: {
+ Component: TokenApproveNftDialog,
+ requiresWallet: true,
+ },
+ [TokenPluginDialogId.WRAP_UNWRAP]: {
+ Component: TokenWrapUnwrapDialog,
+ requiresWallet: true,
},
- [TokenPluginDialogId.APPROVE_NFT]: { Component: TokenApproveNftDialog },
- [TokenPluginDialogId.WRAP_UNWRAP]: { Component: TokenWrapUnwrapDialog },
[TokenPluginDialogId.DELEGATION_ONBOARDING_INTRO]: {
Component: TokenDelegationOnboardingIntroDialog,
hiddenTitle:
diff --git a/apps/app/src/shared/components/dialogProvider/dialogProvider.api.ts b/apps/app/src/shared/components/dialogProvider/dialogProvider.api.ts
index 39715fb56..fa64d7afe 100644
--- a/apps/app/src/shared/components/dialogProvider/dialogProvider.api.ts
+++ b/apps/app/src/shared/components/dialogProvider/dialogProvider.api.ts
@@ -103,4 +103,11 @@ export interface IDialogComponentDefinitions<
* Component to be rendered.
*/
Component: ComponentType>;
+ /**
+ * Marks a dialog that requires a connected wallet to be meaningful: DialogRoot unmounts it
+ * and closes it on wallet disconnect, before a dialog reading the address can throw.
+ * Dialogs stacked on top of a flagged dialog should be flagged too, otherwise the child is
+ * left orphaned on the stack once its parent closes.
+ */
+ requiresWallet?: boolean;
}
diff --git a/apps/app/src/shared/components/dialogRoot/dialogRoot.test.tsx b/apps/app/src/shared/components/dialogRoot/dialogRoot.test.tsx
index dc9824fd1..9649cb2e8 100644
--- a/apps/app/src/shared/components/dialogRoot/dialogRoot.test.tsx
+++ b/apps/app/src/shared/components/dialogRoot/dialogRoot.test.tsx
@@ -1,5 +1,8 @@
+import { invariant } from '@aragon/gov-ui-kit';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
+import { useState } from 'react';
+import * as useWalletAccountHook from '@/modules/application/hooks/useWalletAccount';
import { generateDialogContext } from '@/shared/testUtils';
import { testLogger } from '@/test/utils';
import * as useDialogContext from '../dialogProvider';
@@ -11,12 +14,33 @@ describe(' component', () => {
'useDialogContext',
);
+ const useWalletAccountSpy = jest.spyOn(
+ useWalletAccountHook,
+ 'useWalletAccount',
+ );
+
+ const connectedAccount = {
+ address: '0x1234567890123456789012345678901234567890',
+ chainId: 1,
+ isConnecting: false,
+ isReconnecting: false,
+ } as const;
+
+ const disconnectedAccount = {
+ address: undefined,
+ chainId: 1,
+ isConnecting: false,
+ isReconnecting: false,
+ } as const;
+
beforeEach(() => {
useDialogContextSpy.mockReturnValue(generateDialogContext());
+ useWalletAccountSpy.mockReturnValue({ ...connectedAccount });
});
afterEach(() => {
useDialogContextSpy.mockReset();
+ useWalletAccountSpy.mockReset();
});
const createTestComponent = (props?: Partial) => {
@@ -95,4 +119,163 @@ describe(' component', () => {
render(createTestComponent({ dialogs }));
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
});
+
+ it('renders a wallet-requiring dialog while the wallet is connected', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+ const dialogs = {
+ [dialogId]: {
+ Component: () => dialogContent,
+ requiresWallet: true,
+ },
+ };
+ const locations = [{ id: dialogId }];
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations }),
+ );
+ render(createTestComponent({ dialogs }));
+ expect(screen.getByText(dialogContent)).toBeInTheDocument();
+ });
+
+ it('unmounts and closes a wallet-requiring dialog when the wallet disconnects', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+ const dialogs = {
+ [dialogId]: {
+ Component: () => dialogContent,
+ requiresWallet: true,
+ },
+ };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+ useWalletAccountSpy.mockReturnValue({ ...disconnectedAccount });
+ render(createTestComponent({ dialogs }));
+ expect(screen.queryByText(dialogContent)).not.toBeInTheDocument();
+ expect(close).toHaveBeenCalledWith(dialogId);
+ });
+
+ it('keeps dialogs without the requires-wallet flag open when the wallet disconnects', () => {
+ const dialogId = 'connect-wallet';
+ const dialogContent = 'connect-wallet-content';
+ const dialogs = { [dialogId]: { Component: () => dialogContent } };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+ useWalletAccountSpy.mockReturnValue({ ...disconnectedAccount });
+ render(createTestComponent({ dialogs }));
+ expect(screen.getByText(dialogContent)).toBeInTheDocument();
+ expect(close).not.toHaveBeenCalled();
+ });
+
+ it('unmounts but does not close a wallet-requiring dialog while reconnecting', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+ const dialogs = {
+ [dialogId]: {
+ Component: () => dialogContent,
+ requiresWallet: true,
+ },
+ };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+ useWalletAccountSpy.mockReturnValue({
+ ...disconnectedAccount,
+ isReconnecting: true,
+ });
+ render(createTestComponent({ dialogs }));
+ expect(screen.queryByText(dialogContent)).not.toBeInTheDocument();
+ expect(close).not.toHaveBeenCalled();
+ });
+
+ it('unmounts but does not close a wallet-requiring dialog while connecting on a connector switch', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+ const dialogs = {
+ [dialogId]: {
+ Component: () => dialogContent,
+ requiresWallet: true,
+ },
+ };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+ useWalletAccountSpy.mockReturnValue({
+ ...disconnectedAccount,
+ isConnecting: true,
+ });
+ render(createTestComponent({ dialogs }));
+ expect(screen.queryByText(dialogContent)).not.toBeInTheDocument();
+ expect(close).not.toHaveBeenCalled();
+ });
+
+ it('restores a wallet-requiring dialog once the connector switch completes', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+ const dialogs = {
+ [dialogId]: {
+ Component: () => dialogContent,
+ requiresWallet: true,
+ },
+ };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+
+ useWalletAccountSpy.mockReturnValue({
+ ...disconnectedAccount,
+ isConnecting: true,
+ });
+ const { rerender } = render(createTestComponent({ dialogs }));
+ expect(screen.queryByText(dialogContent)).not.toBeInTheDocument();
+
+ useWalletAccountSpy.mockReturnValue({ ...connectedAccount });
+ rerender(createTestComponent({ dialogs }));
+
+ expect(screen.getByText(dialogContent)).toBeInTheDocument();
+ expect(close).not.toHaveBeenCalled();
+ });
+
+ it('does not throw when the wallet disconnects while a wallet-requiring dialog is open', () => {
+ const dialogId = 'vote';
+ const dialogContent = 'vote-content';
+
+ // Mirrors the real wallet-requiring dialogs: address assertion followed by more hooks.
+ const VoteDialog: React.FC = () => {
+ const { address } = useWalletAccountHook.useWalletAccount();
+ invariant(address != null, 'VoteDialog: user must be connected.');
+ const [state] = useState(dialogContent);
+
+ return state;
+ };
+
+ const dialogs = {
+ [dialogId]: { Component: VoteDialog, requiresWallet: true },
+ };
+ const locations = [{ id: dialogId }];
+ const close = jest.fn();
+ useDialogContextSpy.mockReturnValue(
+ generateDialogContext({ locations, close }),
+ );
+
+ const { rerender } = render(createTestComponent({ dialogs }));
+ expect(screen.getByText(dialogContent)).toBeInTheDocument();
+
+ useWalletAccountSpy.mockReturnValue({ ...disconnectedAccount });
+
+ expect(() => rerender(createTestComponent({ dialogs }))).not.toThrow();
+ expect(screen.queryByText(dialogContent)).not.toBeInTheDocument();
+ expect(close).toHaveBeenCalledWith(dialogId);
+ });
});
diff --git a/apps/app/src/shared/components/dialogRoot/dialogRoot.tsx b/apps/app/src/shared/components/dialogRoot/dialogRoot.tsx
index 370f3c07c..f683f4bc5 100644
--- a/apps/app/src/shared/components/dialogRoot/dialogRoot.tsx
+++ b/apps/app/src/shared/components/dialogRoot/dialogRoot.tsx
@@ -5,6 +5,8 @@ import {
DialogAlert,
type IDialogRootProps as IGukDialogRootProps,
} from '@aragon/gov-ui-kit';
+import { useEffect } from 'react';
+import { useWalletAccount } from '@/modules/application/hooks/useWalletAccount';
import {
type IDialogComponentDefinitions,
useDialogContext,
@@ -23,6 +25,29 @@ export const DialogRoot: React.FC = (props) => {
const { t } = useTranslations();
const { locations, close } = useDialogContext();
+ const { address, isConnecting, isReconnecting } = useWalletAccount();
+
+ // Wallet-requiring dialogs read the address during render, so they must be unmounted from
+ // here before a disconnect makes them throw.
+ // Only close once wagmi settles: both 'connecting' (connector switch) and 'reconnecting'
+ // (mount) blank the address for a moment without the wallet actually being gone.
+
+ const missingAddress = address == null;
+ const walletSettling = isConnecting || isReconnecting;
+ const walletDisconnected = missingAddress && !walletSettling;
+
+ useEffect(() => {
+ if (!walletDisconnected) {
+ return;
+ }
+
+ // Use the context close directly as custom onClose handlers might not pop the stack.
+ for (const location of locations) {
+ if (dialogs[location.id]?.requiresWallet) {
+ close(location.id);
+ }
+ }
+ }, [walletDisconnected, locations, dialogs, close]);
// Render each dialog in the stack but only top one should be visible.
// Non-visible dialogs should still be rendered in order to keep the state. Useful in parent-child dialog relationships.
@@ -40,9 +65,16 @@ export const DialogRoot: React.FC = (props) => {
Component: ActiveDialogComponent,
hiddenTitle,
hiddenDescription,
+ requiresWallet,
...otherDialogProps
} = dialogDefinition;
+ // Unmount before the dialog renders without an address. While the wallet is
+ // settling this only hides it; a real disconnect also pops it via the effect above.
+ if (missingAddress && requiresWallet) {
+ return null;
+ }
+
const isAlertDialog = 'variant' in otherDialogProps;
const { disableOutsideClick, modal, onClose } = location;
diff --git a/apps/app/src/shared/hooks/useDaoExecutePermission/useDaoExecutePermission.test.ts b/apps/app/src/shared/hooks/useDaoExecutePermission/useDaoExecutePermission.test.ts
index 5002a8e1c..eafe5bb8b 100644
--- a/apps/app/src/shared/hooks/useDaoExecutePermission/useDaoExecutePermission.test.ts
+++ b/apps/app/src/shared/hooks/useDaoExecutePermission/useDaoExecutePermission.test.ts
@@ -20,6 +20,7 @@ describe('useDaoExecutePermission hook', () => {
useWalletAccountSpy.mockReturnValue({
address: walletAddress,
chainId: 1,
+ isConnecting: false,
isReconnecting: false,
});
useReadContractSpy.mockReturnValue({
@@ -75,6 +76,7 @@ describe('useDaoExecutePermission hook', () => {
useWalletAccountSpy.mockReturnValue({
address: undefined,
chainId: undefined,
+ isConnecting: false,
isReconnecting: false,
});
diff --git a/apps/app/src/shared/hooks/useNetworkSwitch/useNetworkSwitch.test.ts b/apps/app/src/shared/hooks/useNetworkSwitch/useNetworkSwitch.test.ts
index 617877e98..68c354a86 100644
--- a/apps/app/src/shared/hooks/useNetworkSwitch/useNetworkSwitch.test.ts
+++ b/apps/app/src/shared/hooks/useNetworkSwitch/useNetworkSwitch.test.ts
@@ -23,6 +23,7 @@ describe('useNetworkSwitch hook', () => {
useWalletAccountSpy.mockReturnValue({
address: '0x123',
chainId: ethereumChainId,
+ isConnecting: false,
isReconnecting: false,
});
});
@@ -54,6 +55,7 @@ describe('useNetworkSwitch hook', () => {
useWalletAccountSpy.mockReturnValue({
address: undefined,
chainId: undefined,
+ isConnecting: false,
isReconnecting: false,
});
@@ -94,6 +96,7 @@ describe('useNetworkSwitch hook', () => {
useWalletAccountSpy.mockReturnValue({
address: undefined,
chainId: undefined,
+ isConnecting: false,
isReconnecting: false,
});