Skip to content

feat(identity): first-run setup store, step table and pages (composition follows in the next PR) - #1819

Open
matt2e wants to merge 1 commit into
onboarding/05-first-run-account-clientfrom
onboarding/06-first-run-pages-and-setup-store
Open

matt2e wants to merge 1 commit into
onboarding/05-first-run-account-clientfrom
onboarding/06-first-run-pages-and-setup-store

Conversation

@matt2e

@matt2e matt2e commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Onboarding stack (merge in order, top to bottom):

  1. fix(navigation): run the visit deadline only while a presenter is attached #1814 navigation: visit deadline only while a presenter is attached (merged)
  2. refactor(communities): extract the join owner and add the pre-join relay lookup #1815 communities: join owner and pre-join relay lookup (merged)
  3. feat(identity): observable nsec backup status with pass tickets, explicit lock release, and refused-recovery copy #1816 identity: native nsec backup status (merged)
  4. feat(identity): first-run Builderlab account client — sign-in without a plugin session, kgoose reads, link/unlink, backup, saved communities — and its dev seed #1818 identity: first-run Builderlab account client and dev seed
  5. feat(identity): first-run setup store, step table and pages (composition follows in the next PR) #1819 identity: first-run setup store, step table and pages ← this PR
  6. feat(identity): the first-run Welcome flow replaces the key-setup gate (FOUNDATION: App.tsx) #1820 identity: Welcome flow replaces the key-setup gate
  7. feat(settings): read-only Key backup row in Settings → Identity #1821 settings: read-only Key backup row
  8. [DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822 DO NOT MERGE: isolated debug Keychain slot (full stack for native testing)

Depends on: the merged #1815 (destination, find-community types, api, ProfileFields autoFocus) and #1816 (service, recoveryOff, BackupError); this layer adds RestoreIdentity's optional expectedPubkey/onImport props with their only caller, KeyRestore, and uses #1818's builderlab-account types. #1820 composes these; nothing imports them until then.

Diff: production +1590/−3; tests +636/−0

What this layer is

The stateless building blocks of first run, reviewable as components against DESIGN.md and Kenny's onboarding layouts. setup.ts: createSetup (start/retry/reset; running/paused{at}/failed{retryable, skipsLink}/done/idle), the step factories (checkAccount, checkBackup, createKey, importKey, linkKey, unlinkKey, restoreCommunities) and finish(); restore-communities.ts (restoreKnown, 3-wide, 45 s cutoff); key-import.ts (classifyKeyImport, readKeyFile); and the pages CommunityUrlField, InviteCodeField, JoinCommunity, KeyChoice, KeyImport, KeyLost, KeyRestore, BackupOffer, ConnectChoice, WelcomeBack, PublicKeyPanel, SetupProgress, Wireframe, ProfileStep. Dead code for exactly one PR by design (the repo has no unused-export check), so that #1820 is the composition and the product flow alone.

Review focus

  • setup.ts: retry() re-runs only the failed step; a step's resting answer overrides the caller's then; failed.skipsLink is derived from the steps left. checkAccount/checkBackup never fail setup on the list/lookup reads.
  • restore-communities.ts: concurrency 3, nothing started after 45 s, outcomes in list order, never throws; joined only for admitted destinations.
  • Pages against DESIGN.md: shared Button/Field/Input/Checkbox/Avatar/LoadingRegion/OnboardingScreen, role colours and type roles only, Kenny's CSS module classes reused unchanged; tabIndex={-1} headings and data-return-focus triggers serve the focus handoff Welcome performs. Rendered checks were done on the branch in scratch fixtures (recorded in its history); mount a page with stub props to repeat them.
  • KeyImport: a mismatched key (expectedPubkey) is refused in the page and never reaches identity_import; the key lives only in component state. CommunityUrlField: Continue stays enabled while empty/invalid (Enter is handled by the input, not a native form submit).

Checks run on this branch's tree

Native evidence

None on this tree. Native first-run runs use the full tree in the DO-NOT-MERGE #1822 (it adds the isolated debug Keychain slot), and none has been made since the rebase. Treat every PR before #1822 as unit/integration-tested only.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review October 28, 2026 13:40
@matt2e
matt2e requested a review from a team as a code owner October 28, 2026 13:40
@matt2e
matt2e force-pushed the onboarding/06-first-run-pages-and-setup-store branch from f5752d2 to d35338d Compare October 28, 2026 14:18
@matt2e
matt2e force-pushed the onboarding/05-first-run-account-client branch 2 times, most recently from b202f48 to 6e41931 Compare October 28, 2026 14:21
@matt2e
matt2e force-pushed the onboarding/06-first-run-pages-and-setup-store branch from d35338d to 4972a16 Compare October 28, 2026 14:21

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account: two changes needed, detailed inline—preserve the portable-avatar guard during community restoration, and distinguish partial backup recovery from a missing linked-key backup. One optional focus improvement is also inline.

Rocket, Mantis and Groot completed independent lanes; I verified and integrated their findings. Head 5c9a65404d7d0dd929650f6f7098e7aa5936841d, base 1d5b1b1e8f8278405bda3f883ea0483a7fb94526. Source only: no tests, builds, app execution or CI assessment; native recovery, rendered focus and #1820’s composition remain unverified.

try {
communities.joined(
{ id: destination.id, name: destination.host },
read.exists ? read.profile : client.profile,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 P2 — Keep protected community avatars out of the device default. On a fresh device with an empty local profile name, communities.joined seeds the default from this argument (service.ts:524). Passing read.profile directly also copies an avatar under this community’s /media/ path. CommunityDialog then offers that default when joining another community, where saving can publish a protected, unusable avatar URL. This bypasses the explicit portability rule in docs/communities.md and the existing completeJoin path. Apply profileDefault(next, client.profile, destination.id) before joined, keeping the original read in the outcome. Add a restore case with a community-hosted avatar and verify the previous local picture is retained.

<p role="alert">{recovery.error}</p>
) : mismatched ? (
<p role="alert">
None of your backups holds the key linked to this account, so nothing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 P3 — Preserve retry when recovery is partial. With a non-matching recovered candidate and failedCount > 0, the linked key may belong to a backup that failed to recover. This branch nevertheless says no backup holds it, and the action branch offers import instead of retry. Distinguish incomplete recovery from confirmed absence and offer retry alongside import. Wire retry through a successful cancel followed by beginRecovery in KeyRestore: merely exposing the existing callback is insufficient because service.ts refuses a new attempt while raw candidates remain. Cover this mixed success/failure case, including that cancellation failure does not start another attempt. This also follows DESIGN.md’s rule that failed reads do not prove absence.

type="submit"
variant="prominent"
loading={busy}
disabled={!canContinue}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Optional P3 — Preserve focus during a failed profile submission. canContinue includes !busy, so pending Continue receives both loading and disabled. That bypasses shared Button’s loading && !disabled focus-preservation behavior; Skip likewise becomes non-focusable. Consider keeping the active action focusable while guarding repeated submission, or restoring a deliberate focus target on failure. Keep the busy guard in form submission. This is source inference; verify the failed Continue/Skip keyboard paths in the rendered host before choosing the repair (DESIGN.md, “Loading without flicker”).

@matt2e
matt2e force-pushed the onboarding/05-first-run-account-client branch from 1d5b1b1 to 8eefb4a Compare October 29, 2026 23:48
@matt2e
matt2e force-pushed the onboarding/06-first-run-pages-and-setup-store branch from 5c9a654 to 32731cd Compare October 29, 2026 23:48

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated follow-up source review via Wes’s account: no new findings in this rebase, but the two previously reported changes are still needed: P2 avatar portability and P3 partial-recovery retry. The existing repair advice remains applicable; the optional profile-submit focus suggestion remains optional.

The feature patch differs from the previously reviewed revision only in comments; we checked its affected integration with the changed base. Rocket, Mantis and Groot completed independent lanes, and I verified their findings against the pinned source. Head 32731cd75ccc4cacfea3249f9016fc894435747f, base 8eefb4a0d909841610f87eed10bf65f615e2908c. This is a follow-up of the prior findings and affected integration, not a review of every intervening base change.

Source only: no tests, builds, app execution or CI assessment. Native recovery, rendered focus/accessibility, performance and #1820’s composition remain unverified. The public description and commit metadata were inspected; no attached images were present. Reported author test results were not independently verified.

…ion follows in the next PR)

The stateless building blocks of the first-run flow, reviewable as
components against DESIGN.md and Kenny's onboarding layouts. Nothing
imports them until PR 07 (`Welcome.tsx`); the repo has no unused-export
check, so lint and typecheck pass with them standing alone.

Setup store and steps (`setup.ts`): `createSetup()` with `start(steps,
then)`, `retry()` (re-runs only the failed step, then the rest) and
`reset()`, over snapshots `idle` / `running{label}` / `paused{at}` /
`failed{error, retryable, skipsLink}` / `done`; a step's `run` may resolve
to a resting state that overrides the caller's `then`. Step factories:
`checkAccount(account, onLinked, onKnown)` (the linked-key and
known-communities reads in parallel under one label; the list read never
fails setup), `checkBackup(account, onListed)` (never fails setup),
`createKey`, `importKey`, `linkKey`, `unlinkKey(account, old, new)`,
`restoreCommunities(known, communities, decide)` and `finish()`.
`restore-communities.ts`: `restoreKnown(known, communities, { concurrency:
3, startBy: 45_000 })` — the same signed profile read the Join Community
page uses, at most three at once, nothing started after 45 s, each admitted
destination saved through `communities.joined`; outcomes `admitted` /
`denied` / `unavailable` in list order; never throws. `key-import.ts`:
`classifyKeyImport` (empty / `ncryptsec` refused with a clear message /
`nsec` with its npub preview / invalid) and `readKeyFile` (1 KiB cap, first
non-blank line).

Pages (all `src/features/identity/`, stateless, props-driven):
`CommunityUrlField` (bare host, `wss://`/`https://`, or an invite link —
`https://<host>/invite/<code>`, `buzz://join?relay=…&code=…` — via
`enteredCommunity`; Continue stays enabled while empty/invalid and shows one
inline error), `InviteCodeField`, `JoinCommunity` (owned rows named "Join
{host}" for assistive technology, the URL field, the disabled "Scan a QR
code" / "Create a community — Coming soon" buttons, on the onboarding
layout), `KeyChoice` (Create my key / I already have a key / Use a
different account, the signed-in address, the backed-up-key sentence only
when the lookup listed none), `KeyImport` (masked field, Reveal/Hide, file
drop/picker, npub preview, `expectedPubkey` mismatch refused in the page,
"Importing your key…" on the same screen), `KeyLost`, `KeyRestore` (the K3
column; "Your key is stored securely on this device." only while the
restore may still install the key; the unavailable/refused page),
`BackupOffer`, `ConnectChoice` ("Connect to your community" with the host
on its own mono line), `WelcomeBack` (the linked npub with Copy, Restore
from backup when listed, Enter your private key, I lost this key, Use a
different account), `PublicKeyPanel`, `SetupProgress`, `Wireframe`
(`WireframeScreen` on Kenny's frame, `WireframePanel` as the one stacking
group), `ProfileStep` (a pure form over `ProfileFields`: Continue hands the
trimmed draft out, Skip for now leaves). Every `h1` carries `tabIndex={-1}`
and the Back triggers `data-return-focus`, for the heading-focus handoff
`Welcome` performs. Shared owners only: `Button`, `Field`, `Input`,
`Checkbox`, `Avatar`, `LoadingRegion`, `OnboardingScreen` and Kenny's
`IdentitySetup.module.css` classes (unchanged); role colours and type roles
only, no new tokens or variants.

Tests: `setup.test.ts` (retry/rest/idle semantics, `checkAccount` and
`checkBackup` never failing setup, `unlinkKey` sending nothing while the
old key is on the device), `restore-communities.test.ts` (three reads at
once, outcomes in list order, dedupe and insecure skip, throwing `joined`,
the 45 s cutoff under fake timers), `key-import.test.ts`,
`CommunityUrlField.test.ts` (8 accepted and 10 refused forms),
`ProfileStep.test.tsx`. The pages' rendered checks (Chromium/WebKit, light/
dark, 480/1100 px, 150 % text) were done on the branch in scratch fixtures
and are recorded in the branch's commit history; mount a page with stub
props to repeat them.

Stack: on the first-run account client; uses the merged `destination`,
`find-community` types, `api`, `ProfileFields autoFocus`, `service`,
`RestoreIdentity` `expectedPubkey`/`onImport` and `BackupError`, plus the
account client's `builderlab-account` types. The Welcome flow composes these.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e force-pushed the onboarding/06-first-run-pages-and-setup-store branch from 32731cd to 235e33e Compare October 30, 2026 00:30
@matt2e
matt2e force-pushed the onboarding/05-first-run-account-client branch from 8eefb4a to 40812cc Compare October 30, 2026 00:30

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated follow-up source review via Wes’s account: no new findings in this limited rebase follow-up. The previously reported P2 protected-avatar restoration issue and P3 partial-backup recovery issue remain unchanged and still need the repairs described there. The earlier focus suggestion remains optional; existing inline feedback is not duplicated.

Reviewed head 235e33ead40427dcf9d905783e33105c9894a2b5 against base 40812ccf4c000363b47d30fce9e4a028b5cfcf50. The feature patch is byte-identical to the patch reviewed at 32731cd75ccc4cacfea3249f9016fc894435747f; the endpoint delta consists only of the inherited Claude spare-process guard and its regression test (20 additions, 1 deletion). This qualifies for the limited-follow-up solo exception; Star Lord performed this pass without new teammate lanes.

Source-only: inspected that delta, its process lifecycle/callers, prior findings, and public PR description/commit material (no attached images). No new disclosure issue found. Tests, app/native flows, rendered accessibility, focus behavior, performance, and CI were not exercised or verified. This is a COMMENT review, not approval or merge authorization.

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.

3 participants