angularangular-route-guards

Angular Router Guard not resolving when the end result is true


The following code works correctly when the final if statement is true. Does not ever resolve the requested route when the final if statement is false. I've tried adding awaits and asyncs. I've tried moving the code into a separate function that returns an await with a boolean and nothing is working to resolve the route when it should. It always works when it should reject an redirect to settings.

If Statement

if(moduleSnapshot.size >= planLimit) {
   this.toast.info(`You've reached your plan maximum, upgrade to add more ${mod}.`, 'Plan Maximum');
   this.router.navigateByUrl('/settings/profile/subscription');
   return false;
}
return true;

Full Router Guard

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

import { ToastrService } from 'ngx-toastr';
import { AngularFirestore } from '@angular/fire/firestore';
import { AuthService } from '../services/auth/auth.service';
import { SubscriptionsService } from '../services/subscriptions/subscriptions.service';

@Injectable({
  providedIn: 'root'
})
export class SubscriptionGuard implements CanActivate {

  constructor( private router: Router, private toast: ToastrService, private authService: AuthService, private subscriptionService: SubscriptionsService, private afs: AngularFirestore ) { }

  canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): any {
    this.authService.userData.subscribe((observer1) => {
      if(observer1) {
        let subscriptions = this.subscriptionService.fetchUserSubscription(observer1.uid);
        subscriptions.onSnapshot((observer:any) => {
          observer.forEach((subscription:any) => {
            if(subscription.exists) {
              this.authService.allUserData.subscribe( async(userDataObserver:any) => {
                let mod:string = state.url.split('/')[1];
                await this.subscriptionService.fetchPlan(subscription.data().productID).then((plan:any) => {
                  let planLimit:number = parseInt(plan.metadata[mod]);
                  let companyUid:string = userDataObserver.companies[0].company;
                  this.afs.collection('companies').doc(companyUid).collection(mod).ref.get().then((moduleSnapshot:any) => {
                    if(moduleSnapshot.size >= planLimit) {
                      this.toast.info(`You've reached your plan maximum, upgrade to add more ${mod}.`, 'Plan Maximum');
                      this.router.navigateByUrl('/settings/profile/subscription');
                      return false;
                    }
                    console.log('Plan max not met, should resolve');
                    return true;
                  });
                });
              });
            }
          });
        });
      }
    });
  }
  
}

Solution

  • As per Angular's implementation, the canActivate method (required by the CanActivate interface) requires a return type.

    export declare interface CanActivate {
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
    }
    

    Without trying to dive to deep into the logic of your route guard, what I can see is that you're not actually returning anything. Because userData is an Observable, the subscription logic is handled asynchronously. This means that the canActivate method is invoked by the router, the subscription logic is started asynchronously and then the method call resolves with no value.

    To fix the issue, you'll need to return an Observable stream which contains the boolean. For this I would suggest using rxjs .pipe() in conjunction with the .switchMap() operator in order to preserve the subscription logic.

    return this.authService.userData.pipe(
        switchMap((user) => {
            // Handle subscription logic and return observable of a boolean value
            return this.someService.doSomething();
        })
    );