Add fingerprint (local_auth) session + 3-min inactivity timeout (default), activity resets timer, settings: biometric toggle + timeout picker

This commit is contained in:
cania
2026-08-12 20:13:57 +02:00
parent 9c3fe4f8e9
commit 68fca2c2c6
12 changed files with 305 additions and 52 deletions

View File

@@ -1,8 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet permission required for Google Sign-In --> <!-- Internet permission required for Google Sign-In -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- Biometric / fingerprint authentication -->
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<queries> <queries>
<!-- Used by Google Sign-In to query browser packages -->
<intent> <intent>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
@@ -31,8 +33,6 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />

View File

@@ -5,7 +5,8 @@ import 'package:login_vault_app/auth_service.dart';
import 'package:login_vault_app/dashboard_screen.dart'; import 'package:login_vault_app/dashboard_screen.dart';
import 'package:login_vault_app/theme_provider.dart'; import 'package:login_vault_app/theme_provider.dart';
/// First-launch screen: register with email+password, or with Google. /// First screen: register, or (if registered) re-authenticate.
/// If a valid session exists, skip straight to the Dashboard.
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
final AuthService auth; final AuthService auth;
const AuthScreen({super.key, required this.auth}); const AuthScreen({super.key, required this.auth});
@@ -17,12 +18,29 @@ class AuthScreen extends StatefulWidget {
class _AuthScreenState extends State<AuthScreen> { class _AuthScreenState extends State<AuthScreen> {
final _emailCtrl = TextEditingController(); final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController(); final _passCtrl = TextEditingController();
bool _isLogin = true; // toggle register/login bool _isLogin = true;
bool _busy = false; bool _busy = false;
String? _error; String? _error;
@override
void initState() {
super.initState();
_trySilentAuth();
}
/// On open: if a valid session is present, go straight to Dashboard.
Future<void> _trySilentAuth() async {
final timeout = await widget.auth.getTimeoutMinutes();
final valid = await widget.auth.hasValidSession(timeout);
if (valid && mounted) {
final email = await widget.auth.getEmail() ?? '';
_goToDashboard(email);
}
}
void _goToDashboard(String email) { void _goToDashboard(String email) {
if (!mounted) return; if (!mounted) return;
widget.auth.openSession(); // refresh session timestamp
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => DashboardScreen( builder: (_) => DashboardScreen(

View File

@@ -1,35 +1,31 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:login_vault_app/constants.dart'; import 'package:login_vault_app/constants.dart';
/// Handles app-level authentication and persistence of the local identity. /// Handles app-level authentication, session, and local identity.
///
/// On first launch the user registers (email+password OR Google). The chosen
/// email is stored locally as the key ([AppConstants.secureKeyEmail]) and the
/// app password (used to lock the dashboard) is stored in secure storage.
class AuthService { class AuthService {
final FlutterSecureStorage _storage = const FlutterSecureStorage(); final FlutterSecureStorage _storage = const FlutterSecureStorage();
final LocalAuthentication _localAuth = LocalAuthentication();
// ---------- Identity (email + app password) ----------
/// Returns the stored email, or null if not registered yet.
Future<String?> getEmail() async => Future<String?> getEmail() async =>
await _storage.read(key: AppConstants.secureKeyEmail); await _storage.read(key: AppConstants.secureKeyEmail);
Future<String?> getAppPassword() async => Future<String?> getAppPassword() async =>
await _storage.read(key: AppConstants.secureKeyAppPassword); await _storage.read(key: AppConstants.secureKeyAppPassword);
/// True if a user identity already exists locally.
Future<bool> isRegistered() async => (await getEmail()) != null; Future<bool> isRegistered() async => (await getEmail()) != null;
/// Register with email + app password.
Future<void> registerWithEmail(String email, String appPassword) async { Future<void> registerWithEmail(String email, String appPassword) async {
await _storage.write(key: AppConstants.secureKeyEmail, value: email); await _storage.write(key: AppConstants.secureKeyEmail, value: email);
await _storage.write( await _storage.write(
key: AppConstants.secureKeyAppPassword, value: appPassword); key: AppConstants.secureKeyAppPassword, value: appPassword);
} }
/// Register with Google (email supplied by Google sign-in).
Future<void> registerWithGoogle(String email) async { Future<void> registerWithGoogle(String email) async {
await _storage.write(key: AppConstants.secureKeyEmail, value: email); await _storage.write(key: AppConstants.secureKeyEmail, value: email);
// App password defaults to a marker; user can change it later in settings.
final existing = await getAppPassword(); final existing = await getAppPassword();
if (existing == null) { if (existing == null) {
await _storage.write( await _storage.write(
@@ -41,27 +37,103 @@ class AuthService {
Future<bool> verifyPassword(String password) async { Future<bool> verifyPassword(String password) async {
final stored = await getAppPassword(); final stored = await getAppPassword();
if (stored == null) return false; if (stored == null) return false;
if (stored == '__google__') { if (stored == '__google__') return true; // google users: any pass ok
// Google users have no password by default; let any entry pass OR force set.
return true;
}
return stored == password; return stored == password;
} }
/// Change the app password (used in settings).
Future<void> changePassword(String newPassword) async { Future<void> changePassword(String newPassword) async {
await _storage.write( await _storage.write(
key: AppConstants.secureKeyAppPassword, value: newPassword); key: AppConstants.secureKeyAppPassword, value: newPassword);
} }
Future<void> logout() async { // ---------- Biometric (fingerprint) ----------
// Keep email (identity) but we treat logout as returning to auth screen
// with the email still known. To fully reset, call [reset]. /// Can the device authenticate with biometrics (fingerprint etc.)?
Future<bool> canUseBiometrics() async {
try {
final supported = await _localAuth.isDeviceSupported();
if (!supported) return false;
final available = await _localAuth.getAvailableBiometrics();
return available.isNotEmpty;
} catch (_) {
return false;
}
} }
/// Wipe local identity (for completeness / debugging). /// Prompt the OS biometric dialog. Returns true on success.
Future<bool> authenticateWithBiometrics() async {
try {
return await _localAuth.authenticate(
localizedReason: 'Gunakan sidik jari untuk membuka Login Vault',
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
} catch (_) {
return false;
}
}
// ---------- Session ----------
/// Persist that a session was just opened (with timestamp).
Future<void> openSession() async {
await _storage.write(
key: AppConstants.secureKeySession, value: DateTime.now().toIso8601String());
}
Future<void> closeSession() async {
await _storage.delete(key: AppConstants.secureKeySession);
}
/// Returns true if a valid, non-expired session exists.
/// [timeoutMinutes] = 0 means "stay logged in until manual logout".
Future<bool> hasValidSession(int timeoutMinutes) async {
if (timeoutMinutes <= 0) {
// never auto-expire; just check a session was opened
final v = await _storage.read(key: AppConstants.secureKeySession);
return v != null;
}
final v = await _storage.read(key: AppConstants.secureKeySession);
if (v == null) return false;
final opened = DateTime.tryParse(v);
if (opened == null) return false;
final diff = DateTime.now().difference(opened).inMinutes;
return diff < timeoutMinutes;
}
// ---------- Timeout setting ----------
/// Get configured session timeout (minutes). Default 3.
Future<int> getTimeoutMinutes() async {
final sp = await _prefs();
return sp.getInt(AppConstants.prefTimeout) ?? 3;
}
Future<void> setTimeoutMinutes(int minutes) async {
final sp = await _prefs();
await sp.setInt(AppConstants.prefTimeout, minutes);
}
// ---------- Biometric toggle setting ----------
Future<bool> isBiometricEnabled() async {
final sp = await _prefs();
return sp.getBool(AppConstants.prefBiometric) ?? false;
}
Future<void> setBiometricEnabled(bool on) async {
final sp = await _prefs();
await sp.setBool(AppConstants.prefBiometric, on);
}
Future<SharedPreferences> _prefs() async =>
await SharedPreferences.getInstance();
Future<void> reset() async { Future<void> reset() async {
await _storage.delete(key: AppConstants.secureKeyEmail); await _storage.delete(key: AppConstants.secureKeyEmail);
await _storage.delete(key: AppConstants.secureKeyAppPassword); await _storage.delete(key: AppConstants.secureKeyAppPassword);
await _storage.delete(key: AppConstants.secureKeySession);
} }
} }

View File

@@ -13,7 +13,12 @@ class AppConstants {
/// SQLite database file name. /// SQLite database file name.
static const String dbName = 'login_vault.db'; static const String dbName = 'login_vault.db';
/// Application package-ish identifiers for secure storage keys. /// Secure storage keys.
static const String secureKeyEmail = 'vault_email'; static const String secureKeyEmail = 'vault_email';
static const String secureKeyAppPassword = 'vault_app_password'; static const String secureKeyAppPassword = 'vault_app_password';
static const String secureKeySession = 'vault_session_opened_at';
/// SharedPreferences keys (settings).
static const String prefTimeout = 'session_timeout_minutes';
static const String prefBiometric = 'biometric_enabled';
} }

View File

@@ -29,17 +29,25 @@ class _DashboardScreenState extends State<DashboardScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_searchCtrl.addListener(_onSearchChanged); _searchCtrl.addListener(() {
_touch(); // user activity
_onSearchChanged();
});
_refresh(); _refresh();
} }
@override @override
void dispose() { void dispose() {
_searchCtrl.removeListener(_onSearchChanged);
_searchCtrl.dispose(); _searchCtrl.dispose();
super.dispose(); super.dispose();
} }
/// Refresh the session timestamp — called on every user interaction so the
/// 3-minute inactivity timeout only fires when the user is truly idle.
void _touch() {
widget.auth.openSession();
}
void _onSearchChanged() => _refresh(); void _onSearchChanged() => _refresh();
Future<void> _refresh() async { Future<void> _refresh() async {
@@ -66,6 +74,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
} }
Future<void> _editEntry(VaultEntry e) async { Future<void> _editEntry(VaultEntry e) async {
_touch();
final result = await showEntryDialog( final result = await showEntryDialog(
context, context,
title: 'Edit konten', title: 'Edit konten',
@@ -80,6 +89,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
} }
Future<void> _addEntry() async { Future<void> _addEntry() async {
_touch();
final result = await showEntryDialog( final result = await showEntryDialog(
context, context,
title: 'Tambah entri baru', title: 'Tambah entri baru',
@@ -110,13 +120,16 @@ class _DashboardScreenState extends State<DashboardScreen> {
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
tooltip: 'Pengaturan', tooltip: 'Pengaturan',
onPressed: () => Navigator.push( onPressed: () {
context, _touch();
MaterialPageRoute( Navigator.push(
builder: (_) => SettingsScreen( context,
auth: widget.auth, theme: widget.theme), MaterialPageRoute(
), builder: (_) => SettingsScreen(
).then((_) => _refresh()), auth: widget.auth, theme: widget.theme),
),
).then((_) => _refresh());
},
), ),
], ],
), ),
@@ -139,6 +152,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
? IconButton( ? IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () { onPressed: () {
_touch();
_searchCtrl.clear(); _searchCtrl.clear();
_refresh(); _refresh();
}, },
@@ -167,20 +181,20 @@ class _DashboardScreenState extends State<DashboardScreen> {
child: const Icon(Icons.delete, color: Colors.white), child: const Icon(Icons.delete, color: Colors.white),
), ),
confirmDismiss: (_) async { confirmDismiss: (_) async {
// Use our own dialog for nicer copy. _touch();
final c = await showConfirmDialog( final c = await showConfirmDialog(
context, context,
title: 'Hapus entri?', title: 'Hapus entri?',
body: body: 'Yakin hapus "${e.id}"?',
'Yakin hapus "${e.id}"?',
); );
if (c == true) await _deleteEntry(e); if (c == true) await _deleteEntry(e);
return false; // we handle deletion ourselves return false;
}, },
child: Card( child: Card(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6), horizontal: 12, vertical: 6),
child: ExpansionTile( child: ExpansionTile(
onExpansionChanged: (_) => _touch(),
title: Text(e.id, title: Text(e.id,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),

View File

@@ -11,26 +11,39 @@ void main() async {
final theme = ThemeProvider(); final theme = ThemeProvider();
final registered = await auth.isRegistered(); final registered = await auth.isRegistered();
final email = await auth.getEmail() ?? ''; final email = await auth.getEmail() ?? '';
final timeout = await auth.getTimeoutMinutes();
final hasSession = await auth.hasValidSession(timeout);
final startAtDashboard = registered && email.isNotEmpty && hasSession;
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
Provider<AuthService>.value(value: auth), Provider<AuthService>.value(value: auth),
ChangeNotifierProvider<ThemeProvider>.value(value: theme), ChangeNotifierProvider<ThemeProvider>.value(value: theme),
], ],
child: MyApp(registered: registered, email: email), child: MyApp(
startAtDashboard: startAtDashboard,
auth: auth,
theme: theme,
email: email,
),
), ),
); );
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
final bool registered; final bool startAtDashboard;
final AuthService auth;
final ThemeProvider theme;
final String email; final String email;
const MyApp({super.key, required this.registered, required this.email}); const MyApp(
{super.key,
required this.startAtDashboard,
required this.auth,
required this.theme,
required this.email});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Provider.of<ThemeProvider>(context);
final auth = Provider.of<AuthService>(context, listen: false);
return MaterialApp( return MaterialApp(
title: 'Login Vault', title: 'Login Vault',
theme: ThemeData( theme: ThemeData(
@@ -44,8 +57,7 @@ class MyApp extends StatelessWidget {
colorSchemeSeed: Colors.indigo, colorSchemeSeed: Colors.indigo,
), ),
themeMode: theme.mode, themeMode: theme.mode,
// decide home: if registered AND email known -> Dashboard, else Auth. home: startAtDashboard
home: (registered && email.isNotEmpty)
? DashboardScreen(auth: auth, theme: theme, email: email) ? DashboardScreen(auth: auth, theme: theme, email: email)
: AuthScreen(auth: auth), : AuthScreen(auth: auth),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@@ -4,7 +4,8 @@ import 'package:flutter/services.dart';
import 'package:login_vault_app/auth_service.dart'; import 'package:login_vault_app/auth_service.dart';
import 'package:login_vault_app/theme_provider.dart'; import 'package:login_vault_app/theme_provider.dart';
/// Settings: change app password, switch theme (dark/system), and Exit. /// Settings: change app password, switch theme (dark/system), session timeout,
/// fingerprint toggle, and Exit.
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
final AuthService auth; final AuthService auth;
final ThemeProvider theme; final ThemeProvider theme;
@@ -18,6 +19,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _oldCtrl = TextEditingController(); final _oldCtrl = TextEditingController();
final _newCtrl = TextEditingController(); final _newCtrl = TextEditingController();
String? _msg; String? _msg;
bool _biometricOn = false;
bool _biometricAvailable = false;
int _timeout = 3;
final List<int> _timeoutOptions = [3, 5, 10, 15, 30, 60, 0];
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final on = await widget.auth.isBiometricEnabled();
final avail = await widget.auth.canUseBiometrics();
final t = await widget.auth.getTimeoutMinutes();
if (mounted) {
setState(() {
_biometricOn = on && avail;
_biometricAvailable = avail;
_timeout = t;
});
}
}
Future<void> _changePassword() async { Future<void> _changePassword() async {
final oldP = _oldCtrl.text; final oldP = _oldCtrl.text;
@@ -37,6 +62,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
_newCtrl.clear(); _newCtrl.clear();
} }
Future<void> _toggleBiometric(bool value) async {
if (value && !_biometricAvailable) {
setState(() => _msg = 'Perangkat tidak mendukung sidik jari');
return;
}
if (value) {
final ok = await widget.auth.authenticateWithBiometrics();
if (!ok) {
setState(() => _msg = 'Verifikasi sidik jari gagal');
return;
}
}
await widget.auth.setBiometricEnabled(value);
if (mounted) setState(() => _biometricOn = value);
}
Future<void> _setTimeout(int? value) async {
if (value == null) return;
await widget.auth.setTimeoutMinutes(value);
if (mounted) setState(() => _timeout = value);
widget.auth.openSession(); // activity
}
String _timeoutLabel(int m) => m == 0 ? 'Selamanya' : '$m menit';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -44,8 +94,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
body: ListView( body: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
const Text('Tema', const Text('Tema', style: TextStyle(fontWeight: FontWeight.bold)),
style: TextStyle(fontWeight: FontWeight.bold)),
ListTile( ListTile(
title: const Text('Gelap (Dark)'), title: const Text('Gelap (Dark)'),
leading: const Icon(Icons.dark_mode), leading: const Icon(Icons.dark_mode),
@@ -67,6 +116,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => widget.theme.setMode(ThemeMode.system), onTap: () => widget.theme.setMode(ThemeMode.system),
), ),
const Divider(), const Divider(),
const Text('Session', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Timeout (tanpa aktivitas)'),
subtitle: Text('Logout otomatis setelah: ${_timeoutLabel(_timeout)}'),
trailing: DropdownButton<int>(
value: _timeout,
items: _timeoutOptions
.map((m) => DropdownMenuItem(value: m, child: Text(_timeoutLabel(m))))
.toList(),
onChanged: _setTimeout,
),
),
SwitchListTile(
title: const Text('Login dengan sidik jari'),
subtitle: Text(_biometricAvailable
? 'Gunakan fingerprint untuk membuka app'
: 'Perangkat tidak mendukung biometrik'),
value: _biometricOn,
onChanged: _toggleBiometric,
),
const Divider(),
const Text('Ubah Password Aplikasi', const Text('Ubah Password Aplikasi',
style: TextStyle(fontWeight: FontWeight.bold)), style: TextStyle(fontWeight: FontWeight.bold)),
TextField( TextField(
@@ -87,15 +157,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_msg != null) if (_msg != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(_msg!, child: Text(_msg!, style: const TextStyle(color: Colors.green)),
style: const TextStyle(color: Colors.green)),
), ),
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.exit_to_app), leading: const Icon(Icons.exit_to_app),
title: const Text('Keluar (Exit)'), title: const Text('Keluar (Exit)'),
onTap: () { onTap: () async {
// On Android, pop the whole stack then ask the system to close. await widget.auth.closeSession();
Navigator.of(context).popUntil((r) => r.isFirst); Navigator.of(context).popUntil((r) => r.isFirst);
if (Platform.isAndroid) { if (Platform.isAndroid) {
SystemNavigator.pop(); SystemNavigator.pop();

View File

@@ -7,12 +7,14 @@ import Foundation
import flutter_secure_storage_macos import flutter_secure_storage_macos
import google_sign_in_ios import google_sign_in_ios
import local_auth_darwin
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

View File

@@ -102,6 +102,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -232,6 +240,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
intl:
dependency: transitive
description:
name: intl
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev"
source: hosted
version: "0.20.3"
jni: jni:
dependency: transitive dependency: transitive
description: description:
@@ -296,6 +312,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.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:

View File

@@ -16,6 +16,7 @@ dependencies:
google_sign_in: ^6.2.1 google_sign_in: ^6.2.1
provider: ^6.1.2 provider: ^6.1.2
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
local_auth: ^2.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
} }

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows flutter_secure_storage_windows
local_auth_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST