Guest blogger Jen Looper
The powerful combination of NativeScript, Firebase, and Angular 2 can kickstart your app building into high gear, especially during the holidays when you find yourself confronted with the need to speed up your app development AND meet your family's gift-giving needs! Just in time, I am happy to presentto you (see what I did there 🎁) a demo of how to leverage Firebase in your Angular 2-powered NativeScript apps using several elements of Eddy Verbruggen's famousNativeScript-Firebase plugin.
In this tutorial, I'm going to show you how to use four popular Firebase elements in your NativeScript app: Authentication with a login and registration routine; Database for data storage and real-time updates; Remote Config to make changes to an app remotely; and Storage for saving photos. To do this, I decided to rewrite my Giftler app, originally written in Ionic.
Before we get started, I encourage you to read through thedocumentation before starting in on your project, and make sure that a few prerequisites are in place:
- Ensure thatNativeScript is installed on your local machine and that the CLI works as expected
- Configure your preferred IDE for NativeScript and Angular development. You're going to need TypeScript, so ensure that your transpiling process is working. There are excellent NativeScript plugins available forVisual Studio, Visual Studio Code, andJetbrains-compatible IDEs, among others. Visual Studio Code in particular has handysnippets that speed up development
- Log in to your Firebase account and find your console
- Create a new project in the Firebase console. I named mine 'Giftler'. Also create an iOS and Android app in the Firebase console. As part of this process you'll download both a GoogleServices-Info.plist and google-services.json file. Make sure you note where you place those files, and you'll need them in a minute.
Install the dependencies
I've built Giftler as an example of an authenticated NativeScript app where users can list the gifts that they would like to receive for the holidays, including photos and text descriptions. For the time being, this app does the following on iOS and Android:- allows login and logout, registration, and a 'forgot password' routine
- lets users enter gift items into a list
- lets users delete items from a list
- lets users edit items in the list individually by adding descriptions and photos
- provides messaging from the Remote Config service in Firebase that can be quickly changed in the backend
- In the
/app/App_Resources/Android folder
, put the google.services.json file that you downloaded from Firebase. - Likewise, in
/app/App_Resources/iOS folder
, put the GoogleService-Info.plist file also downloaded from Firebase.
Now, let's take a look at the
package.json
at the root of this app. It contains the plugins that you'll use in this app. I want to draw your attention to the NativeScript-oriented plugins: "nativescript-angular": "1.2.0",The NativeScript-Angular plugin is NativeScript's integration of Angular. The Camera plugin makes managing the camera a bit easier. IQKeyboardManager is an iOS-specific plugin that handles the finicky keyboard on iOS. The Theme plugin is a great way to add default styles to your app without having to skin the app entirely yourself. And finally, the most important plugin in this app is the Firebase plugin.
"nativescript-camera": "^0.0.8",
"nativescript-iqkeyboardmanager": "^1.0.1",
"nativescript-plugin-firebase": "^3.8.4",
"nativescript-theme-core": "^1.0.2",
With the dependencies in place and the plugins ready to install, you can build your app to create your
platforms
folder with iOS and Android-specific code and initialize the Firebase plugin along with the rest of the npm-based plugins. Using the NativeScript CLI, navigate to the root of your cloned app and type tns run ios
or tns run android.
This will start the plugin building routines and, in particular, you'll see the various parts of the Firebase plugin start to install. The install script that runs will prompt you to install several elements to integrate to the various Firebase services. We're going to select everything except Messaging and social authentication for the moment. A great feature is that a firebase.nativescript.json file is installed at the root of the app, so if you need to install a new part of the plugin later, you can edit that file and reinstall the plugin. At this point, if you run
tns livesync ios --watch
or tns livesync android --watch
to see the app running on an emulator and watching for changes, you would see a the app running and ready to accept your new login. Before you initialize a login, however, ensure that Firebase handles Email/Password type logins by enabling this feature in the Firebase console in the Authentication tab: Let's take a look under the covers a bit to see what's happening behind the scenes. Before you can log in to Firebase, you need to initialize the Firebase services that you installed. In
app/main.ts
, there are a few interesting bits. // this import should be first in order to load some required settings (likeFirst, we import firebase from the plugin, and then we call .init(). Edit the storageBucket property to reflect the value in the Storage tab of your Firebase console:
globals and reflect-metadata)
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
import { AppModule } from "./app.module";
import { BackendService } from "./services/backend.service";
import firebase = require("nativescript-plugin-firebase");
firebase.init({
//persist should be set to false as otherwise numbers aren't returned during
livesync
persist: false,
storageBucket: 'gs://giftler-f48c4.appspot.com',
onAuthStateChanged: (data: any) => {
console.log(JSON.stringify(data))
if (data.loggedIn) {
BackendService.token = data.user.uid;
}
else {
BackendService.token = "";
}
}
}).then(
function (instance) {
console.log("firebase.init done");
},
function (error) {
console.log("firebase.init error: " + error);
}
);
platformNativeScriptDynamic().bootstrapModule(AppModule);
Now your app is customized to your own Firebase account and you should be able to register a new user and login in the app. You can edit the user.email and password variables in
app/login/login.component.ts
file to change the default login credentials from user@nativescript.org
to your own login and password if you like. Code Structure and Authentication
Angular 2 design patterns require that you modularize your code, so we will oblige by using the following code structure:—login
- login.component.ts
- login.html
- login.module.ts
- login.routes.ts
—list-detail …
—models
- gift.model.ts
- user.model.ts
- index.ts
- backend.service.ts
- firebase.service.ts
- utils.service.ts
- index.ts
app.css
app.module.ts
app.routes.ts
auth-guard.service.ts
main.ts
I want to draw your attention to the way Firebase authentication works with the Angular 2 auth-guard.service. When Firebase is initialized in your app in
app/main.ts
as we saw above, the onAuthStateChanged
function is called: onAuthStateChanged: (data: any) => {When the app starts, check the console for the stringified data being returned by Firebase. If this user is flagged as being
console.log(JSON.stringify(data))
if (data.loggedIn) {
BackendService.token = data.user.uid;
}
else {
BackendService.token = "";
}
}
loggedIn
, we will simply set a token
which is the userId sent back by Firebase. We'll use the NativeScript application settings module, which functions like localStorage, to keep this userId available and associate it to the data that we create. This token and the authentication tests that use it, managed in the app/services/backend.service.ts
file, are made available to the app/auth-guard.service.ts
file. The auth-guard file offers a neat way to manage logged-in and logged-out app state. The AuthGuard class implements the CanActivate interface from the Angular Router module.
export class AuthGuard implements CanActivate {Essentially, if the token is set during the above login routine, and the BackendService.isLoggedIn function returns true, then the app is allowed to navigate to the default route which is our wish list; otherwise, the user is sent back to login:
constructor(private router: Router) { }
canActivate() {
if (BackendService.isLoggedIn()) {
return true;
}
else {
this.router.navigate(["/login"]);
return false;
}
}
const listRoutes: Routes = [Now that you have initialized your Firebase-powered NativeScript app, let's learn how to populate it with data and use Firebase's amazing realtime power to watch for the database to be updated.
{ path: "", component: ListComponent, canActivate: [AuthGuard] },
];
Making your list, checking it twice
Starting inapp/list/list.html
, which is the basis of the wish list, you'll see a textfield and a blank list. Go ahead, tell Santa what you want! The items are sent to the database and added to your list in realtime. Let's see how this is done. First, note that in
app/list/list.component.ts
, we set up an observable to hold the list of gifts: public gifts$: Observable;
then, we populate that list from the database when the component is initialized:
ngOnInit(){It's in the firebaseService file that things get interesting. Note the way that this function adds a listener and returns an rxjs observable, checking for changes on the Gifts collection in the Firebase database:
this.gifts$ =this.firebaseService.getMyWishList();
}
getMyWishList(): ObservableThe results of this query are handled in a{
return new Observable((observer: any) => {
let path = 'Gifts';
let onValueEvent = (snapshot: any) => {
this.ngZone.run(() => {
let results = this.handleSnapshot(snapshot.value);
console.log(JSON.stringify(results))
observer.next(results);
});
};
firebase.addValueEventListener(onValueEvent, `/${path}`);
}).share();
}
handleSnapshot
function below, which filters the data by user, populating an _allItems array: handleSnapshot(data: any) {And finally, publishUpdates is called, which sorts the data by date so that newer items are shown first:
//empty array, then refill and filter
this._allItems = [];
if (data) {
for (let id in data) {
let result = (Object).assign({id: id}, data[id]);
if(BackendService.token === result.UID){
this._allItems.push(result);
}
}
this.publishUpdates();
}
return this._allItems;
}
publishUpdates() {Once the data has populated your $gifts observable, you can edit and delete elements of it and it will be handled by the listener and the front end updated accordingly. Note that the onValueEvent function of getMyWishList method includes the use ofngZone which ensures that, although data updates occur asynchronously, the UI is updated accordingly. A good overview of ngZone in NativeScript apps can be foundhere.
// here, we sort must emit a *new* value (immutability!)
this._allItems.sort(function(a, b){
if(a.date < b.date) return -1;
if(a.date > b.date) return 1;
return 0;
})
this.items.next([...this._allItems]);
}
Remotely Configured Messages from Beyond
Another cool piece of Firebase's service includes "Remote Config", a way to provide app updates from the Firebase backend. You can use Remote Config to toggle features on and off in your app, make UI changes, or send messages from Santa, which is what we're going to do!In
app/list/list.html
, you'll find a message box: <Label class="gold card" textWrap="true" [text]="message$ | async"></Label>
The
message$
observable is built in much the same way as the data list; changes are picked up in this case each time the app is freshly initialized: ngOnInit(){And the magic occurs in the service layer
this.message$ =this.firebaseService.getMyMessage();
}
(app/services/firebase.service.ts
): getMyMessage(): Observable{
return new Observable((observer:any) => {
firebase.getRemoteConfig({
developerMode: false,
cacheExpirationSeconds: 300,
properties: [{
key: "message",
default: "Happy Holidays!"
}]
}).then(
function (result) {
console.log("Fetched at " + result.lastFetch + (result.throttled ? "
(throttled)" : ""));
for (let entry in result.properties)
{
observer.next(result.properties[entry]);
}
}
);
}).share();
}
Take a picture!
One of the more interesting parts of this project, I think, is the ability to take a picture of your present of choice and store it in Firebase Storage. I leveraged the Camera plugin, as mentioned above, which makes managing the hardware a little easier. To start, ensure that your app has access to the device camera by getting permissions set in the ngOnInit() method inapp/list-detail/list-detail.component.ts
: ngOnInit() {A chain of events begins when the user clicks the 'Photo' button in the detail screen. First,
camera.requestPermissions();
...
}
takePhoto() {The camera takes a picture, and then that photo is stored as an imageAsset and displayed on the screen. The image is then named with a date stamp and saved to a file locally. That path is reserved for future use.
let options = {
width: 300,
height: 300,
keepAspectRatio: true,
saveToGallery: true
};
camera.takePicture(options)
.then(imageAsset => {
imageSource.fromAsset(imageAsset).then(res => {
this.image = res;
//save the source image to a file, then send that file path to
firebase
this.saveToFile(this.image);
})
}).catch(function (err) {
console.log("Error -> " + err.message);
});
}
saveToFile(res){Once the 'Save' button is pressed, this image, via its local path, is sent to Firebase and saved in the storage module. Its full path in Firebase is returned to the app and stored in the
let imgsrc = res;
this.imagePath =
this.utilsService.documentsPath(`photo-${Date.now()}.png`);
imgsrc.saveToFile(this.imagePath, enums.ImageFormat.png);
}
/Gifts
database collection: editGift(id: string){This chain of events seems complicated, but it boils down to a few lines in the Firebase service file:
if(this.image){
//upload the file, then save all
this.firebaseService.uploadFile(this.imagePath).then((uploadedFile: any) =>
{
this.uploadedImageName = uploadedFile.name;
//get downloadURL and store it as a full path;
this.firebaseService.getDownloadUrl(this.uploadedImageName).then((downloadUrl:
string) => {
this.firebaseService.editGift(id,this.description,downloadUrl).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
})
}, (error: any) => {
alert('File upload error: ' + error);
});
}
else {
//just edit the description
this.firebaseService.editDescription(id,this.description).then((result:any)
=> {
alert(result)
}, (error: any) => {
alert(error);
});
}
}
uploadFile(localPath: string, file?: any): PromiseThe end result is a nice way to capture both photos and descriptions of the gifts for your wish list. No more excuses that Santa didn't know exactly WHICH Kylie Eyeliner to buy. By combining the power of NativeScript and Angular, you can create a native iOS and Android app in a matter of minutes. By adding Firebase you have a powerful way of storing your app's users, images and data, and a way of updating that data in real-time across devices. Cool, huh? It looks like this:{
let filename = this.utils.getFilename(localPath);
let remotePath = `${filename}`;
return firebase.uploadFile({
remoteFullPath: remotePath,
localFullPath: localPath,
onProgress: function(status) {
console.log("Uploaded fraction: " + status.fractionCompleted);
console.log("Percentage complete: " + status.percentageCompleted);
}
});
}
getDownloadUrl(remoteFilePath: string): Promise{
return firebase.getDownloadUrl({
remoteFullPath: remoteFilePath})
.then(
function (url:string) {
return url;
},
function (errorMessage:any) {
console.log(errorMessage);
});
}
editGift(id:string, description: string, imagepath: string){
this.publishUpdates();
return firebase.update("/Gifts/"+id+"",{
description: description,
imagepath: imagepath})
.then(
function (result:any) {
return 'You have successfully edited this gift!';
},
function (errorMessage:any) {
console.log(errorMessage);
});
}
We are well on our way to create a solid wishlist management app! It remains to figure out the best way to inform Santa of our wishes - a Mailgun email integration or using push notifications would be the obvious next route. In the meantime, best wishes for a wonderful holiday season, and I hope you have a great time creating awesome NativeScript apps using Firebase!
Want to learn more about NativeScript? Visit http://www.nativescript.org. If you need help, join the NativeScript Slack channel here.