summaryrefslogtreecommitdiffstats
path: root/browser/components/shopping/ShoppingUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/shopping/ShoppingUtils.sys.mjs')
-rw-r--r--browser/components/shopping/ShoppingUtils.sys.mjs308
1 files changed, 308 insertions, 0 deletions
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
+);