Files
2026-08-22 16:09:05 +02:00

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);
}
}