From 5ea6269bca9cc21f9a3ec34ff9840ea22384b717 Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Wed, 6 Apr 2022 12:56:40 -0400 Subject: [PATCH] feat(account): ability to update account email and password --- assets/localization/en.json | 10 ++ lib/core/firebase/auth.dart | 40 ++++++ .../platform/window_manager_io.dart | 2 +- lib/core/ui/icons/icon.dart | 2 + lib/modules/settings/core/dialogs.dart | 120 ++++++++++++++++++ .../routes/account/pages/settings.dart | 12 +- .../settings/routes/account/widgets.dart | 1 - .../account/widgets/change_email_tile.dart | 59 +++++++++ .../account/widgets/change_password_tile.dart | 61 +++++++++ .../account/widgets/delete_account_tile.dart | 6 +- localization/settings/en.json | 10 ++ pubspec.lock | 7 + pubspec.yaml | 1 + 13 files changed, 323 insertions(+), 8 deletions(-) create mode 100644 lib/modules/settings/routes/account/widgets/change_email_tile.dart create mode 100644 lib/modules/settings/routes/account/widgets/change_password_tile.dart diff --git a/assets/localization/en.json b/assets/localization/en.json index 72c62e8f..73f30313 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -327,6 +327,7 @@ "settings.ConnectionDetails": "Connection Details", "settings.ConnectionDetailsDescription": "Connection Details for {}", "settings.ConnectionTestFailed": "Connection Test Failed", + "settings.CurrentPassword": "Current Password", "settings.Custom": "Custom…", "settings.CustomHeader": "Custom Header", "settings.CustomHeaders": "Custom Headers", @@ -378,10 +379,14 @@ "settings.EmailSentFailure": "Failed to Reset Password", "settings.EmailSentSuccess": "Email Sent", "settings.EmailSentSuccessMessage": "An email to reset your password has been sent!", + "settings.EmailUpdated": "Email Updated", + "settings.EmailValidation": "Invalid Email Address", "settings.EnableModule": "Enable {}", "settings.EnabledProfile": "Enabled Profile", "settings.EncryptionKey": "Encryption Key", "settings.FailedToDeleteAccount": "Failed to Delete Account", + "settings.FailedToUpdateEmail": "Failed to Update Email", + "settings.FailedToUpdatePassword": "Failed to Update Password", "settings.FeedbackBoard": "Feedback Board", "settings.FeedbackBoardDescription": "Request New Features", "settings.FilterCategory": "Filter Category", @@ -425,6 +430,7 @@ "settings.MustBeValueBetween": "Must be a value between {} and {}", "settings.Network": "Network", "settings.NetworkDescription": "Customize Network Features", + "settings.NewPassword": "New Password", "settings.Notifications": "Notifications", "settings.NotificationsDescription": "Set up Webhooks for Push Notifications", "settings.NoBackupsFound": "No Backups Found", @@ -433,9 +439,11 @@ "settings.OpenLinksIn": "Open Links In…", "settings.Password": "Password", "settings.PasswordHint1": "If your password includes special characters, considering adding a basic authentication header with your username and password instead for better support", + "settings.PasswordUpdated": "Password Updated", "settings.PasswordValidation": "Password Required", "settings.PastDays": "Past Days", "settings.PastDaysInScheduleView": "Past Days In Schedule View", + "settings.PleaseSignInAgain": "Please sign in again", "settings.Profiles": "Profiles", "settings.ProfilesBannerLine1": "Profiles allow you to add multiple instances of modules into LunaSea. You can switch between profiles in the main navigation drawer.", "settings.ProfilesBannerLine2": "Newznab indexer searching and external modules are enabled and shared across all profiles.", @@ -487,6 +495,8 @@ "settings.TestConnection": "Test Connection", "settings.TLSCertificateValidation": "TLS Certificate Validation", "settings.TLSCertificateValidationDescription": "Validate Certificates in TLS Connections", + "settings.UpdateEmail": "Update Email", + "settings.UpdatePassword": "Update Password", "settings.Use24HourTime": "Use 24 Hour Time", "settings.Use24HourTimeDescription": "Show Timestamps in 24 Hour Style", "settings.Username": "Username", diff --git a/lib/core/firebase/auth.dart b/lib/core/firebase/auth.dart index dcd82a78..6141d7fa 100644 --- a/lib/core/firebase/auth.dart +++ b/lib/core/firebase/auth.dart @@ -87,6 +87,46 @@ class LunaFirebaseAuth { } } + Future updateEmail( + String newEmail, + String password, + ) async { + try { + return await user! + .reauthenticateWithCredential(EmailAuthProvider.credential( + email: email!, + password: password, + )) + .then((credentials) => credentials.user!.updateEmail(newEmail)) + .then((_) => LunaFirebaseResponse(state: true)); + } on FirebaseAuthException catch (error) { + return LunaFirebaseResponse(state: false, error: error); + } catch (error, stack) { + LunaLogger().error('Failed to set email: ${user!.email}', error, stack); + rethrow; + } + } + + Future updatePassword( + String newPassword, + String password, + ) async { + try { + return await user! + .reauthenticateWithCredential(EmailAuthProvider.credential( + email: email!, + password: password, + )) + .then((credentials) => credentials.user!.updatePassword(newPassword)) + .then((_) => LunaFirebaseResponse(state: true)); + } on FirebaseAuthException catch (error) { + return LunaFirebaseResponse(state: false, error: error); + } catch (error, stack) { + LunaLogger().error('Failed to set pass: ${user!.email}', error, stack); + rethrow; + } + } + /// Reset a user's password by sending them a password reset email. Future resetPassword(String email) async { instance.sendPasswordResetEmail(email: email); diff --git a/lib/core/system/window_manager/platform/window_manager_io.dart b/lib/core/system/window_manager/platform/window_manager_io.dart index 31e4cf58..460b3583 100644 --- a/lib/core/system/window_manager/platform/window_manager_io.dart +++ b/lib/core/system/window_manager/platform/window_manager_io.dart @@ -41,7 +41,7 @@ class IO implements LunaWindowManager { const minSize = Size(min, min); const initSize = Size(init, init); - await windowManager.setSize(initSize); + if (!kDebugMode) await windowManager.setSize(initSize); // Currently broken on Linux if (!LunaPlatform().isLinux) { await windowManager.setMinimumSize(minSize); diff --git a/lib/core/ui/icons/icon.dart b/lib/core/ui/icons/icon.dart index 1aa44ccd..faab6d74 100644 --- a/lib/core/ui/icons/icon.dart +++ b/lib/core/ui/icons/icon.dart @@ -11,6 +11,7 @@ class LunaIcons { static const IconData DELETE = Icons.delete_rounded; static const IconData DEVICES = Icons.devices_rounded; static const IconData DOWNLOAD = Icons.file_download_rounded; + static const IconData EMAIL = Icons.email_rounded; static const IconData ERROR = Icons.error_rounded; static const IconData FILTER = Icons.filter_list_rounded; static const IconData HISTORY = Icons.history_rounded; @@ -18,6 +19,7 @@ class LunaIcons { static const IconData MONITOR_OFF = Icons.turned_in_not_rounded; static const IconData MONITOR_ON = Icons.turned_in_rounded; static const IconData MUSIC = Icons.music_note_rounded; + static const IconData PASSWORD = Icons.vpn_key_rounded; static const IconData PLAY = Icons.play_arrow_rounded; static const IconData PROFILES = Icons.switch_account_rounded; static const IconData REFRESH = Icons.refresh_rounded; diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart index e7404e23..dfec167c 100644 --- a/lib/modules/settings/core/dialogs.dart +++ b/lib/modules/settings/core/dialogs.dart @@ -1,3 +1,4 @@ +import 'package:email_validator/email_validator.dart'; import 'package:flutter/material.dart'; import 'package:lunasea/modules/settings/core/types/header.dart'; import 'package:tuple/tuple.dart'; @@ -556,6 +557,125 @@ class SettingsDialogs { return Tuple2(_flag, _textController.text); } + Future> updateAccountEmail( + BuildContext context, + ) async { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _flag = false; + + void _setValues(bool flag) { + if (_formKey.currentState!.validate()) { + _flag = flag; + Navigator.of(context).pop(); + } + } + + await LunaDialog.dialog( + context: context, + title: 'settings.UpdateEmail'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Update'.tr(), + onPressed: () => _setValues(true), + ), + ], + content: [ + Form( + key: _formKey, + child: Column( + children: [ + LunaDialog.textFormInput( + controller: _emailController, + title: 'settings.Email'.tr(), + onSubmitted: (_) => _setValues(true), + validator: (value) { + return EmailValidator.validate(value ?? '') + ? null + : 'settings.EmailValidation'.tr(); + }, + ), + LunaDialog.textFormInput( + controller: _passwordController, + title: 'settings.CurrentPassword'.tr(), + obscureText: true, + onSubmitted: (_) => _setValues(true), + validator: (value) { + return value?.isEmpty ?? true + ? 'settings.PasswordValidation'.tr() + : null; + }, + ), + ], + ), + ), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return Tuple3(_flag, _emailController.text, _passwordController.text); + } + + Future> updateAccountPassword( + BuildContext context, + ) async { + final _formKey = GlobalKey(); + final _currentPassController = TextEditingController(); + final _newPassController = TextEditingController(); + bool _flag = false; + + void _setValues(bool flag) { + if (_formKey.currentState!.validate()) { + _flag = flag; + Navigator.of(context).pop(); + } + } + + await LunaDialog.dialog( + context: context, + title: 'settings.UpdatePassword'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Update'.tr(), + onPressed: () => _setValues(true), + ), + ], + content: [ + Form( + key: _formKey, + child: Column( + children: [ + LunaDialog.textFormInput( + controller: _currentPassController, + title: 'settings.CurrentPassword'.tr(), + obscureText: true, + onSubmitted: (_) => _setValues(true), + validator: (value) { + return value?.isEmpty ?? true + ? 'settings.PasswordValidation'.tr() + : null; + }, + ), + LunaDialog.textFormInput( + controller: _newPassController, + title: 'settings.NewPassword'.tr(), + obscureText: true, + onSubmitted: (_) => _setValues(true), + validator: (value) { + return value?.isEmpty ?? true + ? 'settings.PasswordValidation'.tr() + : null; + }, + ), + ], + ), + ), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return Tuple3(_flag, _newPassController.text, _currentPassController.text); + } + Future> addProfile( BuildContext context, List profiles, diff --git a/lib/modules/settings/routes/account/pages/settings.dart b/lib/modules/settings/routes/account/pages/settings.dart index 61553289..d54ee8ea 100644 --- a/lib/modules/settings/routes/account/pages/settings.dart +++ b/lib/modules/settings/routes/account/pages/settings.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/settings.dart'; +import '../widgets/change_email_tile.dart'; +import '../widgets/change_password_tile.dart'; +import '../widgets/delete_account_tile.dart'; + class SettingsAccountSettingsRouter extends SettingsPageRouter { SettingsAccountSettingsRouter() : super('/settings/account/settings'); @@ -27,12 +31,12 @@ class _State extends State<_SettingsAccountSettingsRoute> Widget build(BuildContext context) { return LunaScaffold( scaffoldKey: _scaffoldKey, - appBar: _appBar() as PreferredSizeWidget?, + appBar: _appBar(), body: _body(), ); } - Widget _appBar() { + PreferredSizeWidget _appBar() { return LunaAppBar( title: 'settings.AccountSettings'.tr(), scrollControllers: [scrollController], @@ -43,7 +47,9 @@ class _State extends State<_SettingsAccountSettingsRoute> return LunaListView( controller: scrollController, children: const [ - SettingsAccountDeleteAccountTile(), + ChangeEmailTile(), + ChangePasswordTile(), + DeleteAccountTile(), ], ); } diff --git a/lib/modules/settings/routes/account/widgets.dart b/lib/modules/settings/routes/account/widgets.dart index e2cba8bb..85cd10ff 100644 --- a/lib/modules/settings/routes/account/widgets.dart +++ b/lib/modules/settings/routes/account/widgets.dart @@ -1,4 +1,3 @@ export 'widgets/config_backup_tile.dart'; export 'widgets/config_delete_tile.dart'; export 'widgets/config_restore_tile.dart'; -export 'widgets/delete_account_tile.dart'; diff --git a/lib/modules/settings/routes/account/widgets/change_email_tile.dart b/lib/modules/settings/routes/account/widgets/change_email_tile.dart new file mode 100644 index 00000000..f27bd2ef --- /dev/null +++ b/lib/modules/settings/routes/account/widgets/change_email_tile.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class ChangeEmailTile extends StatefulWidget { + const ChangeEmailTile({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _loadingState = LunaLoadingState.INACTIVE; + + void updateState(LunaLoadingState state) { + if (mounted) setState(() => _loadingState = state); + } + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'settings.UpdateEmail'.tr(), + trailing: LunaIconButton( + icon: LunaIcons.EMAIL, + loadingState: _loadingState, + ), + onTap: _update, + ); + } + + Future _update() async { + if (_loadingState == LunaLoadingState.ACTIVE) return; + updateState(LunaLoadingState.ACTIVE); + + Tuple3 _result = + await SettingsDialogs().updateAccountEmail(context); + if (_result.item1) { + await LunaFirebaseAuth() + .updateEmail(_result.item2, _result.item3) + .then((res) { + if (res.state) { + showLunaSuccessSnackBar( + title: 'settings.EmailUpdated'.tr(), + message: _result.item2, + ); + } else { + updateState(LunaLoadingState.INACTIVE); + showLunaErrorSnackBar( + title: 'settings.FailedToUpdateEmail'.tr(), + message: res.error?.message ?? 'lunasea.UnknownError'.tr(), + ); + } + }); + } + updateState(LunaLoadingState.INACTIVE); + } +} diff --git a/lib/modules/settings/routes/account/widgets/change_password_tile.dart b/lib/modules/settings/routes/account/widgets/change_password_tile.dart new file mode 100644 index 00000000..1ca15eb5 --- /dev/null +++ b/lib/modules/settings/routes/account/widgets/change_password_tile.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class ChangePasswordTile extends StatefulWidget { + const ChangePasswordTile({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _loadingState = LunaLoadingState.INACTIVE; + + void updateState(LunaLoadingState state) { + if (mounted) setState(() => _loadingState = state); + } + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'settings.UpdatePassword'.tr(), + trailing: LunaIconButton( + icon: LunaIcons.PASSWORD, + loadingState: _loadingState, + ), + onTap: _delete, + ); + } + + Future _delete() async { + if (_loadingState == LunaLoadingState.ACTIVE) return; + updateState(LunaLoadingState.ACTIVE); + + Tuple3 _result = + await SettingsDialogs().updateAccountPassword(context); + if (_result.item1) { + await LunaFirebaseAuth() + .updatePassword(_result.item2, _result.item3) + .then((res) { + if (res.state) { + showLunaSuccessSnackBar( + title: 'settings.PasswordUpdated'.tr(), + message: 'settings.PleaseSignInAgain'.tr(), + ); + Navigator.of(context).lunaSafetyPop(); + LunaFirebaseAuth().signOut(); + } else { + updateState(LunaLoadingState.INACTIVE); + showLunaErrorSnackBar( + title: 'settings.FailedToUpdatePassword'.tr(), + message: res.error?.message ?? 'lunasea.UnknownError'.tr(), + ); + } + }); + } + updateState(LunaLoadingState.INACTIVE); + } +} diff --git a/lib/modules/settings/routes/account/widgets/delete_account_tile.dart b/lib/modules/settings/routes/account/widgets/delete_account_tile.dart index 0288ae12..43a46741 100644 --- a/lib/modules/settings/routes/account/widgets/delete_account_tile.dart +++ b/lib/modules/settings/routes/account/widgets/delete_account_tile.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/settings.dart'; -class SettingsAccountDeleteAccountTile extends StatefulWidget { - const SettingsAccountDeleteAccountTile({ +class DeleteAccountTile extends StatefulWidget { + const DeleteAccountTile({ Key? key, }) : super(key: key); @@ -11,7 +11,7 @@ class SettingsAccountDeleteAccountTile extends StatefulWidget { State createState() => _State(); } -class _State extends State { +class _State extends State { LunaLoadingState _loadingState = LunaLoadingState.INACTIVE; void updateState(LunaLoadingState state) { diff --git a/localization/settings/en.json b/localization/settings/en.json index 13c618fb..bc81cd61 100644 --- a/localization/settings/en.json +++ b/localization/settings/en.json @@ -65,6 +65,7 @@ "settings.ConnectionDetails": "Connection Details", "settings.ConnectionDetailsDescription": "Connection Details for {}", "settings.ConnectionTestFailed": "Connection Test Failed", + "settings.CurrentPassword": "Current Password", "settings.Custom": "Custom…", "settings.CustomHeader": "Custom Header", "settings.CustomHeaders": "Custom Headers", @@ -116,10 +117,14 @@ "settings.EmailSentFailure": "Failed to Reset Password", "settings.EmailSentSuccess": "Email Sent", "settings.EmailSentSuccessMessage": "An email to reset your password has been sent!", + "settings.EmailUpdated": "Email Updated", + "settings.EmailValidation": "Invalid Email Address", "settings.EnableModule": "Enable {}", "settings.EnabledProfile": "Enabled Profile", "settings.EncryptionKey": "Encryption Key", "settings.FailedToDeleteAccount": "Failed to Delete Account", + "settings.FailedToUpdateEmail": "Failed to Update Email", + "settings.FailedToUpdatePassword": "Failed to Update Password", "settings.FeedbackBoard": "Feedback Board", "settings.FeedbackBoardDescription": "Request New Features", "settings.FilterCategory": "Filter Category", @@ -163,6 +168,7 @@ "settings.MustBeValueBetween": "Must be a value between {} and {}", "settings.Network": "Network", "settings.NetworkDescription": "Customize Network Features", + "settings.NewPassword": "New Password", "settings.Notifications": "Notifications", "settings.NotificationsDescription": "Set up Webhooks for Push Notifications", "settings.NoBackupsFound": "No Backups Found", @@ -171,9 +177,11 @@ "settings.OpenLinksIn": "Open Links In…", "settings.Password": "Password", "settings.PasswordHint1": "If your password includes special characters, considering adding a basic authentication header with your username and password instead for better support", + "settings.PasswordUpdated": "Password Updated", "settings.PasswordValidation": "Password Required", "settings.PastDays": "Past Days", "settings.PastDaysInScheduleView": "Past Days In Schedule View", + "settings.PleaseSignInAgain": "Please sign in again", "settings.Profiles": "Profiles", "settings.ProfilesBannerLine1": "Profiles allow you to add multiple instances of modules into LunaSea. You can switch between profiles in the main navigation drawer.", "settings.ProfilesBannerLine2": "Newznab indexer searching and external modules are enabled and shared across all profiles.", @@ -225,6 +233,8 @@ "settings.TestConnection": "Test Connection", "settings.TLSCertificateValidation": "TLS Certificate Validation", "settings.TLSCertificateValidationDescription": "Validate Certificates in TLS Connections", + "settings.UpdateEmail": "Update Email", + "settings.UpdatePassword": "Update Password", "settings.Use24HourTime": "Use 24 Hour Time", "settings.Use24HourTimeDescription": "Show Timestamps in 24 Hour Style", "settings.Username": "Username", diff --git a/pubspec.lock b/pubspec.lock index f3c53ade..850fd4c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -274,6 +274,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.2" + email_validator: + dependency: "direct main" + description: + name: email_validator + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" encrypt: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f4cc36e0..1f450e3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: device_preview: ^1.0.0 dio: ^4.0.6 easy_localization: ^3.0.0 + email_validator: ^2.0.1 encrypt: ^5.0.1 envify: ^2.0.2 expandable: ^5.0.1