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 activation empreinte V2.6.1
Ce patch corrige l'écran Sécurité.
## Problème
L'écran affichait :
```text
PIN actuel incorrect.
```
quand on essayait d'activer l'empreinte, parce que l'ancien écran utilisait le champ `PIN actuel` visible dans la page. C'était peu clair, donc naturellement ça a créé une erreur inutile. L'espèce humaine et les interfaces mal pensées, une vieille histoire.
## Correction
Maintenant, quand tu actives l'empreinte :
1. tu appuies sur l'interrupteur `Déverrouillage par empreinte` ;
2. une boîte de dialogue demande le PIN actuel ;
3. Android demande l'empreinte ;
4. si l'empreinte est validée, l'option est activée.
Le champ `PIN actuel` sur la page sert maintenant surtout à changer le PIN.
## Installation
Décompresse puis lance :
```powershell
.\apply_biometric_activation_ui_fix.bat
```
Ensuite :
```powershell
cd D:\07-Logiciels\20-FIN65\fin65_mobile
flutter run
```
## Test
1. Ouvre `Paramètres > Sécurité`.
2. Active `Déverrouillage par empreinte`.
3. Entre le PIN actuel dans la boîte de dialogue.
4. Pose le doigt quand Android le demande.
@@ -0,0 +1,62 @@
@echo off
setlocal
REM ============================================================
REM FIN65 - Fix activation empreinte V2.6.1
REM ============================================================
set "ROOT_DIR=D:\07-Logiciels\20-FIN65"
set "MOBILE_DIR=%ROOT_DIR%\fin65_mobile"
set "PUB_CACHE=%ROOT_DIR%\.pub-cache"
set "TARGET=%MOBILE_DIR%\lib\screens\security_screen.dart"
echo.
echo ==========================================
echo FIN65 - Fix activation empreinte V2.6.1
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 :
echo %MOBILE_DIR%\lib\screens
pause
exit /b 1
)
for /f "tokens=1-4 delims=/ " %%a in ("%date%") do set DATESTAMP=%%d%%b%%c
set TIMESTAMP=%time::=%
set TIMESTAMP=%TIMESTAMP: =0%
set BACKUP=%TARGET%.before_biometric_ui_fix_%DATESTAMP%_%TIMESTAMP%.bak
if exist "%TARGET%" (
copy /Y "%TARGET%" "%BACKUP%" >nul
echo Sauvegarde :
echo %BACKUP%
echo.
)
copy /Y "%~dp0lib\screens\security_screen.dart" "%TARGET%" >nul
cd /d "%MOBILE_DIR%"
echo.
echo Nettoyage Flutter...
flutter clean
flutter pub get
echo.
echo ==========================================
echo Fix activation empreinte applique.
echo ==========================================
echo.
echo Relance maintenant :
echo flutter run
echo.
pause
@@ -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.',
),
),
),
],
),
);
}
}