As we entered a new year, I’ve been reflecting on the wins, losses, and lessons learned from the previous. One of the most important lessons I’ve embraced is the need to prioritize mental health. I’ve learned that meditation and journaling, in particular, can be a powerful way to process emotions and improve mental well-being, especially for those navigating stress, depression, or anxiety.
For my next project, I’ve decided to build a Mental Health Journal app with Sentiment Analysis and document the journey through a blog series.
Journaling is an intimate activity, and users need to feel assured that their thoughts and emotions are secure. That’s why implementing strong authentication and user management systems is a critical first step.
This week I focused on creating a seamless login and signup experience using Firebase Authentication. Whether you’re a developer looking to implement secure user accounts or simply curious about the process, this post will walk you through building authentication that’s both robust and user-friendly. Let’s lay the groundwork for an app that empowers users to journal with confidence and peace of mind!
Why Authentication Matters
When building an app that deals with sensitive information like a mental health journal, having a robust authentication system isn’t just a technical feature, it’s a promise to the user; It ensures users can securely access their data while maintaining privacy and preventing unauthorized access.
To achieve this, I implemented email and password based authentication using Firebase Authentication. Firebase provides a secure, easy to integrate solution that handles user management, token generation, and session handling. Firebase also ensures compliance with security best practices like password hashing and token expiration, making it easy for developers while maintaining strong security for end user
The Development Process
The authentication system for this app was implemented in several phases
Setting up Firebase
To get started, after creating a new Flutter project in android studio, I:
- Created a new Firebase project and enabled email/password authentication
- Added the app’s google-services.json or GoogleService-Info.plist for platform-specific configuration in the Flutter project
![](https://tsounguicodes.com/wp-content/uploads/2025/01/image-1024x487.png)
![](https://tsounguicodes.com/wp-content/uploads/2025/01/image-1.png)
Dependencies
I used to the following dependencies
dependencies:
cloud_firestore: ^5.6.0 # For Firebase Core, enabling connecting to multiple Firebase apps
cupertino_icons: ^1.0.6
dartz: ^0.10.1
equatable: ^2.0.5
firebase_auth: ^5.3.4 # For Firebase Authentication
firebase_core: ^3.9.0
firebase_storage: 12.3.7
flutter:
sdk: flutter
flutter_bloc: ^8.0.0
get_it: ^7.2.0
provider: ^6.1.2
dev_dependencies:
bloc_test: ^9.1.7
fake_cloud_firestore: ^3.1.0 # For mocking cloud firestore
firebase_storage_mocks: ^0.7.0 # For mocking firebase storage
flutter_test:
sdk: flutter
mocktail: ^1.0.0
very_good_analysis: ^7.0.0 # For lint rules for Dart and Flutter used at Very Good Ventures
- firebase_core was used to initialize Firebase in the Flutter app.
- firebase_auth was configured to handle user registration, login, and session management.
- cloud_firestore package was added to prepare for storing additional user-related data in a future phase.
Implementing Authentication Logic
Using the principle of Test Driven Development and clean architecture, I implemented the authentication logic
Domain Layer
Starting with the domain layer, I created the UserEntity
import 'package:equatable/equatable.dart';
class UserEntity extends Equatable {
const UserEntity({
required this.uid,
required this.name,
required this.email,
required this.dateCreated,
required this.isVerified,
this.profilePictureUrl,
});
UserEntity.empty()
: this(
uid: '',
name: '',
email: '',
dateCreated: DateTime.now(),
profilePictureUrl: '',
isVerified: false,
);
final String uid;
final String name;
final String email;
final DateTime dateCreated;
final String? profilePictureUrl;
final bool isVerified;
@override
List<Object?> get props => [
uid,
name,
email,
dateCreated,
profilePictureUrl,
isVerified,
];
@override
String toString() => '''
UserEntity {
uid: $uid,
name: $name,
email: $email,
dateCreated: $dateCreated,
profilePictureUrl: $profilePictureUrl,
isVerified: $isVerified,
}
''';
}
In the repository contract I defined the methods needed for the authentication logic, which I implement in the data layer
import 'package:mental_health_journal_app/core/enums/update_user_action.dart';
import 'package:mental_health_journal_app/core/utils/typedefs.dart';
import 'package:mental_health_journal_app/features/auth/domain/entities/user.dart';
abstract class AuthRepository {
const AuthRepository();
ResultFuture<UserEntity> createUserAccount({
required String name,
required String email,
required String password,
});
ResultFuture<UserEntity> signIn({
required String email,
required String password,
});
ResultVoid signOut();
ResultVoid forgotPassword({
required String email,
});
ResultVoid updateUser({
required UpdateUserAction action,
required dynamic userData,
});
ResultVoid deleteAccount({
required String password,
});
}
You may have noticed that the methods return ResultFuture or ResultVoid. I created these types in a separate file to not be repetitive.
import 'package:dartz/dartz.dart';
import 'package:mental_health_journal_app/core/errors/failures.dart';
typedef ResultFuture<T> = Future<Either<Failure, T>>;
typedef ResultVoid = ResultFuture<void>;
typedef DataMap = Map<String, dynamic>;
These Either types ensure that error handling is baked into the architecture
Data Layer
I created an AuthRemoteDataSource that directly interacts with Firebase Authentication.
abstract class AuthRemoteDataSource {
Future<UserModel> createUserAccount({
required String name,
required String email,
required String password,
});
Future<UserModel> signIn({
required String email,
required String password,
});
Future<void> signOut();
Future<void> forgotPassword({
required String email,
});
Future<void> updateUser({
required UpdateUserAction action,
required dynamic userData,
});
Future<void> deleteAccount({
required String password,
});
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
const AuthRemoteDataSourceImpl({
required FirebaseAuth authClient,
required FirebaseFirestore firestoreClient,
required FirebaseStorage storageClient,
}) : _authClient = authClient,
_firestoreClient = firestoreClient,
_storageClient = storageClient;
final FirebaseAuth _authClient;
final FirebaseFirestore _firestoreClient;
final FirebaseStorage _storageClient;
@override
Future<UserModel> createUserAccount({
required String name,
required String email,
required String password,
}) async {
try {
final userCredential = await _authClient.createUserWithEmailAndPassword(
email: email,
password: password,
);
await userCredential.user?.updateDisplayName(name);
await _setUserData(_authClient.currentUser, email);
final user = UserModel(
uid: userCredential.user?.uid ?? '',
name: userCredential.user?.displayName ?? '',
email: userCredential.user?.email ?? '',
dateCreated: DateTime.fromMicrosecondsSinceEpoch(
timestamp.microsecondsSinceEpoch,
),
profilePictureUrl: userCredential.user?.photoURL,
isVerified: false,
);
return user;
} on FirebaseAuthException catch (e) {
throw CreateUserAccountException(
message: e.message ?? 'Error Occurred',
statusCode: e.code,
);
} on CreateUserAccountException {
rethrow;
} catch (e, s) {
debugPrintStack(stackTrace: s);
throw CreateUserAccountException(
message: e.toString(),
statusCode: '400',
);
}
}
@override
Future<UserModel> signIn({
required String email,
required String password,
}) async {
try {
final result = await _authClient.signInWithEmailAndPassword(
email: email,
password: password,
);
final user = result.user;
// get user data from firestore with user uid
var userData = await _getUserData(user!.uid);
var userModel = UserModel.empty();
if (!userData.exists) {
// if user doesn't have data in firestore
// upload data to firestore
await _setUserData(user, email);
// get user data from firestore with user uid
userData = await _getUserData(user.uid);
userModel = UserModel.fromMap(userData.data()!);
} else {
userModel = UserModel.fromMap(userData.data()!);
}
return userModel;
} on FirebaseAuthException catch (e) {
var errorMessage = e.message ?? 'An error occurred. Please try again';
throw SignInException(
message: errorMessage,
statusCode: e.code,
);
} on SignInException {
rethrow;
} catch (e, s) {
debugPrintStack(stackTrace: s);
throw SignInException(
message: e.toString(),
statusCode: '400',
);
}
}
@override
Future<void> updateUser({
required UpdateUserAction action,
required dynamic userData,
}) async {
try {
switch (action) {
case UpdateUserAction.name:
await _authClient.currentUser?.updateDisplayName(
userData as String,
);
await _updateUserData({'name': userData});
case UpdateUserAction.email:
await _authClient.currentUser?.verifyBeforeUpdateEmail(
userData as String,
);
await _updateUserData({'email': userData});
case UpdateUserAction.password:
// this case is when user is already logged in
// and is trying to change password in user settings
final newData = jsonDecode(userData as String) as DataMap;
if (_authClient.currentUser?.email == null) {
throw const UpdateUserException(
message: 'User does not exist',
statusCode: 'Insufficient Permission',
);
}
await _authClient.currentUser?.reauthenticateWithCredential(
EmailAuthProvider.credential(
email: _authClient.currentUser!.email!,
password: newData['oldPassword'] as String,
),
);
await _authClient.currentUser?.updatePassword(
newData['newPassword'] as String,
);
case UpdateUserAction.profilePictureUrl:
// Save new picture in firebase storage
final ref = _storageClient.ref().child('profile_pics/${_authClient.currentUser?.uid}');
await ref.putFile(userData as File);
// Save get url from firebase storage
final url = await ref.getDownloadURL();
// Update it in firebase auth
await _authClient.currentUser?.updatePhotoURL(url);
// Update document url in firestore
await _updateUserData({'photoUrl': url});
}
} on FirebaseException catch (e) {
throw UpdateUserException(
message: e.message ?? 'Error Occurred',
statusCode: e.code,
);
} catch (e, s) {
debugPrintStack(stackTrace: s);
throw UpdateUserException(
message: e.toString(),
statusCode: '400',
);
}
}
@override
Future<void> deleteAccount({required String password}) async {
...
}
@override
Future<void> forgotPassword({required String email}) async {
...
}
@override
Future<void> signOut() async {
try {
await _authClient.signOut();
await _authClient.currentUser!.reload();
} on FirebaseException catch (e) {
throw SignOutException(
message: e.message ?? 'Error Occurred',
statusCode: e.code,
);
} catch (e, s) {
debugPrintStack(stackTrace: s);
throw SignOutException(
message: e.toString(),
statusCode: '400',
);
}
}
Future<void> _setUserData(
User? user,
String fallbackEmail,
) async {
await _users.doc(user?.uid).set(
UserModel(
uid: user?.uid ?? '',
name: user?.displayName ?? '',
email: user?.email ?? fallbackEmail,
dateCreated: DateTime.now(),
profilePictureUrl: user?.photoURL,
isVerified: false,
).toMap(),
);
}
Future<void> _updateUserData(DataMap data) async {
await _users
.doc(
_authClient.currentUser?.uid,
)
.update(data);
}
CollectionReference<DataMap> get _users => _firestoreClient.collection(
'users',
);
Future<DocumentSnapshot<DataMap>> _getUserData(String uid) async {
return _users.doc(uid).get();
}
}
- createUserAccount: Handles user registration using createUserWithEmailAndPassword.
- signIn: Authenticates users with their email and password using signInWithEmailAndPassword.
- forgotPassword: Sends password reset emails via sendPasswordResetEmail.
- deleteAccount: Removes user accounts after re-authentication using reauthenticateWithCredential.
- signOut: Signs user out
- updateUser: Updates user data
Presentation Layer
To separate UI concerns from the authentication logic, and to manage state I used flutter_bloc.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc({
required CreateUserAccount createUserAccount,
required SignIn signIn,
required SignOut signOut,
required ForgotPassword forgotPassword,
required DeleteAccount deleteAccount,
required UpdateUser updateUser,
}) : _createUserAccount = createUserAccount,
_deleteAccount = deleteAccount,
_forgotPassword = forgotPassword,
_signIn = signIn,
_signOut = signOut,
_updateUser = updateUser,
super(const AuthInitial()) {
on<SignInEvent>(_signInHandler);
on<SignUpEvent>(_signUpHandler);
on<ForgotPasswordEvent>(_forgotPasswordHandler);
on<DeleteAccountEvent>(_deleteAccountHandler);
on<UpdateUserEvent>(_updateUserHandler);
on<SignOutEvent>(_signOutHandler);
}
final SignIn _signIn;
final SignOut _signOut;
final CreateUserAccount _createUserAccount;
final ForgotPassword _forgotPassword;
final DeleteAccount _deleteAccount;
final UpdateUser _updateUser;
Future<void> _signInHandler(
SignInEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _signIn(
SignInParams(email: event.email, password: event.password),
);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(user) => emit(SignedIn(user: user)),
);
}
Future<void> _signOutHandler(
SignOutEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _signOut();
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(result) => emit(const SignedOut()),
);
}
Future<void> _signUpHandler(
SignUpEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _createUserAccount(
CreateUserAccountParams(
name: event.name,
email: event.email,
password: event.password,
),
);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(user) => emit(SignedUp(user: user)),
);
}
Future<void> _forgotPasswordHandler(
ForgotPasswordEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _forgotPassword(event.email);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(success) => emit(const ForgotPasswordSent()),
);
}
Future<void> _deleteAccountHandler(
DeleteAccountEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _deleteAccount(event.password);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(success) => emit(const AccountDeleted()),
);
}
Future<void> _updateUserHandler(
UpdateUserEvent event,
Emitter<AuthState> emit,
) async {
emit(const AuthLoading());
final result = await _updateUser(
UpdateUserParams(
action: event.action,
userData: event.userData,
),
);
result.fold(
(failure) => emit(AuthError(message: failure.message)),
(success) => emit(const UserUpdated()),
);
}
}
I also used provider for exposing things across the entire widget tree, and for minor state management
import 'package:flutter/material.dart';
import 'package:mental_health_journal_app/core/common/app/providers/user_provider.dart';
import 'package:mental_health_journal_app/features/auth/domain/entities/user.dart';
import 'package:provider/provider.dart';
extension ContextExtension on BuildContext {
ThemeData get theme => Theme.of(this);
MediaQueryData get mediaQuery => MediaQuery.of(this);
Size get size => mediaQuery.size;
double get width => size.width;
double get height => size.height;
UserProvider get userProvider => read<UserProvider>();
UserEntity? get currentUser => userProvider.user;
}
import 'package:flutter/material.dart';
import 'package:mental_health_journal_app/features/auth/data/models/user_model.dart';
class UserProvider extends ChangeNotifier {
UserModel? _user;
UserModel? get user => _user;
set user(UserModel? user) {
if (_user != user) {
_user = user;
Future.delayed(Duration.zero, notifyListeners);
}
}
void initUser(UserModel? user) {
if (_user != user) _user = user;
}
}
Building the User Interface
When building the user interface for the Sign-In, Sign-Up, Forgot-Password screens, I focused on creating an intuitive and responsive design, maintaining a consistent style with the rest of the app, and keeping clean architecture and TDD principles in mind.
The Sign-In Screen
![](https://tsounguicodes.com/wp-content/uploads/2025/01/Screenshot_20250108-182733-461x1024.png)
The Sign-In Screen is where users log in to access their journal entries. Here’s how it’s structured:
- Components:
- SignInForm widget: Handles the input fields (email and password) and their validation.
- LongButton: A reusable button widget styled consistently across the app.
- Navigation Links: Direct users to the Sign-Up and Forgot Password screens.
- Functionality:
- Validates user input using a GlobalKey before submitting the credentials.
- Handles authentication state changes using the AuthBloc with BlocConsumer, ensuring seamless navigation to the dashboard after successful login or displaying an error message otherwise.
...
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colours.backgroundColor,
body: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
CoreUtils.showSnackBar(context, state.message);
} else if (state is SignedIn) {
context.userProvider.initUser(state.user as UserModel);
Navigator.pushNamed(context, Dashboard.id);
}
},
builder: (context, state) {
return SafeArea(
child: Center(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
LogoWidget(
size: 75,
color: context.theme.primaryColor,
),
const SizedBox(height: 75),
Text(
Strings.loginText,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 32,
color: context.theme.primaryColor,
),
),
const SizedBox(height: 10),
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
Strings.loginSubtext,
style: TextStyle(
fontSize: 14,
),
),
],
),
const SizedBox(height: 20),
SignInForm(
emailController: emailController,
passwordController: passwordController,
onPasswordFieldSubmitted: (_) => signIn(context),
formKey: formKey,
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.pushNamed(
context,
ForgotPasswordScreen.id,
);
},
child: Text(
Strings.forgotPasswordTextButtonText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: context.theme.primaryColor,
),
),
),
),
const SizedBox(
height: 50,
),
if (state is AuthLoading)
const Center(
child: CircularProgressIndicator(),
)
else
LongButton(
onPressed: () => signIn(context),
label: Strings.loginButtonText,
),
const SizedBox(height: 30),
TextButton(
onPressed: () {
Navigator.pushReplacementNamed(
context,
SignUpScreen.id,
);
},
child: RichText(
text: TextSpan(
text: Strings.dontHaveAccountText,
style: TextStyle(
fontSize: 14,
color: context.theme.textTheme.bodyMedium?.color,
),
children: [
TextSpan(
text: Strings.registerTextButtonText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: context.theme.primaryColor,
decoration: TextDecoration.underline,
decorationColor: context.theme.primaryColor,
decorationThickness: 2,
),
),
],
),
),
),
],
),
),
);
},
),
);
}
The Sign-Up Screen
![](https://tsounguicodes.com/wp-content/uploads/2025/01/Screenshot_20250108-182824-461x1024.png)
The Sign-Up Screen allows new users to register an account. Its layout and functionality are similar to the Sign-In screen but include extra fields for name and password confirmation.
- Components:
- SignUpForm widget: Includes fields for the user’s name, email, password, and password confirmation, with validation for each field.
- LongButton: Reused here for the Sign-Up action.
- Navigation Link: Directs users to the Sign-In screen if they already have an account.
- Functionality:
- On successful registration, the app automatically logs in the user and navigates them to the dashboard.
- Password confirmation ensures users input their intended password accurately.
...
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colours.backgroundColor,
body: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
CoreUtils.showSnackBar(context, state.message);
} else if (state is SignedUp) {
context.read<AuthBloc>().add(
SignInEvent(
email: emailController.text.trim(),
password: passwordController.text.trim(),
),
);
} else if (state is SignedIn) {
context.userProvider.initUser(state.user as UserModel);
Navigator.pushReplacementNamed(context, '/');
}
},
builder: (context, state) {
return SafeArea(
child: Center(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 35,
).copyWith(top: 15),
children: [
const SizedBox(height: 35),
LogoWidget(
size: 75,
color: context.theme.primaryColor,
),
const SizedBox(height: 50),
Text(
Strings.signUpText,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 32,
color: context.theme.primaryColor,
),
),
const SizedBox(height: 10),
Text(
Strings.signUpSubtext,
style: TextStyle(
fontSize: 14,
color: context.theme.textTheme.bodyMedium?.color,
fontWeight: FontWeight.normal,
),
),
const SizedBox(height: 20),
SignUpForm(
formKey: formKey,
nameController: nameController,
emailController: emailController,
passwordController: passwordController,
confirmPasswordController: confirmPasswordController,
onConfirmPasswordFieldSubmitted: (_) => signUp(context),
),
const SizedBox(height: 50),
if (state is AuthLoading)
const Center(
child: CircularProgressIndicator(),
)
else
LongButton(
onPressed: () => signUp(context),
label: Strings.signUpButtonText,
),
const SizedBox(height: 30),
TextButton(
onPressed: () {
Navigator.pushReplacementNamed(
context,
SignInScreen.id,
);
},
child: RichText(
text: TextSpan(
text: Strings.alreadyHaveAccountText,
style: TextStyle(
fontSize: 14,
color: context.theme.textTheme.bodyMedium?.color,
),
children: [
TextSpan(
text: Strings.loginTextButtonText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: context.theme.primaryColor,
decoration: TextDecoration.underline,
),
),
],
),
),
),
],
),
),
);
},
),
);
}
Forgot Password Screen
![](https://tsounguicodes.com/wp-content/uploads/2025/01/Screenshot_20250108-182855-461x1024.png)
The Forgot Password Screen helps users reset their password via an email link.
- Components:
- IField: Reusable widget that returns a TextFormField, which I used in SignInForm and SignUpForm
- LongButton: Triggers the password reset action.
- Status Indicator: Displays whether the reset email was successfully sent or not.
- Functionality:
- After submitting the email, the app uses AuthBloc to send a password reset link.
- Users are navigated back to the Sign-In screen after a successful request.
...
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colours.backgroundColor,
body: BlocConsumer<AuthBloc, AuthState>(
listener: (_, state) {
if (state is AuthError) {
CoreUtils.showSnackBar(context, state.message);
} else if (state is ForgotPasswordSent) {
CoreUtils.showSnackBar(context, Strings.forgotPasswordSnackBarMessage);
Navigator.pushReplacementNamed(context, SignInScreen.id);
}
},
builder: (context, state) {
return SafeArea(
child: Center(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 35,
).copyWith(
top: 0,
),
children: [
LogoWidget(
size: 75,
color: context.theme.primaryColor,
),
const SizedBox(height: 100),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
Strings.forgotPasswordText,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 32,
color: context.theme.primaryColor,
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
state is ForgotPasswordSent ? Strings.passwordSentText : Strings.passwordNotSentText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
],
),
),
const SizedBox(height: 50),
Visibility(
visible: state is! ForgotPasswordSent,
child: Form(
key: formKey,
child: IField(
controller: emailController,
hintText: Strings.emailHintText,
keyboardType: TextInputType.emailAddress,
),
),
),
const SizedBox(height: 50),
if (state is AuthLoading)
const Center(
child: CircularProgressIndicator(),
)
else if (state is! ForgotPasswordSent)
LongButton(
onPressed: () {
FocusManager.instance.primaryFocus?.unfocus();
FirebaseAuth.instance.currentUser?.reload();
if (formKey.currentState!.validate()) {
context.read<AuthBloc>().add(
ForgotPasswordEvent(
email: emailController.text.trim(),
),
);
}
},
label: Strings.resetPasswordButtonText,
),
const SizedBox(height: 30),
Center(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
Strings.goBackTextButtonText,
style: TextStyle(fontSize: 14),
),
),
),
],
),
),
);
},
),
);
}
By focusing on modular and reusable widgets, this UI not only serves the functional purpose of authentication but also lays a strong foundation for the app’s user experience.
You can checkout the full code for this section and the rest of the project in my Github by clicking here.
Testing
To ensure the authentication system was both robust and maintainable, I wrote unit tests for the domain layer, the data layer, and the Authbloc in the presentation layer, follwing the principles of TDD.
Challenges and Lessons Learned
Although it’s not my first time, the biggest challenge for me when it come to authentication using Firebase has always been the testing. Firebase doesn’t provide built-in support for offline testing, making it challenging to isolate authentication methods during unit tests. It’s the most time cosuming part of this feature for me.
Fortunately tools like fake_cloud_firestore and mocktail make it easier to test methods like createUserAcount() and signIn() without interacting with the actual backend
Conclusion
By utilizing dependencies like firebase_auth, flutter_bloc, and dartz, we built a scalable and secure authentication system that adheres to clean architecture principles. Testing tools like fake_cloud_firestore and mocktail played a crucial role in validating the system before release.
This phase set the groundwork for securely managing user data and interactions. In the next development cycle, we’ll focus on creating journal entry storage and ensuring seamless access to user content. Stay tuned!