JianJye.com

This article has been updated for the latest version of Cordova and Ionic. Please refer to How to Set Up Apache Cordova Facebook Plugin and Parse in Ionic Framework V2

Previously when I wrote the article How to set up HelloJS in Ionic Framework with Angular UI Router (defunct), I didn’t know how much I was missing until I started a Phonegap / Ionic app development recently. When I started seriously look into implementing Facebook Login into my app together with a back-end, I discovered that there are a lot more details to be added.

So, I am writing this new article, with a new approach to Facebook Login in Cordova / Phonegap / Ionic app, that integrates with Parse.com as the backend provider. Like my previous article, it will utilize Angular UI Router as the state manager.

PS: I am writing this in a rush but hope that I have included enough information. Please leave a comment if additional clarification is required.

First thing first…

  • The example in this article is hosted at Github.
  • I assume you already have a Parse account.
  • I started this app on Ionic. Ionic is based on AngularJS and Phonegap. Even if you are not using Ionic / AngularJS on your Phonegap apps, the general idea should be applicable.
  • This article focuses on setting it up as a web plugin. I assume you would follow the guide to set up Phonegap’s Facebook Connect Plugin as native plugin on iOS and Android on your own.
    • However, I have tested it on iOS emulator and it works. After setting it up accordingly as native plugin of course.

Requirements:

  • Parse account
  • Angular UI Router
  • Facebook Conncet Plugin
  • Ionic (or AngularJS + Phonegap / Cordova)
  • Facebook App ID

Let’s start

Let’s start an app called massiveapp in Ionic based on a blank template.

$ ionic start massiveapp blank

It will create a lot of files and when it’s done, a basic AngularJS and Phonegap app structure will be created.

Add Facebook Connect Plugin

Install the Facebook Connect Plugin, then copy the Javascript file to our web folder. Please refer to their Github page for instructions on how to set up the plugin as native plugin.

$ cd massiveapp
$ ionic platform add ios
$ ionic platform add android
$ cordova -d plugin add https://github.com/Wizcorp/phonegap-facebook-plugin.git --variable APP_ID="123456789" --variable APP_NAME="myApplication"
$ cp plugins/com.phonegap.plugins.facebookconnect/www/js/facebookConnectPlugin.js www/lib

Parse Javascript SDK

Download Parse Javascript SDK from Parse.com and put it in massiveapp/www/lib.


Let’s go through this on a file-by-file basis.

On www/index.html

Include and initialize Parse SDK

First, within <head></head>, you will see the following codes:

<!-- ionic/angularjs js -->
<script src="lib/ionic/js/ionic.bundle.js"></script>

<!-- cordova script (this will be a 404 during development) -->
<script src="cordova.js"></script>

<!-- Add the code snippet here -->

<!-- your app's js -->
<script src="js/app.js"></script>

Include and initialize Parse SDK. Remember to replace PARSE_APP_ID and PARSE_JAVASCRIPT_KEY with your own values from your Parse account.

<!-- Parse -->
<script src="lib/parse-1.3.0.min.js"></script>
<script>
  Parse.initialize("PARSE_APP_ID", "PARSE_JAVASCRIPT_KEY");
</script>

Inject Angular UI Router

Look for <ionpane></ionpane>, and replace the contents. It should look like this now on:

<ion-pane>
  <div ui-view></div>
</ion-pane>

This way, the UI Router will manage the states and inject the necessary HTML codes into ui-view as necessary.

Include Facebook Connect Plugin

Right before </body>, or the body closing tag, add the following:

<div id="fb-root"></div>
<script src="lib/facebookConnectPlugin.js"></script>

The reason we put the Facebook Connect Plugin at the very last is to prevent a appendChild error, which you will see in your console if you put this plugin within <head></head>. Seems like it is required for fb-root to be loaded first before the plugin is added.

It is worth noting that this plugin itself contains the Facebook SDK, so there is no need to also include the Facebook Javascript SDK into your app.

Start Ionic

This will start the web server for your Ionic app, which allows you to look at your changes with your browser easily.

$ cd ..   # Go to your app's root directory
$ ionic serve

On www/home.html

This is our homepage, and it contains some very simple HTML that will be injected by the UI Router when needed.

On this page, we show a welcome message and a logout button. Only a logged in user can see this page.

<ion-header-bar class="bar-stable">
    <h1 class="title">Massive App</h1>
</ion-header-bar>
<ion-content>
    <div style="padding: 10px;">
        Welcome to MassiveApp!
        <button class="button button-block button-positive" ng-click="logout()">
            Log out from Facebook
        </button>
    </div>
</ion-content>

On www/login.html

This is our login page. Again, very simple HTML codes that will be injected by UI Router as needed.

It comes with a login button that will trigger the Facebook Login action when clicked.

<ion-header-bar class="bar-stable">
    <h1 class="title">Login</h1>
</ion-header-bar>
<ion-content>
    <div style="padding: 10px; height: 100%;">
          <button class="button button-block button-positive" ng-click="login()">
            Log in with Facebook
          </button>
    </div>
</ion-content>

On www/js/app.js

Here’s where we do all the heavy lifting and where I do all the explanations.

Add authentication checking for each state

Add $rootScope and $state into the .run() method.

.run(function($ionicPlatform, $rootScope, $state) {
    ...
}

Then, we are going to listen to every state change, and check for authentication. If a state requires authentication and the user is not logged in, then the user will be redirected to a state called login, which we will define shortly.

Here, Parse.User.current() is a Parse function that checks if a user is currently logged in.

.run(function($ionicPlatform, $rootScope, $state) {
    ...

    // UI Router Authentication Check
    $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
          if (toState.data.authenticate && !Parse.User.current()) {
            // User isn’t authenticated
            $state.transitionTo("login");
            event.preventDefault(); 
          }
    });
}

Add state configurations

Next, we will add the state configurations right after the .run() method.

Here we defined 3 states, ie root, home and login, which are accessible via localhost:8100, localhost:8100/#/home, localhost:8100/#/login. Because of the additional /#/ hash in the URL, I like to add a redirection for my root URL to my home URL, which you will see later.

Otherwise, the configurations are very straight forward. We define the controller and view file that we want Angular UI Router to inject for each state. We also define our own custom parameter under data.authenticate, which is used to define if authentication is required for each of the state.

Remember our authentication checking added above? toState.data.authenticate look for this parameter to determine if a state requires authentication before it is displayed.

.run(function($ionicPlatform, $rootScope, $state) {
    ...
};

.config(function($stateProvider, $urlRouterProvider){
    $stateProvider
        .state('root', {
            url: '',
            controller: 'rootCtrl',
            data: { 
                authenticate: false
            }
        })
        .state('home', {
            url: '/home',
            templateUrl: 'home.html',
            controller: 'homeCtrl',
            data: {
               authenticate: true
            }
        })
        .state('login', {
            url: '/login',
            templateUrl: 'login.html',
            controller: 'loginCtrl',
            data: {
                authenticate: false
            }
        })
    ;

    // Send to login if the URL was not found
    $urlRouterProvider.otherwise('/login');
})

Controllers

rootCtrl

As mentioned, root state doesn’t do much but redirect users to the home state. I redirect it as such because I noticed some oddities when dealing with root URL in Angular UI Router. Ie, if a user is logged in and is redirected to the root URL, the URL as shown in the browser does not change.

Not sure if it is an intended feature. Out of conservativeness I decided to opt out of it altogether.

.controller('rootCtrl', ['$state', function($state) {
    $state.go('home');
}])
homeCtrl

The homeCtrl is a very simple controller as well. The one thing that we have to manage here is the logout action. When a user clicks the logout button, Parse.User.logOut() will be triggered, which will immediately invalidate Parse.User.current() that is used in the .run() method to check for authentication.

Note that while we have invalidated user’s Parse session, user’s Facebook session remains active. If a user wants to login again, he will not be prompted for password and will be logged in immediately.

It is upto you if this is an intended behaviour. You may use facebookConnectPlugin.logout() to terminate the Facebook session too if you desire.

.controller('homeCtrl', ['$scope', '$state', function($scope, $state) {
      $scope.logout = function() {
          console.log('Logout');
          Parse.User.logOut();
          $state.go('login');
      };
}])

loginCtrl

The login controller is where all the magic of Facebook Login is happening. It’s quite a big chunk of codes here, so I am going to break it down in the next section.

One thing is important here is: remember to replace FACEBOOK_APP_ID with your own APP_ID

.controller('loginCtrl', ['$scope', '$state', function($scope, $state) {
    var fbLogged = new Parse.Promise();

    var fbLoginSuccess = function(response) {
        if (!response.authResponse){
            fbLoginError("Cannot find the authResponse");
            return;
        }
        var expDate = new Date(
            new Date().getTime() + response.authResponse.expiresIn * 1000
        ).toISOString();

        var authData = {
            id: String(response.authResponse.userID),
            access_token: response.authResponse.accessToken,
            expiration_date: expDate
        }
        fbLogged.resolve(authData);
        fbLoginSuccess = null;
        console.log(response);
    };

    var fbLoginError = function(error){
        fbLogged.reject(error);
    };

    $scope.login = function() {
        console.log('Login');
        if (!window.cordova) {
            facebookConnectPlugin.browserInit('FACEBOOK_APP_ID');
        }
        facebookConnectPlugin.login(['email'], fbLoginSuccess, fbLoginError);

        fbLogged.then( function(authData) {
            console.log('Promised');
            return Parse.FacebookUtils.logIn(authData);
        })
        .then( function(userObject) {
            var authData = userObject.get('authData');
            facebookConnectPlugin.api('/me', null, 
                function(response) {
                    console.log(response);
                    userObject.set('name', response.name);
                    userObject.set('email', response.email);
                    userObject.save();
                },
                function(error) {
                    console.log(error);
                }
            );
            facebookConnectPlugin.api('/me/picture', null,
                function(response) {
                    userObject.set('profilePicture', response.data.url);
                    userObject.save();
                }, 
                function(error) {
                    console.log(error);
                }
            );
            $state.go('home');
        }, function(error) {
            console.log(error);
        });
    };
}])

loginCtrl Explained

Let’s start with…

fbLogged and Parse Promise

To be honest I am not entirely sure how to explain what a promise is. Basically it is an async method that allows our app to proceed without being blocking while waiting for callback.

You can read more about it via the Parse documention here.

Here, we created a new Parse.Promise as fbLogged, which will be used later when the actual login is triggered to communicate with the Parse backend.

var fbLogged = new Parse.Promise();

fbLoginSuccess

This is basically our success function that will triggered when Facebook Login is successful. Ie once a User has authorized our app to access their information, this function will be triggered in our app subsequently.

Essentially what we are doing here is to massage the response we received from Facebook API so that we can pass it to Parse later on, which you will see under $scope.login().

var fbLoginSuccess = function(response) {
    if (!response.authResponse){
        fbLoginError("Cannot find the authResponse");
        return;
    }
    var expDate = new Date(
        new Date().getTime() + response.authResponse.expiresIn * 1000
    ).toISOString();

    var authData = {
        id: String(response.authResponse.userID),
        access_token: response.authResponse.accessToken,
        expiration_date: expDate
    }
    fbLogged.resolve(authData);
    fbLoginSuccess = null;
    console.log(response);
};

fbLoginError

The error function that deals with failed Facebook login attempt.

var fbLoginError = function(error){
    fbLogged.reject(error);
};

$scope.login

When a user clicks on the login button, immediately $scope.login is triggered.

The first thing we do is to initialize the Facebook SDK via Facebook Connect Plugin. This requires your Facebook_APP_ID.

Once that is done, we will initiate the login via FCP (Facebook Connect Plugin). It takes three arguments, ie Facebook permissions required, a success function and a error function, where the latter two have been covered above.

$scope.login = function() {
    if (!window.cordova) {
        facebookConnectPlugin.browserInit('FACEBOOK_APP_ID');
    }
    facebookConnectPlugin.login(['email'], fbLoginSuccess, fbLoginError);

    fbLogged.then( function(authData) {
        ...
    });
};

fbLogged.then

When a user logins via Facebook successfully, fbLoginSuccess will be called. Within that function, response from Facebook will be massaged. Once all of that is done, when fbLogged.resolve(authData) is called, this part of the codes will be triggered.

You can see that once we received the response from Facebook, and if it’s successful, we will login the user via Parse with the massaged response from fbLoginSuccess. This will reply with a userObject from Parse, and Parse.User.current() will also be set automatically.

fbLogged.then( function(authData) {
    return Parse.FacebookUtils.logIn(authData);
})
.then( function(userObject) {
    ...
}, function(error) {
    ...
});

Next with the userObject, we want to grab some information (email and name) about the user via Facebook Graph API. We can do that via FCP. Once the data is retrieved from Graph API, we save them into Parse.

After that, we redirect user to home and consider the login successful.

Note that Parse is a schema-based datastore. If you would like to save any additional User data from Facebook, you need to add the column into User class first.

fbLogged.then( function(authData) {
    ...
})
.then( function(userObject) {
    facebookConnectPlugin.api('/me', null, 
        function(response) {
            console.log(response);
            userObject.set('name', response.name);
            userObject.set('email', response.email);
            userObject.save();
        },
        function(error) {
            console.log(error);
        }
    );
    $state.go('home');
}, function(error) {
  console.log(error);
});

Set up Facebook App

Before you test the app, make sure your Facebook App is set up correctly. For our example, you need to add localhost into the settings before it will work correctly.

You may have to add an iOS setting or Android setting too eventually when you deploy to either platform.

Facebook App Login Settings

Now try logging in with your own account. If everything is successful, you should see your account showing up in your Parse account.

I write for fun. I write to share what I have learned, to remind myself what I have learned, and hopefully you will find something useful too.

View Comments

Next Post