In this blog post we’ll walk through how you can make a phone call from a web brower to a phone with Nexmo In-App Voice using the Nexmo Stitch JavaScript SDK and Vue.JS. Nexmo In-App Voice and In-App Messaging is in Developer Preview so we’d love your feedback on both the development experience that you have and on the functionality that’s provided. You can get in touch via the Nexmo Community Slack or by emailing [email protected].

In order to make a phone call from a web browser we’re going to need a number of components in our app. A Vue.JS application that runs in the browser and that uses the Nexmo Stitch JavaScript SDK, an application server used to authenticate the application user with Nexmo by generating a User JWT, and a phone to receive the phone call.

The sequence diagram below shows how things will work once we’ve built our app. In this blog post we’ll first create the Vue.JS application with a UI that allows a phone number to be entered. We’ll then create an application server that can generate the required User JWT. Once the app server is up and running we’ll update the Vue.JS app to retrieve the JWT and use that with the Nexmo Stitch JavaScript SDK to login to the Nexmo platform and initiate the phone call. We then need to update the app server to handle a GET request that Nexmo will make in order to retrieve instructions for how to proceed with the phone call. Those instructions will tell Nexmo to connect the call from the Vue.JS application in the web browser to a phone.

So, there are a few steps involved but the result is worthwhile.

If you’d rather dive straight into the code you can find the Call from Browser code on GitHub.

Before we begin

Yarn for package management

The Vue CLI for scaffolding out our app and running a dev server

A Nexmo account to enable us to use the Stitch JavaScript SDK and make phone calls

The Nexmo CLI to quickly create and setup a Nexmo application from the command line. Please use the beta version of the CLI e.g. npm install -g [email protected]

A local tunnel solution such as Ngrok so that the Nexmo platform can reach a locally running web server). For this blog post we’re going to use Ngrok.

With those in place, let’s get started.

Scaffold a new Vue.JS Project

Run the following vue command in the terminal and pick the default of (babel, eslint) when prompted.

$ vue create call-from-browser # navigate into the newly created Vue project folder cd call-from-browser 1 2 3 4 5 $ vue create call - from - browser # navigate into the newly created Vue project folder cd call - from - browser

This will result with you having the following directory structure and files:

call-from-browser ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ └── main.js └── yarn.lock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 call - from - browser ├── README . md ├── babel . config . js ├── package . json ├── public │ ├── favicon . ico │ └── index . html ├── src │ ├── App . vue │ ├── assets │ │ └── logo . png │ ├── components │ │ └── HelloWorld . vue │ └── main . js └── yarn . lock

We’re going to build a CallFromBrowser component so let’s rename the default HelloWorld one that was created.

mv src/components/HelloWorld.vue src/components/CallFromBrowser.vue 1 2 mv src / components / HelloWorld . vue src / components / CallFromBrowser . vue

With that we’re ready to start building the UI.

Create a Simple Phone Number Input & Dialing Interface

Vue.JS has a strong and growing ecosystem so it makes sense to utilize existing components, if there are any. Luckily, there are a few options and we’ll go with the vue-tel-input component by Steven Dao.

Install the component:

$ yarn add vue-tel-input 1 2 $ yarn add vue - tel - input

Now that we have the component to help us validate phone numbers we can add it to the CallFromBrowser component. Open src/components/CallFromBrowser.vue in a code editor.

Update the <template> element as follows:

<template> <main class="call-from-browser"> <vue-tel-input @onInput="onInput"> </vue-tel-input> <button class="call-control" v-bind:class="{'call-in-progress': callInProgress}" v-on:click="controlCallClick"></button> <p>{{infoMessage}}</p> </main> </template> 1 2 3 4 5 6 7 8 9 10 11 < template > < main class = "call-from-browser" > < vue - tel - input @ onInput = "onInput" > < / vue - tel - input > < button class = "call-control" v - bind : class = "{'call-in-progress': callInProgress}" v - on : click = "controlCallClick" > < / button > < p > { { infoMessage } } < / p > < / main > < / template >

The template utilises the <vue-tel-input> component and sets an @onInput handler. We have a <button> that will have a call-in-progress class set based on a callInProgress property value and a click handler that will call a controlCallClick method. The template also has a <p>{{infoMessage}}</p> that enables us to provide some user feedback via a data.infoMessage property that we’re just about to define.

Let’s next update the contents of the <script> tag within the same file.

<script> import 'vue-tel-input/dist/vue-tel-input.css' import VueTelInput from 'vue-tel-input' export default { name: 'CallFromBrowser', components: { 'vue-tel-input': VueTelInput }, ... 1 2 3 4 5 6 7 8 9 10 11 12 < script > import 'vue-tel-input/dist/vue-tel-input.css' import VueTelInput from 'vue-tel-input' export default { name : 'CallFromBrowser' , components : { 'vue-tel-input' : VueTelInput } , . . .

This imports the CSS and the component definition for the telephone input component, sets the name of the component to CallFromBrowser and registers the vue-tel-input component dependency so it can be used within the template.

Next let’s set up some properties for data binding such as the callInProgress property relied upon by the template and add any methods that are expected to be in place, as show, in the template:

export default { name: 'CallFromBrowser', components: { 'vue-tel-input': VueTelInput }, data() { return { phone: { number: '', isValid: false, country: {} }, infoMessage: "", callInProgress: false } }, methods: { onInput({ number, isValid, country }) { this.phone.number = number; this.phone.isValid = isValid; this.phone.country = country; if(!isValid) { this.infoMessage = "Please enter a valid phone number" } else { this.infoMessage = `Thanks for entering a valid ${this.phone.country.name} phone number` } }, controlCallClick() { } } } </script> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export default { name : 'CallFromBrowser' , components : { 'vue-tel-input' : VueTelInput } , data ( ) { return { phone : { number : '' , isValid : false , country : { } } , infoMessage : "" , callInProgress : false } } , methods : { onInput ( { number , isValid , country } ) { this . phone . number = number ; this . phone . isValid = isValid ; this . phone . country = country ; if ( ! isValid ) { this . infoMessage = "Please enter a valid phone number" } else { this . infoMessage = ` Thanks for entering a valid $ { this . phone . country . name } phone number ` } } , controlCallClick ( ) { } } } < / script >

The data function returns a phone object that we populate in the onInput handler. In that handler we set the phone number that the user has entered into the vue-tel-input component and properties representing the validity of the phone number and the country that the number is for.

We also provide the user with some feedback on the phone number validity by setting the infoMessage property. Vue data-binding means that the value we set here is reflected in the UI.

A stub controlCallClick method is also added to handle the <button> being clicked.

A final step in setting up the CallFromBrowser component is to add some styling. Replace the existing <style> element and contents with the following:

<style scoped> .vue-tel-input { width: 200px; margin: auto; } .call-control { font-size: 11em; } .call-control:before { content: '☎️'; } .call-control.call-in-progress:before { content: '?' } </style> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <style scoped> .vue-tel-input { width : 200px ; margin : auto ; } .call-control { font-size : 11em ; } .call-control:before { content : '☎️' ; } .call-control.call-in-progress:before { content : '?' } </style>

The style sets the default content of the <button> to be the red telephone emoji (☎️). If the call-in-progress class is present, dynamically set if the callInProgress property returns true then the content will instead be a an old-style phone handset emoji (?).

The final step in getting the basic UI in place is to update App.vue by replacing the template and script tags. Leave the style tag as-is.

<template> <div id="app"> <CallFromBrowser/> </div> </template> <script> import CallFromBrowser from './components/CallFromBrowser.vue' export default { name: 'app', components: { CallFromBrowser } } </script> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 < template > < div id = "app" > < CallFromBrowser / > < / div > < / template > <script> import CallFromBrowser from './components/CallFromBrowser.vue' export default { name : 'app' , components : { CallFromBrowser } } </script>

Replace <template> contents, import the CallFromBrowser.vue component definition and registering the imported component.

We can now run the app:

yarn serve 1 2 yarn serve

With this running, navigating in a browser to http://localhost:8080 and trying out entering phone numbers into the vue-input-tel component. We’ll see the phone number validated at the bottom of the app UI.

Creating a User JWT to login to the Nexmo Plaform

The Nexmo Stitch JavaScript SDK connects to the Nexmo platform to enable In-App Voice functionality within the web browser. In order to connect to the Nexmo platform we need to login with a valid user authentication JWT (JSON Web Token) for the application user that defines that user’s permissions. To create a User JWT we’re going to need to create a few things:

a simple server that generates the User JWT that can be retrieved by the CallFromBrowser Vue.JS component a Nexmo Application within the Nexmo platform – we can do this using the Nexmo CLI a User within the Nexmo Application for the current web app users

Let’s start by creating a simple server. Create a server directory, install a few dependencies and create an index.js and .env files for the functionality we need.

$ mkdir server cd server yarn init -yp #create package.json for server yarn add express body-parser cors dotenv nexmo touch index.js touch .env # for environment variables 1 2 3 4 5 6 7 $ mkdir server cd server yarn init - yp #create package.json for server yarn add express body - parser cors dotenv nexmo touch index . js touch . env # for environment variables

For the server we’re going to use Express.js with the CORS and body-parser middleware. dotenv is used to load in the .env file which itself will contain configuration that we wouldn’t want in source control. We’ve also installed the Nexmo Node.JS library to help with User JWT generation.

Before we look at the server code let’s also create the Nexmo Application and the User for that application. We can do this using the Nexmo CLI:

$ nexmo app:create call-from-browser https://example.com/answer https://example.com/event --keyfile=private.key 1 2 $ nexmo app : create call - from - browser https : //example.com/answer https://example.com/event --keyfile=private.key

Running this command will output an application ID. It will also add the Nexmo Application details to a .nexmo-app file. Take the application ID and add it to the .env file along with a variable for the private.key location:

NEXMO_PRIVATE_KEY=private.key NEXMO_APP_ID=YOUR_APPLICATION_ID 1 2 3 NEXMO_PRIVATE_KEY = private . key NEXMO_APP_ID = YOUR_APPLICATION _ ID

The last piece of Nexmo Application setup is to create a user with in the application. It’s possible to do this using Nexmo libraries but in this case we’ll set up a user using the Nexmo CLI:

$ nexmo user:create name=demo 1 2 $ nexmo user : create name = demo

This command will create the user for the application ID identified within the .nexmo-app file. Add and environment variable for the user name to the .env file.

NEXMO_PRIVATE_KEY=private.key NEXMO_APP_ID=YOUR_APPLICATION_ID NEXMO_APP_USER_NAME=demo 1 2 3 4 NEXMO_PRIVATE_KEY = private . key NEXMO_APP_ID = YOUR_APPLICATION_ID NEXMO_APP_USER_NAME = demo

Now open up index.js to add the basic server code:

// Load .env config require('dotenv').config({ path: __dirname + '/.env' }); const Nexmo = require('nexmo') const express = require('express') const bodyParser = require('body-parser') const cors = require('cors') const app = express() app.use(bodyParser.json()) app.use(cors()) // endpoint that doesn't authenticate the user // it will simply return a JWT with every request app.get('/no-auth', (req, res) => { res.json({userJwt: null}) }) app.listen(3000, () => console.log('Example app listening on port 3000!')) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Load .env config require ( 'dotenv' ) . config ( { path : __dirname + '/.env' } ) ; const Nexmo = require ( 'nexmo' ) const express = require ( 'express' ) const bodyParser = require ( 'body-parser' ) const cors = require ( 'cors' ) const app = express ( ) app . use ( bodyParser . json ( ) ) app . use ( cors ( ) ) // endpoint that doesn't authenticate the user // it will simply return a JWT with every request app . get ( '/no-auth' , ( req , res ) = > { res . json ( { userJwt : null } ) } ) app . listen ( 3000 , ( ) = > console . log ( 'Example app listening on port 3000!' ) )

In the above code we load in Express and set it up to parse inbound requests as JSON (we’ll use this later). We also set Express up to support Cross Origin Resource Sharing (CORS). This is require because the Vue.JS app is running on localhost:8080 and the JavaScript code running in the browser needs to make a call to this server running on localhost:3000 , a different port.

You can now run node index.js and then access http://localhost:3000/no-auth to ensure the endpoint returns the expected JSON.

Now, let’s add the code to generate the User JWT to be used with the Nexmo Stitch JavaScript SDK.

const userAcl = { "paths": { "/v1/users/**": {}, "/v1/conversations/**": {}, "/v1/sessions/**": {}, "/v1/knocking/**": {} } } // endpoint that doesn't authenticate the user // it will simply return a JWT with every request app.get('/no-auth', (req, res) => { const jwt = Nexmo.generateJwt(process.env.NEXMO_PRIVATE_KEY, { application_id: process.env.NEXMO_APP_ID, sub: process.env.NEXMO_APP_USER_NAME, exp: new Date().getTime() + 86400, acl: userAcl }) res.json({userJwt: jwt}) }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const userAcl = { "paths" : { "/v1/users/**" : { } , "/v1/conversations/**" : { } , "/v1/sessions/**" : { } , "/v1/knocking/**" : { } } } // endpoint that doesn't authenticate the user // it will simply return a JWT with every request app . get ( '/no-auth' , ( req , res ) = > { const jwt = Nexmo . generateJwt ( process . env . NEXMO_PRIVATE_KEY , { application_id : process . env . NEXMO_APP_ID , sub : process . env . NEXMO_APP_USER_NAME , exp : new Date ( ) . getTime ( ) + 86400 , acl : userAcl } ) res . json ( { userJwt : jwt } ) } )

The userAcl variables provides a set of claims or access rules that are used when creating the JWT along with the application ID, a sub for the name of the user and a exp as an expiry time for the JWT. See the JWT and ACL overview in Nexmo Developer for more information.

Restarting the node index.js processing and accessing http://localhost:3000/no-auth will show a real JWT having been generated.

Note: It can sometimes be handy to take a look at JWT Debugger to check the contents of your JWT.

Fetching the User JWT from the Web Browser

With the JWT generation in place we can move back to the client to retrieve the User JWT that we create on the server.

Since it’d be bad practice to 100% hard code the server URL we’ll make this setable via a Vue.JS property with a default that’s handy for our development setup.

... callInProgress: false } }, props: { jwtUrl: { type: String, default: process.env.VUE_APP_JWT_URL || "http://localhost:3000/no-auth" } }, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 . . . callInProgress : false } } , props : { jwtUrl : { type : String , default : process . env . VUE_APP_JWT_URL || "http://localhost:3000/no-auth" } } ,

The jwtUrl value can then be overwritten by setting a jwt-url property on the <CallFromBrowser> element and the default can be changed when the component is built by having a VUE_APP_JWT_URL value set in a .env file in our top-level directory. For more info see Vue.JS props and Vue CLI 3 Environment Variables and Modes.

With the server URL set we can now fetch the User JWT. Vue.JS has various life cycle hooks. We’ll retrieve the JWT within the created hook. To do this define a created function within the CallFromBrowser definition.

props: { jwtUrl: { type: String, default: process.env.VUE_APP_JWT_URL || "http://localhost:3000/no-auth" } }, created() { fetch(this.$props.jwtUrl) .then(response => { return response.json(); }) .then(json => { console.log(json) }) .catch(error => { console.error(error) }) }, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 props : { jwtUrl : { type : String , default : process . env . VUE_APP_JWT_URL || "http://localhost:3000/no-auth" } } , created ( ) { fetch ( this . $ props . jwtUrl ) . then ( response = > { return response . json ( ) ; } ) . then ( json = > { console . log ( json ) } ) . catch ( error = > { console . error ( error ) } ) } ,

Ensure your Vue.JS development server is still running (run yarn serve from within the call-from-browser directory if not) navigate to http://localhost:8080 and open up your developer tools to check the console and make sure the User JWT us logged.

Adding the Nexmo Stitch JS SDK for In-App Voice

With the User JWT in the client we can move to including the Nexmo Stitch JavaScript SDK:

cd .. # navigate up from the 'server' directory yarn add nexmo-stitch 1 2 3 cd . . # navigate up from the 'server' directory yarn add nexmo - stitch

Next, include the SDK within the CallFromBrowser.vue component and import the ConversationClient object definition:

<script> import 'vue-tel-input/dist/vue-tel-input.css' import VueTelInput from 'vue-tel-input' import ConversationClient from 'nexmo-stitch' 1 2 3 4 5 6 < script > import 'vue-tel-input/dist/vue-tel-input.css' import VueTelInput from 'vue-tel-input' import ConversationClient from 'nexmo-stitch'

Once we have the required definition included we need to create a new instance of the ConversationClient and login . We’ll do this after retrieving the User JWT:

created() { fetch(this.$props.jwtUrl) .then(response => { return response.json(); }) .then(json => { this.conversationClient = new ConversationClient({debug: true}) return this.conversationClient.login(json.userJwt) }) .then(app => { this.app = app // When the active member (the user) makes a call // keep a reference to the Call object so we can // hang up later this.app.on("member:call", (member, call) => { this.call = call }); // Keep track of call status so we know how to // interact with the call e.g. hangup this.app.on("call:status:changed", (call) => { this.callInProgress = [ "machine", "timeout", "unanswered", "rejected", "busy", "failed", "completed" ].indexOf(call.status) === -1; }) }) .catch(error => { console.error(error) }) }, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 created ( ) { fetch ( this . $ props . jwtUrl ) . then ( response = > { return response . json ( ) ; } ) . then ( json = > { this . conversationClient = new ConversationClient ( { debug : true } ) return this . conversationClient . login ( json . userJwt ) } ) . then ( app = > { this . app = app // When the active member (the user) makes a call // keep a reference to the Call object so we can // hang up later this . app . on ( "member:call" , ( member , call ) = > { this . call = call } ) ; // Keep track of call status so we know how to // interact with the call e.g. hangup this . app . on ( "call:status:changed" , ( call ) = > { this . callInProgress = [ "machine" , "timeout" , "unanswered" , "rejected" , "busy" , "failed" , "completed" ] . indexOf ( call . status ) === - 1 ; } ) } ) . catch ( error = > { console . error ( error ) } ) } ,

After the login Promise resolves we receive a reference to a representation of the Nexmo Application via the app variable. We keep a reference of that app for future use ( this.app ) and also bind to two events on the app.

We bind to member:call which is triggered when the active user makes a call. In the event handler we store a reference to the current call with this.call .

We also bind to call:status:changed to keep track of the call status. Within the event handler we updated the callInProgress property based on the call status. If the call is in any of the final states then the call is not in progress. Otherwise, the call status is in progress. These states will be reflected in the <button> in the template.

Making a Phone Call from the Web Browser

The final thing we need to do in the client – before we make one last update to the server and complete the app – is handle the user click on the <button> .

... }, controlCallClick() { if(this.callInProgress) { this.call.hangUp() } else if(this.phone.isValid) { this.app.callPhone(this.phone.number) } } 1 2 3 4 5 6 7 8 9 10 11 12 . . . } , controlCallClick ( ) { if ( this . callInProgress ) { this . call . hangUp ( ) } else if ( this . phone . isValid ) { this . app . callPhone ( this . phone . number ) } }

Above we’ve updated controlCallClick with the logic to check if there is no call in progress and the user has entered a valid phone number this should trigger the call. If there is an active call this should hangup the call. In both cases we call the appropriate function on the this.call reference that was set in the member:call event handler.

With all the client-side functionality in place you can enter a valid phone number, click the call button and you’ll then see an error in the console that comes from the Nexmo platform

conversation:error:not-found

When a call is initiated or received by the Nexmo platform it makes an HTTP request to an answer_url for the relevant associated Nexmo application. The server that receives that HTTP request must return a Nexmo Conversation Control Object (NCCO); a set of instructions informing Nexmo how to proceed with the call.

Connecting the Browser to a Phone

Back in server/index.js add an /answer endpoint to handle the GET request from the Nexmo platform:

app.get('/answer', (req, res) => { const ncco = [{ "action": "connect", "from": process.env.NEXMO_FROM_NUMBER, "endpoint": [{ "type": "phone", "number": req.query.to }] }] res.json(ncco) }) 1 2 3 4 5 6 7 8 9 10 11 12 13 app . get ( '/answer' , ( req , res ) = > { const ncco = [ { "action" : "connect" , "from" : process . env . NEXMO_FROM_NUMBER , "endpoint" : [ { "type" : "phone" , "number" : req . query . to } ] } ] res . json ( ncco ) } )

Nexmo expects a JSON structure, the NCCO, to be returned instructing it how to proceed with the call. The above ncco JSON structure that we return informs Nexmo to connect the call to a phone endpoint with the number identified by the value in req.query.to – the to query parameter in the inbound GET request. This number is the number we passed to this.app.callPhone in our Vue.JS app.

Notes:

1. Please remember that we have no application level authentication in our web app so you’ll need to add this yourself e.g. within the answer URL endpoint you can check the req.query.to and req.query.from to ensure that the user (identified by from ) is allowed to make the requested call.

2. If you have a Nexmo virtual phone number you should add a NEXMO_FROM_NUMBER entry to the .env file so that recipient of phone calls see a number on their inbound call. Otherwise, it may come up as a “Private Number” or “Unknown”.

Restart the Node process for the server so it’s running with the updated code.

Finally, we need to make it possible for the Nexmo platform to reach the answer URL. To do this use Ngrok to create a local tunnel to localhost:3000 .

$ ngrok http 3000 1 2 $ ngrok http 3000

And update the answer_url for your Nexmo Application to utilise the Ngrok tunnel URLs using the Nexmo CLI.

$ nexmo app:update NEXMO_APP_ID "call-from-browser" https://4ca73ac6.ngrok.io/answer https://4ca73ac6.ngrok.io/event 1 2 $ nexmo app : update NEXMO_APP _ ID "call-from-browser" https : //4ca73ac6.ngrok.io/answer https://4ca73ac6.ngrok.io/event

Note: you can find the NEXMO_APP_ID in server/.env or server/.nexmo-app

Head back into the Vue.JS app in the browser, enter a phone number and click the button to make an outbound call from your web brower.

Conclusion

The purpose of this blog post is was to show how to to build an app that enables a user to to call any phone on the planet directly from a web browser using Vue.JS and Nexmo In-App Voice using the Nexmo Stitch JavaScript SDK. It provides the basics and has hopefully provided inspiration for what use cases this can enable. Oh, and you could also update the app to support inbound phone calls.

As mentioned at the start of this post, Nexmo In-App Voice is in Developer Preview so please let us know if you’ve any thoughts on the experience you’ve had whilst building this app or if you have any other feedback. You can reach me on Twitter as @leggetter, join the Nexmo Community Slack or drop us an email via [email protected].

Where next

If you found this blog post interesting then the following resources are also worth checking out: