Passkeys cover the happy path beautifully. The failure modes — the user buys a new laptop, switches from Chrome to Firefox, drops their phone — need explicit handling. This page is the playbook.Documentation Index
Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
Use this file to discover all available pages before exploring further.
The four failure modes
| What happened | What the SDK throws | What the user sees | What you do |
|---|---|---|---|
| User cancelled the prompt | signShhhMessage rejects with NotAllowedError translated to Error("Passkey authentication was cancelled") | ”Authentication cancelled” | Show the prompt button again. No data loss. |
| New device, no passkey synced | hasShhhPasskey() returns false | Sign-up page | If the user already has a wallet, route them to recovery (see below). |
| PRF unavailable after a browser switch (legacy CHIPI v29) | getWalletEncryptKey throws "Passkey PRF unavailable…" | ”Use PIN to sign in” | Show PIN field. The PRF-encrypted wallet can be re-encrypted later via useUpdateWalletEncryption. |
| Lost device, no other passkey | nothing throws — user is locked out | ”Recover your wallet” UI | SHHH: use the guardian recovery flow. CHIPI v29: only recoverable via the PIN backup. |
SHHH wallets — true recovery
A SHHH wallet’s owner set lives on chain. Recovery is a real on-chain primitive, not a workaround. Two paths:- The user still has access (e.g. moved from old phone to new) —
propose_add_owneradds the new passkey as a second owner with a 48-hour timelock, thenexecute_add_owneractivates it. See the recovery story in/services/gasless/recovery. - The user lost all access — a pre-registered guardian (a second device, a recovery email’s WebAuthn credential, a “Guardian-as-a-Service” partner) calls
initiate_recovery. After a 7-day timelock the guardian can finalize a wallet rotation onto a fresh passkey.
<Recover /> which drives both flows. The recipe at a glance:
CHIPI v29 wallets — dual-key fallback
A CHIPI v29 wallet’s passkey only encrypts a STARK key; if the passkey is gone, the encryption key is gone with it. The mitigation is the dual-key architecture: store the same private key TWICE, once encrypted by the passkey, once encrypted by a user-chosen PIN. The wallet shape carries two ciphertexts:verifyWalletPasskeyDetailed() returns reason: "prf_unavailable" or "no_passkey"), prompt for the PIN and use the encryptedPrivateKeyBackup ciphertext instead. The SDK handles which ciphertext to pick when you pass the right encryptKey.
PIN-only mode is a fallback, not a default
A PIN-only wallet (no passkey) is supported, but it’s the weakest path — see the PIN warning we use across every onboarding doc. Default to passkey; show PIN only whenisWebAuthnSupported() returns false or the user explicitly opts in.
Browser switching (the common case)
Even fully-supported browsers don’t always share PRF state. The user registers on Chrome, then opens your app in Firefox a week later. The Firefox WebAuthn ceremony succeeds — the credential is discoverable — but the PRF output is unavailable in that browser. For a SHHH wallet, this is not a problem: the passkey itself is the signer, no encryption key derivation.signShhhMessage works on any browser that supports WebAuthn.
For a legacy CHIPI v29 wallet, this is the problem verifyWalletPasskeyDetailed() catches. Surface the PIN fallback UI, decrypt with the PIN, and offer to re-register the passkey on the new browser via the migration hook.
