diff --git a/mobile/apps/auth/assets/2.0x/lock_screen_icon.png b/mobile/apps/auth/assets/2.0x/lock_screen_icon.png new file mode 100644 index 00000000000..89ef8898159 Binary files /dev/null and b/mobile/apps/auth/assets/2.0x/lock_screen_icon.png differ diff --git a/mobile/apps/auth/assets/2.0x/upload_file.png b/mobile/apps/auth/assets/2.0x/upload_file.png new file mode 100644 index 00000000000..12a92132296 Binary files /dev/null and b/mobile/apps/auth/assets/2.0x/upload_file.png differ diff --git a/mobile/apps/auth/assets/2.0x/warning-grey.png b/mobile/apps/auth/assets/2.0x/warning-grey.png new file mode 100644 index 00000000000..aac9fdf2ac5 Binary files /dev/null and b/mobile/apps/auth/assets/2.0x/warning-grey.png differ diff --git a/mobile/apps/auth/assets/3.0x/lock_screen_icon.png b/mobile/apps/auth/assets/3.0x/lock_screen_icon.png new file mode 100644 index 00000000000..1497653e9ca Binary files /dev/null and b/mobile/apps/auth/assets/3.0x/lock_screen_icon.png differ diff --git a/mobile/apps/auth/assets/3.0x/upload_file.png b/mobile/apps/auth/assets/3.0x/upload_file.png new file mode 100644 index 00000000000..dd49cfd45d0 Binary files /dev/null and b/mobile/apps/auth/assets/3.0x/upload_file.png differ diff --git a/mobile/apps/auth/assets/3.0x/warning-grey.png b/mobile/apps/auth/assets/3.0x/warning-grey.png new file mode 100644 index 00000000000..e8b788289c9 Binary files /dev/null and b/mobile/apps/auth/assets/3.0x/warning-grey.png differ diff --git a/mobile/apps/auth/assets/lock_screen_icon.png b/mobile/apps/auth/assets/lock_screen_icon.png new file mode 100644 index 00000000000..d7e64586fbe Binary files /dev/null and b/mobile/apps/auth/assets/lock_screen_icon.png differ diff --git a/mobile/apps/auth/assets/svg/auth-logo.svg b/mobile/apps/auth/assets/svg/app-logo.svg similarity index 100% rename from mobile/apps/auth/assets/svg/auth-logo.svg rename to mobile/apps/auth/assets/svg/app-logo.svg diff --git a/mobile/apps/auth/assets/upload_file.png b/mobile/apps/auth/assets/upload_file.png new file mode 100644 index 00000000000..3340b77241e Binary files /dev/null and b/mobile/apps/auth/assets/upload_file.png differ diff --git a/mobile/apps/auth/assets/warning-grey.png b/mobile/apps/auth/assets/warning-grey.png new file mode 100644 index 00000000000..bf6720c4533 Binary files /dev/null and b/mobile/apps/auth/assets/warning-grey.png differ diff --git a/mobile/apps/auth/lib/ente_theme_data.dart b/mobile/apps/auth/lib/ente_theme_data.dart index 82fbf3d3134..79d3df27697 100644 --- a/mobile/apps/auth/lib/ente_theme_data.dart +++ b/mobile/apps/auth/lib/ente_theme_data.dart @@ -165,13 +165,6 @@ final darkThemeData = ThemeData( ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), - inputDecorationTheme: const InputDecorationTheme().copyWith( - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 152, 77, 244), - ), - ), - ), checkboxTheme: CheckboxThemeData( side: const BorderSide( color: Colors.grey, diff --git a/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart b/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart index ba4442dc560..4ff93f92f19 100644 --- a/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart +++ b/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart @@ -85,7 +85,7 @@ class _OnboardingPageState extends State { backgroundColor: accentColor, appBar: AppBar( leading: const SizedBox(), - title: SvgPicture.asset("assets/svg/auth-logo.svg"), + title: SvgPicture.asset("assets/svg/app-logo.svg"), backgroundColor: accentColor, elevation: 0, scrolledUnderElevation: 0, diff --git a/mobile/apps/auth/lib/ui/components/auth_qr_dialog.dart b/mobile/apps/auth/lib/ui/components/auth_qr_dialog.dart index 51d6aa785c0..2a975f60ad0 100644 --- a/mobile/apps/auth/lib/ui/components/auth_qr_dialog.dart +++ b/mobile/apps/auth/lib/ui/components/auth_qr_dialog.dart @@ -241,7 +241,7 @@ class _AuthQrDialogState extends State { Align( alignment: Alignment.centerRight, child: SvgPicture.asset( - 'assets/svg/auth-logo.svg', + 'assets/svg/app-logo.svg', height: 16, colorFilter: const ColorFilter.mode( accentColor, diff --git a/mobile/apps/auth/lib/ui/home/widgets/auth_logo_widget.dart b/mobile/apps/auth/lib/ui/home/widgets/auth_logo_widget.dart index 922ee95fff3..c128cb87523 100644 --- a/mobile/apps/auth/lib/ui/home/widgets/auth_logo_widget.dart +++ b/mobile/apps/auth/lib/ui/home/widgets/auth_logo_widget.dart @@ -18,7 +18,7 @@ class AuthLogoWidget extends StatelessWidget { final logoColor = color ?? colorScheme.textBase; return SvgPicture.asset( - 'assets/svg/auth-logo.svg', + 'assets/svg/app-logo.svg', height: height, colorFilter: ColorFilter.mode(logoColor, BlendMode.srcIn), ); diff --git a/mobile/apps/auth/pubspec.yaml b/mobile/apps/auth/pubspec.yaml index d26934cf8cd..0c173447292 100644 --- a/mobile/apps/auth/pubspec.yaml +++ b/mobile/apps/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 4.4.16+600 +version: 4.4.16+672 publish_to: none environment: diff --git a/mobile/apps/auth/pubspec_overrides.yaml b/mobile/apps/auth/pubspec_overrides.yaml index bf3e0b7f5de..2f69237f985 100644 --- a/mobile/apps/auth/pubspec_overrides.yaml +++ b/mobile/apps/auth/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: ente_accounts,ente_base,ente_configuration,ente_events,ente_lock_screen,ente_logging,ente_network,ente_strings,ente_ui,ente_utils,dir_utils +# melos_managed_dependency_overrides: dir_utils,ente_accounts,ente_base,ente_configuration,ente_events,ente_lock_screen,ente_logging,ente_network,ente_strings,ente_ui,ente_utils dependency_overrides: dir_utils: path: ../../packages/dir_utils diff --git a/mobile/packages/lock_screen/assets/2.0x/locker_pin.png b/mobile/apps/locker/assets/2.0x/lock_screen_icon.png similarity index 100% rename from mobile/packages/lock_screen/assets/2.0x/locker_pin.png rename to mobile/apps/locker/assets/2.0x/lock_screen_icon.png diff --git a/mobile/apps/locker/assets/2.0x/locker-logo-blue.png b/mobile/apps/locker/assets/2.0x/locker-logo-blue.png deleted file mode 100644 index 9004f897a39..00000000000 Binary files a/mobile/apps/locker/assets/2.0x/locker-logo-blue.png and /dev/null differ diff --git a/mobile/packages/lock_screen/assets/3.0x/locker_pin.png b/mobile/apps/locker/assets/3.0x/lock_screen_icon.png similarity index 100% rename from mobile/packages/lock_screen/assets/3.0x/locker_pin.png rename to mobile/apps/locker/assets/3.0x/lock_screen_icon.png diff --git a/mobile/apps/locker/assets/3.0x/locker-logo-blue.png b/mobile/apps/locker/assets/3.0x/locker-logo-blue.png deleted file mode 100644 index 98565e59ab5..00000000000 Binary files a/mobile/apps/locker/assets/3.0x/locker-logo-blue.png and /dev/null differ diff --git a/mobile/packages/lock_screen/assets/locker_pin.png b/mobile/apps/locker/assets/lock_screen_icon.png similarity index 100% rename from mobile/packages/lock_screen/assets/locker_pin.png rename to mobile/apps/locker/assets/lock_screen_icon.png diff --git a/mobile/apps/locker/assets/locker-logo-blue.png b/mobile/apps/locker/assets/locker-logo-blue.png deleted file mode 100644 index 06b2434ace9..00000000000 Binary files a/mobile/apps/locker/assets/locker-logo-blue.png and /dev/null differ diff --git a/mobile/apps/locker/assets/svg/app-logo.svg b/mobile/apps/locker/assets/svg/app-logo.svg new file mode 100644 index 00000000000..04b77d04f94 --- /dev/null +++ b/mobile/apps/locker/assets/svg/app-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mobile/apps/locker/ios/Podfile.lock b/mobile/apps/locker/ios/Podfile.lock index bb041a0c2d5..7b279116614 100644 --- a/mobile/apps/locker/ios/Podfile.lock +++ b/mobile/apps/locker/ios/Podfile.lock @@ -77,9 +77,9 @@ PODS: - FlutterMacOS - privacy_screen (0.0.1): - Flutter - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) + - SDWebImage (5.21.5): + - SDWebImage/Core (= 5.21.5) + - SDWebImage/Core (5.21.5) - Sentry/HybridSDK (8.46.0) - sentry_flutter (8.14.2): - Flutter @@ -215,7 +215,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a + SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a diff --git a/mobile/apps/locker/ios/Runner.xcodeproj/project.pbxproj b/mobile/apps/locker/ios/Runner.xcodeproj/project.pbxproj index fa9697de7c9..65a86b926ea 100644 --- a/mobile/apps/locker/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/apps/locker/ios/Runner.xcodeproj/project.pbxproj @@ -459,28 +459,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 95EA1C55C1859216DB1FDA66 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -694,6 +672,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -734,6 +713,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -773,6 +753,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/mobile/apps/locker/lib/services/trash/trash_service.dart b/mobile/apps/locker/lib/services/trash/trash_service.dart index 35edc08cdfa..bf22e1903c8 100644 --- a/mobile/apps/locker/lib/services/trash/trash_service.dart +++ b/mobile/apps/locker/lib/services/trash/trash_service.dart @@ -205,18 +205,12 @@ class TrashService { for (final fileID in uniqueFileIds) { params["fileIDs"].add(fileID); } - try { - await _enteDio.post( - "/trash/delete", - data: params, - ); - await _trashDB.delete(uniqueFileIds); - - await _collectionDB.deleteFilesByUploadedFileIDs(uniqueFileIds); - } catch (e, s) { - _logger.severe("failed to delete from trash", e, s); - rethrow; - } + await _enteDio.post( + "/trash/delete", + data: params, + ); + await _trashDB.delete(uniqueFileIds); + await _collectionDB.deleteFilesByUploadedFileIDs(uniqueFileIds); // no need to await on syncing trash from remote unawaited(syncTrash()); } @@ -224,23 +218,19 @@ class TrashService { Future emptyTrash() async { final params = {}; params["lastUpdatedAt"] = _getSyncTime(); - try { - final trashFiles = await _trashDB.getAllTrashFiles(); - final fileIDs = - trashFiles.map((trashFile) => trashFile.uploadedFileID!).toList(); + final trashFiles = await _trashDB.getAllTrashFiles(); + final fileIDs = + trashFiles.map((trashFile) => trashFile.uploadedFileID!).toList(); - await _enteDio.post( - "/trash/empty", - data: params, - ); + await _enteDio.post( + "/trash/empty", + data: params, + ); - await _trashDB.clearTable(); - await _collectionDB.deleteFilesByUploadedFileIDs(fileIDs); - unawaited(syncTrash()); - } catch (e, s) { - _logger.severe("failed to empty trash", e, s); - rethrow; - } + await _trashDB.clearTable(); + await _collectionDB.deleteFilesByUploadedFileIDs(fileIDs); + unawaited(syncTrash()); + _logger.info("Successfully emptied trash"); } Future restore(List files, Collection toCollection) async { @@ -264,20 +254,15 @@ class TrashService { ).toMap(), ); } - try { - await _enteDio.post( - "/collections/restore-files", - data: params, - ); - await _trashDB.delete(files.map((e) => e.uploadedFileID!).toList()); - // Refresh collections so restored files are immediately available in UI - await CollectionService.instance.sync(); - Bus.instance.fire(CollectionsUpdatedEvent("file_restore")); - Bus.instance.fire(UserDetailsRefreshEvent()); - } catch (e, s) { - _logger.severe("failed to restore files", e, s); - rethrow; - } + await _enteDio.post( + "/collections/restore-files", + data: params, + ); + await _trashDB.delete(files.map((e) => e.uploadedFileID!).toList()); + // Refresh collections so restored files are immediately available in UI + await CollectionService.instance.sync(); + Bus.instance.fire(CollectionsUpdatedEvent("file_restore")); + Bus.instance.fire(UserDetailsRefreshEvent()); } } diff --git a/mobile/apps/locker/lib/ui/components/change_email_dialog_locker.dart b/mobile/apps/locker/lib/ui/components/change_email_dialog_locker.dart deleted file mode 100644 index c472de2da14..00000000000 --- a/mobile/apps/locker/lib/ui/components/change_email_dialog_locker.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'dart:async'; - -import 'package:ente_accounts/ente_accounts.dart'; -import 'package:ente_ui/components/close_icon_button.dart'; -import 'package:ente_ui/theme/ente_theme.dart'; -import 'package:ente_ui/utils/dialog_util.dart'; -import 'package:ente_utils/email_util.dart'; -import 'package:flutter/material.dart'; -import "package:locker/l10n/l10n.dart"; -import "package:locker/ui/components/gradient_button.dart"; - -class ChangeEmailDialogLocker extends StatefulWidget { - const ChangeEmailDialogLocker({super.key}); - - @override - State createState() => - _ChangeEmailDialogLockerState(); -} - -class _ChangeEmailDialogLockerState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _obscurePassword = true; - - // Validation states - String _email = ''; - String _password = ''; - - bool get _isFormValid => - _email.isNotEmpty && isValidEmail(_email) && _password.isNotEmpty; - - @override - void initState() { - super.initState(); - _emailController.addListener(() { - setState(() { - _email = _emailController.text.trim(); - }); - }); - _passwordController.addListener(() { - setState(() { - _password = _passwordController.text; - }); - }); - } - - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); - final textTheme = getEnteTextTheme(context); - - return Dialog( - backgroundColor: colorScheme.backgroundElevated2, - insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.l10n.changeEmail, - style: textTheme.h3Bold, - ), - const CloseIconButton(), - ], - ), - const SizedBox(height: 24), - Container( - decoration: BoxDecoration( - color: _email.isNotEmpty && isValidEmail(_email) - ? colorScheme.primary500.withValues(alpha: 0.05) - : colorScheme.fillFaint, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _email.isNotEmpty && isValidEmail(_email) - ? colorScheme.primary500.withValues(alpha: 0.3) - : colorScheme.strokeFaint, - width: 1, - ), - ), - child: TextFormField( - controller: _emailController, - decoration: InputDecoration( - hintText: 'Enter your new email address', - hintStyle: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: InputBorder.none, - ), - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - autocorrect: false, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.next, - autofocus: true, - ), - ), - const SizedBox(height: 16), - Container( - decoration: BoxDecoration( - color: _password.isNotEmpty - ? colorScheme.primary500.withValues(alpha: 0.05) - : colorScheme.fillFaint, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _password.isNotEmpty - ? colorScheme.primary500.withValues(alpha: 0.3) - : colorScheme.strokeFaint, - width: 1, - ), - ), - child: TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - hintText: 'Enter your current password', - hintStyle: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: InputBorder.none, - suffixIcon: GestureDetector( - onTap: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Icon( - _obscurePassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: colorScheme.textMuted, - size: 20, - ), - ), - ), - ), - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _handleSubmit(), - ), - ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: _isFormValid - ? GradientButton( - onTap: _handleSubmit, - text: 'Apply changes', - ) - : Container( - height: 56, // Match GradientButton height - decoration: BoxDecoration( - color: colorScheme.fillMuted, - borderRadius: BorderRadius.circular(20), - ), - alignment: Alignment.center, - child: Text( - 'Apply changes', - style: textTheme.bodyBold.copyWith( - color: colorScheme.textFaint, - ), - ), - ), - ), - ], - ), - ), - ); - } - - void _handleSubmit() async { - if (!_isFormValid) return; - - try { - await UserService.instance.sendOtt( - context, - _email, - isChangeEmail: true, - ); - } catch (e) { - if (mounted) { - unawaited( - showErrorDialog( - context, - 'Error', - 'Failed to send verification email. Please try again.', - ), - ); - } - } - } -} - -Future showChangeEmailDialogLocker(BuildContext context) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return const ChangeEmailDialogLocker(); - }, - ); -} diff --git a/mobile/apps/locker/lib/ui/drawer/account_section_widget.dart b/mobile/apps/locker/lib/ui/drawer/account_section_widget.dart index 207655fc815..e453dfcaf90 100644 --- a/mobile/apps/locker/lib/ui/drawer/account_section_widget.dart +++ b/mobile/apps/locker/lib/ui/drawer/account_section_widget.dart @@ -1,7 +1,11 @@ +import "package:ente_accounts/pages/change_email_dialog.dart"; import "package:ente_accounts/pages/delete_account_page.dart"; +import "package:ente_accounts/pages/password_entry_page.dart"; import "package:ente_accounts/services/user_service.dart"; import "package:ente_crypto_dart/ente_crypto_dart.dart"; import "package:ente_lock_screen/local_authentication_service.dart"; +import "package:ente_ui/components/alert_bottom_sheet.dart"; +import "package:ente_ui/components/buttons/gradient_button.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:ente_ui/utils/dialog_util.dart"; import "package:ente_utils/navigation_util.dart"; @@ -9,9 +13,9 @@ import "package:flutter/material.dart"; import "package:hugeicons/hugeicons.dart"; import "package:locker/l10n/l10n.dart"; import "package:locker/services/configuration.dart"; -import "package:locker/ui/components/change_email_dialog_locker.dart"; import "package:locker/ui/components/expandable_menu_item_widget.dart"; import "package:locker/ui/components/recovery_key_sheet.dart"; +import "package:locker/ui/pages/home_page.dart"; class AccountSectionWidget extends StatelessWidget { const AccountSectionWidget({super.key}); @@ -43,14 +47,7 @@ class AccountSectionWidget extends StatelessWidget { ); if (hasAuthenticated) { // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return const ChangeEmailDialogLocker(); - }, - barrierColor: Colors.black.withValues(alpha: 0.85), - barrierDismissible: false, - ); + showChangeEmailDialog(context); } }, ), @@ -83,6 +80,31 @@ class AccountSectionWidget extends StatelessWidget { } }, ), + ExpandableChildItem( + title: l10n.changePassword, + trailingIcon: Icons.chevron_right, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToChangeYourPassword, + ); + if (hasAuthenticated) { + // ignore: unawaited_futures + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return PasswordEntryPage( + Configuration.instance, + PasswordEntryMode.update, + const HomePage(), + ); + }, + ), + ); + } + }, + ), ExpandableChildItem( title: l10n.deleteAccount, textColor: colorScheme.warning500, @@ -106,14 +128,20 @@ class AccountSectionWidget extends StatelessWidget { } void _onLogoutTapped(BuildContext context) { - showChoiceActionSheet( + showAlertBottomSheet( context, - title: context.l10n.areYouSureYouWantToLogout, - firstButtonLabel: context.l10n.yesLogout, - isCritical: true, - firstButtonOnTap: () async { - await UserService.instance.logout(context); - }, + title: context.l10n.warning, + message: context.l10n.areYouSureYouWantToLogout, + assetPath: "assets/warning-grey.png", + buttons: [ + GradientButton( + buttonType: GradientButtonType.critical, + text: context.l10n.yesLogout, + onTap: () async { + await UserService.instance.logout(context); + }, + ), + ], ); } } diff --git a/mobile/apps/locker/lib/ui/pages/home_page.dart b/mobile/apps/locker/lib/ui/pages/home_page.dart index a6301d66827..53797f1fbbd 100644 --- a/mobile/apps/locker/lib/ui/pages/home_page.dart +++ b/mobile/apps/locker/lib/ui/pages/home_page.dart @@ -311,6 +311,8 @@ class _HomePageState extends UploaderPageState title: l10n.sessionExpired, message: l10n.pleaseLoginAgain, assetPath: "assets/warning-grey.png", + isDismissible: false, + showCloseButton: false, buttons: [ SizedBox( width: double.infinity, diff --git a/mobile/apps/locker/lib/ui/pages/login_page.dart b/mobile/apps/locker/lib/ui/pages/login_page.dart deleted file mode 100644 index f62d7451a72..00000000000 --- a/mobile/apps/locker/lib/ui/pages/login_page.dart +++ /dev/null @@ -1,471 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:ente_accounts/ente_accounts.dart'; -import 'package:ente_accounts/models/errors.dart'; -import "package:ente_crypto_dart/ente_crypto_dart.dart"; -import 'package:ente_ui/components/alert_bottom_sheet.dart'; -import 'package:ente_ui/components/buttons/button_widget.dart'; -import 'package:ente_ui/theme/ente_theme.dart'; -import 'package:ente_ui/utils/dialog_util.dart'; -import 'package:flutter/material.dart'; -import 'package:locker/l10n/l10n.dart'; -import 'package:locker/services/configuration.dart'; -import "package:locker/ui/components/gradient_button.dart"; -import 'package:logging/logging.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - final Logger _logger = Logger('LoginPageState'); - String? _email; - bool _emailIsValid = false; - bool _passwordVisible = false; - bool _isLoggingIn = false; - Color? _emailInputFieldColor; - final _passwordController = TextEditingController(); - final FocusNode _passwordFocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - _email = Configuration.instance.getEmail(); - if (_email != null) { - _emailIsValid = EmailValidator.validate(_email!); - } - } - - @override - void dispose() { - _passwordController.dispose(); - _passwordFocusNode.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - // Prevent multiple simultaneous login attempts - if (_isLoggingIn) return; - - setState(() { - _isLoggingIn = true; - }); - - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog( - context, - context.l10n.pleaseWait, - isDismissible: true, - ); - await dialog.show(); - - try { - await UserService.instance.setEmail(_email!); - Configuration.instance.resetVolatilePassword(); - SrpAttributes? attr; - bool isEmailVerificationEnabled = true; - try { - attr = await UserService.instance.getSrpAttributes(_email!); - isEmailVerificationEnabled = attr.isEmailMFAEnabled; - } catch (e) { - if (e is! SrpSetupNotCompleteError) { - _logger.severe('Error getting SRP attributes', e); - } - } - - await dialog.hide(); - - final password = _passwordController.text; - if (attr != null && !isEmailVerificationEnabled && password.isNotEmpty) { - await _verifyPassword(attr, password); - } else { - await UserService.instance.sendOtt( - context, - _email!, - isCreateAccountScreen: false, - purpose: 'login', - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe('Login error', e); - await showGenericErrorDialog(context: context, error: e); - } finally { - if (mounted) { - setState(() { - _isLoggingIn = false; - }); - } - } - } - - Future _verifyPassword( - SrpAttributes srpAttributes, - String password, - ) async { - final dialog = createProgressDialog( - context, - context.l10n.pleaseWait, - isDismissible: true, - ); - await dialog.show(); - - try { - await UserService.instance.verifyEmailViaPassword( - context, - srpAttributes, - password, - dialog, - ); - } on DioException catch (e, s) { - await dialog.hide(); - final enteErrCode = e.response?.data is Map - ? e.response?.data["code"] - : null; - if (e.response != null && e.response!.statusCode == 401) { - _logger.severe('Server reject, failed verify SRP login', e, s); - await showErrorDialog( - context, - context.l10n.incorrectPassword, - context.l10n.pleaseTryAgain, - ); - } else { - _logger.severe('API failure during SRP login ${e.type}', e, s); - if (e.type == DioExceptionType.connectionTimeout || - e.type == DioExceptionType.receiveTimeout || - e.type == DioExceptionType.sendTimeout) { - await showErrorDialog( - context, - context.l10n.noInternetConnection, - context.l10n.checkInternetConnection, - ); - } else if (enteErrCode != null && - enteErrCode == 'LOCKER_REGISTRATION_DISABLED') { - await showErrorDialog( - context, - context.l10n.oops, - context.l10n.unlockLockerNewUserBody, - ); - } else if (enteErrCode != null && - enteErrCode == 'LOCKER_ROLLOUT_LIMIT') { - await showErrorDialog( - context, - "We're out of beta seats for now", - "This preview access has reached capacity. We'll be opening it to more users soon.", - ); - } else { - await showErrorDialog( - context, - context.l10n.somethingWentWrong, - context.l10n.verificationFailedTryAgain, - ); - } - } - } catch (e, s) { - _logger.info('error during loginViaPassword', e); - await dialog.hide(); - if (e is LoginKeyDerivationError) { - _logger.severe('loginKey derivation error', e, s); - await UserService.instance.sendOtt( - context, - _email!, - isCreateAccountScreen: true, - ); - return; - } else if (e is KeyDerivationError) { - final dialogChoice = await showChoiceDialog( - context, - title: context.l10n.recreatePassword, - body: context.l10n.deviceCannotVerifyPassword, - firstButtonLabel: context.l10n.useRecoveryKey, - ); - if (dialogChoice!.action == ButtonAction.first) { - await UserService.instance.sendOtt( - context, - _email!, - isResetPasswordScreen: true, - ); - } - return; - } else { - _logger.severe('unexpected error while verifying password', e, s); - await _showContactSupportDialog( - context, - context.l10n.oops, - context.l10n.verificationFailedTryAgain, - ); - } - } - } - - Future _showContactSupportDialog( - BuildContext context, - String title, - String message, - ) async { - final dialogChoice = await showChoiceDialog( - context, - title: title, - body: message, - firstButtonLabel: context.l10n.contactSupport, - secondButtonLabel: context.l10n.ok, - ); - if (dialogChoice!.action == ButtonAction.first) { - final Uri emailUri = Uri( - scheme: 'mailto', - path: 'support@ente.io', - query: - 'subject=${Uri.encodeComponent(context.l10n.lockerLoginIssueSubject)}', - ); - if (await canLaunchUrl(emailUri)) { - await launchUrl(emailUri); - } - } - } - - void _handleForgotPassword() async { - if (_email == null || !_emailIsValid) { - await showErrorDialog( - context, - context.l10n.invalidEmail, - context.l10n.enterValidEmailFirst, - ); - return; - } - - await UserService.instance.sendOtt( - context, - _email!, - isResetPasswordScreen: true, - ); - } - - void _handleSignUp() { - showAlertBottomSheet( - context, - title: context.l10n.unlockLockerNewUserTitle, - message: context.l10n.unlockLockerNewUserBody, - assetPath: "assets/file_lock.png", - ); - } - - void updateEmail(String value) { - _email = value.trim(); - _emailIsValid = EmailValidator.validate(_email!); - } - - @override - Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); - final textTheme = getEnteTextTheme(context); - final l10n = context.l10n; - - return Scaffold( - backgroundColor: colorScheme.backgroundBase, - appBar: AppBar( - title: Image.asset( - 'assets/locker-logo-blue.png', - height: 28, - ), - centerTitle: true, - backgroundColor: colorScheme.backgroundBase, - elevation: 0, - leading: IconButton( - icon: Icon( - Icons.arrow_back, - color: colorScheme.primary700, - ), - onPressed: () => Navigator.of(context).pop(), - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: AutofillGroup( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 24), - Text( - l10n.emailIdLabel, - style: textTheme.bodyBold.copyWith( - color: colorScheme.textBase, - ), - ), - const SizedBox(height: 8), - TextFormField( - key: const ValueKey("emailInputField"), - autofillHints: const [AutofillHints.email], - decoration: InputDecoration( - hintText: l10n.emailIdHint, - hintStyle: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - fillColor: - _emailInputFieldColor ?? colorScheme.fillFaint, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(8), - ), - suffixIcon: _emailIsValid - ? Icon( - Icons.check, - size: 20, - color: colorScheme.primary700, - ) - : null, - ), - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - onChanged: (value) { - setState(() { - updateEmail(value); - if (_emailIsValid) { - _emailInputFieldColor = colorScheme.primary300 - .withValues(alpha: 0.1); - } else { - _emailInputFieldColor = null; - } - }); - }, - onFieldSubmitted: _emailIsValid - ? (value) => _passwordFocusNode.requestFocus() - : null, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: _email, - autofocus: true, - ), - const SizedBox(height: 24), - Text( - l10n.loginPasswordLabel, - style: textTheme.bodyBold.copyWith( - color: colorScheme.textBase, - ), - ), - const SizedBox(height: 8), - TextFormField( - key: const ValueKey("passwordInputField"), - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - hintText: l10n.loginPasswordHint, - hintStyle: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(8), - ), - fillColor: colorScheme.fillFaint, - suffixIcon: IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - color: colorScheme.textMuted, - size: 20, - ), - onPressed: () { - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ), - ), - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - controller: _passwordController, - autofocus: false, - autocorrect: false, - obscureText: !_passwordVisible, - keyboardType: TextInputType.visiblePassword, - focusNode: _passwordFocusNode, - onChanged: (_) { - setState(() {}); - }, - ), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: _handleForgotPassword, - child: Text( - l10n.forgotPassword, - style: textTheme.bodyBold.copyWith( - color: colorScheme.primary700, - decoration: TextDecoration.underline, - decorationColor: colorScheme.primary700, - ), - ), - ), - ), - ], - ), - ), - ), - ), - SizedBox( - width: double.infinity, - child: GradientButton( - onTap: _emailIsValid && - _passwordController.text.isNotEmpty && - !_isLoggingIn - ? () async { - await _handleLogin(); - } - : null, - text: l10n.logInAction, - ), - ), - const SizedBox(height: 16), - Center( - child: TextButton( - onPressed: _handleSignUp, - child: RichText( - text: TextSpan( - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - children: [ - TextSpan(text: '${l10n.dontHaveAccount} '), - TextSpan( - text: l10n.signUp, - style: textTheme.bodyBold.copyWith( - color: colorScheme.primary700, - decoration: TextDecoration.underline, - decorationColor: colorScheme.primary700, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(height: 24), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/apps/locker/lib/ui/pages/onboarding_page.dart b/mobile/apps/locker/lib/ui/pages/onboarding_page.dart index 6acd51d288d..dd91badc4b3 100644 --- a/mobile/apps/locker/lib/ui/pages/onboarding_page.dart +++ b/mobile/apps/locker/lib/ui/pages/onboarding_page.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:dots_indicator/dots_indicator.dart'; +import 'package:ente_accounts/pages/email_entry_page.dart'; import 'package:ente_accounts/pages/login_page.dart'; import 'package:ente_accounts/pages/password_entry_page.dart'; import 'package:ente_accounts/pages/password_reentry_page.dart'; -import 'package:ente_ui/components/alert_bottom_sheet.dart'; import 'package:ente_ui/components/buttons/button_widget.dart'; -import "package:ente_ui/components/buttons/models/button_result.dart"; import "package:ente_ui/pages/developer_settings_page.dart"; -import "package:ente_ui/pages/web_page.dart"; import "package:ente_ui/theme/ente_theme.dart"; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:flutter/material.dart'; @@ -121,37 +119,36 @@ class _OnboardingPageState extends State { scrolledUnderElevation: 0, centerTitle: true, ), - body: GestureDetector( - onTap: () async { - _developerModeTapCount++; - if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) { - _developerModeTapCount = 0; - final result = await showChoiceDialog( - context, - title: l10n.developerSettings, - firstButtonLabel: l10n.yes, - body: l10n.developerSettingsWarning, - isDismissible: false, - ); - if (result?.action == ButtonAction.first) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return DeveloperSettingsPage(Configuration.instance); - }, - ), + body: SafeArea( + child: GestureDetector( + onTap: () async { + _developerModeTapCount++; + if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) { + _developerModeTapCount = 0; + final result = await showChoiceDialog( + context, + title: l10n.developerSettings, + firstButtonLabel: l10n.yes, + body: l10n.developerSettingsWarning, + isDismissible: false, ); - setState(() {}); + if (result?.action == ButtonAction.first) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return DeveloperSettingsPage(Configuration.instance); + }, + ), + ); + setState(() {}); + } } - } - }, - child: CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 32), + }, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, child: Column( children: [ const SizedBox(height: 28), @@ -187,63 +184,51 @@ class _OnboardingPageState extends State { ), const SizedBox(height: 48), Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: GradientButton( - text: l10n.loginToEnteAccount, - backgroundColor: Colors.white, - textColor: colorScheme.primary700, - onTap: _navigateToSignInPage, - ), - ), - const SizedBox(height: 16), - Center( - child: TextButton( - onPressed: () async { - final result = - await showAlertBottomSheet( - context, - title: l10n.unlockLockerNewUserTitle, - message: l10n.unlockLockerNewUserBody, - assetPath: "assets/file_lock.png", - buttons: [ - GradientButton( - text: l10n.checkoutEntePhotos, - onTap: () => Navigator.of(context).pop( - ButtonResult(ButtonAction.first), + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + GradientButton( + text: l10n.loginToEnteAccount, + backgroundColor: Colors.white, + textColor: colorScheme.primary700, + onTap: _navigateToSignInPage, + ), + const SizedBox(height: 20), + Center( + child: GestureDetector( + onTap: _navigateToSignUpPage, + child: Text.rich( + TextSpan( + text: "${l10n.dontHaveAccount} ", + style: + getEnteTextTheme(context).body.copyWith( + color: Colors.white, + ), + children: [ + TextSpan( + text: l10n.signUp, + style: getEnteTextTheme(context) + .bodyBold + .copyWith( + color: Colors.white, + decoration: + TextDecoration.underline, + decorationColor: Colors.white, + ), + ), + ], ), ), - ], - ); - - if (result?.action == ButtonAction.first) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - l10n.checkoutEntePhotos, - "https://ente.io/", - ); - }, - ), - ); - } - }, - child: Text( - l10n.noAccountCta, - style: getEnteTextTheme(context).bodyBold.copyWith( - color: Colors.white, - decoration: TextDecoration.underline, - decorationColor: Colors.white, - ), - ), + ), + ), + ], ), ), - const SizedBox(height: 20), ], ), ), - ), - ], + ], + ), ), ), ); @@ -279,6 +264,39 @@ class _OnboardingPageState extends State { ); } + void _navigateToSignUpPage() { + Widget page; + if (Configuration.instance.getEncryptedToken() == null) { + page = EmailEntryPage(Configuration.instance); + } else { + // No key + if (Configuration.instance.getKeyAttributes() == null) { + // Never had a key + page = PasswordEntryPage( + Configuration.instance, + PasswordEntryMode.set, + const HomePage(), + ); + } else if (Configuration.instance.getKey() == null) { + // Yet to decrypt the key + page = PasswordReentryPage( + Configuration.instance, + const HomePage(), + ); + } else { + // All is well + page = const HomePage(); + } + } + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return page; + }, + ), + ); + } + void _navigateToSignInPage() { Widget page; if (Configuration.instance.getEncryptedToken() == null) { diff --git a/mobile/apps/locker/lib/ui/pages/recovery_key_page.dart b/mobile/apps/locker/lib/ui/pages/recovery_key_page.dart deleted file mode 100644 index 1d59e840692..00000000000 --- a/mobile/apps/locker/lib/ui/pages/recovery_key_page.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:ente_ui/components/text_input_widget.dart'; -import 'package:ente_ui/theme/ente_theme.dart'; -import 'package:flutter/material.dart'; -import "package:locker/ui/components/gradient_button.dart"; - -class RecoveryKeyPage extends StatefulWidget { - const RecoveryKeyPage({super.key}); - - @override - State createState() => _RecoveryKeyPageState(); -} - -class _RecoveryKeyPageState extends State { - String? _recoveryKey; - final ValueNotifier _submitNotifier = ValueNotifier(false); - - @override - void dispose() { - _submitNotifier.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - // TODO: Implement actual recovery key validation and login logic - // For now, just simulate a delay - await Future.delayed(const Duration(seconds: 1)); - } - - void _handleForgotRecoveryKey() { - // TODO: Implement forgot recovery key logic - } - - void _handleSignUp() { - // TODO: Implement sign up navigation or dialog - } - - @override - Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); - final textTheme = getEnteTextTheme(context); - - return Scaffold( - backgroundColor: colorScheme.backgroundBase, - appBar: AppBar( - backgroundColor: colorScheme.backgroundBase, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: colorScheme.primary700), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - "Locker", - style: textTheme.h3.copyWith( - color: colorScheme.primary700, - fontWeight: FontWeight.w600, - ), - ), - centerTitle: true, - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 48), - TextInputWidget( - label: "Recovery key", - hintText: "Enter your recovery key", - initialValue: _recoveryKey, - autoFocus: true, - textCapitalization: TextCapitalization.none, - onChange: (value) { - setState(() { - _recoveryKey = value; - }); - }, - submitNotifier: _submitNotifier, - onSubmit: (value) async { - _recoveryKey = value; - await _handleLogin(); - }, - shouldSurfaceExecutionStates: false, - ), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: _handleForgotRecoveryKey, - child: Text( - "Forgot Recovery key?", - style: textTheme.bodyBold.copyWith( - color: colorScheme.primary700, - decoration: TextDecoration.underline, - decorationColor: colorScheme.primary700, - ), - ), - ), - ), - ], - ), - ), - ), - SizedBox( - width: double.infinity, - child: GradientButton( - onTap: _recoveryKey != null && _recoveryKey!.isNotEmpty - ? () async { - _submitNotifier.value = !_submitNotifier.value; - } - : null, - text: "Log In", - ), - ), - const SizedBox(height: 16), - Center( - child: TextButton( - onPressed: _handleSignUp, - child: RichText( - text: TextSpan( - style: textTheme.body.copyWith( - color: colorScheme.textBase, - ), - children: [ - const TextSpan(text: "Don't have an account? "), - TextSpan( - text: "Sign up", - style: textTheme.bodyBold.copyWith( - color: colorScheme.primary700, - decoration: TextDecoration.underline, - decorationColor: colorScheme.primary700, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(height: 24), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/apps/locker/pubspec.yaml b/mobile/apps/locker/pubspec.yaml index d3cd80e8228..450b77a6621 100644 --- a/mobile/apps/locker/pubspec.yaml +++ b/mobile/apps/locker/pubspec.yaml @@ -89,6 +89,7 @@ flutter: assets: - assets/ + - assets/svg/ fonts: - family: Inter diff --git a/mobile/packages/accounts/lib/pages/change_email_dialog.dart b/mobile/packages/accounts/lib/pages/change_email_dialog.dart index 860b627918d..24273367a1c 100644 --- a/mobile/packages/accounts/lib/pages/change_email_dialog.dart +++ b/mobile/packages/accounts/lib/pages/change_email_dialog.dart @@ -1,6 +1,9 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_strings/ente_strings.dart'; -import 'package:ente_ui/utils/dialog_util.dart'; +import 'package:ente_ui/components/alert_bottom_sheet.dart'; +import 'package:ente_ui/components/base_bottom_sheet.dart'; +import 'package:ente_ui/components/buttons/gradient_button.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_utils/email_util.dart'; import 'package:flutter/material.dart'; @@ -12,70 +15,108 @@ class ChangeEmailDialog extends StatefulWidget { } class _ChangeEmailDialogState extends State { - String _email = ""; + final _emailController = TextEditingController(); + String _email = ''; + bool _emailIsValid = false; + + @override + void initState() { + super.initState(); + _emailController.addListener(() { + setState(() { + _email = _emailController.text.trim(); + _emailIsValid = isValidEmail(_email); + }); + }); + } + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(context.strings.enterNewEmailHint), - content: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration( - hintText: context.strings.email, - hintStyle: const TextStyle( - color: Colors.white30, - ), - contentPadding: const EdgeInsets.all(12), - ), - onChanged: (value) { - setState(() { - _email = value; - }); - }, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: _email, - autofocus: true, + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _emailController, + decoration: InputDecoration( + fillColor: _emailIsValid + ? colorScheme.primary700.withValues(alpha: 0.2) + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterNewEmailHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, ), - ], - ), - ), - actions: [ - TextButton( - child: Text( - context.strings.cancel, - style: const TextStyle( - color: Colors.redAccent, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), ), - ), - onPressed: () { - Navigator.pop(context); - }, - ), - TextButton( - child: Text( - context.strings.verify, - style: const TextStyle( - color: Colors.purple, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), ), + suffixIcon: _emailIsValid + ? Icon( + Icons.check, + size: 20, + color: colorScheme.primary700, + ) + : null, + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, ), - onPressed: () { - if (!isValidEmail(_email)) { - showErrorDialog( - context, - context.strings.invalidEmailTitle, - context.strings.invalidEmailMessage, - ); - return; - } - UserService.instance.sendOtt(context, _email, isChangeEmail: true); - }, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + autofocus: true, + onFieldSubmitted: (_) => _handleSubmit(), + ), + const SizedBox(height: 24), + GradientButton( + text: context.strings.verify, + onTap: _emailIsValid ? _handleSubmit : null, ), ], ); } + + Future _handleSubmit() async { + if (!_emailIsValid) { + await showAlertBottomSheet( + context, + title: context.strings.invalidEmailTitle, + message: context.strings.invalidEmailMessage, + assetPath: 'assets/warning-grey.png', + ); + return; + } + + await UserService.instance.sendOtt( + context, + _email, + isChangeEmail: true, + ); + } +} + +Future showChangeEmailDialog(BuildContext context) { + return showBaseBottomSheet( + context, + title: context.strings.changeEmail, + headerSpacing: 20, + isKeyboardAware: true, + child: const ChangeEmailDialog(), + ); } diff --git a/mobile/packages/accounts/lib/pages/delete_account_page.dart b/mobile/packages/accounts/lib/pages/delete_account_page.dart index dc0afd66670..6d8332b74e3 100644 --- a/mobile/packages/accounts/lib/pages/delete_account_page.dart +++ b/mobile/packages/accounts/lib/pages/delete_account_page.dart @@ -76,7 +76,6 @@ class DeleteAccountPage extends StatelessWidget { ), GradientButton( text: context.strings.yesSendFeedbackAction, - iconData: Icons.check, onTap: () async { await sendEmail( context, diff --git a/mobile/packages/accounts/lib/pages/email_entry_page.dart b/mobile/packages/accounts/lib/pages/email_entry_page.dart index 62c49e29222..a6189ceeec5 100644 --- a/mobile/packages/accounts/lib/pages/email_entry_page.dart +++ b/mobile/packages/accounts/lib/pages/email_entry_page.dart @@ -1,3 +1,4 @@ +import 'package:dots_indicator/dots_indicator.dart'; import 'package:email_validator/email_validator.dart'; import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_configuration/base_configuration.dart'; @@ -7,8 +8,8 @@ import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:password_strength/password_strength.dart'; -import "package:step_progress_indicator/step_progress_indicator.dart"; import "package:styled_text/styled_text.dart"; class EmailEntryPage extends StatefulWidget { @@ -68,7 +69,7 @@ class _EmailEntryPageState extends State { // Initialize theme-aware color final colorScheme = getEnteColorScheme(context); - _validFieldValueColor = colorScheme.primary300.withOpacity(0.2); + _validFieldValueColor = colorScheme.primary700.withValues(alpha: 0.2); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -78,47 +79,70 @@ class _EmailEntryPageState extends State { } } - final appBar = AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Material( - type: MaterialType.transparency, - child: StepProgressIndicator( - totalSteps: 4, - currentStep: 1, - selectedColor: getEnteColorScheme(context).alternativeColor, - roundedEdges: const Radius.circular(10), - unselectedColor: - getEnteColorScheme(context).stepProgressUnselectedColor, - ), - ), - ); return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, - appBar: appBar, + backgroundColor: colorScheme.backgroundBase, + appBar: AppBar( + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: colorScheme.primary700, + onPressed: () => Navigator.of(context).pop(), + ), + ), body: _getBody(), - floatingActionButton: DynamicFAB( - isKeypadOpen: isKeypadOpen, - isFormValid: _isFormValid(), - buttonText: context.strings.createAccount, - onPressedFunction: () { - UserService.instance.setEmail(_email!); - widget.config.setVolatilePassword(_passwordController1.text); - UserService.instance.setRefSource(_referralSource); - UserService.instance.sendOtt( - context, - _email!, - isCreateAccountScreen: true, - purpose: "signup", - ); - FocusScope.of(context).unfocus(); - }, + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isKeypadOpen) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: DotsIndicator( + dotsCount: 3, + position: 0, + decorator: DotsDecorator( + activeColor: colorScheme.primary700, + color: colorScheme.primary700.withValues(alpha: 0.32), + activeShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + size: const Size(10, 10), + activeSize: const Size(20, 10), + spacing: const EdgeInsets.all(6), + ), + ), + ), + DynamicFAB( + isKeypadOpen: isKeypadOpen, + isFormValid: _isFormValid(), + buttonText: context.strings.createAccount, + onPressedFunction: () { + UserService.instance.setEmail(_email!); + widget.config.setVolatilePassword(_passwordController1.text); + UserService.instance.setRefSource(_referralSource); + UserService.instance.sendOtt( + context, + _email!, + isCreateAccountScreen: true, + purpose: "signup", + ); + FocusScope.of(context).unfocus(); + }, + ), + ], ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -126,6 +150,9 @@ class _EmailEntryPageState extends State { } Widget _getBody() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + var passwordStrengthText = context.strings.weakStrength; var passwordStrengthColor = Colors.redAccent; if (_passwordStrength > kStrongPasswordStrengthThreshold) { @@ -135,266 +162,294 @@ class _EmailEntryPageState extends State { passwordStrengthText = context.strings.moderateStrength; passwordStrengthColor = Colors.orangeAccent; } - return Column( - children: [ - Expanded( - child: AutofillGroup( - child: ListView( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.strings.createNewAccount, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - style: Theme.of(context).textTheme.titleMedium, - autofillHints: const [AutofillHints.email], - decoration: InputDecoration( - fillColor: _emailIsValid ? _validFieldValueColor : null, - filled: true, - hintText: context.strings.email, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _emailIsValid - ? Icon( - Icons.check, - size: 20, - color: getEnteColorScheme(context).primary300, - ) - : null, - ), - onChanged: (value) { - _email = value.trim(); - if (_emailIsValid != EmailValidator.validate(_email!)) { - setState(() { - _emailIsValid = EmailValidator.validate(_email!); - }); - } - }, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - //initialValue: _email, - textInputAction: TextInputAction.next, - ), - ), - const Padding(padding: EdgeInsets.all(4)), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - keyboardType: TextInputType.text, - textInputAction: TextInputAction.next, - controller: _passwordController1, - obscureText: !_password1Visible, - enableSuggestions: true, - autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( - fillColor: - _passwordIsValid ? _validFieldValueColor : null, - filled: true, - hintText: context.strings.password, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - suffixIcon: _password1InFocus - ? IconButton( - icon: Icon( - _password1Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password1Visible = !_password1Visible; - }); - }, - ) - : _passwordIsValid - ? Icon( - Icons.check, - color: getEnteColorScheme(context).primary300, - ) - : null, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), - focusNode: _password1FocusNode, - onChanged: (password) { - if (password != _password) { - setState(() { - _password = password; - _passwordStrength = - estimatePasswordStrength(password); - _passwordIsValid = _passwordStrength >= - kMildPasswordStrengthThreshold; - _passwordsMatch = _password == _cnfPassword; - }); - } - }, - onEditingComplete: () { - _password1FocusNode.unfocus(); - _password2FocusNode.requestFocus(); - TextInput.finishAutofillContext(); - }, - ), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - keyboardType: TextInputType.visiblePassword, - controller: _passwordController2, - obscureText: !_password2Visible, - autofillHints: const [AutofillHints.newPassword], - onEditingComplete: () => TextInput.finishAutofillContext(), - decoration: InputDecoration( - fillColor: _passwordsMatch && _passwordIsValid - ? _validFieldValueColor - : null, - filled: true, - hintText: context.strings.confirmPassword, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - suffixIcon: _password2InFocus - ? IconButton( - icon: Icon( - _password2Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, + return SafeArea( + child: Column( + children: [ + Expanded( + child: AutofillGroup( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + context.strings.email, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + autofillHints: const [AutofillHints.email], + decoration: InputDecoration( + fillColor: _emailIsValid + ? _validFieldValueColor + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.emailHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _emailIsValid + ? Icon( + Icons.check, + size: 20, + color: colorScheme.primary700, + ) + : null, + ), + onChanged: (value) { + _email = value.trim(); + if (_emailIsValid != + EmailValidator.validate(_email!)) { + setState(() { + _emailIsValid = + EmailValidator.validate(_email!); + }); + } + }, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + Text( + context.strings.password, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + controller: _passwordController1, + obscureText: !_password1Visible, + enableSuggestions: true, + autofillHints: const [AutofillHints.newPassword], + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + decoration: InputDecoration( + fillColor: _passwordIsValid + ? _validFieldValueColor + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterYourPasswordHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _password1InFocus + ? IconButton( + icon: Icon( + _password1Visible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, + ), + onPressed: () { + setState(() { + _password1Visible = !_password1Visible; + }); + }, + ) + : _passwordIsValid + ? Icon( + Icons.check, + color: colorScheme.primary700, + ) + : null, + ), + focusNode: _password1FocusNode, + onChanged: (password) { + if (password != _password) { + setState(() { + _password = password; + _passwordStrength = + estimatePasswordStrength(password); + _passwordIsValid = _passwordStrength >= + kMildPasswordStrengthThreshold; + _passwordsMatch = _password == _cnfPassword; + }); + } + }, + onEditingComplete: () { + _password1FocusNode.unfocus(); + _password2FocusNode.requestFocus(); + TextInput.finishAutofillContext(); + }, + ), + const SizedBox(height: 16), + Text( + context.strings.confirmPassword, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + keyboardType: TextInputType.visiblePassword, + controller: _passwordController2, + obscureText: !_password2Visible, + autofillHints: const [AutofillHints.newPassword], + onEditingComplete: () => + TextInput.finishAutofillContext(), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + decoration: InputDecoration( + fillColor: _passwordsMatch && _passwordIsValid + ? _validFieldValueColor + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.reEnterPassword, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _password2InFocus + ? IconButton( + icon: Icon( + _password2Visible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, + ), + onPressed: () { + setState(() { + _password2Visible = !_password2Visible; + }); + }, + ) + : _passwordsMatch + ? Icon( + Icons.check, + color: colorScheme.primary700, + ) + : null, + ), + focusNode: _password2FocusNode, + onChanged: (cnfPassword) { + setState(() { + _cnfPassword = cnfPassword; + if (_password != null || _password != '') { + _passwordsMatch = _password == _cnfPassword; + } + }); + }, + ), + Opacity( + opacity: + (_password != '') && _password1InFocus ? 1 : 0, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + context.strings + .passwordStrength(passwordStrengthText), + style: TextStyle( + color: passwordStrengthColor, + fontWeight: FontWeight.w500, + fontSize: 12, ), - onPressed: () { - setState(() { - _password2Visible = !_password2Visible; - }); - }, - ) - : _passwordsMatch - ? Icon( - Icons.check, - color: getEnteColorScheme(context).primary300, - ) - : null, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), - focusNode: _password2FocusNode, - onChanged: (cnfPassword) { - setState(() { - _cnfPassword = cnfPassword; - if (_password != null || _password != '') { - _passwordsMatch = _password == _cnfPassword; - } - }); - }, - ), - ), - Opacity( - opacity: (_password != '') && _password1InFocus ? 1 : 0, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: Text( - context.strings.passwordStrength(passwordStrengthText), - style: TextStyle( - color: passwordStrengthColor, - fontWeight: FontWeight.w500, - fontSize: 12, - ), - ), - ), - ), - const SizedBox(height: 4), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 20), - child: Text( - context.strings.hearUsWhereTitle, - style: getEnteTextTheme(context).smallFaint, - ), - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - style: Theme.of(context).textTheme.titleMedium, - decoration: InputDecoration( - fillColor: null, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: InkWell( - onTap: () { - showToast( - context, - context.strings.hearUsExplanation, - ); - }, - child: Icon( - Icons.info_outline_rounded, - color: getEnteColorScheme(context).strokeMuted, + ), + ), + ), + const SizedBox(height: 8), + Text( + context.strings.hearUsWhereTitle, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + decoration: InputDecoration( + fillColor: colorScheme.backdropBase, + filled: true, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (value) { + _referralSource = value.trim(); + }, + autocorrect: false, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, ), - ), + ], ), - onChanged: (value) { - _referralSource = value.trim(); - }, - autocorrect: false, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.next, ), - ), - const Divider(thickness: 1), - const SizedBox(height: 12), - _getAgreement(), - const SizedBox(height: 40), - ], + const SizedBox(height: 16), + Column( + children: [ + _getTOSAgreement(), + _getPasswordAgreement(), + ], + ), + ], + ), ), ), - ), - ], - ); - } - - Container _getAgreement() { - return Container( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Column( - children: [ - _getTOSAgreement(), - _getPasswordAgreement(), ], ), ); } Widget _getTOSAgreement() { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + return GestureDetector( onTap: () { setState(() { @@ -407,6 +462,15 @@ class _EmailEntryPageState extends State { Checkbox( value: _hasAgreedToTOS, side: CheckboxTheme.of(context).side, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return colorScheme.primary700; + } + return Colors.transparent; + }), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), onChanged: (value) { setState(() { _hasAgreedToTOS = value!; @@ -416,10 +480,7 @@ class _EmailEntryPageState extends State { Expanded( child: StyledText( text: context.strings.signUpTerms, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 12), + style: textTheme.small.copyWith(color: colorScheme.textMuted), tags: { 'u-terms': StyledTextActionTag( (String? text, Map attrs) => @@ -452,6 +513,9 @@ class _EmailEntryPageState extends State { } Widget _getPasswordAgreement() { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + return GestureDetector( onTap: () { setState(() { @@ -464,6 +528,15 @@ class _EmailEntryPageState extends State { Checkbox( value: _hasAgreedToE2E, side: CheckboxTheme.of(context).side, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return colorScheme.primary700; + } + return Colors.transparent; + }), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), onChanged: (value) { setState(() { _hasAgreedToE2E = value!; @@ -473,10 +546,7 @@ class _EmailEntryPageState extends State { Expanded( child: StyledText( text: context.strings.ackPasswordLostWarning, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 12), + style: textTheme.small.copyWith(color: colorScheme.textMuted), tags: { 'underline': StyledTextActionTag( (String? text, Map attrs) => @@ -504,6 +574,4 @@ class _EmailEntryPageState extends State { _hasAgreedToE2E && _passwordIsValid; } - - void showToast(BuildContext context, String hearUsExplanation) {} } diff --git a/mobile/packages/accounts/lib/pages/login_page.dart b/mobile/packages/accounts/lib/pages/login_page.dart index 3caeebea2c7..0aebc313087 100644 --- a/mobile/packages/accounts/lib/pages/login_page.dart +++ b/mobile/packages/accounts/lib/pages/login_page.dart @@ -7,6 +7,7 @@ import 'package:ente_ui/components/buttons/dynamic_fab.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_utils/platform_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:logging/logging.dart'; import "package:styled_text/tags/styled_text_tag_action.dart"; import "package:styled_text/widgets/styled_text.dart"; @@ -70,6 +71,7 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -81,11 +83,22 @@ class _LoginPageState extends State { return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, @@ -104,6 +117,9 @@ class _LoginPageState extends State { } Widget _getBody() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Column( children: [ Expanded( @@ -111,102 +127,102 @@ class _LoginPageState extends State { child: ListView( children: [ Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.strings.welcomeBack, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - autofillHints: const [AutofillHints.email], - onFieldSubmitted: - _emailIsValid ? (value) => onPressed() : null, - decoration: InputDecoration( - fillColor: _emailInputFieldColor, - filled: true, - hintText: context.strings.email, - contentPadding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 15, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + context.strings.email, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), + const SizedBox(height: 8), + TextFormField( + autofillHints: const [AutofillHints.email], + onFieldSubmitted: + _emailIsValid ? (value) => onPressed() : null, + decoration: InputDecoration( + fillColor: + _emailInputFieldColor ?? colorScheme.backdropBase, + filled: true, + hintText: context.strings.emailHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _emailIsValid + ? Icon( + Icons.check, + size: 20, + color: colorScheme.primary700, + ) + : null, + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + onChanged: (value) { + setState(() { + _email = value.trim(); + _emailIsValid = EmailValidator.validate(_email!); + if (_emailIsValid) { + _emailInputFieldColor = + getEnteColorScheme(context) + .primary700 + .withValues(alpha: 0.2); + } else { + _emailInputFieldColor = null; + } + }); + }, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + //initialValue: _email, + autofocus: true, ), - suffixIcon: _emailIsValid - ? Icon( - Icons.check, - size: 20, - color: getEnteColorScheme(context).primary300, - ) - : null, - ), - onChanged: (value) { - setState(() { - _email = value.trim(); - _emailIsValid = EmailValidator.validate(_email!); - if (_emailIsValid) { - _emailInputFieldColor = getEnteColorScheme(context) - .primary300 - .withOpacity(0.2); - } else { - _emailInputFieldColor = null; - } - }); - }, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - //initialValue: _email, - autofocus: true, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Expanded( - flex: 5, - child: StyledText( - text: context.strings.loginTerms, - style: getEnteTextTheme(context).small, - tags: { - 'u-terms': StyledTextActionTag( - (String? text, Map attrs) => - PlatformUtil.openWebView( - context, - context.strings.termsOfServicesTitle, - "https://ente.io/terms", - ), - style: const TextStyle( - decoration: TextDecoration.underline, - ), + const SizedBox(height: 24), + StyledText( + text: context.strings.loginTerms, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + tags: { + 'u-terms': StyledTextActionTag( + (String? text, Map attrs) => + PlatformUtil.openWebView( + context, + context.strings.termsOfServicesTitle, + "https://ente.io/terms", ), - 'u-policy': StyledTextActionTag( - (String? text, Map attrs) => - PlatformUtil.openWebView( - context, - context.strings.privacyPolicyTitle, - "https://ente.io/privacy", - ), - style: const TextStyle( - decoration: TextDecoration.underline, - ), + style: TextStyle( + decoration: TextDecoration.underline, + color: colorScheme.textMuted, ), - }, - ), - ), - Expanded( - flex: 2, - child: Container(), + ), + 'u-policy': StyledTextActionTag( + (String? text, Map attrs) => + PlatformUtil.openWebView( + context, + context.strings.privacyPolicyTitle, + "https://ente.io/privacy", + ), + style: TextStyle( + decoration: TextDecoration.underline, + color: colorScheme.textMuted, + ), + ), + }, ), ], ), @@ -215,7 +231,6 @@ class _LoginPageState extends State { ), ), ), - const Padding(padding: EdgeInsets.all(8)), ], ); } diff --git a/mobile/packages/accounts/lib/pages/login_pwd_verification_page.dart b/mobile/packages/accounts/lib/pages/login_pwd_verification_page.dart index 48edb06d8ce..ec125c22213 100644 --- a/mobile/packages/accounts/lib/pages/login_pwd_verification_page.dart +++ b/mobile/packages/accounts/lib/pages/login_pwd_verification_page.dart @@ -3,12 +3,14 @@ import "package:ente_accounts/ente_accounts.dart"; import "package:ente_configuration/base_configuration.dart"; import "package:ente_crypto_dart/ente_crypto_dart.dart"; import "package:ente_strings/ente_strings.dart"; -import "package:ente_ui/components/buttons/button_widget.dart"; +import "package:ente_ui/components/alert_bottom_sheet.dart"; import "package:ente_ui/components/buttons/dynamic_fab.dart"; +import "package:ente_ui/components/buttons/gradient_button.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:ente_ui/utils/dialog_util.dart"; import "package:ente_utils/email_util.dart"; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import "package:logging/logging.dart"; // LoginPasswordVerificationPage is a page that allows the user to enter their password to verify their identity. @@ -34,10 +36,10 @@ class _LoginPasswordVerificationPageState extends State { final _logger = Logger((_LoginPasswordVerificationPageState).toString()); final _passwordController = TextEditingController(); - final FocusNode _passwordFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); String? email; - bool _passwordInFocus = false; bool _passwordVisible = false; + bool _passwordInFocus = false; Future onPressed() async { FocusScope.of(context).unfocus(); @@ -58,6 +60,7 @@ class _LoginPasswordVerificationPageState @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -69,11 +72,22 @@ class _LoginPasswordVerificationPageState return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, @@ -108,40 +122,28 @@ class _LoginPasswordVerificationPageState ); } on DioException catch (e, s) { await dialog.hide(); - final String? enteErrCode = e.response?.data["code"]; - if (enteErrCode != null && - enteErrCode == 'LOCKER_REGISTRATION_DISABLED') { - await _showContactSupportDialog( - context, - context.strings.oops, - context.strings.lockerExistingUserRequired, - ); - } else if (enteErrCode != null && enteErrCode == 'LOCKER_ROLLOUT_LIMIT') { - await _showContactSupportDialog( - context, - "We're out of beta seats for now", - "This preview access has reached capacity. We'll be opening it to more users soon.", - ); - } else if (e.response != null && e.response!.statusCode == 401) { + + if (e.response != null && e.response!.statusCode == 401) { _logger.severe('server reject, failed verify SRP login', e, s); await _showContactSupportDialog( context, - context.strings.incorrectPasswordTitle, - context.strings.pleaseTryAgain, + title: context.strings.incorrectPasswordTitle, + message: context.strings.pleaseTryAgain, ); } else { _logger.severe('API failure during SRP login', e, s); if (e.type == DioExceptionType.connectionError) { await _showContactSupportDialog( context, - context.strings.noInternetConnection, - context.strings.pleaseCheckYourInternetConnectionAndTryAgain, + title: context.strings.noInternetConnection, + message: + context.strings.pleaseCheckYourInternetConnectionAndTryAgain, ); } else { await _showContactSupportDialog( context, - context.strings.oops, - context.strings.verificationFailedPleaseTryAgain, + title: context.strings.oops, + message: context.strings.verificationFailedPleaseTryAgain, ); } } @@ -159,13 +161,21 @@ class _LoginPasswordVerificationPageState return; } else if (e is KeyDerivationError) { // device is not powerful enough to perform derive key - final dialogChoice = await showChoiceDialog( + final result = await showAlertBottomSheet( context, title: context.strings.recreatePasswordTitle, - body: context.strings.recreatePasswordBody, - firstButtonLabel: context.strings.useRecoveryKey, + message: context.strings.recreatePasswordBody, + assetPath: 'assets/warning-grey.png', + buttons: [ + GradientButton( + text: context.strings.useRecoveryKey, + onTap: () { + Navigator.of(context).pop(true); + }, + ), + ], ); - if (dialogChoice!.action == ButtonAction.first) { + if (result == true) { await UserService.instance.sendOtt( context, email!, @@ -177,26 +187,33 @@ class _LoginPasswordVerificationPageState _logger.severe('unexpected error while verifying password', e, s); await _showContactSupportDialog( context, - context.strings.oops, - context.strings.verificationFailedPleaseTryAgain, + title: context.strings.oops, + message: context.strings.verificationFailedPleaseTryAgain, ); } } } Future _showContactSupportDialog( - BuildContext context, - String title, - String message, - ) async { - final dialogChoice = await showChoiceDialog( + BuildContext context, { + required String title, + required String message, + }) async { + final result = await showAlertBottomSheet( context, title: title, - body: message, - firstButtonLabel: context.strings.contactSupport, - secondButtonLabel: context.strings.ok, + message: message, + assetPath: 'assets/warning-grey.png', + buttons: [ + GradientButton( + text: context.strings.contactSupport, + onTap: () { + Navigator.of(context).pop(true); + }, + ), + ], ); - if (dialogChoice!.action == ButtonAction.first) { + if (result == true) { await sendLogs( context, "support@ente.io", @@ -206,6 +223,9 @@ class _LoginPasswordVerificationPageState } Widget _getBody() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Column( children: [ Expanded( @@ -213,142 +233,147 @@ class _LoginPasswordVerificationPageState child: ListView( children: [ Padding( - padding: const EdgeInsets.only(top: 30, left: 20, right: 20), - child: Text( - context.strings.enterPassword, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 22, - right: 20, - ), - child: Text( - email ?? '', - style: getEnteTextTheme(context).smallMuted, - ), - ), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - onFieldSubmitted: _passwordController.text.isNotEmpty - ? (_) => onPressed() - : null, - key: const ValueKey("passwordInputField"), - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - hintText: context.strings.enterYourPasswordHint, - filled: true, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _passwordInFocus - ? IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ) - : null, - ), - style: const TextStyle( - fontSize: 14, - ), - controller: _passwordController, - autofocus: true, - autocorrect: false, - obscureText: !_passwordVisible, - keyboardType: TextInputType.visiblePassword, - focusNode: _passwordFocusNode, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - await UserService.instance.sendOtt( - context, - email!, - isResetPasswordScreen: true, - ); - }, - child: Center( - child: Text( - context.strings.forgotPassword, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + const SizedBox(height: 24), + Text.rich( + TextSpan( + text: "${context.strings.enterThePasswordFor} ", + style: textTheme.body.copyWith( + color: colorScheme.textMuted, ), + children: [ + TextSpan( + text: email, + style: textTheme.body.copyWith( + color: colorScheme.primary700, + ), + ), + ], ), ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final dialog = createProgressDialog( - context, - context.strings.pleaseWait, - ); - await dialog.show(); - await widget.config.logout(); - await dialog.hide(); - Navigator.of(context) - .popUntil((route) => route.isFirst); - }, - child: Center( - child: Text( - context.strings.changeEmail, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + const SizedBox(height: 24), + Text( + context.strings.password, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), + ), + TextFormField( + onFieldSubmitted: _passwordController.text.isNotEmpty + ? (_) => onPressed() + : null, + key: const ValueKey("passwordInputField"), + autofillHints: const [AutofillHints.password], + decoration: InputDecoration( + fillColor: colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterYourPasswordHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _passwordInFocus + ? IconButton( + icon: Icon( + _passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, + ), + onPressed: () { + setState(() { + _passwordVisible = !_passwordVisible; + }); + }, + ) + : null, + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, ), + controller: _passwordController, + autofocus: true, + autocorrect: false, + obscureText: !_passwordVisible, + keyboardType: TextInputType.visiblePassword, + focusNode: _passwordFocusNode, + onChanged: (_) { + setState(() {}); + }, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + await UserService.instance.sendOtt( + context, + email!, + isResetPasswordScreen: true, + ); + }, + child: Text( + "${context.strings.forgotPassword}?", + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + final dialog = createProgressDialog( + context, + context.strings.pleaseWait, + ); + await dialog.show(); + await widget.config.logout(); + await dialog.hide(); + Navigator.of(context) + .popUntil((route) => route.isFirst); + }, + child: Text( + context.strings.changeEmail, + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), + ], ), ], ), diff --git a/mobile/packages/accounts/lib/pages/ott_verification_page.dart b/mobile/packages/accounts/lib/pages/ott_verification_page.dart index cc49d1f88d4..c85e27e18a4 100644 --- a/mobile/packages/accounts/lib/pages/ott_verification_page.dart +++ b/mobile/packages/accounts/lib/pages/ott_verification_page.dart @@ -1,10 +1,11 @@ +import 'package:dots_indicator/dots_indicator.dart'; import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_strings/ente_strings.dart'; import 'package:ente_ui/components/buttons/dynamic_fab.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:flutter/material.dart'; -import 'package:step_progress_indicator/step_progress_indicator.dart'; -import 'package:styled_text/styled_text.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:pinput/pinput.dart'; class OTTVerificationPage extends StatefulWidget { final String email; @@ -25,28 +26,35 @@ class OTTVerificationPage extends StatefulWidget { } class _OTTVerificationPageState extends State { - final _verificationCodeController = TextEditingController(); + final _pinController = TextEditingController(); Future onPressed() async { if (widget.isChangeEmail) { await UserService.instance.changeEmail( context, widget.email, - _verificationCodeController.text, + _pinController.text, ); } else { await UserService.instance.verifyEmail( context, - _verificationCodeController.text, + _pinController.text, isResettingPasswordScreen: widget.isResetPasswordScreen, ); } FocusScope.of(context).unfocus(); } + @override + void dispose() { + _pinController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -58,35 +66,59 @@ class _OTTVerificationPageState extends State { return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, ), - title: widget.isCreateAccountScreen - ? Material( - type: MaterialType.transparency, - child: StepProgressIndicator( - totalSteps: 4, - currentStep: 2, - selectedColor: getEnteColorScheme(context).alternativeColor, - roundedEdges: const Radius.circular(10), - unselectedColor: - getEnteColorScheme(context).stepProgressUnselectedColor, - ), - ) - : null, ), body: _getBody(), - floatingActionButton: DynamicFAB( - isKeypadOpen: isKeypadOpen, - isFormValid: _verificationCodeController.text.isNotEmpty, - buttonText: context.strings.verify, - onPressedFunction: onPressed, + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.isCreateAccountScreen && !isKeypadOpen) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: DotsIndicator( + dotsCount: 3, + position: 1, + decorator: DotsDecorator( + activeColor: colorScheme.primary700, + color: colorScheme.primary700.withValues(alpha: 0.32), + activeShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + size: const Size(10, 10), + activeSize: const Size(20, 10), + spacing: const EdgeInsets.all(6), + ), + ), + ), + DynamicFAB( + isKeypadOpen: isKeypadOpen, + isFormValid: _pinController.text.length == 6, + buttonText: context.strings.verify, + onPressedFunction: onPressed, + ), + ], ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -94,128 +126,118 @@ class _OTTVerificationPageState extends State { } Widget _getBody() { - return ListView( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 30, 20, 15), - child: Text( - context.strings.verifyEmail, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 12), - child: StyledText( - text: - context.strings.weHaveSendEmailTo(widget.email), - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - tags: { - 'green': StyledTextTag( - style: TextStyle( - color: getEnteColorScheme(context) - .alternativeColor, - ), - ), - }, - ), - ), - widget.isResetPasswordScreen - ? Text( - context.strings.toResetVerifyEmail, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - ) - : Text( - context.strings.checkInboxAndSpamFolder, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - ), - ], - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width * 0.2, - height: 1, - ), - ], + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + final defaultPinTheme = PinTheme( + height: 48, + width: 48, + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.strokeFaint, + width: 1.75, + ), + borderRadius: BorderRadius.circular(18), + ), + ); + + final focusedPinTheme = defaultPinTheme.copyWith( + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.primary700, + width: 1.75, + ), + borderRadius: BorderRadius.circular(18), + ), + ); + + final submittedPinTheme = defaultPinTheme.copyWith( + textStyle: textTheme.h3Bold.copyWith( + color: colorScheme.primary700, + ), + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.strokeFaint, + width: 1.75, + ), + borderRadius: BorderRadius.circular(18), + ), + ); + + return SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 24), + Image.asset('assets/upload_file.png'), + const SizedBox(height: 24), + Text( + context.strings.weHaveSentCode(widget.email), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + textAlign: TextAlign.center, ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), - child: TextFormField( - style: Theme.of(context).textTheme.titleMedium, - onFieldSubmitted: _verificationCodeController.text.isNotEmpty - ? (_) => onPressed() - : null, - decoration: InputDecoration( - filled: true, - hintText: context.strings.tapToEnterCode, - contentPadding: const EdgeInsets.all(15), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), + const SizedBox(height: 8), + Text( + context.strings.checkInboxAndSpamFolder, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, ), - controller: _verificationCodeController, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Pinput( + length: 6, + controller: _pinController, autofocus: true, - autocorrect: false, + defaultPinTheme: defaultPinTheme, + focusedPinTheme: focusedPinTheme, + submittedPinTheme: submittedPinTheme, + showCursor: false, keyboardType: TextInputType.number, + onCompleted: (value) { + if (value.length == 6) { + onPressed(); + } + }, onChanged: (_) { setState(() {}); }, ), - ), - const Divider( - thickness: 1, - ), - Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - UserService.instance.sendOtt( - context, - widget.email, - isCreateAccountScreen: widget.isCreateAccountScreen, - isChangeEmail: widget.isChangeEmail, - isResetPasswordScreen: widget.isResetPasswordScreen, - ); - }, - child: Text( - context.strings.resendEmail, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + UserService.instance.sendOtt( + context, + widget.email, + isCreateAccountScreen: widget.isCreateAccountScreen, + isChangeEmail: widget.isChangeEmail, + isResetPasswordScreen: widget.isResetPasswordScreen, + ); + }, + child: Text( + context.strings.resendCode, + style: textTheme.small.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, ), ), - ], + ), ), - ), - ], + ], + ), ), - ], + ), ); - // ); } } diff --git a/mobile/packages/accounts/lib/pages/passkey_page.dart b/mobile/packages/accounts/lib/pages/passkey_page.dart index 57b9f06afdc..2435c19bb2e 100644 --- a/mobile/packages/accounts/lib/pages/passkey_page.dart +++ b/mobile/packages/accounts/lib/pages/passkey_page.dart @@ -5,12 +5,14 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_accounts/models/errors.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_strings/ente_strings.dart'; -import 'package:ente_ui/components/buttons/button_widget.dart'; -import 'package:ente_ui/components/buttons/models/button_type.dart'; +import 'package:ente_ui/components/alert_bottom_sheet.dart'; +import 'package:ente_ui/components/buttons/gradient_button.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:ente_ui/utils/toast_util.dart'; import 'package:ente_utils/navigation_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -70,10 +72,11 @@ class _PasskeyPageState extends State { showToast(context, context.strings.passKeyPendingVerification); return; } on PassKeySessionExpiredError { - await showErrorDialog( + await showAlertBottomSheet( context, - context.strings.loginSessionExpired, - context.strings.loginSessionExpiredDetails, + title: context.strings.loginSessionExpired, + message: context.strings.loginSessionExpiredDetails, + assetPath: 'assets/warning-grey.png', ); Navigator.of(context).pop(); return; @@ -140,10 +143,28 @@ class _PasskeyPageState extends State { @override Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return Scaffold( + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( - title: Text( - context.strings.passkeyAuthTitle, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: colorScheme.primary700, + onPressed: () { + Navigator.of(context).pop(); + }, ), ), body: _getBody(), @@ -151,29 +172,37 @@ class _PasskeyPageState extends State { } Widget _getBody() { - return Center( + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + return SafeArea( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 12), + Text( + context.strings.passkeyAuthTitle, + style: textTheme.h3Bold.copyWith( + color: colorScheme.textBase, + ), + ), Text( context.strings.waitingForVerification, - style: const TextStyle( - height: 1.4, - fontSize: 16, + style: textTheme.body.copyWith( + color: colorScheme.textMuted, ), ), - const SizedBox(height: 16), - ButtonWidget( - buttonType: ButtonType.primary, - labelText: context.strings.tryAgain, + const SizedBox(height: 24), + GradientButton( + text: context.strings.tryAgain, onTap: () => launchPasskey(), ), const SizedBox(height: 16), - ButtonWidget( - buttonType: ButtonType.secondary, - labelText: context.strings.checkStatus, + GradientButton( + text: context.strings.checkStatus, + buttonType: GradientButtonType.secondary, onTap: () async { try { await checkStatus(); @@ -182,54 +211,51 @@ class _PasskeyPageState extends State { showGenericErrorDialog(context: context, error: e).ignore(); } }, - shouldSurfaceExecutionStates: true, ), - const Padding(padding: EdgeInsets.all(30)), - if (widget.totp2FASessionID.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - routeToPage( - context, - TwoFactorAuthenticationPage( - widget.totp2FASessionID, - ), - ); - }, - child: Container( - padding: const EdgeInsets.all(10), - child: Center( - child: Text( - context.strings.loginWithTOTP, - style: const TextStyle( - decoration: TextDecoration.underline, - fontSize: 12, - ), - ), - ), - ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - UserService.instance.recoverTwoFactor( - context, - widget.sessionID, - TwoFactorType.passkey, - ); - }, - child: Container( - padding: const EdgeInsets.all(10), - child: Center( + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + widget.totp2FASessionID.isNotEmpty + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + routeToPage( + context, + TwoFactorAuthenticationPage( + widget.totp2FASessionID, + ), + ); + }, + child: Text( + context.strings.loginWithTOTP, + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ) + : const SizedBox.shrink(), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.passkey, + ); + }, child: Text( context.strings.recoverAccount, - style: const TextStyle( + style: textTheme.body.copyWith( + color: colorScheme.primary700, decoration: TextDecoration.underline, - fontSize: 12, + decorationColor: colorScheme.primary700, ), ), ), - ), + ], ), ], ), diff --git a/mobile/packages/accounts/lib/pages/password_entry_page.dart b/mobile/packages/accounts/lib/pages/password_entry_page.dart index 85b6f63b25f..fbf04534190 100644 --- a/mobile/packages/accounts/lib/pages/password_entry_page.dart +++ b/mobile/packages/accounts/lib/pages/password_entry_page.dart @@ -1,8 +1,10 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_strings/ente_strings.dart'; +import "package:ente_ui/components/alert_bottom_sheet.dart"; +import "package:ente_ui/components/base_bottom_sheet.dart"; import 'package:ente_ui/components/buttons/dynamic_fab.dart'; -import 'package:ente_ui/components/buttons/models/button_type.dart'; +import "package:ente_ui/components/buttons/gradient_button.dart"; import 'package:ente_ui/pages/base_home_page.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_ui/utils/dialog_util.dart'; @@ -11,6 +13,7 @@ import 'package:ente_utils/navigation_util.dart'; import 'package:ente_utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:logging/logging.dart'; import 'package:password_strength/password_strength.dart'; import 'package:styled_text/styled_text.dart'; @@ -87,7 +90,6 @@ class _PasswordEntryPageState extends State { @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; - _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -97,6 +99,9 @@ class _PasswordEntryPageState extends State { } } + final colorScheme = getEnteColorScheme(context); + _validFieldValueColor = colorScheme.primary700.withValues(alpha: 0.2); + String title = context.strings.setPasswordTitle; if (widget.mode == PasswordEntryMode.update) { title = context.strings.changePasswordTitle; @@ -107,17 +112,28 @@ class _PasswordEntryPageState extends State { } return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: widget.mode == PasswordEntryMode.reset ? Container() : IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, ), - elevation: 0, ), body: _getBody(title), floatingActionButton: DynamicFAB( @@ -138,8 +154,11 @@ class _PasswordEntryPageState extends State { ); } - Widget _getBody(String buttonTextAndHeading) { + Widget _getBody(String title) { final email = widget.config.getEmail(); + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + var passwordStrengthText = context.strings.weakStrength; var passwordStrengthColor = Colors.redAccent; if (_passwordStrength > kStrongPasswordStrengthThreshold) { @@ -149,6 +168,7 @@ class _PasswordEntryPageState extends State { passwordStrengthText = context.strings.moderateStrength; passwordStrengthColor = Colors.orangeAccent; } + if (_volatilePassword != null) { return Container(); } @@ -161,228 +181,248 @@ class _PasswordEntryPageState extends State { child: ListView( children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 30, - horizontal: 20, - ), - child: Text( - buttonTextAndHeading, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - widget.mode == PasswordEntryMode.set - ? context.strings.enterPasswordToEncrypt - : context.strings.enterNewPasswordToEncrypt, - textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - ), - ), - const Padding(padding: EdgeInsets.all(8)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: StyledText( - text: context.strings.passwordWarning, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - tags: { - 'underline': StyledTextTag( - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + title, + style: textTheme.h3Bold.copyWith( + color: colorScheme.textBase, + ), ), - }, - ), - ), - const Padding(padding: EdgeInsets.all(12)), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - autofillHints: const [AutofillHints.newPassword], - onFieldSubmitted: (_) { - do { - FocusScope.of(context).nextFocus(); - } while (FocusScope.of(context).focusedChild!.context == - null); - }, - decoration: InputDecoration( - fillColor: _isPasswordValid - ? _validFieldValueColor - : getEnteColorScheme(context).fillFaint, - filled: true, - hintText: context.strings.password, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), + const SizedBox(height: 8), + Text( + widget.mode == PasswordEntryMode.set + ? context.strings.enterPasswordToEncrypt + : context.strings.enterNewPasswordToEncrypt, + textAlign: TextAlign.start, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), ), - suffixIcon: _password1InFocus - ? IconButton( - icon: Icon( - _password1Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password1Visible = !_password1Visible; - }); - }, - ) - : _isPasswordValid - ? Icon( - Icons.check, - color: - getEnteColorScheme(context).primary300, - ) - : null, - ), - obscureText: !_password1Visible, - controller: _passwordController1, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.visiblePassword, - onChanged: (password) { - setState(() { - _passwordInInputBox = password; - _passwordStrength = - estimatePasswordStrength(password); - _isPasswordValid = _passwordStrength >= - kMildPasswordStrengthThreshold; - _passwordsMatch = _passwordInInputBox == - _passwordInInputConfirmationBox; - }); - }, - textInputAction: TextInputAction.next, - focusNode: _password1FocusNode, - ), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - keyboardType: TextInputType.visiblePassword, - controller: _passwordController2, - obscureText: !_password2Visible, - autofillHints: const [AutofillHints.newPassword], - onEditingComplete: () => - TextInput.finishAutofillContext(), - decoration: InputDecoration( - fillColor: _passwordsMatch - ? _validFieldValueColor - : getEnteColorScheme(context).fillFaint, - filled: true, - hintText: context.strings.confirmPassword, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, + const SizedBox(height: 8), + StyledText( + text: context.strings.passwordWarning, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + tags: { + 'underline': StyledTextTag( + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + decoration: TextDecoration.underline, + ), + ), + }, + ), + const SizedBox(height: 24), + Text( + context.strings.password, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), ), - suffixIcon: _password2InFocus - ? IconButton( - icon: Icon( - _password2Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password2Visible = !_password2Visible; - }); - }, - ) - : _passwordsMatch - ? Icon( - Icons.check, - color: - getEnteColorScheme(context).primary300, + TextFormField( + autofillHints: const [AutofillHints.newPassword], + onFieldSubmitted: (_) { + do { + FocusScope.of(context).nextFocus(); + } while ( + FocusScope.of(context).focusedChild!.context == + null); + }, + decoration: InputDecoration( + fillColor: _isPasswordValid + ? _validFieldValueColor + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterYourPasswordHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: _password1InFocus + ? IconButton( + icon: Icon( + _password1Visible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, + ), + onPressed: () { + setState(() { + _password1Visible = !_password1Visible; + }); + }, ) - : null, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), + : _isPasswordValid + ? Icon( + Icons.check, + color: colorScheme.primary700, + ) + : null, + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + obscureText: !_password1Visible, + controller: _passwordController1, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + onChanged: (password) { + setState(() { + _passwordInInputBox = password; + _passwordStrength = + estimatePasswordStrength(password); + _isPasswordValid = _passwordStrength >= + kMildPasswordStrengthThreshold; + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + }); + }, + textInputAction: TextInputAction.next, + focusNode: _password1FocusNode, ), - ), - focusNode: _password2FocusNode, - onChanged: (cnfPassword) { - setState(() { - _passwordInInputConfirmationBox = cnfPassword; - if (_passwordInInputBox != '') { - _passwordsMatch = _passwordInInputBox == - _passwordInInputConfirmationBox; - } - }); - }, - ), - ), - Opacity( - opacity: (_passwordInInputBox != '') && _password1InFocus - ? 1 - : 0, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, - ), - child: Text( - context.strings.passwordStrength(passwordStrengthText), - style: TextStyle( - color: passwordStrengthColor, + Opacity( + opacity: + (_passwordInInputBox != '') && _password1InFocus + ? 1 + : 0, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + context.strings + .passwordStrength(passwordStrengthText), + style: TextStyle( + color: passwordStrengthColor, + ), + ), + ), ), - ), - ), - ), - const SizedBox(height: 8), - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - PlatformUtil.openWebView( - context, - context.strings.howItWorks, - "https://ente.io/architecture", - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: RichText( - text: TextSpan( - text: context.strings.howItWorks, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + const SizedBox(height: 8), + Text( + context.strings.confirmPassword, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + keyboardType: TextInputType.visiblePassword, + controller: _passwordController2, + obscureText: !_password2Visible, + autofillHints: const [AutofillHints.newPassword], + onEditingComplete: () => + TextInput.finishAutofillContext(), + decoration: InputDecoration( + fillColor: _passwordsMatch + ? _validFieldValueColor + : colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterYourPasswordHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + suffixIcon: _password2InFocus + ? IconButton( + icon: Icon( + _password2Visible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, + ), + onPressed: () { + setState(() { + _password2Visible = !_password2Visible; + }); + }, + ) + : _passwordsMatch + ? Icon( + Icons.check, + color: colorScheme.primary700, + ) + : null, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + focusNode: _password2FocusNode, + onChanged: (cnfPassword) { + setState(() { + _passwordInInputConfirmationBox = cnfPassword; + if (_passwordInInputBox != '') { + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + } + }); + }, ), - ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + PlatformUtil.openWebView( + context, + "${context.strings.howItWorks}?", + "https://ente.io/architecture", + ); + }, + child: Text( + "${context.strings.howItWorks}?", + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), + ), + const SizedBox(height: 20), + ], ), ), - const Padding(padding: EdgeInsets.all(20)), ], ), ), @@ -394,6 +434,9 @@ class _PasswordEntryPageState extends State { void _updatePassword() async { final logOutFromOthers = await logOutFromOtherDevices(context); + if (logOutFromOthers == null) { + return; + } final dialog = createProgressDialog(context, context.strings.generatingEncryptionKeys); await dialog.show(); @@ -422,23 +465,55 @@ class _PasswordEntryPageState extends State { } } - Future logOutFromOtherDevices(BuildContext context) async { - bool logOutFromOther = true; - await showChoiceDialog( + Future logOutFromOtherDevices(BuildContext context) async { + bool? logOutFromOther; + + await showBaseBottomSheet( context, + headerSpacing: 20, title: context.strings.signOutFromOtherDevices, - body: context.strings.signOutOtherBody, isDismissible: false, - firstButtonLabel: context.strings.signOutOtherDevices, - firstButtonType: ButtonType.critical, - firstButtonOnTap: () async { - logOutFromOther = true; - }, - secondButtonLabel: context.strings.doNotSignOut, - secondButtonOnTap: () async { - logOutFromOther = false; - }, + enableDrag: false, + showCloseButton: true, + child: Builder( + builder: (bottomSheetContext) { + final colorScheme = getEnteColorScheme(bottomSheetContext); + final textTheme = getEnteTextTheme(bottomSheetContext); + return Column( + children: [ + Text( + context.strings.signOutOtherBody, + style: textTheme.body.copyWith(color: colorScheme.textMuted), + ), + const SizedBox(height: 20), + GradientButton( + text: context.strings.doNotSignOut, + onTap: () { + logOutFromOther = false; + Navigator.of(bottomSheetContext).pop(); + }, + ), + const SizedBox(height: 20), + GestureDetector( + onTap: () { + logOutFromOther = true; + Navigator.of(bottomSheetContext).pop(); + }, + child: Text( + context.strings.signOutOtherDevices, + style: textTheme.bodyBold.copyWith( + color: colorScheme.warning400, + decoration: TextDecoration.underline, + decorationColor: colorScheme.warning400, + ), + ), + ), + ], + ); + }, + ), ); + return logOutFromOther; } @@ -504,10 +579,12 @@ class _PasswordEntryPageState extends State { await dialog.hide(); if (e is UnsupportedError) { // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.insecureDevice, - context.strings.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease, + title: context.strings.insecureDevice, + message: context + .strings.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease, + assetPath: 'assets/warning-grey.png', ); } else { // ignore: unawaited_futures diff --git a/mobile/packages/accounts/lib/pages/password_reentry_page.dart b/mobile/packages/accounts/lib/pages/password_reentry_page.dart index 4078cd1ffee..603f6659371 100644 --- a/mobile/packages/accounts/lib/pages/password_reentry_page.dart +++ b/mobile/packages/accounts/lib/pages/password_reentry_page.dart @@ -6,12 +6,15 @@ import 'package:ente_accounts/models/errors.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:ente_strings/ente_strings.dart'; -import 'package:ente_ui/components/buttons/button_widget.dart'; +import "package:ente_ui/components/alert_bottom_sheet.dart"; import 'package:ente_ui/components/buttons/dynamic_fab.dart'; +import "package:ente_ui/components/buttons/gradient_button.dart"; import 'package:ente_ui/pages/base_home_page.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:ente_utils/email_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:logging/logging.dart'; class PasswordReentryPage extends StatefulWidget { @@ -31,9 +34,7 @@ class PasswordReentryPage extends StatefulWidget { class _PasswordReentryPageState extends State { final _logger = Logger((_PasswordReentryPageState).toString()); final _passwordController = TextEditingController(); - final FocusNode _passwordFocusNode = FocusNode(); String? email; - bool _passwordInFocus = false; bool _passwordVisible = false; String? _volatilePassword; @@ -49,16 +50,12 @@ class _PasswordReentryPageState extends State { () => verifyPassword(_volatilePassword!, usingVolatilePassword: true), ); } - _passwordFocusNode.addListener(() { - setState(() { - _passwordInFocus = _passwordFocusNode.hasFocus; - }); - }); } @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { @@ -70,11 +67,22 @@ class _PasswordReentryPageState extends State { return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, @@ -85,7 +93,7 @@ class _PasswordReentryPageState extends State { key: const ValueKey("verifyPasswordButton"), isKeypadOpen: isKeypadOpen, isFormValid: _passwordController.text.isNotEmpty, - buttonText: context.strings.verifyPassword, + buttonText: context.strings.logInLabel, onPressedFunction: () async { FocusScope.of(context).unfocus(); await verifyPassword(_passwordController.text); @@ -115,13 +123,22 @@ class _PasswordReentryPageState extends State { } on KeyDerivationError catch (e, s) { _logger.severe("Password verification failed", e, s); await dialog.hide(); - final dialogChoice = await showChoiceDialog( + + final result = await showAlertBottomSheet( context, title: context.strings.recreatePasswordTitle, - body: context.strings.recreatePasswordBody, - firstButtonLabel: context.strings.useRecoveryKey, + message: context.strings.recreatePasswordBody, + assetPath: 'assets/warning-grey.png', + buttons: [ + GradientButton( + text: context.strings.useRecoveryKey, + onTap: () { + Navigator.of(context).pop(true); + }, + ), + ], ); - if (dialogChoice!.action == ButtonAction.first) { + if (result == true) { // ignore: unawaited_futures Navigator.of(context).push( MaterialPageRoute( @@ -138,14 +155,22 @@ class _PasswordReentryPageState extends State { } catch (e, s) { _logger.severe("Password verification failed", e, s); await dialog.hide(); - final dialogChoice = await showChoiceDialog( + + final result = await showAlertBottomSheet( context, title: context.strings.incorrectPasswordTitle, - body: context.strings.pleaseTryAgain, - firstButtonLabel: context.strings.contactSupport, - secondButtonLabel: context.strings.ok, + message: context.strings.pleaseTryAgain, + assetPath: 'assets/warning-grey.png', + buttons: [ + GradientButton( + text: context.strings.contactSupport, + onTap: () { + Navigator.of(context).pop(true); + }, + ), + ], ); - if (dialogChoice!.action == ButtonAction.first) { + if (result == true) { await sendLogs( context, "support@ente.io", @@ -189,6 +214,9 @@ class _PasswordReentryPageState extends State { } Widget _getBody() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Column( children: [ Expanded( @@ -196,134 +224,129 @@ class _PasswordReentryPageState extends State { child: ListView( children: [ Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.strings.welcomeBack, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - key: const ValueKey("passwordInputField"), - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - hintText: context.strings.enterYourPasswordHint, - filled: true, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _passwordInFocus - ? IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ) - : null, - ), - style: const TextStyle( - fontSize: 14, - ), - controller: _passwordController, - autofocus: true, - autocorrect: false, - obscureText: !_passwordVisible, - keyboardType: TextInputType.visiblePassword, - focusNode: _passwordFocusNode, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return RecoveryPage( - widget.config, - widget.homePage, - ); - }, + const SizedBox(height: 24), + Text( + context.strings.password, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), + ), + TextFormField( + key: const ValueKey("passwordInputField"), + autofillHints: const [AutofillHints.password], + decoration: InputDecoration( + fillColor: colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterYourPasswordHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: IconButton( + icon: Icon( + _passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: colorScheme.textMuted, + size: 20, ), - ); - }, - child: Center( - child: Text( - context.strings.forgotPassword, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + onPressed: () { + setState(() { + _passwordVisible = !_passwordVisible; + }); + }, ), ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final dialog = createProgressDialog( - context, - context.strings.pleaseWait, - ); - await dialog.show(); - await widget.config.logout(); - await dialog.hide(); - Navigator.of(context) - .popUntil((route) => route.isFirst); + style: textTheme.body.copyWith( + color: colorScheme.textBase, + ), + controller: _passwordController, + autofocus: true, + autocorrect: false, + obscureText: !_passwordVisible, + keyboardType: TextInputType.visiblePassword, + onChanged: (_) { + setState(() {}); }, - child: Center( - child: Text( - context.strings.changeEmail, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return RecoveryPage( + widget.config, + widget.homePage, + ); + }, ), + ); + }, + child: Text( + "${context.strings.forgotPassword}?", + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), ), - ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + final dialog = createProgressDialog( + context, + context.strings.pleaseWait, + ); + await dialog.show(); + await widget.config.logout(); + await dialog.hide(); + Navigator.of(context) + .popUntil((route) => route.isFirst); + }, + child: Text( + context.strings.changeEmail, + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), + ], ), ], ), diff --git a/mobile/packages/accounts/lib/pages/recovery_key_page.dart b/mobile/packages/accounts/lib/pages/recovery_key_page.dart index ada9cea37ea..a08d75a5ce3 100644 --- a/mobile/packages/accounts/lib/pages/recovery_key_page.dart +++ b/mobile/packages/accounts/lib/pages/recovery_key_page.dart @@ -2,21 +2,20 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:bip39/bip39.dart' as bip39; -import 'package:dotted_border/dotted_border.dart'; +import 'package:dots_indicator/dots_indicator.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_configuration/constants.dart'; import 'package:ente_strings/ente_strings.dart'; import 'package:ente_ui/components/buttons/gradient_button.dart'; import 'package:ente_ui/theme/ente_theme.dart'; -import "package:ente_ui/theme/ente_theme_data.dart"; import 'package:ente_ui/utils/toast_util.dart'; import 'package:ente_utils/platform_util.dart'; import 'package:ente_utils/share_utils.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:step_progress_indicator/step_progress_indicator.dart'; class RecoveryKeyPage extends StatefulWidget { final BaseConfiguration config; @@ -68,11 +67,8 @@ class _RecoveryKeyPageState extends State { 'recovery code should have $mnemonicKeyWordCount words', ); } - final double topPadding = widget.showAppBar! - ? 40 - : widget.showProgressBar - ? 32 - : 120; + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); Future copy() async { await Clipboard.setData( @@ -90,185 +86,147 @@ class _RecoveryKeyPageState extends State { } return Scaffold( - appBar: widget.showProgressBar - ? AppBar( - automaticallyImplyLeading: false, - elevation: 0, - title: Hero( - tag: "recovery_key", - child: StepProgressIndicator( - totalSteps: 4, - currentStep: 3, - selectedColor: getEnteColorScheme(context).alternativeColor, - roundedEdges: const Radius.circular(10), - unselectedColor: - getEnteColorScheme(context).stepProgressUnselectedColor, - ), + backgroundColor: colorScheme.backgroundBase, + appBar: AppBar( + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), + leading: widget.showAppBar == false + ? const SizedBox.shrink() + : IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + color: colorScheme.primary700, ), - ) - : widget.showAppBar! - ? AppBar( - elevation: 0, - title: Text(widget.title ?? context.strings.recoveryKey), - ) - : null, - body: Padding( - padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: constraints.maxWidth, - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( - mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - widget.showAppBar! - ? const SizedBox.shrink() - : Text( - widget.title ?? context.strings.recoveryKey, - style: Theme.of(context).textTheme.headlineMedium, - ), - Padding( - padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12), - ), + const SizedBox(height: 24), Text( widget.text ?? context.strings.recoveryKeyOnForgotPassword, - style: Theme.of(context).textTheme.titleMedium, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), ), - const Padding(padding: EdgeInsets.only(top: 24)), + const SizedBox(height: 24), Container( - padding: const EdgeInsets.all(1), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - getEnteColorScheme(context).primary700, - getEnteColorScheme(context).primary300, - ], - stops: const [0.0, 0.9753], - ), + color: colorScheme.primary700, + borderRadius: BorderRadius.circular(16), ), - child: DottedBorder( - options: const RoundedRectDottedBorderOptions( - padding: EdgeInsets.zero, - strokeWidth: 1, - color: Color(0xFF6B6B6B), - dashPattern: [6, 6], - radius: Radius.circular(8), - ), - child: SizedBox( - width: double.infinity, - child: Stack( - children: [ - Column( - children: [ - Builder( - builder: (context) { - final content = Container( - padding: const EdgeInsets.all(20), - width: double.infinity, - child: Text( - recoveryKey, - textAlign: TextAlign.justify, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: Colors.white, - ), - ), - ); - - if (PlatformUtil.isMobile()) { - return GestureDetector( - onTap: () async => await copy(), - child: content, - ); - } else { - return SelectableRegion( - focusNode: FocusNode(), - selectionControls: - PlatformUtil.selectionControls, - child: content, - ); - } - }, - ), - ], - ), - Positioned( - right: 0, - top: 0, - child: PlatformCopy( - onPressed: copy, - ), + child: Builder( + builder: (context) { + final content = Padding( + padding: const EdgeInsets.symmetric( + horizontal: 22, + vertical: 24, + ), + child: Text( + recoveryKey, + textAlign: TextAlign.justify, + style: textTheme.body.copyWith( + color: Colors.white, + fontFamily: 'monospace', + letterSpacing: 0.5, + height: 1.5, ), - ], - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Text( - widget.subText ?? - context.strings.recoveryKeySaveDescription, - style: Theme.of(context).textTheme.bodyLarge, + ), + ); + + if (PlatformUtil.isMobile()) { + return GestureDetector( + onTap: () async => await copy(), + child: content, + ); + } else { + return SelectableRegion( + focusNode: FocusNode(), + selectionControls: + PlatformUtil.selectionControls, + child: content, + ); + } + }, ), ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - width: double.infinity, - padding: const EdgeInsets.fromLTRB(10, 10, 10, 42), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _saveOptions(context, recoveryKey), - ), + const SizedBox(height: 20), + Text( + widget.subText ?? + context.strings.recoveryKeySaveDescription, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, ), ), ], ), ), ), - ); - }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _saveOptions(context, recoveryKey), + ), + ), + ], ), ), ); } List _saveOptions(BuildContext context, String recoveryKey) { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); final List childrens = []; - if (!_hasTriedToSave) { - childrens.add( - SizedBox( - height: 56, - child: ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: () async { - await _saveKeys(); - }, - child: Text(context.strings.doThisLater), + + childrens.add( + Center( + child: DotsIndicator( + dotsCount: 3, + position: 2, + decorator: DotsDecorator( + activeColor: colorScheme.primary700, + color: colorScheme.primary700.withValues(alpha: 0.32), + activeShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + size: const Size(10, 10), + activeSize: const Size(20, 10), + spacing: const EdgeInsets.all(6), ), ), - ); - childrens.add(const SizedBox(height: 10)); - } + ), + ); + + childrens.add(const SizedBox(height: 20)); childrens.add( GradientButton( onTap: () async { - await shareDialog( + await showShareSheet( context, context.strings.recoveryKey, saveAction: () async { @@ -283,21 +241,20 @@ class _RecoveryKeyPageState extends State { ), ); - if (_hasTriedToSave) { - childrens.add(const SizedBox(height: 10)); - childrens.add( - SizedBox( - height: 56, - child: ElevatedButton( - child: Text(widget.doneText), - onPressed: () async { - await _saveKeys(); - }, + childrens.add( + TextButton( + onPressed: () async { + await _saveKeys(); + }, + child: Text( + _hasTriedToSave ? widget.doneText : context.strings.continueLabel, + style: textTheme.bodyBold.copyWith( + color: colorScheme.primary700, ), ), - ); - } - childrens.add(const SizedBox(height: 12)); + ), + ); + return childrens; } @@ -354,24 +311,3 @@ class _RecoveryKeyPageState extends State { widget.onDone!(); } } - -class PlatformCopy extends StatelessWidget { - const PlatformCopy({ - super.key, - required this.onPressed, - }); - - final void Function() onPressed; - - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: () => onPressed(), - visualDensity: VisualDensity.compact, - icon: const Icon( - Icons.copy, - size: 16, - ), - ); - } -} diff --git a/mobile/packages/accounts/lib/pages/recovery_page.dart b/mobile/packages/accounts/lib/pages/recovery_page.dart index 93d22991bf0..471a87101b4 100644 --- a/mobile/packages/accounts/lib/pages/recovery_page.dart +++ b/mobile/packages/accounts/lib/pages/recovery_page.dart @@ -1,11 +1,16 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_strings/ente_strings.dart'; +import "package:ente_ui/components/alert_bottom_sheet.dart"; import 'package:ente_ui/components/buttons/dynamic_fab.dart'; +import "package:ente_ui/components/buttons/gradient_button.dart"; import 'package:ente_ui/pages/base_home_page.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:ente_ui/utils/toast_util.dart'; +import "package:ente_utils/email_util.dart"; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class RecoveryPage extends StatefulWidget { final BaseConfiguration config; @@ -48,13 +53,21 @@ class _RecoveryPageState extends State { if (e is AssertionError) { errMessage = '$errMessage : ${e.message}'; } - await showErrorDialog(context, "Incorrect recovery key", errMessage); + await showAlertBottomSheet( + context, + title: context.strings.incorrectRecoveryKey, + message: errMessage, + assetPath: 'assets/warning-grey.png', + ); } } @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + FloatingActionButtonLocation? fabLocation() { if (isKeypadOpen) { return null; @@ -65,11 +78,22 @@ class _RecoveryPageState extends State { return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, + color: colorScheme.primary700, onPressed: () { Navigator.of(context).pop(); }, @@ -78,7 +102,7 @@ class _RecoveryPageState extends State { floatingActionButton: DynamicFAB( isKeypadOpen: isKeypadOpen, isFormValid: _recoveryKey.text.isNotEmpty, - buttonText: 'Recover', + buttonText: context.strings.recover, onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), @@ -89,73 +113,85 @@ class _RecoveryPageState extends State { child: ListView( children: [ Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.strings.forgotPassword, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - decoration: InputDecoration( - filled: true, - hintText: "Enter your recovery key", - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + context.strings.recoveryKey, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), ), - ), - style: const TextStyle( - fontSize: 14, - fontFeatures: [FontFeature.tabularFigures()], - ), - controller: _recoveryKey, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.multiline, - maxLines: null, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - showErrorDialog( - context, - "Sorry", - "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Center( + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + fillColor: colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterRecoveryKeyHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + fontFeatures: [const FontFeature.tabularFigures()], + ), + minLines: 4, + maxLines: 5, + controller: _recoveryKey, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.multiline, + onChanged: (_) { + setState(() {}); + }, + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + showAlertBottomSheet( + context, + title: context.strings.sorry, + message: + context.strings.noRecoveryKeyNoDecryption, + assetPath: 'assets/warning-grey.png', + buttons: [ + GradientButton( + text: context.strings.contactSupport, + onTap: () async { + await openSupportPage("", null); + }, + ), + ], + ); + }, child: Text( context.strings.noRecoveryKeyTitle, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + style: textTheme.body.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), ), ), ), - ), - ], + ], + ), ), ], ), diff --git a/mobile/packages/accounts/lib/pages/two_factor_authentication_page.dart b/mobile/packages/accounts/lib/pages/two_factor_authentication_page.dart index 29cd484aca0..082e80934e0 100644 --- a/mobile/packages/accounts/lib/pages/two_factor_authentication_page.dart +++ b/mobile/packages/accounts/lib/pages/two_factor_authentication_page.dart @@ -1,9 +1,11 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_strings/ente_strings.dart'; +import 'package:ente_ui/components/buttons/dynamic_fab.dart'; import 'package:ente_ui/lifecycle_event_handler.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:pinput/pinput.dart'; class TwoFactorAuthenticationPage extends StatefulWidget { @@ -24,6 +26,7 @@ class _TwoFactorAuthenticationPageState @override void initState() { + super.initState(); _lifecycleEventHandler = LifecycleEventHandler( resumeCallBack: () async { if (mounted) { @@ -35,128 +38,184 @@ class _TwoFactorAuthenticationPageState }, ); WidgetsBinding.instance.addObserver(_lifecycleEventHandler); - super.initState(); } @override void dispose() { WidgetsBinding.instance.removeObserver(_lifecycleEventHandler); + _pinController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; final colorScheme = getEnteColorScheme(context); - // TODO: Use primary500 instead of strokeMuted once theming is properly - // set up for Locker app to show app-specific colors. Currently, - // colorScheme.primary500 doesn't work on Locker app. - final pinPutDecoration = PinTheme( - height: 45, - width: 45, - decoration: BoxDecoration( - border: Border.all(color: colorScheme.strokeMuted), - borderRadius: BorderRadius.circular(15.0), - ), - ); + FloatingActionButtonLocation? fabLocation() { + if (isKeypadOpen) { + return null; + } else { + return FloatingActionButtonLocation.centerFloat; + } + } return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( - title: Text( - context.strings.twoFactorAuthTitle, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: colorScheme.primary700, + onPressed: () { + Navigator.of(context).pop(); + }, ), ), - body: _getBody(pinPutDecoration), + body: _getBody(), + floatingActionButton: DynamicFAB( + isKeypadOpen: isKeypadOpen, + isFormValid: _code.length == 6, + buttonText: context.strings.verify, + onPressedFunction: () async { + await _verifyTwoFactorCode(_code); + }, + ), + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), ); } - Widget _getBody(PinTheme pinPutDecoration) { + Widget _getBody() { final colorScheme = getEnteColorScheme(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - context.strings.enterCodeHint, - style: const TextStyle( - height: 1.4, - fontSize: 16, - ), - textAlign: TextAlign.center, + final textTheme = getEnteTextTheme(context); + + final defaultPinTheme = PinTheme( + height: 48, + width: 48, + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.strokeFaint, + width: 1.75, + ), + borderRadius: BorderRadius.circular(18), + ), + ); + + final focusedPinTheme = defaultPinTheme.copyWith( + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.primary700, + width: 1.75, ), - const Padding(padding: EdgeInsets.all(32)), - Padding( - padding: const EdgeInsets.fromLTRB(40, 0, 40, 0), - child: Pinput( - length: 6, - onCompleted: (String code) { - _verifyTwoFactorCode(code); - }, - onChanged: (String pin) { - setState(() { - _code = pin; - }); - }, - controller: _pinController, - submittedPinTheme: pinPutDecoration.copyWith( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.0), - border: Border.all( - color: colorScheme.strokeMuted.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(18), + ), + ); + + final submittedPinTheme = defaultPinTheme.copyWith( + textStyle: textTheme.h3Bold.copyWith( + color: colorScheme.primary700, + ), + decoration: BoxDecoration( + color: colorScheme.backdropBase, + border: Border.all( + color: colorScheme.strokeFaint, + width: 1.75, + ), + borderRadius: BorderRadius.circular(18), + ), + ); + + return SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 24), + Image.asset( + 'assets/lock_screen_icon.png', + width: 129, + height: 95, + ), + const SizedBox(height: 24), + Text( + context.strings.twoFactorAuthTitle, + style: textTheme.body.copyWith( + color: colorScheme.textBase, ), + textAlign: TextAlign.center, ), - ), - defaultPinTheme: pinPutDecoration, - followingPinTheme: pinPutDecoration.copyWith( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - border: Border.all( - color: colorScheme.strokeMuted.withValues(alpha: 0.5), + const SizedBox(height: 8), + Text( + context.strings.enterCodeHint, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, ), + textAlign: TextAlign.center, ), - ), - autofocus: true, - ), - ), - const Padding(padding: EdgeInsets.all(24)), - Container( - padding: const EdgeInsets.fromLTRB(80, 0, 80, 0), - width: double.infinity, - height: 64, - child: OutlinedButton( - onPressed: _code.length == 6 - ? () async { - await _verifyTwoFactorCode(_code); - } - : null, - child: Text(context.strings.verify), - ), - ), - const Padding(padding: EdgeInsets.all(30)), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - UserService.instance.recoverTwoFactor( - context, - widget.sessionID, - TwoFactorType.totp, - ); - }, - child: Container( - padding: const EdgeInsets.all(10), - child: Center( - child: Text( - context.strings.lostDeviceTitle, - style: const TextStyle( - decoration: TextDecoration.underline, - fontSize: 12, + const SizedBox(height: 32), + Pinput( + length: 6, + controller: _pinController, + autofocus: true, + keyboardType: TextInputType.number, + defaultPinTheme: defaultPinTheme, + focusedPinTheme: focusedPinTheme, + submittedPinTheme: submittedPinTheme, + showCursor: false, + onCompleted: (String code) { + _verifyTwoFactorCode(code); + }, + onChanged: (String pin) { + setState(() { + _code = pin; + }); + }, + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.totp, + ); + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Text( + context.strings.lostDeviceTitle, + style: textTheme.small.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), ), ), - ), + ], ), ), - ], + ), ); } diff --git a/mobile/packages/accounts/lib/pages/two_factor_recovery_page.dart b/mobile/packages/accounts/lib/pages/two_factor_recovery_page.dart index c4ef3eec763..7ff273912a3 100644 --- a/mobile/packages/accounts/lib/pages/two_factor_recovery_page.dart +++ b/mobile/packages/accounts/lib/pages/two_factor_recovery_page.dart @@ -1,8 +1,10 @@ import 'package:ente_accounts/ente_accounts.dart'; import 'package:ente_strings/ente_strings.dart'; +import 'package:ente_ui/components/buttons/dynamic_fab.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_utils/email_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class TwoFactorRecoveryPage extends StatefulWidget { final String sessionID; @@ -25,81 +27,138 @@ class TwoFactorRecoveryPage extends StatefulWidget { class _TwoFactorRecoveryPageState extends State { final _recoveryKey = TextEditingController(); + Future _onRecover() async { + await UserService.instance.removeTwoFactor( + context, + widget.type, + widget.sessionID, + _recoveryKey.text, + widget.encryptedSecret, + widget.secretDecryptionNonce, + ); + } + @override Widget build(BuildContext context) { + final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); + + FloatingActionButtonLocation? fabLocation() { + if (isKeypadOpen) { + return null; + } else { + return FloatingActionButtonLocation.centerFloat; + } + } + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + backgroundColor: colorScheme.backgroundBase, appBar: AppBar( - title: Text( - context.strings.recoverAccount, - style: const TextStyle( - fontSize: 18, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: colorScheme.backgroundBase, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorScheme.primary700, + BlendMode.srcIn, ), ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: colorScheme.primary700, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: _getBody(), + floatingActionButton: DynamicFAB( + isKeypadOpen: isKeypadOpen, + isFormValid: _recoveryKey.text.isNotEmpty, + buttonText: context.strings.recover, + onPressedFunction: _onRecover, ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), + ); + } + + Widget _getBody() { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + return SafeArea( + child: Column( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(60, 0, 60, 0), - child: TextFormField( - decoration: InputDecoration( - hintText: context.strings.enterRecoveryKeyHint, - contentPadding: const EdgeInsets.all(20), - ), - style: const TextStyle( - fontSize: 14, - fontFeatures: [FontFeature.tabularFigures()], - ), - controller: _recoveryKey, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.multiline, - maxLines: null, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding(padding: EdgeInsets.all(24)), - Container( - padding: const EdgeInsets.fromLTRB(80, 0, 80, 0), - width: double.infinity, - height: 64, - child: OutlinedButton( - onPressed: _recoveryKey.text.isNotEmpty - ? () async { - await UserService.instance.removeTwoFactor( - context, - widget.type, - widget.sessionID, - _recoveryKey.text, - widget.encryptedSecret, - widget.secretDecryptionNonce, - ); - } - : null, - child: Text(context.strings.recover), - ), - ), - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () async { - await openSupportPage(null, null); - }, - child: Container( - padding: const EdgeInsets.all(40), - child: Center( - child: Text( - context.strings.noRecoveryKeyTitle, - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: 12, - color: getEnteColorScheme(context) - .textBase - .withValues(alpha: 0.9), - ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + context.strings.recoveryKey, + style: textTheme.bodyBold.copyWith( + color: colorScheme.textBase, + ), + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + fillColor: colorScheme.backdropBase, + filled: true, + hintText: context.strings.enterRecoveryKeyHint, + hintStyle: TextStyle(color: colorScheme.textMuted), + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(8), + ), + ), + style: textTheme.body.copyWith( + color: colorScheme.textBase, + fontFeatures: [const FontFeature.tabularFigures()], + ), + controller: _recoveryKey, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 4, + onChanged: (_) { + setState(() {}); + }, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () async { + await openSupportPage(null, null); + }, + child: Text( + context.strings.noRecoveryKeyTitle, + style: textTheme.small.copyWith( + color: colorScheme.primary700, + decoration: TextDecoration.underline, + decorationColor: colorScheme.primary700, + ), + ), + ), + ], + ), + ], ), ), ), diff --git a/mobile/packages/accounts/lib/services/user_service.dart b/mobile/packages/accounts/lib/services/user_service.dart index 661dd4f5a73..7d917277ef3 100644 --- a/mobile/packages/accounts/lib/services/user_service.dart +++ b/mobile/packages/accounts/lib/services/user_service.dart @@ -30,6 +30,7 @@ import 'package:ente_events/event_bus.dart'; import 'package:ente_events/models/user_details_changed_event.dart'; import 'package:ente_network/network.dart'; import 'package:ente_strings/ente_strings.dart'; +import "package:ente_ui/components/alert_bottom_sheet.dart"; import 'package:ente_ui/components/progress_dialog.dart'; import 'package:ente_ui/pages/base_home_page.dart'; import 'package:ente_ui/utils/dialog_util.dart'; @@ -126,43 +127,29 @@ class UserService { final String? enteErrCode = e.response?.data["code"]; if (enteErrCode != null && enteErrCode == "USER_ALREADY_REGISTERED") { unawaited( - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.emailAlreadyRegistered, + title: context.strings.oops, + message: context.strings.emailAlreadyRegistered, + assetPath: 'assets/warning-grey.png', ), ); } else if (enteErrCode != null && enteErrCode == "USER_NOT_REGISTERED") { unawaited( - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.emailNotRegistered, - ), - ); - } else if (enteErrCode != null && - enteErrCode == "LOCKER_REGISTRATION_DISABLED") { - unawaited( - showErrorDialog( - context, - context.strings.oops, - context.strings.lockerExistingUserRequired, - ), - ); - } else if (enteErrCode != null && enteErrCode == "LOCKER_ROLLOUT_LIMIT") { - unawaited( - showErrorDialog( - context, - "We're out of beta seats for now", - "This preview access has reached capacity. We'll be opening it to more users soon.", + title: context.strings.oops, + message: context.strings.emailNotRegistered, + assetPath: 'assets/warning-grey.png', ), ); } else if (e.response != null && e.response!.statusCode == 403) { unawaited( - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.thisEmailIsAlreadyInUse, + title: context.strings.oops, + message: context.strings.thisEmailIsAlreadyInUse, + assetPath: 'assets/warning-grey.png', ), ); } else { @@ -525,47 +512,32 @@ class UserService { } on DioException catch (e) { _logger.info(e); await dialog.hide(); - final dynamic data = e.response?.data; - final String? enteErrCode = - data is Map ? data["code"] as String? : null; - if (enteErrCode != null && - enteErrCode == 'LOCKER_REGISTRATION_DISABLED') { - await showErrorDialog( - context, - context.strings.oops, - context.strings.lockerExistingUserRequired, - ); - return; - } else if (enteErrCode != null && enteErrCode == 'LOCKER_ROLLOUT_LIMIT') { - await showErrorDialog( - context, - "We're out of beta seats for now", - "This preview access has reached capacity. We'll be opening it to more users soon.", - ); - return; - } else if (e.response != null && e.response!.statusCode == 410) { - await showErrorDialog( + if (e.response != null && e.response!.statusCode == 410) { + await showAlertBottomSheet( context, - context.strings.oops, - context.strings.yourVerificationCodeHasExpired, + title: context.strings.oops, + message: context.strings.yourVerificationCodeHasExpired, + assetPath: 'assets/warning-grey.png', ); Navigator.of(context).pop(); } else { // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.incorrectCode, - context.strings.sorryTheCodeYouveEnteredIsIncorrect, + title: context.strings.incorrectCode, + message: context.strings.sorryTheCodeYouveEnteredIsIncorrect, + assetPath: 'assets/warning-grey.png', ); } } catch (e) { await dialog.hide(); _logger.severe(e); // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.verificationFailedPleaseTryAgain, + title: context.strings.oops, + message: context.strings.verificationFailedPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } } @@ -940,25 +912,7 @@ class UserService { } on DioException catch (e) { await dialog.hide(); _logger.severe(e); - final dynamic data = e.response?.data; - final String? enteErrCode = - data is Map ? data["code"] as String? : null; - if (enteErrCode != null && - enteErrCode == 'LOCKER_REGISTRATION_DISABLED') { - // ignore: unawaited_futures - showErrorDialog( - context, - context.strings.oops, - context.strings.lockerExistingUserRequired, - ); - } else if (enteErrCode != null && enteErrCode == 'LOCKER_ROLLOUT_LIMIT') { - // ignore: unawaited_futures - showErrorDialog( - context, - "We're out of beta seats for now", - "This preview access has reached capacity. We'll be opening it to more users soon.", - ); - } else if (e.response != null && e.response!.statusCode == 404) { + if (e.response != null && e.response!.statusCode == 404) { showToast(context, "Session expired"); // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( @@ -971,20 +925,22 @@ class UserService { ); } else { // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.incorrectCode, - context.strings.authenticationFailedPleaseTryAgain, + title: context.strings.incorrectCode, + message: context.strings.authenticationFailedPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } } catch (e) { await dialog.hide(); _logger.severe(e); // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.authenticationFailedPleaseTryAgain, + title: context.strings.oops, + message: context.strings.authenticationFailedPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } } @@ -1037,20 +993,22 @@ class UserService { ); } else { // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.somethingWentWrongPleaseTryAgain, + title: context.strings.oops, + message: context.strings.somethingWentWrongPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } } catch (e) { await dialog.hide(); _logger.severe(e); // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.somethingWentWrongPleaseTryAgain, + title: context.strings.oops, + message: context.strings.somethingWentWrongPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } finally { await dialog.hide(); @@ -1086,10 +1044,11 @@ class UserService { ); } catch (e) { await dialog.hide(); - await showErrorDialog( + await showAlertBottomSheet( context, - context.strings.incorrectRecoveryKey, - context.strings.theRecoveryKeyYouEnteredIsIncorrect, + title: context.strings.incorrectRecoveryKey, + message: context.strings.theRecoveryKeyYouEnteredIsIncorrect, + assetPath: 'assets/warning-grey.png', ); return; } @@ -1138,20 +1097,22 @@ class UserService { ); } else { // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.somethingWentWrongPleaseTryAgain, + title: context.strings.oops, + message: context.strings.somethingWentWrongPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } } catch (e) { await dialog.hide(); _logger.severe(e); // ignore: unawaited_futures - showErrorDialog( + showAlertBottomSheet( context, - context.strings.oops, - context.strings.somethingWentWrongPleaseTryAgain, + title: context.strings.oops, + message: context.strings.somethingWentWrongPleaseTryAgain, + assetPath: 'assets/warning-grey.png', ); } finally { await dialog.hide(); diff --git a/mobile/packages/accounts/pubspec.lock b/mobile/packages/accounts/pubspec.lock index 8272ae4fa15..77c51c60b40 100644 --- a/mobile/packages/accounts/pubspec.lock +++ b/mobile/packages/accounts/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dots_indicator: + dependency: "direct main" + description: + name: dots_indicator + sha256: "2a53e0321a3f0d87e38ef30f6e9ff1deb5d6c0e2c5708e057d26cdaa94a95f10" + url: "https://pub.dev" + source: hosted + version: "3.1.0" dotted_border: dependency: "direct main" description: @@ -493,7 +501,7 @@ packages: source: hosted version: "0.1.3" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 diff --git a/mobile/packages/accounts/pubspec.yaml b/mobile/packages/accounts/pubspec.yaml index c10c9e4c40d..3ae81ac6a89 100644 --- a/mobile/packages/accounts/pubspec.yaml +++ b/mobile/packages/accounts/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: bip39: ^1.0.6 collection: ^1.18.0 dio: ^5.4.0 + dots_indicator: ^3.0.0 dotted_border: ^3.1.0 email_validator: ^3.0.0 ente_base: @@ -35,6 +36,7 @@ dependencies: file_saver: ^0.3.0 flutter: sdk: flutter + flutter_svg: ^2.0.10+1 hugeicons: ^1.1.1 logging: ^1.2.0 password_strength: ^0.2.0 diff --git a/mobile/packages/legacy/pubspec.lock b/mobile/packages/legacy/pubspec.lock index c7dce2e5106..5fbb78dd1f8 100644 --- a/mobile/packages/legacy/pubspec.lock +++ b/mobile/packages/legacy/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dots_indicator: + dependency: transitive + description: + name: dots_indicator + sha256: "2a53e0321a3f0d87e38ef30f6e9ff1deb5d6c0e2c5708e057d26cdaa94a95f10" + url: "https://pub.dev" + source: hosted + version: "3.1.0" dotted_border: dependency: transitive description: diff --git a/mobile/packages/lock_screen/lib/lock_screen_config.dart b/mobile/packages/lock_screen/lib/lock_screen_config.dart deleted file mode 100644 index 3ac27f61c55..00000000000 --- a/mobile/packages/lock_screen/lib/lock_screen_config.dart +++ /dev/null @@ -1,175 +0,0 @@ -import "package:ente_ui/theme/colors.dart"; -import "package:ente_ui/theme/ente_theme.dart"; -import "package:ente_ui/theme/theme_config.dart"; -import "package:flutter/material.dart"; - -/// Configuration for lock screen UI -class LockScreenConfig { - final Widget titleWidget; - final Widget Function(BuildContext, TextEditingController?) iconBuilder; - final double pinBoxHeight; - final double pinBoxWidth; - final EdgeInsets? pinBoxPadding; - final double pinBoxBorderRadius; - final Color? pinBoxBorderColor; - final Color? pinBoxBackgroundColor; - final bool useDynamicColors; - - const LockScreenConfig({ - required this.titleWidget, - required this.iconBuilder, - required this.pinBoxHeight, - required this.pinBoxWidth, - this.pinBoxPadding, - required this.pinBoxBorderRadius, - this.pinBoxBorderColor, - this.pinBoxBackgroundColor, - this.useDynamicColors = false, - }); - - /// Default configuration for Auth app - static const LockScreenConfig auth = LockScreenConfig( - titleWidget: SizedBox.shrink(), - iconBuilder: _buildAuthIcon, - pinBoxHeight: 48, - pinBoxWidth: 48, - pinBoxPadding: EdgeInsets.only(top: 6.0), - pinBoxBorderRadius: 15.0, - pinBoxBorderColor: Color.fromRGBO(45, 194, 98, 1.0), - pinBoxBackgroundColor: null, - useDynamicColors: false, - ); - - /// Configuration for Locker app - static LockScreenConfig locker = LockScreenConfig( - titleWidget: _buildLockerTitle(), - iconBuilder: _buildLockerIcon, - pinBoxHeight: 48, - pinBoxWidth: 48, - pinBoxPadding: const EdgeInsets.only(top: 6.0), - pinBoxBorderRadius: 15.0, - pinBoxBorderColor: null, - pinBoxBackgroundColor: null, - useDynamicColors: true, - ); - - /// Get current configuration based on AppThemeConfig - static LockScreenConfig get current { - switch (AppThemeConfig.currentApp) { - case EnteApp.locker: - return locker; - case EnteApp.auth: - return auth; - } - } - - /// Check if title should be shown (not a SizedBox.shrink) - bool get showTitle => titleWidget is! SizedBox; - - /// Get border color based on config and theme - Color getBorderColor(EnteColorScheme colorTheme) { - if (useDynamicColors) { - return colorTheme.fillMuted; - } - return pinBoxBorderColor ?? colorTheme.fillMuted; - } - - /// Get background color based on config and theme - Color? getBackgroundColor(EnteColorScheme colorTheme) { - if (useDynamicColors) { - return colorTheme.backgroundBase; - } - return pinBoxBackgroundColor; - } - - // Helper methods for building title - static Widget _buildLockerTitle() { - return Image.asset( - 'assets/locker-logo-blue.png', - height: 24, - ); - } - - // Helper methods for building icons - static Widget _buildAuthIcon( - BuildContext context, - TextEditingController? controller, - ) { - final colorTheme = getEnteColorScheme(context); - return SizedBox( - height: 120, - width: 120, - child: Stack( - alignment: Alignment.center, - children: [ - Container( - width: 82, - height: 82, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - Colors.grey.shade500.withValues(alpha: 0.2), - Colors.grey.shade50.withValues(alpha: 0.1), - Colors.grey.shade400.withValues(alpha: 0.2), - Colors.grey.shade300.withValues(alpha: 0.4), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Padding( - padding: const EdgeInsets.all(1.0), - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: colorTheme.backgroundBase, - ), - ), - ), - ), - if (controller != null) - SizedBox( - height: 75, - width: 75, - child: ValueListenableBuilder( - valueListenable: controller, - builder: (context, value, child) { - return TweenAnimationBuilder( - tween: Tween( - begin: 0, - end: controller.text.length / 4, - ), - curve: Curves.ease, - duration: const Duration(milliseconds: 250), - builder: (context, value, _) => CircularProgressIndicator( - backgroundColor: colorTheme.fillFaintPressed, - value: value, - color: colorTheme.primary400, - strokeWidth: 1.5, - ), - ); - }, - ), - ), - Icon( - Icons.lock, - color: colorTheme.textBase, - size: 30, - ), - ], - ), - ); - } - - static Widget _buildLockerIcon( - BuildContext context, - TextEditingController? controller, - ) { - return Image.asset( - 'packages/ente_lock_screen/assets/locker_pin.png', - width: 129, - height: 95, - ); - } -} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart index dc8e52b2275..c3e43657f0e 100644 --- a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart @@ -1,4 +1,3 @@ -import "package:ente_lock_screen/lock_screen_config.dart"; import "package:ente_lock_screen/lock_screen_settings.dart"; import "package:ente_strings/ente_strings.dart"; import "package:ente_ui/components/buttons/dynamic_fab.dart"; @@ -6,6 +5,7 @@ import "package:ente_ui/components/text_input_widget.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_svg/flutter_svg.dart"; class LockScreenConfirmPassword extends StatefulWidget { const LockScreenConfirmPassword({ @@ -58,7 +58,6 @@ class _LockScreenConfirmPasswordState extends State { Widget build(BuildContext context) { final colorTheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); - final config = LockScreenConfig.current; final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; FloatingActionButtonLocation? fabLocation() { @@ -70,11 +69,12 @@ class _LockScreenConfirmPasswordState extends State { } return Scaffold( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, elevation: 0, + scrolledUnderElevation: 0, leading: IconButton( onPressed: () { FocusScope.of(context).unfocus(); @@ -85,8 +85,14 @@ class _LockScreenConfirmPasswordState extends State { color: colorTheme.textBase, ), ), - centerTitle: config.showTitle, - title: config.titleWidget, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorTheme.primary700, + BlendMode.srcIn, + ), + ), ), floatingActionButton: ValueListenableBuilder( valueListenable: _isFormValid, @@ -106,15 +112,17 @@ class _LockScreenConfirmPasswordState extends State { body: SingleChildScrollView( child: Center( child: Padding( - padding: EdgeInsets.symmetric( - horizontal: config.showTitle ? 16.0 : 0, - ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: config.showTitle ? 40 : 0), - config.iconBuilder(context, null), - SizedBox(height: config.showTitle ? 24 : 0), + const SizedBox(height: 40), + Image.asset( + 'assets/lock_screen_icon.png', + width: 129, + height: 95, + ), + const SizedBox(height: 24), Text( context.strings.reEnterPassword, textAlign: TextAlign.center, diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart index 3353d45cb2d..796823cd95e 100644 --- a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart @@ -1,12 +1,14 @@ import "dart:io"; -import "package:ente_lock_screen/lock_screen_config.dart"; import "package:ente_lock_screen/lock_screen_settings.dart"; import "package:ente_lock_screen/ui/custom_pin_keypad.dart"; import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/theme/colors.dart"; import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/theme/text_style.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_svg/flutter_svg.dart"; import "package:pinput/pinput.dart"; class LockScreenConfirmPin extends StatefulWidget { @@ -58,13 +60,13 @@ class _LockScreenConfirmPinState extends State { Widget build(BuildContext context) { final colorTheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); - final config = LockScreenConfig.current; return Scaffold( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, appBar: AppBar( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, elevation: 0, + scrolledUnderElevation: 0, leading: IconButton( onPressed: () { Navigator.of(context).pop(false); @@ -74,43 +76,53 @@ class _LockScreenConfirmPinState extends State { color: colorTheme.textBase, ), ), - centerTitle: config.showTitle, - title: config.titleWidget, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorTheme.primary700, + BlendMode.srcIn, + ), + ), ), floatingActionButton: isPlatformDesktop ? null : CustomPinKeypad(controller: _confirmPinController), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, body: SingleChildScrollView( - child: _getBody(colorTheme, textTheme, config), + child: _getBody(colorTheme, textTheme), ), ); } - Widget _getBody(colorTheme, textTheme, LockScreenConfig config) { + Widget _getBody(EnteColorScheme colorTheme, EnteTextTheme textTheme) { final pinPutDecoration = PinTheme( - height: config.pinBoxHeight, - width: config.pinBoxWidth, - padding: config.pinBoxPadding, + height: 48, + width: 48, + padding: const EdgeInsets.only(top: 6.0), decoration: BoxDecoration( - color: config.getBackgroundColor(colorTheme), + color: colorTheme.backgroundBase, border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, width: 1, ), - borderRadius: BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), ), ); return Center( child: Padding( - padding: EdgeInsets.symmetric(horizontal: config.showTitle ? 24.0 : 0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: config.showTitle ? 40 : 0), - config.iconBuilder(context, _confirmPinController), - SizedBox(height: config.showTitle ? 24 : 0), + const SizedBox(height: 40), + Image.asset( + 'assets/lock_screen_icon.png', + width: 129, + height: 95, + ), + const SizedBox(height: 24), Text( context.strings.reEnterPin, style: textTheme.bodyBold, @@ -125,51 +137,42 @@ class _LockScreenConfirmPinState extends State { defaultPinTheme: pinPutDecoration.copyWith( textStyle: textTheme.h3Bold, decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, ), ), ), submittedPinTheme: pinPutDecoration.copyWith( textStyle: textTheme.h3Bold.copyWith( - color: config.showTitle ? colorTheme.primary700 : null, + color: colorTheme.primary700, ), decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.showTitle - ? colorTheme.primary700 - : colorTheme.fillBase, + color: colorTheme.primary700, ), ), ), followingPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, ), ), ), focusedPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.showTitle - ? colorTheme.fillBase - : config.getBorderColor(colorTheme), + color: colorTheme.fillBase, ), ), ), errorPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( color: colorTheme.warning400, ), diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart index 06b4a930b9b..0033c491374 100644 --- a/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart @@ -1,7 +1,6 @@ import "dart:convert"; import "package:ente_crypto_dart/ente_crypto_dart.dart"; -import "package:ente_lock_screen/lock_screen_config.dart"; import "package:ente_lock_screen/lock_screen_settings.dart"; import "package:ente_lock_screen/ui/lock_screen_confirm_password.dart"; import "package:ente_lock_screen/ui/lock_screen_options.dart"; @@ -11,6 +10,7 @@ import "package:ente_ui/components/text_input_widget.dart"; import "package:ente_ui/theme/ente_theme.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_svg/flutter_svg.dart"; /// [isChangingLockScreenSettings] Authentication required for changing lock screen settings. /// Set to true when the app requires the user to authenticate before allowing @@ -69,7 +69,6 @@ class _LockScreenPasswordState extends State { Widget build(BuildContext context) { final colorTheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); - final config = LockScreenConfig.current; final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; FloatingActionButtonLocation? fabLocation() { @@ -81,11 +80,12 @@ class _LockScreenPasswordState extends State { } return Scaffold( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, elevation: 0, + scrolledUnderElevation: 0, leading: IconButton( onPressed: () { FocusScope.of(context).unfocus(); @@ -96,8 +96,14 @@ class _LockScreenPasswordState extends State { color: colorTheme.textBase, ), ), - centerTitle: config.showTitle, - title: config.titleWidget, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorTheme.primary700, + BlendMode.srcIn, + ), + ), ), floatingActionButton: ValueListenableBuilder( valueListenable: _isFormValid, @@ -117,15 +123,17 @@ class _LockScreenPasswordState extends State { body: SingleChildScrollView( child: Center( child: Padding( - padding: EdgeInsets.symmetric( - horizontal: config.showTitle ? 16.0 : 0, - ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: config.showTitle ? 40 : 0), - config.iconBuilder(context, null), - SizedBox(height: config.showTitle ? 24 : 0), + const SizedBox(height: 40), + Image.asset( + 'assets/lock_screen_icon.png', + width: 129, + height: 95, + ), + const SizedBox(height: 24), Text( widget.isChangingLockScreenSettings ? context.strings.enterAppLockPassword diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart index ee8b6f7ff6a..3ae887b297b 100644 --- a/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart @@ -2,7 +2,6 @@ import "dart:convert"; import "dart:io"; import "package:ente_crypto_dart/ente_crypto_dart.dart"; -import "package:ente_lock_screen/lock_screen_config.dart"; import "package:ente_lock_screen/lock_screen_settings.dart"; import "package:ente_lock_screen/ui/custom_pin_keypad.dart"; import "package:ente_lock_screen/ui/lock_screen_confirm_pin.dart"; @@ -13,6 +12,7 @@ import "package:ente_ui/theme/ente_theme.dart"; import "package:ente_ui/theme/text_style.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_svg/flutter_svg.dart"; import 'package:pinput/pinput.dart'; /// [isChangingLockScreenSettings] Authentication required for changing lock screen settings. @@ -49,6 +49,7 @@ class _LockScreenPinState extends State { bool isPinValid = false; int invalidAttemptsCount = 0; bool isPlatformDesktop = false; + @override void initState() { super.initState(); @@ -125,26 +126,26 @@ class _LockScreenPinState extends State { Widget build(BuildContext context) { final colorTheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); - final config = LockScreenConfig.current; final pinPutDecoration = PinTheme( - height: config.pinBoxHeight, - width: config.pinBoxWidth, - padding: config.pinBoxPadding, + height: 48, + width: 48, + padding: const EdgeInsets.only(top: 6.0), decoration: BoxDecoration( - color: config.getBackgroundColor(colorTheme), + color: colorTheme.backgroundBase, border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, width: 1, ), - borderRadius: BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), ), ); return Scaffold( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, appBar: AppBar( - backgroundColor: config.getBackgroundColor(colorTheme), + backgroundColor: colorTheme.backgroundBase, elevation: 0, + scrolledUnderElevation: 0, leading: IconButton( onPressed: () { Navigator.of(context).pop(false); @@ -154,15 +155,21 @@ class _LockScreenPinState extends State { color: colorTheme.textBase, ), ), - centerTitle: config.showTitle, - title: config.titleWidget, + centerTitle: true, + title: SvgPicture.asset( + 'assets/svg/app-logo.svg', + colorFilter: ColorFilter.mode( + colorTheme.primary700, + BlendMode.srcIn, + ), + ), ), floatingActionButton: isPlatformDesktop ? null : CustomPinKeypad(controller: _pinController), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, body: SingleChildScrollView( - child: _getBody(colorTheme, textTheme, pinPutDecoration, config), + child: _getBody(colorTheme, textTheme, pinPutDecoration), ), ); } @@ -171,17 +178,20 @@ class _LockScreenPinState extends State { EnteColorScheme colorTheme, EnteTextTheme textTheme, PinTheme pinPutDecoration, - LockScreenConfig config, ) { return Center( child: Padding( - padding: EdgeInsets.symmetric(horizontal: config.showTitle ? 24.0 : 0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: config.showTitle ? 40 : 0), - config.iconBuilder(context, _pinController), - SizedBox(height: config.showTitle ? 24 : 0), + const SizedBox(height: 40), + Image.asset( + 'assets/lock_screen_icon.png', + width: 129, + height: 95, + ), + const SizedBox(height: 24), Text( widget.isChangingLockScreenSettings ? context.strings.enterAppLockPin @@ -198,51 +208,42 @@ class _LockScreenPinState extends State { defaultPinTheme: pinPutDecoration.copyWith( textStyle: textTheme.h3Bold, decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, ), ), ), submittedPinTheme: pinPutDecoration.copyWith( textStyle: textTheme.h3Bold.copyWith( - color: config.showTitle ? colorTheme.primary700 : null, + color: colorTheme.primary700, ), decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.showTitle - ? colorTheme.primary700 - : colorTheme.fillBase, + color: colorTheme.primary700, ), ), ), followingPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.getBorderColor(colorTheme), + color: colorTheme.fillMuted, ), ), ), focusedPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( - color: config.showTitle - ? colorTheme.fillBase - : config.getBorderColor(colorTheme), + color: colorTheme.fillBase, ), ), ), errorPinTheme: pinPutDecoration.copyWith( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(config.pinBoxBorderRadius), + borderRadius: BorderRadius.circular(15.0), border: Border.all( color: colorTheme.warning400, ), diff --git a/mobile/packages/lock_screen/pubspec.lock b/mobile/packages/lock_screen/pubspec.lock index 677f0004481..2175439fbbf 100644 --- a/mobile/packages/lock_screen/pubspec.lock +++ b/mobile/packages/lock_screen/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dots_indicator: + dependency: transitive + description: + name: dots_indicator + sha256: "2a53e0321a3f0d87e38ef30f6e9ff1deb5d6c0e2c5708e057d26cdaa94a95f10" + url: "https://pub.dev" + source: hosted + version: "3.1.0" dotted_border: dependency: transitive description: @@ -493,7 +501,7 @@ packages: source: hosted version: "0.1.3" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 diff --git a/mobile/packages/lock_screen/pubspec.yaml b/mobile/packages/lock_screen/pubspec.yaml index 1775f0e8beb..eba6176095b 100644 --- a/mobile/packages/lock_screen/pubspec.yaml +++ b/mobile/packages/lock_screen/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: flutter: sdk: flutter flutter_animate: ^4.1.0 + flutter_svg: ^2.0.10+1 flutter_local_authentication: git: url: https://github.com/eaceto/flutter_local_authentication @@ -49,5 +50,3 @@ dev_dependencies: sdk: flutter flutter: - assets: - - assets/ diff --git a/mobile/packages/sharing/pubspec.lock b/mobile/packages/sharing/pubspec.lock index 5118fbccef6..bd1474cb5a9 100644 --- a/mobile/packages/sharing/pubspec.lock +++ b/mobile/packages/sharing/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dots_indicator: + dependency: transitive + description: + name: dots_indicator + sha256: "2a53e0321a3f0d87e38ef30f6e9ff1deb5d6c0e2c5708e057d26cdaa94a95f10" + url: "https://pub.dev" + source: hosted + version: "3.1.0" dotted_border: dependency: "direct main" description: diff --git a/mobile/packages/strings/lib/l10n/arb/strings_en.arb b/mobile/packages/strings/lib/l10n/arb/strings_en.arb index 3c8301667c2..f0cc61569c4 100644 --- a/mobile/packages/strings/lib/l10n/arb/strings_en.arb +++ b/mobile/packages/strings/lib/l10n/arb/strings_en.arb @@ -137,6 +137,7 @@ "@email": { "description": "Email field label" }, + "emailHint": "Enter your email id", "verify": "Verify", "@verify": { "description": "Verify button label" @@ -269,6 +270,10 @@ "@password": { "description": "Password field label" }, + "enterThePasswordFor": "Enter the password for", + "@enterThePasswordFor": { + "description": "Text shown before email on login password verification page" + }, "confirmPassword": "Confirm password", "@confirmPassword": { "description": "Confirm password field label" @@ -316,6 +321,10 @@ "@logInLabel": { "description": "Log in button label" }, + "alreadyHaveAccount": "Already have an account?", + "@alreadyHaveAccount": { + "description": "Text asking if user already has an account" + }, "welcomeBack": "Welcome back!", "@welcomeBack": { "description": "Welcome back message" @@ -360,17 +369,6 @@ "@verifyEmail": { "description": "Verify email title" }, - "weHaveSendEmailTo": "We have sent a mail to {email}", - "@weHaveSendEmailTo": { - "description": "Text to indicate that we have sent a mail to the user", - "placeholders": { - "email": { - "description": "The email address of the user", - "type": "String", - "example": "example@ente.io" - } - } - }, "toResetVerifyEmail": "To reset your password, please verify your email first.", "@toResetVerifyEmail": { "description": "Message asking user to verify email before password reset" @@ -379,6 +377,15 @@ "@checkInboxAndSpamFolder": { "description": "Message asking user to check inbox and spam folder" }, + "weHaveSentCode": "We have sent a code to {email}", + "@weHaveSentCode": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "resendCode": "Resend Code", "tapToEnterCode": "Tap to enter code", "@tapToEnterCode": { "description": "Hint for entering verification code" @@ -528,6 +535,10 @@ "@noRecoveryKeyTitle": { "description": "No recovery key title" }, + "noRecoveryKeyNoDecryption": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", + "@noRecoveryKeyNoDecryption": { + "description": "Explanation that data cannot be decrypted without password or recovery key" + }, "twoFactorAuthTitle": "Two-factor authentication", "@twoFactorAuthTitle": { "description": "Two-factor authentication title" @@ -1014,5 +1025,6 @@ "type": "String" } } - } + }, + "sorry": "Sorry" } diff --git a/mobile/packages/ui/lib/components/alert_bottom_sheet.dart b/mobile/packages/ui/lib/components/alert_bottom_sheet.dart index a19b97c57c3..acf0017b350 100644 --- a/mobile/packages/ui/lib/components/alert_bottom_sheet.dart +++ b/mobile/packages/ui/lib/components/alert_bottom_sheet.dart @@ -8,17 +8,21 @@ Future showAlertBottomSheet( required String message, required String assetPath, List? buttons, + bool isDismissible = true, + bool showCloseButton = true, }) { return showModalBottomSheet( context: context, isScrollControlled: true, - isDismissible: true, + isDismissible: isDismissible, + enableDrag: isDismissible, backgroundColor: Colors.transparent, builder: (context) => AlertBottomSheet( title: title, message: message, assetPath: assetPath, buttons: buttons, + showCloseButton: showCloseButton, ), ); } @@ -28,12 +32,14 @@ class AlertBottomSheet extends StatelessWidget { final String message; final String assetPath; final List? buttons; + final bool showCloseButton; const AlertBottomSheet({ required this.title, required this.message, required this.assetPath, this.buttons, + this.showCloseButton = true, super.key, }); @@ -58,13 +64,14 @@ class AlertBottomSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - CloseIconButton(), - ], - ), - const SizedBox(height: 12), + if (showCloseButton) + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CloseIconButton(), + ], + ), + SizedBox(height: showCloseButton ? 12 : 24), Center(child: Image.asset(assetPath)), const SizedBox(height: 20), Text( diff --git a/mobile/packages/ui/lib/components/buttons/dynamic_fab.dart b/mobile/packages/ui/lib/components/buttons/dynamic_fab.dart index a6e9be99909..4f3e3a40580 100644 --- a/mobile/packages/ui/lib/components/buttons/dynamic_fab.dart +++ b/mobile/packages/ui/lib/components/buttons/dynamic_fab.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import "package:ente_ui/components/buttons/gradient_button.dart"; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:ente_ui/theme/ente_theme_data.dart'; import 'package:flutter/material.dart'; @@ -58,14 +59,14 @@ class DynamicFAB extends StatelessWidget { ), ); } else { - return Container( - width: double.infinity, - height: 56, - padding: const EdgeInsets.symmetric(horizontal: 20), - child: OutlinedButton( - onPressed: - isFormValid! ? onPressedFunction as void Function()? : null, - child: Text(buttonText!), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.infinity, + child: GradientButton( + text: buttonText!, + onTap: isFormValid! ? onPressedFunction as void Function()? : null, + ), ), ); } diff --git a/mobile/packages/ui/lib/components/buttons/gradient_button.dart b/mobile/packages/ui/lib/components/buttons/gradient_button.dart index 4744cbf8d73..c5f9487d1e1 100644 --- a/mobile/packages/ui/lib/components/buttons/gradient_button.dart +++ b/mobile/packages/ui/lib/components/buttons/gradient_button.dart @@ -1,83 +1,109 @@ +import 'package:ente_ui/theme/colors.dart'; import 'package:ente_ui/theme/ente_theme.dart'; import 'package:flutter/material.dart'; -class GradientButton extends StatelessWidget { - final Function? onTap; +enum GradientButtonType { + primary, + secondary, + critical, +} - // text is ignored if child is specified +class GradientButton extends StatelessWidget { + final VoidCallback? onTap; final String text; - - // nullable - final IconData? iconData; - - // padding between the text and icon + final IconData? icon; final double paddingValue; + final GradientButtonType buttonType; const GradientButton({ super.key, this.onTap, this.text = '', - this.iconData, - this.paddingValue = 0.0, + this.icon, + this.paddingValue = 6.0, + this.buttonType = GradientButtonType.primary, }); + static const TextStyle _textStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontFamily: 'Inter-SemiBold', + fontSize: 18, + ); + @override Widget build(BuildContext context) { - Widget buttonContent; - if (iconData == null) { - buttonContent = Text( - text, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - fontFamily: 'Inter-SemiBold', - fontSize: 18, - ), - ); - } else { - buttonContent = Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - iconData, - size: 20, - color: Colors.white, - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 6)), - Text( - text, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - fontFamily: 'Inter-SemiBold', - fontSize: 18, - ), - ), - ], - ); - } - return InkWell( - onTap: onTap as void Function()?, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Container( - height: 56, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: const Alignment(0.1, -0.9), - end: const Alignment(-0.6, 0.9), - colors: onTap != null - ? getEnteColorScheme(context).gradientButtonBgColors - : [ - getEnteColorScheme(context).fillMuted, - getEnteColorScheme(context).fillMuted, - ], - ), + final colorScheme = getEnteColorScheme(context); + final bool isEnabled = onTap != null; + + final Color effectiveBackgroundColor = + _getBackgroundColor(colorScheme, isEnabled); + final Color effectiveTextColor = _getTextColor(colorScheme, isEnabled); + + final TextStyle effectiveTextStyle = _textStyle.copyWith( + color: effectiveTextColor, + ); + + final Widget textWidget = Text(text, style: effectiveTextStyle); + + final Widget content = (icon != null) + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: effectiveTextColor, + ), + Padding(padding: EdgeInsets.symmetric(horizontal: paddingValue)), + if (text.isNotEmpty) textWidget, + ], + ) + : textWidget; + + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Material( + color: effectiveBackgroundColor, + child: InkWell( + onTap: onTap, + splashColor: isEnabled ? null : Colors.transparent, + highlightColor: isEnabled ? null : Colors.transparent, + child: SizedBox( + height: 56, + child: Center(child: content), ), - child: Center(child: buttonContent), ), ), ); } + + Color _getBackgroundColor(EnteColorScheme colorScheme, bool isEnabled) { + if (!isEnabled) { + return colorScheme.fillFaint; + } + switch (buttonType) { + case GradientButtonType.primary: + return colorScheme.primary700; + case GradientButtonType.secondary: + return colorScheme.backdropBase; + case GradientButtonType.critical: + return colorScheme.warning700; + } + } + + Color _getTextColor(EnteColorScheme colorScheme, bool isEnabled) { + if (!isEnabled) { + return colorScheme.textMuted; + } + switch (buttonType) { + case GradientButtonType.primary: + return Colors.white; + case GradientButtonType.secondary: + return colorScheme.textBase; + case GradientButtonType.critical: + return Colors.white; + } + } } diff --git a/mobile/packages/utils/lib/email_util.dart b/mobile/packages/utils/lib/email_util.dart index da27e8c7d90..c3fcf3fd3b7 100644 --- a/mobile/packages/utils/lib/email_util.dart +++ b/mobile/packages/utils/lib/email_util.dart @@ -5,10 +5,14 @@ import 'package:email_validator/email_validator.dart'; import 'package:ente_configuration/base_configuration.dart'; import 'package:ente_logging/logging.dart'; import 'package:ente_strings/extensions.dart'; +import 'package:ente_ui/components/base_bottom_sheet.dart'; import 'package:ente_ui/components/buttons/button_widget.dart'; +import 'package:ente_ui/components/buttons/gradient_button.dart'; import 'package:ente_ui/components/buttons/models/button_type.dart'; import 'package:ente_ui/components/dialog_widget.dart'; import 'package:ente_ui/pages/log_file_viewer.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; +import 'package:ente_ui/utils/toast_util.dart'; import 'package:ente_utils/directory_utils.dart'; import 'package:ente_utils/platform_util.dart'; import 'package:ente_utils/share_utils.dart'; @@ -283,28 +287,34 @@ Future _clientInfo(BaseConfiguration? configuration) async { } void _showNoMailAppsDialog(BuildContext context, String toEmail) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - icon: const Icon(Icons.email_outlined), - title: Text('Email us at $toEmail'), - actions: [ - TextButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: toEmail)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Email address copied')), - ); - }, - child: const Text('Copy Email Address'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, + showBaseBottomSheet( + context, + title: 'Contact Support', + headerSpacing: 20, + child: Builder( + builder: (bottomSheetContext) { + final colorScheme = getEnteColorScheme(bottomSheetContext); + final textTheme = getEnteTextTheme(bottomSheetContext); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Email us at $toEmail', + style: textTheme.body.copyWith(color: colorScheme.textMuted), + textAlign: TextAlign.left, + ), + const SizedBox(height: 20), + GradientButton( + text: 'Copy Email Address', + onTap: () async { + await Clipboard.setData(ClipboardData(text: toEmail)); + Navigator.of(bottomSheetContext).pop(); + showShortToast(context, 'Email address copied'); + }, + ), + ], + ); + }, + ), ); } diff --git a/mobile/packages/utils/lib/share_utils.dart b/mobile/packages/utils/lib/share_utils.dart index 884901c4424..7d867f63f0a 100644 --- a/mobile/packages/utils/lib/share_utils.dart +++ b/mobile/packages/utils/lib/share_utils.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:ente_strings/extensions.dart'; +import "package:ente_ui/components/base_bottom_sheet.dart"; import 'package:ente_ui/components/buttons/button_widget.dart'; +import "package:ente_ui/components/buttons/gradient_button.dart"; import 'package:ente_ui/components/buttons/models/button_type.dart'; import 'package:ente_ui/components/dialog_widget.dart'; +import "package:ente_ui/theme/ente_theme.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:share_plus/share_plus.dart'; @@ -52,6 +55,61 @@ Future shareDialog( ); } +Future showShareSheet( + BuildContext context, + String title, { + required Function saveAction, + required Function sendAction, +}) async { + return showBaseBottomSheet( + context, + headerSpacing: 20, + title: title, + child: Builder( + builder: (context) { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + Platform.isLinux || Platform.isWindows + ? context.strings.saveOnlyDescription + : context.strings.saveOrSendDescription, + style: textTheme.body.copyWith( + color: colorScheme.textMuted, + ), + ), + const SizedBox(height: 20), + GradientButton( + onTap: () async { + await saveAction(); + }, + text: context.strings.save, + ), + if (!Platform.isWindows && !Platform.isLinux) + const SizedBox(height: 20), + if (!Platform.isWindows && !Platform.isLinux) + GestureDetector( + onTap: () async { + await sendAction(); + }, + child: Text( + context.strings.send, + style: textTheme.bodyBold.copyWith( + color: colorScheme.primary700, + decorationColor: colorScheme.primary700, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ); + }, + ), + ); +} + Rect _sharePosOrigin(BuildContext? context, GlobalKey? key) { late final Rect rect; if (context != null) {