diff --git a/lib/constants/routes.dart b/lib/constants/routes.dart index 8109976..ae6e995 100644 --- a/lib/constants/routes.dart +++ b/lib/constants/routes.dart @@ -1,5 +1 @@ -const loginRoute = '/login/'; -const registerRoute = '/register/'; -const notesRoute = '/notes/'; -const verifyEmailRoute = '/verify-email/'; const createOrUpdateNoteRoute = '/notes/new-note/'; diff --git a/lib/main.dart b/lib/main.dart index 4bb19ff..bc37217 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,10 +24,6 @@ void main() { child: const HomePage(), ), routes: { - loginRoute: (context) => const LoginView(), - registerRoute: (context) => const RegisterView(), - notesRoute: (context) => const NotesView(), - verifyEmailRoute: (context) => const VerifyEmailView(), createOrUpdateNoteRoute: (context) => const CreateUpdateNoteView(), }, ), @@ -48,6 +44,8 @@ class HomePage extends StatelessWidget { return const VerifyEmailView(); } else if (state is AuthStateLoggedOut) { return const LoginView(); + } else if (state is AuthStateRegistering) { + return const RegisterView(); } else { return const Scaffold( body: CircularProgressIndicator(), diff --git a/lib/services/auth/bloc/auth_bloc.dart b/lib/services/auth/bloc/auth_bloc.dart index 70e8dd8..1901887 100644 --- a/lib/services/auth/bloc/auth_bloc.dart +++ b/lib/services/auth/bloc/auth_bloc.dart @@ -4,13 +4,37 @@ import 'package:mynotes/services/auth/bloc/auth_event.dart'; import 'package:mynotes/services/auth/bloc/auth_state.dart'; class AuthBloc extends Bloc { - AuthBloc(AuthProvider provider) : super(const AuthStateLoading()) { + AuthBloc(AuthProvider provider) : super(const AuthStateUninitialized()) { + // send email verification + on((event, emit) async { + await provider.sendEmailVerification(); + emit(state); + }); + on((event, emit) async { + final email = event.email; + final password = event.password; + try { + await provider.createUser( + email: email, + password: password, + ); + await provider.sendEmailVerification(); + emit(const AuthStateNeedsVerification()); + } on Exception catch (e) { + emit(AuthStateRegistering(e)); + } + }); // initialize on((event, emit) async { await provider.initialize(); final user = provider.currentUser; if (user == null) { - emit(const AuthStateLoggedOut(null)); + emit( + const AuthStateLoggedOut( + exception: null, + isLoading: false, + ), + ); } else if (!user.isEmailVerified) { emit(const AuthStateNeedsVerification()); } else { @@ -19,6 +43,12 @@ class AuthBloc extends Bloc { }); // log in on((event, emit) async { + emit( + const AuthStateLoggedOut( + exception: null, + isLoading: true, + ), + ); final email = event.email; final password = event.password; try { @@ -26,19 +56,50 @@ class AuthBloc extends Bloc { email: email, password: password, ); - emit(AuthStateLoggedIn(user)); + + if (!user.isEmailVerified) { + emit( + const AuthStateLoggedOut( + exception: null, + isLoading: false, + ), + ); + emit(const AuthStateNeedsVerification()); + } else { + emit( + const AuthStateLoggedOut( + exception: null, + isLoading: false, + ), + ); + emit(AuthStateLoggedIn(user)); + } } on Exception catch (e) { - emit(AuthStateLoggedOut(e)); + emit( + AuthStateLoggedOut( + exception: e, + isLoading: false, + ), + ); } }); // log out on((event, emit) async { try { - emit(const AuthStateLoading()); await provider.logOut(); - emit(const AuthStateLoggedOut(null)); + emit( + const AuthStateLoggedOut( + exception: null, + isLoading: false, + ), + ); } on Exception catch (e) { - emit(AuthStateLogoutFailure(e)); + emit( + AuthStateLoggedOut( + exception: e, + isLoading: false, + ), + ); } }); } diff --git a/lib/services/auth/bloc/auth_event.dart b/lib/services/auth/bloc/auth_event.dart index 121e1e2..72c14d7 100644 --- a/lib/services/auth/bloc/auth_event.dart +++ b/lib/services/auth/bloc/auth_event.dart @@ -9,12 +9,26 @@ class AuthEventInitialize extends AuthEvent { const AuthEventInitialize(); } +class AuthEventSendEmailVerification extends AuthEvent { + const AuthEventSendEmailVerification(); +} + class AuthEventLogIn extends AuthEvent { final String email; final String password; const AuthEventLogIn(this.email, this.password); } +class AuthEventRegister extends AuthEvent { + final String email; + final String password; + const AuthEventRegister(this.email, this.password); +} + +class AuthEventShouldRegister extends AuthEvent { + const AuthEventShouldRegister(); +} + 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 a372e39..a73cbf2 100644 --- a/lib/services/auth/bloc/auth_state.dart +++ b/lib/services/auth/bloc/auth_state.dart @@ -1,13 +1,19 @@ import 'package:flutter/foundation.dart' show immutable; import 'package:mynotes/services/auth/auth_user.dart'; +import 'package:equatable/equatable.dart'; @immutable abstract class AuthState { const AuthState(); } -class AuthStateLoading extends AuthState { - const AuthStateLoading(); +class AuthStateUninitialized extends AuthState { + const AuthStateUninitialized(); +} + +class AuthStateRegistering extends AuthState { + final Exception? exception; + const AuthStateRegistering(this.exception); } class AuthStateLoggedIn extends AuthState { @@ -19,12 +25,14 @@ class AuthStateNeedsVerification extends AuthState { const AuthStateNeedsVerification(); } -class AuthStateLoggedOut extends AuthState { +class AuthStateLoggedOut extends AuthState with EquatableMixin { final Exception? exception; - const AuthStateLoggedOut(this.exception); -} + final bool isLoading; + const AuthStateLoggedOut({ + required this.exception, + required this.isLoading, + }); -class AuthStateLogoutFailure extends AuthState { - final Exception exception; - const AuthStateLogoutFailure(this.exception); + @override + List get props => [exception, isLoading]; } diff --git a/lib/utilities/dialogs/loading_dialog.dart b/lib/utilities/dialogs/loading_dialog.dart new file mode 100644 index 0000000..47351a2 --- /dev/null +++ b/lib/utilities/dialogs/loading_dialog.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +typedef CloseDialog = void Function(); + +CloseDialog showLoadingDialog({ + required BuildContext context, + required String text, +}) { + final dialog = AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 10.0), + Text(text), + ], + ), + ); + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => dialog, + ); + + return () => Navigator.of(context).pop(); +} diff --git a/lib/views/login_view.dart b/lib/views/login_view.dart index aa677da..35115b1 100644 --- a/lib/views/login_view.dart +++ b/lib/views/login_view.dart @@ -6,6 +6,7 @@ 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:flutter_bloc/flutter_bloc.dart'; +import 'package:mynotes/utilities/dialogs/loading_dialog.dart'; class LoginView extends StatefulWidget { const LoginView({Key? key}) : super(key: key); @@ -17,6 +18,7 @@ class LoginView extends StatefulWidget { class _LoginViewState extends State { late final TextEditingController _email; late final TextEditingController _password; + CloseDialog? _closeDialogHandle; @override void initState() { @@ -34,43 +36,54 @@ class _LoginViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - 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', + return BlocListener( + listener: (context, state) async { + if (state is AuthStateLoggedOut) { + final closeDialog = _closeDialogHandle; + + if (!state.isLoading && closeDialog != null) { + closeDialog(); + _closeDialogHandle = null; + } else if (state.isLoading && closeDialog == null) { + _closeDialogHandle = showLoadingDialog( + context: context, + text: 'Loading...', + ); + } + if (state.exception is UserNotFoundAuthException) { + await showErrorDialog(context, 'User not found'); + } else if (state.exception is WrongPasswordAuthException) { + await showErrorDialog(context, 'Wrong credentials'); + } else if (state.exception is GenericAuthException) { + await showErrorDialog(context, 'Authentication error'); + } + } + }, + child: Scaffold( + 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', + ), ), - ), - 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', + ), ), - ), - BlocListener( - listener: (context, state) async { - if (state is AuthStateLoggedOut) { - if (state.exception is UserNotFoundAuthException) { - await showErrorDialog(context, 'User not found'); - } else if (state.exception is WrongPasswordAuthException) { - await showErrorDialog(context, 'Wrong credentials'); - } else if (state.exception is GenericAuthException) { - await showErrorDialog(context, 'Authentication error'); - } - } - }, - child: TextButton( + TextButton( onPressed: () async { final email = _email.text; final password = _password.text; @@ -83,17 +96,16 @@ class _LoginViewState extends State { }, child: const Text('Login'), ), - ), - TextButton( - onPressed: () { - Navigator.of(context).pushNamedAndRemoveUntil( - registerRoute, - (route) => false, - ); - }, - child: const Text('Not registered yet? Register here!'), - ) - ], + 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 dd2a549..0e552a7 100644 --- a/lib/views/register_view.dart +++ b/lib/views/register_view.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mynotes/constants/routes.dart'; import 'package:mynotes/services/auth/auth_exceptions.dart'; import 'package:mynotes/services/auth/auth_service.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'; class RegisterView extends StatefulWidget { @@ -31,75 +35,67 @@ class _RegisterViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - 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', + return BlocListener( + listener: (context, state) async { + if (state is AuthStateRegistering) { + if (state.exception is WeakPasswordAuthException) { + await showErrorDialog(context, 'Weak password'); + } else if (state.exception is EmailAlreadyInUseAuthException) { + await showErrorDialog(context, 'Email is already in use'); + } else if (state.exception is GenericAuthException) { + await showErrorDialog(context, 'Failed to register'); + } else if (state.exception is InvalidEmailAuthException) { + await showErrorDialog(context, 'Invalid email'); + } + } + }, + child: Scaffold( + 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', + ), ), - ), - 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; - try { - await AuthService.firebase().createUser( - email: email, - password: password, - ); - AuthService.firebase().sendEmailVerification(); - Navigator.of(context).pushNamed(verifyEmailRoute); - } on WeakPasswordAuthException { - await showErrorDialog( - context, - 'Weak password', - ); - } on EmailAlreadyInUseAuthException { - await showErrorDialog( - context, - 'Email is already in use', - ); - } on InvalidEmailAuthException { - await showErrorDialog( - context, - 'This is an invalid email address', - ); - } on GenericAuthException { - await showErrorDialog( - context, - 'Failed to register', - ); - } - }, - child: const Text('Register'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pushNamedAndRemoveUntil( - loginRoute, - (route) => false, - ); - }, - child: const Text('Already registered? Login 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!'), + ) + ], + ), ), ); } diff --git a/lib/views/verify_email_view.dart b/lib/views/verify_email_view.dart index e5b7a95..d1f546a 100644 --- a/lib/views/verify_email_view.dart +++ b/lib/views/verify_email_view.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:mynotes/constants/routes.dart'; import 'package:mynotes/services/auth/auth_service.dart'; +import 'package:mynotes/services/auth/bloc/auth_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mynotes/services/auth/bloc/auth_event.dart'; class VerifyEmailView extends StatefulWidget { const VerifyEmailView({Key? key}) : super(key: key); @@ -23,18 +26,18 @@ class _VerifyEmailViewState extends State { const Text( "If you haven't received a verification email yet, press the button below"), TextButton( - onPressed: () async { - await AuthService.firebase().sendEmailVerification(); + onPressed: () { + context.read().add( + const AuthEventSendEmailVerification(), + ); }, child: const Text('Send email verification'), ), TextButton( onPressed: () async { - await AuthService.firebase().logOut(); - Navigator.of(context).pushNamedAndRemoveUntil( - registerRoute, - (route) => false, - ); + context.read().add( + const AuthEventLogOut(), + ); }, child: const Text('Restart'), ) diff --git a/pubspec.lock b/pubspec.lock index a34ca80..06baadc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -127,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index dbd7e94..c9e6d83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: share_plus: ^3.0.4 bloc: ^8.0.2 flutter_bloc: ^8.0.1 + equatable: ^2.0.3 dev_dependencies: flutter_test: