Building a Mental Health Journal App: Authentication and User Management

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

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

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

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

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!

Christian
Christian
Articles: 11

Leave a Reply

Your email address will not be published. Required fields are marked *