0

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;
                  });
                });
              });
            }
          });
        });
      }
    });
  }
  
}
SDMitch
  • 39
  • 2
  • 6
  • What does your RouterModule or your routing in AppModule imports look like? I take it you already tried to just 'hard return' true? What did that do? – H3AR7B3A7 Oct 08 '21 at 00:57
  • I did try to simply return true and false before I added any code and it worked flawlessly. The canActivate on a child route if that makes a difference. – SDMitch Oct 08 '21 at 01:10
  • The console log does also show when it should. – SDMitch Oct 08 '21 at 01:12

1 Answers1

0

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();
    })
);
El-Mo
  • 370
  • 2
  • 13
  • Suppose it got me closer. Still no dice though. Just trying to do too much in a router guard I suppose. Clearly they don't like promises and subscriptions. Frankly, quite annoying. Thank you though. I guess I'm just not going to be able to use a router guard. – SDMitch Oct 08 '21 at 02:45
  • subscribing in a canActivate is a bad idea, consider returning a observable instead, that way your canActivate remains async and resolves correctly for your output. @SDMitch – mak15 Oct 08 '21 at 05:05
  • Which would require me to rewrite my entire app or create separate functions just for the router guard. This is where Angular really struggles. Thank you though. – SDMitch Oct 10 '21 at 00:01
  • I disagree, @user3929590. The entire reason that the canActivate method provides handling logic for an Observable stream is for the intent purpose of processing asynchronous data. In some cases, making an asynchronous call is the only way of accurately determining if the route should be visible. If it was a bad idea, as you suggest, then I would think that it wouldn't be supported or at least deprecated with some form of warning. @SDMitch Promises can be converted to observables using RxJS's `from()` method. More here: https://stackoverflow.com/questions/39319279/convert-promise-to-observable – El-Mo Oct 13 '21 at 02:15
  • Hey @El-Mo, by subscribing in a canActivate is a bad idea, I meant that not to manually subscribe, instead, let angular handle that subscription instead. Whenever you return an observable in canActivate, angular does subscribe to it and only allows it once the conditions are met. Hence, I mentioned that returning an observable instead of manually subscribing and then returning true or false as you have done in your code snippet. – mak15 Oct 13 '21 at 04:37