diff --git a/functional-samples/tutorial.open-api-reference/api-list.js b/functional-samples/tutorial.open-api-reference/api-list.js
new file mode 100644
index 00000000..72554e9c
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/api-list.js
@@ -0,0 +1,81 @@
+export default [
+ {
+ content: 'commands',
+ description:
+ 'Use the Commands API to add a keyboard shortcuts.'
+ },
+ {
+ content: 'contextmenus',
+ description:
+ "Use the ContextMenus API to add a custom item to Chrome's context menu."
+ },
+ {
+ content: 'declarativeNetRequest',
+ description:
+ 'Use the DeclarativeNetRequest API to block or modify network requests.'
+ },
+ {
+ content: 'downloads',
+ description:
+ 'Use the Downloads API to programmatically manipulate downloads.'
+ },
+ {
+ content: 'i18n',
+ description: 'Use the i18n API to localize your extension'
+ },
+ {
+ content: 'identity',
+ description:
+ 'Use the Identity API to get OAuth2 access tokens.'
+ },
+ {
+ content: 'notifications',
+ description:
+ 'Use the Notifications API show notifications to users in the system tray.'
+ },
+ {
+ content: 'offscreen',
+ description:
+ 'Use the Offscreen API to create and manage offscreen documents.'
+ },
+ {
+ content: 'omnibox',
+ description:
+ "Use the Omnibox API to register a keyword with Chrome's address bar."
+ },
+ {
+ content: 'permissions',
+ description:
+ 'Use the Permissions API to request optional permissions at run time.'
+ },
+ {
+ content: 'runtime',
+ description:
+ 'Use the Runtime API pass messages, manage extension lifecycle, and access other helper utils.'
+ },
+ {
+ content: 'scripting',
+ description:
+ 'Use the Scripting API to execute scripts in different contexts.'
+ },
+ {
+ content: 'storage',
+ description:
+ 'Use the Storage API to store, retrieve, and track changes to user data.'
+ },
+ {
+ content: 'tabs',
+ description:
+ 'Use the Tabs API to create, update and manipulate tabs.'
+ },
+ {
+ content: 'topSites',
+ description:
+ 'Use the TopSites API to access the most visited sites that are displayed on the new tab page.'
+ },
+ {
+ content: 'webNavigation',
+ description:
+ 'Use the WebNavigation API to receive notifications about the status of navigation requests in-flight.'
+ }
+];
diff --git a/functional-samples/tutorial.open-api-reference/content.js b/functional-samples/tutorial.open-api-reference/content.js
new file mode 100644
index 00000000..1a5f65d4
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/content.js
@@ -0,0 +1,30 @@
+// Popover API https://chromestatus.com/feature/5463833265045504
+
+(async () => {
+ const nav = document.querySelector('.navigation-rail__links');
+
+ const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });
+
+ const tipWidget = createDomElement(`
+
+ `);
+
+ const popover = createDomElement(
+ `
${tip}
`
+ );
+
+ document.body.append(popover);
+ nav.append(tipWidget);
+})();
+
+function createDomElement(html) {
+ const dom = new DOMParser().parseFromString(html, 'text/html');
+ return dom.body.firstElementChild;
+}
diff --git a/functional-samples/tutorial.open-api-reference/icon-128.png b/functional-samples/tutorial.open-api-reference/icon-128.png
new file mode 100644
index 00000000..8fb13686
Binary files /dev/null and b/functional-samples/tutorial.open-api-reference/icon-128.png differ
diff --git a/functional-samples/tutorial.open-api-reference/icon-16.png b/functional-samples/tutorial.open-api-reference/icon-16.png
new file mode 100644
index 00000000..4537f9d5
Binary files /dev/null and b/functional-samples/tutorial.open-api-reference/icon-16.png differ
diff --git a/functional-samples/tutorial.open-api-reference/manifest.json b/functional-samples/tutorial.open-api-reference/manifest.json
new file mode 100644
index 00000000..e88d16a1
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/manifest.json
@@ -0,0 +1,25 @@
+{
+ "manifest_version": 3,
+ "name": "Open extension API reference",
+ "version": "1.0.0",
+ "icons": {
+ "16": "icon-16.png",
+ "128": "icon-128.png"
+ },
+ "background": {
+ "service_worker": "service-worker.js",
+ "type": "module"
+ },
+ "minimum_chrome_version": "102",
+ "omnibox": {
+ "keyword": "api"
+ },
+ "permissions": ["alarms", "storage"],
+ "content_scripts": [
+ {
+ "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
+ "js": ["content.js"]
+ }
+ ],
+ "host_permissions": ["https://extension-tips.glitch.me/*"]
+}
diff --git a/functional-samples/tutorial.open-api-reference/service-worker.js b/functional-samples/tutorial.open-api-reference/service-worker.js
new file mode 100644
index 00000000..1af54df2
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/service-worker.js
@@ -0,0 +1,2 @@
+import './sw-omnibox.js';
+import './sw-tips.js';
diff --git a/functional-samples/tutorial.open-api-reference/sw-omnibox.js b/functional-samples/tutorial.open-api-reference/sw-omnibox.js
new file mode 100644
index 00000000..005869bf
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/sw-omnibox.js
@@ -0,0 +1,37 @@
+import { getApiSuggestions } from './sw-suggestions.js';
+
+console.log('sw-omnibox.js');
+
+// Initialize default API suggestions
+chrome.runtime.onInstalled.addListener(({ reason }) => {
+ if (reason === 'install') {
+ chrome.storage.local.set({
+ apiSuggestions: ['tabs', 'storage', 'scripting']
+ });
+ }
+});
+
+const URL_CHROME_EXTENSIONS_DOC =
+ 'https://developer.chrome.com/docs/extensions/reference/';
+const NUMBER_OF_PREVIOUS_SEARCHES = 4;
+
+// Displays the suggestions after user starts typing
+chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
+ const { description, suggestions } = await getApiSuggestions(input);
+ await chrome.omnibox.setDefaultSuggestion({ description });
+ suggest(suggestions);
+});
+
+// Opens the reference page of the chosen API
+chrome.omnibox.onInputEntered.addListener((input) => {
+ chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
+ // Saves the latest keyword
+ updateHistory(input);
+});
+
+async function updateHistory(input) {
+ const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
+ apiSuggestions.unshift(input);
+ apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
+ await chrome.storage.local.set({ apiSuggestions });
+}
diff --git a/functional-samples/tutorial.open-api-reference/sw-suggestions.js b/functional-samples/tutorial.open-api-reference/sw-suggestions.js
new file mode 100644
index 00000000..42dd477a
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/sw-suggestions.js
@@ -0,0 +1,23 @@
+import apiList from './api-list.js';
+
+/**
+ * Returns a list of suggestions and a description for the default suggestion
+ */
+export async function getApiSuggestions(input) {
+ const suggestions = apiList.filter((api) => api.content.startsWith(input));
+
+ // return suggestions if any exist
+ if (suggestions.length) {
+ return {
+ description: 'Matching Chrome APIs',
+ suggestions: suggestions
+ };
+ }
+
+ // return past searches if no match was found
+ const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
+ return {
+ description: 'No matches found. Choose from past searches',
+ suggestions: apiList.filter((item) => apiSuggestions.includes(item.content))
+ };
+}
diff --git a/functional-samples/tutorial.open-api-reference/sw-tips.js b/functional-samples/tutorial.open-api-reference/sw-tips.js
new file mode 100644
index 00000000..912d236d
--- /dev/null
+++ b/functional-samples/tutorial.open-api-reference/sw-tips.js
@@ -0,0 +1,28 @@
+console.log('sw-tips.js');
+
+// Fetch tip & save in storage
+const updateTip = async () => {
+ const response = await fetch('https://extension-tips.glitch.me/tips.json');
+ const tips = await response.json();
+ const randomIndex = Math.floor(Math.random() * tips.length);
+ await chrome.storage.local.set({ tip: tips[randomIndex] });
+};
+
+// Create a daily alarm and retrieves the first tip when extension is installed.
+chrome.runtime.onInstalled.addListener(({ reason }) => {
+ if (reason === 'install') {
+ chrome.alarms.create({ delayInMinutes: 1, periodInMinutes: 1440 });
+ updateTip();
+ }
+});
+
+// Retrieve tip of the day
+chrome.alarms.onAlarm.addListener(updateTip);
+
+// Send tip to content script via messaging
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message.greeting === 'tip') {
+ chrome.storage.local.get('tip').then(sendResponse);
+ return true;
+ }
+});