Search code examples
angulartypescriptionic-frameworkionic4

Why is my service not updating on my app component?


I am working with Ionic 4 and I want to show information about the current logged user but is not working I mean it works on any component except the app component, I am using a variable called userData which is a BehaviorSubject. Do you know what is wrong with my code?

auth.service.ts

import { Platform } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
import { BehaviorSubject, Observable, from, of, throwError } from 'rxjs';
import { take, map,tap, switchMap, catchError } from 'rxjs/operators';
import { JwtHelperService } from "@auth0/angular-jwt";
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Tokens } from '../models/tokens';
import { environment } from 'src/environments/environment';

const helper = new JwtHelperService();
//Agregar token access y refresh
const accessToken = 'access';
const refreshToken = 'refresh';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  public user: Observable<any>;
  private userData = new BehaviorSubject(null);
  private access: string = null;
  private refresh: string = null;
  constructor(private storage: Storage, private http: HttpClient, private plt: Platform, private router: Router) { 
    this.loadStoredTokens();  
  }

  getTokenExpirationDate(userData): Date {
    if (userData['exp'] === undefined) return null;

    const date = new Date(0); 
    date.setUTCSeconds(userData['exp']);
    return date;
  }

  isAcessTokenExpired(): boolean {
    let userData = this.userData.getValue();
    console.log(userData);
    if(!userData) return true;

    const date = this.getTokenExpirationDate(userData);
    if(date === undefined) return false;
    return !(date.valueOf() > new Date().valueOf());
  }

  loadStoredTokens() {
    let platformObs = from(this.plt.ready());

    this.user = platformObs.pipe(
      switchMap(() => {
        return from(this.storage.get(refreshToken));
      }),
      map(refreshToken => {
        if (refreshToken) {
          this.refresh = refreshToken;
        } else {
          this.refresh = null;
        }
      }),
      switchMap(() => {
        return from(this.storage.get(accessToken));
      }),
      map(accessToken => {
        if (accessToken) {
          this.access = accessToken;
          let decoded = helper.decodeToken(accessToken); 
          this.userData.next(decoded);
          return true;
        } else {
          this.access = null;
          return null;
        }
      })
    );

  }

  login(credentials: {username: string, password: string }) {
    return this.http
    .post(`${environment.apiUrl}/token/obtain/`,credentials)
    .pipe(
      take(1),
      map(res => {
        // Extract the JWT
        return res;
      }),
      switchMap(token => {
        let decoded = helper.decodeToken(token['access']);
        this.userData.next(decoded);

        from(this.storage.set(refreshToken, token['refresh']))
        let storageObs = from(this.storage.set(accessToken, token['access']));
        return storageObs;
      })
    );

  }

  getUser() {
    return this.userData.getValue();
  }

  logout() {
    this.storage.remove(accessToken).then(() => {

      this.storage.remove(refreshToken).then(() => {
        this.router.navigateByUrl('/');
        this.userData.next(null);
      });

    });
  }

getAccessToken() {
  return  this.access;
}

 getRefreshToken() {
  return this.refresh;
}

setAccessToken(token) {
  this.storage.set(accessToken, token);
}

  refreshToken() {
    return this.http.post<any>(`${environment.apiUrl}/token/refresh/`, {
    'refresh': this.getRefreshToken()
    }).pipe(tap((tokens: Tokens) => {
      this.setAccessToken(tokens.access);
      this.access = tokens.access;
    }));
  }
}

app.component.ts (I am using getUser() function in order to get the current user information)

import { Component, OnInit, ViewChildren, QueryList, OnChanges } from '@angular/core';

import { Platform, ModalController, ActionSheetController, PopoverController, IonRouterOutlet, MenuController } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { Router } from '@angular/router';
import { faHome, faInfo, faFileContract } from '@fortawesome/free-solid-svg-icons';
import { ToastController } from '@ionic/angular';
import { AuthService } from './auth/services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent implements OnInit {
  public selectedIndex = 0;
  user = null;

  public appPages = [
    {
      title: 'Home',
      url: '/home',
      icon: faHome
    },
    {
      title: 'About',
      url: '/about',
      icon: faInfo
    },
    {
      title: 'Legal',
      url: '/legal',
      icon: faFileContract
    }
  ];


  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public modalCtrl: ModalController,
    private menu: MenuController,
    private actionSheetCtrl: ActionSheetController,
    private popoverCtrl: PopoverController,
    private router: Router,
    public toastController: ToastController,
    private auth: AuthService
  ) {
    this.initializeApp();
  }



  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.backgroundColorByHexString('#D4AF37');
      this.splashScreen.hide();
    });
  }

  ngOnInit() {

    this.user = this.auth.getUser();
    const path = window.location.pathname.split('home/')[1];
    if (path !== undefined) {
      this.selectedIndex = this.appPages.findIndex(page => page.title.toLowerCase() === path.toLowerCase());
    }



  }


}

app.component.html (here I want to show the user information, the same code is working in every component except this)

<ion-app>
  <ion-split-pane contentId="main-content">
    <ion-menu contentId="main-content" type="overlay">
      <ion-content>
        <ion-list id="inbox-list">
          <ion-list-header>Menu</ion-list-header>
          <ion-note *ngIf="user">{{ user.email }} </ion-note>

          <ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index">
            <ion-item (click)="selectedIndex = i" routerDirection="root" [routerLink]="[p.url]" lines="none" detail="false" [class.selected]="selectedIndex == i">
              <fa-icon (keyup)="onKeyFaIcon($event)" slot="start" [icon]="p.icon"></fa-icon>

              <ion-label>{{ p.title }}</ion-label>
            </ion-item>
          </ion-menu-toggle>
        </ion-list>

      </ion-content>
    </ion-menu>
    <ion-router-outlet id="main-content"></ion-router-outlet>
  </ion-split-pane>
</ion-app>

Solution

  • It appears the userData variable is assigned a new value in functions loadStoredTokens() and login(). I don't see login() function called anywhere and the loadStoreTokens() function is wrongfully invoked synchronously. It handles data asynchronously, and the data flow should be asynchronous as well.

    Service

    export class AuthService {
      constructor(private storage: Storage, private http: HttpClient, private plt: Platform, private router: Router) { 
        this.loadStoredTokens().subscribe(
          accessToken => {
            let decoded = helper.decodeToken(accessToken); 
            this.userData.next(decoded);
          },
        );  
      }
    
      loadStoredTokens() {
        const result = new Subject<any>();
        let platformObs = from(this.plt.ready());
    
        this.user = platformObs.pipe(
          switchMap(() => {
            return from(this.storage.get(refreshToken));
          }),
          map(refreshToken => {
            if (refreshToken) {
              this.refresh = refreshToken;
            } else {
              this.refresh = null;
            }
          }),
          switchMap(() => {
            return from(this.storage.get(accessToken));
          }),
          map(accessToken => {
            if (accessToken) {
              this.access = accessToken;
              result.next(accessToken);
            } else {
              this.access = null;
              result.next(null);
            }
          })
        );
    
      return result.asObservable();
    }
    

    Also if you're using getValue() function of BehaviorSubject there isn't any use of the multicast observable. Instead you could skip the getValue() in the service and subscribe to it in the component.

    Service

    getUser() {
      return this.userData.asObservable();
    }
    

    Component

    ngOnInit() {
      this.auth.getUser().subscribe(
        user => { this.user = user }
      );
    }
    

    Use safe navigation operator ?. in the template to check if the user variable is defined before trying to access it's properties. Because initially the value is null as specified from the default value of the BehaviorSubject.

    <ion-note *ngIf="user">{{ user?.email }} </ion-note>
    

    Or alternatively you could skip the subscription in the component controller and use async pipe in the template.

    Component

    ngOnInit() {
      this.user = this.auth.getUser();
    }
    

    Template

    <ion-note *ngIf="user">{{ (user | async)?.email }} </ion-note>