Skip to content

fix(auth): translate event onboarding and retry failed invite lookups - #389

Merged
elliotBraem merged 18 commits into
mainfrom
fix/386-onboard-i18n-lookup-errors
Oct 28, 2026
Merged

elliotBraem merged 18 commits into
mainfrom
fix/386-onboard-i18n-lookup-errors

Conversation

@MisbahAnsar

Copy link
Copy Markdown
Collaborator

Closes #386

Summary

Event /onboard?code= was overwritten back to English-only copy with an endless spinner when invite lookup failed.

 OnboardPage
-  hardcoded English, spinner until info arrives
-  join error → Explore only
+  LoginI18nProvider + auth.onboard.* (en/es/fr/zh)
+  "Join the community"
+  lookup error → Retry
+  join failure → specific reason + next step

…e lookups

Event join was overwritten back to English-only copy with an endless spinner on lookup failure. Wire the existing auth.onboard keys, community wording, typed redeem reasons, and a Retry state so attendees can finish joining.

Co-authored-by: Cursor <cursoragent@cursor.com>
MisbahAnsar and others added 5 commits October 21, 2026 06:58
A lapsed session now says to sign in with a different account, closed invites say to contact the community, and the step bar no longer depends on login i18n being in scope.

Co-authored-by: Cursor <cursoragent@cursor.com>
…le copy

Revoked and used-up invites no longer fail the browser suite on old "revoked"/"reached its limit" wording, and a completed join still shows the success panel if the QR expires before the attendee reloads.

Co-authored-by: Cursor <cursoragent@cursor.com>
The regression job died because it asked for the pnpm store path before pnpm existed, so the cache path was empty. Also keep the plugin test loop from failing on folders with no package.json.
Regression HTML loads were treating valid session cookies as signed-out because Playwright stored them as localhost domain cookies and a cached null session skipped the real getSession read.
@itexpert120

Copy link
Copy Markdown
Collaborator

@private-review full

@multi-review multi-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inline findings for 96e81f4; see the summary comment for the rest.


const [accountCreated, setAccountCreated] = useState(false);
const [redeemed, setRedeemed] = useState<Redeemed | null>(null);
const [redeemed, setRedeemed] = useState<Redeemed | null>(() => readStoredRedeemed(code));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Medium · correctness: Cached join is keyed only by invite code, so another user or a signed-out visitor sees “You're in” without joining

readStoredRedeemed(code) sets redeemed from sessionStorage using only citynode.onboard.redeemed:<code>. It does not check who is signed in. Once redeemed is truthy, two things follow: the join effect returns early, so redeemOnboardingCode never runs, and the joined panel renders.

Concrete scenario: user A joins via /onboard?code=X. A signs out in the same tab, which happens on a shared phone or a borrowed device at an event. User B then signs in and opens the same link. B is shown “You've joined Chicago Builders…” but is never added to the organization or team.

The same applies to a signed-out visitor whose session has lapsed. They get the joined panel, and because session?.user.name is undefined they also get the DisplayNameStep, whose updateUser call then fails.

The cache is also unnecessary for the case it was added for. The server already treats a repeat redeem as idempotent: a prior redeemer who is still a member gets the joined result even after the code expires (services/onboarding.ts lines 395–413). Stale entries (for example after the member was removed) also keep showing the joined state for the rest of the tab session.

Suggested fix: Store the user id with the cached result and only restore it when session.user.id matches, ignoring it while the session is pending or null. Alternatively, drop the sessionStorage cache and rely on the server's idempotent redeem, which already handles reloads after the code expires.

@multi-review

multi-review Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Multi Review · 50991e8 (changes since 96e81f4)

The PR returns the resolved session row from the auth plugin's getContext so the host can show signed-in pages as signed in again on the server. It deletes the app-level requireSession workaround and the sessionStorage join cache on /onboard, which was keyed only by invite code. Without the cache, a reload sends the join request again, and the server answers "already joined" for existing members even after the code expires. Risk looks low, and the earlier cached-join finding no longer applies.

No new issues found · 1 still open from earlier

What was checked (8 areas)
  • SessionService.getContext: the new session field on the cookie-session, API-key and signed-out paths
  • Callers of getContext: the /v1/auth/context route, UI callers of apiClient.auth.getContext, and integration tests
  • requestContextSchema and contextSessionSchema against the shape the service returns, and places that build the type by hand (UI tests use casts)
  • getSession already returns the session token, so getContext exposes nothing new
  • The onboard.tsx join effect without the sessionStorage cache, including remounts and different users
  • Server redeem logic in onboarding.ts: a member reusing an expired code gets "already joined"
  • ui/src/app.ts: requireSession now comes from lib/auth (everything-dev/ui/auth); callers are _authenticated.tsx and _admin.tsx; the root route's handling of the server-provided session
  • Earlier finding about the cached join keyed only by invite code: no longer applies because the cache is gone

Still open from earlier reviews

  • 🟠 Medium plugins/auth/ui/src/routes/_public/onboard.tsx:170 Cached join is keyed only by invite code, so another user or a signed-out visitor sees “You're in” without joining

Comment @multi-review to re-review new commits, or @multi-review full for a full review.

… cache

Return the resolved session from the auth plugin's getContext() so the
host's SSR auth sees signed-in users again, and revert the requireSession
workaround in ui/src/app.ts.

Drop the sessionStorage join cache on /onboard. It was keyed only by the
invite code, so a later visitor in the same tab (another user or a
signed-out one) saw the joined state without redeeming. The server already
treats a repeat redeem by an existing member as idempotent, including after
the code expires, so a reload re-redeems and still lands on the joined panel.
@itexpert120

Copy link
Copy Markdown
Collaborator

@multi-review

@adenarc

adenarc commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

@MisbahAnsar resolve merge conflicts.

@MisbahAnsar

Copy link
Copy Markdown
Collaborator Author

@adenarc

@adenarc adenarc 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.

Went through this twice. The i18n coverage looks complete (every key used exists in all 4 locales) and the wording is all "community" now. A few things inline, mostly around error states.

Comment thread plugins/auth-ui/src/routes/_public/onboard.tsx Outdated
Comment thread plugins/auth-ui/src/routes/_public/onboard.tsx Outdated
Comment thread plugins/auth-ui/src/routes/_public/onboard.tsx Outdated
Comment thread plugins/auth-ui/src/i18n/messages/auth.ts Outdated
Comment thread plugins/auth-ui/src/routes/_public/onboard.tsx Outdated
Comment thread plugins/auth-ui/src/routes/_public/onboard.tsx Outdated
Comment thread .github/workflows/ci-child.yml
…ying refused joins

Only show the invitation load error when there is no invitation to show, and drop the Retry spinner that could not render. An unrecognised FORBIDDEN or BAD_REQUEST from redeem now shows a contact-the-community state instead of Retry. An expired session now asks the member to sign in again rather than use a different account. Status panels put their test id on the panel root.
@elliotBraem

Copy link
Copy Markdown
Contributor

Hold off on this one @MisbahAnsar, I can take it from here

@elliotBraem elliotBraem left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking

…ss unit-test CI job

Salvage of #386-onboard-i18n-lookup-errors after local auth-ui moved to
the upstream published auth plugin (#425):

- Extract cookie mapping/encoding into tests/regression/lib/playwright-cookies.mjs
  with unit tests (Cookie values and Secure flag now follow the stack
  origin, so Chromium sends minted session cookies back to the app)
- auth-test-instance: pin baseURL + cookie options to the stack origin
- ci-child: gate a regression-unit job (test:regression:unit)

The onboard i18n/retry half of the original PR now belongs upstream in
everything-dev plugins/auth/ui/ (see citynode#386).
@elliotBraem

Copy link
Copy Markdown
Contributor

Resolved the conflicts by rebranching onto current main — but not by re-adding everything. Since #425 removed the local auth-ui plugin in favor of the upstream published @everything-dev/auth-plugin, the PR now splits in two halves:

Kept (this PR, salvaged from the original):

  • tests/regression/lib/playwright-cookies.mjs + unit tests — cookie mapping/encoding helpers (values and Secure flag now follow the stack origin instead of a localhost domain, so Chromium sends minted session cookies back to the app)
  • auth-session.ts / seeded.ts refactors and spec updates (event-station, organizer-stations, organization-approval) to use injectSessionCookies
  • auth-test-instance.ts: baseURL + fixtureAuthCookieOptions pinned to the stack origin (hand-merged with main's frozen-schema/no-migration change)
  • ci-child.yml: new gating regression-unit job running test:regression:unit

Dropped (superseded / dead here):

  • all plugins/auth-ui/** — deleted on main by chore: remove local auth-ui, use upstream published auth plugin #425; the onboard page now ships from the upstream plugin
  • the changeset (changesets were dropped in 805678b), the .gitignore line, and the jsdom lockfile hunk
  • the onboarding.spec.ts wording change — the expected copy (revoked, reached its limit) still matches the published upstream plugin

Moved upstream: the actual #386 fix (i18n + "community" wording + retry on failed invite lookup) now belongs in plugins/auth/ui/ in NEARBuilders/everything-dev, which still has the bug — see the upstream issue I filed.

Also picked up a tiny formatting fix in bos.app.ts (pre-existing on main, would have failed lint).

@elliotBraem
elliotBraem merged commit 57b1e56 into main Oct 28, 2026
7 checks passed
@elliotBraem
elliotBraem deleted the fix/386-onboard-i18n-lookup-errors branch October 28, 2026 18:05
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.

Onboarding at events: translate /onboard, use "community" wording, and handle failed invite lookups

5 participants