This commit is contained in:
2026-03-20 20:50:26 +03:00
parent 50a34bf277
commit e2c6e38560
10 changed files with 213 additions and 3 deletions
+2
View File
@@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<application <application
android:label="Casha" android:label="Casha"
android:name="${applicationName}" android:name="${applicationName}"
@@ -1,5 +1,5 @@
package com.example.casha package com.example.casha
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterActivity() class MainActivity : FlutterFragmentActivity()
+37
View File
@@ -0,0 +1,37 @@
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
class BiometricService {
static final _auth = LocalAuthentication();
static const _key = 'biometric_enabled';
static Future<bool> isAvailable() async {
final canCheck = await _auth.canCheckBiometrics;
final isSupported = await _auth.isDeviceSupported();
return canCheck && isSupported;
}
static Future<bool> isEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_key) ?? false;
}
static Future<void> setEnabled(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_key, value);
}
static Future<bool> authenticate() async {
try {
return await _auth.authenticate(
localizedReason: 'Confirm your identity to open Casha',
options: const AuthenticationOptions(
biometricOnly: false,
stickyAuth: true,
),
);
} catch (_) {
return false;
}
}
}
+106
View File
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../core/constants.dart'; import '../../core/constants.dart';
import '../../core/services/biometric_service.dart';
import '../../shared/utils/currency_utils.dart'; import '../../shared/utils/currency_utils.dart';
import '../../shared/providers/amount_format_provider.dart'; import '../../shared/providers/amount_format_provider.dart';
import '../dashboard/provider.dart'; import '../dashboard/provider.dart';
@@ -387,6 +388,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const _BiometricSection(),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -543,3 +546,106 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
); );
} }
} }
class _BiometricSection extends StatefulWidget {
const _BiometricSection();
@override
State<_BiometricSection> createState() => _BiometricSectionState();
}
class _BiometricSectionState extends State<_BiometricSection> {
bool _available = false;
bool _enabled = false;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final available = await BiometricService.isAvailable();
final enabled = await BiometricService.isEnabled();
if (mounted) {
setState(() {
_available = available;
_enabled = enabled;
_loading = false;
});
}
}
Future<void> _onToggle(bool val) async {
if (val) {
final ok = await BiometricService.authenticate();
if (!ok) return;
}
await BiometricService.setEnabled(val);
if (mounted) setState(() => _enabled = val);
}
@override
Widget build(BuildContext context) {
if (_loading || !_available) return const SizedBox.shrink();
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.accent.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.fingerprint,
color: AppColors.accent,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biometric Lock',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
'Require fingerprint on app launch',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
Switch(
value: _enabled,
onChanged: _onToggle,
activeColor: const Color(0xFF7C6DED),
),
],
),
),
const SizedBox(height: 16),
],
);
}
}
+11 -1
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../core/services/biometric_service.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@@ -28,9 +30,17 @@ class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderSt
_controller.forward(); _controller.forward();
_controller.addStatusListener((status) { _controller.addStatusListener((status) async {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
if (mounted) { if (mounted) {
final isEnabled = await BiometricService.isEnabled();
if (isEnabled) {
final authenticated = await BiometricService.authenticate();
if (!authenticated) {
SystemNavigator.pop();
return;
}
}
context.go('/dashboard'); context.go('/dashboard');
} }
} }
@@ -5,8 +5,10 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import local_auth_darwin
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }
+48
View File
@@ -166,6 +166,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_riverpod: flutter_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -288,6 +296,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
url: "https://pub.dev"
source: hosted
version: "1.0.56"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev"
source: hosted
version: "1.6.1"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
url: "https://pub.dev"
source: hosted
version: "1.1.0"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
logging: logging:
dependency: transitive dependency: transitive
description: description:
+1
View File
@@ -20,6 +20,7 @@ dependencies:
path_provider: ^2.1.5 path_provider: ^2.1.5
http: ^1.2.0 http: ^1.2.0
sensors_plus: ^6.1.0 sensors_plus: ^6.1.0
local_auth: ^2.3.0
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <local_auth_windows/local_auth_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
} }
+1
View File
@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
local_auth_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST