Building Chrome Extension (Manifest V3) with RingCentral Embeddable

Embbnux Ji
RingCentral Developers
8 min readSep 27, 2023

--

RingCentral Embeddable Chrome extension demo

In our earlier blog, we explored the seamless integration of RingCentral Embeddable with Chrome extension manifest V2. Now Chrome introduces the new manifest V3, and requires developers to upgrade. In this blog, we will dive into the process of building a Chrome extension using Manifest V3, while integrating RingCentral Embeddable for efficient communication within web apps.

Understanding Chrome Extension Manifest V3

Manifest V3 is the latest version of the manifest file that defines the structure and behavior of a Chrome extension. It offers enhanced security and performance, making extensions more reliable and user-friendly. Unlike Manifest V2, V3 enforces a declarative approach to extension architecture, providing better isolation between components. Chrome extension support for manifest V2 will be discontinued in 2024.

Breaking Changes when upgrade to Manifest V3

  1. Background pages to service workers (no persistence)
  2. No remote codes (no load scripts from no local files)

Relate links

Introducing RingCentral Embeddable

RingCentral Embeddable is a suite of tools that allow developers to seamlessly integrate communication features, such as voice and video calls, messaging, and meetings, directly into web applications. This integration enhances user productivity by eliminating the need to switch between different platforms for communication.

Relate links

Prerequisites for Building the Extension

Before diving into the development process, ensure you have a basic understanding of JavaScript, HTML, and Chrome extension concepts. Additionally, sign up for a free RingCentral developer account to access the necessary API credentials.

Step-by-Step Guide

Setting up the Extension Directory

Firstly, please create a directory for your extension project with following file structures. For files, we can just keep them blank, we will implement them in the next section.

├── embeddable
│ ├── app.js
│ ├── app.html
├── images
├── c2d
│ ├── index.js
├── manifest.json
├── background.js
├── content.js
├── popup.js
├── popup.html

As manifest V3 doesn’t allow for remote loading of javascript files, we need to include prebuilt embeddable files in the project.

The embeddable folder is the prebuilt files of RingCentral Embeddable for Chrome extension. Please download from here to ensure you have the latest version.

The c2d folder is only required when you would like to add Click To Dial/SMS features for the Chrome extension. You can get c2d/index.js from here.

Defining the Manifest File:

Add the following code into your manifest.json file:

{
"name": "RingCentral Embeddable Demo",
"description": "A RingCentral Embeddable extension demo",
"version": "0.0.1",
"permissions": [
"storage",
"alarms",
"identity",
"activeTab",
"tabs",
"background",
"tabCapture",
"unlimitedStorage"
],
"host_permissions": [
"https://contacts.google.com/*"
],
"content_scripts": [
{
"matches": ["https://contacts.google.com/*"],
"js": [
"./c2d/index.js",
"./content.js"
]
}
],
"web_accessible_resources": [
{
"resources": ["/embeddable/*", "/c2d/*"],
"matches": ["https://contacts.google.com/*"]
}
],
"action": {
"default_icon": {
"16": "/images/logo16.png",
"32": "/images/logo32.png",
"48": "/images/logo48.png",
"128": "/images/logo128.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"manifest_version": 3,
"icons": {
"16": "/images/logo16.png",
"32": "/images/logo32.png",
"48": "/images/logo48.png",
"128": "/images/logo128.png"
}
}

From V2, we change background scripts to service worker . Please keep in mind that the worker is not persistent. It may be stopped and restarted. So every variable in memory will be lost. We need to use storage persistence for some variables.

We also declare “content_scripts” and “web_accessible_resources” as we will integrate the embeddable widget into the Google contacts page as part of this demo project. You need to change the domain to what you want to integrate.

After the manifest file is created, we can try to install it into Chrome for testing. Go to “chrome://extensions/”, enable developer mode, and click Load unpacked to install the extension.

RingCentral Embeddable demo extension

Setup popup window

For Chrome extension, we recommend to integrate the embeddable widget as a popup window, not inside the web page. With the popup window, the call will be kept connected even if the user refreshes or close web pages.

Add following HTML into popup.html file:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="IE=edge">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="Sat, 01 Jan 2000 00:00:00 GMT">
<title>RingCentral Embeddable</title>
<meta name="description" content="RingCentral Embeddable">
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1"
>
<style>
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#appLoading {
text-align: center;
padding-top: 30px;
}
.Adapter_root {
left: 0!important;
top: 0!important;
}
.Adapter_root .Adapter_header {
cursor: default;
}
.Adapter_header .Adapter_button {
display: none;
}
</style>
</head>
<body>
<div id='viewport'>
<p id="appLoading">
Loading...
</p>
</div>
<script src="./popup.js"></script>
<script src="./embeddable/adapter.js"></script>
</body>
</html>

Then add the following javascript into popup.js :

// popup.js
window.__ON_RC_POPUP_WINDOW = 1;

Use service worker for opening a popup window

In manifest V2, we use background script to handle app open events. And now we use service worker:

// background.js
const apiConfig = {
clientId: 'your_ringcentral_app_client_id',
server: 'https://platform.devtest.ringcentral.com',
redirectUri: 'https://ringcentral.github.io/ringcentral-embeddable/redirect.html',
};
async function openPopupWindow() {
console.log('open popup');
const { popupWindowId } = await chrome.storage.local.get('popupWindowId');
if (popupWindowId) {
try {
await chrome.windows.update(popupWindowId, { focused: true });
return;
} catch (e) {
// ignore
}
}
// const redirectUri = chrome.identity.getRedirectURL('redirect.html'); // set this when oauth with chrome.identity.launchWebAuthFlow
const redirectUri = apiConfig.redirectUri;
let popupUri = `popup.html?multipleTabsSupport=1&disableLoginPopup=1&appServer=${apiConfig.server}&redirectUri=${redirectUri}`;
if (apiConfig.clientId.length > 0) {
popupUri = `${popupUri}&clientId=${apiConfig.clientId}`;
}
const popup = await chrome.windows.create({
url: popupUri,
type: 'popup',
width: 300,
height: 566,
});
await chrome.storage.local.set({
popupWindowId: popup.id,
});
}

chrome.action.onClicked.addListener(function (tab) {
openPopupWindow();
});

chrome.windows.onRemoved.addListener(async (windowId) => {
const { popupWindowId } = await chrome.storage.local.get('popupWindowId');
if (popupWindowId === windowId) {
console.log('close popup');
await chrome.storage.local.remove('popupWindowId');
}
});

We use storage to save popupWindowId , so it will not be lost after the service worker becomes inactive.

Go to “chrome://extensions/”, then reload the extension. We can see the app when the user clicks the extension icon in the browser extensions list.

RingCentral Embeddable extension demo — popup

Handle RingCentral authorization

We need to handle RingCentral authorization as the default login popup as RingCentral Embeddable default authorization can’t work with manifest V3.

// In popup.js
function responseMessage(request, response) {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-post-message-response',
responseId: request.requestId,
response,
}, '*');
}

// Interact with RingCentral Embeddable Voice:
window.addEventListener('message', (e) => {
const data = e.data;
if (data) {
switch (data.type) {
case 'rc-login-popup-notify':
handleOAuthWindow(data.oAuthUri);
break;
default:
break;
}
}
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'oauthCallBack') {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-adapter-authorization-code',
callbackUri: request.callbackUri,
}, '*');
sendResponse({ result: 'ok' });
}
});

async function handleOAuthWindow(oAuthUri) {
chrome.runtime.sendMessage({
type: 'openOAuthWindow',
oAuthUri,
});
// // Authorize with chrome identify API. Require to have a fixed chrome extension id.
// chrome.identity.launchWebAuthFlow(
// {
// url: oAuthUri,
// interactive: true,
// },
// (responseUrl) => {
// if (responseUrl) {
// document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
// type: 'rc-adapter-authorization-code',
// callbackUri: responseUrl,
// }, '*');
// }
// },
// );
}
// background.js
chrome.alarms.onAlarm.addListener(async () => {
const { loginWindowId } = await chrome.storage.local.get('loginWindowId');
if (!loginWindowId) {
return;
}
const tabs = await chrome.tabs.query({ windowId: loginWindowId });
if (tabs.length === 0) {
return;
}
const loginWindowUrl = tabs[0].url
console.log('loginWindowUrl', loginWindowUrl);
if (loginWindowUrl.indexOf(apiConfig.redirectUri) !== 0) {
chrome.alarms.create('oauthCheck', { when: Date.now() + 3000 });
return;
}
console.log('login success', loginWindowUrl);
chrome.runtime.sendMessage({
type: 'oauthCallBack',
callbackUri: loginWindowUrl,
});
await chrome.windows.remove(loginWindowId);
await chrome.storage.local.remove('loginWindowId');
});

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.type === 'openOAuthWindow') {
const loginWindow = await chrome.windows.create({
url: request.oAuthUri,
type: 'popup',
width: 600,
height: 600,
});
await chrome.storage.local.set({
loginWindowId: loginWindow.id,
});
chrome.alarms.create('oauthCheck', { when: Date.now() + 3000 });
sendResponse({ result: 'ok' });
return;
}
});

We can also use the chrome.identify API to set up the RingCentral authorization flow. But the API’s redirect URI is related to Chrome extension id. The Chrome extension id is random and changeable for unpacked and not uploaded to the Chrome extension store.

Integrate with third party service

Now we have RingCentral features from the embeddable widget. And we can use content scripts to integrate RingCentral with third party services.

Here we integrate RingCentral Click To Dial feature into Google contacts:

// content.js
window.clickToDialInject = new window.RingCentralC2D();
window.clickToDialInject.on(
window.RingCentralC2D.events.call,
function(phoneNumber) {
console.log('Click To Dial:', phoneNumber);
// alert('Click To Dial:' + phoneNumber);
chrome.runtime.sendMessage({
type: 'c2d',
phoneNumber,
});
},
);
window.clickToDialInject.on(
window.RingCentralC2D.events.text,
function(phoneNumber) {
console.log('Click To SMS:', phoneNumber);
// alert('Click To SMS:' + phoneNumber);
chrome.runtime.sendMessage({
type: 'c2sms',
phoneNumber,
});
},
);
// background.js
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.type === 'c2d' || request.type === 'c2sms') {
openPopupWindow();
}
});
// popup.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'oauthCallBack') {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-adapter-authorization-code',
callbackUri: request.callbackUri,
}, '*');
sendResponse({ result: 'ok' });
} else if (request.type === 'c2sms') {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-adapter-new-sms',
phoneNumber: request.phoneNumber,
}, '*');
sendResponse({ result: 'ok' });
} else if (request.type === 'c2d') {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-adapter-new-call',
phoneNumber: request.phoneNumber,
toCall: true,
}, '*');
sendResponse({ result: 'ok' });
}
});

After the code, when a user hovers on a web page’s phone number and clicks the dial button, the extension will open the previous app window:

Interact with RingCentral Embeddable

RingCentral Embeddable provides a lot of API to get data or control the widget. We can interact with it withinpopup.js .

// in popup.js:
// Get event data from RingCentral Embeddable
window.addEventListener('message', (e) => {
const data = e.data;
if (data) {
switch (data.type) {
case 'rc-call-ring-notify':
// get call on ring event
console.log('RingCentral Embeddable Extension:', data.call);
break;
case 'rc-call-end-notify':
// get call on call end event
console.log('RingCentral Embeddable Extension:', data.call);
break;
case 'rc-call-start-notify':
// get call on start a outbound call event
console.log('RingCentral Embeddable Extension:', data.call);
break;
default:
break;
}
}
});
function registerService() {
document.querySelector("#rc-widget-adapter-frame").contentWindow.postMessage({
type: 'rc-adapter-register-third-party-service',
service: {
name: 'TestService',
}
}, '*');
}

var registered = false;
window.addEventListener('message', function (e) {
const data = e.data;
if (data && data.type === 'rc-adapter-pushAdapterState' && registered === false) {
registered = true;
registerService();
}
});

For more integration possibilities with RingCentral Embeddable, please refer to the following document.

Conclusion

Building a Chrome extension using Manifest V3 and integrating RingCentral Embeddable opens up exciting possibilities for enhancing communication within Chrome, Edge or Firefox browser. By following this guide, you will be able to create a feature-rich extension that leverages modern extension architecture and empowers users with seamless communication capabilities.

You can get the full extension code here. Follow its README document guide to install the extension and try it.

--

--