/* 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, { IPPProxyManager: "moz-src:///toolkit/components/ipprotection/IPPProxyManager.sys.mjs", IPPStartupCache: "moz-src:///toolkit/components/ipprotection/IPPStartupCache.sys.mjs", IPProtectionService: "moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs", }); /** * Manages enrollment and entitlement for the IP Protection proxy service. * Delegates enrollment and entitlement fetching to the active auth provider. */ class IPPEnrollAndEntitleManagerSingleton extends EventTarget { #entitlement = null; #signInWatcher = null; // Promises to queue enrolling and entitling operations. #enrollingPromise = null; #entitlementPromise = null; constructor() { super(); this.handleEvent = this.#handleEvent.bind(this); } get entitlement() { return this.#entitlement; } init() { // We will use data from the cache until we are fully functional. Then we // will recompute the state in `initOnStartupCompleted`. this.#entitlement = lazy.IPPStartupCache.entitlement; // Duck typing: signInWatcher is not part of the base IPPAuthProvider contract. // This manager requires the active auth provider to be an IPPFxaAuthProvider. this.#signInWatcher = lazy.IPProtectionService.authProvider.signInWatcher; if (!this.#signInWatcher) { throw new Error( "IPPEnrollAndEntitleManager requires an auth provider with a signInWatcher" ); } this.#signInWatcher.addEventListener( "IPPSignInWatcher:StateChanged", this.handleEvent ); } initOnStartupCompleted() { if (!this.#signInWatcher.isSignedIn) { return; } // This bit must be async because we want to trigger the updateState at // the end of the rest of the initialization. this.updateEntitlement(); } uninit() { this.#signInWatcher.removeEventListener( "IPPSignInWatcher:StateChanged", this.handleEvent ); this.#signInWatcher = null; this.#entitlement = null; } #handleEvent(_event) { if (!this.#signInWatcher.isSignedIn) { this.#setEntitlement(null); return; } this.updateEntitlement(); } /** * Updates the entitlement status. * This will run only one fetch at a time, and queue behind any ongoing enrollment. * * @param {boolean} forceRefetch - If true, will refetch the entitlement even when one is present. * @returns {Promise} status * @returns {boolean} status.isEntitled - True if the user is entitled. * @returns {string} [status.error] - Error message if entitlement fetch failed. */ async updateEntitlement(forceRefetch = false) { if (this.#entitlementPromise) { return this.#entitlementPromise; } // Queue behind any ongoing enrollment. if (this.#enrollingPromise) { await this.#enrollingPromise; } let deferred = Promise.withResolvers(); this.#entitlementPromise = deferred.promise; // Notify listeners that an entitlement check has started so they can // react to isCheckingEntitlement becoming true. this.dispatchEvent( new CustomEvent("IPPEnrollAndEntitleManager:StateChanged", { bubbles: true, composed: true, }) ); const entitled = await this.#entitle(forceRefetch); deferred.resolve(entitled); if (entitled?.isEntitled) { lazy.IPPProxyManager.refreshUsage(); } this.#entitlementPromise = null; // Notify listeners that the entitlement check has completed so they can // react to isCheckingEntitlement becoming false. this.dispatchEvent( new CustomEvent("IPPEnrollAndEntitleManager:StateChanged", { bubbles: true, composed: true, }) ); return entitled; } /** * Enrolls and entitles the current Firefox account when possible. * This is a long-running request that will set isEnrolling while in progress * and will only run once until it completes. * * @param {AbortSignal} [abortSignal=null] - a signal to indicate the process should be aborted * @returns {Promise} result * @returns {boolean} result.isEnrolledAndEntitled - True if the user is enrolled and entitled. * @returns {string} [result.error] - Error message if enrollment or entitlement failed. */ async maybeEnrollAndEntitle(abortSignal = null) { if (this.#enrollingPromise) { return this.#enrollingPromise; } let deferred = Promise.withResolvers(); this.#enrollingPromise = deferred.promise; const enrolledAndEntitled = await this.#enrollAndEntitle(abortSignal); deferred.resolve(enrolledAndEntitled); this.#enrollingPromise = null; // By the time enrollingPromise is unset, notify listeners so that they // can react to isEnrolling becoming false. this.dispatchEvent( new CustomEvent("IPPEnrollAndEntitleManager:StateChanged", { bubbles: true, composed: true, }) ); return enrolledAndEntitled; } /** * Enroll and entitle the current Firefox account. * * @param {AbortSignal} abortSignal - a signal to abort the enrollment * @returns {Promise} status * @returns {boolean} status.isEnrolledAndEntitled - True if the user is enrolled and entitled. * @returns {string} [status.error] - Error message if enrollment or entitlement failed. */ async #enrollAndEntitle(abortSignal = null) { if (this.#entitlement) { return { isEnrolledAndEntitled: true }; } // Duck typing: enrollAndEntitle() is not part of the base IPPAuthProvider // contract. This manager requires a provider that implements it. const { isEnrolledAndEntitled, entitlement, error } = await lazy.IPProtectionService.authProvider.enrollAndEntitle(abortSignal); if (!isEnrolledAndEntitled) { this.#setEntitlement(null); return { isEnrolledAndEntitled: false, error }; } this.#setEntitlement(entitlement ?? null); return { isEnrolledAndEntitled: true }; } /** * Fetch and update the entitlement. * * @param {boolean} forceRefetch - If true, will refetch the entitlement even when one is present. * @returns {Promise} status * @returns {boolean} status.isEntitled - True if the user is entitled. * @returns {string} [status.error] - Error message if entitlement fetch failed. */ async #entitle(forceRefetch = false) { if (this.#entitlement && !forceRefetch) { return { isEntitled: true }; } // Duck typing: getEntitlement() is not part of the base IPPAuthProvider // contract. This manager requires a provider that implements it. const { entitlement, error } = await lazy.IPProtectionService.authProvider.getEntitlement(forceRefetch); if (error || !entitlement) { this.#setEntitlement(null); return { isEntitled: false, error }; } this.#setEntitlement(entitlement); return { isEntitled: true }; } /** * Sets the entitlement and updates the cache and IPProtectionService state. * * @param {object | null} entitlement - The entitlement object or null to unset. */ #setEntitlement(entitlement) { this.#entitlement = entitlement; lazy.IPPStartupCache.storeEntitlement(this.#entitlement); if (!this.#signInWatcher) { return; } lazy.IPProtectionService.updateState(); this.dispatchEvent( new CustomEvent("IPPEnrollAndEntitleManager:StateChanged", { bubbles: true, composed: true, }) ); } /** * Checks if we have the entitlement */ get isEnrolledAndEntitled() { return !!this.#entitlement; } /** * Checks if a user has upgraded. * * @returns {boolean} */ get hasUpgraded() { return this.#entitlement?.subscribed; } /** * Checks if the entitlement exists and it contains a UUID */ get hasEntitlementUid() { return !!this.#entitlement?.uid; } /** * Checks if we are currently enrolling. */ get isEnrolling() { return !!this.#enrollingPromise; } /** * Checks if we are currently checking entitlement. */ get isCheckingEntitlement() { return !!this.#entitlementPromise; } /** * Waits for the current enrollment to complete, if any. */ async waitForEnrollment() { return this.#enrollingPromise; } /** * Refetches the entitlement even if it is cached. */ async refetchEntitlement() { await this.updateEntitlement(true); } /** * Unsets any stored entitlement. */ resetEntitlement() { this.#setEntitlement(null); } } const IPPEnrollAndEntitleManager = new IPPEnrollAndEntitleManagerSingleton(); export { IPPEnrollAndEntitleManager };