commit 186822e44db0341697995e2ea9c32268ee039f18
parent 165c9f04a88e3d694b4d06f72d2c3492d69f8aec
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date: Sat, 18 Oct 2025 08:13:49 +0000
Bug 1994974 - Introduce IPPNimbusHelper to entralize all the Nimbus calls, r=ip-protection-reviewers,fchasen
Differential Revision: https://phabricator.services.mozilla.com/D269028
Diffstat:
6 files changed, 75 insertions(+), 50 deletions(-)
diff --git a/browser/components/ipprotection/IPPNimbusHelper.sys.mjs b/browser/components/ipprotection/IPPNimbusHelper.sys.mjs
@@ -0,0 +1,56 @@
+/* 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/. */
+
+/**
+ * Note: If you add or modify the list of helpers, make sure to update the
+ * corresponding documentation in the `docs` folder as well.
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ IPProtectionService:
+ "resource:///modules/ipprotection/IPProtectionService.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+/**
+ * This class monitors the eligibility flag from Nimbus
+ */
+class IPPNimbusHelperSingleton {
+ init() {}
+
+ initOnStartupCompleted() {
+ lazy.NimbusFeatures.ipProtection.onUpdate(
+ lazy.IPProtectionService.updateState
+ );
+ }
+
+ uninit() {
+ lazy.NimbusFeatures.ipProtection.offUpdate(
+ lazy.IPProtectionService.updateState
+ );
+ }
+
+ /**
+ * Check if this device is in the experiment with a variant branch.
+ *
+ * @returns {boolean}
+ */
+ get isEligible() {
+ let inExperiment = lazy.NimbusFeatures.ipProtection.getEnrollmentMetadata();
+ let isEligible = inExperiment?.branch && inExperiment.branch !== "control";
+
+ if (inExperiment) {
+ lazy.NimbusFeatures.ipProtection.recordExposureEvent({
+ once: true,
+ });
+ }
+
+ return isEligible;
+ }
+}
+
+const IPPNimbusHelper = new IPPNimbusHelperSingleton();
+
+export { IPPNimbusHelper };
diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs
@@ -21,10 +21,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource:///modules/ipprotection/IPProtectionService.sys.mjs",
IPProtectionStates:
"resource:///modules/ipprotection/IPProtectionService.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});
import { IPPAutoStartHelpers } from "resource:///modules/ipprotection/IPPAutoStart.sys.mjs";
+import { IPPNimbusHelper } from "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs";
import { IPPSignInWatcher } from "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs";
import { IPPStartupCache } from "resource:///modules/ipprotection/IPPStartupCache.sys.mjs";
@@ -142,26 +142,7 @@ class VPNAddonHelper {
}
}
-/**
- * This class monitors the eligibility flag from Nimbus
- */
-class EligibilityHelper {
- init() {}
-
- initOnStartupCompleted() {
- lazy.NimbusFeatures.ipProtection.onUpdate(
- lazy.IPProtectionService.updateState
- );
- }
-
- uninit() {
- lazy.NimbusFeatures.ipProtection.offUpdate(
- lazy.IPProtectionService.updateState
- );
- }
-}
-
-// The order is important! Eligibility must be the last one because nimbus
+// The order is important! NimbusHelper must be the last one because nimbus
// triggers the callback immdiately, which could compute a new state for all
// the helpers.
const IPPHelpers = [
@@ -170,8 +151,8 @@ const IPPHelpers = [
new UIHelper(),
new AccountResetHelper(),
new VPNAddonHelper(),
- new EligibilityHelper(),
...IPPAutoStartHelpers,
+ IPPNimbusHelper,
];
export { IPPHelpers };
diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs
@@ -9,12 +9,12 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
GuardianClient: "resource:///modules/ipprotection/GuardianClient.sys.mjs",
IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs",
+ IPPNimbusHelper: "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs",
IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
IPPSignInWatcher: "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs",
IPPStartupCache: "resource:///modules/ipprotection/IPPStartupCache.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});
import {
@@ -322,24 +322,6 @@ class IPProtectionServiceSingleton extends EventTarget {
}
/**
- * Check if this device is in the experiment with a variant branch.
- *
- * @returns {boolean}
- */
- get isEligible() {
- let inExperiment = lazy.NimbusFeatures.ipProtection.getEnrollmentMetadata();
- let isEligible = inExperiment?.branch && inExperiment.branch !== "control";
-
- if (inExperiment) {
- lazy.NimbusFeatures.ipProtection.recordExposureEvent({
- once: true,
- });
- }
-
- return isEligible;
- }
-
- /**
* Clear the current entitlement and requests a state update to dispatch
* the current hasUpgraded status.
*
@@ -461,7 +443,7 @@ class IPProtectionServiceSingleton extends EventTarget {
// For non authenticated users, we can check if they are eligible (the UI
// is shown and they have to login) or we don't know yet their current
// enroll state (no UI is shown).
- let eligible = this.isEligible;
+ let eligible = lazy.IPPNimbusHelper.isEligible;
if (!lazy.IPPSignInWatcher.isSignedIn) {
return !eligible
? IPProtectionStates.UNAVAILABLE
diff --git a/browser/components/ipprotection/docs/Components.rst b/browser/components/ipprotection/docs/Components.rst
@@ -32,7 +32,7 @@ A diagram of all the main components is the following:
UIHelper["UI Helper"]
AccountResetHelper["Account Reset Helper"]
VPNAddonHelper["VPN Add-on Helper"]
- EligibilityHelper["Nimbus Eligibility Helper"]
+ IPPNimbusHelper["Nimbus Eligibility Helper"]
IPPAutoStart["Auto-Start Helper"]
IPPEarlyStartupFilter["Early Startup Filter Helper"]
end
@@ -132,8 +132,9 @@ VPNAddonHelper
Monitors the installation of the Mozilla VPN add‑on and removes the UI when
appropriate.
-EligibilityHelper
- Monitors the Nimbus experiment flag and triggers state updates when it changes.
+IPPNimbusHelper
+ Monitors the Nimbus feature (``NimbusFeatures.ipProtection``) and triggers a
+ state recomputation on updates.
How to implement new components
-------------------------------
@@ -148,7 +149,8 @@ Recommended steps:
2. If your helper reacts to state changes, listen to the
``IPProtectionService:StateChanged`` event.
3. Add your helper to the ``IPPHelpers`` array in ``IPProtectionHelpers.sys.mjs``.
- Be mindful of ordering if your helper depends on others (e.g. Nimbus
- eligibility is registered last to avoid premature updates).
+ Be mindful of ordering if your helper depends on others. For example,
+ ``IPPNimbusHelper`` is registered last to avoid premature state updates
+ triggered by Nimbus’ immediate callback.
4. If your component needs to trigger a recomputation, call
``IPProtectionService.updateState``.
diff --git a/browser/components/ipprotection/moz.build b/browser/components/ipprotection/moz.build
@@ -15,6 +15,7 @@ EXTRA_JS_MODULES.ipprotection += [
"IPPChannelFilter.sys.mjs",
"IPPExceptionsManager.sys.mjs",
"IPPNetworkErrorObserver.sys.mjs",
+ "IPPNimbusHelper.sys.mjs",
"IPPProxyManager.sys.mjs",
"IPProtection.sys.mjs",
"IPProtectionHelpers.sys.mjs",
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionStates.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionStates.js
@@ -6,6 +6,9 @@ https://creativecommons.org/publicdomain/zero/1.0/ */
const { IPProtectionService, IPProtectionStates } = ChromeUtils.importESModule(
"resource:///modules/ipprotection/IPProtectionService.sys.mjs"
);
+const { IPPNimbusHelper } = ChromeUtils.importESModule(
+ "resource:///modules/ipprotection/IPPNimbusHelper.sys.mjs"
+);
const { IPPSignInWatcher } = ChromeUtils.importESModule(
"resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs"
);
@@ -66,7 +69,7 @@ add_task(async function test_IPProtectionStates_uninitialized() {
"IP Protection service should be unavailable"
);
- sandbox.stub(IPProtectionService, "isEligible").get(() => true);
+ sandbox.stub(IPPNimbusHelper, "isEligible").get(() => true);
await IPProtectionService.updateState();
@@ -98,7 +101,7 @@ add_task(async function test_IPProtectionStates_unauthenticated() {
"IP Protection service should be unavailable"
);
- sandbox.stub(IPProtectionService, "isEligible").get(() => true);
+ sandbox.stub(IPPNimbusHelper, "isEligible").get(() => true);
await IPProtectionService.updateState();
@@ -131,7 +134,7 @@ add_task(async function test_IPProtectionStates_enrolling() {
sandbox
.stub(IPProtectionService.guardian, "isLinkedToGuardian")
.resolves(false);
- sandbox.stub(IPProtectionService, "isEligible").get(() => true);
+ sandbox.stub(IPPNimbusHelper, "isEligible").get(() => true);
sandbox.stub(IPProtectionService.guardian, "enroll").resolves({ ok: true });
sandbox.stub(IPProtectionService.guardian, "fetchUserInfo").resolves({
status: 200,