319 lines
10 KiB
Dart
319 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../core/theme.dart';
|
|
import '../../services/biometric_service.dart';
|
|
|
|
/// Settings screen with biometric authentication configuration
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
final BiometricService _biometricService = BiometricService();
|
|
|
|
bool _biometricsEnabled = false;
|
|
bool _biometricsAvailable = false;
|
|
LockPolicy _lockPolicy = LockPolicy.afterInactive;
|
|
String _biometricTypeName = 'Biometrics';
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSettings();
|
|
}
|
|
|
|
Future<void> _loadSettings() async {
|
|
final available = await _biometricService.isBiometricsAvailable();
|
|
final enabled = await _biometricService.isBiometricEnabled();
|
|
final policy = await _biometricService.getLockPolicy();
|
|
final typeName = await _biometricService.getBiometricTypeName();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_biometricsAvailable = available;
|
|
_biometricsEnabled = enabled;
|
|
_lockPolicy = policy;
|
|
_biometricTypeName = typeName;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _toggleBiometrics(bool value) async {
|
|
if (value) {
|
|
// Verify biometrics work before enabling
|
|
final result = await _biometricService.authenticate(
|
|
reason: 'Verify biometrics to enable',
|
|
);
|
|
|
|
if (result != BiometricResult.success) {
|
|
if (mounted) {
|
|
_showError(_biometricService.getErrorMessage(result));
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
await _biometricService.setBiometricEnabled(value);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_biometricsEnabled = value;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _changeLockPolicy(LockPolicy? policy) async {
|
|
if (policy == null) return;
|
|
|
|
await _biometricService.setLockPolicy(policy);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_lockPolicy = policy;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _showError(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: Colors.red.shade700,
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
|
|
String _policyDisplayName(LockPolicy policy) {
|
|
switch (policy) {
|
|
case LockPolicy.always:
|
|
return 'Always';
|
|
case LockPolicy.afterInactive:
|
|
return 'After 5 minutes inactive';
|
|
case LockPolicy.never:
|
|
return 'Never';
|
|
}
|
|
}
|
|
|
|
String _policyDescription(LockPolicy policy) {
|
|
switch (policy) {
|
|
case LockPolicy.always:
|
|
return 'Require authentication every time you open the app';
|
|
case LockPolicy.afterInactive:
|
|
return 'Require authentication after 5 minutes of inactivity';
|
|
case LockPolicy.never:
|
|
return 'Never require authentication (not recommended)';
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Settings'),
|
|
centerTitle: true,
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: ListView(
|
|
children: [
|
|
_buildSection(
|
|
title: 'Security',
|
|
children: [
|
|
// Biometric toggle
|
|
SwitchListTile(
|
|
title: Text('$_biometricTypeName Authentication'),
|
|
subtitle: Text(
|
|
_biometricsAvailable
|
|
? 'Use $_biometricTypeName to unlock the app'
|
|
: 'Not available on this device',
|
|
),
|
|
value: _biometricsEnabled && _biometricsAvailable,
|
|
onChanged: _biometricsAvailable ? _toggleBiometrics : null,
|
|
activeColor: AppTheme.primaryColor,
|
|
secondary: Icon(
|
|
_biometricsAvailable
|
|
? Icons.fingerprint
|
|
: Icons.no_encryption,
|
|
color: _biometricsAvailable
|
|
? AppTheme.primaryColor
|
|
: Colors.grey,
|
|
),
|
|
),
|
|
|
|
// Lock policy (only shown if biometrics enabled)
|
|
if (_biometricsEnabled && _biometricsAvailable) ...[
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
leading: const Icon(Icons.lock_clock),
|
|
title: const Text('Lock Timing'),
|
|
subtitle: Text(_policyDisplayName(_lockPolicy)),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () => _showLockPolicyDialog(),
|
|
),
|
|
],
|
|
|
|
// Biometrics not enrolled warning
|
|
if (!_biometricsAvailable) ...[
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
leading: Icon(
|
|
Icons.warning_amber,
|
|
color: Colors.orange.shade400,
|
|
),
|
|
title: const Text('Set Up Biometrics'),
|
|
subtitle: const Text(
|
|
'Enable Face ID, Touch ID, or fingerprint in your device settings',
|
|
),
|
|
onTap: () {
|
|
_showBiometricsSetupInfo();
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
_buildSection(
|
|
title: 'Input',
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.camera_alt),
|
|
title: const Text('Camera Permissions'),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () {
|
|
// TODO: Open camera permissions
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.mic),
|
|
title: const Text('Microphone Permissions'),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () {
|
|
// TODO: Open microphone permissions
|
|
},
|
|
),
|
|
],
|
|
),
|
|
_buildSection(
|
|
title: 'About',
|
|
children: [
|
|
const ListTile(
|
|
leading: Icon(Icons.info_outline),
|
|
title: Text('Version'),
|
|
trailing: Text('1.0.0'),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.code),
|
|
title: const Text('Open Source Licenses'),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () {
|
|
showLicensePage(context: context);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showLockPolicyDialog() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
backgroundColor: AppTheme.surfaceColor,
|
|
title: const Text('Lock Timing'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: LockPolicy.values.map((policy) {
|
|
return RadioListTile<LockPolicy>(
|
|
title: Text(_policyDisplayName(policy)),
|
|
subtitle: Text(
|
|
_policyDescription(policy),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.textColor.withOpacity(0.7),
|
|
),
|
|
),
|
|
value: policy,
|
|
groupValue: _lockPolicy,
|
|
activeColor: AppTheme.primaryColor,
|
|
onChanged: (value) {
|
|
_changeLockPolicy(value);
|
|
Navigator.of(context).pop();
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showBiometricsSetupInfo() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
backgroundColor: AppTheme.surfaceColor,
|
|
title: const Text('Set Up Biometrics'),
|
|
content: const Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('To use biometric authentication:'),
|
|
SizedBox(height: 16),
|
|
Text('iOS:'),
|
|
Text('Settings → Face ID & Passcode (or Touch ID)'),
|
|
SizedBox(height: 12),
|
|
Text('Android:'),
|
|
Text('Settings → Security → Fingerprint / Face unlock'),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'After setting up biometrics on your device, return to this app to enable authentication.',
|
|
style: TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Got it'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSection({
|
|
required String title,
|
|
required List<Widget> children,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
color: AppTheme.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
...children,
|
|
const Divider(),
|
|
],
|
|
);
|
|
}
|
|
}
|