commit fc938b0812fafe9b20a0c6f56c9dc91efa63f954
parent c3a2211fbe1d24bf77a4283a2c29b96fe1feba74
Author: Rebecca King <rking@mozilla.com>
Date: Thu, 13 Nov 2025 21:03:00 +0000
Bug 1997469 - Add opted out state to IPProtectionService - r=ip-protection-reviewers,omc-reviewers,baku,mimi
Differential Revision: https://phabricator.services.mozilla.com/D271645
Diffstat:
9 files changed, 98 insertions(+), 0 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -3509,6 +3509,8 @@ pref("browser.contextual-services.contextId.rust-component.enabled", true);
// Pref to enable the IP protection feature
pref("browser.ipProtection.enabled", false);
+// Pref to track whether the user has opted out of using IP Protection
+pref("browser.ipProtection.optedOut", false);
// Pref to enable IP protection autostart
pref("browser.ipProtection.autoStartEnabled", false);
pref("browser.ipProtection.autoStartPrivateEnabled", false);
diff --git a/browser/components/ipprotection/IPPOptOutHelper.sys.mjs b/browser/components/ipprotection/IPPOptOutHelper.sys.mjs
@@ -0,0 +1,42 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ IPProtectionService:
+ "resource:///modules/ipprotection/IPProtectionService.sys.mjs",
+});
+
+const OPTED_OUT_PREF = "browser.ipProtection.optedOut";
+
+/**
+ * This class monitors the optedOut pref and if it sees an opted-out state, it
+ * sets the state on IPProtectionService
+ */
+class IPPOptedOutHelperSingleton {
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "optedOut",
+ OPTED_OUT_PREF,
+ false,
+ () => {
+ lazy.IPProtectionService.updateState();
+ }
+ );
+ }
+
+ init() {}
+
+ uninit() {}
+
+ initOnStartupCompleted() {}
+}
+
+const IPPOptOutHelper = new IPPOptedOutHelperSingleton();
+
+export { IPPOptOutHelper };
diff --git a/browser/components/ipprotection/IPProtection.sys.mjs b/browser/components/ipprotection/IPProtection.sys.mjs
@@ -15,6 +15,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource:///modules/ipprotection/IPProtectionPanel.sys.mjs",
IPProtectionService:
"resource:///modules/ipprotection/IPProtectionService.sys.mjs",
+ IPProtectionStates:
+ "resource:///modules/ipprotection/IPProtectionService.sys.mjs",
IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
IPPProxyStates: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
@@ -335,6 +337,13 @@ class IPProtectionWidget {
event.type == "IPProtectionService:StateChanged" ||
event.type == "IPPProxyManager:StateChanged"
) {
+ if (
+ lazy.IPProtectionService.state === lazy.IPProtectionStates.OPTED_OUT
+ ) {
+ lazy.CustomizableUI.removeWidgetFromArea(IPProtectionWidget.WIDGET_ID);
+ return;
+ }
+
let status = {
isActive: lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
isError:
diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs
@@ -26,6 +26,7 @@ import { IPPNimbusHelper } from "resource:///modules/ipprotection/IPPNimbusHelpe
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";
+import { IPPOptOutHelper } from "resource:///modules/ipprotection/IPPOptOutHelper.sys.mjs";
import { IPPVPNAddonHelper } from "resource:///modules/ipprotection/IPPVPNAddonHelper.sys.mjs";
/**
@@ -86,6 +87,7 @@ const IPPHelpers = [
new UIHelper(),
IPPVPNAddonHelper,
...IPPAutoStartHelpers,
+ IPPOptOutHelper,
IPPNimbusHelper,
];
diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs
@@ -12,6 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource:///modules/ipprotection/IPPEnrollAndEntitleManager.sys.mjs",
IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs",
IPPNimbusHelper: "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs",
+ IPPOptOutHelper: "resource:///modules/ipprotection/IPPOptOutHelper.sys.mjs",
IPPSignInWatcher: "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs",
IPPStartupCache: "resource:///modules/ipprotection/IPPStartupCache.sys.mjs",
IPPVPNAddonHelper:
@@ -33,6 +34,8 @@ const ENABLED_PREF = "browser.ipProtection.enabled";
* The user is not eligible (via nimbus) or still not signed in. No UI is available.
* @property {string} UNAUTHENTICATED
* The user is signed out but eligible (via nimbus). The panel should show the login view.
+ * @property {string} OPTED_OUT
+ * The user has opted out from using VPN. The toolbar icon and panel should not be visible.
* @property {string} READY
* Ready to be activated.
*
@@ -43,6 +46,7 @@ export const IPProtectionStates = Object.freeze({
UNINITIALIZED: "uninitialized",
UNAVAILABLE: "unavailable",
UNAUTHENTICATED: "unauthenticated",
+ OPTED_OUT: "optedout",
READY: "ready",
});
@@ -156,6 +160,10 @@ class IPProtectionServiceSingleton extends EventTarget {
return IPProtectionStates.UNINITIALIZED;
}
+ if (lazy.IPPOptOutHelper.optedOut) {
+ return IPProtectionStates.OPTED_OUT;
+ }
+
// Maybe we have to use the cached state, because we are not initialized yet.
if (!lazy.IPPStartupCache.isStartupCompleted) {
return lazy.IPPStartupCache.state;
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",
+ "IPPOptOutHelper.sys.mjs",
"IPPProxyManager.sys.mjs",
"IPProtection.sys.mjs",
"IPProtectionHelpers.sys.mjs",
diff --git a/browser/components/ipprotection/tests/browser/browser.toml b/browser/components/ipprotection/tests/browser/browser.toml
@@ -30,6 +30,8 @@ prefs = [
["browser_ipprotection_message_bar.js"]
+["browser_ipprotection_optout.js"]
+
["browser_ipprotection_panel.js"]
["browser_ipprotection_proxy_errors.js"]
diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_optout.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_optout.js
@@ -0,0 +1,31 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ IPProtectionWidget: "resource:///modules/ipprotection/IPProtection.sys.mjs",
+});
+
+/**
+ * Tests that setting the optedOut pref removes the toolbar button
+ */
+add_task(async function test_opt_out() {
+ let buttonBefore = document.getElementById(lazy.IPProtectionWidget.WIDGET_ID);
+
+ Assert.ok(
+ BrowserTestUtils.isVisible(buttonBefore),
+ "ipprotection toolbar button should be present"
+ );
+
+ Services.prefs.setBoolPref("browser.ipProtection.optedOut", true);
+
+ let buttonAfter = document.getElementById(lazy.IPProtectionWidget.WIDGET_ID);
+
+ Assert.ok(!buttonAfter, "ipprotection toolbar button should not be present");
+
+ Services.prefs.clearUserPref("browser.ipProtection.optedOut");
+});
diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs
@@ -251,6 +251,7 @@ export const SpecialMessageActions = {
"browser.crashReports.unsubmittedCheck.autoSubmit2",
"browser.dataFeatureRecommendations.enabled",
"browser.ipProtection.enabled",
+ "browser.ipProtection.optedOut",
"browser.migrate.content-modal.about-welcome-behavior",
"browser.migrate.content-modal.import-all.enabled",
"browser.migrate.preferences-entrypoint.enabled",