User Authentication with Firebase Auth and RxJS

In the past few posts of this series, we discussed the software architecture, the lower-level design post and the observable streaming post. Now, we’ll be taking a look at the implementation of the user authentication service, using Firebase with the Angular official API – @angular/fire.

Table of Contents

User Authentication Factory Class

Same as we did with the Firestore service, we will extract the Firebase authentication methods we want to use an abstract class, and then we will implement this class in the user service we will create.

In the core directory, add a new file named auth.abstract.ts and edit it with the following code:

import { Inject, Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/compat/auth';
import { GoogleAuthProvider } from 'firebase/auth';
@Injectable({
  providedIn: 'root',
})
export abstract class AuthAbstract {
  constructor(@Inject(AngularFireAuth) protected auth: AngularFireAuth) {}

  get user$() {
    return this.auth.user;
  }
  signInWithPopup() {
    return this.auth.signInWithPopup(new GoogleAuthProvider());
  }
  signInWithEmailAndPassword(email: string, password: string) {
    return this.auth.signInWithEmailAndPassword(email, password);
  }
  signUpWithEmailAndPassword(email: string, password: string) {
    return this.auth.createUserWithEmailAndPassword(email, password);
  }
  signInAnonymously() {
    return this.auth.signInAnonymously();
  }
  signOut() {
    return this.auth.signOut();
  }
}

The firebase library was installed by the ng add @angular/fire command. You can add logs to the class methods as we did in our firestroe abstract class, it might help with debugging.

Generate User Module

Run the following command in your terminal:

ng g m user

Since this post is not going to implement the user components, we will leave now the UserModule, in the next post we will import the modules we need for our user components.

User Authentication State Interface

Unlike the flag entity that we defined in the flag interface, for the user we will be using the firebase user observable, in a real-life app, you might have a user interface.

In the user directory, create a file named user.state.ts and edit it as follows:

export interface UserState {
  loading: boolean;
  uid: string;
  formStatus: string;
  isLoggedIn: boolean;
  error?: string;
  action?: string;
}

User Pagestore Class

On the same directory create a new file named user.pagestore.ts, it should look like this:

import { Injectable } from '@angular/core';
import { PagestoreAbstract } from '../core/pagestore.abstract';
import type { UserState } from './user.state';

@Injectable({
  providedIn: 'root',
})
export class UserPagestore extends PagestoreAbstract<UserState> {
  protected pagestore = 'user';
  constructor() {
    super({
      loading: true,
      uid: undefined,
      error: undefined,
      action: undefined,
    });
  }
}

Generate User Service

Using the Angular CLI, generate a new service named user.

ng g s --skip-tests=true user/user

Edit the newly generated file src/app/user/user.service.ts with the following code:

import { AuthAbstract } from '../core/auth.abstract';
import { Injectable } from '@angular/core';
import { UserPagestore } from './user.pagestroe';
import { map } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  constructor(private pagestore: UserPagestore, private auth: AuthAbstract) {
    this.auth.user$
      .pipe(
        map((user) => {
          this.pagestore.patch(
            {
              loading: false,
              uid: user?.uid,
              isLoggedIn: user?.uid ? true : false,
            },
            'auth subscription'
          );
        })
      )
      .subscribe();
  }

  get uid$() {
    return this.pagestore.state$.pipe(map((state) => state.uid));
  }
  get loading$() {
    return this.pagestore.state$.pipe(map((state) => state.loading));
  }
  get isLoggedIn$() {
    return this.pagestore.state$.pipe(map((state) => state.isLoggedIn));
  }
  get error$() {
    return this.pagestore.state$.pipe(map((state) => state.error));
  }
  get action$() {
    return this.pagestore.state$.pipe(map((state) => state.action));
  }
  get formStatus$() {
    return this.pagestore.state$.pipe(map((state) => state.formStatus));
  }

  async signInWithGoogle() {
    this.pagestore.patch(
      { loading: true, formStatus: 'signing in' },
      'signing in with google'
    );
    try {
      await this.auth.signInWithPopup();
      setTimeout(() => {
        this.pagestore.patch(
          {
            loading: false,
            formStatus: 'signed in',
            isLoggedIn: true,
          },
          'signed in with google'
        );
        window.location.href = '/';
      }, 1000);
    } catch (error) {
      this.pagestore.patch(
        {
          loading: false,
          formStatus: 'error',
          error: new Error(error as string).message,
          action: 'try again',
        },
        'error signing in with google'
      );
    }
  }

  async signInWithEmailAndPassword(email: string, password: string) {
    this.pagestore.patch(
      { loading: true, formStatus: 'signing in' },
      'signing in'
    );
    try {
      await this.auth.signInWithEmailAndPassword(email, password);
      setTimeout(() => {
        this.pagestore.patch(
          {
            loading: false,
            formStatus: 'signed in',
            isLoggedIn: true,
          },
          'signed in'
        );
        window.location.href = '/';
      }, 1000);
    } catch (error) {
      this.pagestore.patch(
        {
          loading: false,
          formStatus: 'error',
          error: new Error(error as string).message,
          action: 'try again',
        },
        'error signing in'
      );
    }
  }

  async signUpWithEmailAndPassword(email: string, password: string) {
    this.pagestore.patch(
      { loading: true, formStatus: 'signing up' },
      'signing up'
    );
    try {
      await this.auth.signUpWithEmailAndPassword(email, password);
      setTimeout(() => {
        this.pagestore.patch(
          { loading: false, formStatus: 'signed up' },
          'signed up'
        );
        window.location.href = '/';
      }, 1000);
    } catch (error) {
      this.pagestore.patch(
        {
          loading: false,
          formStatus: 'error',
          error: new Error(error as string).message,
          action: 'try again',
        },
        'error signing up'
      );
    }
  }

  async signUpAnonymously() {
    try {
      this.pagestore.patch(
        { loading: true, formStatus: 'signin in' },
        'signing in anonymously'
      );
      await this.auth.signInAnonymously();
      setTimeout(() => {
        this.pagestore.patch(
          {
            loading: false,
            formStatus: 'signed in',
            isLoggedIn: true,
          },
          'signed in successfully'
        );
        window.location.href = '/';
      }, 1000);
    } catch (error) {
      this.pagestore.patch(
        {
          loading: false,
          formStatus: 'error',
          error: new Error(error as string).message,
          action: 'try again',
        },
        'error signing in'
      );
    }
  }

  async signOut() {
    this.pagestore.patch(
      { loading: true, formStatus: 'signin out' },
      'signin out'
    );
    try {
      await this.auth.signOut();
      setTimeout(() => {
        this.pagestore.patch(
          {
            loading: false,
            formStatus: 'signed out',
            uid: undefined,
          },
          'signed out'
        );
        window.location.href = '/';
      }, 1000);
    } catch (error) {
      this.pagestore.patch(
        {
          loading: false,
          formStatus: 'error',
          error: new Error(error as string).message,
          action: 'try again',
        },
        'error signin out'
      );
    }
  }
}

Next, we will add Angular Material, we will generate the LayoutModule, and finalize our components.

chevron_left
chevron_right

Leave a comment

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

Comment
Name
Email
Website