Weird async issue during logout

Hi, i’ve recently been working on a ionic cordova project which uses firebase, and I have this strange error which keeps occurring whenever the logout function is executed. I keep getting a “ERROR Error: permission_denied at /profile/ihmMrHCdUQReyiqPnuVj4M3U9812: Client doesn’t have permission to access the desired data” error message whenever the user hits logout. I understand this is to do with an observable not being unsubscribed. I believe the problem lies within the app.html file, within the async data, as whenever I comment this code out the logout function works perfectly. The code is here:

app.html

<ion-list *ngIf="profileData | async; let profileData">
     <h5 class="menu_id">{{ profileData.username }}</h5>
     <h3 class="menu_id">{{ profileData.firstName }} {{ profileData.lastName }}</h3>
     <button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)">
       {{p.title}}
     </button>
     <button (click)="logoutUser()">Logout</button>
    </ion-list>

app.component.ts

import { Component, ViewChild, OnDestroy } from '@angular/core';
import { Nav, Platform, App } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { AngularFireAuth } from 'angularfire2/auth';
import { AngularFireDatabase, FirebaseObjectObservable } from 'angularfire2/database-deprecated';
import { WelcomePage } from '../pages/welcome/welcome';
import { ProfilePage } from '../pages/profile/profile';
import { FriendsPage } from '../pages/friends/friends';
import { LoginPage } from '../pages/login/login';
import { SettingsPage } from '../pages/settings/settings';
import { TabsPage } from '../pages/tabs/tabs';
import { Profile } from '../models/profile';
import { Subscription } from 'rxjs/Subscription';

@Component({
  templateUrl: 'app.html'
})
export class MyApp implements OnDestroy {
  @ViewChild(Nav) nav: Nav;
  public userDetails : any;
  rootPage: any = WelcomePage;
  profileData: FirebaseObjectObservable<Profile>;
  pages: Array<{title: string, component: any}>;

  unsubbed: any;

  constructor(private afDatabase: AngularFireDatabase, private afAuth: AngularFireAuth, public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen, public app: App) {
    this.initializeApp();



// used for an example of ngFor and navigation
this.pages = [
  { title: 'My World', component: TabsPage },
  { title: 'Profile', component: ProfilePage },
  { title: 'Friends', component: FriendsPage },
  { title: 'Settings', component: SettingsPage }
];


 this.unsubbed = this.afAuth.authState.subscribe(data => {
  if(data){
    this.profileData = this.afDatabase.object(`profile/${data.uid}`);
  }
 });


 }

 ngOnDestroy() {
  this.unsubbed.unsubscribe();
 }

 initializeApp() {
  this.platform.ready().then(() => {
    this.statusBar.styleDefault();
    this.splashScreen.hide();
  });
 }

 openPage(page) {
  this.nav.setRoot(page.component);
 }

 logoutUser(){
  this.nav.setRoot(WelcomePage);
  this.unsubbed.unsubscribe();
  this.afAuth.auth.signOut();
 }

}

I understand the error, it is trying to access the database and is getting rejected because the user has been logged out. This is strange because as you can see, the observable is unsubscribed before the user is logged out. I’m fairly sure the problem lies within the html code and the async data there, as when I comment the html code out the application works perfectly. I’ve looked everywhere for help regarding this issue but can’t seem to find anything. Any help is greatly appreciated!

The unsubscribe method may be async on its own, hence the af.signout may come too fast

And why are u using angular lifecycle event and not ionic ones?

And another thought: u subscribe to auth state. Thrpugh the signout this state changes and then through the subscribe function (not being unsubscribed) there is an attempt to access remote data

How would you suggest handling it so that the async data in the html file can unsubscribe before the af.signout occurs? I’m using those lifecycle events because I saw them in a tutorial, o you think I should swap them to the navcontroller events instead?

U r unsubscribing the auth state, not the async pipe in the template

Console log data in that subscription to see the content. And test for valid authentication before assigning to prfoiledata

At least that is how u can see what isbhappening

The async pipe should unsubscribe automatically though, right? Which is why i don’t understand this error

Yes
And for sure u r in async hell (my bet)

Try putting a setTimeput of 5seconds on the singout of firebase. If that helps then u know the signout should wait for something instead

If not, then I am about learn something too here, which is great!

A few things

  1. Assign unsubbed a Type of Subscription.
import { Subscription } from ‘rxjs/Subscription’;
unsubbed: Subscription;

this.unsubbed = …

  1. Put your subscription in platform.ready.

That’s a start on maybe getting things working the way you want.

No offense, sincerely, but your code is not written well to handle what you are attempting. Check the forum for some examples.

  1. FirebaseObjectObservables are no longer supported when using up to date angularfire2. If it’s working for you, you’re clearly using an old version. Updating might be helpful in finding relevant advice while searching the forum and google.

  2. Your Subscription should handle an if else case.

if (data) {
 //assign page
  } else {
  //assign other page
  }

This automatically sets the home page based on if a user is logged in or not. As soon as afAuth.signOut() is called, a redirect occurs.

Based on what you’re attempting, which is a little unorthodox, this might not be the exact solution, but should get you going on the right track.

It definitely needs to occur within platform.ready.

There’s also an onAuthStateChanged() option that angularfire2 offers that you can set and forget. Maybe take a look at that. It may be more fitting for what you want.

  1. I think the most important change you could make is create an auth provider that holds angularAuth and exports authState, authStateChanges, etc.

Delegating that logic to a provider and injecting it into your app.component.ts makes it much easier to get a handle on it.

I’ll post something from one of my apps at some point tomorrow if you would like. I’m sorry if it seems like I’m ripping apart your work. I’m truly not. It looks like you’ve put a good foot forward and have the right idea. You just need a hand in getting it done the right way.

4 Likes

@jaydz That post should be pinned as part of a forum FAQ.

1 Like

@AaronSterling, that’s the most meaningful compliment I have ever received in regard to anything involving what I do with a computer. Thank you.

Thanks for the advice. I’m going to try and implement your suggestions later tonight and see if it works! And no worries, i appreciate and welcome any constructive criticism! I’ve only been using ionic/angular/firebase for the past week or so, so there’s still a long way for me to go in terms of implementing things the optimal way. I’ll post later when i’ve tried to fix this. I’d definitely be interested in seeing something from one of your apps too!

Any time. I’ll get a small sample posted as soon as I can. Today was busier than expected, so it may not be until tomorrow. But I will get it posted as soon as I can.

Looking forward to seeing what you come up. You seem to have a really good grasp on things for being just a week in.

Happy coding, and don’t forget to take sanity breaks.

You could try

logoutUser(){
  this.nav.setRoot(WelcomePage);
  this.unsubbed.unsubscribe();
  this.profileData = undefined;
  this.afAuth.auth.signOut();
 }

Because setting profileData to undefined is synchronous code, it should disable the view before calling signOut().

If it works, it is an easy hack, but I would recommend @jaydz approach for better maintainability in the future.

Hey @a19941. Sorry for the delay. Crazy couple of days. So i’m posting 2 pretty simple examples of how I’ve handled auth changes in the past. the first is really standard. The second, I use ionic Events to publish auth changes to app.component.ts from my auth provider. I put that together to see just how isolated I could keep my auth provider.

  1. The pretty standard approach
    First things first, you’ve got to have that auth provider.
    It’s done in a heartbeat via the command line
ionic generate provider AuthProvider

Now you have AuthProvider under src/providers. It’s now a file named auth.ts
This is where I import AngularFireAuth, and where I export authState stuff to components that need it.
I’ll write out a few key, core functions for you. You’ll notice I add Types to absolutely everything. If I did not in this excerpt, it’s because I’m exhausted.

import { AngularFireAuth } from 'angularfire2/auth';
import { Observable } from 'rxjs/Observable';
import firebase from 'firebase';

  export class AuthProvider { 
    constructor(public afAuth: AngularFireAuth) { 
    // this is where I / we would be setting items to empty values, etc. But lets skip it 
    // in the interest of brevity
    }

 loginUser(email: string, password: string): Promise<any> {
   return this.afAuth.auth.signInWithEmailAndPassword(email, password);
  }

 logoutUser(): void {
    this.afAuth.auth.signOut();
  }
  
 exportAuth(): Observable<firebase.User> {
    return this.afAuth.authState;
    // this is what I'm going to send to app.component.ts to allow that component to 
    // subscribe to the firebase user object
  }
}

In app.component.ts

import { AuthProvider } from '../providers/auth/auth';
import { Subscription } from 'rxjs/Subscription';
  
  export class MyApp {
  @ViewChild(Nav) nav: Nav;
    rootpage: any;  <-- this is literally the only instance of any I ever have in an app
    userSubscription: Subscription;
    
    constructor(public authProvider: AuthProvider) {
     this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
    this.userSubscription = this authProvider.exportAuth().subscribe(user => {
       if (user) {
           console.log(user);
           this.nav.setRoot(HomePage);
       } else {
           this.nav.setRoot(LoginPage);
       }
      });
    });
   }
 }

I usually leave it at that. I don’t typically unsubscribe because I do like the convenience of that subscription staying active. As soon as you call

this.authProvider.logoutUser();

from anywhere in the app, the user is redirected to the LoginPage (or some page of your choosing). The same goes for when you call

this.authProvider.loginUser(email, password);

app.component now has a user object in its subscription that is not null, it knows someone is signed in, and redirects to the page that logged in users should land on.

One thing I will point out is this

 if (user) {
     this.nav.setRoot(HomePage);
 } else {
     this.nav.setRoot(LoginPage);
 }

It may be that using

if (user) {
    this.rootPage = //etc....
 } else {
    this.rootPage = //etc...
 }

Is the correct way of going about that process.

That little piece of information just hasn’t been on my list of priorities at any point yet.

I’ll have to post my ionic Events version tomorrow. Need to get some sleep. I’m really liking broadcasting from my providers with events and it’s something I’ve been doing a little more of lately.

Hope it helps.

And FYI, you can handle what you want to handle in regard to user data objects, etc. in the if else of the userSubscription

1 Like

I think you can use the observable here (app.component.ts) without unsubscribing, because that component never gets destroyed without the web view. If you use the user observable in other pages, I would highly recommend to use unsubscribe in OnDestroy or ionViewWillUnload or to use async pipes. If you have a page where you often push to and pop from and this page subscribes to the user observable a new subscription will be created every time and live as long as the observable subscribed to does not fire the completed event.

1 Like

That’s absolutely correct. Definitely unsubscribe in other components. In this case, not only does app.component not get destroyed, it never gets rebuilt. So that redundant subscription isn’t a concern. Unless there’s some edge case I’m not thinking of.