Build an SMS location notification app with Vue.js, Python, and Firebase

Published July 16, 2020 by Diana Rodriguez

No-Panic! 🚀

A comprehensive and rewarding step-by-step tutorial covering the basic concepts of Vue.js, Google Maps Platform, Firebase, and Deployment to make an awesome panic button progressive web app!

This app is not intended for use in cases of a real emergency. It should be used solely for educational purposes.

No-Panic is an app built with Vue.js to track a user’s location in real-time and send it to their trusted contacts via SMS.

To develop the features of this app, the following resources will be used:

  • Firebase (real-time database, authentication)
  • Google Cloud Platform (cloud functions deployments)
  • Google Maps (maps and geolocation)
  • Vonage Communication APIs (SMS messaging)

This app needs access to a device’s geolocation. However, access to this data is only allowed for websites using HTTPS (except for the localhost domain)—otherwise, the feature will be blocked by the browser. By deploying through Firebase, the service will take care of the TLS certificate settings needed.

In this tutorial, you will learn on the go—as fundamental blocks of code are explained, new essential insights will be introduced.

Repo: https://github.com/nexmo-community/nopanic

Demo: https://nopanic-app.web.app

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Start building with Vonage

What is Vue.js?

Vue (pronounced /vjuː/, as in “view”) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only and is easy to pick up and integrate with other libraries or existing projects.

Installing Node.js

As a prerequisite for using the Vue CLI, you will need to install Node.js and NPM (a package manager). One of the best ways to install Node.js is by using NVM (Node Version Manager), because with NVM you can install different versions of Node.js and switch versions depending on the project you are working on.

You can find more ways to install it, depending on your operating system or your particular situation, in the official repository: https://github.com/nvm-sh/nvm.

Assuming a Linux environment, you can run the following in your terminal:

Check that it’s installed correctly:

If everything is okay, you’re ready to install Node.js. At the moment of writing this tutorial, I used version 12.12.0.

Having finished the installation, you can check which Node.js version was installed by running:

Installing vue-cli

Vue CLI aims to be the standard tooling baseline for the Vue ecosystem. It ensures the various build tools work smoothly together with sensible defaults so the developer can focus on writing their app instead of spending time with build tool configurations.

You can read more about its use and features here.

Vue CLI should be installed globally with NPM, adding the parameter -g to the installation command:

Then, you can check that you have version 3.x with this command:

You can also check out the help section:

Developing the app 🤓

Creating the project

Vue CLI allows you to create a project with a directory structure, and it will also guide you through a series of questions so that you can configure the tools according to the needs of your project.

To start, execute the following command:

First, it will ask about certain tools. The default ones are Babel and ESLint. Use the arrow keys to choose Manually select features, where you can enable more features for the CLI to configure for you. To select more features, use arrow keys and press the space bar to select or unselect. Once you’ve finished, press enter .

The second prompt is about whether you want to use history mode for the router, to which you should respond Y (yes).

The third question is about linter and formatter configuration—pick the ESLint + Prettier option.

Then, it will ask when Lint should make changes—pick Lint on save.

For the last prompt about where to place configurations, select the default option, In dedicated config files.

The project creation process, installation of dependencies and configuration setup may take a few minutes, depending on your connection speed and processing capacity.

Running the app in development mode

As you’re developing, you need to have your app running so that you can see the results in your browser. To do this, go to the root directory of your app and run the following:

Once it is compiled successfully, you’ll see a message with the URLs you can use to load your app in the browser. In this case, you should always use http://localhost:8080. When you make changes to your files, the server will restart the compilation process and refresh the browser, allowing you to instantly see any changes made.

Adding small configurations

This is a configuration related to Prettier, which indicates that single quotes should always be used and that semicolons at the end of sentences should be removed:

Modify the .eslintrc.js file so that this section appears as follows:

Installing Vuetify 💅

Vuetify is a component library that allows you to develop your UI more quickly, all while complying with Material standards, thus making your app compatible on both desktop and mobile.

To install Vuetify in your project, simply run:

If you would like to read more about Vuetify and components, you can start here.

Installing other libraries 📚

You’ll need to use other libraries that allow you to consume third-party services. In this case, we’ll be consuming services from Firebase for both authentication and database, as well as back-end processing with their Cloud Function service.

Use NPM to install packages, and make sure to add the parameter --save so that the package is added to the list of dependencies in the package.json file.

Adding a configuration to load components globally

You can make Vue load different components that can be used in websites and other components without having to “import” them into the scripts to be used. In your configuration, these components should have names starting with Base, and they should also be in the /src/components directory, where the files will have a .js or .vue extension.

Let’s edit the /src/main.js file:

Creating your first component 👩‍🎨

The first component you’re going to create is a global component for the top navbar, which will allow you to show the app title and navigation menu.

What is a component?

A web component is a DOM element created with reusable code. Each component is designed to save developers time when creating websites that use similar elements across lots of pages.

The idea behind a web component is to package a UI or app element in a functional and encapsulated way. Thus, the HTML, CSS, and JavaScript used by a piece of your web app are all encapsulated in a component, without interfering with the functioning of other sections of the app, resulting in apps that are more scalable and code that is more reusable.

If you are interested in more Vue-related topics, take a look at the documentation below:

  • Component basics: https://vuejs.org/v2/guide/components.html
  • Template syntax: https://vuejs.org/v2/guide/syntax.html
  • Event handling: https://vuejs.org/v2/guide/events.html
  • Props: https://vuejs.org/v2/guide/components-props.html
  • Routing: https://vuejs.org/v2/guide/routing.html
  • State Handling with Vuex: https://vuex.vuejs.org/guide/state.html

BaseNavbar Component

In Vue, you can insert all of your HTML, CSS and Javascript or Typescript code in a single file. Vue files use the .vue extension and have three well-delimited sections:

  • <template></template>, where the HTML code goes, and where there should only be a single parent tag
  • <script></script>, where your JavaScript or TypeScript code is stored
  • <style scoped></style>, where you write your CSS or SASS

Now, you’ll need to use the component in your app. Note that there are also links to certain routes that don’t exist yet, but you’ll be creating them later.

Vuetify’s v-list-tile components allow you to insert links with the to attribute. That’s why you’ll see that <v-list-tile :to="{ path: '/contacts' }"> guides you to insert a link to the /contacts path.

Using BaseNavbar in the app

Now, you’ll need to modify the /src/App.vue file, which is the app’s main component. This component will be responsible for loading and displaying all of your components.

In this case, we’ll display the BaseNavbar component, and we’ll dynamically display the pages of the app with the app router.

Vuetify needs our app to be “wrapped” in a component called v-app, so you should also keep that in mind.

Adding routes 🛣

In Vue, all visual elements are expressed via components, but there are two types:

  • Pages or views that will be stored in the /src/views directory. These components correspond to a complete view, like when you go to different section of the app, such as: Users, Contacts, Clients, etc.
  • Components, which are stored in the /src/components directory. These are small pieces of the UI that can be reused or whose logic should be encapsulated, such as: navbar, sidebars, buttons, forms, etc. They are usually used with Page-type components.

The Vue Router allows you to change the Page component being visualized; it will load wherever you include the tags <router-view :key="$route.fullPath"></router-view>.

The /src/views/HelloWorld.vue filename will change to Dashboard.vue

Make sure that your /src/router.js file has the following content:

Adding Firebase to your project 🔥

Create a Firebase account

  • First, go to: https://firebase.google.com/
  • Log in with your Google or GSuite account
  • Click on “Add Project”
  • Enter the name of your project, for example: no-panic

Connect the project to the Firebase app

Once the project has been created in Firebase, you’ll need to access its services from the app. From the Firebase console, click on the gear icon (Settings) > Project settings.

On the General tab, enter the public-facing name and select the support email.

On the same tab, in the Your apps section, click on Add app. When given the option to choose the platform, select the web icon. Then, enter a nickname for your Firebase app. You can leave everything else as-is and set it up later.

Once the app has been created, you’ll have to initialize the SDK in your app, meaning you’ll need the configuration object. In the details of the recently created projects, there are several options for the Firebase SDK snippet: automatic, CDN and Config. Select the latter and copy the content displayed by Firebase.

Create the /src/utils/firebaseSecrets.js file, where the content will be similar to the following:

Then, create another file with path /src/utils/firebase.js, which will take care of initializing the SDK:

Google as an identity provider

Using the sidebar menu, go to Authentication and activate the service by clicking on the Start button. On the Access Methods tab, enable the Google access method and click Save.

Adding the AuthService functionality

You’ll create a small service with which you can interact with the SDK functions for authentication. You’ll also create a log for users who register for the first time so that their settings can be stored in database.

Create the /src/services/AuthServices.js file with the following content:

Creating the Sign Up page

You can create your own Sign Up page, which will also allow users to log in. The file path is /src/views/SignIn.vue and it will have the following content:

This is a very simple page displaying a button to Sign Up or Login using Google. When you click on this button, it executes an event: @click.prevent="signInWithGoogle". This event executes a function (a Vuex Action), which you can access from the component with mapActions, which will allow you to use the action as if it were a method defined in the component. You can add the action to your code later.

Configuring the router

Edit the router, /src/router.js, adding the following:

The navigation guard beforeEach allows you to evaluate the route and decide what to do with it before executing an action associated with said route. Check to see if the route that it matched with has any meta attributes named requiresAuth, which is the attribute you should use to mark the routes that you want to protect with user authentication.

This way, if the route requires authentication and there is an active user, you can use the next() function to go to the route; otherwise, you will be redirected to the route named sigin. Finally, if no authentication is required, the route will be loaded.

Obtaining the user’s information after they’ve logged in

Once the user has been correctly authenticated with the SSO, Firebase will redirect to your domain, so you should tell the app to check with SDK whether the authentication state has changed and, if it has, to provide you with the user’s information. This functionality can be added in the /src/App.vue file:

If there is no authenticated user, the onAuthStateChangedvalue will be null for user, and once it is authenticated, it will dispatch two actions from the store: user/signInWithUserAndToken and user/fetchOrCreateUser, which you can add soon.

Adding authentication to the app state

A few concepts

  • State: Vuex uses a single state tree. This single object contains all your different application-level states and serves as the “single source of truth”. The data you store in Vuex follows the same rules as the data in a Vue instance.
  • Mutation: The only way to actually change state in Vuex is through mutations, which are very similar to events: each mutation has a string type and a handler (handler function). The handler function is where actual state modifications occur, and the handler function always has the state as the first argument:
  • Actions: These are similar to mutations, the differences being that, instead of mutating the state, actions call mutations, and actions can contain arbitrary asynchronous operations.

Using modules, creating the user module for the store

Instead of having the app state in a very large object, you can divide your store into several modules that will also have code for: actions, mutations, getters, etc. You can read more about using modules in the Vuex documentation: https://vuex.vuejs.org/guide/modules.html

Create a directory, /src/store/modules, and move /src/store.js to the path /src/store/store.js. Then, create your first module, the user module, by creating a /src/store/modules/user.js file with the following content:

Then, import the new module to storage, editing /src/store/store.js:

The actions you just added to the user module are called from App.vue using store.dispatch. They can also be dispatched by a method in a component with this.$store.dispatch, as a parameter using the module name, the action, and possible information as a parameter, such as: this.$store.dispatch('user/signInWithUserAndToken', { user, token }).

Check in App.vue whether the user is logged in to run some redirects

You can access the store state from App.vue to verify an isLogged value, and then obtain the user’s information from the database and redirect them to the home view, if they are authenticated.

Let’s edit the /src/App.vue file:

That completes the entire user login/sign up process. You can take a look at the repository files to see the styles used and a few other details that have been omitted to simplify what is shown here.

Configuring the dashboard

This component will show a map with the user’s location and the button for sending a panic message, which is the app’s main function. You can also edit the message that will be sent via SMS.

You can look up the device’s geolocation in lifecycle hook created(), which will run once the component has been instantiated. Store the setInterval function in the component’s data so that the interval memory can be released with lifecycle hook beforeDestroy(), which is run before destroying the component.

Before getting started, you’ll need to install several components that we’re going to use:

Writing the template

Add the code from the template, where you’ll also be adding attributes to the field components, which will allow you to interact with Vuelidate and perform a few simple validations.

This component will pull information from the store and dispatch actions. You will also be mapping the State and Actions to be accessed from this component.

In the input @blur events, use the $touch method from the fields stated in Vuelidate. This is to label the field as $dirty, which will force Vuelidate to validate the information entered into the input. Then use v-model for the double binding between the template input and a data attribute.

Writing the functionality

With regards to customMessage, you should use a setter and a getter, where the getter returns the stored message to the Store, while the setter stores the new value written by the user in the component’s data. This is done this way so that the new value won’t replace the value in the Store until the Save button sends for the new message to be saved in the database, thus keeping you from accidentally changing the customMessage value in Store.

Defining actions in the Store

As you’ve seen, the Dashboard gets data from the Store and also dispatches actions. You’ll need to define each one:

/src/store/store.js

You can see that we’re using the HTML5 API to obtain the device’s geolocation through the web browser: navigator.geolocation.getCurrentPosition. Then we save the coordinates in the app’s store to make it available for the components that need this information.

/src/store/modules/user.js

To update the customMessage, you should first save the new string in the Firebase database with an AuthService function. Once this asynchronous function has been resolved, you can change the customMessage in your app’s Store, making it consistent with the existing one in the database.

/src/services/AuthService.js

This method requires the user’s uid and the new string for the customMessage. Updating the log is quite easy to do by using the update method for the reference to the document in the database and the object containing the attribute to be updated along with the new value.

Using Google Maps on the Dashboard

Obtaining the API Key

To use Maps JavaScript API, you’ll need an API Key, which is a unique identifier used to authenticate the requests associated with your project for payment accounting purposes.

To get an API key, do the following:

  • Go to the GCP Console: https://cloud.google.com/console/google/maps-apis/overview
  • Use the drop-down menu to select or create the project that you want a new API Key for
  • Click the button to open the sidebar and go to API & Services > Credentials
  • On the Credentials page, click on Create Credentials > API key. The API Key Created dialog will show the new API Key.

You can read more about the Google Maps API Key in the documentation: https://developers.google.com/maps/documentation/javascript/get-api-key

Initializing the library

You can initialize the library with a function that returns a promise, which is resolved once the Google Maps library is downloaded in the browser and initialized.

Here is a basic example of how to create a map and add markers: https://developers.google.com/maps/documentation/javascript/adding-a-google-map

To actively use the Google Maps library in your Vue project, create a new script, /src/utils/gmaps.js:

Creating a component to display the map

The choice to create a minimalistic component specifically for the map is aimed at limiting the number of dependencies in your app and reducing the final size of the app, since it will only contain what’s needed.

The required prop for the component is center, which contains the coordinates for the center of the map and can receive a collection of markers via a prop with the same name.

The mounted lifecycle hook is asynchronous, and it waits for the Google Maps library to be initialized so that it can proceed to render the map using the drawMap method, adding a marker to represent the user’s location on the map.

Create a /src/components/GoogleMaps.vue component:

Using the GoogleMaps component on the Dashboard

Once you have the new component, edit the /src/views/Dashboard.vue file to display the map:

Initializing Google Cloud Functions

We are going to create a Python cloud function to send the SMS. Why Python? Because Python has good performance for back-end tasks, as well as being popular and friendly to use.

You’ll need a backend service that listens for requests from the web app for sending SMS messages. You could create an app in Python with Flask for this purpose, but to make things easier, let’s use Google’s Cloud Functions service.

At this point maybe you’ll notice that we are talking about using Google Cloud functions and not a Firebase Cloud function. This is because Firebase cloud functions only allow you to deploy functions written in JavaScript and TypeScript. So to achieve our purpose, we are going to use Google Cloud functions to deploy our Python backend and the firebase-functions library to execute our function in the frontend.

Cloud Functions allow you to automatically run back-end code in response to events triggered by other Firebase services, as well as HTTP requests. The code is stored in the Google cloud and is run in a controlled environment that does not need to be managed.

To deploy your function the gcloud cli will be required. You can check this link to follow the instructions for installation according to your platform.

When the process finishes you will connect to your Google account. You can start by checking your profiles:

Because no profiles have been created, you can configure yours:

Once this process is complete, you are ready for deployment operations.

For Python there are some simple rules that we need to follow for deployment purposes. First, we’ll create a functions directory, then proceed to create the files requirements.txt and main.py (empty at the moment) inside the nopanic directory. Now you will have a similar structure to the one shown below:

Let’s create our requirements.txt file, with the next lines:

When we deploy a cloud function written in Python, the gcloud cli checks this file and installs the dependencies. As you can see there are three of them: Flask (just to use some handy functions from the flask module), nexmo (for sending the SMS messages), and firebase-admin (to verify the Firebase token, allowing us execute requests from our environment and deny unauthorized requests).

Now we can focus in the cloud function development.

Creating the function for sending SMS messages with Vonage

Your back end will have a single, very simple function. To set it up, you’ll need some data from your Vonage account, and you’ll need to install the Nexmo library.

It’s a good practice to store your credentials in environment variables. To send as SMS, you’ll only need NEXMO_API_KEY, NEXMO_API_SECRET, and NEXMO_NUMBER. We will init them later when deploying the function.

Your function will listen for an HTTP event, and the expected data will be the actual message and the recipient’s phone number. Because we are using gcloud functions instead of Firebase cloud functions, we need to write our own authentication and validation to send requests to our Cloud Function.

Add the following code to the main.py file:

In this file we define the firebase_auth_required decorator—in other words a wrapper function that allows the user to verify the Firebase token. Then we have send_sms (our cloud function), using the @firebase_auth_required indicating that authorization goes first and if it succeeds then the cloud function is executed.

Inside the nopanic directory, use the following command to deploy the function you just created:

send_sms is the name of the function we want to deploy on the cloud (gcloud cli is going to read main.py automatically). This is the right moment to define our environment variables—here we define the version of Python used for deployment and the project where our function is going to be deployed.

It is important to define the same project we are using with our firebase credentials.

Creating the request to send SMS messages from the app

On your app Dashboard, there is a button that will run the sendPanicSMS method, which is an action in the Store of the app. You can use this action to call the Cloud Function as if it were a function of your web app, obtaining the Cloud Function reference with firebase.functions().httpsCallable('send_sms').

Edit the /src/store/store.js file by adding:

Once this is done, you can send SMS messages using Nexmo and Google Cloud Functions.

Deployment

Deploy the application using Firebase hosting 🔥

  • Login to Firebase with the CLI
  • Compile the application using the command

  • Deploy the application using the CLI

Deploy with Docker ⛴

  • Install Docker in your environment
  • Create the Docker image, using:

  • Upload the image to the registry you are using (i.e: Dockerhub)
  • To run the app in your production server you may use the following command: (Remember the port mapping is <HOST_PORT>:<CONTAINER_PORT>. The image configuration of this application exposes the port 80 of the container)

Deploy the Docker image using Kubernetes in GCP (GKE) 🚀

A good option to host and run our app is using Google Cloud Platform, in which we can store our Docker images and run the application inside a single VM or using Kubernetes. GKE is very easy to start with and ensures we have scalability and many more resources at hand.

There are a few things you need to set up before deploying your app to GKE. The current description is to be run locally, but it can be run from the Console in GCP without installing the CLI (because it is already installed).

Installing gcloud CLI

In the documentation you can find how to install the CLI in another OS https://cloud.google.com/sdk/docs/quickstarts?hl=es-419. Make sure to have installed Python 2.7 or greater, then follow the next commands to install the CLI:

When you login, you will be asked for the project to use as default. The list of your projects will be displayed on the console.

It’s a good practice to specify the region and zone. If you want to know which zone is best for your project please check the documentation: https://cloud.google.com/compute/docs/regions-zones/?hl=es-419#choosing_a_region_and_zone. In this case I’ll be using us-east1-b.

Install the command tool to manage Kubernetes:

Add the app image to Container Registry

Before pulling or pushing images to Container Registry you need to configure Docker to use gcloud to authenticate requests to Container Registry.

Before pushing your newly created image to Container Registry, you need to tag your image to be able to be pushed to your project registry.

Now you may push the image to the Registry

Creating the cluster in GKE

Now that you have your image in the Container Registry you may use Kubernetes to run your application. A cluster consists of at least one cluster master machine and multiple worker machines called nodes. Nodes are virtual machines that run the Kubernetes processes.
Create your cluster with the following command:

To interact with your cluster you need to authenticate using this command:

GKE uses Kubernetes objects to create and manage your cluster’s resources. Kubernetes provides the Deployment object for deploying stateless applications like web servers. Run the following command:

After deploying the application, you need to expose it to the internet so that users can access it. --port initializes public port 80 to the internet and --target-port routes the traffic to port 80 of the application:

You may inspect the Service running the command:

And you may see the EXTERNAL-IP from which you may access your application.


Whew! that was definitely exciting!! Hope you enjoyed it as much as I did!!

Leave a Reply

Your email address will not be published.

Get the latest posts from Nexmo’s next-generation communications blog delivered to your inbox.

By signing up to our communications blog, you accept our privacy policy , which sets out how we use your data and the rights you have in respect of your data. You can opt out of receiving our updates by clicking the unsubscribe link in the email or by emailing us at [email protected].