{"record":{"id":"4e901c4757b0bdcb","repo":"Wei-Shaw/sub2api","slug":"passkey-creation-was-cancelled","errorCode":null,"errorMessage":"Passkey creation was cancelled","messagePattern":"Passkey creation was cancelled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"frontend/src/api/passkey.ts","lineNumber":135,"sourceCode":"  }\n  const { data } = await apiClient.post<AuthResponse>('/auth/passkey/login/finish', {\n    session_token: begin.session_token,\n    credential: serializeAssertionCredential(credential)\n  })\n  return data\n}\n\nasync function register(name: string, password: string): Promise<PasskeyCredentialSummary> {\n  requirePasskeySupport()\n  const { data: begin } = await apiClient.post<CeremonyOptionsResponse>(\n    '/user/passkeys/register/begin',\n    { password }\n  )\n  const credential = await navigator.credentials.create({\n    publicKey: creationOptionsFromJSON(begin.options.publicKey)\n  })\n  if (!(credential instanceof PublicKeyCredential)) {\n    throw new Error('Passkey creation was cancelled')\n  }\n  const { data } = await apiClient.post<PasskeyCredentialSummary>(\n    '/user/passkeys/register/finish',\n    {\n      session_token: begin.session_token,\n      name,\n      credential: serializeRegistrationCredential(credential)\n    }\n  )\n  return data\n}\n\nasync function list(): Promise<PasskeyCredentialSummary[]> {\n  const { data } = await apiClient.get<PasskeyCredentialSummary[]>('/user/passkeys')\n  return data\n}\n\nasync function rename(id: number, name: string): Promise<void> {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/frontend/src/api/passkey.ts#L117-L153","documentation":"In frontend/src/api/passkey.ts:135, after navigator.credentials.create({publicKey}) for passkey registration, the code requires the resolved value to be an instanceof PublicKeyCredential. When the browser returns null (ceremony aborted in browsers that resolve rather than reject) or a non-PublicKeyCredential object, the code throws 'Passkey creation was cancelled'. Like sign-in, most explicit user cancellations reject with NotAllowedError from credentials.create() itself.","triggerScenarios":"Registering a passkey where credentials.create() resolves null: dismissed Windows Hello / Touch ID sheet in certain browser builds, WebView without a proper WebAuthn UI, or unit tests mocking create() with null/generic objects. Also triggered by a resident-credential request when the authenticator silently fails to create one and the browser resolves empty.","commonSituations":"E2E tests (Cypress historically cannot drive real WebAuthn); users on Android WebView-based browsers; users cancelling the save-prompt for a passkey; rp.name/user.name fields missing from creation options causing some browsers to abort.","solutions":["Treat this error and NotAllowedError identically in the UI: show 'registration cancelled, try again' rather than a generic failure.","Validate creationOptionsFromJSON(begin.options.publicKey) output includes user.id, user.name, rp.id, challenge and pubKeyCredParams — malformed options cause silent aborts in some browsers.","In E2E tests, use virtual authenticators (Playwright/Chrome --enable-features or CDP WebAuthn) instead of mocking create().","Refactor to `if (!credential || !(credential instanceof PublicKeyCredential))` for clearer null handling."],"exampleFix":"// before\nconst credential = await navigator.credentials.create({ publicKey: creationOptionsFromJSON(begin.options.publicKey) })\nif (!(credential instanceof PublicKeyCredential)) {\n  throw new Error('Passkey creation was cancelled')\n}\n\n// after\nconst credential = await navigator.credentials.create({ publicKey: creationOptionsFromJSON(begin.options.publicKey) })\nif (!credential || typeof credential.id === 'undefined') {\n  throw new Error('Passkey creation was cancelled')\n}\n// narrow via duck-typing instead of instanceof so mocks/edge browsers still pass\nif (credential && !(credential instanceof PublicKeyCredential) && !('rawId' in credential)) {\n  throw new Error('Passkey creation was cancelled')\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function hasRegistrationShape(c: Credential | null): c is PublicKeyCredential {\n  return !!c && 'rawId' in c && 'response' in c;\n}","tryCatchPattern":"try {\n  await registerPasskey(name, password);\n} catch (e) {\n  if (e.name === 'NotAllowedError' || e.message === 'Passkey creation was cancelled') {\n    showInfo('Registration cancelled — try again'); return;\n  }\n  throw e;\n}","preventionTips":["Validate creation options include user.id, user.name, rp.id, challenge and pubKeyCredParams before create()","Surface a retry action on cancellation rather than a dead-end error","Use Playwright/CDP virtual authenticators in automated tests"],"tags":["webauthn","passkeys","registration","user-cancellation","frontend"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}