Repository navigation
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
Conversation
…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>
This was referenced Oct 28, 2026
Closed
matt2e
marked this pull request as ready for review
October 28, 2026 12:26
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 2 of 12 of the
onboarding-flowstack (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 ontomain; the combined tree of the whole stack equalsonboarding-flowat572d69398rebased ontomain(verified by diff, see the stack notes in the last PR). Built onmainatdf5180b4a(2026-10-28, theorigin/mainof the build);mainhas moved on since and the stack is not rebased onto the newer tip.Stack
onboarding/01-welcome-find-join2. 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 PRonboarding/03-builderlab-tenantsonboarding/04-key-choice-import-welcome-backonboarding/05-link-and-native-backuponboarding/06-unlink-restore-create-my-keyonboarding/07-connect-choiceonboarding/08-invite-links-known-communitiesonboarding/09-onboarding-layouts-and-copyonboarding/10-navigation-deadlineonboarding/11-review-fixesonboarding/12-do-not-merge-keychain-slotScope
ProfileStep.tsx(sharedProfileFields, publish kind 0, Skip for now), the live landing-page Community URL field extracted intoCommunityUrlField.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
syncoutbox expectation) folded into the profile-step commit;f916b517cfolded into the landing-URL commit it completes.ad3a5653dkept on its own (a product removal with its own rationale).Checks at this level (worktree
/tmp/onb-stack, head ofonboarding/02-profile-step-landing-url)bin/pnpm exec tsc --noEmit— 0 errorsbin/pnpm lint— 0 errors (21 pre-existing infos)bin/pnpm design:check— all checks passgit diff --check origin/main...HEAD— cleanbin/pnpm vitest run src/features/identity src/features/communities— 31 files, 445 tests passed, 0 failedNative 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-completeduntil Matt confirms it.🤖 Generated with Claude Code