mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-02 09:35:31 +02:00
186 lines
7.0 KiB
Dart
186 lines
7.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// Manages app lock: PIN, biometrics, and two independent lock modes.
|
|
///
|
|
/// Modes (both can be on at the same time):
|
|
/// - **App-wide lock** — shown on cold start (before WebView) and after
|
|
/// background timeout.
|
|
/// - **Messages tab lock** — shown when navigating to Instagram DMs.
|
|
///
|
|
/// Both use the same PIN (stored in secure storage).
|
|
class AppLockService extends ChangeNotifier {
|
|
static const _pinAppWideKey = 'app_lock_pin_app_wide';
|
|
static const _pinMessagesKey = 'app_lock_pin_messages';
|
|
static const _prefAppWide = 'app_lock_app_wide';
|
|
static const _prefLockMessages = 'app_lock_lock_messages';
|
|
static const _prefScramble = 'app_lock_scramble_keypad';
|
|
static const _prefBio = 'app_lock_biometrics_enabled';
|
|
static const _prefTimeout = 'app_lock_timeout_ms';
|
|
|
|
final _secure = const FlutterSecureStorage();
|
|
final _auth = LocalAuthentication();
|
|
|
|
// ─── Mode toggles ──────────────────────────────────────────
|
|
bool _lockAppWide = false; // locks the whole app on start / bg timeout
|
|
bool _lockMessages = false; // locks only the DMs tab
|
|
|
|
// ─── Settings ──────────────────────────────────────────────
|
|
bool _scramble = false;
|
|
bool _bioEnabled = false;
|
|
int _timeoutMs = 120000; // 2 min
|
|
bool _hasPin = false;
|
|
|
|
// ─── Runtime state ─────────────────────────────────────────
|
|
bool _isShowingLock = false; // true while lock screen is displayed
|
|
DateTime? _bgAt;
|
|
|
|
// ─── Getters ───────────────────────────────────────────────
|
|
bool get lockAppWide => _lockAppWide;
|
|
bool get lockMessages => _lockMessages;
|
|
bool get isShowingLock => _isShowingLock;
|
|
bool get scrambleKeypad => _scramble;
|
|
bool get biometricsEnabled => _bioEnabled;
|
|
bool get hasPin => _hasPin;
|
|
bool get anyLockEnabled => _lockAppWide || _lockMessages;
|
|
|
|
/// Whether the app-wide lock screen should show on cold start.
|
|
bool get needsUnlockOnStart => _lockAppWide && _hasPin;
|
|
|
|
/// Whether the messages tab lock is enabled and can function.
|
|
bool get messagesLockReady => _lockMessages && _hasPin;
|
|
|
|
// ─── Init ──────────────────────────────────────────────────
|
|
Future<void> init() async {
|
|
final p = await SharedPreferences.getInstance();
|
|
_lockAppWide = p.getBool(_prefAppWide) ?? false;
|
|
_lockMessages = p.getBool(_prefLockMessages) ?? false;
|
|
_scramble = p.getBool(_prefScramble) ?? false;
|
|
_bioEnabled = p.getBool(_prefBio) ?? true;
|
|
_timeoutMs = p.getInt(_prefTimeout) ?? 120000;
|
|
|
|
// Check if either PIN exists
|
|
final hashA = await _secure.read(key: _pinAppWideKey);
|
|
final hashM = await _secure.read(key: _pinMessagesKey);
|
|
_hasPin =
|
|
(hashA != null && hashA.isNotEmpty) ||
|
|
(hashM != null && hashM.isNotEmpty);
|
|
}
|
|
|
|
// ─── PIN management ────────────────────────────────────────
|
|
String _hash(String pin) => utf8
|
|
.encode('fg_${pin}_salt26')
|
|
.map((x) => x.toRadixString(16).padLeft(2, '0'))
|
|
.join();
|
|
|
|
/// Set PIN for a specific lock mode.
|
|
Future<void> setPin(String pin, {required bool forAppWide}) async {
|
|
final key = forAppWide ? _pinAppWideKey : _pinMessagesKey;
|
|
await _secure.write(key: key, value: _hash(pin));
|
|
_hasPin = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Verify PIN for the given mode.
|
|
Future<bool> verifyPin(String pin, {required bool forAppWide}) async {
|
|
final key = forAppWide ? _pinAppWideKey : _pinMessagesKey;
|
|
final stored = await _secure.read(key: key);
|
|
return stored != null && stored == _hash(pin);
|
|
}
|
|
|
|
/// Check whether a specific mode has a PIN set.
|
|
Future<bool> hasPinFor({required bool forAppWide}) async {
|
|
final key = forAppWide ? _pinAppWideKey : _pinMessagesKey;
|
|
final hash = await _secure.read(key: key);
|
|
return hash != null && hash.isNotEmpty;
|
|
}
|
|
|
|
// ─── Toggles ───────────────────────────────────────────────
|
|
Future<void> setLockAppWide(bool v) async {
|
|
_lockAppWide = v;
|
|
(await SharedPreferences.getInstance()).setBool(_prefAppWide, v);
|
|
if (!v && !_isShowingLock) _isShowingLock = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setLockMessages(bool v) async {
|
|
_lockMessages = v;
|
|
(await SharedPreferences.getInstance()).setBool(_prefLockMessages, v);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setScrambleKeypad(bool v) async {
|
|
_scramble = v;
|
|
(await SharedPreferences.getInstance()).setBool(_prefScramble, v);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setBiometricsEnabled(bool v) async {
|
|
_bioEnabled = v;
|
|
(await SharedPreferences.getInstance()).setBool(_prefBio, v);
|
|
notifyListeners();
|
|
}
|
|
|
|
// ─── Lock / Unlock lifecycle ───────────────────────────────
|
|
|
|
/// Call when app-wide lock screen is opened.
|
|
void onLockScreenShown() {
|
|
_isShowingLock = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Call after successful unlock (PIN or biometric).
|
|
void onUnlocked() {
|
|
_isShowingLock = false;
|
|
_bgAt = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Call when app goes to background.
|
|
void onBackgrounded() {
|
|
_bgAt = DateTime.now();
|
|
}
|
|
|
|
/// Whether the app-wide lock should trigger on resume.
|
|
bool get shouldLockOnResume {
|
|
if (!_lockAppWide || !_hasPin || _bgAt == null) return false;
|
|
return DateTime.now().difference(_bgAt!).inMilliseconds >= _timeoutMs;
|
|
}
|
|
|
|
// ─── Biometrics ────────────────────────────────────────────
|
|
Future<bool> isBiometricsAvailable() async {
|
|
try {
|
|
return await _auth.canCheckBiometrics || await _auth.isDeviceSupported();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> authenticateWithBiometrics() async {
|
|
if (!_bioEnabled) return false;
|
|
try {
|
|
return await _auth.authenticate(
|
|
localizedReason: 'Unlock FocusGram',
|
|
options: const AuthenticationOptions(
|
|
biometricOnly: false,
|
|
stickyAuth: true,
|
|
),
|
|
);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─── Scrambled keypad ──────────────────────────────────────
|
|
List<int> getScrambledDigits() {
|
|
final d = List<int>.generate(10, (i) => i);
|
|
d.shuffle(Random());
|
|
return d;
|
|
}
|
|
}
|