Initial upload: fin65-mobile

This commit is contained in:
Abderrahim-Mouhcine
2026-08-16 19:59:55 +01:00
commit 46558a5ec8
242 changed files with 16334 additions and 0 deletions
@@ -0,0 +1,251 @@
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'),
),
],
),
),
),
),
);
}
}