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'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,399 @@
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import '../services/security_service.dart';
class SecurityScreen extends StatefulWidget {
const SecurityScreen({
super.key,
required this.securityService,
});
final SecurityService securityService;
@override
State<SecurityScreen> createState() => _SecurityScreenState();
}
class _SecurityScreenState extends State<SecurityScreen> {
final _currentPinController = TextEditingController();
final _newPinController = TextEditingController();
final _confirmPinController = TextEditingController();
bool _loading = true;
bool _saving = false;
bool _enabled = false;
bool _biometricAvailable = false;
bool _biometricEnabled = false;
List<BiometricType> _availableBiometrics = const [];
String? _message;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_currentPinController.dispose();
_newPinController.dispose();
_confirmPinController.dispose();
super.dispose();
}
Future<void> _load() async {
final enabled = await widget.securityService.isPinEnabled();
final biometricAvailable = await widget.securityService.isBiometricAvailable();
final biometricEnabled = await widget.securityService.isBiometricEnabled();
final availableBiometrics = await widget.securityService.getAvailableBiometrics();
if (!mounted) return;
setState(() {
_enabled = enabled;
_biometricAvailable = biometricAvailable;
_biometricEnabled = biometricEnabled;
_availableBiometrics = availableBiometrics;
_loading = false;
});
}
Future<bool> _verifyCurrentPinFromFieldIfNeeded() async {
if (!_enabled) return true;
final current = _currentPinController.text.trim();
final ok = await widget.securityService.verifyPin(current);
if (!ok) {
setState(() => _message = 'PIN actuel incorrect.');
return false;
}
return true;
}
Future<String?> _askCurrentPinDialog({
required String title,
required String actionLabel,
}) async {
final controller = TextEditingController();
final result = await showDialog<String>(
context: context,
barrierDismissible: true,
builder: (context) {
return AlertDialog(
title: Text(title),
content: TextField(
controller: controller,
autofocus: true,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 8,
decoration: const InputDecoration(
labelText: 'PIN actuel',
border: OutlineInputBorder(),
counterText: '',
),
onSubmitted: (_) => Navigator.of(context).pop(controller.text.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
child: Text(actionLabel),
),
],
);
},
);
controller.dispose();
return result?.trim();
}
Future<bool> _askAndVerifyCurrentPin({
required String title,
required String actionLabel,
}) async {
final pin = await _askCurrentPinDialog(
title: title,
actionLabel: actionLabel,
);
if (pin == null) {
setState(() => _message = 'Action annulée.');
return false;
}
final ok = await widget.securityService.verifyPin(pin);
if (!ok) {
setState(() => _message = 'PIN actuel incorrect.');
return false;
}
return true;
}
Future<void> _savePin() async {
final newPin = _newPinController.text.trim();
final confirm = _confirmPinController.text.trim();
if (newPin != confirm) {
setState(() => _message = 'Le nouveau PIN et la confirmation ne correspondent pas.');
return;
}
setState(() {
_saving = true;
_message = null;
});
try {
final currentOk = await _verifyCurrentPinFromFieldIfNeeded();
if (!currentOk) return;
await widget.securityService.setPin(newPin);
_currentPinController.clear();
_newPinController.clear();
_confirmPinController.clear();
await _load();
setState(() {
_message = 'Verrouillage PIN activé.';
});
} catch (e) {
setState(() => _message = 'Erreur : $e');
} finally {
if (mounted) setState(() => _saving = false);
}
}
Future<void> _disablePin() async {
setState(() {
_saving = true;
_message = null;
});
try {
final currentOk = await _askAndVerifyCurrentPin(
title: 'Désactiver le verrouillage PIN',
actionLabel: 'Désactiver',
);
if (!currentOk) return;
await widget.securityService.disablePin();
_currentPinController.clear();
_newPinController.clear();
_confirmPinController.clear();
await _load();
setState(() {
_message = 'Verrouillage PIN et biométrie désactivés.';
});
} catch (e) {
setState(() => _message = 'Erreur : $e');
} finally {
if (mounted) setState(() => _saving = false);
}
}
Future<void> _toggleBiometrics(bool value) async {
if (_saving) return;
setState(() {
_saving = true;
_message = null;
});
try {
if (value) {
if (!_enabled) {
setState(() => _message = 'Active d’abord un PIN avant la biométrie.');
return;
}
if (!_biometricAvailable) {
setState(() => _message = 'Aucune empreinte/biométrie disponible sur ce téléphone.');
return;
}
final currentOk = await _askAndVerifyCurrentPin(
title: 'Activer l’empreinte',
actionLabel: 'Continuer',
);
if (!currentOk) return;
await widget.securityService.setBiometricEnabled(true);
final confirmed = await widget.securityService.authenticateWithBiometrics();
if (!confirmed) {
await widget.securityService.setBiometricEnabled(false);
setState(() => _message = 'Empreinte annulée ou refusée. Biométrie non activée.');
return;
}
} else {
final currentOk = await _askAndVerifyCurrentPin(
title: 'Désactiver l’empreinte',
actionLabel: 'Désactiver',
);
if (!currentOk) return;
await widget.securityService.setBiometricEnabled(false);
}
await _load();
setState(() {
_message = value
? 'Déverrouillage par empreinte activé.'
: 'Déverrouillage par empreinte désactivé.';
});
} catch (e) {
setState(() => _message = 'Erreur : $e');
} finally {
if (mounted) setState(() => _saving = false);
}
}
String _biometricLabel() {
if (_availableBiometrics.isEmpty) return 'Aucune biométrie détectée.';
final labels = _availableBiometrics.map((type) {
return switch (type) {
BiometricType.fingerprint => 'empreinte',
BiometricType.face => 'visage',
BiometricType.iris => 'iris',
BiometricType.strong => 'biométrie forte',
BiometricType.weak => 'biométrie faible',
};
}).toSet().join(', ');
return 'Disponible : $labels.';
}
@override
Widget build(BuildContext context) {
if (_loading) {
return Scaffold(
appBar: AppBar(title: const Text('Sécurité')),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Sécurité')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: SwitchListTile(
value: _enabled,
onChanged: null,
secondary: Icon(_enabled ? Icons.lock : Icons.lock_open),
title: const Text('Verrouillage PIN'),
subtitle: Text(
_enabled
? 'Activé. L’app se verrouille à la fermeture ou au changement d’application.'
: 'Désactivé. Tes données locales sont accessibles sans PIN.',
),
),
),
if (_enabled) ...[
const SizedBox(height: 12),
Card(
child: SwitchListTile(
value: _biometricEnabled,
onChanged: _saving || !_biometricAvailable ? null : _toggleBiometrics,
secondary: const Icon(Icons.fingerprint),
title: const Text('Déverrouillage par empreinte'),
subtitle: Text(
_biometricAvailable
? '${_biometricLabel()} Appuie sur l’interrupteur, entre le PIN actuel, puis valide l’empreinte.'
: 'Aucune empreinte/biométrie disponible ou configurée sur ce téléphone.',
),
),
),
],
const SizedBox(height: 16),
if (_enabled) ...[
TextField(
controller: _currentPinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 8,
decoration: const InputDecoration(
labelText: 'PIN actuel',
border: OutlineInputBorder(),
helperText: 'Uniquement nécessaire pour changer le PIN.',
counterText: '',
),
),
const SizedBox(height: 12),
],
TextField(
controller: _newPinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 8,
decoration: InputDecoration(
labelText: _enabled ? 'Nouveau PIN' : 'PIN',
border: const OutlineInputBorder(),
helperText: '4 à 8 chiffres.',
counterText: '',
),
),
const SizedBox(height: 12),
TextField(
controller: _confirmPinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 8,
decoration: const InputDecoration(
labelText: 'Confirmer le PIN',
border: OutlineInputBorder(),
counterText: '',
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: _saving ? null : _savePin,
icon: const Icon(Icons.save),
label: Text(_enabled ? 'Changer le PIN' : 'Activer le PIN'),
),
if (_enabled) ...[
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _saving ? null : _disablePin,
icon: const Icon(Icons.lock_open),
label: const Text('Désactiver le PIN'),
),
],
if (_message != null) ...[
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(_message!),
),
),
],
const SizedBox(height: 24),
const Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
'Le PIN protège l’accès local à l’application. L’empreinte utilise la sécurité biométrique du téléphone. Le PIN reste le secours si l’empreinte échoue.',
),
),
),
],
),
);
}
}