Monday, August 15, 2016

Build an iOS app with Push Notifications using Ionic Framework_part 1


Introduction

Hybrid apps are gaining popularity. The Gartner research firm predicts that by 2016, hybrid apps will constitute the majority of mobile apps. So it is good time to learn some modern technologies for building hybrid apps.

In this tutorial we are going to see how it is possible to build Cordova application with push notifications based on the Ionic Framework and ngCordova created by the Ionic team.

Ionic uses AngularJS, so it is assumed you are familiar with it.

What are we building in this tutorial?

Unfortunately, there are a lot of bad things happening every day. So we are going to create an iOS application to show some good news. We will also create own server for sending push notifications using apn, based on Node.js and MongoDB with a simple form to create a new post.

The name of the application will be PushNews, but of course you can use your own.


Prerequisites
  • Mac OS X
  • iOS Developer Program membership
  • Node.js/npm installed on your mac
  • MongoDB
For deploying we will use Heroku and Mongolab, so you need to have accounts there or you can use your own hosting instead.

Install Ionic

Now we should install/update CLI for Cordova and Ionic, to have an opportunity to create new apps, add cordova plugins, etc.

Run this command in your Terminal:
  1. $ npm install -g ionic cordova
App Development

Creating Ionic app

For creating and serving basic Ionic app with tabs starter template, run
  1. $ ionic start pushNews tabs
  2. $ cd pushNews
  3. $ ionic serve

Installing additional libs and setting up routes
  1. $ bower install --save ngCordova angular-local-storage
Include ng-cordova.min.js and angular-local-storage.min.js in the index.html
  1. <!-- index.html -->
  2. ...
  3. <script src="lib/angular-local-storage/dist/angular-local-storage.min.js"></script>
  4. <script src="lib/ngCordova/dist/ng-cordova.min.js"></script>
  5. <script src="cordova.js"></script>
  6. ...
Inject as an Angular dependency:
  1. // www/js/app.js
  2. ...
  3. angular.module('app', ['ngCordova', 'LocalStorageModule'])
  4. ...
We need to change routes now. There will be four of them:
  • Login page
  • All news list
  • News view page
  • User profile page
  1. // www/js/app.js
  2. ...
  3. .config(function ($stateProvider, $urlRouterProvider) {
  4.   $urlRouterProvider.otherwise('/login');
  5.   $stateProvider
  6.     .state('login', {
  7.       url: '/login',
  8.       templateUrl: 'templates/login.html',
  9.       controller: 'LoginCtrl'
  10.     })
  11.     .state('tab', {
  12.       abstract: true,
  13.       templateUrl: "templates/tabs.html"
  14.     })
  15.     .state('tab.news', {
  16.       url: '/news',
  17.       views: {
  18.         'tab-news': {
  19.           templateUrl: 'templates/tab-news.html',
  20.           controller: 'NewsCtrl'
  21.         }
  22.       }
  23.     })
  24.     .state('tab.details', {
  25.       url: '/news/:id',
  26.       views: {
  27.         'tab-news': {
  28.           templateUrl: 'templates/details.html',
  29.           controller: 'DetailsCtrl'
  30.         }
  31.       }
  32.     })
  33.     .state('tab.profile', {
  34.       url: '/profile',
  35.       views: {
  36.         'tab-profile': {
  37.           templateUrl: 'templates/tab-profile.html',
  38.           controller: 'ProfileCtrl'
  39.         }
  40.       }
  41.     });
  42. });
Building the Server
  Let's create Express server. I will use Heroku to host it. Follow official Getting Started if you are not familar with Heroku. As an alternative you can use for development your localhost with ngrok.
  1. // server.js
  2. var express = require('express');
  3. var app = express();
  4. var bodyParser = require('body-parser');

  5. require('./config')(app);
  6. require('./models')(app);

  7. var config = app.get('config');

  8. app.use(function (req, res, next) {
  9.   res.header('Access-Control-Allow-Credentials', true);
  10.   res.header('Access-Control-Allow-Origin', req.headers.origin);
  11.   res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  12.   res.header(
  13.     'Access-Control-Allow-Headers', 
  14.     'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
  15.   );
  16.   if ('OPTIONS' === req.method) {
  17.     res.status(200).end();
  18.   } else {
  19.     next();
  20.   }
  21. });

  22. app.use(bodyParser.json());
  23. app.use(bodyParser.urlencoded({
  24.   extended: true
  25. }));

  26. var router = express.Router();
  27. app.set('router', router);
  28. app.use(router);

  29. var http = require('http');
  30. http.createServer(app)
  31.   .listen(config.PORT, function () {
  32.     console.log('app start on port ' + config.PORT);
  33.   });

  34. require('./routes')(app);
Models and routes we will describe later.

Authorization and Push Notifications

There are multiple ways to manage auth process in Ionic apps. We will use the next one: user login with twitter, then we store returned data in localStorage and remove it on logout. If user exists in localStorage he is logged in, if not, we will redirect him to login page. Don't forget to add CSRF or other protection for the real app.

Login with Twitter

To use twitter we must also add sha1 library:
  1. $ bower install --save jsSHA
  1. <!-- www/index.html -->
  2. ...
  3. <script src="lib/jsSHA/src/sha1.js"></script>
  4. ...
Install inAppBrowser plugin:
  1. $ cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-inappbrowser.git
Also you have to create your own twitter app on Twitter App Management to get keys.

A little earlier we have set login.html as template for the login state, let's create it:
  1. <!-- www/templates/login.html -->
  2. <div class="row row-center">
  3.   <div class="col">
  4.     <h2 class="text-center">Good News</h2>

  5.     <div class="padding-top">
  6.       <button class="button button-block button-calm" ng-click="twitter()">
  7.         <i class="icon ion-social-twitter"></i>&nbsp;Login with Twitter
  8.       </button>
  9.     </div>
  10.   </div>
  11. </div>
Login Controller:

It should be noticed, that any cordova plugin call should be wraped with the deviceready event. Or use $ionicPlatform.ready() available in Ionic.
  1. // www/js/controllers.js
  2. ...
  3. .controller('LoginCtrl', function ($scope, $state, $cordovaOauth, 
  4.                                    UserService, Config, $ionicPlatform,
  5.                                    $ionicLoading, $cordovaPush) {
  6.   if (UserService.current()) {
  7.     $state.go('tab.news');
  8.   }
  9.   $scope.twitter = function () {
  10.     $ionicPlatform.ready(function () {
  11.       $cordovaOauth.twitter(Config.twitterKey, Config.twitterSecret)
  12.         .then(function (result) {
  13.           $ionicLoading.show({
  14.             template: 'Loading...'
  15.           });
  16.           UserService.login(result).then(function (user) {
  17.             if (user.deviceToken) {
  18.               $ionicLoading.hide();
  19.               $state.go('tab.news');
  20.               return;
  21.             }

  22.             $ionicPlatform.ready(function () {
  23.               $cordovaPush.register({
  24.                 badge: true,
  25.                 sound: true,
  26.                 alert: true
  27.               }).then(function (result) {
  28.                 UserService.registerDevice({
  29.                   user: user, 
  30.                   token: result
  31.                 }).then(function () {
  32.                   $ionicLoading.hide();
  33.                   $state.go('tab.news');
  34.                 }, function (err) {
  35.                   console.log(err);
  36.                 });
  37.               }, function (err) {
  38.                 console.log('reg device error', err);
  39.               });
  40.             });
  41.           });
  42.         }, function (error) {
  43.           console.log('error', error);
  44.         });
  45.     });
  46.   };
  47. })
  48. ...
We have been using $cordovaOauth.twitter() to allow user to login with twitter in our app. This plugin will do for us all rough work, but you should check sources to understand that there is no server on the iPhone and we can't set up callback to redirect twitter back to our app like we could do this on the web. So we should open a new window, parce a token and close the window manually.
To send push notification to our users, we must know device token. We will receive it using $cordovaPush.register() function. Cordova plugin should be installed:
  1. $ cordova plugin add https://github.com/phonegap-build/PushPlugin.git
After getting user data, we send it to the server. Check this article by Jim Cooper to understand why we shouldn't do this in Controller. Let's create Service.
  1. // www/js/userService.js
  2. (function () {
  3.   function _UserService($q, config, $http, localStorageService, $state) {
  4.     var user;
  5.     function loginUser(post) {
  6.       var deferred = $q.defer();

  7.       $http.post(config.server + '/user/login', post)
  8.         .success(function (data) {
  9.           if (data.error || !data.user) {
  10.             deferred.reject(data.error);
  11.           }
  12.           localStorageService.set('user', data.user);
  13.           user = data.user;

  14.           deferred.resolve(data.user);
  15.         })
  16.         .error(function () {
  17.           deferred.reject('error');
  18.         });

  19.         return deferred.promise;
  20.     }

  21.     function logoutUser() {
  22.       localStorageService.remove('user');
  23.       user = null;
  24.       $state.go('login');
  25.     }

  26.     function currentUser() {
  27.       if (!user) {
  28.         user = localStorageService.get('user');
  29.       }
  30.       return user;
  31.     }

  32.     function registerDevice(putData) {
  33.       var deferred = $q.defer();

  34.       $http.put(config.server + '/user/registerDevice', putData)
  35.         .success(function (data) {
  36.           if (data.error || !data.user) {
  37.             deferred.reject(data.error);
  38.           }

  39.           localStorageService.set('user', data.user);
  40.           user = data.user;

  41.           deferred.resolve(data.user);
  42.         })
  43.         .error(function () {
  44.           deferred.reject('error');
  45.         });

  46.         return deferred.promise;
  47.     }

  48.     return {
  49.       login: loginUser,
  50.       logout: logoutUser,
  51.       current: currentUser,
  52.       registerDevice: registerDevice
  53.     };
  54.   }

  55.   function _ConfigService() {
  56.     return {
  57.       server: 'http://push-news.herokuapp.com',
  58.       twitterKey: 'your_twitter_key',
  59.       twitterSecret: 'your_twitter_secret'
  60.     };
  61.   }

  62.   _UserService.$inject = [
  63.     '$q', 'Config', '$http', 'localStorageService',
  64.     '$state', '$cordovaPush', '$ionicPlatform'
  65.   ];

  66.   angular.module('app.services')
  67.     .factory('UserService', _UserService)
  68.     .service('Config', _ConfigService);
  69. })();
Don't forget to include the new file in the index.html
  1. <!-- index.html -->
  2. ...
  3. <script src="js/userService.js"></script>
  4. ...
Server side (user)

We should add user model and routes for /user/login and /user/registerDevice on our server
  1. // models/users.js 
  2. var mongoose = require('mongoose');
  3. var Schema = mongoose.Schema;

  4. module.exports = function () {
  5.   'use strict';

  6.   var UsersSchema = new Schema({
  7.     socialId: {type: String, index: true, unique: true},
  8.     username: {type: String, unique: true},
  9.     deviceToken: {type: String, unique: true},
  10.     deviceRegistered: {type: Boolean, default: false},
  11.     createdAt: {type: Date, default: Date.now}
  12.   });

  13.   mongoose.model('Users', UsersSchema, 'Users');
  14. };
-----------------------------------------------------------------------------
  1. // models/users.js 
  2. var mongoose = require('mongoose');
  3. var Schema = mongoose.Schema;

  4. module.exports = function () {
  5.   'use strict';

  6.   var UsersSchema = new Schema({
  7.     socialId: {type: String, index: true, unique: true},
  8.     username: {type: String, unique: true},
  9.     deviceToken: {type: String, unique: true},
  10.     deviceRegistered: {type: Boolean, default: false},
  11.     createdAt: {type: Date, default: Date.now}
  12.   });

  13.   mongoose.model('Users', UsersSchema, 'Users');
  14. };
---------------------------------------------------------------------------
  1. // routes/user.js
  2. var mongoose = require('mongoose');
  3. var Users = mongoose.model('Users');

  4. module.exports = function (app) {
  5.   'use strict';

  6.   var router = app.get('router');

  7.   router.post('/user/login', function (req, res) {
  8.     var socialId = req.body.user_id;
  9.     var username = req.body.screen_name;

  10.     Users.findOne({socialId: socialId}).lean().exec(function (err, user) {
  11.       if (err) {
  12.         return res.json({error: err});
  13.       }

  14.       if (user) {
  15.         return res.json({error: null, user: user});
  16.       }

  17.       var newUser = new Users({
  18.         socialId: socialId,
  19.         username: username
  20.       });

  21.       newUser.save(function (err, user) {
  22.         if (err) {
  23.           return res.json({error: err});
  24.         }

  25.         res.json({user: user, error: null});
  26.       });
  27.     });
  28.   });

  29.   router.put('/user/registerDevice', function (req, res) {
  30.     var user = req.body.user;
  31.     var token = req.body.token;

  32.     Users.findByIdAndUpdate(user._id, {
  33.       $set: {
  34.         deviceToken: token,
  35.         deviceRegistered: true
  36.       }
  37.     }).lean().exec(function (err, user) {
  38.       if (err || !user) {
  39.         return res.json({error: err});
  40.       }

  41.       res.json({error: null, user: user});
  42.     });
  43.   });
  44. };
If you found this post interesting, follow and support us.
Suggest for you:

No comments:

Post a Comment