Skip to content

fix(android): return stable error codes instead of localized messages#110

Open
alvistar wants to merge 2 commits into
f-23:stablefrom
alvistar:fix/android-stable-error-codes
Open

fix(android): return stable error codes instead of localized messages#110
alvistar wants to merge 2 commits into
f-23:stablefrom
alvistar:fix/android-stable-error-codes

Conversation

@alvistar

Copy link
Copy Markdown

Problem

Both Android exception handlers fall through to the localized message:

// handleRegistrationException / handleAuthenticationException
else -> { return e.errorMessage.toString() }

errorMessage is human-readable display text, so anything not explicitly mapped
reaches JS as an unmatchable string. CreateCredentialException already exposes a
stable type (verified against androidx.credentials:1.5.0).

The case that surfaced this: CreateCredentialNoCreateOptionException, which
Credential Manager throws when no provider will create a passkey — most commonly
Google Password Manager with no Google account signed in, or a device with no
provider installed. That's a reachable real-world state, and apps currently can't
detect it to offer a password fallback; they only get
"No create options available.".

Change

  • Map CreateCredentialNoCreateOptionException"NoCreateOption", matching the
    existing naming style (NoCredentials, UserCancelled, NotSupported).
  • Replace both else branches with e.type (stable identifier) instead of
    e.errorMessage (display text).
  • Add NoCreateOptionError + the handleNativeError case, so the code survives the
    JS layer rather than being swallowed by defaultNativeError.
  • Document it in the README error-code table.

Notes

This follows the same shape as previously accepted mappings (#97 CredentialAlreadyExists,
#86 iOS 1001 → UserCancelled). Native-only + a JS switch case; no API breakage — the
else branch previously returned an unspecified string, so nothing could have depended
on its value.

Verified on an Android emulator (API 36, google_apis_playstore): without a Google
account the create call now rejects with NoCreateOption; after signing in, passkey
creation completes normally.

🤖 Generated with Claude Code

alvistar and others added 2 commits July 19, 2026 08:50
`handleRegistrationException` / `handleAuthenticationException` fall through to
`e.errorMessage.toString()`, which is human-readable display text and cannot be
branched on. Return the androidx `type` constant instead, and map
`CreateCredentialNoCreateOptionException` to a dedicated `NoCreateOption` code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fails without the mapping (the rejection surfaces as `{error: 'Native error'}`),
passes with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alvistar

Copy link
Copy Markdown
Author

Added a deterministic regression test in src/__tests__/PasskeyAndroid.spec.ts.

It reproduces the bug at the JS seam — a native rejection with { code: 'NoCreateOption' }
must surface as a matchable error:

# with the mapping reverted (src/PasskeyError.ts at upstream stable):
✕ should surface NoCreateOption when no provider can create a passkey
    + Received  + 1
    +   "error": "Native error",
Tests: 1 failed, 5 passed, 6 total

# with the fix:
Tests: 6 passed, 6 total

So the test is red on stable and green with the change, rather than only asserting
the new behaviour.

The native half (PasskeyModule.kt) has no JVM test target in this repo, so it was
verified on-device instead — Android emulator, API 36 google_apis_playstore:
with no Google account the create call rejects with NoCreateOption; after signing in,
passkey creation completes and the credential is persisted by the relying party.

@alvistar

Copy link
Copy Markdown
Author

On-device confirmation of the native half, now that the patched module is compiled in
(Android emulator, API 36 google_apis_playstore, androidx.credentials:1.5.0).

Repro: remove the Google account from the device, then call Passkey.create(...).
Credential Manager finds no provider willing to create.

Before (upstream stable): the exception falls through to e.errorMessage, so the app
receives { error: 'Native error' } — the generic catch-all — and cannot distinguish this
from any other failure.

After (this PR):

W System.err: androidx.credentials.exceptions.CreateCredentialNoCreateOptionException: No create options available.
W ReactNativeJS: '[auth] failure:', { error: 'NoCreateOption', ... }

The stable code reaches JS, so the app can branch on it. In our case that turns a dead-end
generic error into "this device can't create a passkey — use a password instead", with the
password option already on screen. Signing the Google account back in, passkey creation
completes normally and the credential is persisted by the relying party, so the mapping
doesn't affect the success path.

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