400 lines
12 KiB
Plaintext
400 lines
12 KiB
Plaintext
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.',
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|