168 lines
6.1 KiB
Dart
168 lines
6.1 KiB
Dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:local_auth/error_codes.dart' as auth_error;
|
|
import 'package:flutter/services.dart';
|
|
|
|
/// Esito dello sblocco all'avvio.
|
|
enum UnlockResult {
|
|
/// Nessun login salvato: mostra la schermata di login normale.
|
|
noSavedLogin,
|
|
|
|
/// Token disponibile e pronto all'uso: entra diretto.
|
|
success,
|
|
|
|
/// C'è un login salvato ma la biometria è fallita/annullata:
|
|
/// resta sul login, l'utente può accedere con password.
|
|
biometricFailed,
|
|
|
|
/// Biometria richiesta ma non disponibile (niente impronte / hardware):
|
|
/// fallback a login con password (scelta B).
|
|
biometricUnavailable,
|
|
}
|
|
|
|
/// Custode unico di token + preferenze di accesso (ricordami + biometria).
|
|
///
|
|
/// Non è un provider: lo si interroga in momenti precisi
|
|
/// (avvio nello splash, login, logout). Vedi nota architetturale nel piano.
|
|
class AuthStorage {
|
|
AuthStorage._();
|
|
static final AuthStorage instance = AuthStorage._();
|
|
|
|
// Su Android forza l'uso di EncryptedSharedPreferences (più robusto).
|
|
static const _secure = FlutterSecureStorage(
|
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
|
);
|
|
|
|
final LocalAuthentication _localAuth = LocalAuthentication();
|
|
|
|
// Chiavi interne della cassaforte.
|
|
static const _kToken = 'auth_token';
|
|
static const _kRemember = 'pref_remember_me';
|
|
static const _kBiometric = 'pref_biometric';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PREFERENZE
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// "Ricordami" attivo? (default: false)
|
|
Future<bool> isRememberMeEnabled() async {
|
|
return (await _secure.read(key: _kRemember)) == 'true';
|
|
}
|
|
|
|
/// Biometria attiva? (default: false)
|
|
Future<bool> isBiometricEnabled() async {
|
|
return (await _secure.read(key: _kBiometric)) == 'true';
|
|
}
|
|
|
|
/// Imposta "ricordami". Se lo spegni, cancella anche il token salvato:
|
|
/// senza ricordami non ha senso tenere un token in cassaforte.
|
|
Future<void> setRememberMe(bool value) async {
|
|
await _secure.write(key: _kRemember, value: value ? 'true' : 'false');
|
|
if (!value) {
|
|
await _secure.delete(key: _kToken);
|
|
// Biometria senza token salvato non ha nulla da proteggere: spegnila.
|
|
await _secure.write(key: _kBiometric, value: 'false');
|
|
}
|
|
}
|
|
|
|
/// Imposta la biometria. Ha senso solo se "ricordami" è attivo.
|
|
Future<void> setBiometric(bool value) async {
|
|
await _secure.write(key: _kBiometric, value: value ? 'true' : 'false');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TOKEN
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Salva il token SOLO se "ricordami" è attivo. Chiamato dopo il login.
|
|
Future<void> saveTokenIfRemember(String token) async {
|
|
if (await isRememberMeEnabled()) {
|
|
await _secure.write(key: _kToken, value: token);
|
|
}
|
|
}
|
|
|
|
/// Cancella tutto: token + preferenze. Chiamato al logout.
|
|
Future<void> clear() async {
|
|
await _secure.delete(key: _kToken);
|
|
await _secure.delete(key: _kRemember);
|
|
await _secure.delete(key: _kBiometric);
|
|
}
|
|
|
|
/// Lettura grezza del token (senza biometria). Uso interno.
|
|
Future<String?> _readToken() => _secure.read(key: _kToken);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BIOMETRIA
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Il dispositivo può fare biometria E ha almeno un metodo registrato?
|
|
Future<bool> canUseBiometrics() async {
|
|
try {
|
|
final supported = await _localAuth.isDeviceSupported();
|
|
if (!supported) return false;
|
|
final canCheck = await _localAuth.canCheckBiometrics;
|
|
if (!canCheck) return false;
|
|
final enrolled = await _localAuth.getAvailableBiometrics();
|
|
return enrolled.isNotEmpty;
|
|
} on PlatformException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Chiede l'impronta/volto. true = passata, false = fallita/annullata.
|
|
Future<bool> _promptBiometric() async {
|
|
try {
|
|
return await _localAuth.authenticate(
|
|
localizedReason: 'Sblocca YogiBook per accedere al tuo account',
|
|
options: const AuthenticationOptions(
|
|
biometricOnly: true, // niente fallback al PIN del telefono (scelta B)
|
|
stickyAuth: true, // regge se l'app va in background durante il prompt
|
|
),
|
|
);
|
|
} on PlatformException catch (e) {
|
|
// Casi tipici: nessuna impronta registrata, hardware assente, troppi tentativi.
|
|
if (e.code == auth_error.notAvailable ||
|
|
e.code == auth_error.notEnrolled ||
|
|
e.code == auth_error.lockedOut ||
|
|
e.code == auth_error.permanentlyLockedOut) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SBLOCCO ALL'AVVIO (chiamato dallo splash)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Decide cosa fare all'avvio dell'app.
|
|
/// Restituisce l'esito; il token (se disponibile) è in [tokenOut].
|
|
Future<({UnlockResult result, String? token})> tryUnlockAtStartup() async {
|
|
// 1. Ricordami spento o nessun token → login normale.
|
|
if (!await isRememberMeEnabled()) {
|
|
return (result: UnlockResult.noSavedLogin, token: null);
|
|
}
|
|
final token = await _readToken();
|
|
if (token == null || token.isEmpty) {
|
|
return (result: UnlockResult.noSavedLogin, token: null);
|
|
}
|
|
|
|
// 2. Biometria spenta → entra diretto col token salvato.
|
|
if (!await isBiometricEnabled()) {
|
|
return (result: UnlockResult.success, token: token);
|
|
}
|
|
|
|
// 3. Biometria accesa ma non disponibile → fallback a password (scelta B).
|
|
if (!await canUseBiometrics()) {
|
|
return (result: UnlockResult.biometricUnavailable, token: null);
|
|
}
|
|
|
|
// 4. Biometria accesa e disponibile → chiedi l'impronta.
|
|
final ok = await _promptBiometric();
|
|
if (ok) {
|
|
return (result: UnlockResult.success, token: token);
|
|
}
|
|
return (result: UnlockResult.biometricFailed, token: null);
|
|
}
|
|
}
|