diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 0e883b1c0..f81b9db84 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,5 +1,6 @@ { "account": "Account", + "unlock": "Unlock", "recoveryKey": "Recovery key", "counterAppBarTitle": "Counter", "@counterAppBarTitle": { @@ -97,6 +98,7 @@ "authToViewYourRecoveryKey": "Please authenticate to view your recovery key", "authToChangeYourEmail": "Please authenticate to change your email", "authToChangeYourPassword": "Please authenticate to change your password", + "authToViewSecrets": "Please authenticate to view your secrets", "ok": "Ok", "cancel": "Cancel", "yes": "Yes", @@ -336,5 +338,57 @@ "deleteCodeAuthMessage": "Authenticate to delete code", "showQRAuthMessage": "Authenticate to show QR code", "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." + } } diff --git a/lib/services/local_authentication_service.dart b/lib/services/local_authentication_service.dart index f161bb89f..d21d1bcb2 100644 --- a/lib/services/local_authentication_service.dart +++ b/lib/services/local_authentication_service.dart @@ -1,5 +1,3 @@ - - import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/utils/auth_util.dart'; @@ -19,7 +17,7 @@ class LocalAuthenticationService { ) async { if (await _isLocalAuthSupportedOnDevice()) { AppLock.of(context)!.setEnabled(false); - final result = await requestAuthentication(infoMessage); + final result = await requestAuthentication(context, infoMessage); AppLock.of(context)!.setEnabled( Configuration.instance.shouldShowLockScreen(), ); @@ -43,6 +41,7 @@ class LocalAuthenticationService { if (await LocalAuthentication().isDeviceSupported()) { AppLock.of(context)!.disable(); final result = await requestAuthentication( + context, infoMessage, ); if (result) { diff --git a/lib/ui/tools/lock_screen.dart b/lib/ui/tools/lock_screen.dart index a1389fc4e..0262de56a 100644 --- a/lib/ui/tools/lock_screen.dart +++ b/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/tools/app_lock.dart'; import 'package:ente_auth/utils/auth_util.dart'; @@ -13,13 +12,19 @@ class LockScreen extends StatefulWidget { State createState() => _LockScreenState(); } -class _LockScreenState extends State { +class _LockScreenState extends State with WidgetsBindingObserver { final _logger = Logger("LockScreen"); + bool _isShowingLockScreen = false; + bool _hasPlacedAppInBackground = false; + bool _hasAuthenticationFailed = false; + int? lastAuthenticatingTime; @override void initState() { - _showLockScreen(); + _logger.info("initState"); super.initState(); + _showLockScreen(source: "initState"); + WidgetsBinding.instance.addObserver(this); } @override @@ -34,16 +39,16 @@ class _LockScreenState extends State { alignment: Alignment.center, children: [ Opacity( - opacity: 0.3, + opacity: 0.2, child: Image.asset('assets/loading_photos_background.png'), ), SizedBox( - width: 142, + width: 180, child: GradientButton( - text: "Unlock", + text: context.l10n.unlock, iconData: Icons.lock_open_outlined, onTap: () async { - _showLockScreen(); + _showLockScreen(source: "tapUnlock"); }, ), ), @@ -55,16 +60,65 @@ class _LockScreenState extends State { ); } - Future _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 _showLockScreen({String source = ''}) async { + final int id = DateTime.now().millisecondsSinceEpoch; + _logger.info("Showing lock screen $source $id"); try { + _isShowingLockScreen = true; final result = await requestAuthentication( - "Please authenticate to view your secrets", + context, + context.l10n.authToViewSecrets, ); + _logger.finest("LockScreen Result $result $id"); + _isShowingLockScreen = false; if (result) { + lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch; 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) { + _isShowingLockScreen = false; _logger.severe(e, s); } } diff --git a/lib/utils/auth_util.dart b/lib/utils/auth_util.dart index 77f973204..0a6320191 100644 --- a/lib/utils/auth_util.dart +++ b/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_android/local_auth_android.dart'; +import 'package:local_auth_ios/types/auth_messages_ios.dart'; import 'package:logging/logging.dart'; -Future requestAuthentication(String reason) async { +Future requestAuthentication(BuildContext context, String reason) async { Logger("AuthUtil").info("Requesting authentication"); await LocalAuthentication().stopAuthentication(); + final l10n = context.l10n; return await LocalAuthentication().authenticate( localizedReason: reason, 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, ), ], ); diff --git a/pubspec.lock b/pubspec.lock index 77916b086..be8b19c1d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -768,7 +768,7 @@ packages: source: hosted version: "2.1.7" local_auth_android: - dependency: transitive + dependency: "direct main" description: name: local_auth_android sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5" @@ -776,7 +776,7 @@ packages: source: hosted version: "1.0.31" local_auth_ios: - dependency: transitive + dependency: "direct main" description: name: local_auth_ios sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940 diff --git a/pubspec.yaml b/pubspec.yaml index 10b24fbcd..82eceba35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 2.0.15+215 +version: 2.0.17+217 publish_to: none environment: @@ -54,6 +54,9 @@ dependencies: intl: ^0.18.0 json_annotation: ^4.5.0 local_auth: ^2.1.7 + + local_auth_android: ^1.0.31 + local_auth_ios: ^1.1.3 logging: ^1.0.1 modal_bottom_sheet: ^3.0.0-pre move_to_background: ^1.0.2