From e2c6e385603701d826b5f4e2c371f54773bd30f4 Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 20 Mar 2026 20:50:26 +0300 Subject: [PATCH] update --- android/app/src/main/AndroidManifest.xml | 2 + .../com/example/first_attempt/MainActivity.kt | 4 +- lib/core/services/biometric_service.dart | 37 ++++++ lib/features/settings/screen.dart | 106 ++++++++++++++++++ lib/features/splash/screen.dart | 12 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 48 ++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 lib/core/services/biometric_service.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9f48c61..b8e3107 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + isAvailable() async { + final canCheck = await _auth.canCheckBiometrics; + final isSupported = await _auth.isDeviceSupported(); + return canCheck && isSupported; + } + + static Future isEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_key) ?? false; + } + + static Future setEnabled(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_key, value); + } + + static Future authenticate() async { + try { + return await _auth.authenticate( + localizedReason: 'Confirm your identity to open Casha', + options: const AuthenticationOptions( + biometricOnly: false, + stickyAuth: true, + ), + ); + } catch (_) { + return false; + } + } +} diff --git a/lib/features/settings/screen.dart b/lib/features/settings/screen.dart index 37d201f..be646ee 100644 --- a/lib/features/settings/screen.dart +++ b/lib/features/settings/screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../core/constants.dart'; +import '../../core/services/biometric_service.dart'; import '../../shared/utils/currency_utils.dart'; import '../../shared/providers/amount_format_provider.dart'; import '../dashboard/provider.dart'; @@ -387,6 +388,8 @@ class _SettingsScreenState extends ConsumerState { ), const SizedBox(height: 16), + const _BiometricSection(), + Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -543,3 +546,106 @@ class _SettingsScreenState extends ConsumerState { ); } } + +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 _load() async { + final available = await BiometricService.isAvailable(); + final enabled = await BiometricService.isEnabled(); + if (mounted) { + setState(() { + _available = available; + _enabled = enabled; + _loading = false; + }); + } + } + + Future _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), + ], + ); + } +} diff --git a/lib/features/splash/screen.dart b/lib/features/splash/screen.dart index 9c399bd..9695fea 100644 --- a/lib/features/splash/screen.dart +++ b/lib/features/splash/screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../core/services/biometric_service.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -28,9 +30,17 @@ class _SplashScreenState extends State with SingleTickerProviderSt _controller.forward(); - _controller.addStatusListener((status) { + _controller.addStatusListener((status) async { if (status == AnimationStatus.completed) { if (mounted) { + final isEnabled = await BiometricService.isEnabled(); + if (isEnabled) { + final authenticated = await BiometricService.authenticate(); + if (!authenticated) { + SystemNavigator.pop(); + return; + } + } context.go('/dashboard'); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..80dc39e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import local_auth_darwin import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index f3b2c51..44c5a8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -166,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -288,6 +296,46 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2f7308..c873cc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: path_provider: ^2.1.5 http: ^1.2.0 sensors_plus: ^6.1.0 + local_auth: ^2.3.0 flutter_launcher_icons: android: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..7407ddd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..ef187dc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + local_auth_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST