This commit is contained in:
Vandad Nahavandipoor 2022-01-11 07:59:53 +01:00
parent cd3aaa46af
commit b27fe5bbc8
13 changed files with 329 additions and 81 deletions

View File

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

View File

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

View File

@ -13,4 +13,5 @@ abstract class AuthProvider {
});
Future<void> logOut();
Future<void> sendEmailVerification();
Future<void> sendPasswordReset({required String toEmail});
}

View File

@ -39,4 +39,8 @@ class AuthService implements AuthProvider {
@override
Future<void> initialize() => provider.initialize();
@override
Future<void> sendPasswordReset({required String toEmail}) =>
provider.sendPasswordReset(toEmail: toEmail);
}

View File

@ -6,6 +6,48 @@ import 'package:mynotes/services/auth/bloc/auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(AuthProvider provider)
: super(const AuthStateUninitialized(isLoading: true)) {
on<AuthEventShouldRegister>((event, emit) {
emit(const AuthStateRegistering(
exception: null,
isLoading: false,
));
});
//forgot password
on<AuthEventForgotPassword>((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<AuthEventSendEmailVerification>((event, emit) async {
await provider.sendEmailVerification();

View File

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

View File

@ -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({

View File

@ -104,4 +104,22 @@ class FirebaseAuthProvider implements AuthProvider {
throw UserNotLoggedInAuthException();
}
}
@override
Future<void> 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();
}
}
}

View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:mynotes/utilities/dialogs/generic_dialog.dart';
Future<void> showPasswordResetSentDialog(BuildContext context) {
return showGenericDialog<void>(
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,
},
);
}

View File

@ -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<ForgotPasswordView> {
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<AuthBloc, AuthState>(
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<AuthBloc>()
.add(AuthEventForgotPassword(email: email));
},
child: const Text('Send me password reset link'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthEventLogOut(),
);
},
child: const Text('Back to login page'),
),
],
),
),
),
);
}
}

View File

@ -39,7 +39,10 @@ class _LoginViewState extends State<LoginView> {
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<LoginView> {
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<AuthBloc>().add(
AuthEventLogIn(
email,
password,
),
);
},
child: const Text('Login'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthEventShouldRegister(),
);
},
child: const Text('Not registered yet? Register here!'),
)
],
TextButton(
onPressed: () async {
final email = _email.text;
final password = _password.text;
context.read<AuthBloc>().add(
AuthEventLogIn(
email,
password,
),
);
},
child: const Text('Login'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthEventForgotPassword(),
);
},
child: const Text('I forgot my password'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthEventShouldRegister(),
);
},
child: const Text('Not registered yet? Register here!'),
)
],
),
),
),
);

View File

@ -53,48 +53,60 @@ class _RegisterViewState extends State<RegisterView> {
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<AuthBloc>().add(
AuthEventRegister(
email,
password,
),
);
},
child: const Text('Register'),
),
TextButton(
onPressed: () {
context.read<AuthBloc>().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<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

@ -144,4 +144,9 @@ class MockAuthProvider implements AuthProvider {
);
_user = newUser;
}
@override
Future<void> sendPasswordReset({required String toEmail}) {
throw UnimplementedError();
}
}