An Interest In:
Web News this Week
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
- April 19, 2024
- April 18, 2024
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:
- 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
<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 onAuthStateChanged
value 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 onCreate 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.apikey
and 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 theport 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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To