refactor: simplify sign-up form #146

Sammanfogat
supernaut sammanfogade 5 incheckningar från refactor/sign-up in i main 2026-08-02 11:52:54 +00:00
Ägare

Makes the sign-up form quicker to get through, and turns the trial from a
side-option into the main choice.

What changed

Country of residence is no longer asked at sign-up. It is only needed for
payment processing, and no account type on the form is a purchase yet, so it
was asking for data we cannot use — the one field that made people stop and
think. It is not hardcoded to SE either: requiresCountry() in
src/lib/eligibility.ts is the single place that decides which account types
must declare a country, and it is deliberately empty today. Adding paid there
when that type lands is the whole change needed to bring the field back, and
the eligibility check runs again automatically for it.

signups.country_code and .region are now nullable (migration
0005_furry_iron_lad.sql, two DROP NOT NULL statements, no data touched).
NULL means "we did not ask", which is distinguishable from "they declared
Sweden". The previous hardcode wrote a value nobody had declared into the table
the schema describes as the legal and banking record.

Account type is one control instead of two. The trial used to be an opt-in
checkbox below the form, easy to fill past without noticing. It is now a radio
group — trial or free — so choosing is an explicit step.

The trial kill switch works again. showTrial was computed and then never
used, so with TRIAL_OPEN=false the trial option still rendered and was still
pre-selected, while the API fell back to a participant account: a user could
deliberately pick the trial, accept its separate terms, and quietly get a free
account. The option is now disabled when the trial is retired, and the free
option becomes the pre-selected one. Disabled rather than hidden, so the page
stays honest about what existed — and it is what the :has(:disabled) styling
already in forms.scss was written for.

The step indicator is horizontal. Four stacked lines sat directly above the
form and pushed the first field down; it is now a wrapping trail with counter
numbers and separators, so narrow screens still get everything on as many rows
as they need.

The form uses two columns on wide displays. Username and email share a row;
the account-type fieldset, consent row, anti-spam widget, notices and submit
button stay full width, where line length still matters. Scoped to a .two-col
modifier rather than form.stack, because the resend form in the same
component is also a form.stack and should keep its narrow measure. Explicit
column spans rather than auto-placement, so the tab order cannot drift from the
visual order.

Less on the page otherwise. The intro paragraph and the standalone trial
hint block are gone; the trial explanation sits with the option it describes.

Accessibility fix. The trial radio's aria-describedby pointed at an id
that did not exist, so screen-reader users got a description on the free option
and nothing on the trial one.

Still open

  • The trial option is pre-selected when the trial is open. The old checkbox
    was deliberately left unticked because selecting the trial accepts a separate
    set of terms, and a pre-ticked consent control is not consent. A radio group
    is arguably a product choice rather than a consent tick, but the trial terms
    are still a distinct document. Left as-is pending a decision.
  • signup.country, signup.countryOther and signup.countryPlaceholder are
    unused in both languages, as are ELIGIBLE_COUNTRIES and the country <select>
    markup. Kept rather than deleted because the paid path needs them back, and
    re-translating is worse than leaving them. Worth a decision either way.
  • The ineligible status is reachable again, but only once requiresCountry()
    returns true for something — so nothing exercises it yet.
  • Copy for the new account-type options has not been through the content style
    guide.

Typecheck, lint, build and 177 tests are clean. The migration is generated but
not applied — the container's scripts/migrate.mjs runs it on deploy.

Makes the sign-up form quicker to get through, and turns the trial from a side-option into the main choice. ## What changed **Country of residence is no longer asked at sign-up.** It is only needed for payment processing, and no account type on the form is a purchase yet, so it was asking for data we cannot use — the one field that made people stop and think. It is not hardcoded to `SE` either: `requiresCountry()` in `src/lib/eligibility.ts` is the single place that decides which account types must declare a country, and it is deliberately empty today. Adding `paid` there when that type lands is the whole change needed to bring the field back, and the eligibility check runs again automatically for it. **`signups.country_code` and `.region` are now nullable** (migration `0005_furry_iron_lad.sql`, two `DROP NOT NULL` statements, no data touched). NULL means "we did not ask", which is distinguishable from "they declared Sweden". The previous hardcode wrote a value nobody had declared into the table the schema describes as the legal and banking record. **Account type is one control instead of two.** The trial used to be an opt-in checkbox below the form, easy to fill past without noticing. It is now a radio group — trial or free — so choosing is an explicit step. **The trial kill switch works again.** `showTrial` was computed and then never used, so with `TRIAL_OPEN=false` the trial option still rendered and was still pre-selected, while the API fell back to a participant account: a user could deliberately pick the trial, accept its separate terms, and quietly get a free account. The option is now `disabled` when the trial is retired, and the free option becomes the pre-selected one. Disabled rather than hidden, so the page stays honest about what existed — and it is what the `:has(:disabled)` styling already in `forms.scss` was written for. **The step indicator is horizontal.** Four stacked lines sat directly above the form and pushed the first field down; it is now a wrapping trail with counter numbers and separators, so narrow screens still get everything on as many rows as they need. **The form uses two columns on wide displays.** Username and email share a row; the account-type fieldset, consent row, anti-spam widget, notices and submit button stay full width, where line length still matters. Scoped to a `.two-col` modifier rather than `form.stack`, because the resend form in the same component is also a `form.stack` and should keep its narrow measure. Explicit column spans rather than auto-placement, so the tab order cannot drift from the visual order. **Less on the page otherwise.** The intro paragraph and the standalone trial hint block are gone; the trial explanation sits with the option it describes. **Accessibility fix.** The trial radio's `aria-describedby` pointed at an id that did not exist, so screen-reader users got a description on the free option and nothing on the trial one. ## Still open - **The trial option is pre-selected when the trial is open.** The old checkbox was deliberately left unticked because selecting the trial accepts a separate set of terms, and a pre-ticked consent control is not consent. A radio group is arguably a product choice rather than a consent tick, but the trial terms are still a distinct document. Left as-is pending a decision. - `signup.country`, `signup.countryOther` and `signup.countryPlaceholder` are unused in both languages, as are `ELIGIBLE_COUNTRIES` and the country `<select>` markup. Kept rather than deleted because the paid path needs them back, and re-translating is worse than leaving them. Worth a decision either way. - The `ineligible` status is reachable again, but only once `requiresCountry()` returns true for something — so nothing exercises it yet. - Copy for the new account-type options has not been through the content style guide. Typecheck, lint, build and 177 tests are clean. The migration is generated but not applied — the container's `scripts/migrate.mjs` runs it on deploy.
supernaut lade till 1 incheckning 2026-08-01 22:02:29 +00:00
feat: simplify sign-up form
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m15s
c88444ad7a
supernaut lade till 1 incheckning 2026-08-01 22:50:37 +00:00
feat(signup): ask for country only when an account type needs it
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m22s
0b4ea3f3fb
Country of residence is only needed for payment processing, and no account
type on the form is a purchase yet, so nobody is asked for it. Replaces the
hardcoded "SE" the field's removal left behind, which wrote a value nobody
declared into the table the schema describes as the legal and banking record.

requiresCountry() is now the single place deciding which account types must
declare one — empty today, so the eligibility check and the ineligible status
are wired but dormant until a paid type is added there.

country_code and region become nullable so "we did not ask" is distinguishable
from "they declared Sweden". Two DROP NOT NULL statements; no data touched.

Also restores the trial kill switch: showTrial was computed and never used, so
with TRIAL_OPEN=false the trial option still rendered and was still selected
while the API fell back to a participant account — a user could pick the trial,
accept its separate terms, and quietly get a free account. It is now disabled
rather than hidden, and free becomes the pre-selected option.

Layout: the step indicator is a wrapping horizontal trail instead of four
stacked lines above the form, and the form goes to two columns on wide
displays (username and email share a row; everything else stays full width).
Scoped to a .two-col modifier so the resend form keeps its narrow measure.

Fixes the trial radio's aria-describedby, which pointed at a non-existent id.
supernaut tvångsskickade refactor/sign-up från 0b4ea3f3fb
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m22s
till 3a8b956c0c
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m15s
2026-08-02 10:16:43 +00:00
Jämför
supernaut lade till 1 incheckning 2026-08-02 10:23:17 +00:00
copy(signup): lead the participant option with its price
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m19s
a4267d3b31
The English description broke the pattern its siblings set: they are imperative
("Create repositories…"), this one switched to third person ("Lets you clone…")
and split the cost into a two-word fragment. The Swedish already folds cost into
the sentence, so the two languages were not parallel either.

Leads with "Free of charge", which is the distinguishing feature of this option
and the thing a reader scanning a radio list needs first, then matches the
sibling strings' imperative voice. Also drops the serial comma, which its own
sibling and the Swedish both omit.

English only — the Swedish is already the version this now matches.
supernaut lade till 1 incheckning 2026-08-02 11:05:03 +00:00
refactor: compact sign-up form
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m22s
264d4ccbfa
supernaut tvångsskickade refactor/sign-up från 264d4ccbfa
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m22s
till f44cfc2b37
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m21s
2026-08-02 11:13:46 +00:00
Jämför
supernaut lade till 1 incheckning 2026-08-02 11:50:40 +00:00
refactor: adjust form styling
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m21s
0400342eac
supernaut ändrade titeln från WIP: simplify the sign-up form till refactor: simplify sign-up form 2026-08-02 11:50:54 +00:00
supernaut sammanfogade incheckning 05ff539b0a till main 2026-08-02 11:52:54 +00:00
supernaut tog bort grenen refactor/sign-up 2026-08-02 11:52:55 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-08-02 11:52:55 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-08-03 09:41:50 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-10-01 19:49:37 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-10-02 17:56:59 +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!146
Ingen beskrivning angiven.