Token Authentication Using Laravel + iOS - Part II

Building a Hybrid iOS App

This is a follow-up to our post on Token Authentication Using Laravel + iOS. In our previous post we covered the backend API and how to configure Laravel token authentication. In this post we will build a hybrid iOS app using Apache Cordova along with the Onsen UI framework.

Our goal was to build a simple iOS to-do app that would connect to our backend API using token authentication (via JSON). This post will show you how to set up Cordova, Onsen UI and create the necessary templates for our to-do app. Let's get started.


Prerequisites

Before we start building the hybrid app we need to setup a base installation of Cordova + Onsen and configure our API server.

1. Install Cordova + Onsen

There is a fantastic guide on Onsen's site that walks you through this process. In summary you'll need to do the following:

  1. Install NodeJS and NPM on your machine. We recommend using Brew for this by simply running brew install node and brew install npm.
  2. Run the command sudo npm install -g cordova to install the Cordova package globally.
  3. Download the master-detail template from the Onsen site.
  4. Navigate to the unzipped folder and run the command cordova platform add ios to add the iOS SDK to the folder.
  5. Ensure you have Xcode Command Line Tools by running xcode-select --install and ios-sim by running sudo npm install -g ios-sim.
  6. You can now run cordova emulate to open the simulator and preview your app at any time.

2. Enable CORS

You need to enable CORS on your API server to allow remote access on the GET, POST and DELETE methods. There are different configuration requirements depending on the HTTP server your using.

Below are configurations for wide open CORS on Nginx and Apache. Use with caution and discretion. It's recommended that you understand what you are doing.

Nginx

Place this block of code inside your Nginx server block and then restart the Nginx service using service nginx restart on Ubuntu. You may optionally need to install the Nginx extras for support on the more_set_headers. You can do that by running this command on Ubuntu: apt-get install nginx-extras.

more_set_headers 'Access-Control-Allow-Origin: $http_origin';  
more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE, HEAD';  
more_set_headers 'Access-Control-Allow-Credentials: true';  
more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept,Authorization';

location / {  
    if ($request_method = 'OPTIONS') {
        more_set_headers 'Access-Control-Allow-Origin: $http_origin';
        more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE, HEAD';
        more_set_headers 'Access-Control-Max-Age: 1728000';
        more_set_headers 'Access-Control-Allow-Credentials: true';
        more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept,Authorization';
        more_set_headers 'Content-Type: text/plain; charset=UTF-8';
        more_set_headers 'Content-Length: 0';
        return 204;
    }
    try_files $uri $uri/ /index.php?$query_string;
}

Apache

On Apache it's a much easier. Simply add this line to your .htaccess file and restart Apache using service apache2 reload on Ubuntu.

Header set Access-Control-Allow-Origin "*"  



Views

Now that we have a base app up and running we need to create the following views for our app:

  • Login
  • Home
  • Add Todo

Please note, all our views will live within the www/index.html file.

We want to clean this file up a little before starting. Lets remove the <style> block in the head and the entire <body> from the starter template. You'll be left with something like this:

<!DOCTYPE html>  
<!-- CSP support mode (required for Windows Universal apps): https://docs.angularjs.org/api/ng/directive/ngCsp -->  
<html lang="en" ng-app="app" ng-csp>  
<head>  
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="mobile-web-app-capable" content="yes" />

    <!-- JS dependencies (order matters!) -->
    <script src="scripts/platformOverrides.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/onsen/js/onsenui.js"></script>

    <!-- CSS dependencies -->
    <link rel="stylesheet" href="lib/onsen/css/onsenui.css" />
    <link rel="stylesheet" href="lib/onsen/css/onsen-css-components-blue-basic-theme.css" />

    <!-- CSP support mode (required for Windows Universal apps) -->
    <link rel="stylesheet" href="lib/angular/angular-csp.css" />


    <!-- --------------- App init --------------- -->
    <script src="js/app.js"></script>

    <title>Onsen UI - Todo App</title>

</head>  
<body>  
    <!-- Cordova reference -->
    <script src="cordova.js"></script>
    <script src="scripts/index.js"></script>
    <!-- -->
</body>  
</html>  


Login View

The login view will contain the following elements:

  • A basic form (email and password) with a submit button.
  • A modal with a loading message: Please wait. Connecting....
  • A button in the toolbar with a loading icon, which shows when we are fetching data from the API. We enable this using the function ng-show="isFetching".

Each view will eventually have its own Angular controller. For this view we have attached a LoginController which will execute at the beginning via the ng-init="init()" function. You will see later in this post how we are going to run this function.

The login view, in conjunction with the controller, will check if we have a token stored in local storage and verify if this token is still valid. If the token is valid it will pass the user to the Home template where the user will see their todos. If the token is invalid it will show the user a login form.

Place this code directly below the cordova-reference in the body.

<ons-navigator var="navi">  
    <ons-page ng-controller="LoginController" ng-init="init()">
        <ons-toolbar>
            <div class="center">Todo</div>
            <div class="right">
                <ons-toolbar-button ng-show="isFetching">
                    <ons-icon icon="ion-load-c" spin="true">
                </ons-toolbar-button>
            </div>
        </ons-toolbar>

        <div class="login-form" ng-hide="isLogged">

            <input type="email" class="text-input--underbar" ng-model="email" placeholder="Email" >
            <input type="password" class="text-input--underbar" ng-model="password" placeholder="Password">                
            <br><br>
            <ons-button modifier="large" class="login-button" ng-click="login()">Log In</ons-button>             

        </div>

        <ons-modal var="loginModal">
            <ons-icon icon="ion-loading-c" spin="true"></ons-icon>
            <br><br>
            Please wait.<br>Connecting...
        </ons-modal> 

    </ons-page>        
</ons-navigator>  


Home View

The home view will use the default Onsen UI list style to render the todos. In addition to listing the todo's we will include a few more UI features:

  • A button to add todos.
  • A loading icon that shows while todos are being added.
  • A swipe to delete interaction. This is achieved by using the <ons-carousel> tag. This tag allows us to include 2 slides - one for the todo and another for the delete button.
  • A modal to indicate when a todo is being deleted.

Add this block of code directly below your login view code.

<ons-template id="home.html">  
    <ons-page ng-controller="HomeController" ng-init="init()">
        <ons-toolbar>
            <div class="left">
                <ons-toolbar-button ng-show="isFetching">
                    <ons-icon icon="ion-load-c" spin="true">
                </ons-toolbar-button>
            </div>
            <div class="center">Todo List</div>
            <div class="right">
                <ons-toolbar-button ng-click="showAddTodo()">
                    <ons-icon icon="ion-compose">
                </ons-toolbar-button>
            </div>
        </ons-toolbar>

        <ons-list modifier="todo-list chevron">
            <ons-list-item class="item" ng-repeat="item in items">
                <ons-carousel style="height: 44px; width: 100%" swipeable initial-index="0" auto-scroll>
                    <ons-carousel-item>
                        <ons-button modifier="large--quiet" class="todo-btn">{{item.title}}</ons-button>
                    </ons-carousel-item>
                    <ons-carousel-item>
                        <ons-button modifier="large" class="delete-btn" ng-click="deleteTodo(item.id)">Delete</ons-button>
                    </ons-carousel-item>
                </ons-carousel>
            </ons-list-item>
        </ons-list>

        <ons-modal var="deleteModal">
            <ons-icon icon="ion-loading-c" spin="true"></ons-icon>
            <br><br>
            Deleting Todo...
        </ons-modal> 

    </ons-page>

</ons-template>


Add Todo View

This view contains a simple form and submit button for adding a new todo item. It will also contain a modal to let the user know when a todo is being saved to the API. This view will work with the addTodoController controller to create new todo items.

Add this block of code directly below your home view code.

<ons-template id="add.html">  
    <ons-page ng-controller="addTodoController">

        <ons-toolbar>
            <div class="left"><ons-back-button>Back</ons-back-button></div>
            <div class="center">Add Todo</div>
        </ons-toolbar>

        <ons-list modifier="inset" style="margin-top: 10px">
            <ons-list-item>
                <input type="text" class="text-input text-input--transparent" ng-model="todo" placeholder="Task" style="width: 100%">
            </ons-list-item>
        </ons-list>

        <div style="padding: 10px 9px">
            <ons-button modifier="large" style="margin: 0 auto;" ng-click="submitTodo()">Add Todo</ons-button>
        </div>

        <ons-modal var="todoModal">
            <ons-icon icon="ion-loading-c" spin="true"></ons-icon>
            <br><br>
            Adding Todo...
        </ons-modal> 

    </ons-page>
</ons-template>  



AngularJS Controllers

At this point you can run cordova emulate and you can preview the app. There isn't much to see, but if you did everything correctly up to this point you should see a simple screen with a "ToDo" title.

In order to bring the app to life we will need an AngularJS controller for each view. We will need to create the following controllers:

  • LoginController
  • HomeController
  • addTodoController

All controller related logic will go in the www/js/app.js file. Before starting, let's strip some of the default code so we end up with the following:

(function(){
  'use strict';
})();


Next, we need to define some variables that will help simplify the process for us. The api variable will be our API endpoint, and the Object.toparams function will convert parameters to JSON in our POST methods. Add this within the main function, after the use strict declaration. Be sure to update the api variable.

  var api = 'http://yourserver.com/api/';
  var module = angular.module('app', ['onsen']);

  Object.toparams = function ObjecttoParams(obj) {
    var p = [];
    for (var key in obj) {
      p.push(key + '=' + encodeURIComponent(obj[key]));
      console.log(obj[key]);
    }
    return p.join('&');
  };


LoginController

Our login controller has two main functions: init() and login().

The init() funtion will run on load and check if we have a key called token in local storage. If the token exists the controller will attempt to validate it by sending a GET request to the validate_token route. If the token is valid, it will redirect the user to the home view. If not, it show the login form.

The login() function will pass the email and password input values as a POST method to the login route. If the credentials are valid, the API will return a token which is stored in the local storage. The user will then be redirected to the home view. If the credentials are invalid, the controller will return an error and show the login form again.

Place this block of code below your ObjecttoParams function.

module.controller('LoginController', function($scope, $http) {

  $scope.isLogged = true;
  $scope.isFetching = false;
  $scope.email = '';
  $scope.password = '';

  $scope.init = function(){
    $scope.isFetching = true;

    if (window.localStorage.getItem("token") !== null ) {
      $http.get(api+'validate_token?token='+window.localStorage.getItem("token")).success(
        function(response) {
          console.log(response);
          if( typeof(response.status) !== "undefined"){
            $scope.isLogged = true;
            $scope.isFetching = false;
            $scope.navi.pushPage('home.html');
          } else {
            $scope.isLogged = false;
            $scope.isFetching = false;
          }
        }
      ).error(function(response){
        $scope.isFetching = false;
      });

    } else {    
      $scope.isLogged = false;
      $scope.isFetching = false;
    } 
  };

  $scope.login = function(){
    if($scope.email==='' || $scope.password===''){     
      ons.notification.alert({message: "You can't leave any fields empty"});
    } else {   
      loginModal.show();
      var data = {
        email: $scope.email,
        password: $scope.password
      };

      $http({
        url: api+'login',
        method: 'POST',
        data: Object.toparams(data),
        headers: {'Content-Type': 'application/x-www-form-urlencoded'}
      }).success(function (response) {
        console.log(response);
        if( typeof(response.token) !== "undefined"){
          window.localStorage.setItem("token", response.token);
          loginModal.hide();
          $scope.navi.pushPage('home.html');
        } else {
          loginModal.hide();
          ons.notification.alert({message: 'Your username/password was incorrect, please try again.'});
        }
        }).error(function(response){
          loginModal.hide();
          $scope.isFetching = false;
      });
    }
  };
});


HomeController

The home controller has 3 main functions: init(), showAddTodo() and deleteTodo().

The init() function will run on load and send a GET request to the todo route (with the token from our local storage). The API will validate the token and respond with a JSON object containing all the todo items for the user. We will assign this list of todos to the $scope.items variable, and include it on the ng-repeat in the home view.

The showAddTodo() function redirects users to the add todo view.

The deleteTodo(), when triggered, will send a DELETE request to the selected Todo upon a successful response it will send you back to the home.html page and also restart the init() function so the app can grab the fresh list of Todos from the API.

We also included an event called rootScope:refreshtodos we are gonna make use of this one in the next controller, when I trigger this event in the next controller I will just refresh the list of Todos by running the init() function again.

Place this block of code below our LoginController.

module.controller('HomeController', function($scope, $http, $rootScope) {  
  $scope.isFetching = true;
  $scope.items = [];

  $scope.init = function(){

    $http.get(api+'todo?token='+window.localStorage.getItem("token")).success(
      function(response) {
        console.log(response);
        if( typeof(response.status) !== "undefined"){

          $scope.isFetching = false;
          $scope.items = response.todos;

        } else {
          ons.notification.alert({message: 'There was an error connecting to the API.'});
          $scope.isFetching = false;
        }
      }
    ).error(function(response){
      ons.notification.alert({message: 'There was an error connecting to the API.'});
      $scope.isFetching = false;
    });

  };

  $scope.showAddTodo = function(index) {
    $scope.navi.pushPage('add.html');
  };

  $scope.deleteTodo = function(id){
    deleteModal.show();

    $http({
      url: api+'todo/'+id+'?token='+window.localStorage.getItem("token"),
      method: 'DELETE',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'}
    }).success(function (response) {
      console.log(response);
      if( typeof(response.status) !== "undefined"){
        deleteModal.hide();
        $scope.init();
      } else {
        deleteModal.hide();
        ons.notification.alert({message: 'There was an error connecting to the API, please try again.'});
      }
    });
  };

  $rootScope.$on('rootScope:refreshtodos', function (event, data) {
    $scope.init();
  });

});


addTodoController

This controller has one function: submitTodo() which will send a POST request to the todo route. If we receive a successful response, the controller will broadcast the event to rootScope:refreshtodos so the home view can refresh the todo list. It will then redirect the user back to the home view.

Place this block of code below your HomeController.

module.controller('addTodoController', function($scope, $http, $rootScope) {

  $scope.isFetching = false;
  $scope.todo = '';

  $scope.submitTodo = function(){
    if($scope.todo===''){    
      ons.notification.alert({message: "You can't leave the field empty"});
    } else {

      todoModal.show();
      var data = {
        title: $scope.todo
      };

      $http({
        url: api+'todo?token='+window.localStorage.getItem("token"),
        method: 'POST',
        data: Object.toparams(data),
        headers: {'Content-Type': 'application/x-www-form-urlencoded'}
      }).success(function (response) {
        console.log(response);
        if( typeof(response.status) !== "undefined"){
          todoModal.hide();
          $rootScope.$broadcast('rootScope:refreshtodos', 'RefreshTodos');
          $scope.navi.popPage();
        } else {
          todoModal.hide();
          ons.notification.alert({message: 'There was an error connecting to the API, please try again.'});
        }
      });
    }
  };
});



Conclusion

You may now test the app by running cordova emulate from the root project folder. At this point feel free to customize the app styles or add additional features.

Distribution

Once everything is running you can prepare the app for distribution using three simple steps:

  1. Run the command cordova prepare to prepare the Xcode files.
  2. Open the platforms/ios/Onsen UI Project.xcodeproj file in Xcode.
  3. Generate an .ipa file by navigating to Project -> Archive.

Repository

You can download this project directly from our Github repo Hybrid Onsen App.

Thanks for reading! If you have any comments or questions please leave them below.