Skip to content
Lucent
SEARCH LUCENT

Search guides, APIs, and examples.

GitHub

Very early and experimental. The language, the generated native code and every package API change without notice. Do not use Lucent in production.

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: canEvaluatePolicy reports why it can't with an NSError out-parameter, read through Out<Error>.
  • Android: the platform's BiometricManager and KeyguardManager, with API-level checks.
localAuthentication.lucent.ts
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();
}
}

Source: localAuthentication.lucent.ts.