feat(onboarding): remove dead ends for new users (#55)

Registration shows the address used and can start over.
Confirm page says an admin approves; no login link.
Welcome puts next steps first; Steam link returns to the dashboard.
Dashboard and library explain an empty or private library.
First sync shows queued, polls slower up to 10 min, then a calm text.
Login: simple checks, resend offer after 401, password toggles.
Only a real expired link counts as invalid; typed passwords stay.
Default language follows the browser; texts are platform-neutral.
This commit is contained in:
lobo committed 2026-10-21 09:37:17 +02:00
1 parent a60010e055
commit f9f0d94e2c
45 files changed
+979 -171

No files matched your search

+24 -7
View File
@@ -138,7 +138,8 @@ flowchart LR
- **Wishlist** with store price and discount.
- **Steam account** linked in the settings with "Sign in through Steam": the user signs in on Steam and Auth gets
the proved Steam ID, never a typed-in one. A Steam ID typed in before shows as "not verified" with a button to
verify it; unlinking is possible.
verify it; unlinking is possible. After a successful link the settings page opens the dashboard (success toast),
where the first sync starts; the other outcomes (in use, cancelled, failed) stay on the settings page.
- **Public profile** at `/u/<display name>`, opt-in in the settings, with perfect games and average completion.
The owner arranges it with the same edit mode on the profile page itself (the tiles, incl. the stat groups of
"Your stats"), and sets its look in the settings: accent, corners, banner and background from own game art
@@ -151,9 +152,16 @@ flowchart LR
- **Registration** (`/register`): no invite code; the form asks for email and display name only (no password); the page says that the user gets a mail with a link, then an admin
checks the request, and stays on "registration sent" after the send. Auth answers `409 PENDING_LIMIT_REACHED` while
20 wait. The mail link opens `/confirm-email#<token>` (the token stays in the fragment and goes to Auth in a POST
body); the page asks for the password (and its repeat) and confirms on a button click, not on load, so mail scanners do not burn the link. An expired or used
link and the page after the send offer "send the link
again" (same answer for any address).
body); the page asks for the password (and its repeat, both with a show/hide toggle and the rules as helper text) and confirms on a button click, not on load, so mail scanners do not burn the link. Only a `410` (or a `400` that names the `token` field) shows "link
invalid"; any other `400` is a form error and keeps the typed password. An expired or used link and the page after
the send offer "send the link again" (same answer for any address). After the send the page shows the address used
and "Wrong address? Start over", which brings the form back. After the confirmation the page says that a mail comes
once an admin has approved the account (no waiting time, no login link).
- **Login** (`/login`): the form checks both fields before it sends (zod, like the other forms) and has a show/hide
toggle on the password. Auth answers every failed login with `401` (a wrong password, an unknown address and an
unconfirmed one look the same), so after a `401` the page offers "Not confirmed yet? Send the link again" with the
typed address.
- **Language default**: the cached language, else the browser language (`de*` gives German, any other English), else German.
- **Forgot password** (`/reset-password`): the login page links to it. Without a fragment it asks for the email and
shows the same text for any address. The mail link opens `/reset-password#<token>` (fragment, sent in a POST body on
the button click only): the page asks for the new password twice. A used or expired link shows the email form again.
@@ -202,8 +210,14 @@ flowchart LR
picked ones, send a test to themselves, confirm with the real recipient count, and see the history with the counts.
- **Welcome page**: `/welcome` opens once after the first login (Auth `settings.welcomeSeenAt` is null; only the
dashboard redirects, deep links stay). Feature cards with links, two next steps (Steam, import) with checks from
`/me` and the library summary. "Let's go" and "Skip" stamp it in Auth and open the dashboard. The settings page
links to it again.
`/me` and the library summary. "Next steps" come first; the main button is the first open step ("Link Steam", then
"Import games"), the one other button is "Go to dashboard". Every way out (buttons, step links, feature links)
stamps it in Auth first, so the gate does not send the user back. The settings page links to it again.
- **Empty library** (dashboard): a card above the tiles. Without a Steam ID or PlayStation link: "Link Steam /
PlayStation" and "Import games". With Steam, a finished sync and no games: Steam's "Game details" must be public,
with a link to the Steam privacy settings (also in the empty library list); no such text for a PlayStation-only user.
The "no Steam" and "empty library" hints stay until the user closes them. Texts are platform neutral ("Welcome"
before the first sync, "Games", "Syncing your library").
- **Correct game data** (AUDITOR or higher): "Correct data" on the game detail page changes name, header image,
speedrun link, moods, endless flag, genres and time to beat (hours and minutes) for every user; each field goes back to the source on its own.
"Merge duplicate" joins two entries of the same game (e.g. the Steam game and the same game added by hand) for
@@ -340,7 +354,10 @@ flowchart LR
- Owner and user lists (`src/components/people/`, `src/utils/people.js`) need a login and show listed users by name
(a link to `/u/<name>`), everyone else as "+N private", the viewer as "You". The setting "show me in owner lists" is
saved on its own (`PUT /api/users/settings/owner-lists`), never through `saveSettings`.
- `useLibrarySync` polls Core's `/api/library/sync-status` while a Steam sync runs, then loads the data again.
- `useLibrarySync` polls Core's `/api/library/sync-status` while a sync runs, then loads the data again. While a first
sync is expected but Core has no status row (`queued`: "First sync queued ..." with "usually under a minute"), it
polls every 3 s for 15 s, then every 15 s, for 10 minutes in all; a run still going after that shows a calm "check
back later" text.
- The bookmarklet hands its list over in the URL fragment (base64url JSON), which the browser sends to no server.
The import page reads it once and removes it from the URL. `RequireSession` keeps the fragment on the way
through the login page.