registration is a control, not a footnote

The heading offers two things and only one of them was a control. Creating an
account was 13px of text at the foot of the login page, on the same line as the
link for people who forgot a password, so a first-time visitor — whose only
business here is that door — had to read past every way of signing in to find it.

It is now a button of the same size and surface as the ways in, directly under
the credential it is the alternative to: 52px tall and the full width of the
column, against 17px of inline text before. It carries the whole OIDC request as
it always did, so registration can still return the new account to the app that
asked for it.

What is left at the foot is the one link that belongs there.
This commit is contained in:
zooqueen
2026-08-16 17:37:43 -07:00
parent 674b6bb263
commit cfc9dbfb80
2 changed files with 44 additions and 6 deletions
+29
View File
@@ -127,6 +127,35 @@ test('prompt=login gets a screen, never a silent mint', async () => {
assert.equal(document.querySelectorAll('input[type=password]').length, 1)
})
// ── The other door ───────────────────────────────────────────────────────────
// The heading offers two things and only one of them was a control: registration
// was 13px of text at the foot of the page, on the same line as the link for
// people who forgot a password. A visitor whose only business here is that door
// had to read past every way of signing in to find it.
test('registration is a control, and it is not in the footnote row', async () => {
land()
await mount([])
const create = [...document.querySelectorAll('a')].find((a) => a.textContent?.trim() === 'Create account')
assert.ok(create, 'the page must offer registration')
assert.ok(
create.className.split(' ').includes('hanzo-id-btn'),
`registration must be a control, not a sentence; it rendered as "${create.className}"`,
)
// Carrying the whole OIDC request is what lets registration return the new
// account to the app that asked for it.
const to = new URL(create.getAttribute('href')!, 'https://hanzo.id')
assert.equal(to.pathname, '/signup')
for (const k of ['client_id', 'redirect_uri', 'state', 'code_challenge']) {
assert.ok(to.searchParams.get(k), `${k} must survive the hop to /signup`)
}
// What is left at the foot is the one link that belongs there.
const foot = document.querySelector('.hanzo-id-footer-links')!
assert.equal(foot.textContent?.trim(), 'Forgot password?')
})
// ── The registration hint ────────────────────────────────────────────────────
// hanzo.app's "Get started" forwards `signup=true` on the authorize request and
// this page ignored it, so a net-new customer landed on "Sign in to Hanzo ID"
+15 -6
View File
@@ -240,14 +240,23 @@ export function Login({ client, brand }: { client: AuthClient; brand: BrandContr
kind={kind}
onKind={setKind}
/>
{/* The heading offers two things and only one of them was a control.
Registration was 13px of text at the foot of the page, beside the
link for people who forgot a password — so a first-time visitor,
whose ONLY business here is this door, had to read past every way of
signing in to find it. It is a button of the same size and surface as
the ways in, directly under the credential it is the alternative to.
Carry the OIDC request across: this is a full page load, so a bare
href drops the client_id, redirect_uri, state and PKCE challenge the
app sent, and registration then has nothing to return the new account
to. `Signup` reads exactly these params. */}
<a className="hanzo-id-btn ghost" href={`/signup${window.location.search}`}>
Create account
</a>
</SocialButtons>
<p className="hanzo-id-footer-links">
{/* Carry the OIDC request across. These are full page loads, so a bare
href drops the client_id, redirect_uri, state and PKCE challenge the
app sent — and registration then has nothing to return the new user
to. `Signup` reads exactly these params. */}
<a href={`/forget${window.location.search}`}>Forgot password?</a> ·{' '}
<a href={`/signup${window.location.search}`}>Create account</a>
<a href={`/forget${window.location.search}`}>Forgot password?</a>
</p>
</main>
<BrandFooter brand={brand} org={client.org} />