Initial upload: fin65-mobile
This commit is contained in:
@@ -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