Search code examples
angulartypescriptrxjsangular-servicesangular-guards

CanActivate requesting a stream after NavigationEnd


I'm trying to build a SubscriptionGuard in Angular. This Guard should check whether the user has paid or not for the subscription.

I'm having a weird issue. I'll show the code and will explain this after.


subscription.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { UsersService } from '../services/users.service';


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

    constructor(
        private usersService: UsersService,
        private router: Router
    ) { }

    async canActivate(route: any, state: RouterStateSnapshot): Promise<UrlTree> {
        const status = await this.usersService.userSubscriptionStatus();
        let tree = this.router.parseUrl(state.url);

        // User should update payment methods.
        if (
            status === 'past_due' ||
            status === 'unpaid' ||
            status === 'incomplete'
        ) {
            tree = this.router.parseUrl('/subscription');
            tree.queryParams.subscriptionReturnUrl = state.url;
        }

        // User should create a new subscription.
        if (
            status === 'canceled' ||
            status === 'incomplete_expired' ||
            status === null
        ) {
            tree = this.router.parseUrl('/subscription');
            tree.queryParams.subReturnUrl = state.url;
            tree.queryParams.firstSub = status === null;
        }

        return tree;
    }
}

users.service.ts

// [ ... ]

async userSubscriptionStatus(): Promise<USS_Status | null> {
  const uid = (await this.getCurrentFire())?.uid;
  if (!uid) return null;
  return new Promise((resolve, reject) => {
    this.db.colWithIds$<UserStripeSubscription>(
      `users/${uid}/subscriptions`,
      (ref: any) => ref.orderBy('created', 'desc')
    )
      .pipe(take(1))
      .subscribe((subs: UserStripeSubscription[]) => {
        let timestamp: number | null = null;
        let status: string | null = null;
        subs.forEach(sub => {
          if (!sub.status) return;
          if (!timestamp) {
            timestamp = sub.created.seconds;
            status = sub.status;
            return;
          }
          if (timestamp <= sub.created.seconds) {
            timestamp = sub.created.seconds;
            status = sub.status;
            return;
          }
          return;
        });
        console.log('status =>', status);
        resolve(status);
      });
  });
}

// [ ... ]

As you can see, the guard relies on the method userSubscriptionStatus() in users.service.ts.

In a first testing phase I was thinking that .pipe(take(1)) wasn't working for some weird reason, but with a closer debugging I noticed that the problem was in fact that SubscriptionGuard was calling continuously the method in UsersService.

I tried everything but I don't know how to fix this. This is what I'm getting in the console:

https://i.sstatic.net/l3p2I.png

Can someone help? I really don't know...

[UPDATE #1] I updated the code. Now it's looking like this:

subscription.guard.ts

import { USS_Status } from './../models/user-stripe-subscription.model';
import { Injectable } from '@angular/core';
import { CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { UsersService } from '../services/users.service';


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

    private status: USS_Status | null | undefined = undefined;

    constructor(
        private usersService: UsersService,
        private router: Router
    ) { }

    canActivate(route: any, state: RouterStateSnapshot): Promise<UrlTree> {
        return new Promise<UrlTree>((resolve, reject) => {
            const method = () => {
                let tree = this.router.parseUrl(state.url);

                if (
                    this.status === 'past_due' ||
                    this.status === 'unpaid' ||
                    this.status === 'incomplete'
                ) {
                    tree = this.router.parseUrl('/subscription');
                    tree.queryParams.subscriptionReturnUrl = state.url;
                }

                else if (
                    this.status === 'canceled' ||
                    this.status === 'incomplete_expired' ||
                    this.status === null
                ) {
                    tree = this.router.parseUrl('/subscription');
                    tree.queryParams.subReturnUrl = state.url;
                    tree.queryParams.firstSub = this.status === null;
                    console.log('...........................................');
                    console.log('this.status === null', this.status === null, this.status);
                    console.log('this.status === canceled', this.status === 'canceled', this.status);
                    console.log('this.status === incomplete_expired', this.status === 'incomplete_expired', this.status);
                }

                resolve(tree);
            };

            if (this.status === undefined)
                this.usersService.userSubscriptionStatus().then((status) => {
                    this.status = status;
                    method();
                    console.log('Guard status is =>', status);
                });
            else method();
        });
    }
}

users.service.ts

// [...]

async userSubscriptionStatus(): Promise<USS_Status | null> {
  const uid = (await this.getCurrentFire())?.uid;
  if (!uid) return null;
  return new Promise((resolve, reject) => {
    this.db.colWithIds$<UserStripeSubscription>(
      `users/${uid}/subscriptions`,
      (ref: any) => ref.orderBy('created', 'desc')
    )
      .pipe(take(1))
      .subscribe((subs: UserStripeSubscription[]) => {
        let timestamp: number | null = null;
        let status: string | null = null;
        console.log('SUBS are => ', subs);
        subs.forEach(sub => {
          if (!sub.status) return;
          if (!timestamp) {
            timestamp = sub.created.seconds;
            status = sub.status;
            return;
          }
          if (timestamp <= sub.created.seconds) {
            timestamp = sub.created.seconds;
            status = sub.status;
            return;
          }
          return;
        });
        console.log('status =>', status);
        resolve(status);
      });
  });
}

// [...]

My console now look like this...

........................................... // subscription.guard.ts
'this.status === null' true null // subscription.guard.ts
'this.status === canceled' false null // subscription.guard.ts
'this.status === incomplete_expired' false null // subscription.guard.ts
'Guard status is =>' null // subscription.guard.ts

[WDS] Live Reloading enabled. // ANGULAR

'SUBS are =>' (2) [{…}, {…}] // users.service.ts
'status =>' 'active' // users.service.ts

........................................... // subscription.guard.ts
'this.status === null' true null // subscription.guard.ts
'this.status === canceled' false null // subscription.guard.ts
'this.status === incomplete_expired' false null // subscription.guard.ts

It seems that the service is running only AFTER the guard. I really don't know how to do it...


Solution

  • From console log output it is clear your userService is running first

    Updated your code with method function accepting a parameter statusParam

    canActivate(route: any, state: RouterStateSnapshot): Promise<UrlTree> {
            return new Promise<UrlTree>((resolve, reject) => {
                const method = (statusParam) => {
                    let tree = this.router.parseUrl(state.url);
    
                    if (
                        statusParam === 'past_due' ||
                        statusParam === 'unpaid' ||
                        tstatusParam === 'incomplete'
                    ) {
                        tree = this.router.parseUrl('/subscription');
                        tree.queryParams.subscriptionReturnUrl = state.url;
                    }
    
                    else if (
                        statusParam === 'canceled' ||
                        statusParam === 'incomplete_expired' ||
                        statusParam === null
                    ) {
                        tree = this.router.parseUrl('/subscription');
                        tree.queryParams.subReturnUrl = state.url;
                        tree.queryParams.firstSub = statusParam === null;
                        console.log('...........................................');
                        console.log('statusParam === null', statusParam === null, statusParam);
                        console.log('statusParam === canceled', statusParam === 'canceled', statusParam);
                        console.log('statusParam === incomplete_expired', statusParam === 'incomplete_expired', statusParam);
                    }
    
                    resolve(tree);
                };
    
                if (this.status === undefined)
                    this.usersService.userSubscriptionStatus().then((status) => {
                        // this.status = status;
                        console.log('Resolved Guard status is =>', status);
                        method(status);                    
                        console.log('Guard status is =>', status);
                    });
                else method(this.status);
            });
        }