From b27fe5bbc82a0fc8f79b100f49675e6036712008 Mon Sep 17 00:00:00 2001 From: Vandad Nahavandipoor Date: Tue, 11 Jan 2022 07:59:53 +0100 Subject: [PATCH] step-27 --- ios/Podfile.lock | 30 ++++++ lib/main.dart | 3 + lib/services/auth/auth_provider.dart | 1 + lib/services/auth/auth_service.dart | 4 + lib/services/auth/bloc/auth_bloc.dart | 42 ++++++++ lib/services/auth/bloc/auth_event.dart | 5 + lib/services/auth/bloc/auth_state.dart | 10 ++ lib/services/auth/firebase_auth_provider.dart | 18 ++++ .../password_reset_email_sent_dialog.dart | 14 +++ lib/views/forgot_password_view.dart | 88 +++++++++++++++++ lib/views/login_view.dart | 98 +++++++++++-------- lib/views/register_view.dart | 92 +++++++++-------- test/auth_test.dart | 5 + 13 files changed, 329 insertions(+), 81 deletions(-) create mode 100644 lib/utilities/dialogs/password_reset_email_sent_dialog.dart create mode 100644 lib/views/forgot_password_view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5c4beae..0cbe142 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -423,6 +423,9 @@ PODS: - GoogleUtilities/UserDefaults (~> 7.6) - PromisesObjC (< 3.0, >= 1.2) - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - GoogleAppMeasurement (8.9.1): - GoogleAppMeasurement/AdIdSupport (= 8.9.1) - GoogleUtilities/AppDelegateSwizzler (~> 7.6) @@ -497,7 +500,16 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) + - path_provider_ios (0.0.1): + - Flutter - PromisesObjC (2.0.0) + - share_plus (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) @@ -505,6 +517,10 @@ DEPENDENCIES: - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: @@ -517,6 +533,7 @@ SPEC REPOS: - FirebaseCoreDiagnostics - FirebaseFirestore - FirebaseInstallations + - FMDB - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities @@ -538,6 +555,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_core/ios" Flutter: :path: Flutter + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: abseil: 6c8eb7892aefa08d929b39f9bb108e5367e3228f @@ -554,6 +579,7 @@ SPEC CHECKSUMS: FirebaseFirestore: 15ae9648476436efed698a909e44c4737498f9b4 FirebaseInstallations: 830327b45345ffc859eaa9c17bcd5ae893fd5425 Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleAppMeasurement: 837649ad3987936c232f6717c5680216f6243d24 GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 @@ -562,7 +588,11 @@ SPEC CHECKSUMS: GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 leveldb-library: 50c7b45cbd7bf543c81a468fe557a16ae3db8729 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b diff --git a/lib/main.dart b/lib/main.dart index 7af87c9..9e8b64f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:mynotes/services/auth/bloc/auth_bloc.dart'; import 'package:mynotes/services/auth/bloc/auth_event.dart'; import 'package:mynotes/services/auth/bloc/auth_state.dart'; import 'package:mynotes/services/auth/firebase_auth_provider.dart'; +import 'package:mynotes/views/forgot_password_view.dart'; import 'package:mynotes/views/login_view.dart'; import 'package:mynotes/views/notes/create_update_note_view.dart'; import 'package:mynotes/views/notes/notes_view.dart'; @@ -55,6 +56,8 @@ class HomePage extends StatelessWidget { return const VerifyEmailView(); } else if (state is AuthStateLoggedOut) { return const LoginView(); + } else if (state is AuthStateForgotPassword) { + return const ForgotPasswordView(); } else if (state is AuthStateRegistering) { return const RegisterView(); } else { diff --git a/lib/services/auth/auth_provider.dart b/lib/services/auth/auth_provider.dart index 864b28c..02b6693 100644 --- a/lib/services/auth/auth_provider.dart +++ b/lib/services/auth/auth_provider.dart @@ -13,4 +13,5 @@ abstract class AuthProvider { }); Future logOut(); Future sendEmailVerification(); + Future sendPasswordReset({required String toEmail}); } diff --git a/lib/services/auth/auth_service.dart b/lib/services/auth/auth_service.dart index fd95ec9..c203d05 100644 --- a/lib/services/auth/auth_service.dart +++ b/lib/services/auth/auth_service.dart @@ -39,4 +39,8 @@ class AuthService implements AuthProvider { @override Future initialize() => provider.initialize(); + + @override + Future sendPasswordReset({required String toEmail}) => + provider.sendPasswordReset(toEmail: toEmail); } diff --git a/lib/services/auth/bloc/auth_bloc.dart b/lib/services/auth/bloc/auth_bloc.dart index ec104f2..65a80bb 100644 --- a/lib/services/auth/bloc/auth_bloc.dart +++ b/lib/services/auth/bloc/auth_bloc.dart @@ -6,6 +6,48 @@ import 'package:mynotes/services/auth/bloc/auth_state.dart'; class AuthBloc extends Bloc { AuthBloc(AuthProvider provider) : super(const AuthStateUninitialized(isLoading: true)) { + on((event, emit) { + emit(const AuthStateRegistering( + exception: null, + isLoading: false, + )); + }); + //forgot password + on((event, emit) async { + emit(const AuthStateForgotPassword( + exception: null, + hasSentEmail: false, + isLoading: false, + )); + final email = event.email; + if (email == null) { + return; // user just wants to go to forgot-password screen + } + + // user wants to actually send a forgot-password email + emit(const AuthStateForgotPassword( + exception: null, + hasSentEmail: false, + isLoading: true, + )); + + bool didSendEmail; + Exception? exception; + try { + await provider.sendPasswordReset(toEmail: email); + didSendEmail = true; + exception = null; + } on Exception catch (e) { + didSendEmail = false; + exception = e; + } + + emit(AuthStateForgotPassword( + exception: exception, + hasSentEmail: didSendEmail, + isLoading: false, + )); + }); // send email verification on((event, emit) async { await provider.sendEmailVerification(); diff --git a/lib/services/auth/bloc/auth_event.dart b/lib/services/auth/bloc/auth_event.dart index 72c14d7..5df5cfe 100644 --- a/lib/services/auth/bloc/auth_event.dart +++ b/lib/services/auth/bloc/auth_event.dart @@ -29,6 +29,11 @@ class AuthEventShouldRegister extends AuthEvent { const AuthEventShouldRegister(); } +class AuthEventForgotPassword extends AuthEvent { + final String? email; + const AuthEventForgotPassword({this.email}); +} + class AuthEventLogOut extends AuthEvent { const AuthEventLogOut(); } diff --git a/lib/services/auth/bloc/auth_state.dart b/lib/services/auth/bloc/auth_state.dart index c16585c..e5e8afd 100644 --- a/lib/services/auth/bloc/auth_state.dart +++ b/lib/services/auth/bloc/auth_state.dart @@ -25,6 +25,16 @@ class AuthStateRegistering extends AuthState { }) : super(isLoading: isLoading); } +class AuthStateForgotPassword extends AuthState { + final Exception? exception; + final bool hasSentEmail; + const AuthStateForgotPassword({ + required this.exception, + required this.hasSentEmail, + required bool isLoading, + }) : super(isLoading: isLoading); +} + class AuthStateLoggedIn extends AuthState { final AuthUser user; const AuthStateLoggedIn({ diff --git a/lib/services/auth/firebase_auth_provider.dart b/lib/services/auth/firebase_auth_provider.dart index c98c237..324fad0 100644 --- a/lib/services/auth/firebase_auth_provider.dart +++ b/lib/services/auth/firebase_auth_provider.dart @@ -104,4 +104,22 @@ class FirebaseAuthProvider implements AuthProvider { throw UserNotLoggedInAuthException(); } } + + @override + Future sendPasswordReset({required String toEmail}) async { + try { + await FirebaseAuth.instance.sendPasswordResetEmail(email: toEmail); + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'firebase_auth/invalid-email': + throw InvalidEmailAuthException(); + case 'firebase_auth/user-not-found': + throw UserNotFoundAuthException(); + default: + throw GenericAuthException(); + } + } catch (_) { + throw GenericAuthException(); + } + } } diff --git a/lib/utilities/dialogs/password_reset_email_sent_dialog.dart b/lib/utilities/dialogs/password_reset_email_sent_dialog.dart new file mode 100644 index 0000000..d4ede00 --- /dev/null +++ b/lib/utilities/dialogs/password_reset_email_sent_dialog.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:mynotes/utilities/dialogs/generic_dialog.dart'; + +Future showPasswordResetSentDialog(BuildContext context) { + return showGenericDialog( + context: context, + title: 'Password Reset', + content: + 'We have now sent you a password reset link. Please check your email for more information.', + optionsBuilder: () => { + 'OK': null, + }, + ); +} diff --git a/lib/views/forgot_password_view.dart b/lib/views/forgot_password_view.dart new file mode 100644 index 0000000..40b53db --- /dev/null +++ b/lib/views/forgot_password_view.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mynotes/services/auth/bloc/auth_bloc.dart'; +import 'package:mynotes/services/auth/bloc/auth_event.dart'; +import 'package:mynotes/services/auth/bloc/auth_state.dart'; +import 'package:mynotes/utilities/dialogs/error_dialog.dart'; +import 'package:mynotes/utilities/dialogs/password_reset_email_sent_dialog.dart'; + +class ForgotPasswordView extends StatefulWidget { + const ForgotPasswordView({Key? key}) : super(key: key); + + @override + _ForgotPasswordViewState createState() => _ForgotPasswordViewState(); +} + +class _ForgotPasswordViewState extends State { + late final TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) async { + if (state is AuthStateForgotPassword) { + if (state.hasSentEmail) { + _controller.clear(); + await showPasswordResetSentDialog(context); + } + if (state.exception != null) { + await showErrorDialog(context, + 'We could not process your request. Please make sure that you are a registered user, or if not, register a user now by going back one step.'); + } + } + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Forgot Password'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Text( + 'If you forgot your password, simply enter your email and we will send you a password reset link.'), + TextField( + keyboardType: TextInputType.emailAddress, + autocorrect: false, + autofocus: true, + controller: _controller, + decoration: const InputDecoration( + hintText: 'Your email address....', + ), + ), + TextButton( + onPressed: () { + final email = _controller.text; + context + .read() + .add(AuthEventForgotPassword(email: email)); + }, + child: const Text('Send me password reset link'), + ), + TextButton( + onPressed: () { + context.read().add( + const AuthEventLogOut(), + ); + }, + child: const Text('Back to login page'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/login_view.dart b/lib/views/login_view.dart index 1469693..43ec825 100644 --- a/lib/views/login_view.dart +++ b/lib/views/login_view.dart @@ -39,7 +39,10 @@ class _LoginViewState extends State { listener: (context, state) async { if (state is AuthStateLoggedOut) { if (state.exception is UserNotFoundAuthException) { - await showErrorDialog(context, 'User not found'); + await showErrorDialog( + context, + 'Cannot find a user with the entered credentials!', + ); } else if (state.exception is WrongPasswordAuthException) { await showErrorDialog(context, 'Wrong credentials'); } else if (state.exception is GenericAuthException) { @@ -51,48 +54,61 @@ class _LoginViewState extends State { appBar: AppBar( title: const Text('Login'), ), - body: Column( - children: [ - TextField( - controller: _email, - enableSuggestions: false, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - hintText: 'Enter your email here', + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Text( + 'Please log in to your account in order to interact with and create notes!'), + TextField( + controller: _email, + enableSuggestions: false, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + hintText: 'Enter your email here', + ), ), - ), - TextField( - controller: _password, - obscureText: true, - enableSuggestions: false, - autocorrect: false, - decoration: const InputDecoration( - hintText: 'Enter your password here', + TextField( + controller: _password, + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: 'Enter your password here', + ), ), - ), - TextButton( - onPressed: () async { - final email = _email.text; - final password = _password.text; - context.read().add( - AuthEventLogIn( - email, - password, - ), - ); - }, - child: const Text('Login'), - ), - TextButton( - onPressed: () { - context.read().add( - const AuthEventShouldRegister(), - ); - }, - child: const Text('Not registered yet? Register here!'), - ) - ], + TextButton( + onPressed: () async { + final email = _email.text; + final password = _password.text; + context.read().add( + AuthEventLogIn( + email, + password, + ), + ); + }, + child: const Text('Login'), + ), + TextButton( + onPressed: () { + context.read().add( + const AuthEventForgotPassword(), + ); + }, + child: const Text('I forgot my password'), + ), + TextButton( + onPressed: () { + context.read().add( + const AuthEventShouldRegister(), + ); + }, + child: const Text('Not registered yet? Register here!'), + ) + ], + ), ), ), ); diff --git a/lib/views/register_view.dart b/lib/views/register_view.dart index 0e552a7..4058af0 100644 --- a/lib/views/register_view.dart +++ b/lib/views/register_view.dart @@ -53,48 +53,60 @@ class _RegisterViewState extends State { appBar: AppBar( title: const Text('Register'), ), - body: Column( - children: [ - TextField( - controller: _email, - enableSuggestions: false, - autocorrect: false, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - hintText: 'Enter your email here', + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Enter your email and password to see your notes!'), + TextField( + controller: _email, + enableSuggestions: false, + autocorrect: false, + autofocus: true, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + hintText: 'Enter your email here', + ), ), - ), - TextField( - controller: _password, - obscureText: true, - enableSuggestions: false, - autocorrect: false, - decoration: const InputDecoration( - hintText: 'Enter your password here', + TextField( + controller: _password, + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: 'Enter your password here', + ), ), - ), - TextButton( - onPressed: () async { - final email = _email.text; - final password = _password.text; - context.read().add( - AuthEventRegister( - email, - password, - ), - ); - }, - child: const Text('Register'), - ), - TextButton( - onPressed: () { - context.read().add( - const AuthEventLogOut(), - ); - }, - child: const Text('Already registered? Login here!'), - ) - ], + Center( + child: Column( + children: [ + TextButton( + onPressed: () async { + final email = _email.text; + final password = _password.text; + context.read().add( + AuthEventRegister( + email, + password, + ), + ); + }, + child: const Text('Register'), + ), + TextButton( + onPressed: () { + context.read().add( + const AuthEventLogOut(), + ); + }, + child: const Text('Already registered? Login here!'), + ), + ], + ), + ), + ], + ), ), ), ); diff --git a/test/auth_test.dart b/test/auth_test.dart index 11d1cc7..f9433b9 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -144,4 +144,9 @@ class MockAuthProvider implements AuthProvider { ); _user = newUser; } + + @override + Future sendPasswordReset({required String toEmail}) { + throw UnimplementedError(); + } }