Josh Bangle
Josh Bangle

Reputation: 261

Setting Observables in a service using async data from another service in Angular14

I'm using an Auth service with OIDC to get user data after a user is logged in. I need that user data to determine what side-nav items a user will see, which is handled in a StepsService.

My current approach is creating an observable User that the StepService can reach out to, and use the data from that Observable to set all of the necessary StepService values. My files are as follows:

steps.service.ts

export class StepsService {
  // Instantiate the initial variables that will act as state storage
  sections: Observable<SectionModel[]>;
  currentSection: Observable<SectionModel>;
  stepsInCurrentSection: Observable<StepModel[]>;
  currentStep: Observable<StepModel>;
  currentStepIndex: BehaviorSubject<number> = new BehaviorSubject<number>(0);
  totalStepsInCurrentSection: number;
  totalSections: number;
  currentSectionIndex: BehaviorSubject<number> = new BehaviorSubject<number>(0);
  numberOfCompletedSteps: BehaviorSubject<number> = new BehaviorSubject<number>(0);

  constructor(private authService: AuthService) {
    // Set state variables based on initial steps available
    this.sections = this.authService.getUser().pipe(
      map((user) => {
        console.log('user is: ', user);
        const userRoles: string[] = user.profile['role'];
        return this.getUserSpecificSteps(userRoles);
      }),
      tap((sections) => (this.totalSections = sections.length))
    );
    this.currentSection = combineLatest([this.sections, this.currentSectionIndex]).pipe(
      map((data) => {
        const sections = data[0];
        const index = data[1];
        return sections[index];
      })
    );
    this.stepsInCurrentSection = this.currentSection.pipe(
      map((sections) => sections.steps),
      tap((steps) => (this.totalStepsInCurrentSection = steps.length))
    );
    this.currentStep = combineLatest([this.stepsInCurrentSection, this.currentStepIndex]).pipe(
      map((data) => {
        const steps = data[0];
        const index = data[1];
        return steps[index];
      })
    );
  }

  getUserSpecificSteps(userRoles: string[]) {
    return userRoles?.includes('Admin') ? adminSections : userSections;
  }
...

auth.service.ts

export class AuthService {

  private config = environment;
  private _isAuthenticated: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  public readonly IsAuthenticated: Observable<boolean> = this._isAuthenticated.asObservable();
  private _userManager: UserManager;
  public _user: User | null;
  public userSubject$: Observable<User>;

  constructor() {
    this._userManager = new UserManager(getClientSettings(this.config));
    this._userManager
      .getUser()
      .then((user: any) => {
        if (user && !user.expired) {
          this._user = user;
          this.userSubject$ = user;
          this._isAuthenticated.next(this.isAuthenticated());
        }
        return user;
      })
      .catch((e) => console.log(e));
    //fires after every user refresh
    this._userManager.events.addUserLoaded((args) => {
      this._userManager
        .getUser()
        .then((user) => {
          this._user = user;
        })
        .catch((e) => console.log(e));
    });
  }

  getUser() {
    return this.userSubject$;
  }
...

Currently this throws an error that this.authService.getUser() is undefined and if I add a null check after the method call, it throws an error that I am passing undefined data to a data stream.

I do also have an APP_INITIALIZER inside of my app.module file that initializes the app with the auth service. I'm not sure if I'm using the observables correctly, or what the problem could be.

Thanks in advance!

Upvotes: 0

Views: 136

Answers (1)

You should probably use something like eslint.

 this._userManager
      .getUser()
      .then((user: any) => { <- remove : any it will not tell you what type you have
        if (user && !user.expired) {
          this._user = user; <- is it user or is it a subject<user>?
          this.userSubject$ = user; <-is it user or is it a subject<user>?
          this._isAuthenticated.next(this.isAuthenticated());
        }
        return user;
      })

my assumption you a are getting authService.getUser() is undefined is due to the subject. I would just set it as behavior subject since you add data to it via a differnet service so.

public userSubject$ = new BehaviorSubject<User| null>(null);

this means you now getUser() is never undefined. however the value might be null

setting the value:

 this._userManager
      .getUser()
      .then((user: User) => {
        if (user && !user.expired) {
          this._user = user; 
          this.userSubject$.next( user); <- assuming that user is type User
          this._isAuthenticated.next(this.isAuthenticated());
        }
        return user;
      })

Upvotes: 1

Related Questions