Pluralsight Logo
Author avatar

Nikola Brežnjak

Author badge Author

Building an App with Ionic Firebase

Nikola Brežnjak

Author BadgeAuthor
  • Oct 15, 2018
  • 26 Min read
  • 71,105 Views
  • Oct 15, 2018
  • 26 Min read
  • 71,105 Views
Front-End JavaScript

Introduction

In my previous guide, How to Set Up Shopping App with Ionic Firebase, we explored how to set up our app and what our end product will look like.

login.html

First, let's now check out the login logic by examining the login.html template. The only interesting part here is the ng-click on the login buttons. This calls the oauthLogin() function (with a proper provider passed in - 'facebook' or 'google') and the call to testAppWithoutLogin() function, which will login a user with an existing demo account.

LoginCtrl

LoginCtrl, found in js/controllers.js file, is the controller that contains the logic for the login.html template.

This controller contains three functions:

  • oauthLogin: Depending on the passed parameter, it calls the corresponding function in the Auth service
  • testAppWithoutLogin: Prepares the demo user object and calls the validateUser function.
  • validateUser: Calls the loginWithEmail function in the aforementioned Auth service.

Auth Service

Auth service is basically a custom factory that, via AngularFire's (Angular wrapper for Firebase) $firebaseAuth service, calls functions like $authWithOAuthPopup and $authWithPassword.

One important thing to note in the Auth service is the onAuth function which wraps the $onAuth function with $timeout so it processes in the digest loop. In case you're wondering, we're listening to this event in the app.js file.

You can learn more about AngularFire from official Firebase documentation. More specifically, they have a step by step tutorial for Ionic as well, which you can follow in order to get a better grasp on the subject.

Social Logins

As for the social logins (Facebook and Google), you’ll have to create applications on their developer sites. You can follow the corresponding step-by-step official tutorials here and here, to learn how to create those applications. Basic Firebase user authentication documentation can be found here.

Linking Facebook and Firebase

All you have to do is enable Facebook authentication in the settings and enter the appropriate App ID and Secret (which you'll get once you create a Facebook application).

Additionally, you must set the proper OAuth to redirect URI on the Facebook Settings -> Advanced tab:

User Profile

Once the user is able to login successfully, we can forward them to the app.profile state, as you can see in the code. Now, we go back to the app.js config part to see the route definition for this state:

1
2
3
4
5
6
7
8
9
10
11
12
13
.state('app.profile', {
    url: '/profile',
    views: {
        'menuContent': {
            templateUrl: 'templates/profile.html',
            controller: 'ProfileCtrl'
        },
        'fabContent': {
            templateUrl: 'templates/profileFabButtonAdd.html',
            controller: 'ProfileCtrlFabButton'
        }
    }
})

From here we can conclude several things:

  • The profile state is app.profile and we can reference it by that string.
  • The profile URL is /profile and that's what you would see in your URL if you ran it in your browser.
  • The profile has two views: menuContent and fabContent.
  • The HTML of the menuContent view is defined in the templates/profile.html file. Also, this template has a corresponding ProfileCtrl controller. *The HTML of the fabContent view is defined in the templates/profileFabButtonAdd.html file. Also, this template has a corresponding ProfileCtrlFabButton controller.

profile.html

The profile.html template displays a (logged in) user’s profile image, name, and email. Also, it lists the existing projects (if they have any) with the ng-repeat directive.

Inside the ng-repeat directive, we’re displaying the project image and name. By using the <ion-option-button> directive, we're adding support for the "swipe left" gesture which reveals Remove and Edit buttons. These buttons, in turn, call the corresponding Remove and Edit functions in the ProfileCtrl via the ng-click directive.

profileFabButtonAdd.html

This file contains a simple button that’s used for adding the new project. The ng-click directive calls the add function that is defined in the ProfileCtrlFabButton controller.

The add function basically shows the popup (via $ionicPopup service) and, after the user enters the new project name and confirms, it calls the GroupAdd function defined on the Groups factory (in the file named data.js).

Adding Items to the List

Once the user creates a new project (list) and clicks on this list in their profile, they’ll be forwarded to the app.list({id: g.id, ime: g.name}) state, as you can see in the code. Again, we'll go back to the app.js config part to see the route definition for this state:

1
2
3
4
5
6
7
8
9
10
11
12
13
.state('app.list', {
    url: '/list/:id/:ime',
    views: {
        'menuContent': {
            templateUrl: 'templates/list.html',
            controller: 'ListCtrl'
        },
        'fabContent': {
            templateUrl: 'templates/listFabButtonAdd.html',
            controller: 'ListCtrlFabButton'
        }
    }
})

From here we can conclude:

  • The list state is app.list and we can reference it by that string.
  • The list URL is /list with additional two URL parameters id and ime (in case you're wondering, that's Croatian for 'name').
  • The list has two views: menuContent and fabContent.
  • The HTML of the menuContent view is defined in the templates/list.html file. Also, this template has a corresponding listCtrl controller.
  • HTML of the fabContent view is defined in the templates/listFabButtonAdd.html file. Also, this template has a corresponding ListCtrlFabButton controller.

list.html

The list.html template displays the items (along with their name, price, and quantity) added to the list.

Inside the ng-repeat directive, we’re displaying the item name, price, and quantity, ordered by isCompleted property, which indicates that the item has been "bought."

Each list item can be deleted and marked as "bought" by clicking the corresponding button. The button then calls the corresponding Remove and CheckOrUncheck functions in the ListCtrl via the ng-click directive.

These two controller functions call the functions defined in the Groups factory. Those function then converse with Firebase in order to delete the item or mark it as completed.

Important to note: the usage of the transaction operation on the returned item basically secures that some change happens in a transaction (from database theory, this means either the data will be saved or it will not be saved at all).

transaction() returns stored data so that you can manipulate it and change the data that is returned. You can learn more about transaction function from the official documentation.

Inviting Users to Your List

This template has one more button, which is used for adding a new user to the list of users who share this concrete list. The ng-click directive calls the addUserToGroup function, defined in ListCtrl controller.

This function shows a popup with a username input field. Once the user enters their friend's email and confirms it, the controller will call the external service on the server, which will actually send an email to this person notifying them that their friend added them to the list.

Also, this list will appear in their project list once they log in to their account. You can, of course, comment this code out or place your own API for this.

listFabButtonAdd.html

This file contains a simple button that’s used for adding the new item to the list. The ng-click directive calls the add function, defined in ListCtrlFabButton controller.

The add function shows the popup (via $ionicPopup service) and, after the user enters the new item name, price and quantity, and confirms, it calls the GroupItemAdd function defined on the Groups factory (in data.js file file).

Groups Factory

Let's take a moment now to go over the functions in the Groups factory.

Groups.byUser: This whole function is done in chained promises. First get all groups that are stored in array in the user object. Then we iterate through this list and create an array with promises for all groups in the list. Next, we get items for that group. After that we iterate through the members array in group and create an array of promises which we resolve in the end with Promise.all. After members are resolved, we return an array of group promises and resolve this whole array in the end.

Groups.Group: Returns a promise for group data.

Groups.GroupAdd: First, we need to create a reference with push() which contains a new unique key for that group. Then we fill the object of that group and, with reference that we got from push(), we write this object with the set() function.

Groups.GroupAddMember: First, we add a group key to user object. Then we add the user to the group object under members.

Groups.GroupRemoveMember:
Here, we use the remove() function to remove a member from the group object and return a promise, which this function returns.

Groups.GroupRemove: Before we can remove the group, we need to clean the items associated with this group. In order to do so, we can get the reference for these items through the GroupItems function. After we successfully delete this group, we remove the group from groups list, and remove the group key from user object.

Groups.GroupItems: Returns reference to the list of group items.

Groups.GroupItem: Returns the reference to one record (by item key) from the list of items for a group.

Groups.GroupItemAdd: Just like when we were adding a new group, we need to first call push() on the reference that we got from Groups.GroupItems in order to get the reference to the new group and a unique key for that group. Then we fill the object of that item and, with the reference that we got from push(), write this object with the set() function.

Groups.GroupItemRemove: To remove the item from the group item list, we call the remove() function on the reference that we got from Groups.GroupItems.

Groups.setCurrentGroupId and Groups.getCurrentGroupId: These functions are used to set and get the currentGroupId that is used in ListCtrl.

Firebase Access Rules

All that is left to show is how the Firebase rules (found in the Dashboard -> Security & Rules in your Firebase admin dashboard) are set up. You’ll find comments on how and what each rule does in the comments (the parts marked with //).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
  "rules": {
    // By default, make all data private unless specified otherwise.
    ".read": false,
    ".write": false,

    "users": {
      "$userId": {
        // Only an authenticated user can read and make changes to its own data (auth.id equal to $userId).
        ".read": "(auth != null) && (auth.uid === $userId)",
        ".write": "(auth != null) && (auth.uid === $userId)",
        ".validate": "($userId === newData.child('id').val()) && (newData.hasChildren(['id','provider']))",
        // Write only if group exists
        "groups": {
          "$groupId": {
            ".validate": "(root.child('groups').hasChild($groupId) && !root.child('users').child($userId).child('groups').hasChild($groupId))"
          }
        }
      }
    },
    "users-metadata": {
      // A list of users and their associated metadata, which can be updated by the single user and read by authenticated users.
      "$userId": {
        ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
        ".write": "(auth != null) && (auth.uid === $userId)",
        ".validate": "($userId === newData.child('id').val() && root.child('users').hasChild($userId)) && (newData.hasChildren(['id','name']))"
      }
    },
    "users-byemail": {
      // A list that contains email keys, which can be updated and read by authenticated users.
      ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
      ".write": "(auth != null) && (root.child('users').hasChild(auth.uid))",
      "$userMailHash": {
        //Write only if users exist.
        "user_id": {
          ".validate": "(root.child('users').hasChild(newData.val()) && newData.val() === auth.uid) || (newData.val() === data.val())"
        },
        "invites": {
          "$groupId": {
            ".read": "(auth != null) && (auth.uid === data.child('fromUserId').val())",
            ".write": "(auth != null) && (!data.exists() || (root.child('groups').hasChild(newData.child('groupId').val()) && newData.child('fromUserId').val() === auth.uid))",
            ".validate": "newData.hasChildren(['fromUserId','groupId']) && (newData.child('groupId').val() === $groupId)"
          }
        }
      }
    },

    "groups": {
      // A list with groups and their metadata, which can be updated and read by authenticated users.
      "$groupId": {
        ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
        ".write": "(auth != null) && (root.child('users').hasChild(auth.uid))",
        ".validate": "((auth.uid === newData.child('owner').val() || newData.child('owner').val() === data.child('owner').val()) && newData.hasChildren(['id','name','owner']) && $groupId === newData.child('id').val())",
        // Write only if user exists.
        "members": {
          "$userId": {
            ".validate": "(auth != null) && (root.child('users').hasChild($userId))"
          }
        }
      }
    },
    "group-items": {
      "$groupId": {
        // A list of items in group, which can be updated and readed by authenticated users that are members of a group.
        ".read": "(auth != null) && (root.child('groups').child($groupId).child('members').hasChild(auth.uid))",
        ".write": "(auth != null) && (root.child('groups').child($groupId).child('members').hasChild(auth.uid))",
        "$itemId": {
          ".validate": "(newData.hasChildren(['id','created','createdByUserId','groupId','isCompleted','name','updated','price','quantity']) && $itemId === newData.child('id').val())",
          "createdByUserId": {
            ".validate": "(newData.val() === auth.uid || data.val() === newData.val()) && (root.child('users').hasChild(newData.val()))"
          },
          "groupId": {
            ".validate": "(root.child('groups').hasChild(newData.val()))"
          },
          "price": {
            ".validate": "(newData.isNumber())"
          },
          "quantity": {
            ".validate": "(newData.isNumber())"
          }
        }
      }
    }
  }
}

Free to Be Free

I hope this guide, and the accompanying project, helped you see the potential that Firebase offers.

You're free to use this project in any way you like. You can fork it, make it better, submit your version to the app store, make money out of it (just make sure you send me a beer or two if you pull it off!).

How to Get Help with Ionic Framework

Few of the resources that proved indispensable when I was learning about Ionic:

  • For a quick framework reference, I'm suggesting the official documentation, which is indeed very good.
  • If you're in search for a good book about Ionic, you don't need to look further than Ionic in Action: Hybrid Mobile Apps with Ionic and AngularJS
  • Of course, if you haven't, check out my posts in this series
  • One of the best resources on the net for programming related questions, about which you've no doubt heard, is StackOverflow. You can view the specific Ionic tagged questions via this link.
  • To get started with AngluarJS try these few resources, not necessarily in that particular order.
  • Let me repeat myself by saying that if you have any questions about Ionic framework, or you had trouble following this tutorial (you couldn't install something), or you would like to suggest what kind of tutorials you would like to see regarding Ionic in the future, please share it in the comments below. Also, you can reach me personally via @HitmanHR or my blog. I'll do my best to answer all of your questions.
  • If, however, you favor one-on-one help, you can reach me via HackHands.

Credits

Matija Lesar helped me with this application and was responsible for communication with Firebase via Angular services and Firebase data model. He is eager to learn new things and try new programming languages but his real passion are databases and backend services. You can look him up on his personal web page.

0