diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart
index e3bfddeb..4d29426f 100644
--- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart
+++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart
@@ -164,6 +164,12 @@ abstract class S {
/// **'Attention'**
String get attention;
+ /// No description provided for @authRequired.
+ ///
+ /// In en, this message translates to:
+ /// **'Auth required'**
+ String get authRequired;
+
/// No description provided for @auto.
///
/// In en, this message translates to:
@@ -218,6 +224,12 @@ abstract class S {
/// **'Run in backgroud'**
String get bgRun;
+ /// No description provided for @bioAuth.
+ ///
+ /// In en, this message translates to:
+ /// **'Biometric auth'**
+ String get bioAuth;
+
/// No description provided for @canPullRefresh.
///
/// In en, this message translates to:
diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart
index 9b2076f3..e3c1007f 100644
--- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart
+++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart
@@ -37,6 +37,9 @@ class SDe extends S {
@override
String get attention => 'Achtung';
+ @override
+ String get authRequired => 'Autorisierung erforderlich';
+
@override
String get auto => 'System folgen';
@@ -64,6 +67,9 @@ class SDe extends S {
@override
String get bgRun => 'Hintergrundaktualisierung';
+ @override
+ String get bioAuth => 'Biozertifizierung';
+
@override
String get canPullRefresh => 'Danach: herunterziehen zum Aktualisieren';
diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart
index 2437f210..e07dd5c1 100644
--- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart
+++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart
@@ -37,6 +37,9 @@ class SEn extends S {
@override
String get attention => 'Attention';
+ @override
+ String get authRequired => 'Auth required';
+
@override
String get auto => 'Auto';
@@ -64,6 +67,9 @@ class SEn extends S {
@override
String get bgRun => 'Run in backgroud';
+ @override
+ String get bioAuth => 'Biometric auth';
+
@override
String get canPullRefresh => 'You can pull to refresh.';
diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart
index 15329c1e..0d359e28 100644
--- a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart
+++ b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart
@@ -37,6 +37,9 @@ class SId extends S {
@override
String get attention => 'Perhatian';
+ @override
+ String get authRequired => 'Auth diperlukan';
+
@override
String get auto => 'Auto';
@@ -64,6 +67,9 @@ class SId extends S {
@override
String get bgRun => 'Jalankan di Backgroud';
+ @override
+ String get bioAuth => 'Biosertifikasi';
+
@override
String get canPullRefresh => 'Anda dapat menarik untuk menyegarkan.';
diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart
index 1c0fd550..c5b915f0 100644
--- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart
+++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart
@@ -37,6 +37,9 @@ class SZh extends S {
@override
String get attention => '注意';
+ @override
+ String get authRequired => '需要认证';
+
@override
String get auto => '自动';
@@ -64,6 +67,9 @@ class SZh extends S {
@override
String get bgRun => '后台运行';
+ @override
+ String get bioAuth => '生物认证';
+
@override
String get canPullRefresh => '可以下拉刷新';
@@ -772,6 +778,9 @@ class SZhTw extends SZh {
@override
String get attention => '注意';
+ @override
+ String get authRequired => '需要認證';
+
@override
String get auto => '自動';
@@ -799,6 +808,9 @@ class SZhTw extends SZh {
@override
String get bgRun => '背景運行';
+ @override
+ String get bioAuth => '生物認證';
+
@override
String get canPullRefresh => '可以下拉更新';
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a034f105..d23f7a33 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
UIViewControllerBasedStatusBarAppearance
+ NSFaceIDUsageDescription
+ Required for auth
diff --git a/ios/Runner/Info-Profile.plist b/ios/Runner/Info-Profile.plist
index 0de86afe..c39cec26 100644
--- a/ios/Runner/Info-Profile.plist
+++ b/ios/Runner/Info-Profile.plist
@@ -64,5 +64,7 @@
NSLocalNetworkUsageDescription
ServerBox needs to access your local network to discover and connect to your server.
+ NSFaceIDUsageDescription
+ Required for auth
diff --git a/ios/Runner/Info-Release.plist b/ios/Runner/Info-Release.plist
index 4e13b6d1..cba4fe0c 100644
--- a/ios/Runner/Info-Release.plist
+++ b/ios/Runner/Info-Release.plist
@@ -58,5 +58,7 @@
UIViewControllerBasedStatusBarAppearance
+ NSFaceIDUsageDescription
+ Required for auth
diff --git a/lib/core/utils/platform/auth.dart b/lib/core/utils/platform/auth.dart
new file mode 100644
index 00000000..22a05a1d
--- /dev/null
+++ b/lib/core/utils/platform/auth.dart
@@ -0,0 +1,63 @@
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:local_auth/local_auth.dart';
+import 'package:toolbox/core/utils/platform/base.dart';
+import 'package:local_auth/error_codes.dart' as errs;
+
+class BioAuth {
+ const BioAuth._();
+
+ static final _auth = LocalAuthentication();
+
+ static bool get isPlatformSupported => isAndroid || isIOS || isWindows;
+
+ static Future get isAvail async {
+ if (!isPlatformSupported) return false;
+ final canCheckBiometrics = await _auth.canCheckBiometrics;
+ if (!canCheckBiometrics) {
+ return false;
+ }
+ final biometrics = await _auth.getAvailableBiometrics();
+ if (biometrics.isEmpty) return false;
+ return biometrics.contains(BiometricType.fingerprint) ||
+ biometrics.contains(BiometricType.face);
+ }
+
+ static Future auth([String? msg]) async {
+ if (!await isAvail) return AuthResult.notAvail;
+ try {
+ final reuslt = await _auth.authenticate(
+ localizedReason: msg ?? 'Auth required',
+ options: const AuthenticationOptions(
+ stickyAuth: true,
+ sensitiveTransaction: true,
+ biometricOnly: true,
+ ),
+ );
+ if (reuslt) {
+ return AuthResult.success;
+ }
+ return AuthResult.fail;
+ } on PlatformException catch (e) {
+ switch (e.code) {
+ case errs.notEnrolled:
+ return AuthResult.notAvail;
+ case errs.lockedOut:
+ case errs.permanentlyLockedOut:
+ exit(0);
+ }
+ return AuthResult.cancel;
+ }
+ }
+}
+
+enum AuthResult {
+ success,
+ // Not match
+ fail,
+ // User cancel
+ cancel,
+ // Device doesn't support biometrics
+ notAvail,
+}
diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart
index db289d00..14da1090 100644
--- a/lib/data/store/setting.dart
+++ b/lib/data/store/setting.dart
@@ -205,6 +205,13 @@ class SettingStore extends PersistentStore {
false,
);
+ /// Only valid on iOS / Android
+ late final useBioAuth = StoreProperty(
+ box,
+ 'useBioAuth',
+ false,
+ );
+
// Never show these settings for users
// Guide for these settings:
// - key should start with `_` and be shorter as possible
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 2480187a..c7cbe3b7 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -11,6 +11,7 @@
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
"alterUrl": "Url ändern",
"attention": "Achtung",
+ "authRequired": "Autorisierung erforderlich",
"auto": "System folgen",
"autoCheckUpdate": "Aktualisierung automatisch prüfen",
"autoConnect": "Automatisch verbinden",
@@ -20,6 +21,7 @@
"backupTip": "Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.",
"backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.",
"bgRun": "Hintergrundaktualisierung",
+ "bioAuth": "Biozertifizierung",
"canPullRefresh": "Danach: herunterziehen zum Aktualisieren",
"cancel": "Abbrechen",
"choose": "Auswählen",
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 631a71ec..4409bf1f 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -11,6 +11,7 @@
"alreadyLastDir": "Already in last directory.",
"alterUrl": "Alter url",
"attention": "Attention",
+ "authRequired": "Auth required",
"auto": "Auto",
"autoCheckUpdate": "Auto check update",
"autoConnect": "Auto connect",
@@ -20,6 +21,7 @@
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.",
"backupVersionNotMatch": "Backup version is not match.",
"bgRun": "Run in backgroud",
+ "bioAuth": "Biometric auth",
"canPullRefresh": "You can pull to refresh.",
"cancel": "Cancel",
"choose": "Choose",
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 6b72da60..ef134fd7 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -11,6 +11,7 @@
"alreadyLastDir": "Sudah di direktori terakhir.",
"alterUrl": "Alter url",
"attention": "Perhatian",
+ "authRequired": "Auth diperlukan",
"auto": "Auto",
"autoCheckUpdate": "Periksa pembaruan otomatis",
"autoConnect": "Hubungkan otomatis",
@@ -20,6 +21,7 @@
"backupTip": "Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.",
"backupVersionNotMatch": "Versi cadangan tidak cocok.",
"bgRun": "Jalankan di Backgroud",
+ "bioAuth": "Biosertifikasi",
"canPullRefresh": "Anda dapat menarik untuk menyegarkan.",
"cancel": "Membatalkan",
"choose": "Memilih",
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 02c24f74..11e654ae 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -11,6 +11,7 @@
"alreadyLastDir": "已经是最上层目录了",
"alterUrl": "备选链接",
"attention": "注意",
+ "authRequired": "需要认证",
"auto": "自动",
"autoCheckUpdate": "自动检查更新",
"autoConnect": "自动连接",
@@ -20,6 +21,7 @@
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"bgRun": "后台运行",
+ "bioAuth": "生物认证",
"canPullRefresh": "可以下拉刷新",
"cancel": "取消",
"choose": "选择",
diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb
index e98681d0..8d83ad2a 100644
--- a/lib/l10n/app_zh_tw.arb
+++ b/lib/l10n/app_zh_tw.arb
@@ -11,6 +11,7 @@
"alreadyLastDir": "已經是最上層目錄了",
"alterUrl": "備選鏈接",
"attention": "注意",
+ "authRequired": "需要認證",
"auto": "自動",
"autoCheckUpdate": "自動檢查更新",
"autoConnect": "自動連接",
@@ -20,6 +21,7 @@
"backupTip": "導出的數據僅進行了簡單加密,請妥善保管。",
"backupVersionNotMatch": "備份版本不匹配,無法還原",
"bgRun": "背景運行",
+ "bioAuth": "生物認證",
"canPullRefresh": "可以下拉更新",
"cancel": "取消",
"choose": "選擇",
diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart
index 723662d1..c419c792 100644
--- a/lib/view/page/home.dart
+++ b/lib/view/page/home.dart
@@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart';
import 'package:toolbox/core/channel/bg_run.dart';
import 'package:toolbox/core/channel/home_widget.dart';
import 'package:toolbox/core/extension/context/dialog.dart';
+import 'package:toolbox/core/utils/platform/auth.dart';
import 'package:toolbox/core/utils/platform/base.dart';
import 'package:toolbox/data/res/github_id.dart';
import 'package:toolbox/data/res/logger.dart';
@@ -46,6 +47,7 @@ class _HomePageState extends State
late S _s;
bool _switchingPage = false;
+ bool _isAuthing = false;
@override
void initState() {
@@ -81,6 +83,7 @@ class _HomePageState extends State
switch (state) {
case AppLifecycleState.resumed:
+ _auth();
if (!Providers.server.isAutoRefreshOn) {
Providers.server.startAutoRefresh();
}
@@ -338,6 +341,8 @@ class _HomePageState extends State
@override
Future afterFirstLayout(BuildContext context) async {
+ // Auth required for first launch
+ _auth();
if (Stores.setting.autoCheckAppUpdate.fetch()) {
doUpdate(context);
}
@@ -385,4 +390,32 @@ class _HomePageState extends State
Loggers.app.warning('Update json settings failed', e, trace);
}
}
+
+ void _auth() {
+ if (Stores.setting.useBioAuth.fetch()) {
+ if (!_isAuthing) {
+ _isAuthing = true;
+ BioAuth.auth(_s.authRequired).then(
+ (val) {
+ switch (val) {
+ case AuthResult.success:
+ // wait for animation
+ Future.delayed(
+ const Duration(seconds: 1), () => _isAuthing = false);
+ break;
+ case AuthResult.fail:
+ case AuthResult.cancel:
+ _isAuthing = false;
+ _auth();
+ break;
+ case AuthResult.notAvail:
+ _isAuthing = false;
+ Stores.setting.useBioAuth.put(false);
+ break;
+ }
+ },
+ );
+ }
+ }
+ }
}
diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart
index d7b81f76..5d8637ab 100644
--- a/lib/view/page/setting/entry.dart
+++ b/lib/view/page/setting/entry.dart
@@ -12,6 +12,7 @@ import 'package:toolbox/core/extension/context/snackbar.dart';
import 'package:toolbox/core/extension/locale.dart';
import 'package:toolbox/core/extension/context/dialog.dart';
import 'package:toolbox/core/extension/stringx.dart';
+import 'package:toolbox/core/utils/platform/auth.dart';
import 'package:toolbox/core/utils/platform/base.dart';
import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart';
@@ -183,13 +184,16 @@ class _SettingPageState extends State {
//_buildLaunchPage(),
_buildCheckUpdate(),
];
+ if (isAndroid) {
+ children.add(_buildBgRun());
+ children.add(_buildAndroidWidgetSharedPreference());
+ }
if (isIOS) {
children.add(_buildPushToken());
children.add(_buildAutoUpdateHomeWidget());
}
- if (isAndroid) {
- children.add(_buildBgRun());
- children.add(_buildAndroidWidgetSharedPreference());
+ if (BioAuth.isPlatformSupported) {
+ children.add(_buildBioAuth());
}
return Column(
children: children.map((e) => RoundRectCard(e)).toList(),
@@ -1096,4 +1100,41 @@ class _SettingPageState extends State {
// trailing: StoreSwitch(prop: _setting.doubleColumnServersPage),
// );
// }
+
+ Widget _buildBioAuth() {
+ return FutureWidget(
+ future: BioAuth.isAvail,
+ loading: ListTile(
+ title: Text(_s.bioAuth),
+ subtitle: Text(_s.serverTabLoading, style: UIs.textGrey),
+ ),
+ error: (e, __) => ListTile(
+ title: Text(_s.bioAuth),
+ subtitle: Text('${_s.failed}: $e', style: UIs.textGrey),
+ ),
+ success: (can) {
+ return ListTile(
+ title: Text(_s.bioAuth),
+ trailing: can
+ ? StoreSwitch(
+ prop: Stores.setting.useBioAuth,
+ func: (val) async {
+ if (val) {
+ Stores.setting.useBioAuth.put(false);
+ return;
+ }
+ // Only auth when turn off (val == false)
+ final result = await BioAuth.auth(_s.authRequired);
+ // If failed, turn on again
+ if (result != AuthResult.success) {
+ Stores.setting.useBioAuth.put(true);
+ }
+ },
+ )
+ : Text(_s.error, style: UIs.textGrey),
+ );
+ },
+ noData: UIs.placeholder,
+ );
+ }
}
diff --git a/lib/view/widget/future_widget.dart b/lib/view/widget/future_widget.dart
index d3fe30c4..72d87f06 100644
--- a/lib/view/widget/future_widget.dart
+++ b/lib/view/widget/future_widget.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
class FutureWidget extends StatelessWidget {
- final Future future;
+ final Future future;
final Widget loading;
final Widget Function(Object? error, StackTrace? trace) error;
final Widget Function(T data) success;
diff --git a/pubspec.lock b/pubspec.lock
index c5fe884e..b32c45bb 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -518,6 +518,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
+ local_auth:
+ dependency: "direct main"
+ description:
+ name: local_auth
+ sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.7"
+ local_auth_android:
+ dependency: transitive
+ description:
+ name: local_auth_android
+ sha256: "9ad0b1ffa6f04f4d91e38c2d4c5046583e23f4cae8345776a994e8670df57fb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.34"
+ local_auth_ios:
+ dependency: transitive
+ description:
+ name: local_auth_ios
+ sha256: "26a8d1ad0b4ef6f861d29921be8383000fda952e323a5b6752cf82ca9cf9a7a9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.4"
+ local_auth_platform_interface:
+ dependency: transitive
+ description:
+ name: local_auth_platform_interface
+ sha256: fc5bd537970a324260fda506cfb61b33ad7426f37a8ea5c461cf612161ebba54
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ local_auth_windows:
+ dependency: transitive
+ description:
+ name: local_auth_windows
+ sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.10"
logging:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 1bee845b..609152d0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -49,6 +49,7 @@ dependencies:
macos_window_utils: ^1.2.0
dynamic_color: ^1.6.6
icloud_storage: ^2.2.0
+ local_auth: ^2.1.7
dev_dependencies:
flutter_native_splash: ^2.1.6
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 1a40a844..7a68426d 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
+ LocalAuthPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("LocalAuthPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 9feb0426..d8bef0fa 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
+ local_auth_windows
share_plus
url_launcher_windows
)