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,46 @@
# FIN65 - Fix stabilité biométrie V2.6.2
Ce patch corrige l'erreur Flutter :
```text
Failed assertion: '_dependents.isEmpty': is not true.
```
## Cause probable
Le fix précédent ouvrait une boîte de dialogue Flutter puis lançait l'authentification biométrique Android dans le même flux. Flutter n'a pas apprécié ce petit numéro de cirque entre navigation, dialogue et prompt système.
## Correction
Cette version simplifie le flux :
- plus de boîte de dialogue Flutter pour activer l'empreinte ;
- plus de lancement automatique de l'empreinte au verrouillage ;
- l’empreinte s’active depuis l’écran Sécurité après avoir entré le PIN actuel dans le champ visible ;
- au verrouillage, l’utilisateur appuie manuellement sur `Utiliser l’empreinte`.
## Installation
Décompresse puis lance :
```powershell
.\apply_biometrics_stability_fix_v2_6_2.bat
```
Ensuite :
```powershell
cd D:\07-Logiciels\20-FIN65\fin65_mobile
flutter run
```
## Test
1. Paramètres > Sécurité.
2. Entre le PIN actuel dans le champ `PIN actuel`.
3. Active l’interrupteur empreinte.
4. Mets l’app en arrière-plan.
5. Rouvre l’app.
6. Appuie sur `Utiliser l’empreinte`.
Cette version est volontairement moins magique, donc moins susceptible de s’écrouler comme une étagère IKEA montée avec colère.
@@ -0,0 +1,60 @@
@echo off
setlocal
REM ============================================================
REM FIN65 - Fix stabilite biometrie V2.6.2
REM ============================================================
set "ROOT_DIR=D:\07-Logiciels\20-FIN65"
set "MOBILE_DIR=%ROOT_DIR%\fin65_mobile"
set "PUB_CACHE=%ROOT_DIR%\.pub-cache"
echo.
echo ==========================================
echo FIN65 - Fix stabilite biometrie V2.6.2
echo ==========================================
echo.
if not exist "%MOBILE_DIR%" (
echo [ERREUR] Dossier mobile introuvable :
echo %MOBILE_DIR%
pause
exit /b 1
)
if not exist "%MOBILE_DIR%\lib\screens" (
echo [ERREUR] Dossier screens introuvable.
pause
exit /b 1
)
set "BACKUP_DIR=%MOBILE_DIR%\backup_before_biometrics_stability_v262"
mkdir "%BACKUP_DIR%" >nul 2>&1
if exist "%MOBILE_DIR%\lib\screens\security_screen.dart" (
copy /Y "%MOBILE_DIR%\lib\screens\security_screen.dart" "%BACKUP_DIR%\security_screen.dart.bak" >nul
)
if exist "%MOBILE_DIR%\lib\screens\app_lock_gate.dart" (
copy /Y "%MOBILE_DIR%\lib\screens\app_lock_gate.dart" "%BACKUP_DIR%\app_lock_gate.dart.bak" >nul
)
copy /Y "%~dp0lib\screens\security_screen.dart" "%MOBILE_DIR%\lib\screens\security_screen.dart" >nul
copy /Y "%~dp0lib\screens\app_lock_gate.dart" "%MOBILE_DIR%\lib\screens\app_lock_gate.dart" >nul
cd /d "%MOBILE_DIR%"
echo.
echo Nettoyage Flutter...
flutter clean
flutter pub get
echo.
echo ==========================================
echo Fix stabilite biometrie applique.
echo ==========================================
echo.
echo Relance maintenant :
echo flutter run
echo.
pause
@@ -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'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,311 @@
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> _verifyCurrentPinFromField() async {
final current = _currentPinController.text.trim();
final ok = await widget.securityService.verifyPin(current);
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 {
if (_enabled) {
final currentOk = await _verifyCurrentPinFromField();
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 _verifyCurrentPinFromField();
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 (!_enabled) {
setState(() => _message = 'Active d’abord un PIN avant la biométrie.');
return;
}
if (value && !_biometricAvailable) {
setState(() => _message = 'Aucune empreinte/biométrie disponible sur ce téléphone.');
return;
}
final currentOk = await _verifyCurrentPinFromField();
if (!currentOk) return;
await widget.securityService.setBiometricEnabled(value);
_currentPinController.clear();
await _load();
setState(() {
_message = value
? 'Déverrouillage par empreinte activé. Verrouille l’app puis teste avec le bouton empreinte.'
: '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()} Entre le PIN actuel ci-dessous puis active l’interrupteur.'
: '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: 'Nécessaire pour changer le PIN, désactiver le PIN ou activer/désactiver l’empreinte.',
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.',
),
),
),
],
),
);
}
}