import "package:flutter/services.dart"; import "package:flutter_secure_storage/flutter_secure_storage.dart"; import "package:local_auth/local_auth.dart"; import "package:local_auth_android/local_auth_android.dart"; /// Biometric ("Face recognition" / "Fingerprint") sign-in. /// /// The app already persists a JWT in SharedPreferences, so a valid session /// auto-restores on boot and the login screen is only shown after logout or /// token expiry. Biometric login covers that case: the user's credentials are /// kept in Android Keystore-backed secure storage and released only after the /// system BiometricPrompt succeeds, then replayed against the normal login API /// (so each biometric sign-in mints a fresh session/token). class BiometricAuth { final LocalAuthentication _auth = LocalAuthentication(); final FlutterSecureStorage _store = const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ); static const _emailKey = "cc_bio_email"; static const _passwordKey = "cc_bio_password"; /// Synchronous mirror of [isEnabled], kept fresh by the async calls below so /// the app-lifecycle handler (which can't await) can decide whether to lock. bool enabledCached = false; /// Whether the device has usable biometric hardware with something enrolled. Future isAvailable() async { try { if (!await _auth.isDeviceSupported()) return false; return await _auth.canCheckBiometrics; } on PlatformException { return false; } } /// Which biometric kinds are enrolled (used to label the sign-in buttons). Future> availableTypes() async { try { return await _auth.getAvailableBiometrics(); } on PlatformException { return const []; } } Future hasFace() async => (await availableTypes()).contains(BiometricType.face); Future hasFingerprint() async => (await availableTypes()).contains(BiometricType.fingerprint); /// The email biometric login was enabled for, or null if it's off. Future enabledEmail() async { final v = await _store.read(key: _emailKey); enabledCached = v != null; return v; } Future isEnabled() async => (await enabledEmail()) != null; /// Remember credentials for biometric sign-in. Caller should only invoke this /// after a successful password login so the stored creds are known-good. Future enable(String email, String password) async { await _store.write(key: _emailKey, value: email); await _store.write(key: _passwordKey, value: password); enabledCached = true; } Future disable() async { await _store.delete(key: _emailKey); await _store.delete(key: _passwordKey); enabledCached = false; } /// Prompt for biometrics and, on success, return the stored (email, password). /// Returns null if the user cancels or nothing is stored; throws /// [BiometricException] with a friendly message on a hard error. Future<(String, String)?> unlock({String? reason}) async { bool ok; try { ok = await _auth.authenticate( localizedReason: reason ?? "Sign in to Car Control", options: const AuthenticationOptions( biometricOnly: true, stickyAuth: true, ), authMessages: const [ AndroidAuthMessages( signInTitle: "Car Control sign-in", biometricHint: "", cancelButton: "Use password", ), ], ); } on PlatformException catch (e) { throw BiometricException(e.message ?? "Biometric authentication failed."); } if (!ok) return null; final email = await _store.read(key: _emailKey); final password = await _store.read(key: _passwordKey); if (email == null || password == null) return null; return (email, password); } } class BiometricException implements Exception { final String message; BiometricException(this.message); @override String toString() => message; } /// App-wide singleton (mirrors the other services in main.dart). final biometricAuth = BiometricAuth();