The Protests Map Part IV – User Module with Firebase Authentication

In the previous posts of this series, we had the software architecture post, the lower-level design post and the observable streaming post. The following post is an implementation of the Firebase authentication service, using the Angular official API – @angular/fire.

Auth Factory Class

Same as we did with the Firestore service, we will extract the Firebase authentication methods we want to use an abstract class, 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 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.

Comment
Name
Email
Website