252 lines
6.8 KiB
Plaintext
252 lines
6.8 KiB
Plaintext
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
|
||
import '../services/security_service.dart';
|
||
|
||
class AppLockGate extends StatefulWidget {
|
||
const AppLockGate({
|
||
super.key,
|
||
required this.securityService,
|
||
required this.child,
|
||
});
|
||
|
||
final SecurityService securityService;
|
||
final Widget child;
|
||
|
||
@override
|
||
State<AppLockGate> createState() => _AppLockGateState();
|
||
}
|
||
|
||
class _AppLockGateState extends State<AppLockGate> with WidgetsBindingObserver {
|
||
bool _checking = true;
|
||
bool _locked = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
_loadLockState();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
|
||
_lockIfNeeded();
|
||
}
|
||
}
|
||
|
||
Future<void> _loadLockState() async {
|
||
final enabled = await widget.securityService.isPinEnabled();
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_locked = enabled;
|
||
_checking = false;
|
||
});
|
||
}
|
||
|
||
Future<void> _lockIfNeeded() async {
|
||
final enabled = await widget.securityService.isPinEnabled();
|
||
if (!mounted || !enabled) return;
|
||
setState(() => _locked = true);
|
||
}
|
||
|
||
void _unlock() {
|
||
setState(() => _locked = false);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_checking) {
|
||
return const Scaffold(
|
||
body: Center(child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
|
||
if (_locked) {
|
||
return PinUnlockScreen(
|
||
securityService: widget.securityService,
|
||
onUnlocked: _unlock,
|
||
);
|
||
}
|
||
|
||
return widget.child;
|
||
}
|
||
}
|
||
|
||
class PinUnlockScreen extends StatefulWidget {
|
||
const PinUnlockScreen({
|
||
super.key,
|
||
required this.securityService,
|
||
required this.onUnlocked,
|
||
});
|
||
|
||
final SecurityService securityService;
|
||
final VoidCallback onUnlocked;
|
||
|
||
@override
|
||
State<PinUnlockScreen> createState() => _PinUnlockScreenState();
|
||
}
|
||
|
||
class _PinUnlockScreenState extends State<PinUnlockScreen> {
|
||
final _pinController = TextEditingController();
|
||
bool _checking = false;
|
||
bool _checkingBiometric = false;
|
||
bool _biometricEnabled = false;
|
||
bool _biometricAvailable = false;
|
||
bool _biometricAttempted = false;
|
||
String? _error;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadBiometricState();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_pinController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadBiometricState() async {
|
||
final enabled = await widget.securityService.isBiometricEnabled();
|
||
final available = await widget.securityService.isBiometricAvailable();
|
||
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_biometricEnabled = enabled;
|
||
_biometricAvailable = available;
|
||
});
|
||
|
||
if (enabled && available && !_biometricAttempted) {
|
||
_biometricAttempted = true;
|
||
await Future.delayed(const Duration(milliseconds: 250));
|
||
if (mounted) await _unlockWithBiometrics();
|
||
}
|
||
}
|
||
|
||
Future<void> _unlockWithBiometrics() async {
|
||
setState(() {
|
||
_checkingBiometric = true;
|
||
_error = null;
|
||
});
|
||
|
||
final ok = await widget.securityService.authenticateWithBiometrics();
|
||
|
||
if (!mounted) return;
|
||
|
||
if (ok) {
|
||
widget.onUnlocked();
|
||
} else {
|
||
setState(() {
|
||
_checkingBiometric = false;
|
||
_error = 'Empreinte refusée ou annulée. Utilise le PIN.';
|
||
});
|
||
}
|
||
}
|
||
|
||
Future<void> _unlock() async {
|
||
setState(() {
|
||
_checking = true;
|
||
_error = null;
|
||
});
|
||
|
||
final ok = await widget.securityService.verifyPin(_pinController.text.trim());
|
||
|
||
if (!mounted) return;
|
||
|
||
if (ok) {
|
||
_pinController.clear();
|
||
widget.onUnlocked();
|
||
} else {
|
||
HapticFeedback.vibrate();
|
||
setState(() {
|
||
_checking = false;
|
||
_error = 'PIN incorrect.';
|
||
});
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final showBiometric = _biometricEnabled && _biometricAvailable;
|
||
|
||
return Scaffold(
|
||
body: SafeArea(
|
||
child: Center(
|
||
child: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: 420),
|
||
child: ListView(
|
||
shrinkWrap: true,
|
||
padding: const EdgeInsets.all(24),
|
||
children: [
|
||
Icon(showBiometric ? Icons.fingerprint : Icons.lock, size: 64),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'FIN65 verrouillé',
|
||
textAlign: TextAlign.center,
|
||
style: Theme.of(context).textTheme.headlineSmall,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
showBiometric
|
||
? 'Déverrouille avec ton empreinte ou ton PIN.'
|
||
: 'Entre ton PIN pour accéder aux données locales.',
|
||
textAlign: TextAlign.center,
|
||
),
|
||
if (showBiometric) ...[
|
||
const SizedBox(height: 24),
|
||
FilledButton.icon(
|
||
onPressed: _checkingBiometric ? null : _unlockWithBiometrics,
|
||
icon: _checkingBiometric
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: const Icon(Icons.fingerprint),
|
||
label: Text(_checkingBiometric ? 'Vérification...' : 'Utiliser l’empreinte'),
|
||
),
|
||
],
|
||
const SizedBox(height: 24),
|
||
TextField(
|
||
controller: _pinController,
|
||
autofocus: !showBiometric,
|
||
obscureText: true,
|
||
keyboardType: TextInputType.number,
|
||
maxLength: 8,
|
||
decoration: InputDecoration(
|
||
labelText: 'PIN',
|
||
border: const OutlineInputBorder(),
|
||
errorText: _error,
|
||
counterText: '',
|
||
),
|
||
onSubmitted: (_) => _checking ? null : _unlock(),
|
||
),
|
||
const SizedBox(height: 12),
|
||
OutlinedButton.icon(
|
||
onPressed: _checking ? null : _unlock,
|
||
icon: _checking
|
||
? const SizedBox(
|
||
width: 18,
|
||
height: 18,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: const Icon(Icons.lock_open),
|
||
label: Text(_checking ? 'Vérification...' : 'Déverrouiller avec le PIN'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|