Repository navigation
feat(identity): the first-run Welcome flow replaces the key-setup gate (FOUNDATION: App.tsx) - #1820
Conversation
f5752d2 to
d35338d
Compare
787e2b9 to
141b42b
Compare
d35338d to
4972a16
Compare
…e (FOUNDATION: App.tsx)
The composition and the product flow. On native desktop with no human key
in the credential store, the app now opens on `Welcome` instead of the
"Your Buzz identity" key import/create screen, and stays there until a key
exists and setup has nothing left to do.
FOUNDATION edit (`src/app/App.tsx`), the three approved lines: import
`Welcome` from `../features/identity/Welcome` instead of `IdentitySetup`,
and wrap `ConnectedApp` in `<Welcome identity={services.identity}
communities={services.communities} invites={services.invites}>`. No other
FOUNDATION file (`services.ts`, `main.tsx`, `communities/service.ts`,
`plugins/api.ts`) is touched. `IdentitySetup` stays for the credential-free
browser fixture and gets a one-line comment.
`Welcome.tsx` (new, 940 lines) owns the flow; its state (`finding`,
`inviteCode`, `entered`, `reads`, `known`, `linked`, `owned`, `backups` and
the `setup` store) is shared by every lane, which is why it lands whole:
- Landing: "Buzz" on Kenny's onboarding screen — Sign in with email,
Community URL (bare host, origin or invite link; Continue starts the
lookup) and Scan a QR code — Coming soon.
- A typed address is looked up before any sign-in (`FindCommunity`, two
unauthenticated GETs) and fails fast; a found community decides the
portal through `discoverAuthentication`: a tenant's relay signs in at that
tenant's portal, an unmatched one pauses on "Connect to your community"
(Sign in with email at the build portal, or I already have a key, which
imports without any account and lands on Join Community).
- After sign-in: Checking your account… (linked key + saved destinations in
parallel) → Checking for a backed-up key… → Set up your key (Create my
key: Creating → Linking → Backing up; I already have a key: Importing →
Linking → Back up this key?; Use a different account) or, for a linked
account, Welcome back (Restore from backup when listed, Enter your private
key accepting only the linked key, I lost this key → Start over with a new
key: Creating → Unlinking the old key → Linking → Backing up, Use a
different account). Both post-sign-in pages show the signed-in address.
- Couldn’t finish setup: Try again for retryable failures; once a key
exists a failed backup offers Continue without backup, a failed link or
unlink blocks with Try again only (nothing links the key later); before a
key exists, Back to the landing. The key choice and reveal pages never
show hex; nothing is signed to a relay before the user chooses to join.
- Restoring your communities…: the account's saved destinations are read
with the signed profile read and admitted ones saved; setup rests on Join
Community (typed URL) or Join a community when nothing was admitted,
enters the app on the first admitted one otherwise, and hands a typed
address or invite code that was not among them to the rail's
Add-a-community dialog through `invites.open`, the intent OS deep links
already use.
- Join Community joins through `admitCommunity`/`completeJoin` (journal
before any effect, read before reclaim, `accept-policy` → `claim` with an
invite code, the signed read); a denied read opens "This community needs
an invite" with the npub, Copy, an invite-code field, Back and Try again;
a user with no profile anywhere gets the Set up your profile step before
the membership is saved; a join after a Builderlab sign-in is also saved
to the account (`saveCommunity`, best effort).
- Invariants: every return to the landing goes through `backToLanding`
(clears discovery, the account reads and the first-run session; keeps the
typed address); `enterApp()` fires a pending handoff, clears what `Welcome`
kept and resets setup. Each page change moves focus to the new page's
heading, or back to the choice that opened it on Back.
Tests: `identity.test.tsx` (whole file, +4,837/−109 vs main: the landing,
discovery, Connect, Join/profile/invite, key import, Welcome back/restore,
Start over/owned, failure and seed journeys over a `nativeRelay` fixture
with a real keypair, `relay_sign`, per-origin `relay_http` and a real
`createCommunities`; the exact `invoke`/`relay_http` sequences are asserted
throughout). This PR's `Welcome.tsx` is the `19df106d5` version, which
turns these six journeys green — they are red against the earlier
`572d69398` Welcome: "a link that fails after the key is created blocks
setup with only Try again…", "a link that keeps failing after a key is
imported…", "a transient unlink failure after Start over…", "an unlink
refused after Start over offers only Try again…", "a transient link failure
after a successful unlink…", "a development seed's unlink status fails Start
over's unlink…". `src/app/first-run-join-handoff.test.tsx` (new): the real
`App` + `createServices()` under mocked Tauri commands, the gate lifting
with the Add-a-community dialog open on a typed address or invite link, the
dialog's claim with the code, and Cancel. `src/app/first-run-navigation.test.tsx`
(new): proves PR 01's deadline rule through the real first-run gate.
Docs: `docs/identity.md` first-run block (every page, what is signed and
what is not, where Back lands, the failed page), `docs/communities.md`
(`Welcome` as the second caller of the join owner and the invite intent),
`docs/deep-links.md`.
Stack: PR 07 of the onboarding stack, on 06; depends on 01–06. 08 is based
on it only to keep the stack linear.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
4972a16 to
5c9a654
Compare
141b42b to
e2f928f
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account.
Three changes requested inline: fence canceled sign-in continuations, preserve the legacy-key migration entry point, and remove deployment rollout details from public documentation. One optional retry-focus improvement is also inline.
Rocket, Mantis and Groot supplied independent review lanes. Reviewed head e2f928f8b8d89f9506ba1b1e703d22fd0eba9da2, base 5c9a65404d7d0dd929650f6f7098e7aa5936841d. Source-only: tests, app/native flows, rendered accessibility and CI were not executed or verified; the author’s deferred validation remains open.
| await login.signIn(); | ||
| if (login.snapshot().status !== "signed-in") return; | ||
| const current = account(login.credential(), origin, identity); | ||
| reads.current = current; |
There was a problem hiding this comment.
🤖 P1 — Fence the sign-in continuation to its own attempt. A canceled login can finish its pending HTTP exchange after a newer login succeeds. createOAuthSession discards the old result but resolves its promise, so this global status check accepts the newer session and constructs account(newCredential, oldOrigin). Once the newer setup is paused, setup.start also accepts the stale account checks; builderlabAccount sends that credential to the earlier configured portal. Native HTTP does not receive the abort signal (oauth/browser.ts:122–132). Invalidate a Welcome attempt generation on new sign-in, cancellation and unmount, then check it immediately after this await before reading credentials or replacing reads.current. Cover canceled A settling after B succeeds, with no account requests to A’s origin.
| <Welcome | ||
| identity={services.identity} | ||
| communities={services.communities} | ||
| invites={services.invites} | ||
| > |
There was a problem hiding this comment.
🤖 P2 — Preserve the explicit legacy-key migration entry point. Replacing the gate removes the only reachable UI calling identity.legacyAvailable() / identity.importLegacy(): those calls remain solely in IdentitySetup, while Welcome never offers them. An existing old-Buzz user now loses “Continue with your Buzz identity” and must manually export/paste the key. docs/identity.md still promises that offer, including after sign-out, and the retained migration test mounts the obsolete gate. Reuse the existing opt-in migration action in Welcome and test it through the live gate. If removal is an intended product change, explicitly resolve that contract and update the documentation/tests.
| [automatic backup](#automatic-backup-and-optional-recovery)); kgoose answers | ||
| `404` wherever backups are not configured, which today is everywhere but | ||
| staging's public Builderlab. Not sent, a `404` or any failure leaves the lookup |
There was a problem hiding this comment.
🤖 P2 — Keep deployment rollout details out of public documentation. This new paragraph identifies which deployed environment currently enables backups; later recovery/backup paragraphs likewise describe current tenant rollout coverage. Those are operational deployment details in a public repository. Replace them with the portable contract: backups/recovery require the corresponding build and service capabilities, and unavailable capabilities produce the documented fallback. Keep the environment-specific rollout evidence in internal tracking.
| </Button> | ||
| )} | ||
| {onRetry && ( | ||
| <Button variant="prominent" onClick={onRetry} disabled={!!busy}> |
There was a problem hiding this comment.
🤖 Optional P3 — Preserve focus across a repeated retry failure. disabled={!!busy} disables the focused Try again button. If the same denial returns, the page key is unchanged and the page-change focus effect does not run. Use the shared Button’s loading/focusable-disabled behavior for this pending action, while keeping it disabled during an invite-code submission, and cover focus after repeated denial. This is source-inferred; browser focus was not exercised.
Onboarding stack (merge in order, top to bottom):
Depends on: PRs 01–06 (
Welcome.tsximports 24 changed modules: 02'sFindCommunity/find-community/join/join-journal/destination, 03'sservice, 04'soauth/browser+builderlab-tenants, 05'sbuilderlab-account/first-run-seed/oauth/first-run, all of 06;first-run-navigation.test.tsxneeds 01).Diff: production +948/−3; tests +5468/−96; docs +473/−17
What this layer is
The composition and the product flow.
Welcome.tsx(940 lines, the19df106d5version): landing (Sign in with email, Community URL, QR placeholder), discovery viaFindCommunity, tenant/none decision, sign-in at the chosen portal, the setup lanes (Create my key / I already have a key / Welcome back / Restore / Start over), Join Community throughadmitCommunity/completeJoin, the profile step, the known-communities restore, the invite-intent handoff, the failure page (Try again / Continue without backup / a blocking link failure), thebackToLanding/enterAppinvariants, and the heading focus handoff. Plus the one FOUNDATION edit:App.tsximportsWelcomeinstead ofIdentitySetupand passesidentity,communities,invites(+7/−3).IdentitySetup.tsxgets a one-line comment. Docs describe the whole flow.Review focus
src/app/App.tsx— the import swap and the<Welcome identity communities invites>wrapper are the only three approved lines; after the rebase ontomaintheLegacyCommunityImportthatmainplaced insideIdentitySetupstays inside<Welcome>, so it still renders only once setup is complete (resolution approved by the author); no other FOUNDATION file (services.ts,main.tsx,communities/service.ts,plugins/api.ts) is in the stack.readyand setup isidle;enterApp()is the only way setup becomes idle with a key, andbackToLandingis the only path back to the landing (clears discovery, reads and the first-run session; keeps the typed address).identity.test.tsxjourneys are grouped by lane in file order (landing/discovery, Connect, Join/profile/invite, key import, Welcome back/restore, Start over/owned, failures & seed) and assert exactinvoke/relay_httpsequences.first-run-join-handoff.test.tsxruns the realApp+createServices();first-run-navigation.test.tsxproves PR 01's rule through the real gate.Checks run on this branch's tree
--no-verify; the hosted checks on this PR are the verification. The plan's checks for this layer were:vitest run src/features/identity src/features/communities src/app,tsc --noEmit,biome check,pnpm design:check,test:browser --project=chromium --project=webkit --no-deps tests/browser/window-chrome.spec.mjs.Native evidence
None on this tree. The native first-run recordings and the hands-on checks on the branch were made on the full tree, which only the DO-NOT-MERGE PR 09 reproduces (it adds the isolated debug Keychain slot). Treat every layer below 09 as unit/integration-tested only.
Why this branch is green where
572d69398was red. This PR'sWelcome.tsxis the19df106d5version (a failed link blocks first run with Try again only, instead of offering Continue without linking). Sixidentity.test.tsxjourneys depend on it and fail against the earlier Welcome: "a link that fails after the key is created blocks setup with only Try again…", "a link that keeps failing after a key is imported…", "a transient unlink failure after Start over…", "an unlink refused after Start over offers only Try again…", "a transient link failure after a successful unlink…", "a development seed's unlink status fails Start over's unlink…".mainwith fix: retry transient finite reads before recovery actions #1791's quiet read retries: theidentity.test.tsxrelay fixture's unreachable saved community now answers a web page instead of a 503 (whichinspectProfilewould retry for seconds, with the retries leaking into later tests), so the restore test keeps asserting exactly one discovery read without waiting out a retry schedule.🤖 Generated with Claude Code