diff --git a/assets/custom-icons/_data/custom-icons.json b/assets/custom-icons/_data/custom-icons.json index ee7dac8f64..96cada4285 100644 --- a/assets/custom-icons/_data/custom-icons.json +++ b/assets/custom-icons/_data/custom-icons.json @@ -95,6 +95,12 @@ "title": "La Poste", "slug": "laposte" }, + { + "title": "Mastodon", + "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"], + "slug": "mastodon", + "hex": "6364FF" + }, { "title": "Microsoft" }, diff --git a/assets/custom-icons/icons/mastodon.svg b/assets/custom-icons/icons/mastodon.svg new file mode 100644 index 0000000000..5e3b7e13cf --- /dev/null +++ b/assets/custom-icons/icons/mastodon.svg @@ -0,0 +1 @@ +Mastodon \ No newline at end of file diff --git a/assets/simple-icons b/assets/simple-icons index 7e1ad45175..8e7701d6a4 160000 --- a/assets/simple-icons +++ b/assets/simple-icons @@ -1 +1 @@ -Subproject commit 7e1ad4517598f36ba625741a4dfbc33610d105d8 +Subproject commit 8e7701d6a40462733043f54b3849faf35af70a83 diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 0e883b1c0a..7d516c5b42 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": { @@ -85,6 +86,7 @@ "importSelectJsonFile": "Select JSON file", "importEnteEncGuide": "Select the encrypted JSON file exported from ente", "importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.", + "importBitwardenGuide": "Use the \"Export vault\" option within Bitwarden Tools and import the unencrypted JSON file.", "importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.", "exportCodes": "Export codes", "importLabel": "Import", @@ -97,6 +99,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 +339,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/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index d2b14477e7..4f510e7de1 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1,5 +1,6 @@ { "account": "账户", + "unlock": "解锁", "recoveryKey": "恢复密钥", "counterAppBarTitle": "计数器", "@counterAppBarTitle": { @@ -97,6 +98,7 @@ "authToViewYourRecoveryKey": "请验证以查看您的恢复密钥", "authToChangeYourEmail": "请验证以更改您的电子邮件", "authToChangeYourPassword": "请验证以更改密码", + "authToViewSecrets": "请进行身份验证以查看您的秘密", "ok": "好的", "cancel": "取消", "yes": "是", @@ -336,5 +338,57 @@ "deleteCodeAuthMessage": "删除代码需要身份验证", "showQRAuthMessage": "显示QR码需要身份验证", "confirmAccountDeleteTitle": "确认删除账户", - "confirmAccountDeleteMessage": "该账户已链接到其他ente旗下的应用程序(如果您使用任何相关的应用程序)。\n\n您在所有ente旗下应用程序中上传的数据都将被安排删除,并且您的账户将被永久删除。" + "confirmAccountDeleteMessage": "该账户已链接到其他ente旗下的应用程序(如果您使用任何相关的应用程序)。\n\n您在所有ente旗下应用程序中上传的数据都将被安排删除,并且您的账户将被永久删除。", + "androidBiometricHint": "验证身份", + "@androidBiometricHint": { + "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricNotRecognized": "未能识别。再试一次。", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidBiometricSuccess": "成功", + "@androidBiometricSuccess": { + "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." + }, + "androidCancelButton": "取消", + "@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": "需要进行身份验证", + "@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": "需要进行生物识别认证", + "@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": "需要设备凭据", + "@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": "需要设备凭据", + "@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": "前往设置", + "@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": "您的设备上未设置生物识别身份验证。转到“设置 > 安全”以添加生物识别身份验证。", + "@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": "生物识别身份验证已禁用。请锁定再解锁您的屏幕以启用它。", + "@iOSLockOut": { + "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." + }, + "iOSGoToSettingsDescription": "您的设备上未设置生物识别身份验证。请在您的手机上启用 触控 ID 或 面容 ID。", + "@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": "好的", + "@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." + } } \ No newline at end of file diff --git a/lib/services/local_authentication_service.dart b/lib/services/local_authentication_service.dart index f161bb89fa..d21d1bcb26 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/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart new file mode 100644 index 0000000000..ce2dce3f2a --- /dev/null +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +import 'package:ente_auth/ui/components/dialog_widget.dart'; +import 'package:ente_auth/ui/components/models/button_type.dart'; +import 'package:ente_auth/ui/settings/data/import/import_success.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +Future showBitwardenImportInstruction(BuildContext context) async { + final l10n = context.l10n; + final result = await showDialogWidget( + context: context, + title: l10n.importFromApp("Bitwarden"), + body: l10n.importBitwardenGuide, + buttons: [ + ButtonWidget( + buttonType: ButtonType.primary, + labelText: l10n.importSelectJsonFile, + isInAlert: true, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + ), + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: context.l10n.cancel, + buttonSize: ButtonSize.large, + isInAlert: true, + buttonAction: ButtonAction.second, + ), + ], + ); + if (result?.action != null && result!.action != ButtonAction.cancel) { + if (result.action == ButtonAction.first) { + await _pickBitwardenJsonFile(context); + } + } +} + +Future _pickBitwardenJsonFile(BuildContext context) async { + final l10n = context.l10n; + FilePickerResult? result = await FilePicker.platform.pickFiles(); + if (result == null) { + return; + } + final progressDialog = createProgressDialog(context, l10n.pleaseWait); + await progressDialog.show(); + try { + String path = result.files.single.path!; + int? count = await _processBitwardenExportFile(context, path); + await progressDialog.hide(); + if (count != null) { + await importSuccessDialog(context, count); + } + } catch (e) { + await progressDialog.hide(); + await showErrorDialog( + context, + context.l10n.sorry, + context.l10n.importFailureDesc, + ); + } +} + +Future _processBitwardenExportFile( + BuildContext context, + String path, +) async { + File file = File(path); + final jsonString = await file.readAsString(); + final data = jsonDecode(jsonString); + List jsonArray = data['items']; + final parsedCodes = []; + for (var item in jsonArray) { + if (item['login']['totp'] != null) { + var issuer = item['name']; + var account = item['login']['username']; + var secret = item['login']['totp']; + + parsedCodes.add( + Code.fromAccountAndSecret( + account, + issuer, + secret, + ), + ); + } + } + + for (final code in parsedCodes) { + await CodeStore.instance.addCode(code, shouldSync: false); + } + unawaited(AuthenticatorService.instance.onlineSync()); + return parsedCodes.length; +} diff --git a/lib/ui/settings/data/import/import_service.dart b/lib/ui/settings/data/import/import_service.dart index 514eb9362f..79f047c7fd 100644 --- a/lib/ui/settings/data/import/import_service.dart +++ b/lib/ui/settings/data/import/import_service.dart @@ -1,5 +1,6 @@ import 'package:ente_auth/ui/settings/data/import/aegis_import.dart'; import 'package:ente_auth/ui/settings/data/import/analyze_qr_code.dart'; +import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart'; import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart'; import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart'; import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart'; @@ -40,6 +41,8 @@ class ImportService { }, ), ); + case ImportType.bitwarden: + showBitwardenImportInstruction(context); break; } } diff --git a/lib/ui/settings/data/import_page.dart b/lib/ui/settings/data/import_page.dart index b7e1211b01..bebcb0a89c 100644 --- a/lib/ui/settings/data/import_page.dart +++ b/lib/ui/settings/data/import_page.dart @@ -15,7 +15,8 @@ enum ImportType { ravio, googleAuthenticator, aegis, - googleAuthenticatorImage + googleAuthenticatorImage, + bitwarden, } class ImportCodePage extends StatelessWidget { @@ -26,6 +27,7 @@ class ImportCodePage extends StatelessWidget { ImportType.aegis, ImportType.googleAuthenticator, ImportType.googleAuthenticatorImage, + ImportType.bitwarden, ]; ImportCodePage({super.key}); @@ -45,6 +47,8 @@ class ImportCodePage extends StatelessWidget { return 'Aegis Authenticator'; case ImportType.googleAuthenticatorImage: return 'Google Authenticator (saved image)'; + case ImportType.bitwarden: + return 'Bitwarden'; } } diff --git a/lib/ui/tools/lock_screen.dart b/lib/ui/tools/lock_screen.dart index a1389fc4ec..202a8291e2 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,67 @@ 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/ui/utils/icon_utils.dart b/lib/ui/utils/icon_utils.dart index 27da8752ba..7cb1299ac5 100644 --- a/lib/ui/utils/icon_utils.dart +++ b/lib/ui/utils/icon_utils.dart @@ -93,6 +93,14 @@ class IconUtils { icon["slug"], icon["hex"], ); + if (icon["altNames"] != null) { + for (final name in icon["altNames"]) { + _customIcons[name] = CustomIconData( + icon["slug"], + icon["hex"], + ); + } + } } } catch (e) { Logger("IconUtils").severe("Error loading icons", e); diff --git a/lib/utils/auth_util.dart b/lib/utils/auth_util.dart index 77f9732041..0a63201912 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 53c93804c5..9dd3376236 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -792,7 +792,7 @@ packages: source: hosted version: "2.1.7" local_auth_android: - dependency: transitive + dependency: "direct main" description: name: local_auth_android sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5" @@ -800,7 +800,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 fc8bba25fa..811900f44d 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 mobile_scanner: ^3.5.2 modal_bottom_sheet: ^3.0.0-pre