Skip to content

v6.1.0

Choose a tag to compare

@mCodex mCodex released this 28 Apr 20:19
· 21 commits to master since this release

🔐 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 classify

Drive 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 iosSynchronizable between 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-compiler now actually runs on the published bundle. The react-native-builder-bob targets opt into configFile: true so they pick up the library's babel.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.secureEnclave semantics across platforms (Secure Enclave on iOS / mirrors strongBox on Android) — gate "hardware-backed key" UX without branching on Platform.OS.
  • README + CHANGELOG correctly state that canUseAccessControl(policy, levels?) only skips the native call when a snapshot is supplied.
  • Android requiresBiometricAuth doc 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.0

Then re-run pod install from ios/ and rebuild.

Full diff: v6.0.0...v6.1.0