Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Ngrx With Firebase Auth Google OAuth Login

Episode 46 written by Jeff Delaney
full courses for pro members

In the third lesson installment of Ngrx, we are going to add Firebase Auth to our basic redux app. It will give our users the ability to authenticate via Google OAuth and logout, while managing the side effects of these actions with @ngrx/effects.

If you’re brand new to Ngrx, make sure to check out these related lessons.

Using Google OAuth in Firebase with Ngrx

Update App Module

Before doing anything, let’s update the app module with the requried files. Here’s a partial glimpse of the NgModule that includes only the pieces needed for Ngrx and AngularFire2. We will be using the AngularFireAuthModule provided by AngularFire2, but these principles could be used with the regular Firebase SDK also.

@NgModule({
imports: [
/// ... omitted
AngularFireModule.initializeApp(firebaseConfig),
AngularFireAuthModule,

EffectsModule.forRoot([
UserEffects
]),

StoreModule.forRoot({
user: userReducer
}),
],
})
export class AppModule { }

Modeling User Data

We are going to keep our users as simple as possible - just 2 required attributes (uid, displayName) and 2 optional ones (loading, error).

You can always determine if a user is logged in or not based on their uid. If the uid is empty, you know the user is NOT authenticated.

export interface IUser {
uid: string;
displayName: string;
loading?: boolean;
error?: string;
}

export class User implements IUser {
constructor(public uid: string, public displayName: string) {}
}

User Authentication Actions in Redux

We are going to reuse actions when possible - specifically AUTHENTICATED and NOT_AUTHENTICATED. Depending on the complexity of app login process, you many need more actions to handle other auth login methods and processes.

user.actions.ts

import { Action } from '@ngrx/store';
import { User } from '../models/user.model';

export const GET_USER = '[Auth] Get user';
export const AUTHENTICATED = '[Auth] Authenticated';
export const NOT_AUTHENTICATED = '[Auth] Not Authenticated';

export const GOOGLE_LOGIN = '[Auth] Google login attempt';
export const LOGOUT = '[Auth] Logout';

export const AUTH_ERROR = '[Auth] Error';


/// Get User AuthState

export class GetUser implements Action {
readonly type = GET_USER;
constructor(public payload?: any) {}
}

export class Authenticated implements Action {
readonly type = AUTHENTICATED;
constructor(public payload?: any) {}
}

export class NotAuthenticated implements Action {
readonly type = NOT_AUTHENTICATED;
constructor(public payload?: any) {}
}

export class AuthError implements Action {
readonly type = AUTH_ERROR;
constructor(public payload?: any) {}
}

/// Google Login Actions

export class GoogleLogin implements Action {
readonly type = GOOGLE_LOGIN;
constructor(public payload?: any) {}
}

/// Logout Actions

export class Logout implements Action {
readonly type = LOGOUT;
constructor(public payload?: any) {}
}

export type All
= GetUser
| Authenticated
| NotAuthenticated
| GoogleLogin
| AuthError
| Logout;

User Reducer

The reducer function will combine update the user details on the data store based on changes to the Firebase auth state.

user.reducer.ts

We can use our user class to create a default GUEST user with a null uid. The reducer will also toggle the loading state for actions that await asynchronous data.

import * as userActions from '../actions/user.actions';
import { User } from '../models/user.model';

export type Action = userActions.All;

const defaultUser = new User(null, 'GUEST');

/// Reducer function
export function userReducer(state: User = defaultUser, action: Action) {
switch (action.type) {

case userActions.GET_USER:
return { ...state, loading: true };

case userActions.AUTHENTICATED:
return { ...state, ...action.payload, loading: false };

case userActions.NOT_AUTHENTICATED:
return { ...state, ...defaultUser, loading: false };

case userActions.GOOGLE_LOGIN:
return { ...state, loading: true };

case userActions.AUTH_ERROR:
return { ...state, ...action.payload, loading: false };

case userActions.LOGOUT:
return { ...state, loading: true };

}
}

User Effects

Now we can get to the fun part - Ngrx Effects. There are three basic problems we need to solve.

  1. Determine if user is logged in or not
  2. Login/Signup with Google OAuth
  3. Logout

Here’s a visual flow of the process using the Redux Chrome plugin.

Using Google OAuth in Firebase with Ngrx

0. Basic Effects Skeleton

First, let’s import the necessary files and decorate our effects class with @Injectable.

import { Injectable }                 from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { User } from '../models/user.model';

import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/of';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/delay';


import * as userActions from '../actions/user.actions';
export type Action = userActions.All;


@Injectable()
export class UserEffects {

constructor(private actions: Actions, private afAuth: AngularFireAuth) {}

/// effects go here
}

1. Get User Effect

First, we need a way to get the user from Firebase if they are already authenticated. We don’t want the user to have to re-authenticate every time they visit the app. This effect will check the auth state in firebase, then update the data store accordingly.

@Effect()
getUser: Observable<Action> = this.actions.ofType(userActions.GET_USER)

.map((action: userActions.GetUser) => action.payload )
.switchMap(payload => this.afAuth.authState )
.delay(2000) // delay to show loading spinner, delete me!
.map( authData => {
if (authData) {
/// User logged in
const user = new User(authData.uid, authData.displayName);
return new userActions.Authenticated(user);
} else {
/// User not logged in
return new userActions.NotAuthenticated();
}

})
.catch(err => Observable.of(new userActions.AuthError()) );

2. Login User with Google OAuth Effect

Now we need a way for new users to signin/signup. Firebase makes this really easy with Google OAuth. The magic happens in googleLogin method, which triggers the OAuth popup window. Once authenticated, we can simple re-trigger the GET_USER action to update the data store.

@Effect()
login: Observable<Action> = this.actions.ofType(userActions.GOOGLE_LOGIN)

.map((action: userActions.GoogleLogin) => action.payload)
.switchMap(payload => {
return Observable.fromPromise( this.googleLogin() );
})
.map( credential => {
// successful login
return new userActions.GetUser();
})
.catch(err => {
return Observable.of(new userActions.AuthError({error: err.message}));
});


private googleLogin(): firebase.Promise<any> {
const provider = new firebase.auth.GoogleAuthProvider();
return this.afAuth.auth.signInWithPopup(provider);
}

3. Logout User Effect

Logout is basically identical to login, except the NOT_AUTHENTICATED action is triggered.

@Effect()
logout: Observable<Action> = this.actions.ofType(userActions.LOGOUT)

.map((action: userActions.Logout) => action.payload )
.switchMap(payload => {
return Observable.of( this.afAuth.auth.signOut() );
})
.map( authData => {
return new userActions.NotAuthenticated();
})
.catch(err => Observable.of(new userActions.AuthError({error: err.message})) );

Logging in on the Frontend

With all of the Ngrx login in place, it’s time update the app component to actually give the user a way to login.

app.component.ts

First, add the user to the AppState interface. Second, dispatch the the GET_USER action during the OnInit lifecycle hook to determine if the user is already logged in. Last, create a couple event handlers so the user can click login and logout buttons.

import { Component, OnInit } from '@angular/core';

import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

import { User } from './models/user.model';
import * as userActions from './actions/user.actions';

interface AppState {
user: User;
}

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

user$: Observable<User>;

constructor(private store: Store<AppState>) {}

ngOnInit() {
this.user$ = this.store.select('user');

this.store.dispatch(new userActions.GetUser());
}

googleLogin() {
this.store.dispatch(new userActions.GoogleLogin());
}

logout() {
this.store.dispatch(new userActions.Logout());
}

}

app.component.html

And finally, let’s put this together in the HTML.

<div *ngIf="user$ | async as user">



<h1>Howdy, {{ user.displayName }}</h1>
<h4>{{ user.uid }}</h4>

<button *ngIf="!user.uid"
(click)="googleLogin()"
[class.is-loading]="user.loading">

Sign In with Google
</button>



<button *ngIf="user.uid"
(click)="logout()">

Logout
</button>
</div>

Up Next

Let me know what Ngrx topics you want to see covered next. I hope to cover Ngrx file uploads and routing in the near future.