Repository navigation
fix(wallet): mount network-mismatch banner app-wide and gate every write on one shared guard - #1585
Open
sommydregent wants to merge 1 commit into
Open
sommydregent wants to merge 1 commit into
sommydregent wants to merge 1 commit into
Conversation
…ite on one shared guard (Disciplr-Org#1510) - Mount the existing NetworkMismatchBanner in Layout (role="alert" region before <main>) so a wrong-network wallet is announced on every route. - Add src/hooks/useWriteGuard.ts: one shared guard returning { canWrite, reason } that refuses submits while the wallet is disconnected / restoring, the address is missing or invalid, the network is still unknown, or the wallet network mismatches APP_EXPECTED_NETWORK. Unknown is treated as "not yet known", not as a mismatch, but writes stay disabled. Demo mode (TESTNET) passes when the app expects TESTNET. - Wire the guard into every write surface: CreateVault confirm (replaces the ad-hoc inline network check), VaultDetail action confirms (re-check at submit time; buttons stay gated by evalVaultActionAuth), ValidationDetail approve/reject and PendingValidations batch approve/reject (buttons disabled with the reason as an aria-describedby description, and handlers are no-ops when called directly with a bad wallet). - Declare VITE_DISCIPLR_NETWORK in ImportMetaEnv. Tests: useWriteGuard state matrix, Layout banner mount, and one RTL test per write surface asserting the disabled state and that the service is never called when the write is forced (including a wallet that flips networks after the modal opens). Existing suites updated where their wallet mocks now need to satisfy the guard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1510
What and why
Disciplr ships a complete
NetworkMismatchBanner(component, telemetry and tests) that was never rendered anywhere outside its test, and the wallet network checks for write actions were ad hoc:CreateVaulthad an inline check,VaultDetailwas covered byevalVaultActionAuth, and the verifier pages (ValidationDetail,PendingValidations) checked nothing before approve/reject. Acting on mainnet while the app is pinned to testnet (or the reverse) is the classic way to lose funds or produce confusing failures in a Stellar dApp — and a guard that exists on one page but not another only gives a false sense of safety.This PR mounts the banner app-wide and enforces one shared write guard on every write surface.
Changes
App-wide banner (
src/components/Layout.tsx)<NetworkMismatchBanner />directly after<TrustlineBanner />, before<main>: an app-widerole="alert"region, so with the wallet on PUBLIC and the app on TESTNET the warning is visible on every route and announced to screen readers (role="alert"). No banner shows when networks match, and dismissing still hides it until the mismatch key (address, network) changes — that behaviour already lived in the component and is covered by its existing tests.Shared guard (
src/hooks/useWriteGuard.ts, new)useWriteGuard()returns{ canWrite, reason }derived from the wallet context: session connected, address present and checksum-valid (isValidStellarAddress), and network matchingAPP_EXPECTED_NETWORK.aria-describedbycontent and error messages.null: unknown is treated as "not yet known", not a mismatch (no banner flash), but writes remain disabled until the state is known.evaluateWriteGuard(snapshot, expectedNetwork)plus the hook, so the state matrix is testable without mounting Freighter.Write surfaces wired to the guard
CreateVault.tsx— replaces the ad-hoc inlineisNetworkMismatchcheck inhandleConfirmwith the shared guard, and the review screen's Confirm Vault button is disabled with the reason as its accessible description (CreateVaultReview.tsxgainedconfirmDisabled/confirmDisabledReasonprops wired viaaria-describedby+ screen-reader text).VaultDetail.tsx— action buttons stay gated byevalVaultActionAuth(which already explains network mismatches via disabled + reason text), andhandleActionConfirmnow re-checks the guard at submit time, so a wallet that flips networks after the modal opens can never reachsubmitVaultAction.ValidationDetail.tsx— Approve / Reject buttons are disabled while the guard refuses, with the reason exposed asaria-describedbyscreen-reader text;handleOpenModalandexecuteActionboth re-check, so calling them directly is a no-op (error surfaced, no store call).PendingValidations.tsx— batch Approve / Reject Selected buttons are disabled witharia-describedbyreason;openBatchandhandleConfirmre-check so confirm-after-flip is a no-op and never callsbatchApprove/batchReject.Environment contract (
src/vite-env.d.ts)VITE_DISCIPLR_NETWORKinImportMetaEnv—networkMismatch.tsalready reads it, but it was undocumented, contradicting the file's "declare everything" contract.Implementation notes / decisions
isConnecting+ address presence whenstatusis absent, so legacy store shapes and test fixtures keep working;statusstays authoritative when present.Testing
New tests (all passing):
src/hooks/__tests__/useWriteGuard.test.tsx— the full state matrix: no context, disconnected, restoring (null network ≠ mismatch), connecting, invalid address, network not yet known, PUBLIC-on-TESTNET mismatch, unknown network string ("FUTURENET"), OK state, demo mode, and a state-change/rerender case.src/components/__tests__/Layout.networkBanner.test.tsx— the banner renders insideLayoutwith a mocked mismatched wallet as arole="alert"region positioned before<main>, and stays hidden for matching/disconnected wallets.CreateVault.networkGuard.test.tsx— fires the real form submit, review opens, Confirm Vault disabled +aria-describedbyreason,createVaultnever called.VaultDetail.networkGuard.test.tsx— opens the action modal in a good state, then flips the wallet to PUBLIC and confirms:submitVaultActionnever called, mismatch error shown.ValidationDetail.networkGuard.test.tsx— buttons disabled with reason; opening-and-forcing confirm after a network flip never reaches the decision store.PendingValidations.networkGuard.test.tsx— batch buttons disabled with reason; confirm-after-flip never reachesbatchApprove/batchReject; the matched-network path still works end-to-end.Existing suites updated only where their wallet mocks now need to satisfy the guard (
ValidationDetail.test.tsx,verifierFlow.integration.test.tsxgained a good-stateWalletContextmock; the fourCreateVault.*suites exposeuseOptionalWallet;Layout.a11y.test.tsxstubs the banner like its other chrome stubs).Verification: full
vitest runsuite green (162 files, 2078 tests),tsc --noEmitdiffed againstupstream/mainproduces no new errors (only pre-existing ones with shifted line numbers), andeslinton all touched files reports no new issues.Screenshots
<main>on every route, and Create confirm / Validate / Cancel / Approve / Reject / batch actions are disabled with a screen-reader explanation; when the network matches, nothing visible changes from before.