lock_screen.dart 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import 'package:local_auth/local_auth.dart';
  4. import '../state/app_state.dart';
  5. class LockScreenPage extends StatefulWidget {
  6. const LockScreenPage({super.key});
  7. @override
  8. State<LockScreenPage> createState() => _LockScreenPageState();
  9. }
  10. class _LockScreenPageState extends State<LockScreenPage> {
  11. final _pinController = TextEditingController();
  12. String? _error;
  13. bool _loading = false;
  14. bool _biometricAvailable = false;
  15. String? _biometricError;
  16. bool _biometricInProgress = false;
  17. @override
  18. void initState() {
  19. super.initState();
  20. _checkBiometricAvailable();
  21. WidgetsBinding.instance.addPostFrameCallback((_) {
  22. _tryBiometric(auto: true);
  23. });
  24. }
  25. Future<void> _checkBiometricAvailable() async {
  26. final localAuth = LocalAuthentication();
  27. final canCheck = await localAuth.canCheckBiometrics;
  28. final supported = await localAuth.isDeviceSupported();
  29. final enrolled = (await localAuth.getAvailableBiometrics()).isNotEmpty;
  30. setState(() {
  31. _biometricAvailable = canCheck && supported && enrolled;
  32. });
  33. }
  34. Future<void> _tryBiometric({bool auto = false}) async {
  35. final appState = Provider.of<AppState>(context, listen: false);
  36. if (!appState.biometricEnabled || !_biometricAvailable || _biometricInProgress) return;
  37. setState(() { _loading = true; _biometricError = null; _biometricInProgress = true; });
  38. final localAuth = LocalAuthentication();
  39. try {
  40. final didAuth = await localAuth.authenticate(
  41. localizedReason: 'Authenticate to unlock',
  42. options: const AuthenticationOptions(biometricOnly: true, stickyAuth: true),
  43. );
  44. if (didAuth) {
  45. appState.updateLastUnlockTime();
  46. if (mounted) {
  47. WidgetsBinding.instance.addPostFrameCallback((_) {
  48. if (mounted) Navigator.of(context).pop(true);
  49. });
  50. }
  51. return;
  52. } else {
  53. setState(() { _biometricError = 'Biometric authentication failed.'; });
  54. }
  55. } catch (e) {
  56. debugPrint('Biometric error: $e');
  57. setState(() { _biometricError = 'Biometric authentication unavailable. ($e)'; });
  58. }
  59. setState(() { _loading = false; _biometricInProgress = false; });
  60. }
  61. Future<void> _checkPin() async {
  62. setState(() { _loading = true; _error = null; });
  63. final appState = Provider.of<AppState>(context, listen: false);
  64. final pin = await appState.getPin();
  65. if (_pinController.text == pin) {
  66. appState.updateLastUnlockTime();
  67. if (mounted) {
  68. WidgetsBinding.instance.addPostFrameCallback((_) {
  69. if (mounted) Navigator.of(context).pop(true);
  70. });
  71. }
  72. } else {
  73. setState(() { _error = 'Incorrect PIN'; _loading = false; });
  74. // After PIN failure, offer biometric again if enabled
  75. if (appState.biometricEnabled && _biometricAvailable) {
  76. _tryBiometric();
  77. }
  78. }
  79. }
  80. @override
  81. Widget build(BuildContext context) {
  82. final appState = Provider.of<AppState>(context);
  83. return Scaffold(
  84. body: Center(
  85. child: Padding(
  86. padding: const EdgeInsets.all(32.0),
  87. child: Column(
  88. mainAxisSize: MainAxisSize.min,
  89. children: [
  90. const Icon(Icons.lock, size: 64),
  91. const SizedBox(height: 16),
  92. Text('Unlock PB Authenticator', style: Theme.of(context).textTheme.titleLarge),
  93. const SizedBox(height: 24),
  94. if (appState.pinEnabled)
  95. TextField(
  96. controller: _pinController,
  97. keyboardType: TextInputType.number,
  98. obscureText: true,
  99. maxLength: 6,
  100. enabled: !_loading,
  101. decoration: InputDecoration(
  102. labelText: 'Enter PIN',
  103. errorText: _error,
  104. ),
  105. onSubmitted: (_) => _checkPin(),
  106. ),
  107. const SizedBox(height: 16),
  108. if (appState.pinEnabled)
  109. ElevatedButton(
  110. onPressed: _loading ? null : _checkPin,
  111. child: _loading ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Unlock'),
  112. ),
  113. if (appState.biometricEnabled)
  114. Padding(
  115. padding: const EdgeInsets.only(top: 16),
  116. child: Column(
  117. children: [
  118. ElevatedButton.icon(
  119. icon: const Icon(Icons.fingerprint),
  120. label: const Text('Use biometrics'),
  121. onPressed: (_loading || !_biometricAvailable) ? null : () => _tryBiometric(),
  122. ),
  123. if (_biometricError != null)
  124. Padding(
  125. padding: const EdgeInsets.only(top: 8),
  126. child: Text(_biometricError!, style: const TextStyle(color: Colors.red)),
  127. ),
  128. if (!_biometricAvailable)
  129. const Padding(
  130. padding: EdgeInsets.only(top: 8),
  131. child: Text('No biometrics enrolled or not available.', style: TextStyle(color: Colors.red)),
  132. ),
  133. ],
  134. ),
  135. ),
  136. ],
  137. ),
  138. ),
  139. ),
  140. );
  141. }
  142. @override
  143. void dispose() {
  144. _pinController.dispose();
  145. super.dispose();
  146. }
  147. }