tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit bc0458bdc99c40e258a8776413832bca4b8ba49d
parent c1a2ceb0deaf5513fd661f7520df21a9ef13154b
Author: Rebecca King <rking@mozilla.com>
Date:   Wed,  3 Dec 2025 16:21:14 +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:
Abrowser/components/ipprotection/IPPOnboardingMessageHelper.sys.mjs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/IPProtectionHelpers.sys.mjs | 2++
Mbrowser/components/ipprotection/content/ipprotection-constants.mjs | 6++++++
Mbrowser/components/ipprotection/moz.build | 1+
Mbrowser/components/ipprotection/tests/browser/browser_exceptions_dialog.js | 6++++++
Mbrowser/components/ipprotection/tests/browser/head.js | 1+
Mbrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js | 5+++++
Abrowser/components/ipprotection/tests/xpcshell/test_IPPOnboardingMessageHelper.js | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/tests/xpcshell/xpcshell.toml | 2++
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"]