commit cac86b578ebe55a63f2f3792f4b14b193b028b73
parent d6a3a654e47ead3ff2543f44aba87fb395c9981c
Author: Rebecca King <rking@mozilla.com>
Date: Wed, 3 Dec 2025 21:15:07 +0000
Bug 1998696 - Add prefs to track whether a user has ever turned on VPN, autostart, or site exceptions - r=ip-protection-reviewers,baku
Differential Revision: https://phabricator.services.mozilla.com/D273636
Diffstat:
9 files changed, 205 insertions(+), 0 deletions(-)
diff --git a/browser/components/ipprotection/IPPOnboardingMessageHelper.sys.mjs b/browser/components/ipprotection/IPPOnboardingMessageHelper.sys.mjs
@@ -0,0 +1,90 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+import { ONBOARDING_PREF_FLAGS } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
+ IPPProxyStates: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
+});
+
+const ONBOARDING_MESSAGE_MASK_PREF =
+ "browser.ipProtection.onboardingMessageMask";
+const AUTOSTART_PREF = "browser.ipProtection.autoStartEnabled";
+const MODE_PREF = "browser.ipProtection.exceptionsMode";
+const PERM_NAME = "ipp-vpn";
+
+/**
+ * This class handles in-panel continuous onboarding messages, including setting
+ * the browser.ipProtection.onboardingMessageMask, a pref that gates messages
+ * according to feature (general VPN, autostart, site exceptions) through bit mask
+ */
+class IPPOnboardingMessageHelper {
+ constructor() {
+ this.handleEvent = this.#handleEvent.bind(this);
+
+ Services.prefs.addObserver(AUTOSTART_PREF, () =>
+ this.setOnboardingFlag(ONBOARDING_PREF_FLAGS.EVER_TURNED_ON_AUTOSTART)
+ );
+
+ let autoStartPref = Services.prefs.getBoolPref(AUTOSTART_PREF, false);
+ if (autoStartPref) {
+ this.setOnboardingFlag(ONBOARDING_PREF_FLAGS.EVER_TURNED_ON_AUTOSTART);
+ }
+
+ Services.prefs.addObserver(MODE_PREF, () =>
+ this.setOnboardingFlag(ONBOARDING_PREF_FLAGS.EVER_USED_SITE_EXCEPTIONS)
+ );
+
+ // If at least one exception is saved, don't show site exceptions onboarding message
+ let savedSites = Services.perms.getAllByTypes([PERM_NAME]);
+ if (savedSites.length !== 0) {
+ this.setOnboardingFlag(ONBOARDING_PREF_FLAGS.EVER_USED_SITE_EXCEPTIONS);
+ }
+ }
+
+ init() {
+ lazy.IPPProxyManager.addEventListener(
+ "IPPProxyManager:StateChanged",
+ this.handleEvent
+ );
+ }
+
+ initOnStartupCompleted() {}
+
+ uninit() {
+ lazy.IPPProxyManager.removeEventListener(
+ "IPPProxyManager:StateChanged",
+ this.handleEvent
+ );
+ }
+
+ readPrefMask() {
+ return Services.prefs.getIntPref(ONBOARDING_MESSAGE_MASK_PREF, 0);
+ }
+
+ writeOnboardingTriggerPref(mask) {
+ Services.prefs.setIntPref(ONBOARDING_MESSAGE_MASK_PREF, mask);
+ }
+
+ setOnboardingFlag(flag) {
+ const mask = this.readPrefMask();
+ this.writeOnboardingTriggerPref(mask | flag);
+ }
+
+ #handleEvent(event) {
+ if (
+ event.type == "IPPProxyManager:StateChanged" &&
+ lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE
+ ) {
+ this.setOnboardingFlag(ONBOARDING_PREF_FLAGS.EVER_TURNED_ON_VPN);
+ }
+ }
+}
+
+const IPPOnboardingMessage = new IPPOnboardingMessageHelper();
+
+export { IPPOnboardingMessage };
diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs
@@ -23,6 +23,7 @@ import { IPPProxyManager } from "resource:///modules/ipprotection/IPPProxyManage
import { IPPAutoStartHelpers } from "resource:///modules/ipprotection/IPPAutoStart.sys.mjs";
import { IPPEnrollAndEntitleManager } from "resource:///modules/ipprotection/IPPEnrollAndEntitleManager.sys.mjs";
import { IPPNimbusHelper } from "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs";
+import { IPPOnboardingMessage } from "resource:///modules/ipprotection/IPPOnboardingMessageHelper.sys.mjs";
import { IPProtectionServerlist } from "resource:///modules/ipprotection/IPProtectionServerlist.sys.mjs";
import { IPPSignInWatcher } from "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs";
import { IPPStartupCache } from "resource:///modules/ipprotection/IPPStartupCache.sys.mjs";
@@ -83,6 +84,7 @@ const IPPHelpers = [
IPPSignInWatcher,
IPProtectionServerlist,
IPPEnrollAndEntitleManager,
+ IPPOnboardingMessage,
IPPProxyManager,
new UIHelper(),
IPPVPNAddonHelper,
diff --git a/browser/components/ipprotection/content/ipprotection-constants.mjs b/browser/components/ipprotection/content/ipprotection-constants.mjs
@@ -35,3 +35,9 @@ export const SIGNIN_DATA = Object.freeze({
utm_term: "fx-vpn-pilot-panel-button",
},
});
+
+export const ONBOARDING_PREF_FLAGS = {
+ EVER_TURNED_ON_AUTOSTART: 1 << 0,
+ EVER_USED_SITE_EXCEPTIONS: 1 << 1,
+ EVER_TURNED_ON_VPN: 1 << 2,
+};
diff --git a/browser/components/ipprotection/moz.build b/browser/components/ipprotection/moz.build
@@ -17,6 +17,7 @@ EXTRA_JS_MODULES.ipprotection += [
"IPPExceptionsManager.sys.mjs",
"IPPNetworkErrorObserver.sys.mjs",
"IPPNimbusHelper.sys.mjs",
+ "IPPOnboardingMessageHelper.sys.mjs",
"IPPOptOutHelper.sys.mjs",
"IPPProxyManager.sys.mjs",
"IPProtection.sys.mjs",
diff --git a/browser/components/ipprotection/tests/browser/browser_exceptions_dialog.js b/browser/components/ipprotection/tests/browser/browser_exceptions_dialog.js
@@ -11,6 +11,8 @@ const { IPPExceptionsManager } = ChromeUtils.importESModule(
const MODE_PREF = "browser.ipProtection.exceptionsMode";
const ALL_MODE = "all";
const SELECT_MODE = "select";
+const ONBOARDING_MESSAGE_MASK_PREF =
+ "browser.ipProtection.onboardingMessageMask";
const PERM_NAME = "ipp-vpn";
@@ -182,6 +184,8 @@ add_task(async function test_filter_dialog_exclusions_only() {
await testExceptionsInDialog(exclusions, capabilityFilter);
cleanupExceptions();
+ await SpecialPowers.popPrefEnv();
+ Services.prefs.clearUserPref(ONBOARDING_MESSAGE_MASK_PREF);
});
/**
@@ -200,4 +204,6 @@ add_task(async function test_filter_dialog_inclusions_only() {
await testExceptionsInDialog(inclusions, capabilityFilter);
cleanupExceptions();
+ await SpecialPowers.popPrefEnv();
+ Services.prefs.clearUserPref(ONBOARDING_MESSAGE_MASK_PREF);
});
diff --git a/browser/components/ipprotection/tests/browser/head.js b/browser/components/ipprotection/tests/browser/head.js
@@ -293,6 +293,7 @@ add_setup(async function setupVPN() {
Services.prefs.clearUserPref("browser.ipProtection.stateCache");
Services.prefs.clearUserPref("browser.ipProtection.entitlementCache");
Services.prefs.clearUserPref("browser.ipProtection.locationListCache");
+ Services.prefs.clearUserPref("browser.ipProtection.onboardingMessageMask");
});
});
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js
@@ -13,6 +13,8 @@ const { TestUtils } = ChromeUtils.importESModule(
const MODE_PREF = "browser.ipProtection.exceptionsMode";
const ALL_MODE = "all";
const SELECT_MODE = "select";
+const ONBOARDING_MESSAGE_MASK_PREF =
+ "browser.ipProtection.onboardingMessageMask";
const PERM_NAME = "ipp-vpn";
@@ -68,6 +70,7 @@ add_task(async function test_IPPExceptionsManager_exclusions() {
Assert.ok(!permissionObj2, `Permission object for ${site2} no longer exists`);
Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(ONBOARDING_MESSAGE_MASK_PREF);
IPPExceptionsManager.uninit();
});
@@ -123,6 +126,7 @@ add_task(async function test_IPPExceptionsManager_inclusions() {
Assert.ok(!permissionObj2, `Permission object for ${site2} no longer exists`);
Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(ONBOARDING_MESSAGE_MASK_PREF);
IPPExceptionsManager.uninit();
});
@@ -207,5 +211,6 @@ add_task(async function test_IPPExceptionsManager_switch_mode() {
);
Services.prefs.clearUserPref(MODE_PREF);
+ Services.prefs.clearUserPref(ONBOARDING_MESSAGE_MASK_PREF);
IPPExceptionsManager.uninit();
});
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPOnboardingMessageHelper.js b/browser/components/ipprotection/tests/xpcshell/test_IPPOnboardingMessageHelper.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+"use strict";
+
+const { IPPOnboardingMessage } = ChromeUtils.importESModule(
+ "resource:///modules/ipprotection/IPPOnboardingMessageHelper.sys.mjs"
+);
+const { ONBOARDING_PREF_FLAGS } = ChromeUtils.importESModule(
+ "chrome://browser/content/ipprotection/ipprotection-constants.mjs"
+);
+const AUTOSTART_PREF = "browser.ipProtection.autoStartEnabled";
+const MODE_PREF = "browser.ipProtection.exceptionsMode";
+
+add_setup(async function () {
+ await putServerInRemoteSettings();
+});
+
+/**
+ * Tests that onboarding message flags are set for VPN start, autostart, and site exceptions
+ */
+add_task(async function test_IPPOnboardingMessage() {
+ let sandbox = sinon.createSandbox();
+ setupStubs(sandbox);
+
+ IPProtectionService.init();
+
+ await waitForEvent(
+ IPProtectionService,
+ "IPProtectionService:StateChanged",
+ () => IPProtectionService.state === IPProtectionStates.READY
+ );
+
+ Assert.ok(
+ !IPPProxyManager.activatedAt,
+ "IP Protection service should not be active initially"
+ );
+
+ let startedEventPromise = waitForEvent(
+ IPPProxyManager,
+ "IPPProxyManager:StateChanged",
+ () => IPPProxyManager.state === IPPProxyStates.ACTIVE
+ );
+
+ IPPProxyManager.start();
+
+ Assert.equal(
+ IPPProxyManager.state,
+ IPPProxyStates.ACTIVATING,
+ "Proxy activation"
+ );
+
+ await startedEventPromise;
+ info("after startedEventPromise");
+ Assert.equal(
+ IPPProxyManager.state,
+ IPPProxyStates.ACTIVE,
+ "IP Protection service should be active after starting"
+ );
+
+ // Check for ever turned on VPN flag
+ Assert.notStrictEqual(
+ IPPOnboardingMessage.readPrefMask() &
+ ONBOARDING_PREF_FLAGS.EVER_TURNED_ON_VPN,
+ 0,
+ "Ever turned on VPN flag should be set"
+ );
+
+ // Turn on autostart
+ Services.prefs.setBoolPref(AUTOSTART_PREF, true);
+ // Check for ever turned on autostart flag
+ Assert.notStrictEqual(
+ IPPOnboardingMessage.readPrefMask() &
+ ONBOARDING_PREF_FLAGS.EVER_TURNED_ON_AUTOSTART,
+ 0,
+ "Ever turned on autostart flag should be set"
+ );
+
+ // Turn on site exceptions
+ Services.prefs.setStringPref(MODE_PREF, "select");
+ // Check for ever turned on site exceptions flag
+ Assert.notStrictEqual(
+ IPPOnboardingMessage.readPrefMask() &
+ ONBOARDING_PREF_FLAGS.EVER_USED_SITE_EXCEPTIONS,
+ 0,
+ "Ever used site exceptions flag should be set"
+ );
+
+ IPProtectionService.uninit();
+ sandbox.restore();
+});
diff --git a/browser/components/ipprotection/tests/xpcshell/xpcshell.toml b/browser/components/ipprotection/tests/xpcshell/xpcshell.toml
@@ -15,6 +15,8 @@ prefs = [
["test_IPPExceptionsManager.js"]
+["test_IPPOnboardingMessageHelper.js"]
+
["test_IPPStartupCache.js"]
["test_IPProtection.js"]