feat(passkeys): offer passkey sign-in on the login form (#41) #335

Merged
bermudalamb merged 1 commits from feature/41-passkey-login-page into main 2026-09-09 15:53:20 -05:00
Owner

Closes #41.

The point at which passkeys become visible to customers. Everything before this issue was reachable only by knowing the endpoints existed.

What changed

  • signInWithPasskey() and passkeysSupported() in frontend/src/customer/customerApi.ts
  • A passkey button on the Log In tab of AuthForm.tsx, below the password form, behind a divider
  • An end-to-end test in frontend/tests/e2e/auth.spec.ts

Decisions

Below the password form, not above it. Passwords are how every existing customer signs in and a passkey is the alternative, so putting it first would demote the path that works for everyone.

Absent entirely where WebAuthn is unavailable, rather than shown disabled. A greyed-out control invites a customer to wonder what they are missing and offers nothing they can act on, and password login is the fallback in every case regardless. The support check is read once at render because it decides whether the control exists, not whether pressing it works.

Its own loading flag. Sharing the form's would leave the password button disabled and spinning while the browser's prompt is open, which is exactly what this issue says must not happen.

Dismissing the prompt is a cancellation and shows nothing. NotAllowedError and AbortError are the two the browser raises for it. Reporting either as a failure would tell a customer something went wrong when they changed their mind, leaving a red alert above a form that is working perfectly. Everything else shows copy that says what to do next rather than only that something failed, and says nothing about whether an account exists.

Testing

The end-to-end test covers the half of this that can be proven without an authenticator. The failure is injected at the first request, before the browser prompt, so it needs no credential and cannot hang waiting for a gesture nobody will make. The password form behind the error is then used to sign in for real, which is the actual requirement: not a dead end.

The other half cannot be tested anywhere but production, and #41 says so itself:

This is the one issue in the project that cannot be fully proven in QA. Credentials are bound to the Relying Party ID, so a passkey registered against the QA hostname will not work in production. QA verifies the flow, the fallbacks, and the copy; production needs its own smoke test with a real registration afterwards.

Local verification: tsc clean for src and tests, lint clean with no warnings, frontend build green.

🤖 Generated with Claude Code

Closes #41. The point at which passkeys become visible to customers. Everything before this issue was reachable only by knowing the endpoints existed. ## What changed - `signInWithPasskey()` and `passkeysSupported()` in `frontend/src/customer/customerApi.ts` - A passkey button on the Log In tab of `AuthForm.tsx`, below the password form, behind a divider - An end-to-end test in `frontend/tests/e2e/auth.spec.ts` ## Decisions **Below the password form, not above it.** Passwords are how every existing customer signs in and a passkey is the alternative, so putting it first would demote the path that works for everyone. **Absent entirely where WebAuthn is unavailable, rather than shown disabled.** A greyed-out control invites a customer to wonder what they are missing and offers nothing they can act on, and password login is the fallback in every case regardless. The support check is read once at render because it decides whether the control exists, not whether pressing it works. **Its own loading flag.** Sharing the form's would leave the password button disabled and spinning while the browser's prompt is open, which is exactly what this issue says must not happen. **Dismissing the prompt is a cancellation and shows nothing.** `NotAllowedError` and `AbortError` are the two the browser raises for it. Reporting either as a failure would tell a customer something went wrong when they changed their mind, leaving a red alert above a form that is working perfectly. Everything else shows copy that says what to do next rather than only that something failed, and says nothing about whether an account exists. ## Testing The end-to-end test covers the half of this that can be proven without an authenticator. The failure is injected at the first request, before the browser prompt, so it needs no credential and cannot hang waiting for a gesture nobody will make. The password form behind the error is then used to sign in for real, which is the actual requirement: not a dead end. The other half cannot be tested anywhere but production, and #41 says so itself: > This is the one issue in the project that cannot be fully proven in QA. Credentials are bound to the Relying Party ID, so a passkey registered against the QA hostname will not work in production. QA verifies the flow, the fallbacks, and the copy; production needs its own smoke test with a real registration afterwards. Local verification: `tsc` clean for src and tests, lint clean with no warnings, frontend build green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bermudalamb added 1 commit 2026-09-09 15:51:25 -05:00
feat(passkeys): offer passkey sign-in on the login form (#41)
SonarQube Analysis / sonarqube (pull_request) Canceled after 0s
Linting / lint (pull_request) Canceled after 0s
72e8090fd1
The point at which passkeys become visible to customers. Everything before this was reachable only by knowing the endpoints existed.

Below the password form rather than above it. Passwords are how every existing customer signs in and a passkey is the alternative, so putting it first would demote the path that works for everyone.

Absent entirely where WebAuthn is unavailable, rather than shown disabled. A greyed-out control invites a customer to wonder what they are missing and offers nothing they can act on, and password login is the fallback in every case regardless. The check is read once at render because it decides whether the control exists, not whether pressing it works.

The passkey button has its own loading flag rather than sharing the form's. The requirement is that a dismissed prompt leaves a usable password form behind it, and a shared flag would leave that form disabled and spinning while the browser's prompt is open.

Dismissing the prompt is a cancellation and shows nothing. NotAllowedError and AbortError are the two the browser raises for it, and reporting either as a failure would tell a customer something went wrong when they changed their mind — leaving a red alert sitting above a form that is working perfectly. Everything else shows a message that says what to do next rather than only that something failed.

That message says nothing about whether an account exists, which costs nothing to hold to here because the server already answers every refusal identically. There is also no email on this path at all, so there is nothing to be asked about.

The end-to-end test covers the half of this issue that can be proven without an authenticator. The failure is injected at the first request, before the browser prompt, so it needs no credential and cannot hang waiting for a gesture nobody will make — and then the password form behind the error is used to sign in for real. That is the requirement: not a dead end.

The other half cannot be tested anywhere but production, and this issue says so itself. Credentials bind to the Relying Party ID, so a passkey registered against QA will not work against production. QA proves the flow, the fallbacks and the copy; production needs its own smoke test with a real registration afterwards, and that is a standing property of the feature rather than a gap in this change.

Verified: tsc clean for src and tests, lint clean with no warnings, frontend build green.

Closes #41

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bermudalamb merged commit a0238ea4ed into main 2026-09-09 15:53:20 -05:00
bermudalamb deleted branch feature/41-passkey-login-page 2026-09-09 15:53:21 -05:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#335