One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Local Authentication

Gate an action behind Face ID, Touch ID, or Android biometrics

One.LocalAuthentication asks the user to prove they are the device owner before your app runs a protected action: unlocking an account, revealing a secret, or confirming a payment. It evaluates the system’s biometric policy only, a face or fingerprint match, with no passcode fallback button.

On iOS it drives LAContext from the LocalAuthentication framework. On Android it uses BiometricManager and BiometricPrompt from androidx.biometric. On web there is no biometric hardware to ask, so availability is always false and evaluation rejects.

Usage

import { One } from 'one'
const status = One.LocalAuthentication.canEvaluatePolicy()
if (status.available) {
const ok = await One.LocalAuthentication.evaluatePolicy('Unlock your saved account')
if (ok) {
// continue to the protected action
}
}

Availability is a synchronous check. Evaluation is async, takes the reason the system shows the user, and resolves true only on a biometric match.

How it works

Each call builds a fresh LAContext on iOS and evaluates deviceOwnerAuthenticationWithBiometrics. Availability reports the context’s biometry type: none, touch ID, face ID, or optic ID. When the policy cannot be evaluated, errorCode carries the Apple LAError numeric code.

Evaluation sets the context’s fallback title to an empty string, which hides the passcode button, then presents the system prompt with your reason. A match resolves true. The user cancelling, the system cancelling, or the fallback action resolves false rather than throwing. Repeated failures lock biometrics out, and an unenrolled device or a missing device passcode rejects, each with its own error code. On a Face ID device the app must declare the Face ID usage text (native.app.ios.faceIdUsageDescription, written to NSFaceIDUsageDescription at prebuild) or evaluation rejects before prompting.

On Android, availability calls BiometricManager.canAuthenticate with BIOMETRIC_STRONG, and errorCode carries that call’s result code. The reported kind is touch ID only when fingerprint hardware is present without face hardware, face ID for the reverse, and none when zero or both kinds are present, since Android cannot prove which kind is enrolled. Evaluation posts to the main thread, builds a BiometricPrompt whose title is your reason with a Cancel button, and authenticates against the current activity, which must be a FragmentActivity. A bad read is not terminal: the prompt stays up until success, cancel, or lockout. Biometric use needs no app manifest entry; USE_BIOMETRIC is a normal permission the library manifest already declares.

On web canEvaluatePolicy returns { available: false, biometryType: 'none' } and evaluatePolicy rejects, so authentication never succeeds without a native build. Every native failure rejects with an Error whose code property holds the stable E_LOCAL_AUTH_* value.

API reference

MethodsTypeDescription
canEvaluatePolicy() => LocalAuthenticationStatusWhether biometrics can be evaluated right now, and which kind.
() => LocalAuthenticationStatusWhether biometrics can be evaluated right now, and which kind.
evaluatePolicy(reason: string) => Promise<boolean>Prompt for a biometric match. Resolves false when cancelled.
(reason: string) => Promise<boolean>Prompt for a biometric match. Resolves false when cancelled.
StatusTypeDescription
availablebooleanTrue when the biometric policy can be evaluated.
booleanTrue when the biometric policy can be evaluated.
biometryTypeLocalBiometryTypeThe enrolled kind: none, touchID, faceID, or opticID.
LocalBiometryTypeThe enrolled kind: none, touchID, faceID, or opticID.
errorCodenumberThe platform result code, present when unavailable.
numberThe platform result code, present when unavailable.

Platform differences

iOSAndroidWeb
APISystem contextSystem promptStubbed out
KindExact enrolled kindOne kind, else noneAlways none
Error codeApple numberManager codeNone
PromptReason in system textReason as titleRejects
SetupUsage text requiredNone neededNone needed

Limits and gotchas

Examples

Unlock a protected screen

import { useState } from 'react'
import { Button, Text } from 'react-native'
import { One } from 'one'
function UnlockGate({ children }: { children: React.ReactNode }) {
const [unlocked, setUnlocked] = useState(false)
const [failed, setFailed] = useState(false)
const status = One.LocalAuthentication.canEvaluatePolicy()
if (unlocked) return <>{children}</>
if (!status.available) return <Text>Biometrics are not available on this device.</Text>
async function unlock() {
try {
setUnlocked(await One.LocalAuthentication.evaluatePolicy('Unlock your saved account'))
} catch {
setFailed(true)
}
}
return (
<>
<Button title="Unlock with biometrics" onPress={unlock} />
{failed ? <Text>Authentication failed. Try again.</Text> : null}
</>
)
}

Availability gates the button at render time, so the prompt only ever opens when it can succeed or cancel. The rejection path stays separate from a cancel, which just leaves the gate locked.

Confirm a payment with lockout handling

import { useState } from 'react'
import { Button, Text } from 'react-native'
import { One } from 'one'
function PayButton({ pay }: { pay: () => void }) {
const [message, setMessage] = useState('')
async function confirm() {
try {
const ok = await One.LocalAuthentication.evaluatePolicy('Confirm this payment')
if (ok) pay()
} catch (error) {
const code = (error as { code?: string }).code
if (code === 'E_LOCAL_AUTH_LOCKOUT') {
setMessage('Too many attempts. Biometrics are locked for now.')
} else if (code === 'E_LOCAL_AUTH_NOT_ENROLLED') {
setMessage('No biometrics are enrolled on this device.')
} else {
setMessage('Authentication failed. Try again.')
}
}
}
return (
<>
<Button title="Pay" onPress={confirm} />
{message ? <Text>{message}</Text> : null}
</>
)
}

The stable error codes let each terminal state show its own message. A cancel resolves false and falls through silently, which is the right response to a user who changed their mind mid-payment.

Edit this page on GitHub.