Angular Observables: building an account activation countdown timer.

Angular Observables: building an account activation countdown timer.

Introduction

One of the approaches generally used to validate a user’s email address during signup is to send a n-digit code to this address. He is then asked to provide this code within a relatively short time in minutes. The problem we want to solve in this article is to implement such a system using Angular Observable.

What will we create ?

We will create an angular application that has 2 views : a signup page, and an account activation page. We will create a countdown service that will be injected in the component. And we will also create a custom pipeline in order to format the time to the user.
In summary :

  • signupComponent

  • AccountActivationComponent

  • countDownService

  • formatTimePipeline

Assumptions

1- We will focus on the front-end part to make reading digestible assuming that your Spring, Jango, Express, etc backend is functional.
2- We will also assume that for a succesful signup operation, your backend replies with a JSON object looking like this :

{
  "account_creation_date" : "2024-03-13T12:08:38.0295378",
  "other_key" : "other_value"
}

3- You have Angular 2+ intalled on your system.

Strategy

When the user submits their information to create an account, the request is sent to the backend authentication service API. This API creates the user in the database with the account marked as deactivated. The backend then sends an email to the user, containing the activation code, valid for 10 minutes in our scenario. Once the process is completed, the API responds by providing an object in the form described above. Subsequently, the front-end application retrieves the creation date of the account which is provided in the response, adds the required 10 minutes to it, and stores this in localStorage. A service calculates the remaining time in real-time, and returns an observable. Finally the component subscribes to this observable and displays the countdown. The advantage of this approach is that even if the page is reloaded, the countdown remains accurate and continues to decrement.

Create an Angular project

ng new accountActivationCountDown

Open the newly created project in your favorite IDE and then start the development server.

ng serve

Create empty components

ng g c component/signup & ng g c component/activateAccount

When these commands complete, a new folder named component is created, containing our two components separeted in two folders.

Replace all the content of app.component.html file with the following and you are done for this step.

<div>
    <app-signup></app-signup>
    <router-outlet></router-outlet>
</div>

NB : Note the presence of router-outlet, which enables the Angular engine to display components based on the selected route. Also, observe the presence of app-signup, which corresponds to the selector of our signup component.

Create the authentication service

ng g s service/authService

Now, open the newly created service and write the following code:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthServiceService {

  //replace with your own apiUrl
  readonly apiUrl = `http://localhost:8088/api/v1/auth/signup`;
  constructor(private http: HttpClient) { }

  signup(firstname:string, lastname: string, email : string, password : string): Observable<any> { 
    return this.http.post(this.apiUrl, {firstname, lastname, email, password});
  }
}

To use HttpClient, it is necessary to add HttpClientModule to the imports array in the app.module.ts file.

Create the countdown service

ng g service service/countDown

In the countDown service, create a method that returns a timer Observable like this :

import { Injectable } from '@angular/core';
import { Observable, map, takeWhile, timer } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CountDownService {

  constructor() { }

  getRemainingTimeObservable(endTime:string):Observable<any> {

    return timer(0, 1000).pipe(
      map(() => {
        const total = Date.parse(endTime) - Date.parse(new Date().toString());
        const seconds =  Math.floor((total / 1000) % 60);
        const minutes =  Math.floor((total / 1000 / 60) % 60);

        return {total, seconds, minutes}
      }),
      takeWhile(({ total }) => total >= 0, true)
    )
  }
}

This function takes as an argument the expiration date of the validation code. It returns an obervable. The timer(0, 1000) method creates an observable that starts emitting values immediately (as indicated by the first argument), and emits a new one every second (1000 milliseconds). This simulates the ticking of a clock.

The rxjs pipe method allows you to apply operations to the data stream produced by the observable. In our case it will allow us to use the map and takeWhile operators on our stream. We use map to produce the data format to return, and the takeWhile to set some kind of stop condition for the timer.

At each time t of the timer, the following line of code will calculate the remaining time until expiration by subtracting the current time from endTime. Here is the heart of our countdown timer.

const total = Date.parse(endTime) - Date.parse(new Date().toString());

Update the signup component

<form >
    <!--Two ways binding with ngModel requires to add FormsModule in the 
      imports array of app.module.ts. 
      Let's use this approach.
    -->
    <input type="text" placeholder="John" name="firstname" [(ngModel)]="firstname" />
    <input type="text" placeholder="Doe" name="lastname" [(ngModel)]="lastname" />
    <input type="email" placeholder="email@mail.com" name="email" [(ngModel)]="email" />
    <input type="password" placeholder="*******" name="password" [(ngModel)]="password" />
    <button (click)="signup()">Submit</button>
</form>

Update the signup.component.ts like this :

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthServiceService } from 'src/app/service/auth-service.service';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  styleUrls: ['./signup.component.css']
})
export class SignupComponent {
  email! : string;
  password! : string;
  firstname!: string;
  lastname!: string;

  authService = inject(AuthServiceService)
  router = inject(Router)

  signup() {
    this.authService.signup(this.firstname, this.lastname, this.email,this.password).subscribe({
      next : apiResponse => {
        //some stuff here
        localStorage.setItem("authentication_date", apiResponse.authentication_date);
        this.router.navigate(['/activate']);
      }, 
      error: (err) => console.error(err),
      complete: () => console.log('Completed')
    })   
  }
}

Create custom pipe

Pipes in Angular are classes that implement the PipeTransform interface to perform data transformations synchronously or asynchronously in templates. You can use built-in pipes such as currency or date, or create custom pipes that fit your specific requirements.

To create a pipe, you can do it manually for a deeper understanding, but also using angular CLI. Let us create two pipe that we will use later :

ng g pipe customPipe/addZeros
ng g pipe customPipe/setToZero

Replace the AddZerosPipe transform method with this one:

// This method add a zero when the number is less than 10.
// Example : 9 -> 09, 10 -> 10
transform(value: number): string {
  if(value < 10) {
    return '0' + value;
  }
  return String(value);
}

And replace the SetToZeroPipe transform method with this one:

// To avoid to print negative value, set to zero when countdown completes
// example : -10 -> 0
transform(value: number): number {
  if(value < 0) return 0;
  return value;
}

Update activation.component.html

Now that we have our signup component, our service, our pipes, let us set the activation component up.

<div>
    <div>
        <p>Enter the code that has been sent to your email adress</p>
        <p>
          remaining time : {{remainingTime?.minutes | setToZero | addZeros}}:{{remainingTime?.seconds | setToZero | addZeros}}</p>
    </div>
    <form >

        <input type="number" name="code" class="form-control" [(ngModel)]="activationCode" 
            id="inputCode" placeholder="00000" />
        <button (submit)="activate">Submit</button>
    </form>
</div>

Note how we use a pipe with the ‘|’ symbol followed by its name. This name corresponds to the one passed in the @Pipe directive.
Update the containt of the activation.component.ts file :

  activationCode! : number;
  activationDeadline: any;

  remainingTime : any;
  subscriptionToTimer!: Subscription;
  timerService = inject(CountDownService);

  ngOnInit() {
    console.log(new Date().toString());
    let endTime = localStorage.getItem('authentication_date') ?? new Date().toString(); 
    this.activationDeadline = new Date(Date.parse(endTime) + 60 * 10 * 1000);
    this.subscriptionToTimer = this.timerService.getRemainingTimeObservable(this.activationDeadline)
      .subscribe({
        next : time => {
          this.remainingTime = time;
        },
        error: (err) => console.error(err),
        complete: () => console.log('Observable completed')
      })
  }

  activate () {
    //TODO implement the logic here
  }

Now open the application on your browser. In terms of UI/UX, nothing too special. fill in your informations and click on the submit button. The countdown starts. And if you reload the page, it doesn’t reset. That was the aim of the game. But if it resets, this means that you have not retrieved the date from the backend (confirm this by checking if the localStorage is not null).

Conclusion

In this article, we implemented a countdown for activating an account after a signup operation. This improves the user experience. The key Angular concepts that were discussed:
- Rxjs Observable,
- angular pipes
- data binding
- Angular CLI
To go in-depth on these concepts, you can read the Angular documentation at the following address : https://angular.io/docs
You can find the source code on my Github repository at the following address: https://github.com/ntjoel19/countDownTime

Thanks for reading and commenting. Let's connect :

https://twitter.com/ntjoel_19
https://github.com/ntjoel19
https://www.linkedin.com/in/jean-joel-n-ntepp-876a78110/