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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user