Skip to content

feat(passkey): let a user finish registration without a passkey - #122

Merged
Bccorb merged 2 commits into
mainfrom
feat/skip-passkey-registration
Jul 31, 2026
Merged

feat(passkey): let a user finish registration without a passkey#122
Bccorb merged 2 commits into
mainfrom
feat/skip-passkey-registration

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Last of four PRs. This is the actual feature.

Draft: blocked on the chain below. Verified locally against a local build of the types package.

  1. feat(system-config): add a public system config response contract seamless-auth-types#23 (contract)
  2. feat(system-config): serve the public system config unauthenticated seamless-auth-api#136 (endpoint)
  3. feat(system-config): proxy the public system config from both adapters seamless-auth-server#143 (both adapters proxy it)
  4. this

The problem

Registration ended on a screen with exactly one control: "Register Passkey". No skip, no back. And if passkeySupported was false it rendered "Passkeys are not supported on this device." and nothing else, on a screen the user could not leave.

The session already exists by the time that screen renders, since the OTP step that leads there establishes it. EmailRegistration already finishes without a passkey when the device does not support one, using exactly await refreshSession(); navigate('/'). So leaving without a passkey was always a legitimate way to finish, just not one the UI offered.

The gate

The skip only appears when a method other than passkey is enabled. With passkey as the only method, skipping would leave a user unable to sign back into the account they just created, so the control is not rendered at all.

Unknown counts as unsafe. useLoginMethods returns null rather than [] when the read fails or is in flight, and hasNonPasskeyLoginMethod(null) is false. A network blip cannot produce a skip button that strands someone.

The unsupported-device branch now distinguishes the two cases: a way forward when one exists, and a clear statement that the application requires a passkey when it does not.

Also fixed

Login started from a hardcoded ['passkey', 'magic_link', 'phone_otp'], which does not match the auth server's SYSTEM_CONFIG_DEFAULTS.login_methods = ['passkey', 'magic_link']. It advertised phone OTP on instances that never enabled it. It now uses the methods the instance reports, with a narrower fallback matching the server. The login response stays authoritative when it carries methods of its own.

Scope I backed out

I briefly routed AuthFallbackOptions' internal default through the same constant. That narrowed its behaviour and broke two of its tests, which were right to fail: that component is the last step of a fallback flow and should stay permissive about handlers a caller explicitly wired up. Reverted to its original default, with only the prop type widened to accept null.

Tests

290 passing, up from 279. Eight new:

  • skip appears with another method enabled, and finishes without calling registerPasskey
  • no skip when passkey is the only method
  • no skip while methods are unknown
  • unsupported device gets a way forward when one exists, and is told plainly when it does not
  • useLoginMethods returns the reported methods, and leaves them null on both a failed read and an empty list
  • hasNonPasskeyLoginMethod across all four inputs

The existing unsupported-state test was updated: it asserted the exact dead-end copy this PR removes.

hasNonPasskeyLoginMethod is deliberately not mocked in the view tests. It encodes the rule that decides whether a skip is safe, so mocking it would test nothing.

Verification

npm run typecheck, npm test (290 passing), npm run lint, npm run format:check, npm run build all clean.

Bccorb added 2 commits July 30, 2026 22:19
Registration ended on a screen with one control on it. A user who did not want
a passkey, or whose device could not make one, had no way forward: the
unsupported branch rendered a message and nothing else, on a screen with no
exit. The session already exists by then, since the OTP step that leads here
establishes it, so leaving without a passkey was always a legitimate way to
finish rather than an escape hatch.

useLoginMethods reads the instance configuration, and the skip only appears when
a method other than passkey is enabled. With passkey as the only one a skip
would leave a user unable to sign back into the account they just created, so
the control is not rendered at all. Unknown counts as unsafe: a failed or
in-flight read shows no skip rather than guessing at one.

The unsupported-device branch now says which case it is and offers the same way
forward when one exists.

Login no longer starts from a hardcoded ['passkey', 'magic_link', 'phone_otp'],
which advertised phone OTP on instances that never enabled it. It uses the
methods the instance reports, falling back to the narrower set matching the auth
server's own defaults. The login response stays authoritative when it carries
methods of its own.

Requires an auth server serving GET /system-config/public and an adapter that
proxies it.

Verified with npm run typecheck, npm test (290 passing), npm run lint,
npm run format:check, and npm run build, against a local build of the types
package.
The release carrying PublicSystemConfigResponse, which the public system config
client method and useLoginMethods are typed against.

Verified against the published package with npm run typecheck, npm test
(290 passing), npm run lint, npm run format:check, and npm run build.
@Bccorb
Bccorb marked this pull request as ready for review July 31, 2026 03:08
@Bccorb
Bccorb merged commit c206f29 into main Jul 31, 2026
4 checks passed
@Bccorb
Bccorb deleted the feat/skip-passkey-registration branch July 31, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant