From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- browser/components/shopping/ShoppingUtils.sys.mjs | 308 ++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 browser/components/shopping/ShoppingUtils.sys.mjs (limited to 'browser/components/shopping/ShoppingUtils.sys.mjs') diff --git a/browser/components/shopping/ShoppingUtils.sys.mjs b/browser/components/shopping/ShoppingUtils.sys.mjs new file mode 100644 index 0000000000..bc61e5ce10 --- /dev/null +++ b/browser/components/shopping/ShoppingUtils.sys.mjs @@ -0,0 +1,308 @@ +/* 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, { + ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", + isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +const OPTED_IN_PREF = "browser.shopping.experience2023.optedIn"; +const ACTIVE_PREF = "browser.shopping.experience2023.active"; +const LAST_AUTO_ACTIVATE_PREF = + "browser.shopping.experience2023.lastAutoActivate"; +const AUTO_ACTIVATE_COUNT_PREF = + "browser.shopping.experience2023.autoActivateCount"; +const ADS_USER_ENABLED_PREF = "browser.shopping.experience2023.ads.userEnabled"; +const AUTO_OPEN_ENABLED_PREF = + "browser.shopping.experience2023.autoOpen.enabled"; +const AUTO_OPEN_USER_ENABLED_PREF = + "browser.shopping.experience2023.autoOpen.userEnabled"; +const SIDEBAR_CLOSED_COUNT_PREF = + "browser.shopping.experience2023.sidebarClosedCount"; + +const CFR_FEATURES_PREF = + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"; + +export const ShoppingUtils = { + initialized: false, + registered: false, + handledAutoActivate: false, + nimbusEnabled: false, + nimbusControl: false, + + _updateNimbusVariables() { + this.nimbusEnabled = + lazy.NimbusFeatures.shopping2023.getVariable("enabled"); + this.nimbusControl = + lazy.NimbusFeatures.shopping2023.getVariable("control"); + }, + + onNimbusUpdate() { + this._updateNimbusVariables(); + if (this.nimbusEnabled) { + ShoppingUtils.init(); + Glean.shoppingSettings.nimbusDisabledShopping.set(false); + } else { + ShoppingUtils.uninit(); + Glean.shoppingSettings.nimbusDisabledShopping.set(true); + } + }, + + // Runs once per session: + // * at application startup, with startup idle tasks, + // * or after the user is enrolled in the Nimbus experiment. + init() { + if (this.initialized) { + return; + } + this.onNimbusUpdate = this.onNimbusUpdate.bind(this); + this.onActiveUpdate = this.onActiveUpdate.bind(this); + + if (!this.registered) { + // Note (bug 1855545): we must set `this.registered` before calling + // `onUpdate`, as it will immediately invoke `this.onNimbusUpdate`, + // which in turn calls `ShoppingUtils.init`, creating an infinite loop. + this.registered = true; + lazy.NimbusFeatures.shopping2023.onUpdate(this.onNimbusUpdate); + this._updateNimbusVariables(); + } + + if (!this.nimbusEnabled) { + return; + } + + // Do startup-time stuff here, like recording startup-time glean events + // or adjusting onboarding-related prefs once per session. + + this.setOnUpdate(undefined, undefined, this.optedIn); + this.recordUserAdsPreference(); + this.recordUserAutoOpenPreference(); + + if (this._isAutoOpenEligible()) { + Services.prefs.setBoolPref(ACTIVE_PREF, true); + } + Services.prefs.addObserver(ACTIVE_PREF, this.onActiveUpdate); + + Services.prefs.setIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0); + + this.initialized = true; + }, + + // Runs once per session: + // * when the user is unenrolled from the Nimbus experiment, + // * or at shutdown, after quit-application-granted. + uninit() { + if (!this.initialized) { + return; + } + + // Do shutdown-time stuff here, like firing glean pings or modifying any + // prefs for onboarding. + + Services.prefs.removeObserver(ACTIVE_PREF, this.onActiveUpdate); + + this.initialized = false; + }, + + isProductPageNavigation(aLocationURI, aFlags) { + if (!lazy.isProductURL(aLocationURI)) { + return false; + } + + // Ignore same-document navigation, except in the case of Walmart + // as they use pushState to navigate between pages. + let isWalmart = aLocationURI.host.includes("walmart"); + let isNewDocument = !aFlags; + + let isSameDocument = + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT; + let isReload = aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD; + let isSessionRestore = + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SESSION_STORE; + + // Unfortunately, Walmart sometimes double-fires history manipulation + // events when navigating between product pages. To dedupe, cache the + // last visited Walmart URL just for a few milliseconds, so we can avoid + // double-counting such navigations. + if (isWalmart) { + if ( + this.lastWalmartURI && + aLocationURI.equalsExceptRef(this.lastWalmartURI) + ) { + return false; + } + this.lastWalmartURI = aLocationURI; + lazy.setTimeout(() => { + this.lastWalmartURI = null; + }, 100); + } + + return ( + // On initial visit to a product page, even from another domain, both a page + // load and a pushState will be triggered by Walmart, so this will + // capture only a single displayed event. + (!isWalmart && (isNewDocument || isReload || isSessionRestore)) || + (isWalmart && isSameDocument) + ); + }, + + // For users in either the nimbus control or treatment groups, increment a + // counter when they visit supported product pages. + recordExposure() { + if (this.nimbusEnabled || this.nimbusControl) { + Glean.shopping.productPageVisits.add(1); + } + }, + + setOnUpdate(_pref, _prev, current) { + Glean.shoppingSettings.componentOptedOut.set(current === 2); + Glean.shoppingSettings.hasOnboarded.set(current > 0); + }, + + recordUserAdsPreference() { + Glean.shoppingSettings.disabledAds.set(!ShoppingUtils.adsUserEnabled); + }, + + recordUserAutoOpenPreference() { + Glean.shoppingSettings.autoOpenUserDisabled.set( + !ShoppingUtils.autoOpenUserEnabled + ); + }, + + /** + * If the user has not opted in, automatically set the sidebar to `active` if: + * 1. The sidebar has not already been automatically set to `active` twice. + * 2. It's been at least 24 hours since the user last saw the sidebar because + * of this auto-activation behavior. + * 3. This method has not already been called (handledAutoActivate is false) + */ + handleAutoActivateOnProduct() { + if (!this.handledAutoActivate && !this.optedIn && this.cfrFeatures) { + let autoActivateCount = Services.prefs.getIntPref( + AUTO_ACTIVATE_COUNT_PREF, + 0 + ); + let lastAutoActivate = Services.prefs.getIntPref( + LAST_AUTO_ACTIVATE_PREF, + 0 + ); + let now = Date.now() / 1000; + // If we automatically set `active` to true in a previous session less + // than 24 hours ago, set it to false now. This is done to prevent the + // auto-activation state from persisting between sessions. Effectively, + // the auto-activation will persist until either 1) the sidebar is closed, + // or 2) Firefox restarts. + if (now - lastAutoActivate < 24 * 60 * 60) { + Services.prefs.setBoolPref(ACTIVE_PREF, false); + } + // Set active to true if we haven't done so recently nor more than twice. + else if (autoActivateCount < 2) { + Services.prefs.setBoolPref(ACTIVE_PREF, true); + Services.prefs.setIntPref( + AUTO_ACTIVATE_COUNT_PREF, + autoActivateCount + 1 + ); + Services.prefs.setIntPref(LAST_AUTO_ACTIVATE_PREF, now); + } + } + this.handledAutoActivate = true; + }, + + /** + * Send a Shopping-related trigger message to ASRouter. + * + * @param {object} trigger The trigger object to send to ASRouter. + * @param {object} trigger.context Additional trigger properties to pass to + * the targeting context. + * @param {string} trigger.id The id of the trigger. + * @param {MozBrowser} trigger.browser The browser to associate with the + * trigger. (This can determine the tab/window the message is shown in, + * depending on the message surface) + */ + async sendTrigger(trigger) { + await lazy.ASRouter.waitForInitialized; + await lazy.ASRouter.sendTriggerMessage(trigger); + }, + + onActiveUpdate(subject, topic, data) { + if (data !== ACTIVE_PREF || topic !== "nsPref:changed") { + return; + } + + let newValue = Services.prefs.getBoolPref(ACTIVE_PREF); + if (newValue === false) { + ShoppingUtils.resetActiveOnNextProductPage = true; + } + }, + + _isAutoOpenEligible() { + return ( + this.optedIn === 1 && this.autoOpenEnabled && this.autoOpenUserEnabled + ); + }, + + onLocationChange(aLocationURI, aFlags) { + let isProductPageNavigation = this.isProductPageNavigation( + aLocationURI, + aFlags + ); + + if (isProductPageNavigation) { + this.recordExposure(aLocationURI, aFlags); + } + + if ( + this._isAutoOpenEligible() && + this.resetActiveOnNextProductPage && + isProductPageNavigation + ) { + this.resetActiveOnNextProductPage = false; + Services.prefs.setBoolPref(ACTIVE_PREF, true); + } + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + ShoppingUtils, + "optedIn", + OPTED_IN_PREF, + 0, + ShoppingUtils.setOnUpdate +); + +XPCOMUtils.defineLazyPreferenceGetter( + ShoppingUtils, + "cfrFeatures", + CFR_FEATURES_PREF, + true +); + +XPCOMUtils.defineLazyPreferenceGetter( + ShoppingUtils, + "adsUserEnabled", + ADS_USER_ENABLED_PREF, + false, + ShoppingUtils.recordUserAdsPreference +); + +XPCOMUtils.defineLazyPreferenceGetter( + ShoppingUtils, + "autoOpenEnabled", + AUTO_OPEN_ENABLED_PREF, + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + ShoppingUtils, + "autoOpenUserEnabled", + AUTO_OPEN_USER_ENABLED_PREF, + false, + ShoppingUtils.recordUserAutoOpenPreference +); -- cgit v1.2.3