feat(passkey): let a user finish registration without a passkey - #122
Merged
Conversation
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
marked this pull request as ready for review
July 31, 2026 03:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
The problem
Registration ended on a screen with exactly one control: "Register Passkey". No skip, no back. And if
passkeySupportedwas 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.
EmailRegistrationalready finishes without a passkey when the device does not support one, using exactlyawait 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
passkeyis 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.
useLoginMethodsreturnsnullrather than[]when the read fails or is in flight, andhasNonPasskeyLoginMethod(null)isfalse. 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
Loginstarted from a hardcoded['passkey', 'magic_link', 'phone_otp'], which does not match the auth server'sSYSTEM_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 acceptnull.Tests
290 passing, up from 279. Eight new:
registerPasskeyuseLoginMethodsreturns the reported methods, and leaves themnullon both a failed read and an empty listhasNonPasskeyLoginMethodacross all four inputsThe existing unsupported-state test was updated: it asserted the exact dead-end copy this PR removes.
hasNonPasskeyLoginMethodis 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 buildall clean.