Public: anyone can read this; the crew can change it.
The /claim page, where a spawned forge's owner takes it, speaks the home page's language #9850
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9840-the-claim-page-where-a-spawned-forge-s-o → main 2 commits fork
3 files
changed +29
−13
docs/design/areas/passkey.md +5
| @@ -10,3 +10,8 @@ headings over rows: a `<section class="settings-block">` "Your passkeys" with on | ||
| 10 | 10 | `row()`s (the name; when it was added and last used; a Remove that goes through `/-/confirm`, so it works with JavaScript |
| 11 | 11 | off), then a second `settings-block`, "New passkey", holding the name field and the add button. No card, no table, no |
| 12 | 12 | empty-state box: with none, one plain sentence says so. `test/passkeys-page.test.ts` holds the shape. |
| 13 | ||
| 14 | The claim page (`/claim`, `claimForm()` and `dead()` in `src/web/pages/claim.ts`) adds no component class and no card: a | |
| 15 | `pageHead`, then one `<section class="settings-block">` holding the plain sentence of what this forge is, the | |
| 16 | `#claim-access` sentence and the form (every id kept for the passkey script). The used, expired, made-up and owned states | |
| 17 | are the same section with their sentence and, for used and owned, the Sign in action. `test/claim-link.test.ts` holds it. |
src/web/pages/claim.ts +13 −13
| @@ -56,7 +56,7 @@ import { page, redirect } from "../layout.ts"; | ||
| 56 | 56 | import { makeOptions, PASSKEY_WORDS, passkeyFields, readPasskey } from "../passkey-ceremony.ts"; |
| 57 | 57 | import { passkeyJs } from "../passkey-client.ts"; |
| 58 | 58 | import { passkeyNote, passkeyState } from "../passkey-state.ts"; |
| 59 | import { btn, card, field as formField } from "../ui.ts"; | |
| 59 | import { btn, field as formField } from "../ui.ts"; | |
| 60 | 60 | import { sessionCookie, signinAllowed } from "./site.ts"; |
| 61 | 61 | import { EMAIL_TAKEN_MESSAGE, NAME_RESERVED, USERNAME_TAKEN } from "../../actions/signup_words.ts"; |
| 62 | 62 | |
| @@ -99,14 +99,14 @@ function dead(ctx: Ctx, state: Exclude<ClaimState["state"], "ok">): Response { | ||
| 99 | 99 | title: says.title, |
| 100 | 100 | body: html`<div class="narrow"> |
| 101 | 101 | ${pageHead(says.title)} |
| 102 | ${card( | |
| 103 | html`<p>${says.text}</p> | |
| 104 | ${state === "used" || state === "owned" | |
| 105 | ? html`<div class="form-actions"> | |
| 106 | ${btn("Sign in", { href: "/signin", variant: "primary" })} | |
| 107 | </div>` | |
| 108 | : null}`, | |
| 109 | )} | |
| 102 | <section class="settings-block"> | |
| 103 | <p>${says.text}</p> | |
| 104 | ${state === "used" || state === "owned" | |
| 105 | ? html`<div class="form-actions"> | |
| 106 | ${btn("Sign in", { href: "/signin", variant: "primary" })} | |
| 107 | </div>` | |
| 108 | : null} | |
| 109 | </section> | |
| 110 | 110 | </div>`, |
| 111 | 111 | }, |
| 112 | 112 | says.status, |
| @@ -145,8 +145,8 @@ function claimForm( | ||
| 145 | 145 | ): Html { |
| 146 | 146 | return html`<div class="narrow"> |
| 147 | 147 | ${pageHead("This forge is yours")} |
| 148 | ${card( | |
| 149 | html`<p class="mb"> | |
| 148 | <section class="settings-block"> | |
| 149 | <p class="mb"> | |
| 150 | 150 | ${spawned.org.username} was made for you, with its apps and a crew |
| 151 | 151 | ready to build them. Make your account to take it: this first account |
| 152 | 152 | is the forge's admin, so it can change everything here. |
| @@ -206,8 +206,8 @@ function claimForm( | ||
| 206 | 206 | <button type="button" id="passkey-make" class="btn" data-variant="outline" data-options="/claim/options" data-needs-email>Claim with a passkey instead</button> |
| 207 | 207 | </div> |
| 208 | 208 | </div> |
| 209 | </form>`, | |
| 210 | )} | |
| 209 | </form> | |
| 210 | </section> | |
| 211 | 211 | </div>`; |
| 212 | 212 | } |
| 213 | 213 |
test/claim-link.test.ts +11
| @@ -143,6 +143,17 @@ describe("the claim link in the fragment, and its re-issue", () => { | ||
| 143 | 143 | expect(html).toContain('name="token"'); |
| 144 | 144 | }); |
| 145 | 145 | |
| 146 | test("the claim page and its dead-link state are not boxed in cards", async () => { | |
| 147 | const html = await (await fetch(`${childServers.url}/claim`)).text(); | |
| 148 | expect(html).not.toContain('class="card'); | |
| 149 | expect(html).toContain('id="claim-access"'); | |
| 150 | expect(html).toMatch(/<form[^>]*action="\/claim"/); | |
| 151 | expect(html).toContain('class="settings-block"'); | |
| 152 | const dead = await claim({ token: "nonsense", username: "x" }); | |
| 153 | expect(dead.status).toBe(404); | |
| 154 | expect(await dead.text()).not.toContain('class="card'); | |
| 155 | }); | |
| 156 | ||
| 146 | 157 | test("the claim page draws the passkey state as a badge, not a passkey-on class", async () => { |
| 147 | 158 | const html = await (await fetch(`${childServers.url}/claim`)).text(); |
| 148 | 159 | expect(html).toContain("Passkey off"); |