Pluralsight Logo
Author avatar

Nikola Brežnjak

Author badge Author

How to build a shopping app with Ionic Firebase

Nikola Brežnjak

Author BadgeAuthor
  • Mar 11, 2016
  • 35 Min read
  • 70,524 Views
  • Mar 11, 2016
  • 35 Min read
  • 70,524 Views
Front-End JavaScript

Intro

In this tutorial, you’ll learn how the app ShoppingTODO (published both in Apple's App Store and Google's Play Store) was built. You'll get access to the full source code on Github and we'll go through each and every file and explain all the bits and pieces that were needed for it to work.

You’ll learn how to use Firebase as your backend and also how to incorporate the Material design. The app will have features like list sharing, budget tracking and live updates.

You can even try the app in your browser here. With Ionic, it's easy to take the same code base and deploy to an actual mobile App store and a standalone website application.

For actual deployment to the App Stores, please take a look at the fourth tutorial which covers the exacts steps you need to take in order to publish a finished application to Apple's App Store and Google's Play Store.

It all starts with an idea

As you already learned through this series, it all starts with an idea. The idea serves to solve a problem that we may have. Our problem is easy to define; we tend to forget things that we have to buy at the store. Even more so, we don't like getting tricked into buying stuff we don't need and then having buyers remorse the second we get home. How many times have you gone to the store with a list full of items to buy? And how many times were you crossing it off with your pen while, in the other hand, using your smartphone to chat with your friend? So, wouldn’t it make more sense to just accomplish both of those tasks directly from your phone and ditch the pen and paper?

The app will let you enter items. You would likely prepare this list at home, and then once in the store you’d be able to enter exact prices and amounts of the stuff you actually bought. The most useful thing when hacking the budget will be its immediate display of the total price. Also, live updates will enable just-in-time edits so that the person you share lists with doesn't have to call you to let you know that you should add that "one more thing.”

Design

The way we’ll approach this implementation is that we'll try to reuse as much as open source help as possible. So, we'll use an open source theme. Since Material design is the new cool thing, we'll use the following open source template. More specifically, we will only use the demo example.

Finished project screens

The finished app, which you can take a look at here, has the following screens:

Login: The app offers Facebook and Google login, and it even has the DEMO mode where you can test the app without needing to log in

Home screen: You can add as many projects (we call them groups in the code) as you like.

Project screen: Shows a list of items that need to be bought and those that are already bought, with a view of the price of bought/to-buy items.

Adding items on the project screen

Adding users to your project

Finished project code

You can clone the project and get it running locally by doing the following:

  • clone the code from Github
  • cd into the new folder
  • run ionic serve
  • you should see the login screen (shown above) in your browser

If you want to run it on your phone, you need to make sure the plugins listed below are installed (after adding the platform with ionic platform add ios or ionic platform add android). You can list the installed plugins by executing ionic plugin list in your project root directory (where your ionic.project file resides).

  • cordova-plugin-admobpro 2.11.1 "AdMob Plugin Pro"
  • cordova-plugin-extension 1.2.9 "Cordova Plugin Extension"
  • cordova-plugin-inappbrowser 1.2.1 "InAppBrowser"
  • cordova-plugin-splashscreen 3.2.0 "Splashscreen"
  • cordova-plugin-statusbar 2.1.1 "StatusBar"
  • cordova-plugin-whitelist 1.2.1 "Whitelist"
  • ionic-plugin-keyboard 1.0.8 "Keyboard"

To install the missing plugins, here's a copy/paste installation shortcut (execute this in your Command prompt/Terminal once in the root of your project):

  • cordova plugin add cordova-plugin-admobpro
  • cordova plugin add cordova-plugin-extension
  • cordova plugin add cordova-plugin-inappbrowser
  • cordova plugin add cordova-plugin-splashscreen
  • cordova plugin add cordova-plugin-statusbar
  • cordova plugin add cordova-plugin-whitelist
  • cordova plugin add ionic-plugin-keyboard

You can also take a look at this project with your Ionic View application (we mentioned this in the previous tutorials) by entering the code: 4f055b6d. Or, as mentioned before, you can download it from the App Store or Play Store or even try it out in your browser.

How does it work?

As I mentioned in the design section above, I used an awesome Ionic Material template as a basis for this project. I cloned the project with:

git clone https://github.com/zachsoft/Ionic-Material

I took out the demo folder for the basis of my project. You can do the same steps, and once you're in the demo folder, if you run `ionic serve --lab’, you'll get the following screen:

Starting point

From this point on, I will not copy/paste the full source code that I'm referring to here, nor will I explain every single line, since that would become too cumbersome (besides, you're already pretty familiar with Ionic framework by now if you've been following the series). Instead, I’ll give you the link on Github to the exact file/function that I'm referring to and will only show those lines of code necessary to explain what's happening. Of course, if you have any questions, please post them as comments on this post and I'll be happy to help.

From my experience, when trying to understand some Ionic framework code, I always go to app.js file first and check out the config() function(click to view on Github) to see how the routes are set up. Looking at our example we can see that the first screen that shows up is at route /app/login:

$urlRouterProvider.otherwise('/app/login');

We can now check the definition of this state and see that it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
.state('app.login', {
    url: '/login',
    views: {
        'menuContent': {
            templateUrl: 'templates/login.html',
            controller: 'LoginCtrl'
        },
        'fabContent': {
            template: ''
        }
    }
})

From here we can conclude several things:

  • the login state is app.login and we can reference it by that string
  • the login url is /login and that's what you would see in your URL if you ran it in your browser
  • the login has two views (menuContent nad fabContent)
  • HTML of the menuContent view is defined in the templates/login.html file
  • fabContent view doesn’t have a template associated with it

Concerning the two views, it's how the author of this template envisioned this. And, in case you're wondering about the name "fab," it comes from the Floating Action Button in Material design documentation. Consequently, this is the view that will hold the FAB button (+ button in the bottom right corner on the app screen images above).

login.html

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

Firebase

In this project, Firebase is used as app's backend, including data storage and user authentication. It features a real-time database, and it can also do static hosting. You can learn more about it (there are guides for a lot of platforms) here.

LoginCtrl

LoginCtrl, found in js/controllers.js(click to view on Github) file, is the controller that contains the logic for the login.html template.

This controller contains three functions:

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.

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

As a quick tip on 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, probably the only part that you must not forget about is to set the proper OAuth redirect URI on the Facebook Settings -> Advanced tab:

User profile

Once the user logs in successfully we forward him to the app.profile state, as you can see in the code. Now, again 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
  • HTML of the menuContent view is defined in the templates/profile.html file. Also, this template has a corresponding ProfileCtrl controller
  • 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(click to view on Github) template displays a (logged in) user’s profile image, name, and email. Also, it lists his existing projects (if he has any) with the ng-repeat directive(click to view on Github).

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, which 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, defined in 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. Now, again, 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.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 several things:

  • 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' - yeah, inconsistency isn't a virtue)
  • the list has two views: menuContent and fabContent
  • 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(click to view on Github) 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, which in turn 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, which then deals with Firebase in order to delete the item or mark it as completed. Important to note is the usage of transaction operation on the returned item, which 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 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

Additionally, 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 his 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 us take a moment now and go over the functions in the Groups factory:

Firebase access rules

What's 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 (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 authenticated user can read and make chanages on it's 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 exists
        "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 readed 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 some 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 tutorial, and the accompanying project, helped you see the potential that Firebase offers and that you'll create something great with it yourself. As I mentioned earlier, I encourage you to share your wishes and ideas about what kind of an app you’d like to see built and then explained. Remember, I'm here to help you get the best of Ionic framework, so don't hesitate to ask.

You're free to use this project in any way you like. You can fork it, make it better, submit your version to 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.

3