Skip to content

feat(expo): move biometric credentials to @clerk/expo-biometrics - #9960

Draft
mikepitre wants to merge 3 commits into
mike/expo-biometrics-androidfrom
mike/expo-biometrics-js-hooks
Draft

mikepitre wants to merge 3 commits into
mike/expo-biometrics-androidfrom
mike/expo-biometrics-js-hooks

Conversation

@mikepitre

@mikepitre mikepitre commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

useBiometricCredentials() (and the deprecated useTrustedDevices() wrapper) now run enrollment, listing, revocation and sign-in in JS. clerk-js talks to FAPI through the experimental trusted device resources from #9953, and @clerk/expo-biometrics (#9959) handles only keys, signing and on-device records. Before this change, these calls went through the ClerkExpo native module in @clerk/expo-native-components.

  • @clerk/expo-biometrics is an optional peer dependency. It is loaded with a guarded require. When it is missing, or the development build doesn't include its native module, the hook's methods throw an error that explains how to install it and rebuild. Web keeps the existing unsupported stub.
  • The orchestration follows clerk-ios BiometricCredentials.swift:
    • Environment gating uses nativeSettings.
    • Local candidates are filtered by app identifier, then id, then identifier hint, newest first.
    • Records whose key is missing are pruned, and records are filtered by which policies the device can evaluate.
    • When a session is active, records are reconciled with the server list.
    • Enrollment runs createKey → prepare → sign → attempt → saveRecord({ removeOtherRecordsForApp: true }). If prepare, sign or attempt fails, the key is deleted. If saving the record fails, the server credential is revoked and the key is deleted.
    • Sign-in runs signIn.create → sign trustedDeviceChallenge.clientData → attemptFirstFactor. The local record is forgotten on form_resource_not_found / trusted_device_not_registered for trusted_device_id, and on key_invalidated / key_not_found.
  • Identifier hints are matched on identifierHintSha256 with hashIdentifierHint(). A small compatibility shim falls back to the normalized raw identifierHint until the Android PR adds those members.
  • The error contract is unchanged. FAPI errors, including reverification-required errors from prepare/revoke, are thrown as biometric credential errors whose code is the API error code, as the native bridge did, with the original ClerkAPIResponseError as cause. Device errors are mapped to the existing BiometricCredentialErrorCode values.
  • reverify() stays on the @clerk/expo-native-components path unchanged. FAPI only lists trusted_device reverification factors from API version 2026-08-20. Its error now names @clerk/expo-native-components when that package is missing.
  • The now-unused enroll/list/revoke/sign-in/availability entries are removed from the ClerkExpo module spec types. The Swift/Kotlin implementations will be removed in a follow-up.

Stack: #9955 ← #9957 ← #9954 ← #9956 ← #9958 ← #9953 ← #9959 ← this PR. This PR will be rebased onto the @clerk/expo-biometrics Android PR once it's open.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 997b6d9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Sep 28, 2026 8:26pm UTC
swingset Ready Ready Preview Sep 28, 2026 8:26pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@mikepitre

mikepitre commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Superseded by #9960 (comment) (re-recorded from a plain JS-only app).

Recorded on a physical iPhone Air (iOS 27) with the full stack (#9955 → #9962) in an Expo SDK 57 app: useBiometricCredentials running on clerk-js plus @clerk/expo-biometrics (Secure Enclave key, Face ID). The simulator can't run this flow because it has no Secure Enclave.

  1. Email-code sign-in → getAvailability() reports no_local_credential
  2. enroll() → Face ID → credential active on the server
  3. Sign out → getAvailability() reports available
  4. signIn() → Face ID → JS signed in, and the native UserButton picks up the session

A system notification near the top of the screen is blurred between about 0:04 and 0:26.

biometrics-device-redacted.mp4

mikepitre and others added 2 commits September 28, 2026 16:21
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
getAvailability() and signIn() report biometric_authentication_unavailable
before reading local records when @clerk/expo-biometrics reports no secure
key storage, and enroll() maps its secure_key_storage_unavailable rejection
to biometric_authentication_unavailable before contacting Clerk.

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

Copy link
Copy Markdown
Contributor Author

Physical iPhone recording (plain create-expo-app JS-only app, no native components): sign in with the test email code → Enroll Face ID (useBiometrics().enroll, status shows Enrolled (active)) → sign out → Sign in with Face ID (useBiometrics().signIn + setActive) → signed back in. Recorded with agent-device at 15fps (its iPhone limit); idle waits trimmed and a personal notification blurred.

bio-phone2-final.mp4

This branch was successfully deployed

2 active deployments
Preview – swingset — 997b6d9e Deployed Sep 28, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 997b6d9e Deployed Sep 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant