summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutwelcome/modules
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutwelcome/modules')
-rw-r--r--browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs74
-rw-r--r--browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs902
-rw-r--r--browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs276
3 files changed, 1252 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs b/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs
new file mode 100644
index 0000000000..715ab650a4
--- /dev/null
+++ b/browser/components/aboutwelcome/modules/AWScreenUtils.sys.mjs
@@ -0,0 +1,74 @@
+/* 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, {
+ ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
+ ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
+});
+
+export const AWScreenUtils = {
+ /**
+ * Filter the given screens in place with a predicate.
+ *
+ * @param {object[]} screens - The screens to filter.
+ * @param {Function} callback - The predicate for filtering the screens.
+ */
+ async removeScreens(screens, callback) {
+ for (let i = 0; i < screens?.length; i++) {
+ if (await callback(screens[i], i)) {
+ screens.splice(i--, 1);
+ }
+ }
+ },
+ /**
+ * Given a JEXL expression, returns the evaluation of the expression or returns
+ * true if the expression did not evaluate successfully
+ *
+ * @param {string} targeting - The JEXL expression that will be evaluated
+ * @returns {boolean}
+ */
+ async evaluateScreenTargeting(targeting) {
+ const result = await lazy.ASRouter.evaluateExpression({
+ expression: targeting,
+ context: lazy.ASRouterTargeting.Environment,
+ });
+ if (result?.evaluationStatus?.success) {
+ return result.evaluationStatus.result;
+ }
+
+ return true;
+ },
+ /**
+ * Filter out screens whose targeting do not match.
+ *
+ * Given an array of screens, each screen will have it's `targeting` property
+ * evaluated, and removed if it's targeting evaluates to false
+ *
+ * @param {object[]} screens - An array of screens that will be looped
+ * through to be evaluated for removal
+ * @returns {object[]} - A new array containing the screens that were not removed
+ */
+ async evaluateTargetingAndRemoveScreens(screens) {
+ const filteredScreens = [...screens];
+ await this.removeScreens(filteredScreens, async screen => {
+ if (screen.targeting === undefined) {
+ // Don't remove the screen if we don't have a targeting property
+ return false;
+ }
+
+ const result = await this.evaluateScreenTargeting(screen.targeting);
+ // Flipping the value because a true evaluation means we
+ // don't want to remove the screen, while false means we do
+ return !result;
+ });
+
+ return filteredScreens;
+ },
+
+ async addScreenImpression(screen) {
+ await lazy.ASRouter.addScreenImpression(screen);
+ },
+};
diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs
new file mode 100644
index 0000000000..828839c8d0
--- /dev/null
+++ b/browser/components/aboutwelcome/modules/AboutWelcomeDefaults.sys.mjs
@@ -0,0 +1,902 @@
+/* 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/. */
+
+// We use importESModule here instead of static import so that
+// the Karma test environment won't choke on this module. This
+// is because the Karma test environment already stubs out
+// AppConstants, and overrides importESModule to be a no-op (which
+// can't be done for a static import statement).
+
+// eslint-disable-next-line mozilla/use-static-import
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+ AWScreenUtils: "resource:///modules/aboutwelcome/AWScreenUtils.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+});
+
+// Message to be updated based on finalized MR designs
+const MR_ABOUT_WELCOME_DEFAULT = {
+ id: "MR_WELCOME_DEFAULT",
+ template: "multistage",
+ // Allow tests to easily disable transitions.
+ transitions: Services.prefs.getBoolPref(
+ "browser.aboutwelcome.transitions",
+ true
+ ),
+ backdrop:
+ "var(--mr-welcome-background-color) var(--mr-welcome-background-gradient)",
+ screens: [
+ {
+ id: "AW_WELCOME_BACK",
+ targeting: "isDeviceMigration",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-100px",
+ image_alt_text: {
+ string_id: "onboarding-device-migration-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/device-migration.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-device-migration-title",
+ },
+ subtitle: {
+ string_id: "onboarding-device-migration-subtitle2",
+ },
+ primary_button: {
+ label: {
+ string_id: "onboarding-device-migration-primary-button-label",
+ },
+ action: {
+ type: "FXA_SIGNIN_FLOW",
+ navigate: "actionResult",
+ data: {
+ entrypoint: "fx-device-migration-onboarding",
+ extraParams: {
+ utm_content: "migration-onboarding",
+ utm_source: "fx-new-device-sync",
+ utm_medium: "firefox-desktop",
+ utm_campaign: "migration",
+ },
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN",
+ targeting:
+ "doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-default-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ hide_secondary_section: "responsive",
+ logo: {},
+ title: {
+ string_id: "onboarding-easy-setup-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-easy-setup-security-and-privacy-subtitle",
+ },
+ tiles: {
+ type: "multiselect",
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-pin-primary-button-label",
+ },
+ action: {
+ type: "PIN_FIREFOX_TO_TASKBAR",
+ },
+ },
+ {
+ id: "checkbox-2",
+ defaultValue: true,
+ label: {
+ string_id:
+ "mr2022-onboarding-easy-setup-set-default-checkbox-label",
+ },
+ action: {
+ type: "SET_DEFAULT_BROWSER",
+ },
+ },
+ {
+ id: "checkbox-3",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-import-checkbox-label",
+ },
+ uncheckedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ },
+ },
+ },
+ ],
+ },
+ },
+ checkedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ value: true,
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-primary-button-label",
+ },
+ action: {
+ type: "MULTI_ACTION",
+ collectSelect: true,
+ navigate: true,
+ data: {
+ actions: [],
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ secondary_button_top: {
+ label: {
+ string_id: "mr1-onboarding-sign-in-button-label",
+ },
+ action: {
+ data: {
+ entrypoint: "activity-stream-firstrun",
+ where: "tab",
+ },
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ addFlowParams: true,
+ },
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_NEEDS_DEFAULT",
+ targeting:
+ "!doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-default-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-easy-setup-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-easy-setup-security-and-privacy-subtitle",
+ },
+ tiles: {
+ type: "multiselect",
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: {
+ string_id:
+ "mr2022-onboarding-easy-setup-set-default-checkbox-label",
+ },
+ action: {
+ type: "SET_DEFAULT_BROWSER",
+ },
+ },
+ {
+ id: "checkbox-2",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-import-checkbox-label",
+ },
+ uncheckedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ },
+ },
+ },
+ ],
+ },
+ },
+ checkedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ value: true,
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-primary-button-label",
+ },
+ action: {
+ type: "MULTI_ACTION",
+ collectSelect: true,
+ navigate: true,
+ data: {
+ actions: [],
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ secondary_button_top: {
+ label: {
+ string_id: "mr1-onboarding-sign-in-button-label",
+ },
+ action: {
+ data: {
+ entrypoint: "activity-stream-firstrun",
+ where: "tab",
+ },
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ addFlowParams: true,
+ },
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_NEEDS_PIN",
+ targeting:
+ "doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-default-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-easy-setup-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-easy-setup-security-and-privacy-subtitle",
+ },
+ tiles: {
+ type: "multiselect",
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-pin-primary-button-label",
+ },
+ action: {
+ type: "PIN_FIREFOX_TO_TASKBAR",
+ },
+ },
+ {
+ id: "checkbox-2",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-import-checkbox-label",
+ },
+ uncheckedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ },
+ },
+ },
+ ],
+ },
+ },
+ checkedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ value: true,
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-primary-button-label",
+ },
+ action: {
+ type: "MULTI_ACTION",
+ collectSelect: true,
+ navigate: true,
+ data: {
+ actions: [],
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ secondary_button_top: {
+ label: {
+ string_id: "mr1-onboarding-sign-in-button-label",
+ },
+ action: {
+ data: {
+ entrypoint: "activity-stream-firstrun",
+ where: "tab",
+ },
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ addFlowParams: true,
+ },
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_ONLY_IMPORT",
+ targeting:
+ "!doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-default-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-easy-setup-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-easy-setup-security-and-privacy-subtitle",
+ },
+ tiles: {
+ type: "multiselect",
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-import-checkbox-label",
+ },
+ uncheckedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ },
+ },
+ },
+ ],
+ },
+ },
+ checkedAction: {
+ type: "MULTI_ACTION",
+ data: {
+ actions: [
+ {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: "showEmbeddedImport",
+ value: true,
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-easy-setup-primary-button-label",
+ },
+ action: {
+ type: "MULTI_ACTION",
+ collectSelect: true,
+ navigate: true,
+ data: {
+ actions: [],
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ secondary_button_top: {
+ label: {
+ string_id: "mr1-onboarding-sign-in-button-label",
+ },
+ action: {
+ data: {
+ entrypoint: "activity-stream-firstrun",
+ where: "tab",
+ },
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ addFlowParams: true,
+ },
+ },
+ },
+ },
+ {
+ id: "AW_LANGUAGE_MISMATCH",
+ content: {
+ position: "split",
+ background: "var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "mr2022-onboarding-live-language-text",
+ },
+ subtitle: {
+ string_id: "mr2022-language-mismatch-subtitle",
+ },
+ hero_text: {
+ string_id: "mr2022-onboarding-live-language-text",
+ useLangPack: true,
+ },
+ languageSwitcher: {
+ downloading: {
+ string_id: "onboarding-live-language-button-label-downloading",
+ },
+ cancel: {
+ string_id: "onboarding-live-language-secondary-cancel-download",
+ },
+ waiting: { string_id: "onboarding-live-language-waiting-button" },
+ skip: { string_id: "mr2022-onboarding-secondary-skip-button-label" },
+ action: {
+ navigate: true,
+ },
+ switch: {
+ string_id: "mr2022-onboarding-live-language-switch-to",
+ useLangPack: true,
+ },
+ continue: {
+ string_id: "mr2022-onboarding-live-language-continue-in",
+ },
+ },
+ },
+ },
+ {
+ id: "AW_IMPORT_SETTINGS_EMBEDDED",
+ targeting: `("messaging-system-action.showEmbeddedImport" |preferenceValue == true) && useEmbeddedMigrationWizard`,
+ content: {
+ tiles: { type: "migration-wizard" },
+ position: "split",
+ split_narrow_bkg_position: "-42px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-import-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-import.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ hide_secondary_section: "responsive",
+ migrate_start: {
+ action: {},
+ },
+ migrate_close: {
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_MOBILE_DOWNLOAD",
+ // The mobile download screen should only be shown to users who
+ // are either not logged into FxA, or don't have any mobile devices syncing
+ targeting: "!isFxASignedIn || sync.mobileDevices == 0",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-160px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-mobile-download-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-mobilecrosspromo.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-mobile-download-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-mobile-download-security-and-privacy-subtitle",
+ },
+ hero_image: {
+ url: "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user.svg",
+ },
+ cta_paragraph: {
+ text: {
+ string_id: "mr2022-onboarding-mobile-download-cta-text",
+ string_name: "download-label",
+ },
+ action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://www.mozilla.org/firefox/mobile/get-app/?utm_medium=firefox-desktop&utm_source=onboarding-modal&utm_campaign=mr2022&utm_content=new-global",
+ where: "tab",
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_AMO_INTRODUCE",
+ // Show to en-* locales only
+ targeting: "localeLanguageCode == 'en'",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-58px",
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-amo-collection.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: { string_id: "amo-screen-title" },
+ subtitle: { string_id: "amo-screen-subtitle" },
+ primary_button: {
+ label: { string_id: "amo-screen-primary-cta" },
+ action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://addons.mozilla.org/en-US/firefox/collections/4757633/25c2b44583534b3fa8fea977c419cd/?page=1&collection_sort=-added",
+ where: "tabshifted",
+ },
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ {
+ id: "AW_GRATITUDE",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-228px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-gratitude-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-gratitude.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "onboarding-gratitude-security-and-privacy-title",
+ },
+ subtitle: {
+ string_id: "onboarding-gratitude-security-and-privacy-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2-onboarding-start-browsing-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+};
+
+async function getAddonFromRepository(data) {
+ const [addonInfo] = await lazy.AddonRepository.getAddonsByIDs([data]);
+ if (addonInfo.sourceURI.scheme !== "https") {
+ return null;
+ }
+
+ return {
+ name: addonInfo.name,
+ url: addonInfo.sourceURI.spec,
+ iconURL: addonInfo.icons["64"] || addonInfo.icons["32"],
+ type: addonInfo.type,
+ screenshots: addonInfo.screenshots,
+ };
+}
+
+async function getAddonInfo(attrbObj) {
+ let { content, source } = attrbObj;
+ try {
+ if (!content || source !== "addons.mozilla.org") {
+ return null;
+ }
+ // Attribution data can be double encoded
+ while (content.includes("%")) {
+ try {
+ const result = decodeURIComponent(content);
+ if (result === content) {
+ break;
+ }
+ content = result;
+ } catch (e) {
+ break;
+ }
+ }
+ // return_to_amo embeds the addon id in the content
+ // param, prefixed with "rta:". Translating that
+ // happens in AddonRepository, however we can avoid
+ // an API call if we check up front here.
+ if (content.startsWith("rta:")) {
+ return await getAddonFromRepository(content);
+ }
+ } catch (e) {
+ console.error("Failed to get the latest add-on version for Return to AMO");
+ }
+ return null;
+}
+
+async function getAttributionContent() {
+ let attribution = await lazy.AttributionCode.getAttrDataAsync();
+ if (attribution?.source === "addons.mozilla.org") {
+ let addonInfo = await getAddonInfo(attribution);
+ if (addonInfo) {
+ return {
+ ...addonInfo,
+ template: "return_to_amo",
+ };
+ }
+ }
+ if (attribution?.ua) {
+ return {
+ ua: decodeURIComponent(attribution.ua),
+ };
+ }
+ return null;
+}
+
+// Return default multistage welcome content
+function getDefaults() {
+ return Cu.cloneInto(MR_ABOUT_WELCOME_DEFAULT, {});
+}
+
+let gSourceL10n = null;
+
+// Localize Firefox download source from user agent attribution to show inside
+// import primary button label such as 'Import from <localized browser name>'.
+// no firefox as import wizard doesn't show it
+const allowedUAs = ["chrome", "edge", "ie"];
+function getLocalizedUA(ua) {
+ if (!gSourceL10n) {
+ gSourceL10n = new Localization(["browser/migrationWizard.ftl"]);
+ }
+ if (allowedUAs.includes(ua)) {
+ return gSourceL10n.formatValue(`migration-source-name-${ua.toLowerCase()}`);
+ }
+ return null;
+}
+
+function prepareMobileDownload(content) {
+ let mobileContent = content?.screens?.find(
+ screen => screen.id === "AW_MOBILE_DOWNLOAD"
+ )?.content;
+
+ if (!mobileContent) {
+ return content;
+ }
+ if (!lazy.BrowserUtils.sendToDeviceEmailsSupported()) {
+ // If send to device emails are not supported for a user's locale,
+ // remove the send to device link and update the screen text
+ delete mobileContent.cta_paragraph.action;
+ mobileContent.cta_paragraph.text = {
+ string_id: "mr2022-onboarding-no-mobile-download-cta-text",
+ };
+ }
+ // Update CN specific QRCode url
+ if (AppConstants.isChinaRepack()) {
+ mobileContent.hero_image.url = `${mobileContent.hero_image.url.slice(
+ 0,
+ mobileContent.hero_image.url.indexOf(".svg")
+ )}-cn.svg`;
+ }
+
+ return content;
+}
+
+async function prepareContentForReact(content) {
+ const { screens } = content;
+
+ if (content?.template === "return_to_amo") {
+ return content;
+ }
+
+ // Set the primary import button source based on attribution.
+ if (content?.ua) {
+ // If available, add the browser source to action data
+ // and localized browser string args to primary button label
+ const { label, action } =
+ content?.screens?.find(
+ screen =>
+ screen?.content?.primary_button?.action?.type ===
+ "SHOW_MIGRATION_WIZARD"
+ )?.content?.primary_button ?? {};
+
+ if (action) {
+ action.data = { ...action.data, source: content.ua };
+ }
+
+ let browserStr = await getLocalizedUA(content.ua);
+
+ if (label?.string_id) {
+ label.string_id = browserStr
+ ? "mr1-onboarding-import-primary-button-label-attribution"
+ : "mr2022-onboarding-import-primary-button-label-no-attribution";
+
+ label.args = browserStr ? { previous: browserStr } : {};
+ }
+ }
+
+ // Remove Firefox Accounts related UI and prevent related metrics.
+ if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled", false)) {
+ delete content.screens?.find(
+ screen =>
+ screen.content?.secondary_button_top?.action?.type ===
+ "SHOW_FIREFOX_ACCOUNTS"
+ )?.content.secondary_button_top;
+ content.skipFxA = true;
+ }
+
+ let shouldRemoveLanguageMismatchScreen = true;
+ if (content.languageMismatchEnabled) {
+ const screen = content?.screens?.find(s => s.id === "AW_LANGUAGE_MISMATCH");
+ if (screen && content.appAndSystemLocaleInfo.canLiveReload) {
+ // Add the display names for the OS and Firefox languages, like "American English".
+ function addMessageArgs(obj) {
+ for (const value of Object.values(obj)) {
+ if (value?.string_id) {
+ value.args = content.appAndSystemLocaleInfo.displayNames;
+ }
+ }
+ }
+
+ addMessageArgs(screen.content.languageSwitcher);
+ addMessageArgs(screen.content);
+ shouldRemoveLanguageMismatchScreen = false;
+ }
+ }
+
+ if (shouldRemoveLanguageMismatchScreen) {
+ await lazy.AWScreenUtils.removeScreens(
+ screens,
+ screen => screen.id === "AW_LANGUAGE_MISMATCH"
+ );
+ }
+
+ return prepareMobileDownload(content);
+}
+
+export const AboutWelcomeDefaults = {
+ prepareContentForReact,
+ getDefaults,
+ getAttributionContent,
+};
diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
new file mode 100644
index 0000000000..1447d3ebde
--- /dev/null
+++ b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
@@ -0,0 +1,276 @@
+/* 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, {
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+ ClientID: "resource://gre/modules/ClientID.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () =>
+ lazy.ClientID.getClientID()
+);
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "browserSessionId",
+ () => lazy.TelemetrySession.getMetadata("").sessionId
+);
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ const { Logger } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/Logger.sys.mjs"
+ );
+ return new Logger("AboutWelcomeTelemetry");
+});
+
+export class AboutWelcomeTelemetry {
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "telemetryEnabled",
+ "browser.newtabpage.activity-stream.telemetry",
+ false
+ );
+ }
+
+ /**
+ * Attach browser attribution data to a ping payload.
+ *
+ * It intentionally queries the *cached* attribution data other than calling
+ * `getAttrDataAsync()` in order to minimize the overhead here.
+ * For the same reason, we are not querying the attribution data from
+ * `TelemetryEnvironment.currentEnvironment.settings`.
+ *
+ * In practice, it's very likely that the attribution data is already read
+ * and cached at some point by `AboutWelcomeParent`, so it should be able to
+ * read the cached results for the most if not all of the pings.
+ */
+ _maybeAttachAttribution(ping) {
+ const attribution = lazy.AttributionCode.getCachedAttributionData();
+ if (attribution && Object.keys(attribution).length) {
+ ping.attribution = attribution;
+ }
+ return ping;
+ }
+
+ async _createPing(event) {
+ if (event.event_context && typeof event.event_context === "object") {
+ event.event_context = JSON.stringify(event.event_context);
+ }
+ let ping = {
+ ...event,
+ addon_version: Services.appinfo.appBuildID,
+ locale: Services.locale.appLocaleAsBCP47,
+ client_id: await lazy.telemetryClientId,
+ browser_session_id: lazy.browserSessionId,
+ };
+
+ return this._maybeAttachAttribution(ping);
+ }
+
+ /**
+ * Augment the provided event with some metadata and then send it
+ * to the messaging-system's onboarding endpoint.
+ *
+ * Is sometimes used by non-onboarding events.
+ *
+ * @param event - an object almost certainly from an onboarding flow (though
+ * there is a case where spotlight may use this, too)
+ * containing a nested structure of data for reporting as
+ * telemetry, as documented in
+ * https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/data_events.html
+ * Does not have all of its data (`_createPing` will augment
+ * with ids and attribution if available).
+ */
+ async sendTelemetry(event) {
+ if (!this.telemetryEnabled) {
+ return;
+ }
+
+ const ping = await this._createPing(event);
+
+ try {
+ this.submitGleanPingForPing(ping);
+ } catch (e) {
+ // Though Glean APIs are forbidden to throw, it may be possible that a
+ // mismatch between the shape of `ping` and the defined metrics is not
+ // adequately handled.
+ Glean.messagingSystem.gleanPingForPingFailures.add(1);
+ }
+ }
+
+ /**
+ * Tries to infer appropriate Glean metrics on the "messaging-system" ping,
+ * sets them, and submits a "messaging-system" ping.
+ *
+ * Does not check if telemetry is enabled.
+ * (Though Glean will check the global prefs).
+ *
+ * Note: This is a very unusual use of Glean that is specific to the use-
+ * cases of Messaging System. Please do not copy this pattern.
+ */
+ submitGleanPingForPing(ping) {
+ lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`);
+ // event.event_context is an object, but it may have been stringified.
+ let event_context = ping?.event_context;
+ let shopping_callout_impression =
+ ping?.message_id?.startsWith("FAKESPOT_CALLOUT") &&
+ ping?.event === "IMPRESSION";
+
+ if (typeof event_context === "string") {
+ try {
+ event_context = JSON.parse(event_context);
+ // This code is for directing Shopping component based clicks into
+ // the Glean Events ping.
+ if (
+ event_context?.page === "about:shoppingsidebar" ||
+ shopping_callout_impression
+ ) {
+ this.handleShoppingPings(ping, event_context);
+ }
+ } catch (e) {
+ // The Empty JSON strings and non-objects often provided by the
+ // existing telemetry we need to send failing to parse do not fit in
+ // the spirit of what this error is meant to capture. Instead, we want
+ // to capture when what we got should have been an object,
+ // but failed to parse.
+ if (event_context.length && event_context.includes("{")) {
+ Glean.messagingSystem.eventContextParseError.add(1);
+ }
+ }
+ }
+
+ // We echo certain properties from event_context into their own metrics
+ // to aid analysis.
+ if (event_context?.reason) {
+ Glean.messagingSystem.eventReason.set(event_context.reason);
+ }
+ if (event_context?.page) {
+ Glean.messagingSystem.eventPage.set(event_context.page);
+ }
+ if (event_context?.source) {
+ Glean.messagingSystem.eventSource.set(event_context.source);
+ }
+ if (event_context?.screen_family) {
+ Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family);
+ }
+ // Screen_index was being coerced into a boolean value
+ // which resulted in 0 (first screen index) being ignored.
+ if (Number.isInteger(event_context?.screen_index)) {
+ Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index);
+ }
+ if (event_context?.screen_id) {
+ Glean.messagingSystem.eventScreenId.set(event_context.screen_id);
+ }
+ if (event_context?.screen_initials) {
+ Glean.messagingSystem.eventScreenInitials.set(
+ event_context.screen_initials
+ );
+ }
+
+ // The event_context is also provided as-is as stringified JSON.
+ if (event_context) {
+ Glean.messagingSystem.eventContext.set(JSON.stringify(event_context));
+ }
+
+ if ("attribution" in ping) {
+ for (const [key, value] of Object.entries(ping.attribution)) {
+ const camelKey = this._snakeToCamelCase(key);
+ try {
+ Glean.messagingSystemAttribution[camelKey].set(value);
+ } catch (e) {
+ // We here acknowledge that we don't know the full breadth of data
+ // being collected. Ideally AttributionCode will later centralize
+ // definition and reporting of attribution data and we can be rid of
+ // this fail-safe for collecting the names of unknown keys.
+ Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1);
+ }
+ }
+ }
+
+ // List of keys handled above.
+ const handledKeys = ["event_context", "attribution"];
+
+ for (const [key, value] of Object.entries(ping)) {
+ if (handledKeys.includes(key)) {
+ continue;
+ }
+ const camelKey = this._snakeToCamelCase(key);
+ try {
+ // We here acknowledge that even known keys might have non-scalar
+ // values. We're pretty sure we handled them all with handledKeys,
+ // but we might not have.
+ // Ideally this can later be removed after running for a version or two
+ // with no values seen in messaging_system.invalid_nested_data
+ if (typeof value === "object") {
+ Glean.messagingSystem.invalidNestedData[camelKey].add(1);
+ } else {
+ Glean.messagingSystem[camelKey].set(value);
+ }
+ } catch (e) {
+ // We here acknowledge that we don't know the full breadth of data being
+ // collected. Ideally we will later gain that confidence and can remove
+ // this fail-safe for collecting the names of unknown keys.
+ Glean.messagingSystem.unknownKeys[camelKey].add(1);
+ // TODO(bug 1600008): For testing, also record the overall count.
+ Glean.messagingSystem.unknownKeyCount.add(1);
+ }
+ }
+
+ // With all the metrics set, now it's time to submit this ping.
+ GleanPings.messagingSystem.submit();
+ }
+
+ _snakeToCamelCase(s) {
+ return s.toString().replace(/_([a-z])/gi, (_str, group) => {
+ return group.toUpperCase();
+ });
+ }
+
+ handleShoppingPings(ping, event_context) {
+ const message_id = ping?.message_id;
+ // This function helps direct a shopping ping to the correct Glean event.
+ if (message_id.startsWith("FAKESPOT_OPTIN_DEFAULT")) {
+ // Onboarding page message IDs are generated, but can reliably be
+ // assumed to start in this manner.
+ switch (ping?.event) {
+ case "CLICK_BUTTON":
+ switch (event_context?.source) {
+ case "privacy_policy":
+ Glean.shopping.surfaceShowPrivacyPolicyClicked.record();
+ break;
+ case "terms_of_use":
+ Glean.shopping.surfaceShowTermsClicked.record();
+ break;
+ case "primary_button":
+ // corresponds to 'Analyze Reviews'
+ Glean.shopping.surfaceOptInClicked.record();
+ break;
+ case "additional_button":
+ // corresponds to "Not Now"
+ Glean.shopping.surfaceNotNowClicked.record();
+ break;
+ case "learn_more":
+ Glean.shopping.surfaceLearnMoreClicked.record();
+ break;
+ }
+ break;
+ case "IMPRESSION":
+ Glean.shopping.surfaceOnboardingDisplayed.record({
+ configuration: ping?.message_id,
+ });
+ break;
+ }
+ }
+ if (message_id.startsWith("FAKESPOT_CALLOUT")) {
+ Glean.shopping.addressBarFeatureCalloutDisplayed.record({
+ configuration: message_id,
+ });
+ }
+ }
+}