111 lines
3.7 KiB
Dart
111 lines
3.7 KiB
Dart
import "package:flutter/material.dart";
|
|
|
|
import "../biometric.dart";
|
|
import "../main.dart";
|
|
import "../theme.dart";
|
|
|
|
/// Shown when the app is authenticated but locked (biometric login is enabled
|
|
/// and the app was just launched or resumed). The session token is still valid;
|
|
/// a successful biometric check simply reveals the app. "Use password" drops the
|
|
/// session and returns to the login screen.
|
|
class LockScreen extends StatefulWidget {
|
|
const LockScreen({super.key});
|
|
@override
|
|
State<LockScreen> createState() => _LockScreenState();
|
|
}
|
|
|
|
class _LockScreenState extends State<LockScreen> {
|
|
bool _busy = false;
|
|
String? _error;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Prompt once as soon as the lock screen appears.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _unlock());
|
|
}
|
|
|
|
Future<void> _unlock() async {
|
|
if (_busy) return;
|
|
setState(() {
|
|
_busy = true;
|
|
_error = null;
|
|
});
|
|
try {
|
|
final creds = await biometricAuth.unlock(reason: "Unlock DriverVault");
|
|
if (creds == null) {
|
|
// Cancelled — leave locked; the buttons let them retry or use password.
|
|
if (mounted) setState(() => _busy = false);
|
|
return;
|
|
}
|
|
// Token from a previous session is still held by ApiClient; just reveal.
|
|
authService.unlock();
|
|
} on BiometricException catch (e) {
|
|
if (mounted) setState(() => _error = e.message);
|
|
} catch (e) {
|
|
if (mounted) setState(() => _error = e.toString());
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final u = authService.user;
|
|
final name = u == null ? null : (u.name.isNotEmpty ? u.name : u.email);
|
|
return Scaffold(
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 64,
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: DriverVault.brand700,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const Icon(Icons.lock_outline, color: Colors.white, size: 32),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text("DriverVault is locked",
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: -0.4)),
|
|
if (name != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Text(name, style: TextStyle(color: DriverVault.muted(context))),
|
|
),
|
|
const SizedBox(height: 24),
|
|
if (_error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Text(_error!,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: DriverVault.danger)),
|
|
),
|
|
SizedBox(
|
|
width: 260,
|
|
child: FilledButton.icon(
|
|
onPressed: _busy ? null : _unlock,
|
|
icon: const Icon(Icons.fingerprint),
|
|
label: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
child: Text(_busy ? "Unlocking…" : "Unlock"),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextButton(
|
|
onPressed: _busy ? null : () => authService.logout(),
|
|
child: const Text("Use password instead"),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|