capacitor-biometric-authentication
Version:
Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!
188 lines (166 loc) • 5.45 kB
text/typescript
import {
BiometricAuthAdapter,
BiometricAuthOptions,
BiometricAuthResult,
BiometricError,
BiometricErrorCode,
BiometryType
} from '../core/types';
export class ReactNativeAdapter implements BiometricAuthAdapter {
platform = 'react-native';
private biometrics: unknown;
constructor() {
// Biometrics module will be loaded dynamically
}
private async getBiometrics(): Promise<{
isSensorAvailable: () => Promise<{ available: boolean; biometryType?: string; error?: string }>;
createSignature: (options: { promptMessage: string; cancelButtonText: string; payload: string }) => Promise<{ success: boolean; signature?: string; error?: string }>;
deleteKeys: () => Promise<void>;
biometricKeysExist: () => Promise<{ keysExist: boolean }>;
}> {
if (this.biometrics) {
return this.biometrics as ReturnType<ReactNativeAdapter['getBiometrics']>;
}
try {
// Dynamic import for React Native biometrics
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
const ReactNativeBiometrics = (require('react-native-biometrics') as any).default;
this.biometrics = new ReactNativeBiometrics();
return this.biometrics as ReturnType<ReactNativeAdapter['getBiometrics']>;
} catch {
throw new Error(
'React Native Biometrics not installed. Please run: npm install react-native-biometrics'
);
}
}
async isAvailable(): Promise<boolean> {
try {
const biometrics = await this.getBiometrics();
const { available } = await biometrics.isSensorAvailable();
return available;
} catch {
return false;
}
}
async getSupportedBiometrics(): Promise<BiometryType[]> {
try {
const biometrics = await this.getBiometrics();
const { available, biometryType } = await biometrics.isSensorAvailable();
if (!available) {
return [];
}
// Map React Native biometry types to our types
switch (biometryType) {
case 'TouchID':
return [BiometryType.TOUCH_ID];
case 'FaceID':
return [BiometryType.FACE_ID];
case 'Biometrics':
case 'Fingerprint':
return [BiometryType.FINGERPRINT];
default:
return [BiometryType.UNKNOWN];
}
} catch {
return [];
}
}
async authenticate(options?: BiometricAuthOptions): Promise<BiometricAuthResult> {
try {
const biometrics = await this.getBiometrics();
// Check if biometrics are available
const { available, biometryType, error } = await biometrics.isSensorAvailable();
if (!available) {
return {
success: false,
error: {
code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE,
message: error || 'Biometric authentication is not available'
}
};
}
// Create signature for authentication
const { success, signature, error: authError } = await biometrics.createSignature({
promptMessage: options?.reason || 'Authenticate',
cancelButtonText: options?.cancelTitle || 'Cancel',
payload: 'biometric-auth-payload'
});
if (success && signature) {
return {
success: true,
biometryType: this.mapBiometryType(biometryType),
sessionId: this.generateSessionId(),
platform: 'react-native'
};
} else {
return {
success: false,
error: this.mapError(authError)
};
}
} catch (error) {
return {
success: false,
error: this.mapError(error)
};
}
}
async deleteCredentials(): Promise<void> {
try {
const biometrics = await this.getBiometrics();
await biometrics.deleteKeys();
} catch {
// Ignore errors when deleting credentials
}
}
async hasCredentials(): Promise<boolean> {
try {
const biometrics = await this.getBiometrics();
const { keysExist } = await biometrics.biometricKeysExist();
return keysExist;
} catch {
return false;
}
}
private mapBiometryType(type?: string): BiometryType {
if (!type) {
return BiometryType.UNKNOWN;
}
switch (type) {
case 'TouchID':
return BiometryType.TOUCH_ID;
case 'FaceID':
return BiometryType.FACE_ID;
case 'Biometrics':
case 'Fingerprint':
return BiometryType.FINGERPRINT;
default:
return BiometryType.UNKNOWN;
}
}
private mapError(error: unknown): BiometricError {
let code = BiometricErrorCode.UNKNOWN_ERROR;
let message = 'An unknown error occurred';
if (typeof error === 'string') {
message = error;
// Map common error messages to error codes
if (error.includes('cancelled') || error.includes('canceled')) {
code = BiometricErrorCode.USER_CANCELLED;
} else if (error.includes('failed') || error.includes('not recognized')) {
code = BiometricErrorCode.AUTHENTICATION_FAILED;
} else if (error.includes('locked')) {
code = BiometricErrorCode.LOCKOUT;
}
} else if (error instanceof Error) {
message = error.message;
}
return {
code,
message,
details: error
};
}
private generateSessionId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}