Skip to content

Wallet Connection & Network-Switch Guard with Persistent Session - #45

Merged
meshackyaro merged 2 commits into
workman-labs:devfrom
libby-coder:feature/25-wallet-network-switch-guard
Aug 22, 2026
Merged

Wallet Connection & Network-Switch Guard with Persistent Session#45
meshackyaro merged 2 commits into
workman-labs:devfrom
libby-coder:feature/25-wallet-network-switch-guard

Conversation

@libby-coder

Copy link
Copy Markdown
Contributor

Summary

Closes #25.

  • useWallet() (src/lib/wallet.ts) now restores a session on reload by verifying Freighter's own isAllowed() grant instead of trusting a localStorage flag alone, so a session revoked inside the extension self-heals instead of showing stale "connected" UI.
  • A live guard, backed by Freighter's WatchWalletChanges poller, exposes isWrongNetwork / expectedNetwork for the lifetime of a connected session and reacts the moment the user switches networks or accounts inside the extension — no reload required.
  • NetworkGuard (src/components/wallet/NetworkGuard.tsx), mounted app-wide in the root layout, shows a banner guiding the user through switching networks inside Freighter (there's no API for a dApp to switch it on their behalf) and clears itself automatically once the watcher detects the change; a "check again" action short-circuits the wait.
  • EscrowFundingWizard's connect-wallet step now blocks progress while the wallet is on the wrong network, since funding escrow there isn't recoverable after the fact.
  • Fixed WalletButton being defined but never mounted in the navbar (its styling was also updated to match the current sand/light navbar — it was written for an older dark navbar).
  • The expected network is configurable via NEXT_PUBLIC_STELLAR_NETWORK (defaults to TESTNET; see .env.example).

No new dependenciesWatchWalletChanges and isAllowed() are both already part of the installed @stellar/freighter-api version. Documented in the README's "Web3 / Stellar touches" section.

Test plan

  • npm run lint — 0 errors (pre-existing react-hooks/set-state-in-effect warnings only, consistent with the rest of the codebase)
  • npx tsc --noEmit — no TypeScript errors
  • npm test — 85/85 passing, including a new src/lib/test/wallet.test.tsx covering connect, freighter-missing, declined access, wrong-network detection, session restore (allowed/revoked/uninstalled), the live watcher's network-change and access-revoked paths, manual recheck, and disconnect
  • npm run build — production build succeeds
  • Verified in a live dev server with Playwright (no Freighter extension installed in this environment): navbar renders "Connect Wallet", clicking it shows the "Freighter wallet not found — Install it" hint, no console errors

…Closes workman-labs#25)

Extends useWallet() with session restore that verifies Freighter's isAllowed()
grant (not just a localStorage flag, so a revoked/stale session self-heals) and
a live WatchWalletChanges-backed guard that detects the wrong Stellar network
and guides the user to switch inside the extension, since Freighter has no API
to switch networks on a dApp's behalf. NetworkGuard surfaces this app-wide, and
EscrowFundingWizard now blocks progress past wallet-connect on the wrong
network. Also wires WalletButton into the navbar, where it was defined but
never mounted.
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

@libby-coder is attempting to deploy a commit to the Meshack Yaro's projects Team on Vercel.

A member of the Team first needs to authorize it.

@meshackyaro

Copy link
Copy Markdown
Contributor

Summary

Closes #25.

  • useWallet() (src/lib/wallet.ts) now restores a session on reload by verifying Freighter's own isAllowed() grant instead of trusting a localStorage flag alone, so a session revoked inside the extension self-heals instead of showing stale "connected" UI.
  • A live guard, backed by Freighter's WatchWalletChanges poller, exposes isWrongNetwork / expectedNetwork for the lifetime of a connected session and reacts the moment the user switches networks or accounts inside the extension — no reload required.
  • NetworkGuard (src/components/wallet/NetworkGuard.tsx), mounted app-wide in the root layout, shows a banner guiding the user through switching networks inside Freighter (there's no API for a dApp to switch it on their behalf) and clears itself automatically once the watcher detects the change; a "check again" action short-circuits the wait.
  • EscrowFundingWizard's connect-wallet step now blocks progress while the wallet is on the wrong network, since funding escrow there isn't recoverable after the fact.
  • Fixed WalletButton being defined but never mounted in the navbar (its styling was also updated to match the current sand/light navbar — it was written for an older dark navbar).
  • The expected network is configurable via NEXT_PUBLIC_STELLAR_NETWORK (defaults to TESTNET; see .env.example).

No new dependenciesWatchWalletChanges and isAllowed() are both already part of the installed @stellar/freighter-api version. Documented in the README's "Web3 / Stellar touches" section.

Test plan

  • npm run lint — 0 errors (pre-existing react-hooks/set-state-in-effect warnings only, consistent with the rest of the codebase)
  • npx tsc --noEmit — no TypeScript errors
  • npm test — 85/85 passing, including a new src/lib/test/wallet.test.tsx covering connect, freighter-missing, declined access, wrong-network detection, session restore (allowed/revoked/uninstalled), the live watcher's network-change and access-revoked paths, manual recheck, and disconnect
  • npm run build — production build succeeds
  • Verified in a live dev server with Playwright (no Freighter extension installed in this environment): navbar renders "Connect Wallet", clicking it shows the "Freighter wallet not found — Install it" hint, no console errors

This looks good to me but not without some minor changes — this is a solid, well-tested feature that meaningfully improves wallet UX and session correctness.

Merge readiness and risk assessment

  • CI/tests: Author reports npx tsc, lint, unit tests, and production build all pass locally. Assuming CI replicates this, it's a strong signal.
  • Code quality: Code appears well-scoped and focused. Tests improve confidence dramatically.
  • Risk: Low-to-medium because wallet/session lifecycle and network-guarding affect funding flows and could block users if incorrect. Tests cover many cases which mitigates risk.
  • Chores before merge (non-diff items):
    • Ensure CI runs the new tests and Playwright/regression checks where applicable.
    • Confirm any required reviewers have been requested.
  • src/lib/wallet.ts — Add explicit cleanup when a session ends or the hook/component unmounts: stop the WatchWalletChanges poller (clearInterval / stopWatching) and remove any event listeners to avoid background polling or memory leaks. Please ensure disconnect() always stops the watcher.
  • src/components/wallet/NetworkGuard.tsx — Improve accessibility: make the banner use role="alert" or aria-live="polite"/"assertive" as appropriate, ensure it is keyboard-focusable when it appears (move focus to the banner or a skip link), and expose a clear accessible label for the "check again" action.
  • src/components/navbar/WalletButton.tsx (or wherever WalletButton was added) — Confirm the button is mounted only once and that CSS changes preserve sufficient color contrast in both light and sand themes. Ensure the element includes accessible text (aria-label) when it uses icon-only rendering.
  • src/lib/test/wallet.test.tsx — Avoid fragile time-based assertions around WatchWalletChanges polling; where possible, mock the poller or use jest fake timers to deterministically advance time. This will make the tests less flaky in CI.
  • .env.example and README — In .env.example and the README section you added, explicitly list valid values for NEXT_PUBLIC_STELLAR_NETWORK (e.g., TESTNET, PUBLIC) and the default. Also mention that changing this changes which network the dApp expects and the security implications of funding on the wrong network.

Possible improvements

  • src/lib/wallet.ts — Consider typing expectedNetwork as a small union type or an enum (e.g., type Network = 'TESTNET' | 'PUBLIC') to prevent subtle typos and make the config safer across the codebase.
  • src/components/wallet/NetworkGuard.tsx — Add a small telemetry/event hook (opt-in) so when a user encounters the guard you can track how often users hit the wrong network (optional, privacy-considerate).
  • src/lib/test/wallet.test.tsx — Add tests that simulate Freighter being uninstalled after a session was active (the session-revoke path is mentioned — ensure test covers uninstalled extension behavior explicitly).

Overall, this PR is well-scoped and well-tested. Addressing the cleanup/accessibility/clarity items above will reduce risk further and make the feature more robust in CI and production.

…edback

Fixes a real bug the review surfaced: useWallet() previously ran a fully
independent instance (session restore + its own WatchWalletChanges poller)
per call site, so the navbar's desktop/mobile WalletButton and NetworkGuard
were each polling Freighter separately and could drift out of sync. Renamed
the engine to useWalletState() and added WalletProvider (mirroring
ThemeProvider's existing pattern) so the whole app shares one instance.

Also: NetworkGuard moves focus to itself and gives its recheck action an
explicit aria-label; WalletButton's collapsed trigger gets an aria-label
covering the wrong-network state and decorative icons are aria-hidden;
expectedNetwork is now typed as StellarNetwork instead of string; documented
NEXT_PUBLIC_STELLAR_NETWORK's valid values and security implications in
.env.example/README; added a regression test proving WalletProvider shares
one instance/poller across consumers and an explicit
extension-uninstalled-mid-session test.
@libby-coder

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Pushed a follow-up commit addressing this:

Cleanup / duplicate polling — good catch. disconnect() and the unmount effect already stopped the watcher, but you were right to push on "mounted only once": useWallet() was a plain hook, so the navbar's desktop and mobile WalletButton (both mounted at once, one hidden via CSS) plus NetworkGuard were each running an independent session-restore + WatchWalletChanges poller. Renamed the engine to useWalletState() and added WalletProvider (same pattern as the existing ThemeProvider) so the whole app shares one instance/poller. Added a regression test (walletProvider.test.tsx) that asserts exactly one poller exists across multiple consumers.

NetworkGuard accessibility — it now moves focus to itself (tabIndex={-1} + .focus()) the moment it appears, and the "check again" button has an explicit aria-label. role="alert" was already there (it implies aria-live="assertive", so I didn't duplicate that).

WalletButton — added an aria-label on the collapsed trigger that includes the wrong-network state (previously only visible inside the opened dropdown), aria-hidden on the decorative dot/icons, and aria-expanded. On contrast: the colors are all existing design-system tokens (--ok, --gold-deep, --line) already used elsewhere for status indicators, not new arbitrary values, and it's mounted only via the shared provider now, not duplicated logic.

Test flakiness — worth flagging: the tests never used real timers or the library's actual polling loop. WatchWalletChanges is fully mocked with a fake whose callback is only ever invoked by hand (watcher.cb?.(...)) — there's no setTimeout in the fake at all, so nothing depends on wall-clock time. I added a comment in the test file making that explicit, and a dedicated test for the extension-uninstalled-mid-session case you asked for (distinct from the revoked-access one, same code path).

TypingexpectedNetwork (and the ConnectWalletStep prop) now use the existing StellarNetwork union ("PUBLIC" | "TESTNET" | "FUTURENET") instead of string.

.env.example / README — both now spell out the three valid values and call out explicitly that this is security-relevant (it's what the guard checks before letting funds move), not just a label.

Telemetry hook — left this out; it's explicitly optional and there's no analytics infrastructure in this project yet, so adding one would be speculative for this PR.

CI choresnpm test in ci.yml already picks up the new test files automatically (no CI config change needed). There's no CODEOWNERS file in this repo, so I don't have a specific reviewer to request — let me know if you'd like me to add one or tag someone directly.

Verified again after these changes: tsc --noEmit clean, eslint 0 errors, npm test 88/88 passing, production build succeeds, and a live dev-server Playwright smoke check still shows no console errors.

@meshackyaro meshackyaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent follow-up. This commit fixes a real bug and substantially improves the code quality on several fronts:

What this does well:

Architecture: The WalletProvider context pattern (mirroring ThemeProvider) is the right fix for the dual-instance problem. I appreciate how clearly you've documented the WHY in the hook's comment block — that failure mode (independent desktop/mobile buttons polling separately and drifting) is exactly the kind of subtle bug that ships into production undetected.

Accessibility: Solid additions across the board:

  • NetworkGuard now moves focus when it appears — that's correct for an urgent role="alert" region.
  • WalletButton's aria-labels are precise and context-aware (e.g., "wrong network, action needed" conditional).
  • Decorative icons are now properly aria-hidden.
  • The recheck button's aria-label includes state ("checking now") — nice touch.

Tests: The new walletProvider.test.tsx directly validates the one-instance invariant with two consumers in the same provider — exactly the scenario that would have exposed this bug in CI if the original code had made it through. The "extension uninstalled mid-session" test in wallet.test.tsx covers an edge case I wouldn't have thought to verify.

Documentation: The .env.example and README updates appropriately flag NEXT_PUBLIC_STELLAR_NETWORK as security-relevant, not just cosmetic. That's important guidance for the next person who deploys this.

Minor suggestions (not blockers):

  1. Type export in wallet.ts: You're re-exporting StellarNetwork from wallet.ts for consumer convenience, which is good. Consider doing the same for the full WalletContextValue type in the WalletProvider module itself — right now a consumer who wants to type-annotate a ref to the context has to reach into lib/wallet. (Not a real pain point, just a polish detail.)

  2. NetworkGuard's address display: The line truncateAddress(address) in the banner is helpful, but I'd make sure the truncation is consistent with what the WalletButton uses — if someone sees "G...YZ" in the banner and "G...AB" in the button's label (due to a truncation mismatch), that confusion outweighs the added verbosity. A quick scan suggests they're already aligned, but worth a comment noting it.

Meanwhile, this is ship-ready. It's a solid architectural correction backed by thoughtful testing and accessibility work. Well done responding to the review feedback.

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
guildworkman-web Ready Ready Preview Aug 22, 2026 9:16pm

@meshackyaro
meshackyaro merged commit ccb933e into workman-labs:dev Aug 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wallet Connection & Network-Switch Guard with Persistent Session

2 participants