SIGN UP

Push notifications

Voximplant push notifications for web applications rely on the Firebase Cloud Messaging (FCM) platform and are implemented as "data messages". Using notifications, a client can be informed about incoming calls when an app tab is inactive or closed (in this case, the app should be started before a call arrives). You can clone a sample application with the coding needed to handle push notifications from our GitHub repository. This tutorial explains that code and outlines additional steps that should be performed to have a working solution for push notifications in a web application.

Push notifications are available in browsers that support Push API and in pages served over HTTPS. You can check a list of supported browsers here

Create a Firebase Project

To enable push notifications, you need to create a Firebase project first:

Connect Firebase to a Voximplant Application

Configure web credentials with FCM and add them to your Voximplant application:

  • Generate a new key pair through the Firebase console: open Settings in the Firebase console, select the Cloud Messaging tab, scroll to the Web configuration section, and click Generate Key Pair.
  • Open the Applications section in the Voximplant control panel, click your target application and switch to the Push Certificates tab.
  • Click Add Certificate, then fill and submit the form:
    • Select GOOGLE in the Platform field
    • Copy and paste your Sender ID from the Firebase console
    • Copy and paste your Server key from the Firebase console
    • Click Add

Send Push Notifications to your Web Application

To configure the Voximplant cloud to send push notifications, add a push service to the JavaScript scenario that initiates a call:

require(Modules.PushService);

This code will make VoxEngine automatically manage timeouts so that they won't be configurable. VoxEngine will wait for a user login event for 20 seconds after a push notification is sent. If the user doesn’t log in within this time, the call is considered failed. If the user logs in or is already logged in, he/she will receive an incoming call event and have the same timeframe to answer the call.

Then, use the callUser or forwardCallToUser method in the scenario – push notifications will be sent automatically to the app so that it can wake up, connect to Voximplant, log in, and accept the call.

PAY ATTENTION
The following sections explain the code from our push notifications demo app. You may clone it from this repo, launch the code and play with it, or continue reading if you want a more thorough step-by-step explanation.

Receive and Display Push Notifications

To receive push notifications, you must define the Firebase messaging service worker in a file called firebase-messaging-sw.js that is located in the root of your app domain. Or you can specify another name of the service worker with useServiceWorker.

Here is the code for your service worker file:

importScripts('https://www.gstatic.com/firebasejs/6.1.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/6.1.0/firebase-messaging.js');

/*
* Initialize the Firebase app in the service worker by passing in the messaging Sender ID
* */
firebase.initializeApp({
 'messagingSenderId': YOUR_SENDER_ID
});

YOUR_SENDER_ID is a Sender ID from the Cloud Messaging Settings of the Firebase console.

/*
* Retrieve an instance of Firebase Messaging so that it can handle background messages.
* */
const messaging = firebase.messaging();

* Variable for storing incoming call info
* */
let message = '';

Use the setBackgroundMessageHandler method to handle push notifications when your app is in the background and to show Voximplant notifications. You can also customize the data that you want to display to users.

messaging.setBackgroundMessageHandler((payload) => {
 console.log('[firebase-messaging-sw.js] Received background message', payload);
 if (payload.data.hasOwnProperty('voximplant')) {
 /*
 * Provide the data you want to show here
 * */
 const data = JSON.parse(payload.data.voximplant);
 const notificationOptions = {
   body: `A call from ${data.display_name}`,
   icon: '/firebase-logo.png',
   click_action: `${self.origin}/`
 };

 /*
 * Save incoming call info
 * */
 message = payload;

 /*
 * Show push notifications
 * */ 
 return self.registration.showNotification('Voximplant app', notificationOptions);
  } 
});

Open or focus your web app page when a notification is clicked:

self.addEventListener('notificationclick', (e) => {
 /*
 * Check if your app is opened, then focus on its tab or open a new one
 * */
 const promiseChain = clients.matchAll({type: 'window', includeUncontrolled: true})
   .then((tabs) => {
     const appTab = tabs.find((tab) => `${self.origin}/` === tab.url);

     if (appTab) {
       appTab.focus();

       return appTab;
     } else {
       return clients.openWindow(`${self.origin}/`);
     }
   })
   .then((tab) => {
   /*
   * Send the incoming call info to the app page
   * */
     tab.postMessage({message: message});
   });

Add Firebase to your Web Application

Add a web app manifest that specifies the gcm_sender_id, a hard-coded value indicating that FCM is authorized to send messages to this app. If your app already has a manifest.json configuration file, make sure to add the browser sender ID exactly as it is shown below (do not change the value):

{
"gcm_sender_id": "103953800507"
}

Create an index.html file for this demo app. Copy and paste this markup into it:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Voximplant push notifications demo page</title>
   <link rel="manifest" href="manifest.json">
   <style>
       html,
       body {
           margin: 0;
       }

       #app {
           display: flex;
           flex-direction: column;
           height: 100vh;
           justify-content: center;
           align-items: center;
           font-family: Roboto, Verdana;
       }

       #subscribe {
           height: 48px;
           padding: 18px 24px;
           font-family: Roboto Mono, Verdana;
           font-weight: 700;
           font-size: 12px;
           line-height: 12px;
           text-transform: uppercase;
           color: #FFFFFF;
           background: #662EFF;
           border: none;
           border-radius: 4px;
       }

       #status {
           padding: 40px 0 16px;
           font-weight: 700;
           font-size: 16px;
           line-height: 24px;
           color: #1C0B43;
       }

       #answer,
       #hangup {
           box-sizing: border-box;
           height: 32px;
           padding: 8px 16px;
           text-align: center;
           font-size: 12px;
           color: #252525;
           background-color: #FFFFFF;
           border: 1px solid #E2E5EC;
           border-radius: 4px;
       }

       #answer[disabled],
       #hangup[disabled] {
           opacity: 0.5;
       }
   </style>
</head>
<body>
<div id="app" hidden>
   <div>
       <button id="subscribe">Get push notifications when offline</button>
   </div>
   <div id="status">No ongoing calls</div>
   <div>
       <button id="answer" disabled>Answer</button>
       <button id="hangup" disabled>Decline / Hang up</button>
   </div>
</div>

Add the core Firebase and Firebase Messaging SDKs to your project by adding the script tag to the bottom of the body tag in your index.html:

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.1.0/firebase-app.js"></script>
<!-- Add Firebase messaging SDK -->
<script src="https://www.gstatic.com/firebasejs/6.1.0/firebase-messaging.js"></script>

In a real-world app, you may also add them by using a packet manager:

yarn add @firebase/app
yarn add @firebase/messaging

Add the Voximplant Web SDK after the Firebase scripts:

<script src="https://cdn.voximplant.com/edge/voximplant.min.js"></script>

Add a main.js file to your push notifications demo app and link it to your index.html file:

<script src="main.js"></script>

Switch to the General tab in Settings of the Firebase console, scroll to Your apps, and add a new web app – Firebase will provide you with your web app's Firebase configuration. Copy and paste a firebaseConfig variable into the main.js file before you use any Firebase services:

const firebaseConfig = {
   apiKey: "AAAAaaaaaaaaaaaaaaaaaaaaaaaaaa",
   authDomain: "voximplant-11111.firebaseapp.com",
   databaseURL: "https://voximplant-11111.firebaseio.com",
   projectId: "voximplant-11111",
   storageBucket: "voximplant-11111.appspot.com",
   messagingSenderId: "1111111111111",
   appId: "1:1111111111111:web:111111111111111"
};

Initialize Firebase:

firebase.initializeApp(firebaseConfig);

Handle Push Notifications in your Web Application

Now, let’s add some logic that deals with messages from a service worker to main.js. We’ll use a demo app to outline all necessary steps, but it’s up to you to implement them into your real-world app with all error checks and other necessities.

After initializing Firebase, get the Firebase Messaging and Voximplant SDKs instances:

const messaging = firebase.messaging();
const sdk = VoxImplant.getInstance();

Add variables to check if you can process a call from a push notification and to store a call in case you need to wait for the user to log in:

let isLoggedIn = false;
let pendingCall = null;

Add a handler for a message event from a service worker:

navigator.serviceWorker.onmessage = (payload) => {
 console.log("[VOXPUSH] Message received. ", payload.data.message);

 /*
 * Check that the message you've received is from Voximplant
 * */
 if (payload.data.message && payload.data.message.data.hasOwnProperty('voximplant')) {
   if (isLoggedIn) {
     sdk.handlePushNotification(JSON.parse(payload.data.message.data.voximplant));
   } else {
     pendingCall = () => sdk.handlePushNotification(JSON.parse(payload.data.message.data.voximplant));
   }
 }
};

The Web SDK handlePushNotification method notifies the Voximplant cloud that a push notification was successfully received and that a user is ready to answer a call.

Before processing any calls or messages from push notifications, you have to:

  • Initialize Web SDK
  • Connect to the Voximplant cloud
  • Log into the Voximplant cloud:
    sdk.init()
     .then(() => sdk.connect(false))
     .then(() => {
       console.log('[VOX] SDK connected');
       return logIntoVoxCloud();
     })
     .then(runApp)
     .catch(handleLoginError);‚Äč

This order is crucial. Don’t worry about the logIntoVoxCloud, handleLoginError and runApp functions for now. We’ll get to them later in this tutorial.

Logging and auto logging into the Voximplant cloud

To log your user in automatically when a push notification arrives, you need to use stored credentials. Storing the password is not secure, so use the loginWithToken method that employs the "token" approach. Token passed as a second parameter is accessToken which is returned after the first login via the login method. That means you can use push notifications only after the first user login. Store all LoginTokens in localStorage.

/*
* Demo data, provide yours
* */
const demoUser = '111@push-notifications.me.voximplant.com';
const demoPassword = '123456';
/*
* localStorage keys
* */
const lsTokensKey = 'voximplant_tokens';
const lsDeviceKey = 'voximplant_device_id';

/*
* Get a Voximplant device ID
* */
let deviceId = localStorage.getItem(lsDeviceKey);

if (!deviceId) {
 deviceId = sdk.getGUID();
 localStorage.setItem(lsDeviceKey, deviceId);
}

/*
* Get stored Voximplant tokens
* */
const lastTokens = localStorage.getItem(lsTokensKey);

const logIntoVoxCloud = () => {
if (lastTokens) {
 console.log('[VOX] last session tokens found');

 return sdk.loginWithToken(demoUser, JSON.parse(lastTokens).accessToken, {deviceToken: deviceId});
} else {
 console.log('[VOX] no last session tokens found. Basic logging in');

 return sdk.login(demoUser, demoPassword, {deviceToken: deviceId});
}
}

If login is not successful, proceed with the response error. This may occur because accessToken has expired (701 error code). If so, try to use refreshToken instead. If that has also expired, use the basic login via password.

const handleLoginError = (err) => {
 /*
 * Login with refreshToken if accessToken has expired
 * */
 if (err.code === 701) {
   console.log('[VOX] can\`t login with access token. Logging in with refresh token', err);

   sdk.loginWithToken(demoUser, JSON.parse(lastTokens).refreshToken, {deviceToken: deviceId})
     .then((result) => {
       /*
       * Update tokens in localStorage
       * */
       localStorage.setItem(lsTokensKey, JSON.stringify(result.tokens), {deviceToken: deviceId});
       runApp();
     })
     .catch((result) => {
       console.log('[VOX] can\’t login with access token. Basic logging in');

       sdk.login(demoUser, config.PASSWORD, {deviceToken: deviceId})
         .then(runApp);
   })
 } else {
   console.log('[VOX] can\’t login with access token. Basic logging in');

   sdk.login(demoUser, demoPassword, {deviceToken: deviceId})
     .then(runApp);
 }
};

Enabling push notifications for a user

Most of the interactions with the Voximplant cloud are available only after login, including enabling and processing push notifications so we’ll place this logic in the runApp function.

const runApp = (loginResult) => {
 console.log('[VOX] logged in');
 isLoggedIn = true;

 /*
 * Update Voximplant tokens after login
 * */
 if (loginResult && loginResult.tokens) {
   localStorage.setItem(lsTokensKey, JSON.stringify(loginResult.tokens));
 }

 /*
 * Show application layout
 * */
 document.getElementById('app').hidden = false;

 //the code below
};

Check and receive an incoming call if it’s an automatic login after receiving a push notification:

if (pendingCall) {
   pendingCall().then(() => {
     pendingCall = null;
   });
 }

After each successful login to the Voximplant cloud, the application should add your FCM key credential to allow it to send message requests to different push services. You will need your public key generated previously in the Firebase console.

const FCM_APP_PUBLIC_KEY = ‘1234567890qwertyuiopasdfghjklzxcvbnm’;
 messaging.usePublicVapidKey(FCM_APP_PUBLIC_KEY );

And you should get a push notification token from Firebase and pass it to the Voximplant Cloud using the registerForPushNotifications method.

/*
 * Show push notifications permission request if a user chose to receive them
 * */
 document.getElementById('subscribe').onclick = () => Notification.requestPermission()
   .then(() => {
     /*
     * Get Firebase Instance ID token
     * */
     messaging.getToken()
       .then((currentToken) => {
       if (currentToken) {
         return sdk.registerForPushNotifications(currentToken);
       } else {
         console.log('[VOXPUSH] No Instance ID token available.');
     }
   })
   .then((result) => {
       console.log('[VOXPUSH] Token register success');
   })
   .catch((err) => {
       console.log('[VOXPUSH] An error occurred while registering token. ', err);
   });
 });

 /*
 * Handle Firebase Instance ID token refresh
 * */
 messaging.onTokenRefresh(() => {
   messaging.getToken()
     .then((refreshedToken) => {
     console.log('[VOXPUSH] New token arrived ', refreshedToken);

     return sdk.registerForPushNotifications(refreshedToken);
   })
   .then(() => {
       console.log('[VOXPUSH] New token register success');
   })
   .catch((err) => {
       console.log('[VOXPUSH] Unable to refresh token', err);
   });
 });

Finally, let’s handle incoming calls by subscribing to the IncomingCall event.

sdk.addEventListener(VoxImplant.Events.IncomingCall, (e) => {
   receiveCall(e.call);
   /*
   * Add handlers for call events
   * */
   e.call.addEventListener(VoxImplant.CallEvents.Connected, (ev) => {
     console.log('[VOXCALL] Call was connected successfully');
     document.getElementById('status').innerText = `Ongoing call with ${e.call.number()}`;
   });
   e.call.addEventListener(VoxImplant.CallEvents.Disconnected, (ev) => {
     console.log('[VOXCALL] Call was disconnected');
     document.getElementById('status').innerText = 'No ongoing calls';
   });
   e.call.addEventListener(VoxImplant.CallEvents.Failed, (ev) => {
     console.log('[VOXCALL] Call failed:', ev.reason, `(${ev.code})`);
     document.getElementById('status').innerText = 'No ongoing calls';
   });
 });
};

function receiveCall(call) {
 console.log('[VOXCALL] A call from', call.number());
 document.getElementById('status').innerText = `Incoming call from ${call.number()}`;
 document.getElementById('answer').disabled = false;
 document.getElementById('hangup').disabled = false;
 document.getElementById('answer').onclick = () => {
   call.answer('', {}, true);
   document.getElementById('answer').disabled = true;
 }
 document.getElementById('hangup').onclick = () => {
   call.hangup();
   document.getElementById('answer').disabled = true;
   document.getElementById('hangup').disabled = true;
 }
}