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.
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
, found in js/controllers.js file, is the controller that contains the logic for the login.html
template.
This controller contains three functions:
validateUser
function.loginWithEmail
function in the aforementioned 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
.
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.
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:
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.state('app.profile', {
2 url: '/profile',
3 views: {
4 'menuContent': {
5 templateUrl: 'templates/profile.html',
6 controller: 'ProfileCtrl'
7 },
8 'fabContent': {
9 templateUrl: 'templates/profileFabButtonAdd.html',
10 controller: 'ProfileCtrlFabButton'
11 }
12 }
13})
From here we can conclude several things:
app.profile
and we can reference it by that string./profile
and that's what you would see in your URL if you ran it in your browser.menuContent
and fabContent
.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.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.
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.state('app.list', {
2 url: '/list/:id/:ime',
3 views: {
4 'menuContent': {
5 templateUrl: 'templates/list.html',
6 controller: 'ListCtrl'
7 },
8 'fabContent': {
9 templateUrl: 'templates/listFabButtonAdd.html',
10 controller: 'ListCtrlFabButton'
11 }
12 }
13})
From here we can conclude:
app.list
and we can reference it by that string./list
with additional two URL parameters id
and ime
(in case you're wondering, that's Croatian for 'name').menuContent
and fabContent
.menuContent
view is defined in the templates/list.html
file. Also, this template has a corresponding listCtrl
controller.fabContent
view is defined in the templates/listFabButtonAdd.html
file. Also, this template has a corresponding ListCtrlFabButton
controller.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.
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.
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.
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 "rules": {
3 // By default, make all data private unless specified otherwise.
4 ".read": false,
5 ".write": false,
6
7 "users": {
8 "$userId": {
9 // Only an authenticated user can read and make changes to its own data (auth.id equal to $userId).
10 ".read": "(auth != null) && (auth.uid === $userId)",
11 ".write": "(auth != null) && (auth.uid === $userId)",
12 ".validate": "($userId === newData.child('id').val()) && (newData.hasChildren(['id','provider']))",
13 // Write only if group exists
14 "groups": {
15 "$groupId": {
16 ".validate": "(root.child('groups').hasChild($groupId) && !root.child('users').child($userId).child('groups').hasChild($groupId))"
17 }
18 }
19 }
20 },
21 "users-metadata": {
22 // A list of users and their associated metadata, which can be updated by the single user and read by authenticated users.
23 "$userId": {
24 ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
25 ".write": "(auth != null) && (auth.uid === $userId)",
26 ".validate": "($userId === newData.child('id').val() && root.child('users').hasChild($userId)) && (newData.hasChildren(['id','name']))"
27 }
28 },
29 "users-byemail": {
30 // A list that contains email keys, which can be updated and read by authenticated users.
31 ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
32 ".write": "(auth != null) && (root.child('users').hasChild(auth.uid))",
33 "$userMailHash": {
34 //Write only if users exist.
35 "user_id": {
36 ".validate": "(root.child('users').hasChild(newData.val()) && newData.val() === auth.uid) || (newData.val() === data.val())"
37 },
38 "invites": {
39 "$groupId": {
40 ".read": "(auth != null) && (auth.uid === data.child('fromUserId').val())",
41 ".write": "(auth != null) && (!data.exists() || (root.child('groups').hasChild(newData.child('groupId').val()) && newData.child('fromUserId').val() === auth.uid))",
42 ".validate": "newData.hasChildren(['fromUserId','groupId']) && (newData.child('groupId').val() === $groupId)"
43 }
44 }
45 }
46 },
47
48 "groups": {
49 // A list with groups and their metadata, which can be updated and read by authenticated users.
50 "$groupId": {
51 ".read": "(auth != null) && (root.child('users').hasChild(auth.uid))",
52 ".write": "(auth != null) && (root.child('users').hasChild(auth.uid))",
53 ".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())",
54 // Write only if user exists.
55 "members": {
56 "$userId": {
57 ".validate": "(auth != null) && (root.child('users').hasChild($userId))"
58 }
59 }
60 }
61 },
62 "group-items": {
63 "$groupId": {
64 // A list of items in group, which can be updated and readed by authenticated users that are members of a group.
65 ".read": "(auth != null) && (root.child('groups').child($groupId).child('members').hasChild(auth.uid))",
66 ".write": "(auth != null) && (root.child('groups').child($groupId).child('members').hasChild(auth.uid))",
67 "$itemId": {
68 ".validate": "(newData.hasChildren(['id','created','createdByUserId','groupId','isCompleted','name','updated','price','quantity']) && $itemId === newData.child('id').val())",
69 "createdByUserId": {
70 ".validate": "(newData.val() === auth.uid || data.val() === newData.val()) && (root.child('users').hasChild(newData.val()))"
71 },
72 "groupId": {
73 ".validate": "(root.child('groups').hasChild(newData.val()))"
74 },
75 "price": {
76 ".validate": "(newData.isNumber())"
77 },
78 "quantity": {
79 ".validate": "(newData.isNumber())"
80 }
81 }
82 }
83 }
84 }
85}
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!).
Few of the resources that proved indispensable when I was learning about Ionic:
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.