This commit is contained in:
Vandad Nahavandipoor 2022-01-09 08:23:10 +01:00
parent 1e6da9da71
commit dada50bb3c
11 changed files with 266 additions and 143 deletions

View File

@ -1,5 +1 @@
const loginRoute = '/login/';
const registerRoute = '/register/';
const notesRoute = '/notes/';
const verifyEmailRoute = '/verify-email/';
const createOrUpdateNoteRoute = '/notes/new-note/';

View File

@ -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(),

View File

@ -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<AuthEvent, AuthState> {
AuthBloc(AuthProvider provider) : super(const AuthStateLoading()) {
AuthBloc(AuthProvider provider) : super(const AuthStateUninitialized()) {
// send email verification
on<AuthEventSendEmailVerification>((event, emit) async {
await provider.sendEmailVerification();
emit(state);
});
on<AuthEventRegister>((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<AuthEventInitialize>((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<AuthEvent, AuthState> {
});
// log in
on<AuthEventLogIn>((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<AuthEvent, AuthState> {
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<AuthEventLogOut>((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,
),
);
}
});
}

View File

@ -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();
}

View File

@ -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<Object?> get props => [exception, isLoading];
}

View File

@ -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();
}

View File

@ -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<LoginView> {
late final TextEditingController _email;
late final TextEditingController _password;
CloseDialog? _closeDialogHandle;
@override
void initState() {
@ -34,43 +36,54 @@ class _LoginViewState extends State<LoginView> {
@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<AuthBloc, AuthState>(
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<AuthBloc, AuthState>(
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<LoginView> {
},
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<AuthBloc>().add(
const AuthEventShouldRegister(),
);
},
child: const Text('Not registered yet? Register here!'),
)
],
),
),
);
}

View File

@ -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<RegisterView> {
@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<AuthBloc, AuthState>(
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<AuthBloc>().add(
AuthEventRegister(
email,
password,
),
);
},
child: const Text('Register'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthEventLogOut(),
);
},
child: const Text('Already registered? Login here!'),
)
],
),
),
);
}

View File

@ -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<VerifyEmailView> {
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<AuthBloc>().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<AuthBloc>().add(
const AuthEventLogOut(),
);
},
child: const Text('Restart'),
)

View File

@ -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:

View File

@ -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: