mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 21:34:45 +01:00
#165 new: bio auth
This commit is contained in:
63
lib/core/utils/platform/auth.dart
Normal file
63
lib/core/utils/platform/auth.dart
Normal file
@@ -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<bool> 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<AuthResult> 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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"alreadyLastDir": "已经是最上层目录了",
|
||||
"alterUrl": "备选链接",
|
||||
"attention": "注意",
|
||||
"authRequired": "需要认证",
|
||||
"auto": "自动",
|
||||
"autoCheckUpdate": "自动检查更新",
|
||||
"autoConnect": "自动连接",
|
||||
@@ -20,6 +21,7 @@
|
||||
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。",
|
||||
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
|
||||
"bgRun": "后台运行",
|
||||
"bioAuth": "生物认证",
|
||||
"canPullRefresh": "可以下拉刷新",
|
||||
"cancel": "取消",
|
||||
"choose": "选择",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"alreadyLastDir": "已經是最上層目錄了",
|
||||
"alterUrl": "備選鏈接",
|
||||
"attention": "注意",
|
||||
"authRequired": "需要認證",
|
||||
"auto": "自動",
|
||||
"autoCheckUpdate": "自動檢查更新",
|
||||
"autoConnect": "自動連接",
|
||||
@@ -20,6 +21,7 @@
|
||||
"backupTip": "導出的數據僅進行了簡單加密,請妥善保管。",
|
||||
"backupVersionNotMatch": "備份版本不匹配,無法還原",
|
||||
"bgRun": "背景運行",
|
||||
"bioAuth": "生物認證",
|
||||
"canPullRefresh": "可以下拉更新",
|
||||
"cancel": "取消",
|
||||
"choose": "選擇",
|
||||
|
||||
@@ -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<HomePage>
|
||||
late S _s;
|
||||
|
||||
bool _switchingPage = false;
|
||||
bool _isAuthing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -81,6 +83,7 @@ class _HomePageState extends State<HomePage>
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
_auth();
|
||||
if (!Providers.server.isAutoRefreshOn) {
|
||||
Providers.server.startAutoRefresh();
|
||||
}
|
||||
@@ -338,6 +341,8 @@ class _HomePageState extends State<HomePage>
|
||||
|
||||
@override
|
||||
Future<void> 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<HomePage>
|
||||
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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SettingPage> {
|
||||
//_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<SettingPage> {
|
||||
// trailing: StoreSwitch(prop: _setting.doubleColumnServersPage),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _buildBioAuth() {
|
||||
return FutureWidget<bool>(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FutureWidget<T> extends StatelessWidget {
|
||||
final Future future;
|
||||
final Future<T> future;
|
||||
final Widget loading;
|
||||
final Widget Function(Object? error, StackTrace? trace) error;
|
||||
final Widget Function(T data) success;
|
||||
|
||||
Reference in New Issue
Block a user