Feat/biometry status#592
Conversation
Introduce fine-grained biometry detection and related UX helpers. Adds a new biometryStatus enum to SecurityAvailability (available | notEnrolled | notAvailable | lockedOut | unknown) while keeping the legacy biometry boolean as an alias. Wire biometryStatus through native probes on Android and iOS, add classify logic on both platforms, and surface it to JS. Provide policy precheck helpers: canUseAccessControl and canUseAccessControlSync (pure TS mapping over SecurityAvailability) so callers can predict whether a given AccessControl will succeed without a native round-trip. Add refreshOnForeground option to useSecurityAvailability to auto-refetch on app foreground (debounced), and introduce useBiometryStatusWatcher — a transition-only hook that fires only on real biometry status changes. Also: update docs and README (biometrics section), example app (BiometryStatusCard + App), diagnostics UI, tests (unit and hook tests, mocks for AppState), and exports. Changes are non-breaking for consumers that continue to use the biometry boolean.
…ompatibility with React Compiler Co-authored-by: Copilot <copilot@github.com>
Avoids a second biometric prompt by skipping lazy re-encryption for entries that require biometric/user authentication (Android and iOS). Adds helpers (requiresBiometricAuth / isBiometricallyProtected) to detect such entries so upgrades only occur via explicit setItem or eager rotateKeys. Replaces direct SecItemAdd/delete flows with upsertKeychainEntry + forceDeleteExisting on iOS to wipe any synchronizable sibling (uses kSecAttrSynchronizableAny) and absorb iCloud restore races with a single bounded retry, preventing errSecDuplicateItem when iosSynchronizable toggles or iCloud restores entries. Also updates CHANGELOG with fixes and refreshes example iOS Podfile.lock (SensitiveInfo -> 6.0.0 and related React binaries).
There was a problem hiding this comment.
Pull request overview
This PR enhances biometric capability detection and UX control by introducing a fine-grained biometryStatus, adding access-control precheck helpers, and extending hooks/docs/example app to support foreground refresh and enrollment-change watching.
Changes:
- Add
BiometryStatus+SecurityAvailability.biometryStatusand wire it through iOS/Android resolvers and JS typings. - Introduce
canUseAccessControl/canUseAccessControlSync, plus hook improvements (refreshOnForeground,useBiometryStatusWatcher) with tests. - Update documentation, example UI, and Babel/tooling to support React Compiler usage and showcase new features.
Reviewed changes
Copilot reviewed 29 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds babel-plugin-react-compiler dependency resolution. |
| package.json | Ensures bob targets load Babel config via configFile: true. |
| babel.config.js | Makes React Compiler plugin run consistently across build callers. |
| src/sensitive-info.nitro.ts | Adds BiometryStatus and SecurityAvailability.biometryStatus typing/docs. |
| src/index.ts | Exports new access-control helpers and BiometryStatus type. |
| src/core/access-control.ts | New helpers for predicting whether an access-control policy can be satisfied. |
| src/hooks/useSecurityAvailability.ts | Adds refreshOnForeground option and AppState-based refetching. |
| src/hooks/useBiometryStatusWatcher.ts | New watcher hook that emits only on biometryStatus transitions. |
| src/hooks/index.ts | Re-exports the new watcher hook/types and new options type. |
| src/hooks/useStableOptions.ts | Adds React Compiler opt-out directive for manual memoization behavior. |
| src/hooks/useSecureStorage.ts | Adds React Compiler opt-out directive for ref-coordination during render. |
| src/hooks/useMutation.ts | Adds React Compiler opt-out directive for compiler-unsupported patterns. |
| src/hooks/useAsync.ts | Adds React Compiler opt-out directive for compiler-unsupported patterns. |
| src/tests/mocks/react-native.ts | Enhances RN mock with AppState event subscription + helpers. |
| src/tests/hooks.useSecurityAvailability.test.tsx | Tests new biometryStatus field and refreshOnForeground behavior. |
| src/tests/hooks.useBiometryStatusWatcher.test.tsx | New tests covering transition-only watcher semantics. |
| src/tests/core.access-control.test.ts | New tests for policy/status mapping and async wrapper behavior. |
| src/tests/core.storage.test.ts | Updates expected SecurityAvailability snapshots to include biometryStatus. |
| ios/Internal/Security/SecurityAvailabilityResolver.swift | Adds native biometryStatus classification via LAError codes. |
| ios/HybridSensitiveInfo.swift | Wires biometryStatus into availability; avoids double prompts by skipping lazy refresh for biometric policies; improves Keychain upsert robustness. |
| android/src/main/java/com/sensitiveinfo/internal/crypto/SecurityAvailabilityResolver.kt | Adds Android biometryStatus classification via BiometricManager results. |
| android/src/main/java/com/sensitiveinfo/HybridSensitiveInfo.kt | Wires biometryStatus; skips lazy re-encryption for auth-gated entries to prevent double prompts. |
| docs/HOOKS.md | Documents biometryStatus, refreshOnForeground, watcher hook, and policy gating. |
| README.md | Adds biometrics section and usage guidance for new API/hook behavior. |
| CHANGELOG.md | Documents new features/fixes for upcoming release. |
| example/package.json | Adds React Compiler plugin dependency for the example app. |
| example/babel.config.js | Runs React Compiler before other transforms in the example. |
| example/src/components/DiagnosticsCard.tsx | Displays biometryStatus in diagnostics. |
| example/src/components/BiometryStatusCard.tsx | New UI card showcasing status + policy precheck + refresh behavior. |
| example/src/App.tsx | Adds the new example card to the app screen. |
| example/ios/Podfile.lock | Updates RN/React pods and example’s SensitiveInfo pod version lock. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * to authorize a `Cipher.init` (i.e. the access policy maps to a biometric | ||
| * or device-credential gate). `devicePasscode`/`none` writes can be | ||
| * re-encrypted silently with no prompt. |
There was a problem hiding this comment.
This doc comment says devicePasscode writes can be “re-encrypted silently”, but AccessControlResolver marks DEVICEPASSCODE as requiresAuthentication = true, meaning Cipher.init can prompt for device credentials. Consider updating the comment to reflect that devicePasscode entries are also auth-gated (even if the code already treats them via entry.requiresAuthentication).
| * to authorize a `Cipher.init` (i.e. the access policy maps to a biometric | |
| * or device-credential gate). `devicePasscode`/`none` writes can be | |
| * re-encrypted silently with no prompt. | |
| * to authorize a `Cipher.init` (for example, biometric- or | |
| * device-credential-gated entries). Entries marked with | |
| * `entry.requiresAuthentication`, including `devicePasscode`, may prompt | |
| * during re-encryption; only entries with no authentication requirement can | |
| * be re-encrypted silently. |
| @@ -255,8 +297,19 @@ export interface SecurityAvailability { | |||
| readonly secureEnclave: boolean | |||
| /** Android StrongBox is present. **Android only** — always `false` on iOS. */ | |||
| readonly strongBox: boolean | |||
There was a problem hiding this comment.
SecurityAvailability.secureEnclave is documented as “always false on Android”, but Android code populates it as hasStrongBox (same as strongBox), and tests expect secureEnclave: true on Android snapshots. Either adjust the native mapping (set secureEnclave=false on Android) or update this comment to match the cross-platform semantics you intend.
| await act(async () => { | ||
| // Wait past the 500 ms debounce window before emitting again. | ||
| await new Promise((r) => setTimeout(r, 600)) | ||
| appState.__emit('background') | ||
| appState.__emit('active') | ||
| }) |
There was a problem hiding this comment.
This test introduces a real-time 600ms sleep to get past the foreground debounce window, which slows the suite and can be flaky under heavy load. Consider using Jest fake timers (advance by 600ms) or exposing the debounce duration for tests so you don’t need wall-clock delays.
| await act(async () => { | |
| // Wait past the 500 ms debounce window before emitting again. | |
| await new Promise((r) => setTimeout(r, 600)) | |
| appState.__emit('background') | |
| appState.__emit('active') | |
| }) | |
| jest.useFakeTimers() | |
| try { | |
| await act(async () => { | |
| // Advance past the 500 ms debounce window before emitting again. | |
| jest.advanceTimersByTime(600) | |
| appState.__emit('background') | |
| appState.__emit('active') | |
| }) | |
| } finally { | |
| jest.useRealTimers() | |
| } |
| `canUseAccessControl(policy)` predicts whether a future `setItem` write with the requested policy will succeed on the current device — it maps the policy onto the {@link SecurityAvailability} snapshot, no extra native round-trip: | ||
|
|
There was a problem hiding this comment.
The README claims canUseAccessControl(policy) requires “no extra native round-trip” and uses {@link SecurityAvailability} (JSDoc syntax) in Markdown. In reality, canUseAccessControl calls getSupportedSecurityLevels() unless a snapshot is passed, so this should be reworded (and SecurityAvailability should be in backticks or a real Markdown link).
| ### Added | ||
|
|
||
| * **biometric availability:** New `biometryStatus` field on `SecurityAvailability` (`'available' | 'notEnrolled' | 'notAvailable' | 'lockedOut' | 'unknown'`) disambiguates *hardware missing*, *hardware present but no enrollment*, and *currently usable*. The legacy `biometry` boolean stays as a backward-compatible alias for `biometryStatus === 'available'`. Mapped natively from `LAError` codes on iOS and `BiometricManager.canAuthenticate` results on Android. | ||
| * **policy precheck:** New `canUseAccessControl(policy, levels?)` and `canUseAccessControlSync(policy, levels)` predict whether a given `AccessControl` policy will succeed on the current device. Pure TS mapping over `SecurityAvailability` — no extra IPC round-trip. |
There was a problem hiding this comment.
Changelog says canUseAccessControl(...) is “no extra IPC round-trip”, but the helper performs an IPC/native call via getSupportedSecurityLevels() when levels is not provided. Please clarify that it’s a pure mapping when a snapshot is supplied, otherwise it will fetch the snapshot first.
| * **policy precheck:** New `canUseAccessControl(policy, levels?)` and `canUseAccessControlSync(policy, levels)` predict whether a given `AccessControl` policy will succeed on the current device. Pure TS mapping over `SecurityAvailability` — no extra IPC round-trip. | |
| * **policy precheck:** New `canUseAccessControl(policy, levels?)` and `canUseAccessControlSync(policy, levels)` predict whether a given `AccessControl` policy will succeed on the current device. When a `SecurityAvailability` snapshot is supplied, they are a pure TS mapping over that snapshot; if `levels` is omitted, `canUseAccessControl(...)` first fetches the current snapshot via the existing native/IPC path. |
Clarify canUseAccessControl semantics in CHANGELOG and README (sync variant requires a snapshot; async will fetch one if none supplied) and improve wording around biometry/secure-enclave semantics. Update Android Kotlin docs in HybridSensitiveInfo to explain requiresBiometricAuth behavior, lazy refresh skipping, and legacy-entry handling. Make useBiometryStatusWatcher test deterministic by advancing Date.now via a jest spy instead of sleeping. Tweak SecurityAvailability.secureEnclave doc to describe cross-platform meaning and relation to StrongBox.
Publish the v6.1.0 changelog entry (2026-04-28) and add documentation clarifications: explain SecurityAvailability.secureEnclave cross-platform semantics (Secure Enclave on iOS / mirrors strongBox on Android), clarify canUseAccessControl(snapshot vs fetch behavior), and update the Android requiresBiometricAuth doc comment to match actual classification and lazy-refresh behavior.
This pull request introduces several new features and important fixes related to biometric authentication, developer experience, and build tooling. The main focus is on improving how biometric capability and enrollment status are detected and handled, preventing double biometric prompts, and enhancing the documentation and example app to showcase these features.
Biometric authentication improvements:
biometryStatusfield toSecurityAvailability(with values like'available','notEnrolled', etc.) to distinguish between hardware absence, unenrolled hardware, and usable biometry. The legacybiometryboolean remains for backward compatibility. [1] [2] [3] [4] [5] [6]canUseAccessControlandcanUseAccessControlSyncfunctions to predict if a specific access control policy will succeed, allowing better UX decisions before prompting users. [1] [2]useSecurityAvailability({ refreshOnForeground: true })to auto-refresh the biometry status when the app returns to the foreground, anduseBiometryStatusWatcherto listen for actual enrollment state changes. [1] [2] [3]Bug fixes for double biometric prompts:
getItem, which previously triggered a second prompt. Now, such entries are only upgraded on explicitsetItemorrotateKeys({ reEncryptEagerly: true }). [1] [2]Documentation and example updates:
README.mdandHOOKS.mdto cover the new biometry status, hooks, and usage patterns, including gating toggles and handling enrollment transitions. Added a dedicated section for biometrics. [1] [2] [3] [4]Build tooling and DX:
babel-plugin-react-compilerruns in all build scenarios, both for the library and the example app. Updatedpackage.jsondependencies accordingly. [1] [2] [3]Summary of most important changes:
Biometric capability and policy detection:
biometryStatustoSecurityAvailabilityand mapped native error codes for fine-grained biometry state detection. [1] [2] [3] [4] [5] [6]canUseAccessControl/canUseAccessControlSyncfor pre-checking access control policy support. [1] [2]Hooks and auto-refresh:
useSecurityAvailability({ refreshOnForeground })anduseBiometryStatusWatcherfor real-time and transition-only updates. [1] [2] [3]Double-prompt bug fixes:
Documentation and example enhancements:
Build tooling: