Bug 1941350 - Add preonboarding modal Nimbus feature r=firefox-desktop-core-reviewers ,omc-reviewers,negin

- Add `preonboarding` Nimbus feature for showing a window modal over about:welcome that cannot be dismissed via ESC
- Include the ability to suppress showing the privacy notice tab on first run using a variable under the new `preonboarding` feature
- Remove legacy `showModal` related `aboutwelcome` Nimbus feature variables (these are not in use and were for [[ https://experimenter.services.mozilla.com/nimbus/window-modal-vs-tab-modal/summary | an old experiment ]])

Differential Revision: https://phabricator.services.mozilla.com/D234039
This commit is contained in:
Meg Viar
2025-01-17 15:18:15 +00:00
parent b12263f84f
commit 3bd09d7884
8 changed files with 137 additions and 91 deletions

View File

@@ -2019,8 +2019,6 @@ pref("browser.newtabpage.activity-stream.hideTopSitesWithSearchParam", "mfadid=a
pref("browser.aboutwelcome.enabled", true);
// Used to set multistage welcome UX
pref("browser.aboutwelcome.screens", "");
// Used to enable window modal onboarding
pref("browser.aboutwelcome.showModal", false);
// Experiment Manager
// See Console.sys.mjs LOG_LEVELS for all possible values

View File

@@ -33,12 +33,12 @@ function fakeShowPolicyTimeout(set, clear) {
reportingPolicy.clearShowInfobarTimeout = clear;
}
function sendSessionRestoredNotification() {
async function sendSessionRestoredNotification() {
let reportingPolicy = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
).Policy;
reportingPolicy.fakeSessionRestoreNotification();
await reportingPolicy.fakeSessionRestoreNotification();
}
/**
@@ -76,7 +76,7 @@ function promiseWaitForNotificationClose(aNotification) {
return deferred.promise;
}
function triggerInfoBar(expectedTimeoutMs) {
async function triggerInfoBar(expectedTimeoutMs) {
let showInfobarCallback = null;
let timeoutMs = null;
fakeShowPolicyTimeout(
@@ -86,7 +86,7 @@ function triggerInfoBar(expectedTimeoutMs) {
},
() => {}
);
sendSessionRestoredNotification();
await sendSessionRestoredNotification();
Assert.ok(!!showInfobarCallback, "Must have a timer callback.");
if (expectedTimeoutMs !== undefined) {
Assert.equal(timeoutMs, expectedTimeoutMs, "Timeout should match");
@@ -179,7 +179,8 @@ add_task(async function test_single_window() {
);
// Wait for the infobar to be displayed.
triggerInfoBar(10 * 1000);
await triggerInfoBar(10 * 1000);
await alertShownPromise;
await promiseNextTick();
@@ -254,7 +255,7 @@ add_task(async function test_multiple_windows() {
);
// Wait for the infobars.
triggerInfoBar(10 * 1000);
await triggerInfoBar(10 * 1000);
await Promise.all(showAlertPromises);
// Both notification were displayed. Close one and check that both gets closed.

View File

@@ -817,14 +817,6 @@ nsBrowserContentHandler.prototype = {
case OVERRIDE_NEW_PROFILE:
// New profile.
gFirstRunProfile = true;
// If we're showing the main onboarding content in a modal, skip
// showing about:welcome as the homepage.
if (
lazy.NimbusFeatures.aboutwelcome.getVariable("showModal") &&
!lazy.NimbusFeatures.aboutwelcome.getVariable("modalScreens")
) {
break;
}
overridePage = Services.urlFormatter.formatURLPref(
"startup.homepage_welcome_url"
);

View File

@@ -4662,18 +4662,18 @@ BrowserGlue.prototype = {
gBrowser.selectedTab = tab;
},
async _showAboutWelcomeModal() {
async _showPreOnboardingModal() {
const { gBrowser } = lazy.BrowserWindowTracker.getTopWindow();
const data = await lazy.NimbusFeatures.aboutwelcome.getAllVariables();
const data = await lazy.NimbusFeatures.preonboarding.getAllVariables();
const config = {
type: "SHOW_SPOTLIGHT",
data: {
content: {
template: "multistage",
id: data?.id || "ABOUT_WELCOME_MODAL",
id: data?.id || "PRE_ONBOARDING_MODAL",
backdrop: data?.backdrop,
screens: data?.modalScreens || data?.screens,
screens: data?.screens,
UTMTerm: data?.UTMTerm,
disableEscClose: data?.requireAction,
// displayed as a window modal by default
@@ -4698,14 +4698,14 @@ BrowserGlue.prototype = {
},
async _maybeShowDefaultBrowserPrompt() {
// Highest priority is about:welcome window modal experiment
// Highest priority is the preonboarding modal
// Second highest priority is the upgrade dialog, which can include a "primary
// browser" request and is limited in various ways, e.g., major upgrades.
if (
lazy.BrowserHandler.firstRunProfile &&
lazy.NimbusFeatures.aboutwelcome.getVariable("showModal")
lazy.NimbusFeatures.preonboarding.getVariable("enabled")
) {
this._showAboutWelcomeModal();
this._showPreOnboardingModal();
return;
}
const dialogVersion = 106;

View File

@@ -9,6 +9,9 @@ XPCOMUtils.defineLazyServiceGetters(this, {
const { SpecialMessageActions } = ChromeUtils.importESModule(
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
);
const { TelemetryReportingPolicyImpl } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
);
const TEST_SCREEN = [
{
@@ -28,18 +31,21 @@ async function waitForClick(selector, win) {
win.document.querySelector(selector).click();
}
async function showAboutWelcomeModal(
async function showPreonboardingModal(
screens = "",
modalScreens = "",
disableFirstRunPolicyTab = false,
requireAction = false
) {
const PREFS_TO_SET = [
["browser.preonboarding.enabled", true],
["browser.preonboarding.screens", screens],
[
"browser.preonboarding.disableFirstRunPolicyTab",
disableFirstRunPolicyTab,
],
["browser.preonboarding.requireAction", requireAction],
["browser.startup.homepage_override.mstone", ""],
["startup.homepage_welcome_url", "about:welcome"],
["browser.aboutwelcome.modalScreens", modalScreens],
["browser.aboutwelcome.screens", screens],
["browser.aboutwelcome.requireAction", requireAction],
["browser.aboutwelcome.showModal", true],
];
await SpecialPowers.pushPrefEnv({
set: PREFS_TO_SET,
@@ -54,41 +60,9 @@ async function showAboutWelcomeModal(
});
}
add_task(async function show_about_welcome_modal() {
add_task(async function show_preonboarding_modal() {
let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await showAboutWelcomeModal(JSON.stringify(TEST_SCREEN));
const [win] = await TestUtils.topicObserved("subdialog-loaded");
Assert.notEqual(
Cc["@mozilla.org/browser/clh;1"]
.getService(Ci.nsIBrowserHandler)
.getFirstWindowArgs(),
"about:welcome",
"First window will not be about:welcome"
);
// Wait for screen content to render
await TestUtils.waitForCondition(() =>
win.document.querySelector(TEST_SCREEN_SELECTOR)
);
Assert.equal(
messageSpy.firstCall.args[0].data.content.disableEscClose,
false
);
Assert.ok(
!!win.document.querySelector(TEST_SCREEN_SELECTOR),
"Modal renders with custom about:welcome screen"
);
await win.close();
sinon.restore();
});
add_task(async function shows_modal_with_custom_screens_over_about_welcome() {
let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await showAboutWelcomeModal("", JSON.stringify(TEST_SCREEN), true);
await showPreonboardingModal(JSON.stringify(TEST_SCREEN));
const [win] = await TestUtils.topicObserved("subdialog-loaded");
Assert.equal(
@@ -104,11 +78,46 @@ add_task(async function shows_modal_with_custom_screens_over_about_welcome() {
win.document.querySelector(TEST_SCREEN_SELECTOR)
);
Assert.equal(messageSpy.firstCall.args[0].data.content.disableEscClose, true);
Assert.ok(
!!win.document.querySelector(TEST_SCREEN_SELECTOR),
"Modal renders with custom modal screen"
"Modal renders with custom screen"
);
Assert.ok(
TelemetryReportingPolicyImpl._openFirstRunPage(),
"Privacy notice will show"
);
Assert.equal(
messageSpy.firstCall.args[0].data.content.disableEscClose,
false,
"Closing via ESC is not disabled"
);
await win.close();
sinon.restore();
});
add_task(async function can_disable_showing_privacy_tab_and_closing_via_esc() {
let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await showPreonboardingModal(JSON.stringify(TEST_SCREEN), true, true);
const [win] = await TestUtils.topicObserved("subdialog-loaded");
// Wait for screen content to render
await TestUtils.waitForCondition(() =>
win.document.querySelector(TEST_SCREEN_SELECTOR)
);
Assert.notEqual(
TelemetryReportingPolicyImpl._openFirstRunPage(),
true,
"Privacy notice tab will not show"
);
Assert.equal(
messageSpy.firstCall.args[0].data.content.disableEscClose,
true,
"Closing via ESC is disabled"
);
await win.close();

View File

@@ -677,10 +677,6 @@ aboutwelcome:
type: json
fallbackPref: browser.aboutwelcome.screens
description: Content to show in the onboarding flow
modalScreens:
type: json
fallbackPref: browser.aboutwelcome.modalScreens
description: Content to show in the onboarding modal, if different from that in the onboarding flow
languageMismatchEnabled:
type: boolean
fallbackPref: intl.multilingual.aboutWelcome.languageMismatchEnabled
@@ -690,16 +686,6 @@ aboutwelcome:
transitions:
type: boolean
description: Enable transition effect between screens
showModal:
type: boolean
fallbackPref: browser.aboutwelcome.showModal
description: >-
Should users see window modal onboarding
requireAction:
type: boolean
fallbackPref: browser.aboutwelcome.requireAction
description: >-
When showModal is enabled, should action be required to proceed (show as a window modal with dismiss using the ESC key disabled)
backdrop:
type: string
fallbackPref: browser.aboutwelcome.backdrop
@@ -713,6 +699,31 @@ aboutwelcome:
description: >-
Should the return to about:welcome toolbar button be shown
preonboarding:
description: "A modal that shows on first startup, typically on top of about:welcome"
owner: omc@mozilla.com
# Exposure is recorded by the spotlight feature used to show the modal
hasExposure: false
variables:
enabled:
type: boolean
fallbackPref: browser.preonboarding.enabled
description: >-
Should users see the preonboarding modal?
screens:
type: json
fallbackPref: browser.preonboarding.screens
description: Content to show in the onboarding flow
disableFirstRunPolicyTab:
type: boolean
fallbackPref: browser.preonboarding.disableFirstRunPolicyTab
description: Should a background tab load the first run policy URL?
requireAction:
type: boolean
fallbackPref: browser.preonboarding.requireAction
description: >-
When showModal is enabled, should action be required to proceed (show as a window modal with dismiss using the ESC key disabled)?
moreFromMozilla:
description: "New page on about:preferences to suggest more Mozilla products"
owner: omc@mozilla.com

View File

@@ -12,6 +12,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});
const LOGGER_NAME = "Toolkit.Telemetry";
@@ -46,7 +47,7 @@ export var Policy = {
now: () => new Date(),
setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearShowInfobarTimeout: id => clearTimeout(id),
fakeSessionRestoreNotification: () => {
fakeSessionRestoreNotification: async () => {
TelemetryReportingPolicyImpl.observe(
null,
"sessionstore-windows-restored",
@@ -162,7 +163,7 @@ export var TelemetryReportingPolicy = {
},
};
var TelemetryReportingPolicyImpl = {
export var TelemetryReportingPolicyImpl = {
_logger: null,
// Keep track of the notification status if user wasn't notified already.
_notificationInProgress: false,
@@ -480,7 +481,7 @@ var TelemetryReportingPolicyImpl = {
/**
* Try to open the privacy policy in a background tab instead of showing the infobar.
*/
_openFirstRunPage() {
async _openFirstRunPage() {
if (!this._shouldNotify()) {
return false;
}
@@ -547,14 +548,19 @@ var TelemetryReportingPolicyImpl = {
win.addEventListener("unload", removeListeners);
win.gBrowser.addTabsProgressListener(progressListener);
tab = win.gBrowser.addTab(firstRunPolicyURL, {
inBackground: true,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
return true;
let res = await lazy.NimbusFeatures.preonboarding.getAllVariables();
if (!res?.disableFirstRunPolicyTab) {
tab = win.gBrowser.addTab(firstRunPolicyURL, {
inBackground: true,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
});
return true;
}
return false;
},
observe(aSubject, aTopic) {
async observe(aSubject, aTopic) {
if (aTopic != "sessionstore-windows-restored") {
return;
}
@@ -562,9 +568,8 @@ var TelemetryReportingPolicyImpl = {
if (this.isFirstRun()) {
// We're performing the first run, flip firstRun preference for subsequent runs.
Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
try {
if (this._openFirstRunPage()) {
if (await this._openFirstRunPage()) {
return;
}
} catch (e) {

View File

@@ -9,6 +9,9 @@
const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
);
const { TelemetryReportingPolicyImpl } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
);
const { UpdateUtils } = ChromeUtils.importESModule(
"resource://gre/modules/UpdateUtils.sys.mjs"
);
@@ -99,13 +102,40 @@ add_task(
);
TelemetryReportingPolicy.reset();
function waitForObserver(topic) {
return new Promise(resolve => {
const originalObserve = TelemetryReportingPolicyImpl.observe;
TelemetryReportingPolicyImpl.observe = async function (
aSubject,
aTopic
) {
try {
await originalObserve.call(this, aSubject, aTopic);
} finally {
if (aTopic === topic) {
TelemetryReportingPolicyImpl.observe = originalObserve;
resolve();
}
}
};
});
}
const firstRunPromise = waitForObserver("sessionstore-windows-restored");
Services.obs.notifyObservers(null, "sessionstore-windows-restored");
await firstRunPromise;
Assert.equal(
startupTimeout,
FIRST_RUN_TIMEOUT_MSEC,
"The infobar display timeout should be 60s on the first run."
);
TelemetryReportingPolicy.reset();
const secondRunPromise = waitForObserver("sessionstore-windows-restored");
Services.obs.notifyObservers(null, "sessionstore-windows-restored");
await secondRunPromise;
// Run again, and check that we actually wait only 10 seconds.
TelemetryReportingPolicy.reset();
Services.obs.notifyObservers(null, "sessionstore-windows-restored");