commit 7f3b1bfeb924d3db10d047be394882a8e2eee110
parent 936fdf8ebfa062e69864b34fee89521500eaa031
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date: Wed, 1 Oct 2025 14:10:14 +0000
Bug 1991875 - Create helper classes to cleanup the IPProtectionService, r=ip-protection-reviewers,rking
Differential Revision: https://phabricator.services.mozilla.com/D266981
Diffstat:
4 files changed, 224 insertions(+), 119 deletions(-)
diff --git a/browser/components/ipprotection/IPPProxyManager.sys.mjs b/browser/components/ipprotection/IPPProxyManager.sys.mjs
@@ -38,6 +38,11 @@ class IPPProxyManager {
#networkErrorObserver = null;
// If this is set, we're awaiting a proxy pass rotation
#rotateProxyPassPromise = null;
+ #activatedAt = false;
+
+ get activatedAt() {
+ return this.#activatedAt;
+ }
get guardian() {
if (!this.#guardian) {
@@ -119,22 +124,26 @@ class IPPProxyManager {
lazy.logConsole.info("Started");
+ if (this.active) {
+ this.#activatedAt = ChromeUtils.now();
+ }
+
return this.active;
}
/**
- * Stops the proxy connection and observers.
+ * Stops the proxy connection and observers. Returns the duration of the connection.
*
- * @returns {Promise<boolean>}
+ * @returns {int}
*/
- async stop() {
+ stop() {
this.#connection?.stop();
this.networkErrorObserver.stop();
this.#connection = null;
lazy.logConsole.info("Stopped");
- return true;
+ return ChromeUtils.now() - this.#activatedAt;
}
/**
diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs
@@ -0,0 +1,190 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ CustomizableUI:
+ "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
+ IPProtection: "resource:///modules/ipprotection/IPProtection.sys.mjs",
+ IPProtectionWidget: "resource:///modules/ipprotection/IPProtection.sys.mjs",
+ IPProtectionService:
+ "resource:///modules/ipprotection/IPProtectionService.sys.mjs",
+ IPProtectionStates:
+ "resource:///modules/ipprotection/IPProtectionService.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+});
+
+const VPN_ADDON_ID = "vpn@mozilla.com";
+
+/**
+ * This simple class controls the UI activation/deactivation.
+ */
+class UIHelper {
+ constructor() {
+ this.handleEvent = this.#handleEvent.bind(this);
+ }
+
+ init() {
+ lazy.IPProtectionService.addEventListener(
+ "IPProtectionService:StateChanged",
+ this.handleEvent
+ );
+ }
+
+ uninit() {
+ lazy.IPProtectionService.removeEventListener(
+ "IPProtectionService:StateChanged",
+ this.handleEvent
+ );
+ lazy.IPProtection.uninit();
+ }
+
+ #handleEvent(_event) {
+ const state = lazy.IPProtectionService.state;
+
+ if (
+ !lazy.IPProtection.isInitialized &&
+ state !== lazy.IPProtectionStates.UNINITIALIZED &&
+ state !== lazy.IPProtectionStates.UNAVAILABLE
+ ) {
+ lazy.IPProtection.init();
+ }
+ }
+}
+
+/**
+ * This simple class resets the account data when needed
+ */
+class AccountResetHelper {
+ constructor() {
+ this.handleEvent = this.#handleEvent.bind(this);
+ }
+
+ init() {
+ lazy.IPProtectionService.addEventListener(
+ "IPProtectionService:StateChanged",
+ this.handleEvent
+ );
+ }
+
+ uninit() {
+ lazy.IPProtectionService.removeEventListener(
+ "IPProtectionService:StateChanged",
+ this.handleEvent
+ );
+ }
+
+ #handleEvent(_event) {
+ // Reset stored account information and stop the proxy,
+ // if the account is no longer available.
+ if (
+ (lazy.IPProtectionService.hasEntitlement &&
+ lazy.IPProtectionService.state ===
+ lazy.IPProtectionStates.UNAVAILABLE) ||
+ lazy.IPProtectionService.state === lazy.IPProtectionStates.UNAUTHENTICATED
+ ) {
+ lazy.IPProtectionService.resetAccount();
+ }
+ }
+}
+
+/**
+ * This class removes the UI widget if the VPN add-on is installed.
+ */
+class VPNAddonHelper {
+ /**
+ * Adds an observer to monitor the VPN add-on installation
+ */
+ init() {
+ this.addonVPNListener = {
+ onInstallEnded(_install, addon) {
+ if (addon.id === VPN_ADDON_ID && lazy.IPProtectionService.hasUpgraded) {
+ // Place the widget in the customization palette.
+ lazy.CustomizableUI.removeWidgetFromArea(
+ lazy.IPProtectionWidget.WIDGET_ID
+ );
+ }
+ },
+ };
+
+ lazy.AddonManager.addInstallListener(this.addonVPNListener);
+ }
+
+ /**
+ * Removes the VPN add-on installation observer
+ */
+ uninit() {
+ if (this.addonVPNListener) {
+ lazy.AddonManager.removeInstallListener(this.addonVPNListener);
+ }
+ }
+}
+
+/**
+ * This class monitors the Sign-In state and triggers the update of the service
+ * if needed.
+ */
+class SignInStateHelper {
+ /**
+ * Adds an observer for the FxA sign-in state.
+ */
+ init() {
+ this.fxaObserver = {
+ QueryInterface: ChromeUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ observe() {
+ let { status } = lazy.UIState.get();
+ let signedIn = status == lazy.UIState.STATUS_SIGNED_IN;
+ if (signedIn !== lazy.IPProtectionService.signedIn) {
+ lazy.IPProtectionService.updateState();
+ }
+ },
+ };
+
+ Services.obs.addObserver(this.fxaObserver, lazy.UIState.ON_UPDATE);
+ }
+
+ /**
+ * Removes the FxA sign-in state observer
+ */
+ uninit() {
+ if (this.fxaObserver) {
+ Services.obs.removeObserver(this.fxaObserver, lazy.UIState.ON_UPDATE);
+ this.fxaObserver = null;
+ }
+ }
+}
+
+/**
+ * This class monitors the eligibility flag from Nimbus
+ */
+class EligibilityHelper {
+ init() {
+ lazy.NimbusFeatures.ipProtection.onUpdate(
+ lazy.IPProtectionService.updateState
+ );
+ }
+
+ uninit() {
+ lazy.NimbusFeatures.ipProtection.offUpdate(
+ lazy.IPProtectionService.updateState
+ );
+ }
+}
+
+const IPPHelpers = [
+ new AccountResetHelper(),
+ new EligibilityHelper(),
+ new SignInStateHelper(),
+ new VPNAddonHelper(),
+ new UIHelper(),
+];
+
+export { IPPHelpers };
diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs
@@ -7,16 +7,13 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
GuardianClient: "resource:///modules/ipprotection/GuardianClient.sys.mjs",
+ IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs",
IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs",
UIState: "resource://services-sync/UIState.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
- IPProtection: "resource:///modules/ipprotection/IPProtection.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
- CustomizableUI:
- "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
});
import {
@@ -26,7 +23,6 @@ import {
const ENABLED_PREF = "browser.ipProtection.enabled";
const LOG_PREF = "browser.ipProtection.log";
-const VPN_ADDON_ID = "vpn@mozilla.com";
const MAX_ERROR_HISTORY = 50;
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
@@ -73,9 +69,6 @@ export const IPProtectionStates = Object.freeze({
* know the current state.
*/
class IPProtectionServiceSingleton extends EventTarget {
- static WIDGET_ID = "ipprotection-button";
- static PANEL_ID = "PanelUI-ipprotection";
-
#state = IPProtectionStates.UNINITIALIZED;
// Prevents multiple `#updateState()` executions at once.
@@ -89,7 +82,8 @@ class IPProtectionServiceSingleton extends EventTarget {
proxyManager = null;
#entitlement = null;
- #activatedAt = false;
+
+ #helpers = null;
/**
* Returns the state of the service. See the description of the state
@@ -111,12 +105,21 @@ class IPProtectionServiceSingleton extends EventTarget {
}
/**
+ * Checks if the service has an entitlement object
+ *
+ * @returns {boolean}
+ */
+ get hasEntitlement() {
+ return !!this.#entitlement;
+ }
+
+ /**
* Checks if the proxy is active and was activated.
*
* @returns {Date}
*/
get activatedAt() {
- return this.proxyManager?.active && this.#activatedAt;
+ return this.proxyManager?.active && this.proxyManager?.activatedAt;
}
constructor() {
@@ -127,6 +130,8 @@ class IPProtectionServiceSingleton extends EventTarget {
this.updateState = this.#updateState.bind(this);
this.setState = this.#setState.bind(this);
this.setErrorState = this.#setErrorState.bind(this);
+
+ this.#helpers = lazy.IPPHelpers;
}
/**
@@ -138,38 +143,31 @@ class IPProtectionServiceSingleton extends EventTarget {
}
this.proxyManager = new lazy.IPPProxyManager(this.guardian);
- this.#addSignInStateObserver();
- this.addVPNAddonObserver();
- this.#addEligibilityListeners();
+ this.#helpers.forEach(helper => helper.init());
await this.#updateState();
}
/**
- * Removes the IPProtectionService and IPProtection widget.
+ * Removes the UI widget.
*/
uninit() {
if (this.#state === IPProtectionStates.UNINITIALIZED) {
return;
}
- lazy.IPProtection.uninit();
-
- this.#removeSignInStateObserver();
- this.removeVPNAddonObserver();
-
if (this.#state === IPProtectionStates.ACTIVE) {
this.stop(false);
}
this.proxyManager?.destroy();
- this.#removeEligibilityListeners();
-
this.#entitlement = null;
this.errors = [];
this.enrolling = null;
this.signedIn = null;
+ this.#helpers.forEach(helper => helper.uninit());
+
this.#setState(IPProtectionStates.UNINITIALIZED);
}
@@ -206,8 +204,6 @@ class IPProtectionServiceSingleton extends EventTarget {
return;
}
- this.#activatedAt = ChromeUtils.now();
-
this.#setState(IPProtectionStates.ACTIVE);
Glean.ipprotection.toggled.record({
@@ -231,9 +227,7 @@ class IPProtectionServiceSingleton extends EventTarget {
return;
}
- let deactivatedAt = ChromeUtils.now();
- let sessionLength = deactivatedAt - this.#activatedAt;
- this.#activatedAt = null;
+ const sessionLength = this.proxyManager.stop();
Glean.ipprotection.toggled.record({
userAction,
@@ -241,7 +235,6 @@ class IPProtectionServiceSingleton extends EventTarget {
enabled: false,
});
- await this.proxyManager.stop();
this.#setState(IPProtectionStates.READY);
this.dispatchEvent(
@@ -336,14 +329,6 @@ class IPProtectionServiceSingleton extends EventTarget {
return isEligible;
}
- #addEligibilityListeners() {
- lazy.NimbusFeatures.ipProtection.onUpdate(this.updateState);
- }
-
- #removeEligibilityListeners() {
- lazy.NimbusFeatures.ipProtection.offUpdate(this.updateState);
- }
-
/**
* Clear the current entitlement and requests a state update to dispatch
* the current hasUpgraded status.
@@ -361,68 +346,6 @@ class IPProtectionServiceSingleton extends EventTarget {
}
/**
- * Adds an observer for the FxA sign-in state.
- */
- #addSignInStateObserver() {
- let manager = this;
- this.fxaObserver = {
- QueryInterface: ChromeUtils.generateQI([
- Ci.nsIObserver,
- Ci.nsISupportsWeakReference,
- ]),
-
- observe() {
- let { status } = lazy.UIState.get();
- let signedIn = status == lazy.UIState.STATUS_SIGNED_IN;
- if (signedIn !== manager.signedIn) {
- manager.updateState();
- }
- },
- };
-
- Services.obs.addObserver(this.fxaObserver, lazy.UIState.ON_UPDATE);
- }
-
- /**
- * Removes the FxA sign-in state observer
- */
- #removeSignInStateObserver() {
- if (this.fxaObserver) {
- Services.obs.removeObserver(this.fxaObserver, lazy.UIState.ON_UPDATE);
- this.fxaObserver = null;
- }
- }
-
- /**
- * Adds an observer to monitor the VPN add-on installation
- */
- addVPNAddonObserver() {
- let service = this;
- this.addonVPNListener = {
- onInstallEnded(_install, addon) {
- if (addon.id === VPN_ADDON_ID && service.hasUpgraded) {
- // Place the widget in the customization palette.
- lazy.CustomizableUI.removeWidgetFromArea(
- IPProtectionServiceSingleton.WIDGET_ID
- );
- lazy.logConsole.info("VPN Extension: Installed");
- }
- },
- };
-
- lazy.AddonManager.addInstallListener(this.addonVPNListener);
- }
-
- /**
- * Removes the VPN add-on installation observer
- */
- removeVPNAddonObserver() {
- if (this.addonVPNListener) {
- lazy.AddonManager.removeInstallListener(this.addonVPNListener);
- }
- }
-
- /**
* Enrolls a users FxA account to use the proxy and updates the state.
*
* @returns {Promise<void>}
@@ -584,24 +507,6 @@ class IPProtectionServiceSingleton extends EventTarget {
* @param {IPProtectionStates} prevState
*/
#stateChanged(state, prevState) {
- // Reset stored account information and stop the proxy,
- // if the account is no longer available.
- if (
- (this.#entitlement && state === IPProtectionStates.UNAVAILABLE) ||
- state === IPProtectionStates.UNAUTHENTICATED
- ) {
- this.resetAccount();
- }
-
- // Add the IPProtection widget if needed.
- if (
- !lazy.IPProtection.isInitialized &&
- state !== IPProtectionStates.UNINITIALIZED &&
- state !== IPProtectionStates.UNAVAILABLE
- ) {
- lazy.IPProtection.init();
- }
-
this.dispatchEvent(
new CustomEvent("IPProtectionService:StateChanged", {
bubbles: true,
diff --git a/browser/components/ipprotection/moz.build b/browser/components/ipprotection/moz.build
@@ -15,6 +15,7 @@ EXTRA_JS_MODULES.ipprotection += [
"IPPNetworkErrorObserver.sys.mjs",
"IPPProxyManager.sys.mjs",
"IPProtection.sys.mjs",
+ "IPProtectionHelpers.sys.mjs",
"IPProtectionPanel.sys.mjs",
"IPProtectionServerlist.sys.mjs",
"IPProtectionService.sys.mjs",