Initial upload: fin65-mobile
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _unlockWithBiometrics() async {
|
||||
if (_checkingBiometric) return;
|
||||
|
||||
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 {
|
||||
if (_checking) return;
|
||||
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user