Bug 1636463 - Refactor ASRouter to use SpecialMessageActions r=k88hudson
authorAndrei Oprea <andrei.br92@gmail.com>
Wed, 20 May 2020 15:26:31 +0000
changeset 531269 250ab0b9289e05387740e2ee517f8ad4eec92e03
parent 531268 379e2c6c049115d64977587961d08b42a09ad3c9
child 531270 abd60784e8c3fde1a2396f5d8f0c4bd6289dfe4d
push id37436
push userncsoregi@mozilla.com
push dateWed, 20 May 2020 21:30:50 +0000
treeherdermozilla-central@6c10970490f3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1636463
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1636463 - Refactor ASRouter to use SpecialMessageActions r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D74921
browser/components/newtab/common/Actions.jsm
browser/components/newtab/content-src/asrouter/asrouter-content.jsx
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/CFRPageActions.jsm
browser/components/newtab/lib/ToolbarPanelHub.jsm
browser/components/newtab/test/browser/browser_asrouter_cfr.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/ASRouterFeed.test.js
browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas.js
toolkit/components/messaging-system/test/browser/specialMessageActions/browser_sma_cancel.js
--- a/browser/components/newtab/common/Actions.jsm
+++ b/browser/components/newtab/common/Actions.jsm
@@ -156,38 +156,16 @@ for (const type of [
   "UPDATE_SEARCH_SHORTCUTS",
   "UPDATE_SECTION_PREFS",
   "WEBEXT_CLICK",
   "WEBEXT_DISMISS",
 ]) {
   actionTypes[type] = type;
 }
 
-// These are acceptable actions for AS Router messages to have. They can show up
-// as call-to-action buttons in snippets, onboarding tour, etc.
-const ASRouterActions = {};
-for (const type of [
-  "HIGHLIGHT_FEATURE",
-  "INSTALL_ADDON_FROM_URL",
-  "OPEN_APPLICATIONS_MENU",
-  "OPEN_PRIVATE_BROWSER_WINDOW",
-  "OPEN_URL",
-  "OPEN_ABOUT_PAGE",
-  "OPEN_PREFERENCES_PAGE",
-  "SHOW_FIREFOX_ACCOUNTS",
-  "PIN_CURRENT_TAB",
-  "ENABLE_FIREFOX_MONITOR",
-  "OPEN_PROTECTION_PANEL",
-  "OPEN_PROTECTION_REPORT",
-  "DISABLE_STP_DOORHANGERS",
-  "SHOW_MIGRATION_WIZARD",
-]) {
-  ASRouterActions[type] = type;
-}
-
 // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 function _RouteMessage(action, options) {
   const meta = action.meta ? { ...action.meta } : {};
   if (!options || !options.from || !options.to) {
     throw new Error(
       "Routed Messages must have options as the second parameter, and must at least include a .from and .to property."
     );
@@ -432,17 +410,16 @@ function WebExtEvent(type, data, importC
       'WebExtEvent actions should include a property "source", the id of the webextension that should receive the event.'
     );
   }
   const action = { type, data };
   return importContext === UI_CODE ? AlsoToMain(action) : action;
 }
 
 this.actionTypes = actionTypes;
-this.ASRouterActions = ASRouterActions;
 
 this.actionCreators = {
   BroadcastToContent,
   UserEvent,
   ASRouterUserEvent,
   UndesiredEvent,
   PerfEvent,
   ImpressionStats,
@@ -510,16 +487,15 @@ this.actionUtils = {
   },
   _RouteMessage,
 };
 
 const EXPORTED_SYMBOLS = [
   "actionTypes",
   "actionCreators",
   "actionUtils",
-  "ASRouterActions",
   "globalImportContext",
   "UI_CODE",
   "BACKGROUND_PROCESS",
   "MAIN_MESSAGE_TYPE",
   "CONTENT_MESSAGE_TYPE",
   "PRELOAD_MESSAGE_TYPE",
 ];
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -1,17 +1,13 @@
 /* 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 {
-  actionCreators as ac,
-  actionTypes as at,
-  ASRouterActions as ra,
-} from "common/Actions.jsm";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
 import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
 import { generateBundles } from "./rich-text-strings";
 import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
 import { LocalizationProvider } from "fluent-react";
 import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
 import React from "react";
 import ReactDOM from "react-dom";
 import { SnippetsTemplates } from "./templates/template-manifest";
@@ -344,19 +340,19 @@ export class ASRouterUISurface extends R
     });
 
     return urlObj.toString();
   }
 
   async onUserAction(action) {
     switch (action.type) {
       // This needs to be handled locally because its
-      case ra.ENABLE_FIREFOX_MONITOR:
+      case "ENABLE_FIREFOX_MONITOR":
         const url = await this.getMonitorUrl(action.data.args);
-        ASRouterUtils.executeAction({ type: ra.OPEN_URL, data: { args: url } });
+        ASRouterUtils.executeAction({ type: "OPEN_URL", data: { args: url } });
         break;
       default:
         ASRouterUtils.executeAction(action);
     }
   }
 
   renderSnippets() {
     const { message } = this.state;
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -205,17 +205,16 @@ module.exports = g;
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MAIN_MESSAGE_TYPE", function() { return MAIN_MESSAGE_TYPE; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CONTENT_MESSAGE_TYPE", function() { return CONTENT_MESSAGE_TYPE; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PRELOAD_MESSAGE_TYPE", function() { return PRELOAD_MESSAGE_TYPE; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "UI_CODE", function() { return UI_CODE; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BACKGROUND_PROCESS", function() { return BACKGROUND_PROCESS; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "globalImportContext", function() { return globalImportContext; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionTypes", function() { return actionTypes; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterActions", function() { return ASRouterActions; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionCreators", function() { return actionCreators; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "actionUtils", function() { return actionUtils; });
 /* 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/. */
 
 
 var MAIN_MESSAGE_TYPE = "ActivityStream:Main";
@@ -235,24 +234,16 @@ const globalImportContext = typeof Windo
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
 
 for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TARGETING_UPDATE", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "CLEAR_PREF", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_RESET", "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", "DISCOVERY_STREAM_DEV_IDLE_DAILY", "DISCOVERY_STREAM_DEV_SYNC_RS", "DISCOVERY_STREAM_DEV_SYSTEM_TICK", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_PERSONALIZATION_INIT", "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", "DISCOVERY_STREAM_PERSONALIZATION_VERSION", "DISCOVERY_STREAM_PERSONALIZATION_VERSION_TOGGLE", "DISCOVERY_STREAM_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_PLACEMENTS", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_BLOCKED", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_PRIVACY_INFO", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_PRIVACY_INFO", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SUBMIT_SIGNIN", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "TRAILHEAD_ENROLL_EVENT", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
   actionTypes[type] = type;
-} // These are acceptable actions for AS Router messages to have. They can show up
-// as call-to-action buttons in snippets, onboarding tour, etc.
-
-
-const ASRouterActions = {};
-
-for (const type of ["HIGHLIGHT_FEATURE", "INSTALL_ADDON_FROM_URL", "OPEN_APPLICATIONS_MENU", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PREFERENCES_PAGE", "SHOW_FIREFOX_ACCOUNTS", "PIN_CURRENT_TAB", "ENABLE_FIREFOX_MONITOR", "OPEN_PROTECTION_PANEL", "OPEN_PROTECTION_REPORT", "DISABLE_STP_DOORHANGERS", "SHOW_MIGRATION_WIZARD"]) {
-  ASRouterActions[type] = type;
 } // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 
 
 function _RouteMessage(action, options) {
   const meta = action.meta ? { ...action.meta
   } : {};
 
@@ -2580,20 +2571,20 @@ class ASRouterUISurface extends react__W
       }
     });
     return urlObj.toString();
   }
 
   async onUserAction(action) {
     switch (action.type) {
       // This needs to be handled locally because its
-      case common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["ASRouterActions"].ENABLE_FIREFOX_MONITOR:
+      case "ENABLE_FIREFOX_MONITOR":
         const url = await this.getMonitorUrl(action.data.args);
         ASRouterUtils.executeAction({
-          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["ASRouterActions"].OPEN_URL,
+          type: "OPEN_URL",
           data: {
             args: url
           }
         });
         break;
 
       default:
         ASRouterUtils.executeAction(action);
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -4,19 +4,16 @@
 "use strict";
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 XPCOMUtils.defineLazyModuleGetters(this, {
-  AddonManager: "resource://gre/modules/AddonManager.jsm",
-  UITour: "resource:///modules/UITour.jsm",
-  FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
   SnippetsTestMessageProvider:
     "resource://activity-stream/lib/SnippetsTestMessageProvider.jsm",
   PanelTestProvider: "resource://activity-stream/lib/PanelTestProvider.jsm",
   ToolbarBadgeHub: "resource://activity-stream/lib/ToolbarBadgeHub.jsm",
   ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
@@ -29,27 +26,26 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ASRouterTriggerListeners:
     "resource://activity-stream/lib/ASRouterTriggerListeners.jsm",
   CFRMessageProvider: "resource://activity-stream/lib/CFRMessageProvider.jsm",
   GroupsConfigurationProvider:
     "resource://activity-stream/lib/GroupsConfigurationProvider.jsm",
   KintoHttpClient: "resource://services-common/kinto-http-client.js",
   Downloader: "resource://services-settings/Attachments.jsm",
   RemoteL10n: "resource://activity-stream/lib/RemoteL10n.jsm",
-  MigrationUtils: "resource:///modules/MigrationUtils.jsm",
   ExperimentAPI: "resource://messaging-system/experiments/ExperimentAPI.jsm",
+  SpecialMessageActions:
+    "resource://messaging-system/lib/SpecialMessageActions.jsm",
 });
 XPCOMUtils.defineLazyServiceGetters(this, {
   BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
 });
-const {
-  ASRouterActions: ra,
-  actionTypes: at,
-  actionCreators: ac,
-} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
+const { actionTypes: at, actionCreators: ac } = ChromeUtils.import(
+  "resource://activity-stream/common/Actions.jsm"
+);
 
 const { CFRMessageProvider } = ChromeUtils.import(
   "resource://activity-stream/lib/CFRMessageProvider.jsm"
 );
 const { OnboardingMessageProvider } = ChromeUtils.import(
   "resource://activity-stream/lib/OnboardingMessageProvider.jsm"
 );
 const { RemoteSettings } = ChromeUtils.import(
@@ -471,64 +467,16 @@ const MessageLoaderUtils = {
         })
         .filter(message => message.weight > 0),
       lastUpdated,
       errors: MessageLoaderUtils.errors,
     };
   },
 
   /**
-   * _loadAddonIconInURLBar - load addons-notification icon by displaying
-   * box containing addons icon in urlbar. See Bug 1513882
-   *
-   * @param  {XULElement} Target browser element for showing addons icon
-   */
-  _loadAddonIconInURLBar(browser) {
-    if (!browser) {
-      return;
-    }
-    const chromeDoc = browser.ownerDocument;
-    let notificationPopupBox = chromeDoc.getElementById(
-      "notification-popup-box"
-    );
-    if (!notificationPopupBox) {
-      return;
-    }
-    if (
-      notificationPopupBox.style.display === "none" ||
-      notificationPopupBox.style.display === ""
-    ) {
-      notificationPopupBox.style.display = "block";
-    }
-  },
-
-  async installAddonFromURL(browser, url, telemetrySource = "amo") {
-    try {
-      MessageLoaderUtils._loadAddonIconInURLBar(browser);
-      const aUri = Services.io.newURI(url);
-      const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
-
-      // AddonManager installation source associated to the addons installed from activitystream's CFR
-      // and RTAMO (source is going to be "amo" if not configured explicitly in the message provider).
-      const telemetryInfo = { source: telemetrySource };
-      const install = await AddonManager.getInstallForURL(aUri.spec, {
-        telemetryInfo,
-      });
-      await AddonManager.installAddonFromWebpage(
-        "application/x-xpinstall",
-        browser,
-        systemPrincipal,
-        install
-      );
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  },
-
-  /**
    * cleanupCache - Removes cached data of removed providers.
    *
    * @param {Array} providers A list of activer AS Router providers
    */
   async cleanupCache(providers, storage) {
     const ids = providers.filter(p => p.type === "remote").map(p => p.id);
     const cache = await MessageLoaderUtils._remoteLoaderCache(storage);
     let dirty = false;
@@ -957,17 +905,16 @@ class _ASRouter {
       addImpression: this.addImpression,
       blockMessageById: this.blockMessageById,
       unblockMessageById: this.unblockMessageById,
       dispatch: this.dispatch,
     });
     ToolbarPanelHub.init(this.waitForInitialized, {
       getMessages: this.handleMessageRequest,
       dispatch: this.dispatch,
-      handleUserAction: this.handleUserAction,
     });
     MomentsPageHub.init(this.waitForInitialized, {
       handleMessageRequest: this.handleMessageRequest,
       addImpression: this.addImpression,
       blockMessageById: this.blockMessageById,
       dispatch: this.dispatch,
     });
 
@@ -1014,16 +961,17 @@ class _ASRouter {
     // set necessary state in the rest of AS
     this.dispatchToAS(
       ac.BroadcastToContent({
         type: at.AS_ROUTER_INITIALIZED,
         data: ASRouterPreferences.specialConditions,
       })
     );
 
+    SpecialMessageActions.blockMessageById = this.blockMessageById;
     Services.obs.addObserver(this._onLocaleChanged, TOPIC_INTL_LOCALE_CHANGED);
     Services.prefs.addObserver(USE_REMOTE_L10N_PREF, this);
     // sets .initialized to true and resolves .waitForInitialized promise
     this._finishInitializing();
   }
 
   uninit() {
     this._storage.set("previousSessionEnd", Date.now());
@@ -1920,120 +1868,16 @@ class _ASRouter {
 
     // Clear and refresh Attribution, and then fetch the messages again to update
     AttributionCode._clearCache();
     await AttributionCode.getAttrDataAsync();
     this._updateMessageProviders();
     await this.loadMessagesFromAllProviders();
   }
 
-  async handleUserAction({ data: action, target }) {
-    switch (action.type) {
-      case ra.SHOW_MIGRATION_WIZARD:
-        MigrationUtils.showMigrationWizard(target.browser.ownerGlobal, [
-          MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB,
-        ]);
-        break;
-      case ra.OPEN_PRIVATE_BROWSER_WINDOW:
-        // Forcefully open about:privatebrowsing
-        target.browser.ownerGlobal.OpenBrowserWindow({ private: true });
-        break;
-      case ra.OPEN_URL:
-        target.browser.ownerGlobal.openLinkIn(
-          Services.urlFormatter.formatURL(action.data.args),
-          action.data.where || "current",
-          {
-            private: false,
-            triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
-              {}
-            ),
-            csp: null,
-          }
-        );
-        break;
-      case ra.OPEN_ABOUT_PAGE:
-        let aboutPageURL = new URL(`about:${action.data.args}`);
-        if (action.data.entrypoint) {
-          aboutPageURL.search = action.data.entrypoint;
-        }
-        target.browser.ownerGlobal.openTrustedLinkIn(
-          aboutPageURL.toString(),
-          "tab"
-        );
-        break;
-      case ra.OPEN_PREFERENCES_PAGE:
-        target.browser.ownerGlobal.openPreferences(
-          action.data.category || action.data.args,
-          action.data.entrypoint && {
-            urlParams: { entrypoint: action.data.entrypoint },
-          }
-        );
-        break;
-      case ra.OPEN_APPLICATIONS_MENU:
-        UITour.showMenu(target.browser.ownerGlobal, action.data.args);
-        break;
-      case ra.HIGHLIGHT_FEATURE:
-        const highlight = await UITour.getTarget(
-          target.browser.ownerGlobal,
-          action.data.args
-        );
-        if (highlight) {
-          await UITour.showHighlight(
-            target.browser.ownerGlobal,
-            highlight,
-            "none",
-            { autohide: true }
-          );
-        }
-        break;
-      case ra.INSTALL_ADDON_FROM_URL:
-        this._updateOnboardingState();
-        await MessageLoaderUtils.installAddonFromURL(
-          target.browser,
-          action.data.url,
-          action.data.telemetrySource
-        );
-        break;
-      case ra.PIN_CURRENT_TAB:
-        let tab = target.browser.ownerGlobal.gBrowser.selectedTab;
-        target.browser.ownerGlobal.gBrowser.pinTab(tab);
-        target.browser.ownerGlobal.ConfirmationHint.show(tab, "pinTab", {
-          showDescription: true,
-        });
-        break;
-      case ra.SHOW_FIREFOX_ACCOUNTS:
-        const url = await FxAccounts.config.promiseConnectAccountURI(
-          "snippets"
-        );
-        // We want to replace the current tab.
-        target.browser.ownerGlobal.openLinkIn(url, "current", {
-          private: false,
-          triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
-            {}
-          ),
-          csp: null,
-        });
-        break;
-      case ra.OPEN_PROTECTION_PANEL:
-        let { gProtectionsHandler } = target.browser.ownerGlobal;
-        gProtectionsHandler.showProtectionsPopup({});
-        break;
-      case ra.OPEN_PROTECTION_REPORT:
-        target.browser.ownerGlobal.gProtectionsHandler.openProtections();
-        break;
-      case ra.DISABLE_STP_DOORHANGERS:
-        await this.blockMessageById([
-          "SOCIAL_TRACKING_PROTECTION",
-          "FINGERPRINTERS_PROTECTION",
-          "CRYPTOMINERS_PROTECTION",
-        ]);
-        break;
-    }
-  }
-
   /**
    * sendAsyncMessageToPreloaded - Sends an action to each preloaded browser, if any
    *
    * @param  {obj} action An action to be sent to content
    */
   sendAsyncMessageToPreloaded(action) {
     const preloadedBrowsers = this.getPreloadedBrowser();
     if (preloadedBrowsers) {
@@ -2152,19 +1996,21 @@ class _ASRouter {
       browserWindow.document.getElementById("whats-new-menu-button")
     );
   }
 
   /* eslint-disable complexity */
   async onMessage({ data: action, target }) {
     switch (action.type) {
       case "USER_ACTION":
-        if (action.data.type in ra) {
-          await this.handleUserAction({ data: action.data, target });
+        // This is to support ReturnToAMO
+        if (action.data.type === "INSTALL_ADDON_FROM_URL") {
+          this._updateOnboardingState();
         }
+        await SpecialMessageActions.handleAction(action.data, target.browser);
         break;
       case "NEWTAB_MESSAGE_REQUEST":
         await this.waitForInitialized;
         await this.sendNewTabMessage(target, action.data);
         break;
       case "TRIGGER":
         await this.waitForInitialized;
         await this.sendTriggerMessage(
--- a/browser/components/newtab/lib/CFRPageActions.jsm
+++ b/browser/components/newtab/lib/CFRPageActions.jsm
@@ -2,19 +2,16 @@
  * 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 { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { ASRouterActions: ra } = ChromeUtils.import(
-  "resource://activity-stream/common/Actions.jsm"
-);
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   RemoteL10n: "resource://activity-stream/lib/RemoteL10n.jsm",
 });
 
@@ -870,17 +867,17 @@ class PageAction {
 
   _executeNotifierAction(browser, message) {
     switch (message.content.layout) {
       case "chiclet_open_url":
         this._dispatchToASRouter(
           {
             type: "USER_ACTION",
             data: {
-              type: ra.OPEN_URL,
+              type: "OPEN_URL",
               data: {
                 args: message.content.action.url,
                 where: message.content.action.where,
               },
             },
           },
           this.window
         );
--- a/browser/components/newtab/lib/ToolbarPanelHub.jsm
+++ b/browser/components/newtab/lib/ToolbarPanelHub.jsm
@@ -6,16 +6,18 @@
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
   EveryWindow: "resource:///modules/EveryWindow.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   Preferences: "resource://gre/modules/Preferences.jsm",
+  SpecialMessageActions:
+    "resource://messaging-system/lib/SpecialMessageActions.jsm",
 });
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "TrackingDBService",
   "@mozilla.org/tracking-db-service;1",
   "nsITrackingDBService"
 );
 
@@ -46,20 +48,19 @@ class _ToolbarPanelHub {
     this._hideToolbarButton = this._hideToolbarButton.bind(this);
     this.insertProtectionPanelMessage = this.insertProtectionPanelMessage.bind(
       this
     );
 
     this.state = {};
   }
 
-  async init(waitForInitialized, { getMessages, dispatch, handleUserAction }) {
+  async init(waitForInitialized, { getMessages, dispatch }) {
     this._getMessages = getMessages;
     this._dispatch = dispatch;
-    this._handleUserAction = handleUserAction;
     // Wait for ASRouter messages to become available in order to know
     // if we can show the What's New panel
     await waitForInitialized;
     // Enable the application menu button so that the user can access
     // the panel outside of the toolbar button
     await this.enableAppmenuButton();
 
     this.state = {
@@ -234,26 +235,26 @@ class _ToolbarPanelHub {
     let url;
     try {
       // Set platform specific path variables for SUMO articles
       url = Services.urlFormatter.formatURL(message.content.cta_url);
     } catch (e) {
       Cu.reportError(e);
       url = message.content.cta_url;
     }
-    this._handleUserAction({
-      target: win,
-      data: {
+    SpecialMessageActions.handleAction(
+      {
         type: message.content.cta_type,
         data: {
           args: url,
           where: "tabshifted",
         },
       },
-    });
+      win.browser
+    );
 
     this.sendUserEventTelemetry(win, "CLICK", message);
   }
 
   /**
    * Attach event listener to dispatch message defined action.
    */
   _attachCommandListener(win, element, message) {
--- a/browser/components/newtab/test/browser/browser_asrouter_cfr.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_cfr.js
@@ -162,17 +162,17 @@ function clearNotifications() {
     "Should have removed the notification"
   );
 }
 
 function trigger_cfr_panel(
   browser,
   trigger,
   {
-    action = { type: "FOO" },
+    action = { type: "CANCEL" },
     heading_text,
     category = "cfrAddons",
     layout,
     skip_address_bar_notifier = false,
     use_single_secondary_button = false,
     template = "cfr_doorhanger",
   } = {}
 ) {
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -205,16 +205,19 @@ describe("ASRouter", () => {
         download() {
           return Promise.resolve("/path/to/download");
         }
       },
       ExperimentAPI: {
         getExperiment: sandbox.stub().returns({ branch: { value: [] } }),
         ready: sandbox.stub().resolves(),
       },
+      SpecialMessageActions: {
+        handleAction: sandbox.stub(),
+      },
     });
     await createRouterAndInit();
   });
   afterEach(() => {
     ASRouterPreferences.uninit();
     sandbox.restore();
     globals.restore();
   });
@@ -278,17 +281,16 @@ describe("ASRouter", () => {
       );
 
       assert.calledWithExactly(
         FakeToolbarPanelHub.init,
         Router.waitForInitialized,
         {
           getMessages: Router.handleMessageRequest,
           dispatch: Router.dispatch,
-          handleUserAction: Router.handleUserAction,
         }
       );
 
       assert.calledWithExactly(
         FakeMomentsPageHub.init,
         Router.waitForInitialized,
         {
           handleMessageRequest: Router.handleMessageRequest,
@@ -2594,274 +2596,16 @@ describe("ASRouter", () => {
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "CLEAR_ALL" }
         );
       });
     });
 
-    describe("#onMessage: Onboarding actions", () => {
-      it("should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW", async () => {
-        let [testMessage] = Router.state.messages;
-        const msg = fakeExecuteUserAction({
-          type: "OPEN_PRIVATE_BROWSER_WINDOW",
-          data: testMessage,
-        });
-        await Router.onMessage(msg);
-
-        assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {
-          private: true,
-        });
-      });
-      it("should call openLinkIn with the correct params on OPEN_URL", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_URL",
-          data: { args: "some/url.com", where: "tabshifted" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openLinkIn,
-          "some/url.com",
-          "tabshifted",
-          { private: false, triggeringPrincipal: undefined, csp: null }
-        );
-      });
-      it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_ABOUT_PAGE",
-          data: { args: "something" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openTrustedLinkIn,
-          "about:something",
-          "tab"
-        );
-      });
-      it("should call openLinkIn with the entrypoint params on OPEN_ABOUT_PAGE", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_ABOUT_PAGE",
-          data: { args: "something", entrypoint: "entryPoint=foo" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openTrustedLinkIn,
-          "about:something?entryPoint=foo",
-          "tab"
-        );
-      });
-      it("should call MigrationUtils.showMigrationWizard on SHOW_MIGRATION_WIZARD", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "SHOW_MIGRATION_WIZARD",
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        globals.set("MigrationUtils", {
-          showMigrationWizard: sandbox
-            .stub()
-            .withArgs(msg.target.browser.ownerGlobal, ["test"]),
-          MIGRATION_ENTRYPOINT_NEWTAB: "test",
-        });
-        await Router.onMessage(msg);
-
-        assert.calledOnce(MigrationUtils.showMigrationWizard);
-        assert.calledWith(
-          MigrationUtils.showMigrationWizard,
-          msg.target.browser.ownerGlobal,
-          [MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB]
-        );
-      });
-    });
-
-    describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
-      beforeEach(() => {
-        globals.set("FxAccounts", {
-          config: {
-            promiseConnectAccountURI: sandbox.stub().resolves("some/url"),
-          },
-        });
-      });
-      it("should call openLinkIn with the correct params on OPEN_URL", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = { type: "SHOW_FIREFOX_ACCOUNTS" };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openLinkIn,
-          "some/url",
-          "current",
-          { private: false, triggeringPrincipal: undefined, csp: null }
-        );
-      });
-    });
-
-    describe("#onMessage: OPEN_PREFERENCES_PAGE", () => {
-      it("should call openPreferences with the correct params on OPEN_PREFERENCES_PAGE", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_PREFERENCES_PAGE",
-          data: { category: "something" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openPreferences);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openPreferences,
-          "something"
-        );
-      });
-      it("should call openPreferences with the correct params on OPEN_PREFERENCES_PAGE (snippets payload)", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_PREFERENCES_PAGE",
-          data: { args: "arg_from_snippets" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openPreferences);
-        assert.calledWith(
-          msg.target.browser.ownerGlobal.openPreferences,
-          "arg_from_snippets"
-        );
-      });
-      it("should call openPreferences with the correct entrypoint if defined", async () => {
-        let [testMessage] = Router.state.messages;
-        testMessage.button_action = {
-          type: "OPEN_PREFERENCES_PAGE",
-          data: { category: "something", entrypoint: "unittest" },
-        };
-        const msg = fakeExecuteUserAction(testMessage.button_action);
-        await Router.onMessage(msg);
-
-        assert.calledOnce(msg.target.browser.ownerGlobal.openPreferences);
-        assert.calledWithExactly(
-          msg.target.browser.ownerGlobal.openPreferences,
-          "something",
-          { urlParams: { entrypoint: "unittest" } }
-        );
-      });
-    });
-
-    describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
-      it("should call installAddonFromURL with correct arguments", async () => {
-        sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
-        const msg = fakeExecuteUserAction({
-          type: "INSTALL_ADDON_FROM_URL",
-          data: { url: "foo.com", telemetrySource: "foo" },
-        });
-
-        await Router.onMessage(msg);
-
-        assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
-        assert.calledWithExactly(
-          MessageLoaderUtils.installAddonFromURL,
-          msg.target.browser,
-          "foo.com",
-          "foo"
-        );
-      });
-
-      it("should add/remove observers for `webextension-install-notify`", async () => {
-        sandbox.spy(global.Services.obs, "addObserver");
-        sandbox.spy(global.Services.obs, "removeObserver");
-
-        sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
-        const msg = fakeExecuteUserAction({
-          type: "INSTALL_ADDON_FROM_URL",
-          data: { url: "foo.com" },
-        });
-
-        await Router.onMessage(msg);
-
-        assert.calledOnce(global.Services.obs.addObserver);
-
-        const [cb] = global.Services.obs.addObserver.firstCall.args;
-
-        cb();
-
-        assert.calledOnce(global.Services.obs.removeObserver);
-        assert.calledOnce(channel.sendAsyncMessage);
-      });
-    });
-
-    describe("#onMessage: PIN_CURRENT_TAB", () => {
-      it("should call pin tab with the selectedTab", async () => {
-        const msg = fakeExecuteUserAction({ type: "PIN_CURRENT_TAB" });
-        const { gBrowser, ConfirmationHint } = msg.target.browser.ownerGlobal;
-
-        await Router.onMessage(msg);
-
-        assert.calledOnce(gBrowser.pinTab);
-        assert.calledWithExactly(gBrowser.pinTab, gBrowser.selectedTab);
-        assert.calledOnce(ConfirmationHint.show);
-        assert.calledWithExactly(
-          ConfirmationHint.show,
-          gBrowser.selectedTab,
-          "pinTab",
-          { showDescription: true }
-        );
-      });
-    });
-
-    describe("#onMessage: OPEN_PROTECTION_PANEL", () => {
-      it("should open protection panel", async () => {
-        const msg = fakeExecuteUserAction({ type: "OPEN_PROTECTION_PANEL" });
-        let { gProtectionsHandler } = msg.target.browser.ownerGlobal;
-
-        await Router.onMessage(msg);
-
-        assert.calledOnce(gProtectionsHandler.showProtectionsPopup);
-        assert.calledWithExactly(gProtectionsHandler.showProtectionsPopup, {});
-      });
-    });
-
-    describe("#onMessage: OPEN_PROTECTION_REPORT", () => {
-      it("should open protection report", async () => {
-        const msg = fakeExecuteUserAction({ type: "OPEN_PROTECTION_REPORT" });
-        let { gProtectionsHandler } = msg.target.browser.ownerGlobal;
-
-        await Router.onMessage(msg);
-
-        assert.calledOnce(gProtectionsHandler.openProtections);
-      });
-    });
-
-    describe("#onMessage: DISABLE_STP_DOORHANGERS", () => {
-      it("should block STP related messages", async () => {
-        const msg = fakeExecuteUserAction({ type: "DISABLE_STP_DOORHANGERS" });
-
-        assert.deepEqual(Router.state.messageBlockList, []);
-
-        await Router.onMessage(msg);
-
-        assert.deepEqual(Router.state.messageBlockList, [
-          "SOCIAL_TRACKING_PROTECTION",
-          "FINGERPRINTERS_PROTECTION",
-          "CRYPTOMINERS_PROTECTION",
-        ]);
-      });
-    });
-
     describe("#dispatch(action, target)", () => {
       it("should an action and target to onMessage", async () => {
         // use the IMPRESSION action to make sure actions are actually getting processed
         sandbox.stub(Router, "addImpression");
         sandbox.spy(Router, "onMessage");
         const target = {};
         const action = { type: "IMPRESSION" };
 
@@ -3153,16 +2897,60 @@ describe("ASRouter", () => {
         const msg = fakeAsyncMessage({ type: "FOO" });
         sandbox.stub(Cu, "reportError");
 
         Router.onMessage(msg);
 
         assert.calledOnce(Cu.reportError);
       });
     });
+    describe("#onMessage: USER_ACTION", () => {
+      it("should dispatch to SpecialMessageActions", async () => {
+        const msg = fakeExecuteUserAction({
+          type: "OPEN_URL",
+          data: {
+            args: "foo",
+          },
+        });
+
+        await Router.onMessage(msg);
+
+        assert.calledOnce(global.SpecialMessageActions.handleAction);
+        assert.calledWithExactly(
+          global.SpecialMessageActions.handleAction,
+          msg.data.data,
+          msg.target.browser
+        );
+      });
+      it("should call update onboarding state on INSTALL_ADDON_FROM_URL", async () => {
+        const spy = sandbox.spy(global.Services.obs, "addObserver");
+        const msg = fakeExecuteUserAction({
+          type: "INSTALL_ADDON_FROM_URL",
+        });
+
+        await Router.onMessage(msg);
+
+        assert.calledOnce(spy);
+        assert.calledWithExactly(
+          spy,
+          sinon.match.func,
+          "webextension-install-notify"
+        );
+
+        const [eventCb] = spy.firstCall.args;
+        // Call the observer cb
+        eventCb();
+        assert.calledOnce(Router.messageChannel.sendAsyncMessage);
+        assert.calledWithExactly(
+          Router.messageChannel.sendAsyncMessage,
+          OUTGOING_MESSAGE_NAME,
+          { type: "CLEAR_INTERRUPT" }
+        );
+      });
+    });
   });
 
   describe("_triggerHandler", () => {
     it("should call #onMessage with the correct trigger", () => {
       const getter = sandbox.stub();
       getter.returns(false);
       sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
       sinon.spy(Router, "onMessage");
@@ -3185,61 +2973,16 @@ describe("ASRouter", () => {
       sinon.spy(Router, "onMessage");
       const target = {};
       const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
       Router._triggerHandler(target, trigger);
       assert.notCalled(Router.onMessage);
     });
   });
 
-  describe("#UITour", () => {
-    let showMenuStub;
-    const highlightTarget = { target: "target" };
-    beforeEach(() => {
-      showMenuStub = sandbox.stub();
-      globals.set("UITour", {
-        showMenu: showMenuStub,
-        getTarget: sandbox
-          .stub()
-          .withArgs(sinon.match.object, "pageAaction-sendToDevice")
-          .resolves(highlightTarget),
-        showHighlight: sandbox.stub(),
-      });
-    });
-    it("should call UITour.showMenu with the correct params on OPEN_APPLICATIONS_MENU", async () => {
-      const msg = fakeExecuteUserAction({
-        type: "OPEN_APPLICATIONS_MENU",
-        data: { args: "appMenu" },
-      });
-      await Router.onMessage(msg);
-
-      assert.calledOnce(showMenuStub);
-      assert.calledWith(
-        showMenuStub,
-        msg.target.browser.ownerGlobal,
-        "appMenu"
-      );
-    });
-    it("should call UITour.showHighlight with the correct params on HIGHLIGHT_FEATURE", async () => {
-      const msg = fakeExecuteUserAction({
-        type: "HIGHLIGHT_FEATURE",
-        data: { args: "pageAction-sendToDevice" },
-      });
-      await Router.onMessage(msg);
-
-      assert.calledOnce(UITour.getTarget);
-      assert.calledOnce(UITour.showHighlight);
-      assert.calledWith(
-        UITour.showHighlight,
-        msg.target.browser.ownerGlobal,
-        highlightTarget
-      );
-    });
-  });
-
   describe("valid preview endpoint", () => {
     it("should report an error if url protocol is not https", () => {
       sandbox.stub(Cu, "reportError");
 
       assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
       assert.calledTwice(Cu.reportError);
     });
   });
--- a/browser/components/newtab/test/unit/asrouter/ASRouterFeed.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterFeed.test.js
@@ -36,16 +36,17 @@ describe("ASRouterFeed", () => {
     };
     globals.set({
       GroupsConfigurationProvider: { getMessages: () => [] },
       ASRouterPreferences,
       BookmarkPanelHub: FakeBookmarkPanelHub,
       ToolbarBadgeHub: FakeToolbarBadgeHub,
       ToolbarPanelHub: FakeToolbarPanelHub,
       MomentsPageHub: FakeMomentsPageHub,
+      SpecialMessageActions: {},
     });
 
     Router = new _ASRouter({ providers: [FAKE_LOCAL_PROVIDER] });
 
     storage = {
       get: sandbox.stub().returns(Promise.resolve([])),
       set: sandbox.stub().returns(Promise.resolve()),
     };
--- a/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
+++ b/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
@@ -1,9 +1,8 @@
-import { GlobalOverrider } from "test/unit/utils";
 import { MessageLoaderUtils } from "lib/ASRouter.jsm";
 const { STARTPAGE_VERSION } = MessageLoaderUtils;
 
 const FAKE_OPTIONS = {
   storage: {
     set() {
       return Promise.resolve();
     },
@@ -425,121 +424,16 @@ describe("MessageLoaderUtils", () => {
           id: "foo",
           lastUpdated: 0,
           updateCycleInMs: 300,
         })
       );
     });
   });
 
-  describe("#_loadAddonIconInURLBar", () => {
-    let notificationContainerEl;
-    let browser;
-    let getContainerStub;
-    beforeEach(() => {
-      notificationContainerEl = { style: {} };
-      browser = {
-        ownerDocument: {
-          getElementById() {
-            return {};
-          },
-        },
-      };
-      getContainerStub = sandbox.stub(browser.ownerDocument, "getElementById");
-    });
-    it("should return for empty args", () => {
-      MessageLoaderUtils._loadAddonIconInURLBar();
-      assert.notCalled(getContainerStub);
-    });
-    it("should return if notification popup box not found", () => {
-      getContainerStub.returns(null);
-      MessageLoaderUtils._loadAddonIconInURLBar(browser);
-      assert.calledOnce(getContainerStub);
-    });
-    it("should unhide notification popup box with display style as none", () => {
-      getContainerStub.returns(notificationContainerEl);
-      notificationContainerEl.style.display = "none";
-      MessageLoaderUtils._loadAddonIconInURLBar(browser);
-      assert.calledWith(
-        browser.ownerDocument.getElementById,
-        "notification-popup-box"
-      );
-      assert.equal(notificationContainerEl.style.display, "block");
-    });
-    it("should unhide notification popup box with display style empty", () => {
-      getContainerStub.returns(notificationContainerEl);
-      notificationContainerEl.style.display = "";
-      MessageLoaderUtils._loadAddonIconInURLBar(browser);
-      assert.calledWith(
-        browser.ownerDocument.getElementById,
-        "notification-popup-box"
-      );
-      assert.equal(notificationContainerEl.style.display, "block");
-    });
-  });
-
-  describe("#installAddonFromURL", () => {
-    let globals;
-    let getInstallStub;
-    let installAddonStub;
-    beforeEach(() => {
-      globals = new GlobalOverrider();
-      getInstallStub = sandbox.stub();
-      installAddonStub = sandbox.stub();
-      sandbox.stub(MessageLoaderUtils, "_loadAddonIconInURLBar").returns(null);
-      globals.set("AddonManager", {
-        getInstallForURL: getInstallStub,
-        installAddonFromWebpage: installAddonStub,
-      });
-    });
-    afterEach(() => {
-      globals.restore();
-    });
-    it("should call the Addons API when passed a valid URL", async () => {
-      getInstallStub.resolves(null);
-      installAddonStub.resolves(null);
-
-      await MessageLoaderUtils.installAddonFromURL({}, "foo.com");
-
-      assert.calledOnce(getInstallStub);
-      assert.calledOnce(installAddonStub);
-
-      // Verify that the expected installation source has been passed to the getInstallForURL
-      // method (See Bug 1496167 for a rationale).
-      assert.calledWithExactly(getInstallStub, "foo.com", {
-        telemetryInfo: { source: "amo" },
-      });
-    });
-    it("should optionally pass a custom telemetrySource to the Addons API if specified", async () => {
-      getInstallStub.resolves(null);
-      installAddonStub.resolves(null);
-
-      await MessageLoaderUtils.installAddonFromURL({}, "foo.com", "foo");
-
-      assert.calledOnce(getInstallStub);
-      assert.calledOnce(installAddonStub);
-
-      // Verify that a custom installation source can be passed to the getInstallForURL
-      // method (See Bug 1549770 for a rationale).
-      assert.calledWithExactly(getInstallStub, "foo.com", {
-        telemetryInfo: { source: "foo" },
-      });
-    });
-    it("should not call the Addons API on invalid URLs", async () => {
-      sandbox
-        .stub(global.Services.scriptSecurityManager, "getSystemPrincipal")
-        .throws();
-
-      await MessageLoaderUtils.installAddonFromURL({}, "https://foo.com");
-
-      assert.notCalled(getInstallStub);
-      assert.notCalled(installAddonStub);
-    });
-  });
-
   describe("#cleanupCache", () => {
     it("should remove data for providers no longer active", async () => {
       const fakeStorage = {
         get: sinon.stub().returns(
           Promise.resolve({
             "id-1": {},
             "id-2": {},
             "id-3": {},
--- a/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
@@ -20,17 +20,16 @@ describe("ToolbarPanelHub", () => {
   let removeObserverStub;
   let getBoolPrefStub;
   let setBoolPrefStub;
   let waitForInitializedStub;
   let isBrowserPrivateStub;
   let fakeDispatch;
   let getEarliestRecordedDateStub;
   let getEventsByDateRangeStub;
-  let handleUserActionStub;
   let defaultSearchStub;
   let scriptloaderStub;
 
   beforeEach(async () => {
     sandbox = sinon.createSandbox();
     globals = new GlobalOverrider();
     instance = new _ToolbarPanelHub();
     waitForInitializedStub = sandbox.stub().resolves();
@@ -137,17 +136,16 @@ describe("ToolbarPanelHub", () => {
     setBoolPrefStub = sandbox.stub();
     fakeDispatch = sandbox.stub();
     isBrowserPrivateStub = sandbox.stub();
     getEarliestRecordedDateStub = sandbox.stub().returns(
       // A random date that's not the current timestamp
       new Date() - 500
     );
     getEventsByDateRangeStub = sandbox.stub().returns([]);
-    handleUserActionStub = sandbox.stub();
     defaultSearchStub = { defaultEngine: { name: "DDG" } };
     globals.set({
       EveryWindow: everyWindowStub,
       Services: {
         ...Services,
         prefs: {
           addObserver: addObserverStub,
           removeObserver: removeObserverStub,
@@ -160,16 +158,19 @@ describe("ToolbarPanelHub", () => {
       PrivateBrowsingUtils: {
         isBrowserPrivate: isBrowserPrivateStub,
       },
       Preferences: preferencesStub,
       TrackingDBService: {
         getEarliestRecordedDate: getEarliestRecordedDateStub,
         getEventsByDateRange: getEventsByDateRangeStub,
       },
+      SpecialMessageActions: {
+        handleAction: sandbox.stub(),
+      },
     });
   });
   afterEach(() => {
     instance.uninit();
     sandbox.restore();
     globals.restore();
     eventListeners = {};
     createdElements = [];
@@ -349,17 +350,16 @@ describe("ToolbarPanelHub", () => {
   });
   describe("#renderMessages", () => {
     let getMessagesStub;
     beforeEach(() => {
       getMessagesStub = sandbox.stub();
       instance.init(waitForInitializedStub, {
         getMessages: getMessagesStub,
         dispatch: fakeDispatch,
-        handleUserAction: handleUserActionStub,
       });
     });
     it("should have correct state", async () => {
       const messages = (await PanelTestProvider.getMessages()).filter(
         m => m.template === "whatsnew_panel_message"
       );
 
       getMessagesStub.returns(messages);
@@ -426,17 +426,17 @@ describe("ToolbarPanelHub", () => {
         assert.ok(
           createdCustomElements.find(el =>
             el.classList.includes("whatsNew-message-content")
           )
         );
       }
       // Call the click handler to make coverage happy.
       eventListeners.mouseup();
-      assert.calledOnce(handleUserActionStub);
+      assert.calledOnce(global.SpecialMessageActions.handleAction);
     });
     it("should clear previous messages on 2nd renderMessages()", async () => {
       const messages = (await PanelTestProvider.getMessages()).filter(
         m => m.template === "whatsnew_panel_message"
       );
       const removeStub = sandbox.stub();
       fakeElementById.querySelectorAll.onCall(0).returns([]);
       fakeElementById.querySelectorAll
@@ -544,22 +544,22 @@ describe("ToolbarPanelHub", () => {
       );
       getMessagesStub.returns(messages);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
       const buttonEl = createdElements.find(el => el.tagName === "button");
       const anchorEl = createdElements.find(el => el.tagName === "a");
 
-      assert.notCalled(handleUserActionStub);
+      assert.notCalled(global.SpecialMessageActions.handleAction);
 
       buttonEl.doCommand();
       anchorEl.doCommand();
 
-      assert.calledTwice(handleUserActionStub);
+      assert.calledTwice(global.SpecialMessageActions.handleAction);
     });
     it("should listen for panelhidden and remove the toolbar button", async () => {
       getMessagesStub.returns([]);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
       assert.calledOnce(fakeElementById.addEventListener);
       assert.calledWithExactly(
@@ -778,17 +778,16 @@ describe("ToolbarPanelHub", () => {
       getMessagesStub = sandbox
         .stub()
         .resolves(
           onboardingMsgs.find(msg => msg.template === "protections_panel")
         );
       await instance.init(waitForInitializedStub, {
         dispatch: fakeDispatch,
         getMessages: getMessagesStub,
-        handleUserAction: handleUserActionStub,
       });
     });
     it("should remember it showed", async () => {
       await fakeInsert();
 
       assert.calledWithExactly(
         setBoolPrefStub,
         "browser.protections_panel.infoMessage.seen",
@@ -824,104 +823,108 @@ describe("ToolbarPanelHub", () => {
         sendTelemetryStub,
         fakeWindow,
         "IMPRESSION",
         msg
       );
 
       eventListeners.mouseup();
 
-      assert.calledOnce(handleUserActionStub);
-      assert.calledWithExactly(handleUserActionStub, {
-        target: fakeWindow,
-        data: {
+      assert.calledOnce(global.SpecialMessageActions.handleAction);
+      assert.calledWithExactly(
+        global.SpecialMessageActions.handleAction,
+        {
           type: "OPEN_URL",
           data: {
             args: sinon.match.string,
             where: "tabshifted",
           },
         },
-      });
+        fakeWindow.browser
+      );
     });
     it("should format the url", async () => {
       const stub = sandbox
         .stub(global.Services.urlFormatter, "formatURL")
         .returns("formattedURL");
       const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
       const msg = onboardingMsgs.find(m => m.template === "protections_panel");
 
       await fakeInsert();
 
       eventListeners.mouseup();
 
       assert.calledOnce(stub);
       assert.calledWithExactly(stub, msg.content.cta_url);
-      assert.calledOnce(handleUserActionStub);
-      assert.calledWithExactly(handleUserActionStub, {
-        target: fakeWindow,
-        data: {
+      assert.calledOnce(global.SpecialMessageActions.handleAction);
+      assert.calledWithExactly(
+        global.SpecialMessageActions.handleAction,
+        {
           type: "OPEN_URL",
           data: {
             args: "formattedURL",
             where: "tabshifted",
           },
         },
-      });
+        fakeWindow.browser
+      );
     });
     it("should report format url errors", async () => {
       const stub = sandbox
         .stub(global.Services.urlFormatter, "formatURL")
         .throws();
       const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
       const msg = onboardingMsgs.find(m => m.template === "protections_panel");
       sandbox.spy(global.Cu, "reportError");
 
       await fakeInsert();
 
       eventListeners.mouseup();
 
       assert.calledOnce(stub);
       assert.calledOnce(global.Cu.reportError);
-      assert.calledOnce(handleUserActionStub);
-      assert.calledWithExactly(handleUserActionStub, {
-        target: fakeWindow,
-        data: {
+      assert.calledOnce(global.SpecialMessageActions.handleAction);
+      assert.calledWithExactly(
+        global.SpecialMessageActions.handleAction,
+        {
           type: "OPEN_URL",
           data: {
             args: msg.content.cta_url,
             where: "tabshifted",
           },
         },
-      });
+        fakeWindow.browser
+      );
     });
     it("should open link on click (directly attached to the message)", async () => {
       const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
       const msg = onboardingMsgs.find(m => m.template === "protections_panel");
       getMessagesStub.resolves({
         ...msg,
         content: { ...msg.content, link_text: null },
       });
       await fakeInsert();
 
       eventListeners.mouseup();
 
-      assert.calledOnce(handleUserActionStub);
-      assert.calledWithExactly(handleUserActionStub, {
-        target: fakeWindow,
-        data: {
+      assert.calledOnce(global.SpecialMessageActions.handleAction);
+      assert.calledWithExactly(
+        global.SpecialMessageActions.handleAction,
+        {
           type: "OPEN_URL",
           data: {
             args: sinon.match.string,
             where: "tabshifted",
           },
         },
-      });
+        fakeWindow.browser
+      );
     });
-    it("should handleUserAction from mouseup and keyup", async () => {
+    it("should handle user actions from mouseup and keyup", async () => {
       await fakeInsert();
 
       eventListeners.mouseup();
       eventListeners.keyup({ key: "Enter" });
       eventListeners.keyup({ key: " " });
-      assert.calledThrice(handleUserActionStub);
+      assert.calledThrice(global.SpecialMessageActions.handleAction);
     });
   });
 });
--- a/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
+++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
@@ -13,16 +13,21 @@ const { XPCOMUtils } = ChromeUtils.impor
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   UITour: "resource:///modules/UITour.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   MigrationUtils: "resource:///modules/MigrationUtils.jsm",
 });
 
 const SpecialMessageActions = {
+  // This is overridden by ASRouter.init
+  blockMessageById() {
+    throw new Error("ASRouter not intialized yet");
+  },
+
   /**
    * loadAddonIconInURLBar - load addons-notification icon by displaying
    * box containing addons icon in urlbar. See Bug 1513882
    *
    * @param  {Browser} browser browser element for showing addons icon
    */
   loadAddonIconInURLBar(browser) {
     if (!browser) {
@@ -137,18 +142,18 @@ const SpecialMessageActions = {
       case "INSTALL_ADDON_FROM_URL":
         await this.installAddonFromURL(
           browser,
           action.data.url,
           action.data.telemetrySource
         );
         break;
       case "PIN_CURRENT_TAB":
-        let tab = browser.selectedTab;
-        browser.pinTab(tab);
+        let tab = window.gBrowser.selectedTab;
+        window.gBrowser.pinTab(tab);
         window.ConfirmationHint.show(tab, "pinTab", {
           showDescription: true,
         });
         break;
       case "SHOW_FIREFOX_ACCOUNTS":
         const url = await FxAccounts.config.promiseConnectAccountURI(
           (action.data && action.data.entrypoint) || "snippets"
         );
@@ -166,15 +171,26 @@ const SpecialMessageActions = {
         gProtectionsHandler.showProtectionsPopup({});
         break;
       case "OPEN_PROTECTION_REPORT":
         window.gProtectionsHandler.openProtections();
         break;
       case "OPEN_AWESOME_BAR":
         window.gURLBar.search("");
         break;
+      case "DISABLE_STP_DOORHANGERS":
+        await this.blockMessageById([
+          "SOCIAL_TRACKING_PROTECTION",
+          "FINGERPRINTERS_PROTECTION",
+          "CRYPTOMINERS_PROTECTION",
+        ]);
+        break;
+      case "CANCEL":
+        // A no-op used by CFRs that minimizes the notification but does not
+        // trigger a dismiss or block (it keeps the notification around)
+        break;
       default:
         throw new Error(
           `Special message action with type ${action.type} is unsupported.`
         );
     }
   },
 };
--- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas.js
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas.js
@@ -228,9 +228,19 @@ const SpecialMessageActionSchemas = {
     properties: {
       type: {
         enum: ["SHOW_MIGRATION_WIZARD"],
         type: "string",
       },
     },
     type: "object",
   },
+  CANCEL: {
+    description: "Cancel (dismiss) the CFR doorhanger.",
+    properties: {
+      type: {
+        enum: ["CANCEL"],
+        type: "string",
+      },
+    },
+    type: "object",
+  },
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/messaging-system/test/browser/specialMessageActions/browser_sma_cancel.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_cancel_event() {
+  let error = null;
+  try {
+    await SMATestUtils.executeAndValidateAction({ type: "CANCEL" });
+  } catch (e) {
+    error = e;
+  }
+  ok(!error, "should not throw for CANCEL");
+});