镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(identity): Set up your profile after the first join, and look up a community URL from the landing page before sign-in - #1803

Closed
matt2e wants to merge 3 commits into
onboarding/01-welcome-find-joinfrom
onboarding/02-profile-step-landing-url
Closed

matt2e wants to merge 3 commits into
onboarding/01-welcome-find-joinfrom
onboarding/02-profile-step-landing-url

Conversation

@matt2e

@matt2e matt2e commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Part 2 of 12 of the onboarding-flow stack (the first-run onboarding work reviewed on #1061, split into stacked PRs per the plan). Base: onboarding/01-welcome-find-join. Each PR is a contiguous slice of the original branch's history, cherry-picked onto main; the combined tree of the whole stack equals onboarding-flow at 572d69398 rebased onto main (verified by diff, see the stack notes in the last PR). Built on main at df5180b4a (2026-10-28, the origin/main of the build); main has moved on since and the stack is not rebased onto the newer tip.

Stack

  1. feat(identity): first-run welcome screen with community lookup, Join Community and the invite page #1802 onboarding/01-welcome-find-join
    2. feat(identity): Set up your profile after the first join, and look up a community URL from the landing page before sign-in #1803 onboarding/02-profile-step-landing-url ← this PR
  2. feat(identity): decide the Builderlab portal from the community URL with a build-time tenant table #1804 onboarding/03-builderlab-tenants
  3. feat(identity): Set up your key — managed, own-key reveal, import — and Welcome back for a linked account #1805 onboarding/04-key-choice-import-welcome-back
  4. feat(identity): link the first-run key to the account, and back it up through the observable native uploader #1806 onboarding/05-link-and-native-backup
  5. feat(identity): real unlink on Start over, Restore from backup, and the Create my key / I already have a key setup with Try again / Continue without backup #1807 onboarding/06-unlink-restore-create-my-key
  6. feat(identity): ask "Connect to your community" — sign in or bring a key — for a community no tenant hosts #1808 onboarding/07-connect-choice
  7. feat(identity): invite links on first run, restore the account's saved communities, and hand a typed address to the join dialog #1809 onboarding/08-invite-links-known-communities
  8. feat(identity): render first run on Kenny's onboarding layouts, and say "Sign in with email" #1810 onboarding/09-onboarding-layouts-and-copy
  9. fix(navigation): run the visit deadline only while a presenter is attached #1811 onboarding/10-navigation-deadline
  10. fix(identity): address Carl's review of #1061 — signed-in account exit, focus handoff, consent copy, shared join owner, a11y names, design pass #1812 onboarding/11-review-fixes
  11. [DO NOT MERGE] onboarding: full stack + isolated debug Keychain slot for native testing #1813 onboarding/12-do-not-merge-keychain-slot

Scope

ProfileStep.tsx (shared ProfileFields, publish kind 0, Skip for now), the live landing-page Community URL field extracted into CommunityUrlField.tsx, FindCommunity.onFound, discovery before sign-in, skipping "Finding communities" when a URL was supplied, and removal of the hard-coded pending-invite rows.

Squashes vs the original branch: the test fix-up S2 (second sync outbox expectation) folded into the profile-step commit; f916b517c folded into the landing-URL commit it completes. ad3a5653d kept on its own (a product removal with its own rationale).

Checks at this level (worktree /tmp/onb-stack, head of onboarding/02-profile-step-landing-url)

  • bin/pnpm exec tsc --noEmit — 0 errors
  • bin/pnpm lint — 0 errors (21 pre-existing infos)
  • bin/pnpm design:check — all checks pass
  • git diff --check origin/main...HEAD — clean
  • bin/pnpm vitest run src/features/identity src/features/communities — 31 files, 445 tests passed, 0 failed

Native evidence — not claimed here

No per-level native run was performed for this PR. Native first-run testing happens on the tree of the final DO-NOT-MERGE PR of the stack (#1813), which carries the isolated debug Keychain slot; the human in-app check for this PR's behaviour is owed there, so this PR stays a draft without buzz-review-completed until Matt confirms it.

🤖 Generated with Claude Code

matt2e and others added 3 commits October 28, 2026 22:24
…rst community joins

After Join on the first-run **Join Community** page saves the key's first
membership, the gate no longer lifts straight into the app. It stays up for a
**Set up your profile** page — "Add a name and avatar. They’ll show up on your
messages, reactions, and agent handoffs in {host}. You can change them any time
in Settings → Profile." — so a brand-new key does not enter its first community
as an unnamed member. A key whose device record already held a community (a
record restored with the key, or the `VITE_BUZZ_OPEN_RELAY` development seed)
keeps today's behaviour and enters the app directly; the condition is read from
the snapshot captured at the top of `join()`, before `communities.joined` adds
the new membership.

New `src/features/identity/ProfileStep.tsx` owns the page so `Welcome.tsx`
stays readable; `Welcome` renders it at the former seam comment and passes the
`found` community, the admission `read`, the Builderlab email's local part as
the suggested name, and the service. The fields are the shared `ProfileFields`
(avatar editor with `community={found.url}`, since the membership is saved and
the relay session acquired by `joined()`; required display name; optional
description) inside the branch's plain `border-standard` wireframe box. The
draft starts from the relay's existing profile when the read found one, else
`{ name: <email local part>, picture: "", about: "" }`; the display name is
focused on mount. Actions follow the branch footer order: **Skip for now**
(secondary) then **Continue** (primary, `loading` while busy), gated on
`canSaveProfile`, not busy and no avatar upload in flight; Enter in the name
field submits through the surrounding form.

Continue trims the name and description, publishes the kind 0 through the
existing `publishProfile` route (`relay_sign`, then `relay_http` POST `/events`
to that one relay) unless an existing relay profile is unchanged
(`profilesEqual`), then `communities.saveProfile(profileDefault(next, …))` so
the device's local default picks up the name, then `setup.reset()`. Deviation
from `ProfileSettings` and `CommunityDialog`: no post-publish readback — the
native route already requires `{ accepted: true, event_id: event.id }`, and a
key created seconds ago has no concurrent editor. A throw renders inline as
`<p role="alert" className="text-body text-danger">` under the fields with the
draft and the step kept and both buttons re-enabled: a denied read kind gets a
"no longer admits this key" line, an `Error` shows its own message (the native
route's "Community request failed (500)" / unconfirmed receipt), anything else
the join's unreachable copy. **Skip for now** is `setup.reset()` only — no
publish, no `saveProfile`.

`Welcome`'s prop widens to `Pick<Communities, "snapshot" | "joined" |
"saveProfile">`; `App.tsx` already passes `services.communities` whole.
`ProfileFields` gains one optional `autoFocus` prop (display name only). **No
FOUNDATION file was touched** (App.tsx, services.ts, main.tsx,
communities/service.ts), no Rust, no new dependencies.

Tests: `identity.test.tsx` extends the `nativeRelay` fixture with an
`/events` branch (stores the event, answers `{ accepted: true, event_id }`, or
500 when `relay.publish` is false) and `communitiesFor` with an `openRelay`
seed; `noJoin` gains a throwing `saveProfile`. Journeys: Join → step shown
with the membership selected and children hidden, "a" prefilled and focused
from `a@example.com`, Continue disabled on an empty name, "Ada{Enter}" → the
exact POST `/events` (kind 0, pubkey, `tags: []`, content
`{"name":"Ada","display_name":"Ada","picture":"","about":""}`) → children with
the profile in `buzz-client.v1:<viewer>`; Skip with an existing relay profile
prefilled → children, no `/events`, relay profile as the local default (moved
from the old join journey); a 500 on `/events` → inline alert, step kept, both
buttons enabled, local default unchanged, then Skip enters; a seeded existing
membership → straight to children, two memberships, no `/events`. The Try again
journey now skips the step after admission. New `ProfileStep.test.tsx` mocks
`publishProfile` for the trimmed publish + save + done sequence, the
unchanged-relay-profile path (save without publish), and publish failure then
Skip. Session acquisition issues its own `GET /` after the join, so publish
assertions filter calls by path. docs/identity.md describes the step, what is
published and Skip; docs/communities.md no longer says `Welcome` publishes no
profile.

Validation: `vitest run src/features/identity src/features/communities src/app`
(58 files, 689 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on both
directories and docs, `git diff --check`. Not exercised in the native app: a
real `/events` acceptance on a deployed relay, the avatar upload/paste paths
from this page against a live relay session, and how the published profile
appears to other members remain to be tried by hand.

Stacked-PR note: as in the Join journey, the Skip for now journey's whole-record
`toEqual` expects the `sync: { known: [], outbox: [{ url:
"wss://relay.example.com:8443", removed: false }] }` that `main`'s `joined()`
(#644) queues; first run sends nothing from that outbox. This is the second
half of the later rebase chore 5bf651b's test hunk, folded in where the
assertion is introduced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…up before sign-in

The first-launch welcome screen's **Community URL** field was a disabled
"Coming soon" placeholder; the only live entry was **Connect with Builderlab**,
and a user who already knew their community's address typed it only after
sign-in and the "Setting up Buzz" steps, on **Join a community**. The field is
now live on the landing page. Submitting an address looks the community up at
once — the same two unauthenticated `relay_http` GETs (`GET /`, `GET
/api/join-policy`) the find page sends, before any sign-in and before a key
exists — so a typo, an unreachable relay or a non-Buzz address fails fast with
the existing classified copy instead of after OAuth has run and a Keychain key
has been created. A found community starts Builderlab sign-in immediately, and
because `Welcome`'s done branch already renders `FindCommunity` for `finding`,
setup then lands on **Join Community** for that address rather than on the
choices page. Cancel there reaches **Join a community** with the address
prefilled, and every way back to the landing keeps it in the field.

- `src/features/identity/CommunityUrlField.tsx` (new): the field, its Join
  button, `invalid` state, `useId`/`aria-describedby` wiring and "Please enter
  a valid community URL" alert, extracted verbatim from `JoinCommunity`, with
  an optional `disabled`. `JoinCommunity` renders it with `onSubmit={onFind}`;
  its props are unchanged. The landing renders it with `onSubmit={setFinding}`
  and `disabled={auth.status === "pending"}`, so the address is held, not
  editable, while sign-in is pending. Both pages keep the button labelled
  **Join**, `variant="primary"`; whether the landing should show two primaries
  once a URL is valid is left for a design look. **Scan a QR code** stays
  disabled "Coming soon", with a comment marking the entry point: a scanned
  code's text takes exactly the field's path (`enteredRelayOrigin` →
  `setFinding`).
- `FindCommunity` gains an optional `onFound(found)`: when set, a found
  community is handed to the caller instead of rendering Join Community, and
  the lookup page stays until the caller moves on. It is read through
  `useEffectEvent` (as Toast and use-file-drop do) so an inline callback does
  not restart the lookup on every parent render. Found/failed rendering and
  the abort-on-Cancel controller are untouched; the post-setup use passes no
  `onFound`.
- `discoverAuthentication(found): Authentication` in `setup.ts`, next to the
  other `[TODO]` seams: returns `"builderlab"` for every community, with a TODO
  noting that nothing a relay publishes identifies its host today (NIP-11
  `self` is the likeliest future signal).
- `Welcome`: a discovery branch between the OAuth-error page and the landing,
  `finding && auth.status === "signed-out"`, rendering `FindCommunity` with
  `onCancel={backToLanding}` and `onFound` → `discoverAuthentication` →
  `signIn()`. `signIn` publishes `pending` synchronously, so the predicate
  flips to the landing's **Cancel sign-in** view in the same render pass with
  no flash back to discovery. New `backToLanding()` clears `finding`, resets
  setup (a no-op while running) and cancels login, and is the handler at all
  four return-to-landing sites: setup-failed Back, OAuth-error Back, **Cancel
  sign-in** and discovery Cancel. That is the one invariant to protect — a
  kept `finding` while signed-out and idle means "discovery in progress", so a
  cancel path that forgot to clear it would re-enter discovery — and routing
  every site through one helper keeps it honest. No new state: `finding` is
  the intent, `entered` already prefills both fields.

Not in this commit: skipping the second lookup after setup —
`FindCommunity` always starts in `finding`, so the same two GETs run again
before Join Community; accepted, since the policy may have changed during OAuth
and the join re-reads NIP-11 anyway.

No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts):
`App.tsx` already passes `identity` and `communities`. No Rust: GETs are
already unauthenticated (`relay_http` authenticates POST only). No new
dependencies.

Tests: identity.test.tsx updates the first-launch case (field enabled and
empty, its Join disabled, QR still disabled, `invoke` still only
`identity_restore`) and adds three journeys on a `landingRelay` fixture that
holds the NIP-11 answer: landing URL → **Finding community** with exactly the
two GETs and `connect` not yet called → answer → `connect` once, **Cancel
sign-in**, field disabled with the address held → credential → **Join
Community** with the key created and **Join a community** absent, four GETs →
Cancel → choices page prefilled → Join → **Join Community**, six GETs; an
unreachable relay → **Couldn’t find community**, Retry offered, `connect`
never called → Cancel → landing with the address kept and `invoke` =
`identity_restore` + two `relay_http`; Cancel sign-in after a URL → landing,
address kept, still two GETs and one `connect`, then Join → fresh lookup →
failing sign-in → Back → landing with the address kept. FindCommunity.test.tsx
adds the `onFound` case: invoked once with `{ url, host, icon, policy }`,
heading stays **Finding community**, no Join Community or Join button, Cancel
still reaches `onCancel`. docs/identity.md describes the live landing field,
the pre-sign-in lookup and its Cancel behaviour, landing on Join Community
after setup when a URL was given, and names `discoverAuthentication` as the
seam.

Validation: `bin/pnpm vitest run src/features/identity src/features/communities`
(25 files, 361 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on both
feature directories and docs, `git diff --check`, and
tests/browser/window-chrome.spec.mjs in Chromium (passes; the "Buzz" heading
after the launch handoff is unchanged). Not exercised in the native app: a
truly key-less `relay_http` GET has never run there (today's find page runs
after "Creating keys…"), so the first real landing-URL run should confirm it
against a deployed relay, along with the pending-sign-in view with the field
disabled.

Squashed for the stacked PR — f916b51, which completes the same feature:

feat(identity): skip Finding communities when setup started from a community URL

When the first-run landing page took a community URL (ab29d267), the user
has already named the community to join, so the "Setting up Buzz" step
"Finding communities… [TODO]" has nothing to find. Setup started from a
found landing URL now runs only Downloading keys, Creating keys and
Uploading keys before landing on Join Community; Connect with Builderlab
without a URL keeps all four steps.

- `setup.ts`: `setupSteps(identity, delay, { findCommunities = true })`
  omits the "Finding communities… [TODO]" step when `findCommunities` is
  false. `createSetup()` no longer takes the steps; `start(steps)` receives
  them, so the store is created once per `Welcome` mount and the step list
  is chosen at the moment sign-in succeeds.
- `Welcome.tsx`: `signIn(findCommunities: boolean)` calls
  `setup.start(setupSteps(identity, delay, { findCommunities }))` — `true`
  from the Connect with Builderlab button, `false` from the discovery
  `onFound` path. The flag is an explicit argument, not read from `finding`
  inside the async callback, so no stale-closure reasoning is needed.

No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts),
no Rust, no new dependencies.

Tests: identity.test.tsx's landing-URL journey now uses the gated delay and
asserts the status reads "Downloading keys… [TODO]" then "Uploading keys…
[TODO]" with no "Finding communities…" step and `delay` called exactly
`[[2000], [2000]]` before Join Community appears; against the previous
source it fails with three delay calls. The Connect-without-URL journey is
unchanged and still sees four steps and three delays. docs/identity.md says
"Finding communities" runs only when no community URL was supplied on the
welcome screen.

Validation: `bin/pnpm vitest run src/features/identity
src/features/communities` (25 files, 361 tests), `tsc --noEmit`,
`pnpm lint`, `biome check` on both feature directories and docs/identity.md,
`git diff --check`. Not exercised in the native app.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…mmunity

The first-run **Join a community** page opened with three wireframe
"Pending invites" rows (`buzz.block.builderlab.xyz`, `design.builderlab.xyz`,
`buzz.example.org`), each an avatar placeholder, the hostname and a disabled
**Join**. They were hard-coded examples: no backend serves a user's pending
invites — kgoose lists only communities the user owns, and the relay has no
per-user invite concept — so the list is removed rather than left looking like
a feature. The page now shows the heading, the live **Community URL** field
with **Join** beside it, and the disabled **Scan a QR code** / **Create a
community** "Coming soon" buttons, unchanged.

`JoinCommunity.tsx` loses `EXAMPLE_INVITES`, the `<ul aria-label="Pending
invites">` and the now-unused `Avatar` import; its props are unchanged, so
`Welcome` is untouched. No FOUNDATION file (App.tsx, services.ts, main.tsx,
communities/service.ts), no Rust, no `.env.local`.

Tests (identity.test.tsx): the Connect-without-URL journey asserts no list is
rendered and a single disabled **Join** instead of three rows plus one; the
`urlJoin()` / `join()` helpers that filtered the URL field's Join out of the
row Joins collapse to `getByRole("button", { name: "Join" })`, and the unused
`within` import goes. docs/identity.md describes the page without the rows.

Validation: `bin/pnpm vitest run src/features/identity` (6 files, 98 tests),
`tsc --noEmit`, `biome check` on the two changed source files, `git diff
--check`. Not exercised in the native app; the change is a pure removal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e marked this pull request as ready for review October 28, 2026 12:26
@matt2e
matt2e requested a review from a team as a code owner October 28, 2026 12:26
@matt2e

matt2e commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Closing: this stack was cut as chronological slices of the branch history rather than logical, independently reviewable layers. It will be replaced by a stack built from the squashed diff. Nothing from this PR was merged.

@matt2e matt2e closed this Oct 28, 2026
@matt2e
matt2e deleted the onboarding/02-profile-step-landing-url branch October 28, 2026 12:31
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.

1 participant