v6.1.0
🔐 react-native-sensitive-info v6.1.0
A focused follow-up to the v6 GA: smarter biometric capability detection, two production bug fixes that eliminate double biometric prompts, and a few DX/build polish items. Fully backward-compatible — apps reading only the biometry boolean keep working unchanged.
✨ What's new
👁️ Fine-grained biometric availability
A single biometry: boolean couldn't tell the difference between "no hardware", "hardware present but the user hasn't enrolled", and "ready to use" — three UX-distinct states. The new biometryStatus field on SecurityAvailability disambiguates them:
type BiometryStatus =
| 'available' // ✅ enrolled and usable right now
| 'notEnrolled' // 🟡 hardware OK, no fingerprint/face registered yet
| 'notAvailable' // 🚫 no hardware / admin-disabled / passcode unset
| 'lockedOut' // ⏳ too many failed attempts
| 'unknown' // ❓ probe could not classifyDrive a "Set up Face ID" CTA off 'notEnrolled' instead of hiding the toggle. Mapped natively from LAError codes on iOS and BiometricManager.canAuthenticate results on Android.
Invariant:
biometry === (biometryStatus === 'available').
🛡️ Policy precheck — canUseAccessControl
Predict whether a future setItem write with a given AccessControl policy will succeed before you try and trigger a prompt:
import { canUseAccessControl, setItem } from 'react-native-sensitive-info'
if (await canUseAccessControl('secureEnclaveBiometry')) {
await setItem('session', token, { accessControl: 'secureEnclaveBiometry' })
} else {
await setItem('session', token, { accessControl: 'devicePasscode' })
}Pass a snapshot you already hold to skip the native call entirely:
import { canUseAccessControlSync } from 'react-native-sensitive-info'
import { useSecurityAvailability } from 'react-native-sensitive-info/hooks'
const { data: caps } = useSecurityAvailability()
const canEnable = caps ? canUseAccessControlSync('secureEnclaveBiometry', caps) : false🔁 Foreground auto-refresh
Users commonly leave the app to enroll a fingerprint and come back — your toggle should catch up automatically:
const { data: caps } = useSecurityAvailability({ refreshOnForeground: true })Subscribes to AppState only when enabled, debounces back-to-back active transitions (~500 ms), and unsubscribes on unmount.
🔔 Enrollment listener — useBiometryStatusWatcher
Transition-only callback (fires once per real BiometryStatus change, never on every render):
import { useBiometryStatusWatcher } from 'react-native-sensitive-info/hooks'
useBiometryStatusWatcher((next, previous) => {
if (previous === 'notEnrolled' && next === 'available') {
showToast('Face ID is ready.')
}
})Lives in its own module so apps that don't watch enrollment changes don't pay for it (sideEffects: false keeps tree-shaking honest).
🐛 Bug fixes
📱 iOS — no more double Face ID / Touch ID prompt on getItem
The lazy re-encryption path that runs after a successful authenticated read used to call SecItemUpdate against the same Keychain item to refresh its key-version metadata; iOS treats that as a separate authorization gate, prompting the user a second time. Biometric items now skip the lazy refresh entirely and are upgraded only by an explicit setItem or rotateKeys({ reEncryptEagerly: true }). Non-biometric items continue to be upgraded silently.
🤖 Android — same double-prompt regression, fixed
Lazy re-encryption inside getItem allocated a new key alias for the active version and Cipher.init on that fresh setUserAuthenticationRequired(true) key required its own biometric authorization, surfacing as a second prompt right after the read. The lazy refresh now skips entries with requiresAuthentication == true (and any biometry-class access policy).
🪪 iOS — errSecDuplicateItem on setItem, fixed
setItem no longer fails with "The specified item already exists in the keychain" when:
- the caller toggles
iosSynchronizablebetween writes, or - iCloud Keychain restores an entry between our delete and add.
The internal upsert helper now wipes prior entries with kSecAttrSynchronizableAny and absorbs the iCloud-restore race with a single bounded retry. Bundle ID + access group already scope the partition, so the overwrite never crosses an app or sharing boundary.
🧰 Tooling & DX
- ⚛️
babel-plugin-react-compilernow actually runs on the published bundle. Thereact-native-builder-bobtargets opt intoconfigFile: trueso they pick up the library'sbabel.config.js, which branches on the bob caller and pairs the compiler with the right preset. - 🚦 Four hooks that coordinate refs across renders (
useStableOptions,useAsync,useMutation,useSecureStorage) carry an explicit'use no memo'opt-out — the compiler can't preserve their structural-stability guarantees and now skips them deliberately.
📚 Docs
- Clarify
SecurityAvailability.secureEnclavesemantics across platforms (Secure Enclave on iOS / mirrorsstrongBoxon Android) — gate "hardware-backed key" UX without branching onPlatform.OS. - README + CHANGELOG correctly state that
canUseAccessControl(policy, levels?)only skips the native call when a snapshot is supplied. - Android
requiresBiometricAuthdoc comment now matches the actual classification.
🧨 Breaking changes
None. 🎉
🙌 Upgrade
npm install react-native-sensitive-info@6.1.0
# or
yarn add react-native-sensitive-info@6.1.0Then re-run pod install from ios/ and rebuild.
Full diff: v6.0.0...v6.1.0