Note that I am writing this when Ionic 2 was in RC2 status. I expect that things may change a bit between now and the final release, but probably not too much.

Earlier this year I wrote a blog post demonstrating the newly released authentication system for Ionic applications: "Testing the New Ionic User Service". I thought it might be fun to work up a new example using Ionic 2. While this example doesn't do anything the docs don't, it does put them all together in one "complete" application. I thought it might be useful to see the various APIs put together into a demo that shows:

  • Checking if the user is logged in on start up
  • Moving the user to a login page if not...
  • And moving them to a home page if so
  • Support logout and send the user back to the login page

The full source code for what I'm about to demonstrate may be found here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/ionicauth2

I began by creating a new Ionic 2 application using the "blank" template. This gave me an app with one page, home. My first task was to add logic to check for existing login and route the user appropriately. I did this within app.component.ts:


import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';

import { HomePage } from '../pages/home/home';
import { LoginPage } from '../pages/login/login';
import { Auth } from '@ionic/cloud-angular';

@Component({
  template: `<ion-nav [root]="rootPage"></ion-nav>`
})
export class MyApp {
  rootPage;

  constructor(platform: Platform, public auth:Auth) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();
      Splashscreen.hide();

      if(this.auth.isAuthenticated()) {
        this.rootPage = HomePage;
      } else {
        this.rootPage = LoginPage;
      }

    });
  }
}

I don't know if I need that rootPage declaration there. I kind of think I don't, but I've left it alone for now. The real important bit is this.auth.isAuthenticated(). The Auth system automatically caches your login for you (and that's something you can disable) so you don't have to worry about adding your own storage for logins.

In case you're curious about that - the Auth system uses LocalStorage when testing in the browser:

Screenshot removed

You'll notice the password is in plain text and they are also storing a third value as well. Outside of the other one, I feel like it's a mistake to use email and password as those are values I could see using myself. (I filed an [issue](https://github.com/driftyco/ionic-cloud-angular/issues/30)).

Sorry folks! I was totally wrong about this. I had written my own code to store auth info in LocalStorage but removed that code when I saw that Ionic was handling for me. However, I forgot that fact when I used devtools to look at my localstorage. Ionic does store a JWT token - but definitely not the password!

For my login, I decided to use a simple UI that defaults to login:

Login

Clicking register simply hides the initial form and shows the (slightly larger) registration form:

Register

I'm not entirely sold on that UX, but it gets the job done. Let's take a look at the HTML behind this:


<!--
  Generated template for the Login page.

  See http://ionicframework.com/docs/v2/components/#navigation for more info on
  Ionic pages and navigation.
-->
<ion-header>

  <ion-navbar>
    <ion-title>Login/Register</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  <div *ngIf="showLogin">
    <ion-item>
      <ion-input type="email" placeholder="Email" [(ngModel)]="email"></ion-input>
    </ion-item>

    <ion-item>
      <ion-input type="password" placeholder="Password" [(ngModel)]="password"></ion-input>
    </ion-item>
  </div>

  <div *ngIf="!showLogin">

    <ion-item>
      <ion-input type="text" placeholder="Name" [(ngModel)]="name"></ion-input>
    </ion-item>

    <ion-item>
      <ion-input type="email" placeholder="Email" [(ngModel)]="email"></ion-input>
    </ion-item>

    <ion-item>
      <ion-input type="password" placeholder="Password" [(ngModel)]="password"></ion-input>
    </ion-item>
  </div>

  <button ion-button color="primary" full (click)="doLogin()">Login</button>
  <button ion-button color="primary" full (click)="doRegister()">Register</button>

</ion-content>

Nothing crazy here except the 2 divs with ngIf controlling their visibility. The real fun is the code behind this:


import { Component } from '@angular/core';
import { NavController, AlertController, LoadingController } from 'ionic-angular';
import { Auth, User, UserDetails, IDetailedError } from '@ionic/cloud-angular';
import { HomePage } from '../home/home';

@Component({
  selector: 'page-login',
  templateUrl: 'login.html'
})
export class LoginPage {

  showLogin:boolean = true;
  email:string = '';
  password:string = '';
  name:string = '';

  constructor(public navCtrl: NavController, public auth:Auth, public user: User, public alertCtrl: AlertController, public loadingCtrl:LoadingController) {}

  ionViewDidLoad() {
    console.log('Hello LoginPage Page');
  }

  /*
  for both of these, if the right form is showing, process the form,
  otherwise show it
  */
  doLogin() {
    if(this.showLogin) {
      console.log('process login');

      if(this.email === '' || this.password === '') {
        let alert = this.alertCtrl.create({
          title:'Register Error', 
          subTitle:'All fields are rquired',
          buttons:['OK']
        });
        alert.present();
        return;
      }     

      let loader = this.loadingCtrl.create({
        content: "Logging in..."
      });
      loader.present();
      
      this.auth.login('basic', {'email':this.email, 'password':this.password}).then(() => {
        console.log('ok i guess?');
        loader.dismissAll();
        this.navCtrl.setRoot(HomePage);        
      }, (err) => {
        loader.dismissAll();
        console.log(err.message);

        let errors = '';
        if(err.message === 'UNPROCESSABLE ENTITY') errors += 'Email isn\'t valid.<br/>';
        if(err.message === 'UNAUTHORIZED') errors += 'Password is required.<br/>';

        let alert = this.alertCtrl.create({
          title:'Login Error', 
          subTitle:errors,
          buttons:['OK']
        });
        alert.present();
      });
    } else {
      this.showLogin = true;
    }
  }

  doRegister() {
    if(!this.showLogin) {
      console.log('process register');

      /*
      do our own initial validation
      */
      if(this.name === '' || this.email === '' || this.password === '') {
        let alert = this.alertCtrl.create({
          title:'Register Error', 
          subTitle:'All fields are rquired',
          buttons:['OK']
        });
        alert.present();
        return;
      }

      let details: UserDetails = {'email':this.email, 'password':this.password, 'name':this.name};
      console.log(details);
      
      let loader = this.loadingCtrl.create({
        content: "Registering your account..."
      });
      loader.present();

      this.auth.signup(details).then(() => {
        console.log('ok signup');
        this.auth.login('basic', {'email':details.email, 'password':details.password}).then(() => {
          loader.dismissAll();
          this.navCtrl.setRoot(HomePage);
        });

      }, (err:IDetailedError<string[]>) => {
        loader.dismissAll();
        let errors = '';
        for(let e of err.details) {
          console.log(e);
          if(e === 'required_email') errors += 'Email is required.<br/>';
          if(e === 'required_password') errors += 'Password is required.<br/>';
          if(e === 'conflict_email') errors += 'A user with this email already exists.<br/>';
          //don't need to worry about conflict_username
          if(e === 'invalid_email') errors += 'Your email address isn\'t valid.';
        }
        let alert = this.alertCtrl.create({
          title:'Register Error', 
          subTitle:errors,
          buttons:['OK']
        });
        alert.present();
      });
     
    } else {
      this.showLogin = false;
    }
  }

}

Let's begin with doRegister. This handles either showing the register form (if hidden) or performing registration. For registration, I do a quick sanity check on the form fields (I know Angular 2 has a fancier way of working with forms, but I don't really know it yet) and if all fields have something in them, I then create a new instance of UserDetails and pass it to the signup method of auth. If it succeeds, awesome. I then log the user in and then send them to the home page.

If it fails, I look at the nice errors and create English versions of them to present to the user. Notice too I'm using the Ionic loading and alert controls as well. Also make note of this.navCtrl.setRoot. I use that instead of push because I do not want to provide a link back to the login page. Basically I'm saying, "This is your new home. Love it. Make peace with it. Share a good beer..."

Now take a look at login. For the most part it follows the same idea - check the form, login, then handle the result. But oddly, errors with login throw exceptions and not "pretty" errors with simple codes like registration. I'm sure there is a good reason for that, but I wish the docs had made it more clear. I literally had copied the error handling code from doRegister with the expectation that only the possible results would change and that wasn't so. For example, a bad email address throws UNPROCESSABLE ENTITY, which, ok, I guess means something, but it isn't terribly obvious. A failed login just returns UNAUTHORIZED, which is a bit more sensible perhaps.

The final part of this demo is the home page. All I do is print out the user's name and provide a logout button.

Home

Here's the HTML:


<ion-header>
  <ion-navbar>
    <ion-title>
      Home
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only (click)="logout()">
        <ion-icon name="exit"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
    <h2>Welcome back, {{ user.details.name }}</h2>
</ion-content>

Nothing too special there, although I should point out that the name value of a user is optional. And here is the code behind the view:


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

import { NavController } from 'ionic-angular';
import { Auth, User } from '@ionic/cloud-angular';
import { LoginPage } from '../login/login';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(public navCtrl: NavController, public user:User, public auth:Auth) {
    console.log(user);
    
  }

  logout() {
    this.auth.logout();
    this.navCtrl.setRoot(LoginPage);
  }

}

Literally the only thing interesting here is logout and frankly, that's not terribly interesting either.

But all in all - I now have a complete Auth demo that actually handles the views and such. I built this just so I could write another blog post but that will be for next week. As always, I hope this helps. I've been kind of avoiding blogging much on Ionic 2 with it changing so much over the year, but with today's release of RC2, it's time to get back on the Ionic Love Train again.

"Cat Train"

What I imagine the "Ionic Love Train" would look like...