/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ const CONTAINER_HIDE_SRC = "/img/container-hide.svg"; const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg"; const DEFAULT_COLOR = "blue"; const DEFAULT_ICON = "circle"; const NEW_CONTAINER_ID = "new"; const ONBOARDING_STORAGE_KEY = "onboarding-stage"; // List of panels const P_ONBOARDING_1 = "onboarding1"; const P_ONBOARDING_2 = "onboarding2"; const P_ONBOARDING_3 = "onboarding3"; const P_ONBOARDING_4 = "onboarding4"; const P_ONBOARDING_5 = "onboarding5"; const P_ONBOARDING_6 = "onboarding6"; const P_ONBOARDING_7 = "onboarding7"; const P_CONTAINERS_LIST = "containersList"; const P_CONTAINERS_EDIT = "containersEdit"; const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_DELETE = "containerDelete"; const P_CONTAINERS_ACHIEVEMENT = "containersAchievement"; async function getExtensionInfo() { const manifestPath = browser.extension.getURL("manifest.json"); const response = await fetch(manifestPath); const extensionInfo = await response.json(); return extensionInfo; } // This object controls all the panels, identities and many other things. const Logic = { _identities: [], _currentIdentity: null, _currentPanel: null, _previousPanel: null, _panels: {}, _onboardingVariation: null, async init() { // Remove browserAction "upgraded" badge when opening panel this.clearBrowserActionBadge(); // Retrieve the list of identities. const identitiesPromise = this.refreshIdentities(); try { await identitiesPromise; } catch (e) { throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message); } // Routing to the correct panel. // If localStorage is disabled, we don't show the onboarding. const onboardingData = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]); let onboarded = onboardingData[ONBOARDING_STORAGE_KEY]; if (!onboarded) { onboarded = 0; this.setOnboardingStage(onboarded); } switch (onboarded) { case 7: this.showAchievementOrContainersListPanel(); break; case 6: this.showPanel(P_ONBOARDING_7); break; case 5: this.showPanel(P_ONBOARDING_6); break; case 4: this.showPanel(P_ONBOARDING_5); break; case 3: this.showPanel(P_ONBOARDING_4); break; case 2: this.showPanel(P_ONBOARDING_3); break; case 1: this.showPanel(P_ONBOARDING_2); break; case 0: default: this.showPanel(P_ONBOARDING_1); break; } }, async showAchievementOrContainersListPanel() { // Do we need to show an achievement panel? let showAchievements = false; const achievementsStorage = await browser.storage.local.get({ achievements: [] }); for (const achievement of achievementsStorage.achievements) { if (!achievement.done) { showAchievements = true; } } if (showAchievements) { this.showPanel(P_CONTAINERS_ACHIEVEMENT); } else { this.showPanel(P_CONTAINERS_LIST); } }, // In case the user wants to click multiple actions, // they have to click the "Done" button to stop the panel // from showing async setAchievementDone(achievementName) { const achievementsStorage = await browser.storage.local.get({ achievements: [] }); const achievements = achievementsStorage.achievements; achievements.forEach((achievement, index, achievementsArray) => { if (achievement.name === achievementName) { achievement.done = true; achievementsArray[index] = achievement; } }); browser.storage.local.set({ achievements }); }, setOnboardingStage(stage) { return browser.storage.local.set({ [ONBOARDING_STORAGE_KEY]: stage }); }, async clearBrowserActionBadge() { const extensionInfo = await getExtensionInfo(); const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] }); browser.browserAction.setBadgeBackgroundColor({ color: null }); browser.browserAction.setBadgeText({ text: "" }); storage.browserActionBadgesClicked.push(extensionInfo.version); // use set and spread to create a unique array const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)]; browser.storage.local.set({ browserActionBadgesClicked }); }, async identity(cookieStoreId) { const defaultContainer = { name: "Default", cookieStoreId, icon: "default-tab", color: "default-tab", numberOfHiddenTabs: 0, numberOfOpenTabs: 0 }; // Handle old style rejection with null and also Promise.reject new style try { return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer; } catch (e) { return defaultContainer; } }, async numTabs() { const activeTabs = await browser.tabs.query({ windowId: browser.windows.WINDOW_ID_CURRENT }); return activeTabs.length; }, _disableMoveTabs(message) { const moveTabsEl = document.querySelector("#container-info-movetabs"); const fragment = document.createDocumentFragment(); const incompatEl = document.createElement("div"); moveTabsEl.classList.remove("clickable"); moveTabsEl.setAttribute("title", message); fragment.appendChild(incompatEl); incompatEl.setAttribute("id", "container-info-movetabs-incompat"); incompatEl.textContent = message; incompatEl.classList.add("container-info-tab-row"); moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); }, async refreshIdentities() { const [identities, state] = await Promise.all([ browser.contextualIdentities.query({}), browser.runtime.sendMessage({ method: "queryIdentitiesState", message: { windowId: browser.windows.WINDOW_ID_CURRENT } }) ]); this._identities = identities.map((identity) => { const stateObject = state[identity.cookieStoreId]; if (stateObject) { identity.hasOpenTabs = stateObject.hasOpenTabs; identity.hasHiddenTabs = stateObject.hasHiddenTabs; identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs; identity.numberOfOpenTabs = stateObject.numberOfOpenTabs; } return identity; }); }, getPanelSelector(panel) { if (this._onboardingVariation === "securityOnboarding" && // eslint-disable-next-line no-prototype-builtins panel.hasOwnProperty("securityPanelSelector")) { return panel.securityPanelSelector; } else { return panel.panelSelector; } }, async showPanel(panel, currentIdentity = null) { // Invalid panel... ?!? if (!(panel in this._panels)) { throw new Error("Something really bad happened. Unknown panel: " + panel); } this._previousPanel = this._currentPanel; this._currentPanel = panel; this._currentIdentity = currentIdentity; // Initialize the panel before showing it. await this._panels[panel].prepare(); Object.keys(this._panels).forEach((panelKey) => { const panelItem = this._panels[panelKey]; const panelElement = document.querySelector(this.getPanelSelector(panelItem)); if (!panelElement.classList.contains("hide")) { panelElement.classList.add("hide"); if ("unregister" in panelItem) { panelItem.unregister(); } } }); const panelEl = document.querySelector(this.getPanelSelector(this._panels[panel])); panelEl.classList.remove("hide"); const focusEl = panelEl.querySelector(".firstTabindex"); if(focusEl) { focusEl.focus(); } }, showPreviousPanel() { if (!this._previousPanel) { throw new Error("Current panel not set!"); } this.showPanel(this._previousPanel, this._currentIdentity); }, registerPanel(panelName, panelObject) { this._panels[panelName] = panelObject; panelObject.initialize(); }, identities() { return this._identities; }, currentIdentity() { if (!this._currentIdentity) { throw new Error("CurrentIdentity must be set before calling Logic.currentIdentity."); } return this._currentIdentity; }, currentUserContextId() { const identity = Logic.currentIdentity(); return Utils.userContextId(identity.cookieStoreId); }, currentCookieStoreId() { const identity = Logic.currentIdentity(); return identity.cookieStoreId; }, removeIdentity(userContextId) { if (!userContextId) { return Promise.reject("removeIdentity must be called with userContextId argument."); } return browser.runtime.sendMessage({ method: "deleteContainer", message: { userContextId } }); }, getAssignment(tab) { return browser.runtime.sendMessage({ method: "getAssignment", tabId: tab.id }); }, getAssignmentObjectByContainer(userContextId) { if (!userContextId) { return {}; } return browser.runtime.sendMessage({ method: "getAssignmentObjectByContainer", message: { userContextId } }); }, generateIdentityName() { const defaultName = "Container #"; const ids = []; // This loop populates the 'ids' array with all the already-used ids. this._identities.forEach(identity => { if (identity.name.startsWith(defaultName)) { const id = parseInt(identity.name.substr(defaultName.length), 10); if (id) { ids.push(id); } } }); // Here we find the first valid id. for (let id = 1; ; ++id) { if (ids.indexOf(id) === -1) { return defaultName + (id < 10 ? "0" : "") + id; } } }, getCurrentPanelElement() { const panelItem = this._panels[this._currentPanel]; return document.querySelector(this.getPanelSelector(panelItem)); }, }; // P_ONBOARDING_1: First page for Onboarding. // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_1, { panelSelector: ".onboarding-panel-1", securityPanelSelector: ".security-onboarding-panel-1", // This method is called when the object is registered. initialize() { // Let's move to the next panel. [...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => { Utils.addEnterHandler(startElement, async () => { await Logic.setOnboardingStage(1); Logic.showPanel(P_ONBOARDING_2); }); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_2: Second page for Onboarding. // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_2, { panelSelector: ".onboarding-panel-2", securityPanelSelector: ".security-onboarding-panel-2", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. [...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => { Utils.addEnterHandler(nextElement, async () => { await Logic.setOnboardingStage(2); Logic.showPanel(P_ONBOARDING_3); }); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_3: Third page for Onboarding. // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_3, { panelSelector: ".onboarding-panel-3", securityPanelSelector: ".security-onboarding-panel-3", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. [...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => { Utils.addEnterHandler(almostElement, async () => { await Logic.setOnboardingStage(3); Logic.showPanel(P_ONBOARDING_4); }); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_4: Fourth page for Onboarding. // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_4, { panelSelector: ".onboarding-panel-4", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. Utils.addEnterHandler(document.querySelector("#onboarding-done-button"), async () => { await Logic.setOnboardingStage(4); Logic.showPanel(P_ONBOARDING_5); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_5: Fifth page for Onboarding: new tab long-press behavior // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_5, { panelSelector: ".onboarding-panel-5", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. Utils.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async () => { await Logic.setOnboardingStage(5); Logic.showPanel(P_ONBOARDING_6); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_6, { panelSelector: ".onboarding-panel-6", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. Utils.addEnterHandler(document.querySelector("#start-sync-button"), async () => { await Logic.setOnboardingStage(6); await browser.storage.local.set({syncEnabled: true}); await browser.runtime.sendMessage({ method: "resetSync" }); Logic.showPanel(P_ONBOARDING_7); }); Utils.addEnterHandler(document.querySelector("#no-sync"), async () => { await Logic.setOnboardingStage(7); await browser.storage.local.set({syncEnabled: false}); await browser.runtime.sendMessage({ method: "resetSync" }); Logic.showPanel(P_CONTAINERS_LIST); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior // ---------------------------------------------------------------------------- Logic.registerPanel(P_ONBOARDING_7, { panelSelector: ".onboarding-panel-7", // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. Utils.addEnterHandler(document.querySelector("#sign-in"), async () => { browser.tabs.create({ url: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=multi-account-containers&utm_source=addon&utm_medium=panel&utm_campaign=container-sync", }); await Logic.setOnboardingStage(7); Logic.showPanel(P_CONTAINERS_LIST); }); Utils.addEnterHandler(document.querySelector("#no-sign-in"), async () => { await Logic.setOnboardingStage(7); Logic.showPanel(P_CONTAINERS_LIST); }); }, // This method is called when the panel is shown. prepare() { return Promise.resolve(null); }, }); // P_CONTAINERS_LIST: The list of containers. The main page. // ---------------------------------------------------------------------------- Logic.registerPanel(P_CONTAINERS_LIST, { panelSelector: "#container-panel", // This method is called when the object is registered. async initialize() { Utils.addEnterHandler(document.querySelector("#manage-containers-link"), (e) => { if (!e.target.classList.contains("disable-edit-containers")) { Logic.showPanel(P_CONTAINERS_EDIT); } }); Utils.addEnterHandler(document.querySelector("#open-new-tab-in"), () => { Logic.showPanel(P_CONTAINER_PICKER, null, OPEN_NEW_CONTAINER_PICKER); }); Utils.addEnterHandler(document.querySelector("#reopen-site-in"), () => { Logic.showPanel(P_CONTAINER_PICKER, null, REOPEN_IN_CONTAINER); }); Utils.addEnterHandler(document.querySelector("#always-open-in"), () => { Logic.showPanel(P_CONTAINER_PICKER, null, ALWAYS_OPEN_IN_PICKER); }); Utils.addEnterHandler(document.querySelector("#sort-containers-link"), async () => { try { await browser.runtime.sendMessage({ method: "sortTabs" }); window.close(); } catch (e) { window.close(); } }); document.addEventListener("keydown", (e) => { const selectables = [...document.querySelectorAll("[tabindex='0'], [tabindex='-1']")]; const element = document.activeElement; const index = selectables.indexOf(element) || 0; function next() { const nextElement = selectables[index + 1]; if (nextElement) { nextElement.focus(); } } function previous() { const previousElement = selectables[index - 1]; if (previousElement) { previousElement.focus(); } } switch (e.keyCode) { case 40: next(); break; case 38: previous(); break; case 13: { const panel = Logic.getCurrentPanelElement(); const button = panel.getElementsByTagName("A")[0]; if(button) { button.click(); } break; } case 39: { const showTabs = element.parentNode.querySelector(".show-tabs"); if(showTabs) { showTabs.click(); } break; } case 37: { const hideTabs = document.querySelector(".panel-back-arrow"); if(hideTabs) { hideTabs.click(); } break; } default: if ((e.keyCode >= 49 && e.keyCode <= 57) && Logic._currentPanel === "containersList") { const element = selectables[e.keyCode - 48]; if (element) { element.click(); } } break; } }); // When the popup is open sometimes the tab will still be updating it's state this.tabUpdateHandler = (tabId, changeInfo) => { const propertiesToUpdate = ["title", "favIconUrl"]; const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => { if (propertiesToUpdate.includes(changeInfoKey)) { return true; } }); if (hasChanged) { this.prepareCurrentTabHeader(); } }; browser.tabs.onUpdated.addListener(this.tabUpdateHandler); }, unregister() { browser.tabs.onUpdated.removeListener(this.tabUpdateHandler); }, setupAssignmentCheckbox(siteSettings, currentUserContextId) { const assignmentCheckboxElement = document.getElementById("container-page-assigned"); let checked = false; if (siteSettings && Number(siteSettings.userContextId) === currentUserContextId) { checked = true; } assignmentCheckboxElement.checked = checked; let disabled = false; if (siteSettings === false) { disabled = true; } assignmentCheckboxElement.disabled = disabled; }, async prepareCurrentTabHeader() { const currentTab = await Logic.currentTab(); const currentTabElement = document.getElementById("current-tab"); const assignmentCheckboxElement = document.getElementById("container-page-assigned"); const currentTabUserContextId = Logic.userContextId(currentTab.cookieStoreId); assignmentCheckboxElement.addEventListener("change", () => { Logic.setOrRemoveAssignment(currentTab.id, currentTab.url, currentTabUserContextId, !assignmentCheckboxElement.checked); }); currentTabElement.hidden = !currentTab; this.setupAssignmentCheckbox(false, currentTabUserContextId); if (currentTab) { const identity = await Logic.identity(currentTab.cookieStoreId); const siteSettings = await Logic.getAssignment(currentTab); this.setupAssignmentCheckbox(siteSettings, currentTabUserContextId); const currentPage = document.getElementById("current-page"); currentPage.innerHTML = escaped`${currentTab.title}`; const favIconElement = Utils.createFavIconElement(currentTab.favIconUrl || ""); currentPage.prepend(favIconElement); const currentContainer = document.getElementById("current-container"); currentContainer.innerText = identity.name; currentContainer.setAttribute("data-identity-color", identity.color); } }, // This method is called when the panel is shown. async prepare() { const fragment = document.createDocumentFragment(); this.prepareCurrentTabHeader(); Logic.identities().forEach(identity => { const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs); const tr = document.createElement("tr"); tr.classList.add("menu-item"); const td = document.createElement("td"); const openTabs = identity.numberOfOpenTabs || "" ; td.innerHTML = Utils.escaped`
`; context.querySelector(".container-name").textContent = identity.name; manage.innerHTML = "