171 lines
4.2 KiB
Dart
171 lines
4.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import '../services/pin_lock_service.dart';
|
|
import '../services/settings_service.dart';
|
|
|
|
class PinLockViewModel extends ChangeNotifier {
|
|
final PinLockService pinLockService;
|
|
final SettingsService? settingsService;
|
|
|
|
PinLockViewModel({required this.pinLockService, this.settingsService});
|
|
|
|
bool _isPinSet = false;
|
|
bool _isUnlocked = false;
|
|
bool _isLoading = false;
|
|
bool _hasLoaded = false;
|
|
bool _pinRequired = true;
|
|
String? _errorMessage;
|
|
|
|
bool get isPinSet => _isPinSet;
|
|
|
|
bool get isUnlocked => _isUnlocked;
|
|
|
|
bool get isLoading => _isLoading;
|
|
|
|
bool get hasLoaded => _hasLoaded;
|
|
|
|
bool get pinRequired => _pinRequired;
|
|
|
|
String? get errorMessage => _errorMessage;
|
|
|
|
void setPinRequired(bool value) {
|
|
if (_pinRequired == value) return;
|
|
_pinRequired = value;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> ensureLoaded() async {
|
|
if (_hasLoaded || _isLoading) return;
|
|
|
|
await load();
|
|
}
|
|
|
|
/// Checks whether a PIN has already been configured on this device.
|
|
Future<void> load() async {
|
|
if (_isLoading) return;
|
|
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_isPinSet = await pinLockService.hasPin();
|
|
|
|
final svc = settingsService;
|
|
if (svc != null) {
|
|
final settings = await svc.getSettings();
|
|
_pinRequired = settings.pinRequired;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('PinLockViewModel: load error: $e');
|
|
_errorMessage = 'Could not check PIN status.';
|
|
} finally {
|
|
_hasLoaded = true;
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Creates a new PIN (first-time setup, or after disabling an old one).
|
|
/// Unlocks the app immediately on success, since the person just proved
|
|
/// they know it by typing it.
|
|
Future<bool> setPin(String pin) async {
|
|
_errorMessage = null;
|
|
|
|
try {
|
|
await pinLockService.setPin(pin);
|
|
_isPinSet = true;
|
|
_isUnlocked = true;
|
|
_pinRequired = true;
|
|
|
|
final svc = settingsService;
|
|
if (svc != null) {
|
|
final current = await svc.getSettings();
|
|
await svc.saveSettings(current.copyWith(pinRequired: true));
|
|
}
|
|
|
|
notifyListeners();
|
|
return true;
|
|
} catch (e) {
|
|
debugPrint('PinLockViewModel: setPin error: $e');
|
|
_errorMessage = 'Could not save PIN.';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Checks an entered PIN against the stored one, unlocking on match.
|
|
Future<bool> verify(String pin) async {
|
|
_errorMessage = null;
|
|
|
|
try {
|
|
final matches = await pinLockService.verifyPin(pin);
|
|
|
|
if (matches) {
|
|
_isUnlocked = true;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
_errorMessage = 'Incorrect PIN.';
|
|
notifyListeners();
|
|
return false;
|
|
} catch (e) {
|
|
debugPrint('PinLockViewModel: verify error: $e');
|
|
_errorMessage = 'Could not verify PIN.';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Replaces the current PIN. Requires the current PIN to match first.
|
|
Future<bool> changePin({
|
|
required String currentPin,
|
|
required String newPin,
|
|
}) async {
|
|
final currentMatches = await pinLockService.verifyPin(currentPin);
|
|
|
|
if (!currentMatches) {
|
|
_errorMessage = 'Current PIN is incorrect.';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
return setPin(newPin);
|
|
}
|
|
|
|
/// Removes the PIN entirely. Requires the current PIN to confirm.
|
|
Future<bool> disablePin(String currentPin) async {
|
|
final matches = await pinLockService.verifyPin(currentPin);
|
|
|
|
if (!matches) {
|
|
_errorMessage = 'Current PIN is incorrect.';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
await pinLockService.clearPin();
|
|
_isPinSet = false;
|
|
_pinRequired = false;
|
|
|
|
final svc = settingsService;
|
|
if (svc != null) {
|
|
final current = await svc.getSettings();
|
|
await svc.saveSettings(current.copyWith(pinRequired: false));
|
|
}
|
|
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
/// Re-locks the app. Call this on app backgrounding (e.g. from a
|
|
/// WidgetsBindingObserver on AppLifecycleState.paused) if you want the
|
|
/// PIN required again after the app is put away, not just on cold start.
|
|
void lock() {
|
|
if (!_isUnlocked) return;
|
|
|
|
_isUnlocked = false;
|
|
notifyListeners();
|
|
}
|
|
}
|