diff --git a/lib/src/providers/auth_provider.dart b/lib/src/providers/auth_provider.dart index 52c5687..ba7ef14 100644 --- a/lib/src/providers/auth_provider.dart +++ b/lib/src/providers/auth_provider.dart @@ -139,4 +139,41 @@ class AuthProvider { return exceptionsFactory.exceptionCaught()!; } } + + Future changeEmail(String newEmail, String password) async { + try { + user = _auth.currentUser; + final currentEmail = user!.email; + final credential = EmailAuthProvider.credential( + email: currentEmail!, + password: password, + ); + await user! + .reauthenticateWithCredential(credential) + .then((value) => user!.updateEmail(newEmail)); + await user!.sendEmailVerification(); + logout(); + } on FirebaseAuthException catch (e) { + exceptionsFactory = ExceptionsFactory(e.code); + return exceptionsFactory.exceptionCaught()!; + } + } + + Future changePassword(String oldPassword, String newPassword) async { + try { + user = _auth.currentUser; + final currentEmail = user!.email; + final credential = EmailAuthProvider.credential( + email: currentEmail!, + password: oldPassword, + ); + await user! + .reauthenticateWithCredential(credential) + .then((value) => user!.updatePassword(newPassword)); + logout(); + } on FirebaseAuthException catch (e) { + exceptionsFactory = ExceptionsFactory(e.code); + return exceptionsFactory.exceptionCaught()!; + } + } } diff --git a/lib/src/screens/change_email/change_email_screen.dart b/lib/src/screens/change_email/change_email_screen.dart new file mode 100644 index 0000000..0de4d0e --- /dev/null +++ b/lib/src/screens/change_email/change_email_screen.dart @@ -0,0 +1,190 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:monerate/src/providers/export.dart'; +import 'package:monerate/src/screens/export.dart'; +import 'package:monerate/src/utilities/export.dart'; +import 'package:monerate/src/widgets/export.dart'; + +class ChangeEmailScreen extends StatefulWidget { + static const kID = 'change_email_screen'; + const ChangeEmailScreen({Key? key}) : super(key: key); + + @override + _ChangeEmailScreenState createState() => _ChangeEmailScreenState(); +} + +class _ChangeEmailScreenState extends State { + final TextEditingController newEmailController = TextEditingController(); + final TextEditingController confirmEmailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final _formKey = GlobalKey(); + + bool _showPassword = false; + + final AuthProvider authProvider = AuthProvider(); + + @override + void dispose() { + // Clean up controllers when form is disposed + newEmailController.dispose(); + confirmEmailController.dispose(); + passwordController.dispose(); + super.dispose(); + } + + void closeDialogBox() { + return Navigator.pop(context); + } + + Future _updateEmail() async { + return authProvider.changeEmail( + confirmEmailController.text, + passwordController.text, + ); + } + + Future displayConfirmationDialog() { + return customAlertDialog( + context: context, + title: "Confirmation Required", + content: + "By continuing with this action, you will be signed out of the current session. A verification email will be sent to the provided email which must be verified before logging in with your new credentials. Do you wish to continue?", + actions: [ + OutlinedButton( + onPressed: () => closeDialogBox(), + child: const Text( + "Cancel", + ), + ), + ElevatedButton( + onPressed: () async { + EasyLoading.show(status: 'loading...'); + final result = await _updateEmail(); + if (result != null) { + closeDialogBox(); + EasyLoading.showError(result); + } else { + Navigator.pushReplacementNamed( + context, + LoginScreen.kID, + ); + EasyLoading.dismiss(); + } + }, + child: const Text("Update Email"), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + "Change Email Address", + ), + centerTitle: true, + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: newEmailController, + keyboardType: TextInputType.emailAddress, + validator: EmailValidator().validateEmail, + onSaved: (value) { + newEmailController.text = value!; + }, + decoration: const InputDecoration( + hintText: 'New Email Address', + ), + ), + const SizedBox( + height: 20, + ), + TextFormField( + controller: confirmEmailController, + keyboardType: TextInputType.emailAddress, + validator: (value) { + return EmailValidator().confirmEmail( + newEmailController.text, + confirmEmailController.text, + ); + }, + onSaved: (value) { + confirmEmailController.text = value!; + }, + decoration: const InputDecoration( + hintText: 'Confirm New Email Address', + ), + ), + const SizedBox( + height: 20, + ), + TextFormField( + controller: passwordController, + validator: PasswordValidator().validatePassword, + obscureText: !_showPassword, + textInputAction: TextInputAction.done, + onSaved: (password) { + passwordController.text = password!; + }, + decoration: InputDecoration( + hintText: 'Password', + suffixIcon: GestureDetector( + onTap: () => setState(() { + _showPassword = !_showPassword; + }), + child: Icon( + _showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + FocusScope.of(context).unfocus(); + await displayConfirmationDialog(); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 40, + vertical: 10, + ), + ), + child: const Text( + "Submit", + style: TextStyle( + fontSize: 24, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/change_email/export.dart b/lib/src/screens/change_email/export.dart new file mode 100644 index 0000000..a299f58 --- /dev/null +++ b/lib/src/screens/change_email/export.dart @@ -0,0 +1 @@ +export 'change_email_screen.dart'; diff --git a/lib/src/screens/change_password/change_password_screen.dart b/lib/src/screens/change_password/change_password_screen.dart new file mode 100644 index 0000000..2c8a1a6 --- /dev/null +++ b/lib/src/screens/change_password/change_password_screen.dart @@ -0,0 +1,201 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:monerate/src/providers/export.dart'; +import 'package:monerate/src/screens/export.dart'; +import 'package:monerate/src/utilities/export.dart'; +import 'package:monerate/src/widgets/export.dart'; + +class ChangePasswordScreen extends StatefulWidget { + static const kID = "change_password_screen"; + const ChangePasswordScreen({Key? key}) : super(key: key); + + @override + _ChangePasswordScreenState createState() => _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends State { + final TextEditingController oldPasswordController = TextEditingController(); + final TextEditingController newPasswordController = TextEditingController(); + final TextEditingController confirmNewPasswordController = + TextEditingController(); + final _formKey = GlobalKey(); + bool _showPassword = false; + + final AuthProvider authProvider = AuthProvider(); + + void closeDialogBox() { + return Navigator.pop(context); + } + + Future _updatePassword() async { + return authProvider.changePassword( + oldPasswordController.text, + confirmNewPasswordController.text, + ); + } + + Future displayConfirmationDialog() { + return customAlertDialog( + context: context, + title: "Confirmation Required", + content: + "By continuing with this action, you will be signed out of the current session and will be asked to login with the new credentials you have provided. Do you wish to continue?", + actions: [ + OutlinedButton( + onPressed: () => closeDialogBox(), + child: const Text( + "Cancel", + ), + ), + ElevatedButton( + onPressed: () async { + EasyLoading.show(status: 'loading...'); + final result = await _updatePassword(); + if (result != null) { + closeDialogBox(); + EasyLoading.showError(result); + } else { + Navigator.pushReplacementNamed( + context, + LoginScreen.kID, + ); + EasyLoading.dismiss(); + } + }, + child: const Text("Update Email"), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + "Change Password", + ), + centerTitle: true, + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Form( + key: _formKey, + child: Column( + children: [ + const SizedBox( + height: 20, + ), + TextFormField( + controller: oldPasswordController, + validator: PasswordValidator().validatePassword, + obscureText: !_showPassword, + textInputAction: TextInputAction.done, + onSaved: (password) { + oldPasswordController.text = password!; + }, + decoration: InputDecoration( + hintText: 'Old Password', + suffixIcon: GestureDetector( + onTap: () => setState(() { + _showPassword = !_showPassword; + }), + child: Icon( + _showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + TextFormField( + controller: newPasswordController, + validator: PasswordValidator().validatePassword, + obscureText: !_showPassword, + textInputAction: TextInputAction.done, + onSaved: (password) { + newPasswordController.text = password!; + }, + decoration: InputDecoration( + hintText: 'New Password', + suffixIcon: GestureDetector( + onTap: () => setState(() { + _showPassword = !_showPassword; + }), + child: Icon( + _showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + TextFormField( + controller: confirmNewPasswordController, + validator: PasswordValidator().validatePassword, + obscureText: !_showPassword, + textInputAction: TextInputAction.done, + onSaved: (password) { + confirmNewPasswordController.text = password!; + }, + decoration: InputDecoration( + hintText: 'Confirm New Password', + suffixIcon: GestureDetector( + onTap: () => setState(() { + _showPassword = !_showPassword; + }), + child: Icon( + _showPassword + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + FocusScope.of(context).unfocus(); + await displayConfirmationDialog(); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 40, + vertical: 10, + ), + ), + child: const Text( + "Submit", + style: TextStyle( + fontSize: 24, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/change_password/export.dart b/lib/src/screens/change_password/export.dart new file mode 100644 index 0000000..9fe08d8 --- /dev/null +++ b/lib/src/screens/change_password/export.dart @@ -0,0 +1 @@ +export 'change_password_screen.dart'; \ No newline at end of file diff --git a/lib/src/screens/dashboard/settings_screen.dart b/lib/src/screens/dashboard/settings_screen.dart index 700bda1..922eef3 100644 --- a/lib/src/screens/dashboard/settings_screen.dart +++ b/lib/src/screens/dashboard/settings_screen.dart @@ -39,44 +39,78 @@ class _SettingsScreenState extends State { const SizedBox( height: 20, ), - ListTile( - title: const Text("Personal Details"), - onTap: () async { - EasyLoading.show(status: 'loading...'); - final result = await _viewPersonalDetails(); - if (result.runtimeType == String) { - EasyLoading.showError(result.toString()); - } else { - Navigator.pushNamed( - context, - ViewProfileScreen.kID, - arguments: result, - ); - EasyLoading.dismiss(); - } - }, - tileColor: Colors.blue, + Card( + elevation: 8, + child: ListTile( + leading: const Icon(Icons.person), + title: const Text("Personal Details"), + onTap: () async { + EasyLoading.show(status: 'loading...'); + final result = await _viewPersonalDetails(); + if (result.runtimeType == String) { + EasyLoading.showError(result.toString()); + } else { + Navigator.pushNamed( + context, + ViewProfileScreen.kID, + arguments: result, + ); + EasyLoading.dismiss(); + } + }, + ), ), const SizedBox( height: 20, ), - ListTile( - title: const Text("Logout"), - onTap: () async { - EasyLoading.show(); - final result = await _signOut(); - if (result != null) { - EasyLoading.showError(result); - } else { - Navigator.pushReplacementNamed( - context, - LoginScreen.kID, - ); - EasyLoading.dismiss(); - } - }, - tileColor: Colors.purple, - ) + Card( + elevation: 8, + child: ListTile( + leading: const Icon(Icons.email), + title: const Text("Change Email"), + onTap: () => Navigator.pushNamed( + context, + ChangeEmailScreen.kID, + ), + ), + ), + const SizedBox( + height: 20, + ), + Card( + elevation: 8, + child: ListTile( + leading: const Icon(Icons.password), + title: const Text("Change Password"), + onTap: () => Navigator.pushNamed( + context, + ChangePasswordScreen.kID, + ), + ), + ), + const SizedBox( + height: 20, + ), + Card( + elevation: 8, + child: ListTile( + leading: const Icon(Icons.logout), + title: const Text("Logout"), + onTap: () async { + EasyLoading.show(); + final result = await _signOut(); + if (result != null) { + EasyLoading.showError(result); + } else { + Navigator.pushReplacementNamed( + context, + LoginScreen.kID, + ); + EasyLoading.dismiss(); + } + }, + ), + ), ], ), ), diff --git a/lib/src/screens/export.dart b/lib/src/screens/export.dart index bbf0c40..0d9afb6 100644 --- a/lib/src/screens/export.dart +++ b/lib/src/screens/export.dart @@ -1,3 +1,5 @@ +export 'change_email/export.dart'; +export 'change_password/export.dart'; export 'complete_profile/export.dart'; export 'construction_page.dart'; export 'dashboard/export.dart'; diff --git a/lib/src/utilities/constants/route_constants.dart b/lib/src/utilities/constants/route_constants.dart index 5e2d00b..b2999f5 100644 --- a/lib/src/utilities/constants/route_constants.dart +++ b/lib/src/utilities/constants/route_constants.dart @@ -17,4 +17,6 @@ Map kRoutes = { const CompleteSupportManagerProfile(), DashboardScreen.kID: (context) => const DashboardScreen(), ViewProfileScreen.kID: (context) => const ViewProfileScreen(), + ChangeEmailScreen.kID: (context) => const ChangeEmailScreen(), + ChangePasswordScreen.kID: (context) => const ChangePasswordScreen(), }; diff --git a/lib/src/utilities/form_validators/password_validator.dart b/lib/src/utilities/form_validators/password_validator.dart index 6b53701..75521ae 100644 --- a/lib/src/utilities/form_validators/password_validator.dart +++ b/lib/src/utilities/form_validators/password_validator.dart @@ -3,7 +3,7 @@ import 'package:monerate/src/utilities/export.dart'; class PasswordValidator extends Validator { String? validatePassword(String? value) { if (super.presenceDetection(value) == false) { - return 'A Password is required to login'; + return 'A Password is required'; } if (!RegExp(r'^.{6,}$').hasMatch(value!)) { return "Enter a valid password (Minimum 6 chararacters)"; diff --git a/pubspec.yaml b/pubspec.yaml index 7529372..565244d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.3.0+1 +version: 3.2.0+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/test/src/utilities/form_validators/password_validator_test.dart b/test/src/utilities/form_validators/password_validator_test.dart index 65ad447..979944b 100644 --- a/test/src/utilities/form_validators/password_validator_test.dart +++ b/test/src/utilities/form_validators/password_validator_test.dart @@ -5,7 +5,7 @@ void main() { group('Password Validator: ', () { test('Empty password returns error', () { final result = PasswordValidator().validatePassword(''); - expect(result, "A Password is required to login"); + expect(result, "A Password is required"); }); test('Password less than six characters returns error', () { @@ -22,7 +22,7 @@ void main() { test('Empty passwords returns error', () { final result = PasswordValidator().confirmPassword('', ''); - expect(result, "A Password is required to login"); + expect(result, "A Password is required"); }); test('If passwords are less than 6 characters, return error', () {