Browse Source

Lock screen fixes (#345)

Neeraj Gupta 1 năm trước cách đây
mục cha
commit
55e9a7049e

+ 55 - 1
lib/l10n/arb/app_en.arb

@@ -1,5 +1,6 @@
 {
 {
   "account": "Account",
   "account": "Account",
+  "unlock": "Unlock",
   "recoveryKey": "Recovery key",
   "recoveryKey": "Recovery key",
   "counterAppBarTitle": "Counter",
   "counterAppBarTitle": "Counter",
   "@counterAppBarTitle": {
   "@counterAppBarTitle": {
@@ -97,6 +98,7 @@
   "authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
   "authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
   "authToChangeYourEmail": "Please authenticate to change your email",
   "authToChangeYourEmail": "Please authenticate to change your email",
   "authToChangeYourPassword": "Please authenticate to change your password",
   "authToChangeYourPassword": "Please authenticate to change your password",
+  "authToViewSecrets": "Please authenticate to view your secrets",
   "ok": "Ok",
   "ok": "Ok",
   "cancel": "Cancel",
   "cancel": "Cancel",
   "yes": "Yes",
   "yes": "Yes",
@@ -336,5 +338,57 @@
   "deleteCodeAuthMessage": "Authenticate to delete code",
   "deleteCodeAuthMessage": "Authenticate to delete code",
   "showQRAuthMessage": "Authenticate to show QR code",
   "showQRAuthMessage": "Authenticate to show QR code",
   "confirmAccountDeleteTitle": "Confirm account deletion",
   "confirmAccountDeleteTitle": "Confirm account deletion",
-  "confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
+  "confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
+  "androidBiometricHint": "Verify identity",
+  "@androidBiometricHint": {
+    "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
+  },
+  "androidBiometricNotRecognized": "Not recognized. Try again.",
+  "@androidBiometricNotRecognized": {
+    "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
+  },
+  "androidBiometricSuccess": "Success",
+  "@androidBiometricSuccess": {
+    "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
+  },
+  "androidCancelButton": "Cancel",
+  "@androidCancelButton": {
+    "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
+  },
+  "androidSignInTitle": "Authentication required",
+  "@androidSignInTitle": {
+    "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
+  },
+  "androidBiometricRequiredTitle": "Biometric required",
+  "@androidBiometricRequiredTitle": {
+    "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
+  },
+  "androidDeviceCredentialsRequiredTitle": "Device credentials required",
+  "@androidDeviceCredentialsRequiredTitle": {
+    "description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
+  },
+  "androidDeviceCredentialsSetupDescription": "Device credentials required",
+  "@androidDeviceCredentialsSetupDescription": {
+    "description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
+  },
+  "goToSettings": "Go to settings",
+  "@goToSettings": {
+    "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
+  },
+  "androidGoToSettingsDescription": "Biometric authentication is not set up on your device. Go to 'Settings > Security' to add biometric authentication.",
+  "@androidGoToSettingsDescription": {
+    "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
+  },
+  "iOSLockOut": "Biometric authentication is disabled. Please lock and unlock your screen to enable it.",
+  "@iOSLockOut": {
+    "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
+  },
+  "iOSGoToSettingsDescription": "Biometric authentication is not set up on your device. Please either enable Touch ID or Face ID on your phone.",
+  "@iOSGoToSettingsDescription": {
+    "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
+  },
+  "iOSOkButton": "OK",
+  "@iOSOkButton": {
+    "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
+  }
 }
 }

+ 2 - 3
lib/services/local_authentication_service.dart

@@ -1,5 +1,3 @@
-
-
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/utils/auth_util.dart';
 import 'package:ente_auth/utils/auth_util.dart';
@@ -19,7 +17,7 @@ class LocalAuthenticationService {
   ) async {
   ) async {
     if (await _isLocalAuthSupportedOnDevice()) {
     if (await _isLocalAuthSupportedOnDevice()) {
       AppLock.of(context)!.setEnabled(false);
       AppLock.of(context)!.setEnabled(false);
-      final result = await requestAuthentication(infoMessage);
+      final result = await requestAuthentication(context, infoMessage);
       AppLock.of(context)!.setEnabled(
       AppLock.of(context)!.setEnabled(
         Configuration.instance.shouldShowLockScreen(),
         Configuration.instance.shouldShowLockScreen(),
       );
       );
@@ -43,6 +41,7 @@ class LocalAuthenticationService {
     if (await LocalAuthentication().isDeviceSupported()) {
     if (await LocalAuthentication().isDeviceSupported()) {
       AppLock.of(context)!.disable();
       AppLock.of(context)!.disable();
       final result = await requestAuthentication(
       final result = await requestAuthentication(
+        context,
         infoMessage,
         infoMessage,
       );
       );
       if (result) {
       if (result) {

+ 65 - 11
lib/ui/tools/lock_screen.dart

@@ -1,5 +1,4 @@
-
-
+import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/ui/common/gradient_button.dart';
 import 'package:ente_auth/ui/common/gradient_button.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/utils/auth_util.dart';
 import 'package:ente_auth/utils/auth_util.dart';
@@ -13,13 +12,19 @@ class LockScreen extends StatefulWidget {
   State<LockScreen> createState() => _LockScreenState();
   State<LockScreen> createState() => _LockScreenState();
 }
 }
 
 
-class _LockScreenState extends State<LockScreen> {
+class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
   final _logger = Logger("LockScreen");
   final _logger = Logger("LockScreen");
+  bool _isShowingLockScreen = false;
+  bool _hasPlacedAppInBackground = false;
+  bool _hasAuthenticationFailed = false;
+  int? lastAuthenticatingTime;
 
 
   @override
   @override
   void initState() {
   void initState() {
-    _showLockScreen();
+    _logger.info("initState");
     super.initState();
     super.initState();
+    _showLockScreen(source: "initState");
+    WidgetsBinding.instance.addObserver(this);
   }
   }
 
 
   @override
   @override
@@ -34,16 +39,16 @@ class _LockScreenState extends State<LockScreen> {
               alignment: Alignment.center,
               alignment: Alignment.center,
               children: [
               children: [
                 Opacity(
                 Opacity(
-                  opacity: 0.3,
+                  opacity: 0.2,
                   child: Image.asset('assets/loading_photos_background.png'),
                   child: Image.asset('assets/loading_photos_background.png'),
                 ),
                 ),
                 SizedBox(
                 SizedBox(
-                  width: 142,
+                  width: 180,
                   child: GradientButton(
                   child: GradientButton(
-                    text: "Unlock",
+                    text: context.l10n.unlock,
                     iconData: Icons.lock_open_outlined,
                     iconData: Icons.lock_open_outlined,
                     onTap: () async {
                     onTap: () async {
-                      _showLockScreen();
+                      _showLockScreen(source: "tapUnlock");
                     },
                     },
                   ),
                   ),
                 ),
                 ),
@@ -55,16 +60,65 @@ class _LockScreenState extends State<LockScreen> {
     );
     );
   }
   }
 
 
-  Future<void> _showLockScreen() async {
-    _logger.info("Showing lockscreen");
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    _logger.info(state.toString());
+    if (state == AppLifecycleState.resumed && !_isShowingLockScreen) {
+      // This is triggered either when the lock screen is dismissed or when
+      // the app is brought to foreground
+      _hasPlacedAppInBackground = false;
+      final bool didAuthInLast5Seconds = lastAuthenticatingTime != null &&
+          DateTime.now().millisecondsSinceEpoch - lastAuthenticatingTime! <
+              5000;
+      if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) {
+        // Show the lock screen again only if the app is resuming from the
+        // background, and not when the lock screen was explicitly dismissed
+        Future.delayed(
+            Duration.zero, () => _showLockScreen(source: "lifeCycle"));
+      } else {
+        _hasAuthenticationFailed = false; // Reset failure state
+      }
+    } else if (state == AppLifecycleState.paused ||
+        state == AppLifecycleState.inactive) {
+      // This is triggered either when the lock screen pops up or when
+      // the app is pushed to background
+      if (!_isShowingLockScreen) {
+        _hasPlacedAppInBackground = true;
+        _hasAuthenticationFailed = false; // reset failure state
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    WidgetsBinding.instance.removeObserver(this);
+    super.dispose();
+  }
+
+  Future<void> _showLockScreen({String source = ''}) async {
+    final int id = DateTime.now().millisecondsSinceEpoch;
+    _logger.info("Showing lock screen $source $id");
     try {
     try {
+      _isShowingLockScreen = true;
       final result = await requestAuthentication(
       final result = await requestAuthentication(
-        "Please authenticate to view your secrets",
+        context,
+        context.l10n.authToViewSecrets,
       );
       );
+      _logger.finest("LockScreen Result $result $id");
+      _isShowingLockScreen = false;
       if (result) {
       if (result) {
+        lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch;
         AppLock.of(context)!.didUnlock();
         AppLock.of(context)!.didUnlock();
+      } else {
+        if (!_hasPlacedAppInBackground) {
+          // Treat this as a failure only if user did not explicitly
+          // put the app in background
+          _hasAuthenticationFailed = true;
+          _logger.info("Authentication failed");
+        }
       }
       }
     } catch (e, s) {
     } catch (e, s) {
+      _isShowingLockScreen = false;
       _logger.severe(e, s);
       _logger.severe(e, s);
     }
     }
   }
   }

+ 25 - 13
lib/utils/auth_util.dart

@@ -1,25 +1,37 @@
+import 'package:ente_auth/l10n/l10n.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:local_auth/local_auth.dart';
 import 'package:local_auth/local_auth.dart';
 import 'package:local_auth_android/local_auth_android.dart';
 import 'package:local_auth_android/local_auth_android.dart';
+import 'package:local_auth_ios/types/auth_messages_ios.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 
 
-Future<bool> requestAuthentication(String reason) async {
+Future<bool> requestAuthentication(BuildContext context, String reason) async {
   Logger("AuthUtil").info("Requesting authentication");
   Logger("AuthUtil").info("Requesting authentication");
   await LocalAuthentication().stopAuthentication();
   await LocalAuthentication().stopAuthentication();
+  final l10n = context.l10n;
   return await LocalAuthentication().authenticate(
   return await LocalAuthentication().authenticate(
     localizedReason: reason,
     localizedReason: reason,
     authMessages: [
     authMessages: [
-      const AndroidAuthMessages(
-        biometricHint: "Verify identity",
-        biometricNotRecognized: "Not recognized, try again",
-        biometricRequiredTitle: "Biometric required",
-        biometricSuccess: "Successfully verified",
-        cancelButton: "Cancel",
-        deviceCredentialsRequiredTitle: "Device credentials required",
-        deviceCredentialsSetupDescription: "Device credentials required",
-        goToSettingsButton: "Go to settings",
-        goToSettingsDescription:
-            "Authentication is not setup on your device, go to Settings > Security to set it up",
-        signInTitle: "Authentication required",
+      AndroidAuthMessages(
+        biometricHint: l10n.androidBiometricHint,
+        biometricNotRecognized: l10n.androidBiometricNotRecognized,
+        biometricRequiredTitle: l10n.androidBiometricRequiredTitle,
+        biometricSuccess: l10n.androidBiometricSuccess,
+        cancelButton: l10n.androidCancelButton,
+        deviceCredentialsRequiredTitle:
+            l10n.androidDeviceCredentialsRequiredTitle,
+        deviceCredentialsSetupDescription:
+            l10n.androidDeviceCredentialsSetupDescription,
+        goToSettingsButton: l10n.goToSettings,
+        goToSettingsDescription: l10n.androidGoToSettingsDescription,
+        signInTitle: l10n.androidSignInTitle,
+      ),
+      IOSAuthMessages(
+        goToSettingsButton: l10n.goToSettings,
+        goToSettingsDescription: l10n.goToSettings,
+        lockOut: l10n.iOSLockOut,
+        // cancelButton default value is "Ok"
+        cancelButton: l10n.iOSOkButton,
       ),
       ),
     ],
     ],
   );
   );

+ 2 - 2
pubspec.lock

@@ -768,7 +768,7 @@ packages:
     source: hosted
     source: hosted
     version: "2.1.7"
     version: "2.1.7"
   local_auth_android:
   local_auth_android:
-    dependency: transitive
+    dependency: "direct main"
     description:
     description:
       name: local_auth_android
       name: local_auth_android
       sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5"
       sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5"
@@ -776,7 +776,7 @@ packages:
     source: hosted
     source: hosted
     version: "1.0.31"
     version: "1.0.31"
   local_auth_ios:
   local_auth_ios:
-    dependency: transitive
+    dependency: "direct main"
     description:
     description:
       name: local_auth_ios
       name: local_auth_ios
       sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940
       sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940

+ 4 - 1
pubspec.yaml

@@ -1,6 +1,6 @@
 name: ente_auth
 name: ente_auth
 description: ente two-factor authenticator
 description: ente two-factor authenticator
-version: 2.0.15+215
+version: 2.0.17+217
 publish_to: none
 publish_to: none
 
 
 environment:
 environment:
@@ -54,6 +54,9 @@ dependencies:
   intl: ^0.18.0
   intl: ^0.18.0
   json_annotation: ^4.5.0
   json_annotation: ^4.5.0
   local_auth: ^2.1.7
   local_auth: ^2.1.7
+
+  local_auth_android: ^1.0.31
+  local_auth_ios: ^1.1.3
   logging: ^1.0.1
   logging: ^1.0.1
   modal_bottom_sheet: ^3.0.0-pre
   modal_bottom_sheet: ^3.0.0-pre
   move_to_background: ^1.0.2
   move_to_background: ^1.0.2