summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm386
1 files changed, 386 insertions, 0 deletions
diff --git a/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm b/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
new file mode 100644
index 0000000000..459603c379
--- /dev/null
+++ b/browser/components/newtab/aboutwelcome/AboutWelcomeChild.jsm
@@ -0,0 +1,386 @@
+/* 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/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["AboutWelcomeChild"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ DEFAULT_SITES: "resource://activity-stream/lib/DefaultSites.jsm",
+ ExperimentAPI: "resource://messaging-system/experiments/ExperimentAPI.jsm",
+ shortURL: "resource://activity-stream/lib/ShortURL.jsm",
+ TippyTopProvider: "resource://activity-stream/lib/TippyTopProvider.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ const { Logger } = ChromeUtils.import(
+ "resource://messaging-system/lib/Logger.jsm"
+ );
+ return new Logger("AboutWelcomeChild");
+});
+
+XPCOMUtils.defineLazyGetter(this, "tippyTopProvider", () =>
+ (async () => {
+ const provider = new TippyTopProvider();
+ await provider.init();
+ return provider;
+ })()
+);
+
+function _parseOverrideContent(value) {
+ let result = {};
+ try {
+ result = value ? JSON.parse(value) : {};
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ return result;
+}
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "multiStageAboutWelcomeContent",
+ "browser.aboutwelcome.overrideContent",
+ "",
+ null,
+ _parseOverrideContent
+);
+
+const SEARCH_REGION_PREF = "browser.search.region";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "searchRegion",
+ SEARCH_REGION_PREF,
+ ""
+);
+
+/**
+ * Lazily get importable sites from parent or reuse cached ones.
+ */
+function getImportableSites(child) {
+ return (
+ getImportableSites.cache ??
+ (getImportableSites.cache = (async () => {
+ // Use tippy top to get packaged rich icons
+ const tippyTop = await tippyTopProvider;
+ // Remove duplicate entries if they would appear the same
+ return `[${[
+ ...new Set(
+ (await child.sendQuery("AWPage:IMPORTABLE_SITES")).map(url => {
+ // Get both rich icon and short name and save for deduping
+ const site = { url };
+ tippyTop.processSite(site, "*");
+ return JSON.stringify({
+ icon: site.tippyTopIcon,
+ label: shortURL(site),
+ });
+ })
+ ),
+ ]}]`;
+ })())
+ );
+}
+
+async function getDefaultSites(child) {
+ // Get default TopSites by region
+ let sites = DEFAULT_SITES.get(
+ DEFAULT_SITES.has(searchRegion) ? searchRegion : ""
+ );
+
+ // Use tippy top to get packaged rich icons
+ const tippyTop = await tippyTopProvider;
+ let defaultSites = sites.split(",").map(link => {
+ let site = { url: link };
+ tippyTop.processSite(site);
+ return {
+ icon: site.tippyTopIcon,
+ title: shortURL(site),
+ };
+ });
+ return Cu.cloneInto(defaultSites, child.contentWindow);
+}
+
+async function getSelectedTheme(child) {
+ let activeThemeId = await child.sendQuery("AWPage:GET_SELECTED_THEME");
+ return activeThemeId;
+}
+
+class AboutWelcomeChild extends JSWindowActorChild {
+ actorCreated() {
+ this.exportFunctions();
+ this.initWebProgressListener();
+ }
+
+ initWebProgressListener() {
+ const webProgress = this.manager.browsingContext.top.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+
+ const listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ listener.onLocationChange = (aWebProgress, aRequest, aLocation, aFlags) => {
+ // Exit if actor 'AboutWelcome' has already been destroyed or
+ // content window doesn't exist
+ if (!this.manager || !this.contentWindow) {
+ return;
+ }
+ log.debug(`onLocationChange handled: ${aWebProgress.DOMWindow}`);
+ this.AWSendToParent("LOCATION_CHANGED");
+ };
+
+ webProgress.addProgressListener(
+ listener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+ }
+
+ /**
+ * Send event that can be handled by the page
+ * @param {{type: string, data?: any}} action
+ */
+ sendToPage(action) {
+ log.debug(`Sending to page: ${action.type}`);
+ const win = this.document.defaultView;
+ const event = new win.CustomEvent("AboutWelcomeChromeToContent", {
+ detail: Cu.cloneInto(action, win),
+ });
+ win.dispatchEvent(event);
+ }
+
+ /**
+ * Export functions that can be called by page js
+ */
+ exportFunctions() {
+ let window = this.contentWindow;
+
+ Cu.exportFunction(this.AWGetExperimentData.bind(this), window, {
+ defineAs: "AWGetExperimentData",
+ });
+
+ Cu.exportFunction(this.AWGetAttributionData.bind(this), window, {
+ defineAs: "AWGetAttributionData",
+ });
+
+ // For local dev, checks for JSON content inside pref browser.aboutwelcome.overrideContent
+ // that is used to override default welcome UI
+ Cu.exportFunction(this.AWGetWelcomeOverrideContent.bind(this), window, {
+ defineAs: "AWGetWelcomeOverrideContent",
+ });
+
+ Cu.exportFunction(this.AWGetFxAMetricsFlowURI.bind(this), window, {
+ defineAs: "AWGetFxAMetricsFlowURI",
+ });
+
+ Cu.exportFunction(this.AWGetImportableSites.bind(this), window, {
+ defineAs: "AWGetImportableSites",
+ });
+
+ Cu.exportFunction(this.AWGetDefaultSites.bind(this), window, {
+ defineAs: "AWGetDefaultSites",
+ });
+
+ Cu.exportFunction(this.AWGetSelectedTheme.bind(this), window, {
+ defineAs: "AWGetSelectedTheme",
+ });
+
+ Cu.exportFunction(this.AWGetRegion.bind(this), window, {
+ defineAs: "AWGetRegion",
+ });
+
+ Cu.exportFunction(this.AWSelectTheme.bind(this), window, {
+ defineAs: "AWSelectTheme",
+ });
+
+ Cu.exportFunction(this.AWSendEventTelemetry.bind(this), window, {
+ defineAs: "AWSendEventTelemetry",
+ });
+
+ Cu.exportFunction(this.AWSendToParent.bind(this), window, {
+ defineAs: "AWSendToParent",
+ });
+
+ Cu.exportFunction(this.AWWaitForMigrationClose.bind(this), window, {
+ defineAs: "AWWaitForMigrationClose",
+ });
+ }
+
+ /**
+ * Wrap a promise so content can use Promise methods.
+ */
+ wrapPromise(promise) {
+ return new this.contentWindow.Promise((resolve, reject) =>
+ promise.then(resolve, reject)
+ );
+ }
+
+ /**
+ * Send multistage welcome JSON data read from aboutwelcome.overrideConetent pref to page
+ */
+ AWGetWelcomeOverrideContent() {
+ return Cu.cloneInto(
+ multiStageAboutWelcomeContent || {},
+ this.contentWindow
+ );
+ }
+
+ AWSelectTheme(data) {
+ return this.wrapPromise(
+ this.sendQuery("AWPage:SELECT_THEME", data.toUpperCase())
+ );
+ }
+
+ async 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 await this.sendQuery("AWPage:GET_ADDON_FROM_REPOSITORY", content);
+ } catch (e) {
+ Cu.reportError(
+ "Failed to get the latest add-on version for Return to AMO"
+ );
+ return null;
+ }
+ }
+
+ hasAMOAttribution(attributionData) {
+ return (
+ attributionData &&
+ attributionData.campaign === "non-fx-button" &&
+ attributionData.source === "addons.mozilla.org"
+ );
+ }
+
+ async formatAttributionData(attribution) {
+ let result = {};
+ if (this.hasAMOAttribution(attribution)) {
+ let extraProps = await this.getAddonInfo(attribution);
+ if (extraProps) {
+ result = {
+ template: "return_to_amo",
+ extraProps,
+ };
+ }
+ }
+ return result;
+ }
+
+ async getAttributionData() {
+ return Cu.cloneInto(
+ await this.formatAttributionData(
+ await this.sendQuery("AWPage:GET_ATTRIBUTION_DATA")
+ ),
+ this.contentWindow
+ );
+ }
+
+ AWGetAttributionData() {
+ return this.wrapPromise(this.getAttributionData());
+ }
+
+ /**
+ * Send initial data to page including experiment information
+ */
+ AWGetExperimentData() {
+ let experimentData;
+ try {
+ // Note that we specifically don't wait for experiments to be loaded from disk so if
+ // about:welcome loads outside of the "FirstStartup" scenario this will likely not be ready
+ experimentData = ExperimentAPI.getExperiment({
+ featureId: "aboutwelcome",
+ // Telemetry handled in AboutNewTabService.jsm
+ sendExposurePing: false,
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ if (experimentData?.slug) {
+ log.debug(
+ `Loading about:welcome with experiment: ${experimentData.slug}`
+ );
+ } else {
+ log.debug("Loading about:welcome without experiment");
+ }
+ return Cu.cloneInto(experimentData || {}, this.contentWindow);
+ }
+
+ AWGetFxAMetricsFlowURI() {
+ return this.wrapPromise(this.sendQuery("AWPage:FXA_METRICS_FLOW_URI"));
+ }
+
+ AWGetImportableSites() {
+ return this.wrapPromise(getImportableSites(this));
+ }
+
+ AWGetDefaultSites() {
+ return this.wrapPromise(getDefaultSites(this));
+ }
+
+ AWGetSelectedTheme() {
+ return this.wrapPromise(getSelectedTheme(this));
+ }
+
+ /**
+ * Send Event Telemetry
+ * @param {object} eventData
+ */
+ AWSendEventTelemetry(eventData) {
+ this.AWSendToParent("TELEMETRY_EVENT", {
+ ...eventData,
+ event_context: {
+ ...eventData.event_context,
+ page: "about:welcome",
+ },
+ });
+ }
+
+ /**
+ * Send message that can be handled by AboutWelcomeParent.jsm
+ * @param {string} type
+ * @param {any=} data
+ */
+ AWSendToParent(type, data) {
+ this.sendAsyncMessage(`AWPage:${type}`, data);
+ }
+
+ AWWaitForMigrationClose() {
+ return this.wrapPromise(this.sendQuery("AWPage:WAIT_FOR_MIGRATION_CLOSE"));
+ }
+
+ AWGetRegion() {
+ return this.wrapPromise(this.sendQuery("AWPage:GET_REGION"));
+ }
+
+ /**
+ * @param {{type: string, detail?: any}} event
+ * @override
+ */
+ handleEvent(event) {
+ log.debug(`Received page event ${event.type}`);
+ }
+}