Building Chrome Extension (Manifest V3) with RingCentral Embeddable
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
- Background pages to service workers (no persistence)
- No remote codes (no load scripts from no local files)
Relate links
- Chrome manifest V3 migration document
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
- Github repository and document: RingCentral Embeddable
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.
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.
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.