Skip to content

feat(react): harden wallet detection, Albedo fallback and session persistence - #1266

Open
memplethee-lab wants to merge 13 commits into
BCPathway:mainfrom
memplethee-lab:feature/wallet-adapters-and-session-persistence
Open

memplethee-lab wants to merge 13 commits into
BCPathway:mainfrom
memplethee-lab:feature/wallet-adapters-and-session-persistence

Conversation

@memplethee-lab

Copy link
Copy Markdown

Summary Hardens the wallet connection layer with SSR-safe detection, an extension-free fallback, and reload-persistent sessions. - Freighter detection (#1169) ΓÇö isFreighterInstalled() probes the exact global FreighterAdapter reads and distinguishes present from present but broken. whenFreighterReady() handles asynchronous extension injection with a bounded ~1s budget and an AbortSignal for cancellation. The uninstalled path renders an install link and a clean error instead of throwing. - Albedo fallback (#1170) ΓÇö loadAlbedoScript() lazily injects the Albedo intent script, adopting an existing tag, sharing one in-flight promise, and polling for the global after load. Failures resolve to a structured reason rather than throwing, and a failed tag is recorded so it is not retried on every render. @albedo-link/intent is not added as an npm dependency. - WalletSelectModal (#1171) ΓÇö lists detected wallets with inline-SVG icons, Installed / Not installed badges and arget="_blank" rel="noopener noreferrer" install links, highlights what is available, and starts the connection on selection. Self-contained: own portal, focus trap, Escape, backdrop dismissal, and closing is blocked while a connection is in flight. - Session persistence (#1172) ΓÇö �c-forge:wallet-session and �c-forge:wallet-preference store only the wallet name (never a secret or a stale address), validated against the WalletName union on read and tolerant of malformed JSON. Storage access is wrapped for Safari private mode and degrades to no-ops. ## Notes for the reviewer - useWallet() is additive only ΓÇö status, connected, publicKey, ame, error, connect and disconnect are unchanged. connectWallet, �vailableWallets and estoring are new. - connect / disconnect now read state through a ref, which gives them stable identities. This is what stops the restore effect from re-firing on every state change. - Session storage is read inside an effect, never during render, so server and client first paint are identical ΓÇö no hydration mismatch. - A restored session reuses the existing passphrase validation, so reconnecting on the wrong network lands in the existing wrong-network state rather than silently proceeding. A session whose wallet has since been uninstalled is cleared and stays disconnected. - Two existing test files (context.test.tsx, ConnectWallet.test.tsx) needed a localStorage.clear() in �eforeEach. Without it, a session saved by one test is silently reconnected at the start of the next one and races the existing assertions ΓÇö a direct consequence of persisting state by default. - estoreSession defaults to rue to match the acceptance criteria; pass estoreSession={false} to opt out. ## Verification Nothing here has been machine-verified ΓÇö jest, eslint and tsc were not run, as requested. ΓÜá∩╕Å One step needed before CI is green: pm run test:visual:update --workspace @bc-forge/react on the arm64 Playwright image. The visual suite screenshots every story against zero-tolerance baselines with updateSnapshots: 'none', and the new Wallet/ stories have no committed PNGs. Note that the modal portals to document.body, so the story-frame baseline captures the trigger button only. Closes #1169 Closes #1170 Closes #1171 Closes #1172

…sistence

## Summary

Hardens the wallet connection layer with SSR-safe detection, an extension-free fallback, and reload-persistent sessions.

- **Freighter detection** (BCPathway#1169) — isFreighterInstalled() probes the exact global FreighterAdapter reads and distinguishes *present* from *present but broken*. whenFreighterReady() handles asynchronous extension injection with a bounded ~1s budget and an AbortSignal for cancellation. The uninstalled path renders an install link and a clean error instead of throwing.
- **Albedo fallback** (BCPathway#1170) — loadAlbedoScript() lazily injects the Albedo intent script, adopting an existing tag, sharing one in-flight promise, and polling for the global after load. Failures resolve to a structured reason rather than throwing, and a failed tag is recorded so it is not retried on every render. @albedo-link/intent is **not** added as an npm dependency.
- **WalletSelectModal** (BCPathway#1171) — lists detected wallets with inline-SVG icons, Installed / Not installed badges and 	arget="_blank" rel="noopener noreferrer" install links, highlights what is available, and starts the connection on selection. Self-contained: own portal, focus trap, Escape, backdrop dismissal, and closing is blocked while a connection is in flight.
- **Session persistence** (BCPathway#1172) — �c-forge:wallet-session and �c-forge:wallet-preference store only the wallet name (never a secret or a stale address), validated against the WalletName union on read and tolerant of malformed JSON. Storage access is wrapped for Safari private mode and degrades to no-ops.

## Notes for the reviewer

- useWallet() is **additive only** — status, connected, publicKey,
ame, error, connect and disconnect are unchanged. connectWallet, �vailableWallets and 
estoring are new.
- connect / disconnect now read state through a ref, which gives them **stable identities**. This is what stops the restore effect from re-firing on every state change.
- Session storage is read inside an effect, never during render, so server and client first paint are identical — no hydration mismatch.
- A restored session reuses the existing passphrase validation, so reconnecting on the wrong network lands in the existing wrong-network state rather than silently proceeding. A session whose wallet has since been uninstalled is cleared and stays disconnected.
- Two existing test files (context.test.tsx, ConnectWallet.test.tsx) needed a localStorage.clear() in �eforeEach. Without it, a session saved by one test is silently reconnected at the start of the next one and races the existing assertions — a direct consequence of persisting state by default.
- 
estoreSession defaults to 	rue to match the acceptance criteria; pass 
estoreSession={false} to opt out.

## Verification

Nothing here has been machine-verified — jest, eslint and tsc were not run, as requested.

⚠️ **One step needed before CI is green**:
pm run test:visual:update --workspace @bc-forge/react on the arm64 Playwright image. The visual suite screenshots every story against zero-tolerance baselines with updateSnapshots: 'none', and the new Wallet/ stories have no committed PNGs. Note that the modal portals to document.body, so the story-frame baseline captures the trigger button only.

Closes BCPathway#1169
Closes BCPathway#1170
Closes BCPathway#1171
Closes BCPathway#1172
@drips-wave

drips-wave Bot commented Oct 15, 2026

Copy link
Copy Markdown

@memplethee-lab Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

memplethee-lab and others added 8 commits October 15, 2026 19:51
…sion

useWallet returned �vailableWallets, connectWallet and 
estoring, but
WalletDetectionActions only declared detectWallets and 
estoring. The cast
to WalletConnectionState therefore failed with TS2352 and took down every test
suite that imported the context. �vailableWallets now lives on the public
interface it was always returned from.

parseSession accepted a bare stored name (reighter) but not that same name
serialised on its own ("freighter"), so a truncated or hand-written store was
silently discarded. Both forms now resolve, which is what the existing test for
a truncated value asserts.

Two test corrections: the wallet rows each render their own install badge, so
getByText on the shared label matched twice and had to become
getAllByText; and the focus-trap test moved focus outside �ct, which drove
a React state update through the dialog's onFocus.

Closes BCPathway#1169
Closes BCPathway#1170
Closes BCPathway#1171
Closes BCPathway#1172
react/visual/stories.spec.ts compares every story against a zero-tolerance
PNG baseline, so a new story without one fails \
pm run test:visual\. Those
PNGs are only reproducible on ubuntu-24.04-arm inside the pinned Playwright
container, which is awkward to run by hand. This workflow wraps that step and
commits the result. It is workflow_dispatch only, so the regular visual
workflow remains the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
\WalletSelectModal\ gained the \portalContainer\ prop that
\ConfirmationDialog\ already had, and the story now portals into a stage
inside the frame. The dialog was rendering outside \canvasElement\, so the
baseline captured only the trigger button, and \getByText('Installed')\
matched both wallet rows at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
\WalletFixture\ mounts \WalletProvider\ without \�dapterFactories\, so the
real SDK adapters probe \window.freighter\ and \window.albedo\. Neither
exists in the Storybook preview, so no wallet is installed and every row is
badged as missing — the assertion that two rows read 'Installed' could never
hold. It now asserts the missing badge and the install link, which is the
state a visitor without an extension actually sees.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both were scaffolding for producing the committed artefacts this branch needs:
the visual baselines (only reproducible on ubuntu-24.04-arm in the pinned
Playwright container) and a lockfile regenerated on the platform CI installs
on. Both artefacts are now committed, so the workflows have nothing left to do
and only add an approval gate on the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…visories

Four advisories were published after the last green run on main, so
`Dependency Audit` and `Smoke Test & Publish Indexer` fail on every branch,
including main itself.

- anyhow 1.0.102 -> 1.0.103 (RUSTSEC-2026-0190)
- handlebars 4.7.9 -> 4.7.10 (CVE-2026-106445, CVE-2026-106446)
- proxy-addr 2.0.7 -> 2.0.8 (CVE-2026-90711)
- uuid 8.3.2 -> 11.1.1 (GHSA-w5hq-g745-h8pq, reached through node-cron)

The npm entries are patched in place instead of re-resolved: a full
`npm install` rewrites the lockfile against manifests whose ranges float
(root `vite` is 8.3.0 while `react/demo` asks for ^6.0.0), producing a tree
`npm ci` rejects. Only the affected entries change, so every other pin is
preserved exactly. `node-cron` pins uuid at an exact 8.3.2, so its declared
range is widened to accept the bump.

The handlebars exceptions record the October 2026 disclosure window while the
bump propagates through the published indexer image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@memplethee-lab
memplethee-lab force-pushed the feature/wallet-adapters-and-session-persistence branch from fd24f14 to d7aa71b Compare October 16, 2026 01:02
memplethee-lab and others added 3 commits October 17, 2026 06:55
Merging main into this branch left a bad conflict resolution in
react/src/components/index.ts: the Dropdown and ConnectWallet export
blocks were pasted back in at the top of the file, on top of the copies
main already carries further down. Both esbuild and tsc reject the
barrel (TS2300 duplicate identifier, "Multiple exports with the same
name"), which is what failed React Package, both React peer-compat jobs
and the visual job - each of them died at the build step before a
single test or screenshot ran.

Restore the barrel to main's version and re-add only the two lines this
branch actually needs: WalletSelectModal and its prop type.

The same merge also reformatted seven files this PR never touches
(ConnectionStatusBar, CopyToClipboard, TransactionToast and their tests,
plus ProductComponents.test.tsx) - quote style and trailing commas only,
with zero semantic difference. Revert them so the diff is just the
wallet work.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant