Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 20, 2019 03:54 pm GMT

No Panic! Vue.js Google Maps Firebase Nexmo SMS Deployment! (HANDS ON STEP BY STEP TUTORIAL)

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!!

WARNING: This app is not intended to be used in cases of real emergency. And will be used solely for educational purposes. In case of emergency dial 911(US)/999(UK) or your country's emergency number.

Dedicated to good friendships and fostering collaboration in the tech ecosystem. I met my now good friend Chloe Condon at NG Conf in 2019 and this tutorial is a result of joint work!. To more collabs!

Repo: https://github.com/alphacentauri82/nopanic
Demo: https://nopanic.codeon.rocks

No-Panic!

No-Panic is an app built with Vue.js, to track the user's location in real time and send it to their trusted contacts with SMS messages.

To develop the features of the app, the following will be used: Firebase (real-time database, functions), Google Maps for maps and geolocation, and, finally, Nexmo for SMS messaging.

This app needs access to the device's geolocation. However, access to this data is only allowed for websites using HTTPS, with the exception of the localhost domain; otherwise, the feature will be blocked by the browser. By deploying through Firebase Hosting for example, 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 as well.

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 Vue CLI (command line), you will need to have 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:

$ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.0/install.sh | bash$ export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

Check that its installed correctly:

command -v nvm

If everything is okay, youre ready to install Node.js. As of the writing of this tutorial , the LTS version is 10.16.3 and the latest version is 12.12.0. For this tutorial, you'll install version 12.12.0.

nvm install 12.12.0

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

node --version

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:

npm install -g @vue/cli

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

vue --version

You can also check out the help section:

vue --help

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:

vue create no-panic

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 click the space bar to select or unselect. Once you've finished, click the enter button.

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, where you should 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:

npm run serve

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:

module.exports = {  singleQuote: true,  semi: false};

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

extends: [    'plugin:vue/essential',    'plugin:prettier/recommended',    '@vue/prettier'  ],

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:

$ cd no-panic$ vue add vuetify

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 back-end processing with their Cloud Function service, which is an interesting serverless option.

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.

npm install --save firebase

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:

import upperFirst from 'lodash/upperFirst'import camelCase from 'lodash/camelCase'/** * Automatic global register of components */const requireComponent = require.context(  './components',  false,  /Base[A-Z]\w+\.(vue|js)$/)requireComponent.keys().forEach(fileName => {  const componentConfig = requireComponent(fileName)  const componentName = upperFirst(    camelCase(fileName.replace(/^\.\/(.*)\.\w+$/, '$1'))  )  Vue.component(componentName, componentConfig.default || componentConfig)})/** ----- */new Vue({  router,  store,  render: h => h(App)}).$mount('#app')

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 a 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:

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
<template>  <div>    <!-- Top Nav toolbar -->    <v-toolbar app absolute color="#220886">      <!-- Dropdown of main navigation -->      <v-menu bottom right nudge-bottom="42">        <template v-slot:activator="{ on }">          <v-btn icon v-on="on" class="white--text">            <v-icon>more_vert</v-icon>          </v-btn>        </template>        <v-list class="pt-0">          <!-- Dashboard -->          <v-list-tile :to="{ path: '/' }">            <v-list-tile-avatar>              <v-icon>home</v-icon>            </v-list-tile-avatar>            <v-list-tile-title>Dashboard</v-list-tile-title>          </v-list-tile>          <!-- Contacts -->          <v-list-tile :to="{ path: '/contacts' }">            <v-list-tile-avatar>              <v-icon>contacts</v-icon>            </v-list-tile-avatar>            <v-list-tile-title>Contacts</v-list-tile-title>          </v-list-tile>        </v-list>        <v-list>          <!-- Users -->          <v-list-tile :to="{ path: '/users' }">            <v-list-tile-avatar>              <v-icon>people</v-icon>            </v-list-tile-avatar>            <v-list-tile-title>Users</v-list-tile-title>          </v-list-tile>          <!-- Logout -->          <v-list-tile>            <v-list-tile-avatar>              <v-icon>mobile_off</v-icon>            </v-list-tile-avatar>            <v-list-tile-title>Sign Out</v-list-tile-title>          </v-list-tile>        </v-list>      </v-menu>      <!-- end of menu -->      <!-- Title of the Navbar -->      <v-toolbar-title class="headline white--text"        >No Panic</v-toolbar-title      >    </v-toolbar>  </div></template><script>export default {  data() {    return {      drawer: {        open: false,        mini: false,        clipped: true,        right: false      }    }  },}</script><style scoped></style>

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.

<template>  <v-app>    <BaseNavbar />    <v-content>      <router-view :key="$route.fullPath"></router-view>    </v-content>  </v-app></template><script>export default {  name: 'App',  components: { },  data() {    return {      updateLocation: null    }  },}<-script>

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 Dahsboard.vue

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

import Vue from 'vue'import Router from 'vue-router'import Dashboard from '@/views/Dashboard.vue'Vue.use(Router)const router = new Router({  mode: 'history',  base: process.env.BASE_URL,  routes: [    {      path: '/',      name: 'home',      component: Dashboard,      meta: {        requiresAuth: true      }    },  ]})export default router

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:

export default {  apiKey: 'XXXXXXXXX',  authDomain: 'xxxxxx.firebaseapp.com',  databaseURL: 'https://xxxxxx.firebaseio.com',  projectId: 'xxxxxxx-xxxxxx',  storageBucket: 'xxxxxxxxxxxxxx.appspot.com',  messagingSenderId: 'xxxxxxxx',  appId: 'xxxxxxxx:web:xxxxxxxxxxxxxxx'}

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

// Firebase App (the core Firebase SDK) is always required and must be listed firstimport firebase from 'firebase'// importing configurationimport firebaseSecrets from './firebaseSecrets'// Initialize firebasefirebase.initializeApp(firebaseSecrets)export default firebase

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:

import firebase from '@/utils/firebase'const Auth = firebase.auth()const db = firebase.firestore()export default {  /**   * Calls the signInWithRedirect() Firebase Auth method to start the OAuth Flow   * @return "{ user, token }" An object with the user and access token   */  async signInWithGoogle() {    // creating the provider    const provider = new firebase.auth.GoogleAuthProvider()    let token = null    let user = null    // calling the sign in    await Auth.signInWithRedirect(provider)      .then(result => {        // This gives you a Google Access Token. You can use it to access the Google API        token = result.credential.accessToken        //The signed-in user info        user = result.user      })      .catch(err => {        throw err      })    return { user, token }  },  /**   * Calls the Sign Out method of Firebase Authh   * @return Promise<any> Returns the Promise of signOut Firebase method   */  signOut() {    return Auth.signOut()  },  /**   * If the session exists, returns the current user from Firebase   * @returns Object  The User object from Firebase   */  async getCurrentUser() {    let user = JSON.parse(localStorage.getItem('user'))    if (user) {      return user    }    return await Auth.currentUser  },  async fetchOrCreateUser(fireUser) {    // getting the user document by the ID    const userRef = db.collection('users').doc(fireUser.uid)    let user = null    await userRef      .get()      .then(readDoc => {        // if the user is found in the DB        if (readDoc.exists) {          user = readDoc.data()        }      })      .catch(err => {        throw err      })    if (!user) {      // if the user doesn't exists then create it      user = {        displayName: fireUser.displayName,        email: fireUser.email,        phoneNumber: fireUser.phoneNumber,        isAdmin: false,        contacts: [],        createdAt: firebase.firestore.Timestamp.fromDate(new Date()),        customMessage:          'URGENT! This is an emergency, my last location is <LOCATION>'      }      await userRef.set(user).then(() => {        console.log(`Document sucessfully witten.`)      })    }    return user  },}

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:

<template>  <v-container fluid fill-height>    <v-layout align-center justify-space-between column fill-height>      <img src="@/assets/logo.svg" alt="g-logo" class="panic-logo" />      <p class="text-xs-center">        Track your current position and send it to your trusted contacts through        SMS      </p>      <v-btn        @click.prevent="signInWithGoogle"        class="white--text mb-5"        color="#220886"        large        round      >        <v-icon class="mr-2">mdi-google</v-icon>Sign in with Google      </v-btn>    </v-layout>  </v-container></template><script>import { mapActions } from 'vuex'export default {  methods: {    ...mapActions('user', ['signInWithGoogle'])  }}</script><style scoped>.panic-logo {  height: 30%;  margin: 15% auto 0;  width: 74%;}</style>

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

Then, edit the router, /src/router.js, and add the following:

...import SignIn from '@/views/SignIn.vue'import AuthService from '@/services/AuthService'...const router = new Router({  mode: 'history',  base: process.env.BASE_URL,  routes: [    ...    {      path: '/sigin',      name: 'sigin',      component: SignIn    }  ]})router.beforeEach(async (routeTo, routeFrom, next) => {  // get the current user  const currentUser = await AuthService.getCurrentUser()  const requiresAuth = routeTo.matched.some(record => record.meta.requiresAuth)  if (requiresAuth) {    if (currentUser) {      next()      return    }    next('sigin')  } else {    next()  }})export default router

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:

...import firebase from '@/utils/firebase'import store from '@/store/store'/** * Observer waiting for the user get obtained from Firebase SDK * if the user is logged in and the token is not expired */firebase.auth().onAuthStateChanged(function(user) {  if (user) {    // if the user is obtained, then converting it to JSON    const jsonUser = user.toJSON()    // dispatching the log in    store.dispatch('user/signInWithUserAndToken', {      user: jsonUser,      token: jsonUser.stsTokenManager.accessToken    })    // getting the user information from DB    store.dispatch('user/fetchOrCreateUser')  }})...

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:

import AuthService from '@/services/AuthService'export const namespaced = trueexport const state = {  user: JSON.parse(localStorage.getItem('user')) || {},  token: localStorage.getItem('token') || '',  isLogged: false,  isAdmin: false,  contacts: [],  customMessage: ''}export const mutations = {  SET_LOG_IN(state, { user, token }) {    state.isLogged = true    state.user = user    state.token = token    // set in local storage    localStorage.setItem('user', JSON.stringify(user))    localStorage.setItem('token', JSON.stringify(token))  },  SET_ADMIN_USER(state, isAdmin) {    state.isAdmin = isAdmin  },  SET_CONTACTS(state, contacts) {    state.contacts = contacts  },  ADD_CONTACT(state, contact) {    state.contacts.push(contact)  },  SET_CUSTOM_MESSAGE(state, message) {    state.customMessage = message  },  LOGOUT(state) {    state.isLogged = false    state.user = {}    state.token = ''    // remove from local storage    localStorage.removeItem('user')    localStorage.removeItem('token')  }}export const actions = {  /**   * Set the login using the user and token of an already opened session with Firebase   * @param {*} Vuex objects   * @param {*} User Object with the User Info and Access Token from Firebase   */  signInWithUserAndToken({ commit, dispatch }, { user, token }) {    commit('SET_LOG_IN', { user, token })    // show notification    publishNotification('success', 'Signed In!', dispatch)  },  /**   * Fetches the information of the User of sign in, if exists in DB, else creates it   * @param {*} Vuex Objects   */  async fetchOrCreateUser({ state, commit }) {    const user = await AuthService.fetchOrCreateUser(state.user)    if (user) {      // commit in the state the stored info in DB      commit('SET_ADMIN_USER', user.isAdmin)      commit('SET_CONTACTS', user.contacts)      commit('SET_CUSTOM_MESSAGE', user.customMessage)    }  },  /**   * Starts the Sign In with Google Flow of Firebase   * @param {*} Vuex Objects   */  async signInWithGoogle({ commit, dispatch }) {    const result = await AuthService.signInWithGoogle().catch(err => {      // make sure is sign out      commit('LOGOUT')      console.error('ERROR:', err)      publishNotification(        'error',        'There was an error creating the user. ' + err.message,        dispatch      )    })    if (result && 'user' in result) {      commit('SET_LOG_IN', { user: result.user, token: result.token })      // show notification      publishNotification('success', 'User successfuly signed in!', dispatch)    }  },  /**   * Perform the Sign Out with Firebase   * @param {*} Vuex objects   */  singOut({ commit, dispatch }) {    AuthService.signOut()      .then(() => {        commit('LOGOUT')      })      .catch(err => {        console.error('ERROR:', err)        publishNotification(          'error',          'There was an error when signing out. ' + err.message,          dispatch        )      })  }}

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

import Vue from 'vue'import Vuex from 'vuex'// importing modulesimport * as user from '@/store/modules/user.js'...export default new Vuex.Store({  modules: {    user  },  state: { },  mutations: {    SET_FETCHING_USER(state, flag) {      state.isFetchingUser = flag    }  },  actions: { }  ...})

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 that the isLogged value and thus 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:

<script>import { mapState } from 'vuex'export default {  name: 'App',  computed: {    ...mapState({      isLogged: state => state.user.isLogged    })  },  watch: {    isLogged: function(newVal) {      // observing the changes in isLogged in order to      // change of view, to home or sig in      console.log(`isLogged: ${newVal}`)      if (newVal) {        this.$router.push({ name: 'home' })      } else {        this.$router.push({ name: 'sigin' })      }    }  }}

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:

npm install --save vuelidate

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.

<template>  <v-container fluid grid-list-md>    <v-layout row fill-height justify-center>      <v-flex xs12 md6>        <div class="text-xs-center">          <p class="my-2 text-xs-center send-panic-text">Send panic Message</p>          <button @click.prevent="sendPanicSMS" class="panic-button">            <v-icon class="panic-button-icon">mdi-message-alert</v-icon>          </button>        </div>      </v-flex>    </v-layout>    <v-layout row fill-height justify-center class="mt-3">      <v-flex xs12 md6>        <v-expansion-panel>          <v-expansion-panel-content>            <template v-slot:header>              <div>Personalize Panic Message</div>            </template>            <v-flex xs12 md12>              <v-card>                <v-card-title primary-title>                  <div>                    <h3 class="headline mb-0">Text Message Template</h3>                    <p class="mb-0">Customize the text message for the SMS</p>                  </div>                </v-card-title>                <v-card-text>                  <v-layout row wrap>                    <v-flex md12 xs12>                      <v-textarea                        @blur="$v.newMessage.$touch()"                        v-model="customMessage"                        :hint="                          `Use < LOCATION > to tell the app where to insert the coordinates. Max 160 characters.`                        "                        :error-messages="messageErrors"                        color="#220886"                        label="SMS Message"                        outline                        persistent-hint                        required                      ></v-textarea>                    </v-flex>                    <v-layout align-center>                      <v-btn                        @click.prevent="updateMessage"                        :disabled="                          !$v.newMessage.$dirty || $v.newMessage.$anyError                        "                        class="white--text btn-google"                        color="#220886"                        block                        large                        round                        >Save</v-btn                      >                    </v-layout>                  </v-layout>                </v-card-text>              </v-card>            </v-flex>          </v-expansion-panel-content>        </v-expansion-panel>      </v-flex>    </v-layout>    <v-layout row fill-height justify-center>      <v-flex md6 xs12>        <v-card>          <v-card-text class="pa-1">            THE MAP GOES HERE!          </v-card-text>        </v-card>      </v-flex>    </v-layout>  </v-container></template>

This component will pull information from the store and dispatch actions, so 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 the 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

<script>import { mapState, mapActions } from 'vuex'import { setInterval, clearInterval } from 'timers'import { required, maxLength } from 'vuelidate/lib/validators'export default {  data() {    return {      map: null,      tileLayer: null,      myMarker: null,      layers: [],      newMessage: '',      updateLocation: null    }  },  validations: {    newMessage: { required, maxLength: maxLength(160) }  },  created() {    // set an interval to update the location every 10 seconds    this.updateLocation = setInterval(() => {      this.$store.dispatch('getCurrentLocation')    }, 20000)  },  beforeDestroy() {    // cleaning up the memory if the component is destroyed    clearInterval(this.updateLocation)  },  mounted() {    this.$store.dispatch('getCurrentLocation')  },  computed: {    customMessage: {      get() {        return this.message      },      set(value) {        this.newMessage = value      }    },    messageErrors() {      const errors = []      if (!this.$v.newMessage.$dirty) return errors      !this.$v.newMessage.required && errors.push('Message is required')      !this.$v.newMessage.maxLength &&        errors.push('Message cannot have more than 160 chars')      return errors    },    ...mapState({      coordinates: state => state.coordinates,      message: state => state.user.customMessage    })  },  methods: {    updateMessage() {      this.updateCustomMessage(this.newMessage)      // to close the bottom sheet      this.sheet = false    },    ...mapActions('user', ['updateCustomMessage']),    ...mapActions(['sendPanicSMS'])  }}

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

...import firebase from '@/utils/firebase'export default new Vuex.Store({  state: {    coordinates: {      lat: null,      lng: null    }  },  mutations: {    SET_LOCATION(state, coor) {      // setting the coordinates of the position      state.coordinates = { lat: coor.latitude, lng: coor.longitude }    }  },  actions: {    getCurrentLocation({ commit }) {      if (navigator.geolocation) {        // gettingt the position using the HTML5 API        navigator.geolocation.getCurrentPosition(position => {          commit('SET_LOCATION', position.coords)        })      } else {        // display notification of lack of support        const notification = {          type: 'warning',          message: 'Geolocation is not supported by this browser.'        }        console.log('ERROR:', notification)      }    },    sendPanicSMS({ state }) {      // TODO: create this functionality    }  }})

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

/src/store/modules/user.js

...export const actions = {  ...  /**   * Stores in DB the custom message to be send through SMS   * @param {*} Vuex Objects   * @param {*} newMessage New Custom Message for SMS   */  updateCustomMessage({ state, commit, dispatch }, newMessage) {    AuthService.updateCustomMessage(state.user.uid, newMessage)      .then(() => {        commit('SET_CUSTOM_MESSAGE', newMessage)      })      .catch(err => {        console.error('ERROR:', err)      })  },}...

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

...export default {  ...  /**   * Store in DB the custom message to be used in SMS   * @param {*} uid ID of the User in DB   * @param {*} newMessage Message that we want to be used in SMS   */  updateCustomMessage(uid, newMessage) {    // getting the user document by the ID    const userRef = db.collection('users').doc(uid)    return userRef.update({ customMessage: newMessage })  },}

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:

const API_KEY = 'YOUR-API-KEY'const CALLBACK_NAME = 'initMap'let initialized = !!window.googlelet resolveInitPromiselet rejectInitPromise// This promise handles the initialization// status of the google maps scriptconst initPromise = new Promise((resolve, reject) => {  resolveInitPromise = resolve  rejectInitPromise = reject})export default function init() {  // if google maps is already init  // the `initPromise` should be resolved  if (initialized) return initPromise  initialized = true  // the callback function is called  // by the Google maps script if it is  // successfully loaded  window[CALLBACK_NAME] = () => resolveInitPromise(window.google)  // we inject a new tag into the Head  // of the HTML to load the Google Maps script  const script = document.createElement('script')  script.async = true  script.defer = true  script.src = `https://maps.googleapis.com/maps/api/js?key=${API_KEY}&callback=${CALLBACK_NAME}`  script.onerror = rejectInitPromise  document.querySelector('head').appendChild(script)  return initPromise}

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:

<template>  <div class="map" ref="map"></div></template><script>import gmapsInit from '@/utils/gmaps'import { isNumber, isArray } from 'util'export default {  name: 'GoogleMaps',  props: {    center: {      type: Object,      required: true    },    markers: {      type: Array    }  },  async mounted() {    try {      // init and wait for the Google script is mounted      this.google = await gmapsInit()      // if the location is already set, for example      // when returning back to this view from another one      if ('lat' in this.myLocation && this.myLocation.lat) {        this.drawMap()        // set the current location        this.addMarker(this.myLocation)      }    } catch (err) {      console.log('ERROR:', err)    }  },  data() {    return {      google: null,      map: null,      innerMarkers: [],      userMarker: null    }  },  computed: {    myLocation() {      // if the coordinates is not set      if (!('lat' in this.center) && !isNumber(this.center.lat)) {        return null      }      // return the object expected by Google Maps      return { lat: this.center.lat, lng: this.center.lng }    }  },  methods: {    drawMap() {      if (this.myLocation.lat && this.myLocation.lng) {        // creating the map object, displaying it in the $el DOM object        this.map = new this.google.maps.Map(this.$refs['map'], {          zoom: 18,          center: this.myLocation        })        // center the canvas of the map to the location of the user        this.map.setCenter(this.myLocation)      }    },    // add a marker with a blue dot to indicate the user location    setUserMarker(location) {      this.userMarker = new this.google.maps.Marker({        position: location,        map: this.map      })    },    // Adds a marker to the map and push to the array    addMarker(location) {      // the marker positioned at `myLocation`      const marker = new this.google.maps.Marker({        position: location,        map: this.map      })      this.innerMarkers.push(marker)    },    // Sets the map on all markers in the array    setAllMarkersInMap(map) {      for (let i = 0; i < this.innerMarkers.length; i++) {        this.innerMarkers[i].setMap(map)      }    },    // Removes the markers from the map, but keeps them in the array    clearMarkers() {      this.setAllMarkersInMap(null)    },    // Deletes all markers in the array by removing references to them    deleteMarkers() {      this.clearMarkers()      this.innerMarkers = []    }  },  watch: {    marker: function(newVal) {      if (isArray(newVal)) {        // clear the markers        this.clearMarkers()        for (let i = 0; i < newVal.length; i++) {          let position = newVal[i]          if (            'lat' in position &&            isNumber(position.lat) &&            isNumber(position.lng)          ) {            // set the current location            this.addMarker(position)          }        }      }    }  }}</script><style scoped>.map,.loading {  width: 100%;  height: 400px;}</style>

Using the GoogleMaps component on the Dashboard

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

<template>...        <v-card>          <v-card-text class="pa-1">            <GoogleMaps :center="{ ...coordinates }" />          </v-card-text>        </v-card>...</template><script>...import GoogleMaps from '@/components/GoogleMaps.vue'...</script>

Sending SMS messages with Twilio

Creating a free Twilio account

To send an SMS, you'll need to use services that allow you to handle a valid phone number and funds for sending SMS messages. To do this, you can use a service called Twilio, that allows you to create a free account that will provide you with $20 for your initial Dev tests.

You can create your account at: ttps://www.twilio.com/try-twilio. After filling in your basic information, it will ask you for a phone number to send an SMS with a verification code. Once that's verified, it will show you the dashboard for your account.

To activate the phone number, go to Programmable SMS and click on the Get Started button, where the first step is accepting or choosing a phone number (you can choose the one it shows you from the start). While using the trial version, you can only send SMS messages to verified numbers, so if you want to test the messaging function, you'll have to verify the phone numbers that you'll be using for your tests.

The section for testing SMS messaging has a link for verifying phone numbers, or you can access this option from the sidebar at Phone Numbers > Verified Caller IDs.

From the Dashboard of your Twilio account, you have access to three things that you'll need for sending SMS messages:

  • Phone number
  • Account SID
  • Auth Token You'll use these in your code later.

Initializing Firebase Cloud Functions

Twilio works with backend languages such as: Java, PHP, Python and Node.js, so you'll need a backend service that listens for requests from the web app for sending SMS messages. You could create an app in Node.js with Express for this purpose, but to make things easier, let's use Firebase's Cloud Functions service.

Cloud Functions allows you to automatically run backend 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.

You need to install Firebse CLI to deploy the functions you develop.

npm install -g firebase-tools

With the Firebase CLI, you can initialize the Firebase SDK for Cloud Functions. This causes the CLI to create a new directory with an empty project that contains example dependencies and code. To initialize the project:

  • Run firebase login, log in with your browser, and authenticate the console tool.
  • Go to the root directory of your project.
  • Run firebase init functions , which will prompt whether you want to install the dependencies with NPS, which you should accept.
  • It will also allow you to choose between TypeScript and JavaScript for your project; you will choose JavaScript.

Once this process is complete, you'll have a new directory and several files in your project:

myproject +- .firebaserc    # Hidden file that helps you easily switch projects |                 # using `firebase use` | +- firebase.json  # Describes the properties of your project | +- functions/     # Directory that contains the code for all of your functions      |      +- .eslintrc.json  # Optional file that contains JavaScript linting rules      |      +- package.json  # npm file that describes the code for your Cloud Functions      |      +- index.js      # main source of code file      |      +- node_modules/ # directory where dependencies (declared in                       # package.json) are installed

Creating the function for sending SMS messages with Nexmo

Your backend will have a single, very simple function where you'll use data from your Nexmo account, in addition to installing the Nexmo package.

Check out Nexmo where you can create a free account and get a number for sending your messages!!

Let's proceed to install all required packages:

npm install --save twilio

Store the Twilio data as Cloud Function configuration variables, which you can access from your code with functions.config().var_name, which in this case are called nexmo.apikeyand nexmo.apisecret. We will also add an extra variable for the "origin" phone number

firebase functions:config:set nexmo.apikey="API_KEY" nexmo.apisecret="API_SECRET" nexmo.phone="PHONE"

Your function will listen for an HTTP event, and the expected data will be the message to be sent via SMS and the recipient's phone number. You do not need to write your own authentication and validation regarding who sends requests to your Cloud Function because Firebase will take care of this with a safe way to send requests from the app.

Replace the code in the /functions/index.js file:

```const functions = require('firebase-functions')

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions

// Init Nexmo
const Nexmo = require('nexmo');
const API_KEY = functions.config().nexmo.apikey
const API_SECRET = functions.config().nexmo.apikey
const FROM = functions.config().nexmo.phone

// init nexmo client
const nexmo = new Nexmo({
apiKey: API_KEY,
apiSecret: API_SECRET,
});

/**

  • Send SMS using Nexmo*/exports.sendSMS = functions.https.onCall((data, context) => {// getting the message and phone numberconst message = data.messageconst phoneTo = data.phoneTo

if (!message || !phoneTo) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
'invalid-argument',
'The function must be called with message and phoneTo.'
)
}

const currDate = new Date()
console.log(${currDate.toISOString()} - Sending SMS to: ${phoneTo})
console.log(${currDate.toISOString()} - Sending Message: ${message})

//sending back the promise of send the SMS through Nexmo
//Note: Because Nexmo doesn't support promises (Just callbacks). We create our Custom promise to handle nexmo response
var NexmoPromise = function(FROM, phoneTo, message){
var pro = new Promise((resolve, reject)=> {

  nexmo.message.sendSms(FROM, phoneTo, message, { type: "unicode" }, (err, responseData) => {    if (err) {      console.log(err);      resolve({ message: err, sucess: false })    } else {      if(responseData.messages[0]['status'] === "0") {        console.log("Message sent successfully.");        resolve({ message: 'Message sent.', success: true })      } else {        console.log(`Message failed with error: ${responseData.messages[0]['error-text']}`);        resolve({ message: responseData.messages[0]['error-text'], success: false })      }    }  })

})

return pro

}

//Execute the nexmo function and return the promise
return NexmoPromise(FROM, phoneTo, message)

})

To deploy the function you just created, use:

firebase deploy --only functions

#### Creating the request to send SMS messages from the appOn 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('sendSMS')`.Edit the `/src/store/store.js` file by adding:```jsexport default new Vuex.Store({...  actions: {    ...    sendPanicSMS({ state, dispatch }) {      // accesing the Cloud Function      const sendSMS = firebase.functions().httpsCallable('sendSMS')      // get the contacts      const contacts = state.user.contacts      const user = state.user.user      const customMessage = state.user.customMessage      const coor = state.coordinates      // add corrdinates to message      let reg = /<LOCATION>/gi      let message =        `FROM: ${user.displayName}. ` +        customMessage.replace(          reg,          `https://maps.google.com/maps?q=${coor.lat},${coor.lng}`        )      contacts.forEach(contact => {        sendSMS({ message, phoneTo: contact.phone })          .then(result => {            console.log(`SMS Sent to ${contact.phone}`, result)            if (result.data.success) {              publishNotification(                'success',                `SMS sent to ${contact.phone}`,                dispatch              )            } else {              publishNotification(                'error',                `Error sending SMS to ${contact.phone}.` + result.data.message,                dispatch              )            }          })          .catch(err => {            console.log(`ERROR sending to ${contact.phone}:`, err)            // publishing notification            publishNotification(              'error',              `Error sending SMS to ${contact.phone}.` + err.message,              dispatch            )          })      })    }  }...})

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

Deployment

Deploy the application using Firebase hosting

  • Login the Firebase with the CLI
  • Compile the aplication using the command
npm run build
  • Deploy the application using the CLI
firebase deploy --only hosting

Deploy with Docker

  • Install Docker in your environment
  • Create the Docker image, using
docker build -t <username/repository:tag> .
  • 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)
docker run -p 80:80 --name <container_name> <username/repository:tag>

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 we need to set up before deploying our app to GKE. The current description is to be ran locally, but can be run from the Console in GCP without installing the CLI (because is already installed).

Installing gcloud CLI

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

$ cd /opt## this is the package with binaries for 64 bits$ sudo curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-231.0.0-linux-x86_64.tar.gz$ sudo tar zxvf [ARCHIVE_FILE] google-cloud-sdk$ sudo ./google-cloud-sdk/install.sh## you may check if is successfully$ gcloud --version

Now you need to login, and you will be asked for the project to use as default, the list of your projects will be displayed on the console

$ gcloud init

Then it's a good practice to specify the region and zone, if you want to know which zone is better 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

$ gcloud config set compute/zone us-east1-b

Install the command tool to manage Kubernetes

$ gcloud components install kubectl

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.

$ gcloud auth configure-docker

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.

$ docker tag <IMAGE-ID> gcr.io/<PROJECT-ID>/<IMAGE_NAME>:<TAG>

Now you may push the image to the Registry

 docker push gcr.io/<PROJECT-ID>/<IMAGE_NAME>:<TAG>

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, it may take a few minutes

$ gcloud container clusters create <CLUSTER_NAME>

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

$ gcloud container clusters get-credentials <CLUSTER_NAME>

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

$ kubectl create deployment <DEPLOYMENT_NAME> --image=gcr.io/<PROJECT-ID>/<IMAGE_NAME>:<TAG> --port 80

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

$ kubectl expose deployment <DEPLOYMENT_NAME> --type LoadBalancer --port 80 --target-port 80

You may inspect the Service running the command

kubectl get service <DEPLOYMENT_NAME>

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!!


Original Link: https://dev.to/vuevixens/no-panic-vue-js-google-maps-firebase-nexmo-sms-deployment-hands-on-step-by-step-tutorial-5g3o

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To