In this blog post we'll walk through how you can make a phone call from a web brower to a phone with Vonage In-App Voice using the Vonage Client SDK for JavaScript and Vue.JS. 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 Vonage Developer Community Slack.
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 Vonage Client SDK for JavaScript, an application server used to authenticate the application user with Vonage 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 Vonage Client SDK for JavaScript to login to the Vonage platform and initiate the phone call. We then need to update the app server to handle a GET request that Vonage will make in order to retrieve instructions for how to proceed with the phone call. Those instructions will tell Vonage to connect the call from the Vue.JS application in the web browser to a phone.
/* https://bramp.github.io/js-sequence-diagrams/ Participant Vue.JS App as V Participant App Server as A Participant Nexmo as N Participant Phone as P V->A: Get User JWT Note right of V: In a production app<br/>this request should<br/>be authenticaterd A-->V: User JWT V -> V: Create Nexmo<br/>Conversation Client V -> N: Login N --> V: Logged In V -> N: Call Phone N -> A: GET answer_url A --> N: NCCO connect N -> P: Call */
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 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 nexmo@beta
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
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
We're going to build a CallFromBrowser
component so let's rename the default HelloWorld
one that was created.
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:
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>
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
},
...
</code></pre>
<p>This imports the CSS and the component definition for the telephone input component sets the name of the component to <code>CallFromBrowser</code> and registers the <code>vue-tel-input</code> component dependency so it can be used within the template.</p>
<p>Next let's set up some properties for data binding such as the <code>callInProgress</code> property relied upon by the <code>template</code> and add any methods that are expected to be in place, as show, in the template:</p>
<pre><code class="language-javascript">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>
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 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>
</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:
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 Vonage Platform
The Nexmo Stitch JavaScript SDK connects to the Vonage 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 componentan Application within the Vonage platform - we can do this using the Nexmo CLI
a User within the 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.
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 Application and the User for that application. We can do this using the Nexmo CLI:
Running this command will output an application ID. It will also add the 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
The last piece of Application setup is to create a user within 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:
This command will create the user for the application ID identified within the .nexmo-app
file. Add an 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
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!'))
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 required 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 Vonage Client SDK for JavaScript.
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 provide 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 an 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 settable 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"
}
},
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)
})
},
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 Vonage Client SDK for JavaScript
With the User JWT in the client we can move to include the Vonage Client SDK for JavaScript:
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'
</code></pre>
<p>Once we have the required definition included we need to create a new instance of the <code>ConversationClient</code> and <code>login</code>. We'll do this after retrieving the User JWT:</p>
<pre><code class="language-js"> 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)
})
},
</code></pre>
<p>After the login Promise resolves we receive a reference to a representation of the Application via the <code>app</code> variable. We keep a reference of that app for future use (<code>this.app</code>) and also bind to two events on the app.</p>
<p>We bind to <code>member:call</code> which is triggered when the active user makes a call. In the event handler we store a reference to the current call with <code>this.call</code>.</p>
<p>We also bind to <code>call:status:changed</code> to keep track of the <a href="/client-sdk/in-app-voice/concepts/call-statuses">call status</a>. Within the event handler we updated the <code>callInProgress</code> 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 <code><button></code> in the template.</p>
<h2>Making a Phone Call from the Web Browser</h2>
<p>The final thing we need to do in the client - before we make one last update to the server and complete the app - is handling the user click on the <code><button></code>.</p>
<pre><code class="language-js"> ...
},
controlCallClick() {
if(this.callInProgress) {
this.call.hangUp()
}
else if(this.phone.isValid) {
this.app.callPhone(this.phone.number)
}
}
</code></pre>
<p>Above we've updated <code>controlCallClick</code> 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 hang up the call. In both cases we call the appropriate function on the <code>this.call</code> reference that was set in the <code>member:call</code> event handler.</p>
<p>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 Vonage platform</p>
<blockquote>
<p>conversation:error:not-found</p>
</blockquote>
<p><img src="https://d226lax1qjow5r.cloudfront.net/blog/blogposts/making-phone-calls-from-a-web-browser-with-vue-js-and-vonage/conversation-not-found.png" alt="conversation-not-found message in browser console" title="conversation-not-found message in browser console"></p>
<p>When a call is initiated or received by the Nexmo platform it makes an HTTP request to an <code>answer_url</code> 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.</p>
<h2>Connecting the Browser to a Phone</h2>
<p>Back in <code>server/index.js</code> add an <code>/answer</code> endpoint to handle the <code>GET</code> request from the Vonage platform:</p>
<pre><code class="language-js">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)
})
</code></pre>
<p>Nexmo expects a JSON structure, the NCCO, to be returned instructing it how to proceed with the call. The above <code>ncco</code> JSON structure that we return informs Nexmo to <code>connect</code> the call to a <code>phone</code> endpoint with the number identified by the value in <code>req.query.to</code> - the <code>to</code> query parameter in the inbound GET request. This number is the number we passed to <code>this.app.callPhone</code> in our Vue.JS app.</p>
<p>_Notes:</p>
<ol>
<li>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 <code>req.query.to</code> and <code>req.query.from</code> to ensure that the user (identified by <code>from</code>) is allowed to make the requested call.</li>
<li>If you have a Nexmo virtual phone number you should add a <code>NEXMO_FROM_NUMBER</code> entry to the <code>.env</code> 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"._</li>
</ol>
<p>Restart the Node process for the server so it's running with the updated code.</p>
<p>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 <code>localhost:3000</code>.</p>
<pre><code class="language-sh">$ ngrok http 3000
</code></pre>
<p><img src="https://d226lax1qjow5r.cloudfront.net/blog/blogposts/making-phone-calls-from-a-web-browser-with-vue-js-and-vonage/ngrok.png" alt="Ngrok output in a terminal" title="Ngrok output in a terminal"></p>
<p>And update the <code>answer_url</code> for your Nexmo Application to utilise the Ngrok tunnel URLs using the Nexmo CLI.</p>
<pre><code class="language-sh">$ nexmo app:update NEXMO_APP_ID "call-from-browser" https://4ca73ac6.ngrok.io/answer https://4ca73ac6.ngrok.io/event
</code></pre>
<p><em>Note: you can find the <code>NEXMO_APP_ID</code> in <code>server/.env</code> or <code>server/.nexmo-app</code></em></p>
<p>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 browser.</p>
<p><img src="https://d226lax1qjow5r.cloudfront.net/blog/blogposts/making-phone-calls-from-a-web-browser-with-vue-js-and-vonage/working-app.png" alt="Full Vue.JS Call from Browser application working alongside screenshot of phone ringing" title="Full Vue.JS Call from Browser application working alongside screenshot of phone ringing"></p>
<h2>Conclusion</h2>
<p>The purpose of this blog post is was to show how to to build an app that enables a user to call any phone on the planet directly from a web browser using Vue.JS and In-App Voice using the Vonage Client SDK for JavaScript. 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 <a href="/client-sdk/in-app-voice/guides/receive-call">support inbound phone calls</a>.</p>
<p>As mentioned at the start of this post, 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 by joining the <a href="/community/slack">Vonage Community Slack</a>.</p>
<h2>Where next</h2>
<p>If you found this blog post interesting then the following resources are also worth checking out:</p>
<ul>
<li><a href="/client-sdk/in-app-voice/overview">In-App Voice docs</a></li>
<li><a href="/client-sdk/in-app-messaging/overview">In-App Messaging docs</a></li>
<li><a href="/blog/build-chat-app-angular-material-stitch-javascript-dr/">Build a Chat Application with Angular Material and the Nexmo In-App JavaScript SDK</a></li>
<li><a href="/blog/next-web-voice-journal-python-vue-javascript-dr/">Creating a Peer-to-Peer Voice Journal with Nexmo Voice APIs, Python and Vue.JS</a></li>
</ul>
</script>