fix(signup): journey follow-ups — heading, announcement, sitemap, sign-in note, journey() seam #125

Sammanfogat
supernaut sammanfogade 2 incheckningar från fix/120-124-journey-followups in i main 2026-07-31 19:14:57 +00:00
Ägare

Closes #120, #121, #122, #123, #124 — all five bitborg-web follow-ups from the sign-up journey epic's final review. Two commits: four small fixes, then one refactor.

#120 + #124 — /welcome said the same thing twice, and omitted what mattered

welcome.title duplicated signup.step.start verbatim in both languages, so the page printed "Start using Bitborg" as step 4 of the list and again as the heading immediately below. It looked like a mistake, and the two strings had to be hand-synced for no reason.

Separately, the design specified a line telling the user they may already be signed in, and the implementation dropped it. That line earns its place: the page is reached straight after authenticating on Gitborg Auth, so clicking through may complete with no prompt at all — which, to someone who has just been asked for a credential, reads as "nothing happened".

Both fixed together, because the heading, body and button were all circling one sentence:

Key Before After
welcome.title "Start using Bitborg" "You are ready" / "Du är klar"
welcome.body "If you have chosen how you sign in, you are ready. Sign in to Bitborg…" "This is the last step. You may be signed in already, in which case it takes you straight there."

The new body restates neither the heading above it nor the button beneath it.

#121 — the step list announced its title twice

The same string was both the region's aria-label and its first visible child, so a screen reader read "This takes four steps" as the region name and then again as content. Now aria-labelledby pointing at that paragraph — the region stays navigable by name, and the accessible name cannot drift from the visible text.

#122 — /welcome was missing from the sitemap filter

astro.config.mjs already excludes /signup, /account and /auth/*. The two new routes are mid-journey pages: a search visitor landing on "You are ready — sign in to finish setting up" has no account and no idea what preceded it.

#123 — journey() conflated page identity with query status

journey() took one string meaning two things. welcome is a page, not a status, but it was smuggled through the status argument — so /signup?status=welcome rendered "steps 1-3 done, step 4 current" above a complete, empty sign-up form.

Unreachable legitimately and harmless, but the conflation would have spread: the next surface needing a step state faces the same choice and the answer keeps being "invent another pseudo-status".

Now journey({ page, status }). The page decides how far along the user is; the status only says what happened on it. page is set by the calling page and is not user input; status stays attacker-controlled from the query string, still falls back safely on unknown values, and is still never used as an index or key lookup.

Written test-first. Two new tests name the defect directly — that a query string cannot make the sign-up page claim near-completion, and that the welcome page reads as step four regardless of any status including success, nomail and nonsense. The superseded states("welcome") test was removed rather than adapted, since the behaviour it asserted is exactly what this change makes impossible.

Both /welcome pages now pass status={null} explicitly, documenting that nothing there depends on a query string.

Verification

  • 140 tests pass (one net new after removing the superseded one)
  • astro check 0 errors; eslint + stylelint clean after an import/declaration-order autofix
  • Content-style detector clean on the new Swedish and English strings

Deployed state this builds on

The epic is live and verified in production: all four pages render the step list, ?status=nomail correctly shows zero aria-current (step 2 is blocked, not current) and carries no inbox instruction, and every page ships four visually-hidden state prefixes.

Closes #120, #121, #122, #123, #124 — all five bitborg-web follow-ups from the sign-up journey epic's final review. Two commits: four small fixes, then one refactor. ## #120 + #124 — /welcome said the same thing twice, and omitted what mattered `welcome.title` duplicated `signup.step.start` verbatim in both languages, so the page printed "Start using Bitborg" as step 4 of the list and again as the heading immediately below. It looked like a mistake, and the two strings had to be hand-synced for no reason. Separately, the design specified a line telling the user they may already be signed in, and the implementation dropped it. That line earns its place: the page is reached straight after authenticating on Gitborg Auth, so clicking through may complete with **no prompt at all** — which, to someone who has just been asked for a credential, reads as "nothing happened". Both fixed together, because the heading, body and button were all circling one sentence: | Key | Before | After | | --- | --- | --- | | `welcome.title` | "Start using Bitborg" | "You are ready" / "Du är klar" | | `welcome.body` | "If you have chosen how you sign in, you are ready. Sign in to Bitborg…" | "This is the last step. You may be signed in already, in which case it takes you straight there." | The new body restates neither the heading above it nor the button beneath it. ## #121 — the step list announced its title twice The same string was both the region's `aria-label` and its first visible child, so a screen reader read "This takes four steps" as the region name and then again as content. Now `aria-labelledby` pointing at that paragraph — the region stays navigable by name, and the accessible name cannot drift from the visible text. ## #122 — /welcome was missing from the sitemap filter `astro.config.mjs` already excludes `/signup`, `/account` and `/auth/*`. The two new routes are mid-journey pages: a search visitor landing on "You are ready — sign in to finish setting up" has no account and no idea what preceded it. ## #123 — journey() conflated page identity with query status `journey()` took one string meaning two things. `welcome` is a **page**, not a status, but it was smuggled through the status argument — so `/signup?status=welcome` rendered "steps 1-3 done, step 4 current" above a complete, empty sign-up form. Unreachable legitimately and harmless, but the conflation would have spread: the next surface needing a step state faces the same choice and the answer keeps being "invent another pseudo-status". Now `journey({ page, status })`. The page decides how far along the user is; the status only says what happened on it. `page` is set by the calling page and is not user input; `status` stays attacker-controlled from the query string, still falls back safely on unknown values, and is still never used as an index or key lookup. Written **test-first**. Two new tests name the defect directly — that a query string cannot make the sign-up page claim near-completion, and that the welcome page reads as step four regardless of any status including `success`, `nomail` and nonsense. The superseded `states("welcome")` test was **removed rather than adapted**, since the behaviour it asserted is exactly what this change makes impossible. Both `/welcome` pages now pass `status={null}` explicitly, documenting that nothing there depends on a query string. ## Verification - **140 tests** pass (one net new after removing the superseded one) - `astro check` 0 errors; eslint + stylelint clean after an import/declaration-order autofix - Content-style detector clean on the new Swedish and English strings ## Deployed state this builds on The epic is live and verified in production: all four pages render the step list, `?status=nomail` correctly shows **zero** `aria-current` (step 2 is blocked, not current) and carries no inbox instruction, and every page ships four visually-hidden state prefixes.
supernaut lade till 2 incheckningar 2026-07-31 18:55:43 +00:00
Closes #120, closes #121, closes #122, closes #124.

#120: /welcome printed "Start using Gitborg" twice, once as step 4 of the list and
again as the page heading, because welcome.title duplicated signup.step.start
verbatim in both languages. It looked like a mistake, and the two strings had to be
kept in sync by hand for no reason. The heading now carries something else ("You are
ready" / "Du är klar") since the step list already says which step this is.

#124: the design specified a line telling the user they may already be signed in,
and the implementation dropped it. It matters because the page is reached straight
after authenticating on Gitborg Auth, so the sign-in may complete with no prompt at
all — which, to someone just asked for a credential, reads as "nothing happened".
Folded into welcome.body, which was rewritten so it no longer restates either the
heading above it or the button beneath it.

#121: the step list announced its own title twice — the same string was both the
region's aria-label and its first visible child. Now aria-labelledby pointing at
that paragraph, so the region stays navigable by name and the accessible name cannot
drift from the visible text.

#122: /welcome and /en/welcome were missing from the sitemap filter that already
excludes /signup, /account and /auth/*. They are mid-journey pages, meaningless out
of context — a search visitor landing there is told to finish signing in to an
account they do not have.

139 tests pass, astro check 0 errors, eslint + stylelint clean, content-style
detector clean on the new Swedish and English strings.
refactor(signup): separate page identity from query status in journey()
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m3s
71981d64d8
Closes #123.

`journey()` took a single string that meant two different things. `welcome` is a
page, not a status, but it was threaded through the status argument — so visiting
/signup?status=welcome rendered "steps 1-3 done, step 4 current" above a complete,
empty sign-up form. Unreachable legitimately, and harmless, but the conflation would
have spread: the next surface needing a step state faces the same choice and the
answer keeps being "invent another pseudo-status".

Now `journey({ page, status })`. The page decides how far along the user is; the
status only says what happened on that page. `page` is set by the calling page and is
not user input, while `status` remains attacker-controlled from the query string —
unknown values fall back to the start of the page's own progress and are never used
as an index or key lookup.

Written test-first. Two new tests name the defect directly: that a query string
cannot make the sign-up page claim near-completion, and that the welcome page reads
as step four regardless of any status — including "success", "nomail" and nonsense.
The superseded states("welcome") test was removed rather than adapted, since the
behaviour it asserted is precisely what this change makes impossible.

Both /welcome pages now pass status={null} explicitly, which also documents that
nothing on those pages depends on a query string.

140 tests pass, astro check 0 errors, eslint and stylelint clean after an import and
declaration-order autofix.
supernaut sammanfogade incheckning 8439fa491f till main 2026-07-31 19:14:57 +00:00
supernaut tog bort grenen fix/120-124-journey-followups 2026-07-31 19:14:57 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-web!125
Ingen beskrivning angiven.