Skip to content

Feat/biometry status#592

Merged
mCodex merged 6 commits into
masterfrom
feat/biometryStatus
Apr 28, 2026
Merged

Feat/biometry status#592
mCodex merged 6 commits into
masterfrom
feat/biometryStatus

Conversation

@mCodex
Copy link
Copy Markdown
Owner

@mCodex mCodex commented Apr 28, 2026

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:

  • Added a new biometryStatus field to SecurityAvailability (with values like 'available', 'notEnrolled', etc.) to distinguish between hardware absence, unenrolled hardware, and usable biometry. The legacy biometry boolean remains for backward compatibility. [1] [2] [3] [4] [5] [6]
  • Introduced canUseAccessControl and canUseAccessControlSync functions to predict if a specific access control policy will succeed, allowing better UX decisions before prompting users. [1] [2]
  • Added useSecurityAvailability({ refreshOnForeground: true }) to auto-refresh the biometry status when the app returns to the foreground, and useBiometryStatusWatcher to listen for actual enrollment state changes. [1] [2] [3]

Bug fixes for double biometric prompts:

  • Fixed double-prompt regressions on both iOS and Android: biometric-protected entries are no longer silently re-encrypted during getItem, which previously triggered a second prompt. Now, such entries are only upgraded on explicit setItem or rotateKeys({ reEncryptEagerly: true }). [1] [2]

Documentation and example updates:

  • Expanded documentation in README.md and HOOKS.md to 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]
  • Updated the example app to showcase the new biometry status card and improved DX for access control. [1] [2]

Build tooling and DX:

  • Improved Babel configuration to ensure babel-plugin-react-compiler runs in all build scenarios, both for the library and the example app. Updated package.json dependencies accordingly. [1] [2] [3]

Summary of most important changes:

Biometric capability and policy detection:

  • Added biometryStatus to SecurityAvailability and mapped native error codes for fine-grained biometry state detection. [1] [2] [3] [4] [5] [6]
  • Introduced canUseAccessControl/canUseAccessControlSync for pre-checking access control policy support. [1] [2]

Hooks and auto-refresh:

  • Added useSecurityAvailability({ refreshOnForeground }) and useBiometryStatusWatcher for real-time and transition-only updates. [1] [2] [3]

Double-prompt bug fixes:

  • Prevented double biometric prompts on both iOS and Android by skipping lazy re-encryption for biometric-protected entries. [1] [2]

Documentation and example enhancements:

  • Expanded docs and example app to demonstrate new biometry features and recommended UX patterns. [1] [2] [3] [4] [5] [6]

Build tooling:

  • Improved Babel config to reliably run the React Compiler across all build paths and updated dependencies. [1] [2] [3]

mCodex and others added 3 commits April 28, 2026 14:25
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).
@mCodex mCodex self-assigned this Apr 28, 2026
Copilot AI review requested due to automatic review settings April 28, 2026 20:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.biometryStatus and 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.

Comment on lines +420 to +422
* 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.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
* 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.

Copilot uses AI. Check for mistakes.
Comment thread src/sensitive-info.nitro.ts Outdated
Comment on lines 296 to 299
@@ -255,8 +297,19 @@ export interface SecurityAvailability {
readonly secureEnclave: boolean
/** Android StrongBox is present. **Android only** — always `false` on iOS. */
readonly strongBox: boolean
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +72
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')
})
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()
}

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +455 to +456
`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:

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md Outdated
### 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.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* **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.

Copilot uses AI. Check for mistakes.
mCodex added 3 commits April 28, 2026 17:14
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.
@mCodex mCodex merged commit 4a1d00f into master Apr 28, 2026
9 checks passed
@mCodex mCodex deleted the feat/biometryStatus branch April 28, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants