Local authentication
expo-local-authentication's API on LAContext and Android's BiometricManager: hardware, enrollment and a Face ID or Touch ID prompt.
- iOS:
await context.evaluatePolicy(policy, reason). The SDK's completion handler becomes a promise, under the name Swift gives its async form. - iOS:
canEvaluatePolicyreports why it can't with anNSErrorout-parameter, read throughOut<Error>. - Android: the platform's
BiometricManagerandKeyguardManager, with API-level checks.
import { PLATFORM } from "lucent:platform";import { LABiometryType, LAContext, LAError_Code, LAPolicy } from "lucent:ios/LocalAuthentication";import { Bundle } from "lucent:ios/Foundation";import { Out } from "lucent:ios";import { BiometricManager, BiometricManager_Authenticators as Authenticators,} from "lucent:android/android.hardware.biometrics";import { KeyguardManager } from "lucent:android/android.app";import { appContext, available } from "lucent:android";import { errorCode } from "lucent:core";
export interface LocalAuthenticationResult { success: boolean; error?: string; warning?: string;}
export interface LocalAuthenticationOptions { promptMessage?: string; cancelLabel?: string; fallbackLabel?: string; disableDeviceFallback?: boolean;}
// --- iOS ---------------------------------------------------------------------
/** The NSError code of a Lucent error made from an NSError ("<domain>:<code>"). */function codeOf(e: Error | null): number | null { const code = e ? errorCode(e) : null; if (!code) return null; return Number(code.slice(code.lastIndexOf(":") + 1));}
function canEvaluate(policy: LAPolicy): { ok: boolean; code: number | null } { const error = new Out<Error>(); const ok = new LAContext().canEvaluatePolicy(policy, error); return { ok, code: codeOf(error.value) };}
function biometryType(): LABiometryType { const context = new LAContext(); context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, null); return context.biometryType;}
function errorName(code: number): string { switch (code) { case LAError_Code.systemCancel: return "system_cancel"; case LAError_Code.appCancel: return "app_cancel"; case LAError_Code.touchIDLockout: return "lockout"; case LAError_Code.userFallback: return "user_fallback"; case LAError_Code.userCancel: return "user_cancel"; case LAError_Code.touchIDNotAvailable: return "not_available"; case LAError_Code.invalidContext: return "invalid_context"; case LAError_Code.touchIDNotEnrolled: return "not_enrolled"; case LAError_Code.passcodeNotSet: return "passcode_not_set"; case LAError_Code.authenticationFailed: return "authentication_failed"; default: return `unknown: ${code}`; }}
async function iosAuthenticate( options: LocalAuthenticationOptions,): Promise<LocalAuthenticationResult> { let warning: string | undefined = undefined; if ( biometryType() === LABiometryType.faceID && Bundle.main.object("NSFaceIDUsageDescription") === null ) { warning = "FaceID is available but has not been configured. To enable FaceID, provide `NSFaceIDUsageDescription`."; } const context = new LAContext(); if (options.fallbackLabel !== undefined) context.localizedFallbackTitle = options.fallbackLabel; if (options.cancelLabel !== undefined) context.localizedCancelTitle = options.cancelLabel; context.interactionNotAllowed = false; const disableDeviceFallback = options.disableDeviceFallback ?? false; if (disableDeviceFallback && warning !== undefined) { const missing: LocalAuthenticationResult = { success: false, error: "missing_usage_description", warning, }; return missing; } const policy = disableDeviceFallback ? LAPolicy.deviceOwnerAuthenticationWithBiometrics : LAPolicy.deviceOwnerAuthentication; const result: LocalAuthenticationResult = { success: false }; if (warning !== undefined) result.warning = warning; try { result.success = await context.evaluatePolicy(policy, options.promptMessage ?? ""); } catch (e) { const code = codeOf(e as Error); result.error = code === null ? "unknown" : errorName(code); } return result;}
// --- Android -----------------------------------------------------------------
const NO_HARDWARE = 12;
function canAuthenticate(authenticators: number): number { if (!available("android", 30)) return NO_HARDWARE; const manager = appContext().getSystemService(BiometricManager); return manager ? manager.canAuthenticate(authenticators) : NO_HARDWARE;}
function isDeviceSecure(): boolean { return appContext().getSystemService(KeyguardManager)?.isDeviceSecure() ?? false;}
function hasSystemFeature(feature: string): boolean { return appContext().getPackageManager()?.hasSystemFeature(feature) ?? false;}
function androidAuthenticate(): LocalAuthenticationResult { const result: LocalAuthenticationResult = { success: false, error: "not_enrolled", warning: "KeyguardManager#isDeviceSecure() returned false", }; if (isDeviceSecure()) { // The prompt needs the current FragmentActivity, which lucent:android does not expose yet. result.error = "not_available"; result.warning = "The biometric prompt is not implemented in this port yet"; } return result;}
// --- The module --------------------------------------------------------------
export async function hasHardwareAsync(): Promise<boolean> { if (PLATFORM === "ios") { const r = canEvaluate(LAPolicy.deviceOwnerAuthenticationWithBiometrics); return r.ok || r.code !== LAError_Code.touchIDNotAvailable; } else { return ( canAuthenticate(Authenticators.BIOMETRIC_WEAK) !== BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ); }}
export async function isEnrolledAsync(): Promise<boolean> { if (PLATFORM === "ios") { const r = canEvaluate(LAPolicy.deviceOwnerAuthenticationWithBiometrics); return (r.ok && r.code === null) || r.code === LAError_Code.touchIDLockout; } else { return canAuthenticate(Authenticators.BIOMETRIC_WEAK) === BiometricManager.BIOMETRIC_SUCCESS; }}
/** 1 fingerprint, 2 facial recognition, 3 iris (AuthenticationType). */export async function supportedAuthenticationTypesAsync(): Promise<number[]> { const types: number[] = []; if (PLATFORM === "ios") { const type = biometryType(); if (type === LABiometryType.touchID) types.push(1); if (type === LABiometryType.faceID) types.push(2); return types; } else { if ( canAuthenticate(Authenticators.BIOMETRIC_WEAK) === BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ) return types; if (hasSystemFeature("android.hardware.fingerprint")) types.push(1); if ( hasSystemFeature("android.hardware.biometrics.face") || hasSystemFeature("com.samsung.android.bio.face") ) types.push(2); if (hasSystemFeature("android.hardware.biometrics.iris")) types.push(3); return types.filter((t, i) => types.indexOf(t) === i).sort((a, b) => a - b); }}
/** 0 none, 1 secret, 2 weak biometric, 3 strong biometric (SecurityLevel). */export async function getEnrolledLevelAsync(): Promise<number> { let level = 0; if (PLATFORM === "ios") { const secret = canEvaluate(LAPolicy.deviceOwnerAuthentication); if (secret.ok && secret.code === null) level = 1; const biometric = canEvaluate(LAPolicy.deviceOwnerAuthenticationWithBiometrics); if (biometric.ok && biometric.code === null) level = 3; } else { if (isDeviceSecure()) level = 1; if (canAuthenticate(Authenticators.BIOMETRIC_WEAK) === BiometricManager.BIOMETRIC_SUCCESS) level = 2; if (canAuthenticate(Authenticators.BIOMETRIC_STRONG) === BiometricManager.BIOMETRIC_SUCCESS) level = 3; } return level;}
export async function authenticateAsync( options: LocalAuthenticationOptions = {},): Promise<LocalAuthenticationResult> { if (PLATFORM === "ios") { return iosAuthenticate(options); } else { return androidAuthenticate(); }}import * as LocalAuthentication from "./src/localAuthentication.lucent";
if (await LocalAuthentication.hasHardwareAsync()) { const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Unlock" }); console.log(result.success);}Source: localAuthentication.lucent.ts.