gecko-dev/browser/base/content/upgradeDialog.js
Ed Lee dde1e1b541 Bug 1706489 - Allow for calculating upgrade dialog funnel by first screen primary action r=nanj
Reuse existing "show" event object to track which primary button was shown on the first screen. Update Events.yaml with longer description of expected values.

Differential Revision: https://phabricator.services.mozilla.com/D112837
2021-04-21 07:42:13 +00:00

221 lines
7.3 KiB
JavaScript

/* 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 {
AddonManager,
document: gDoc,
getShellService,
Services,
} = window.docShell.chromeEventHandler.ownerGlobal;
const SHELL = getShellService();
const IS_DEFAULT = SHELL.isDefaultBrowser();
const NEED_PIN = SHELL.doesAppNeedPin();
// Strings for various elements with matching ids on each screen.
const PIN_OR_THEME_STRING = NEED_PIN.then(pin =>
pin
? "upgrade-dialog-new-primary-pin-button"
: "upgrade-dialog-new-primary-theme-button"
);
const PRIMARY_OR_DEFAULT_STRING = NEED_PIN.then(pin =>
pin
? "upgrade-dialog-new-primary-primary-button"
: "upgrade-dialog-new-primary-default-button"
);
const SCREEN_STRINGS = [
{
title: "upgrade-dialog-new-title",
primary: IS_DEFAULT ? PIN_OR_THEME_STRING : PRIMARY_OR_DEFAULT_STRING,
secondary: "upgrade-dialog-new-secondary-button",
},
{
title: "upgrade-dialog-theme-title",
primary: "upgrade-dialog-theme-primary-button",
secondary: "upgrade-dialog-theme-secondary-button",
},
];
// Themes that can be selected by the button with matching index.
const THEME_IDS = [
"default-theme@mozilla.org",
"firefox-compact-light@mozilla.org",
"firefox-compact-dark@mozilla.org",
"firefox-alpenglow@mozilla.org",
];
// Callbacks to run when the dialog closes (both from this file or externally).
const CLEANUP = [];
addEventListener("pagehide", () => CLEANUP.forEach(f => f()), { once: true });
// Save the previous theme to revert to it.
let gPrevTheme = AddonManager.getAddonsByTypes(["theme"]).then(addons => {
for (const { id, isActive, screenshots } of addons) {
if (isActive) {
// Only show the "keep" theme option for "other" themes.
if (!THEME_IDS.includes(id)) {
THEME_IDS.push(id);
}
// Assume we need to revert the theme unless cleared.
CLEANUP.push(() => gPrevTheme && enableTheme(id));
return {
id,
swatch: screenshots?.[0].url,
};
}
}
// If there were no active themes, the default will be selected.
return { id: THEME_IDS[0] };
});
// Helper to switch themes.
async function enableTheme(id) {
(await AddonManager.getAddonByID(id)).enable();
}
// Helper to show the theme in chrome with an adjusted modal backdrop.
function adjustModalBackdrop() {
const { classList } = gDoc.getElementById("window-modal-dialog");
classList.add("showToolbar");
CLEANUP.push(() => classList.remove("showToolbar"));
}
// Helper to record various events from the dialog content.
function recordEvent(obj, val) {
Services.telemetry.recordEvent("upgrade_dialog", "content", obj, `${val}`);
}
// Assume the dialog closes from an external trigger unless this helper is used.
let gCloseReason = "external";
CLEANUP.push(() => recordEvent("close", gCloseReason));
function closeDialog(reason) {
gCloseReason = reason;
close();
}
// Detect quit requests to proactively dismiss to allow the quit prompt to show
// as otherwise gDialogBox queues the prompt as these share the same display.
const QUIT_TOPIC = "quit-application-requested";
const QUIT_OBSERVER = () => closeDialog(QUIT_TOPIC);
Services.obs.addObserver(QUIT_OBSERVER, QUIT_TOPIC);
CLEANUP.push(() => Services.obs.removeObserver(QUIT_OBSERVER, QUIT_TOPIC));
// Hook up dynamic behaviors of the dialog.
function onLoad(ready) {
const title = document.getElementById("title");
const subtitle = document.getElementById("subtitle");
const items = document.querySelector(".items");
const themes = document.querySelector(".themes");
const primary = document.getElementById("primary");
const secondary = document.getElementById("secondary");
const steps = document.querySelector(".steps");
// Update the screen content and handle actions.
let current = -1;
(async function advance({ target } = {}) {
// Record which button was clicked.
if (target) {
recordEvent("button", target.dataset.l10nId);
}
// Move to the next screen and perform screen-specific behavior.
switch (++current) {
case 0:
// Wait for main button clicks on each screen.
primary.addEventListener("click", advance);
secondary.addEventListener("click", advance);
break;
case 1:
// Handle first screen conditional actions.
if (target === primary) {
if (!IS_DEFAULT) {
SHELL.setAsDefault();
}
SHELL.pinToTaskbar();
} else if (target === secondary && IS_DEFAULT && !(await NEED_PIN)) {
closeDialog("early");
return;
}
// Prepare the initial theme selection and wait for theme button clicks.
const { id, swatch } = await gPrevTheme;
themes.children[THEME_IDS.indexOf(id)].checked = true;
themes.addEventListener("click", ({ target: button }) => {
// Ignore clicks on whitespace of the container around theme buttons.
if (button.parentNode === themes) {
// Enable the theme of the corresponding button position.
const index = [...themes.children].indexOf(button);
enableTheme(THEME_IDS[index]);
recordEvent("theme", index);
}
});
// Remove the last "keep" theme option if the user didn't customize.
if (themes.childElementCount > THEME_IDS.length) {
themes.removeChild(themes.lastElementChild);
} else if (swatch) {
themes.lastElementChild.style.setProperty(
"--theme-swatch",
`url("${swatch}")`
);
}
// Update content and backdrop for theme screen.
subtitle.remove();
items.remove();
themes.classList.remove("hidden");
steps.appendChild(steps.firstChild);
adjustModalBackdrop();
break;
case 2:
// New theme is confirmed, so don't revert to previous.
if (target === primary) {
gPrevTheme = null;
}
closeDialog("complete");
return;
}
// Update strings for reused elements that change between screens.
const strings = SCREEN_STRINGS[current];
document.l10n.setAttributes(title, strings.title);
document.l10n.setAttributes(primary, await strings.primary);
document.l10n.setAttributes(secondary, strings.secondary);
// Wait for initial translations to load before getting sizing information.
await document.l10n.ready;
requestAnimationFrame(() => {
// Ensure the primary button is focused on each screen.
primary.focus();
// Save first screen height, so later screens can flex and anchor content.
if (current === 0) {
document.body.style.minHeight = getComputedStyle(document.body).height;
// Record which of four primary button was shown for the first screen.
recordEvent("show", primary.dataset.l10nId);
// Indicate to SubDialog that we're done sizing the first screen.
ready();
}
// Let testing know the screen is ready to continue.
dispatchEvent(new CustomEvent("ready"));
});
// Record that the screen was shown.
recordEvent("show", current);
})();
}
document.mozSubdialogReady = new Promise(resolve =>
document.addEventListener("DOMContentLoaded", () => onLoad(resolve), {
once: true,
})
);