Initial upload: fin65-mobile
This commit is contained in:
@@ -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.',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user