| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- import 'dart:async';
- import 'package:flutter/foundation.dart';
- import '../../data/services/pin_service.dart';
- /// Enforces the app PIN.
- ///
- /// Deliberately simple: the PIN is checked only while the launcher process is
- /// starting. There is no re-lock on resume or screen-off. Setting or changing
- /// the PIN locks once immediately so the new code is confirmed in use.
- class LockController extends ChangeNotifier {
- LockController(this._pinService);
- final PinService _pinService;
- bool _initialized = false;
- bool _pinSet = false;
- bool _locked = false;
- String? _errorMessage;
- int _failedAttempts = 0;
- DateTime? _cooldownUntil;
- Timer? _cooldownTimer;
- bool _disposed = false;
- bool get initialized => _initialized;
- bool get pinSet => _pinSet;
- bool get isLocked => _locked;
- String? get errorMessage => _errorMessage;
- bool get isCoolingDown {
- final until = _cooldownUntil;
- return until != null && until.isAfter(DateTime.now());
- }
- int get cooldownSecondsRemaining {
- final until = _cooldownUntil;
- if (until == null) return 0;
- final remaining = until.difference(DateTime.now()).inSeconds;
- return remaining < 0 ? 0 : remaining;
- }
- Future<void> initialize() async {
- try {
- _pinSet = await _pinService.isPinSet();
- _locked = _pinSet;
- } catch (_) {
- // If secure storage cannot be read, do not fabricate a PIN. The security
- // section exposes the error so the issue is visible rather than silent.
- _pinSet = false;
- _locked = false;
- _errorMessage = 'Secure storage is unavailable on this device.';
- }
- _initialized = true;
- _notify();
- }
- Future<bool> unlock(String pin) async {
- if (!_pinSet) {
- _locked = false;
- _notify();
- return true;
- }
- if (isCoolingDown) {
- _errorMessage =
- 'Too many attempts. Try again in ${cooldownSecondsRemaining}s.';
- _notify();
- return false;
- }
- _errorMessage = null;
- bool valid;
- try {
- valid = await _pinService.verify(pin);
- } catch (_) {
- _errorMessage = 'Could not verify the PIN. Try again.';
- _notify();
- return false;
- }
- if (valid) {
- _locked = false;
- _failedAttempts = 0;
- _cooldownUntil = null;
- _errorMessage = null;
- _cooldownTimer?.cancel();
- _notify();
- return true;
- }
- _recordFailure();
- return false;
- }
- Future<String?> setPin(String pin) async {
- try {
- await _pinService.setPin(pin);
- } catch (_) {
- return 'Could not save the PIN.';
- }
- _pinSet = true;
- // The PIN is only requested on the next cold start by design. Setting or
- // changing it does not lock the current session.
- _failedAttempts = 0;
- _cooldownUntil = null;
- _cooldownTimer?.cancel();
- _errorMessage = null;
- _notify();
- return null;
- }
- Future<String?> changePin({
- required String currentPin,
- required String newPin,
- }) async {
- final verified = await _verifyCurrent(currentPin);
- if (!verified) {
- return _errorMessage ?? 'Current PIN is incorrect.';
- }
- return setPin(newPin);
- }
- Future<String?> removePin(String currentPin) async {
- final verified = await _verifyCurrent(currentPin);
- if (!verified) {
- return _errorMessage ?? 'Current PIN is incorrect.';
- }
- try {
- await _pinService.clearPin();
- } catch (_) {
- return 'Could not remove the PIN.';
- }
- _pinSet = false;
- _locked = false;
- _errorMessage = null;
- _failedAttempts = 0;
- _cooldownUntil = null;
- _cooldownTimer?.cancel();
- _notify();
- return null;
- }
- /// Verifies the current PIN without changing the lock state. Used by the
- /// change/remove flows in Settings.
- Future<bool> verifyPin(String pin) => _verifyCurrent(pin);
- Future<bool> _verifyCurrent(String pin) async {
- if (isCoolingDown) {
- _errorMessage =
- 'Too many attempts. Try again in ${cooldownSecondsRemaining}s.';
- _notify();
- return false;
- }
- try {
- final valid = await _pinService.verify(pin);
- if (!valid) {
- _recordFailure();
- return false;
- }
- _failedAttempts = 0;
- _errorMessage = null;
- _notify();
- return true;
- } catch (_) {
- _errorMessage = 'Could not verify the PIN.';
- _notify();
- return false;
- }
- }
- void _recordFailure() {
- _failedAttempts++;
- if (_failedAttempts >= 5) {
- _failedAttempts = 0;
- _cooldownUntil = DateTime.now().add(const Duration(seconds: 30));
- _cooldownTimer?.cancel();
- _cooldownTimer = Timer(const Duration(seconds: 30), () {
- _errorMessage = null;
- _notify();
- });
- _errorMessage = 'Too many attempts. Try again in 30s.';
- } else {
- _errorMessage = 'Incorrect PIN. Try again.';
- }
- _notify();
- }
- void clearError() {
- if (_errorMessage == null) return;
- _errorMessage = null;
- _notify();
- }
- void _notify() {
- if (!_disposed) notifyListeners();
- }
- @override
- void dispose() {
- _disposed = true;
- _cooldownTimer?.cancel();
- super.dispose();
- }
- }
|