Bug 1688270 - Refactor save to Pocket button messaging system r=gvn,mconley
authorScott <scott.downe@gmail.com>
Wed, 10 Feb 2021 21:53:25 +0000
changeset 566885 66ae0882ef558ea8b94c8855daaa0bc1a8f442b0
parent 566884 d553759a32e24d179a11ecee590e0e9ca7e13597
child 566886 4a5e7a2e86e29fe311924f4653de23199b62070c
push id38191
push userbtara@mozilla.com
push dateThu, 11 Feb 2021 05:02:45 +0000
treeherdermozilla-central@5cbcb80f72bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgvn, mconley
bugs1688270
milestone87.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 1688270 - Refactor save to Pocket button messaging system r=gvn,mconley Differential Revision: https://phabricator.services.mozilla.com/D102348
browser/actors/AboutPocketChild.jsm
browser/actors/AboutPocketParent.jsm
browser/actors/moz.build
browser/components/BrowserGlue.jsm
browser/components/pocket/content/main.js
browser/components/pocket/content/panels/css/saved.css
browser/components/pocket/content/panels/js/messages.js
browser/components/pocket/content/panels/js/saved.js
browser/components/pocket/content/panels/js/sendtomobile.js
browser/components/pocket/content/panels/js/signup.js
browser/components/pocket/content/panels/saved.html
browser/components/pocket/content/pktApi.jsm
browser/components/pocket/test/unit/browser.ini
browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js
toolkit/modules/RemotePageAccessManager.jsm
new file mode 100644
--- /dev/null
+++ b/browser/actors/AboutPocketChild.jsm
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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 EXPORTED_SYMBOLS = ["AboutPocketChild"];
+
+const { RemotePageChild } = ChromeUtils.import(
+  "resource://gre/actors/RemotePageChild.jsm"
+);
+
+class AboutPocketChild extends RemotePageChild {}
new file mode 100644
--- /dev/null
+++ b/browser/actors/AboutPocketParent.jsm
@@ -0,0 +1,187 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["AboutPocketParent"];
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(
+  this,
+  "pktApi",
+  "chrome://pocket/content/pktApi.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyGetter(this, "gPocketBundle", function() {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/pocket.properties"
+  );
+});
+
+class AboutPocketParent extends JSWindowActorParent {
+  sendResponseMessageToPanel(messageId, panelId, payload) {
+    this.sendAsyncMessage(`${messageId}_response_${panelId}`, payload);
+  }
+
+  isPanalAvailable() {
+    return !!this.manager && !this.manager.isClosed;
+  }
+
+  async receiveMessage(message) {
+    switch (message.name) {
+      case "PKT_initL10N": {
+        var strings = {};
+        for (let str of gPocketBundle.getSimpleEnumeration()) {
+          if (str.key in message.data.payload) {
+            strings[str.key] = gPocketBundle.formatStringFromName(
+              str.key,
+              message.data.payload[str.key]
+            );
+          } else {
+            strings[str.key] = str.value;
+          }
+        }
+
+        this.sendResponseMessageToPanel("PKT_initL10N", message.data.panelId, {
+          strings,
+          dir: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
+        });
+        break;
+      }
+      case "PKT_show_signup": {
+        this.browsingContext.topChromeWindow.pktUI.onShowSignup();
+        break;
+      }
+      case "PKT_show_saved": {
+        this.browsingContext.topChromeWindow.pktUI.onShowSaved();
+        break;
+      }
+      case "PKT_close": {
+        this.browsingContext.topChromeWindow.pktUI.closePanel();
+        break;
+      }
+      case "PKT_openTabWithUrl": {
+        this.browsingContext.topChromeWindow.pktUI.onOpenTabWithUrl(
+          message.data.panelId,
+          message.data.payload,
+          this.browsingContext.embedderElement.contentDocument.nodePrincipal,
+          this.browsingContext.embedderElement.contentDocument.csp
+        );
+        break;
+      }
+      case "PKT_openTabWithPocketUrl": {
+        this.browsingContext.topChromeWindow.pktUI.onOpenTabWithPocketUrl(
+          message.data.panelId,
+          message.data.payload,
+          this.browsingContext.embedderElement.contentDocument.nodePrincipal,
+          this.browsingContext.embedderElement.contentDocument.csp
+        );
+        break;
+      }
+      case "PKT_resizePanel": {
+        this.browsingContext.topChromeWindow.pktUI.resizePanel(
+          message.data.payload
+        );
+        this.sendResponseMessageToPanel(
+          "PKT_resizePanel",
+          message.data.panelId
+        );
+        break;
+      }
+      case "PKT_expandSavePanel": {
+        this.browsingContext.topChromeWindow.pktUI.expandSavePanel(
+          message.data.payload
+        );
+        break;
+      }
+      case "PKT_getTags": {
+        this.sendResponseMessageToPanel(
+          "PKT_getTags",
+          message.data.panelId,
+          pktApi.getTags()
+        );
+        break;
+      }
+      case "PKT_getSuggestedTags": {
+        // Ask for suggested tags based on passed url
+        const result = await new Promise(resolve => {
+          pktApi.getSuggestedTagsForURL(message.data.payload.url, {
+            success: data => {
+              var successResponse = {
+                status: "success",
+                value: {
+                  suggestedTags: data.suggested_tags,
+                },
+              };
+              resolve(successResponse);
+            },
+            error: error => resolve({ status: "error", error }),
+          });
+        });
+
+        // If the doorhanger is still open, send the result.
+        if (this.isPanalAvailable()) {
+          this.sendResponseMessageToPanel(
+            "PKT_getSuggestedTags",
+            message.data.panelId,
+            result
+          );
+        }
+        break;
+      }
+      case "PKT_addTags": {
+        // Pass url and array list of tags, add to existing save item accordingly
+        const result = await new Promise(resolve => {
+          pktApi.addTagsToURL(
+            message.data.payload.url,
+            message.data.payload.tags,
+            {
+              success: () => resolve({ status: "success" }),
+              error: error => resolve({ status: "error", error }),
+            }
+          );
+        });
+
+        // If the doorhanger is still open, send the result.
+        if (this.isPanalAvailable()) {
+          this.sendResponseMessageToPanel(
+            "PKT_addTags",
+            message.data.panelId,
+            result
+          );
+        }
+        break;
+      }
+      case "PKT_deleteItem": {
+        // Based on clicking "remove page" CTA, and passed unique item id, remove the item
+        const result = await new Promise(resolve => {
+          pktApi.deleteItem(message.data.payload.itemId, {
+            success: () => {
+              resolve({ status: "success" });
+              this.browsingContext.topChromeWindow.pktUI
+                .getPanelFrame()
+                .setAttribute("itemAdded", "false");
+            },
+            error: error => resolve({ status: "error", error }),
+          });
+        });
+
+        // If the doorhanger is still open, send the result.
+        if (this.isPanalAvailable()) {
+          this.sendResponseMessageToPanel(
+            "PKT_deleteItem",
+            message.data.panelId,
+            result
+          );
+        }
+        break;
+      }
+      case "PKT_log": {
+        console.log(...Object.values(message.data));
+        break;
+      }
+    }
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -30,16 +30,18 @@ with Files("WebRTCChild.jsm"):
 
 FINAL_TARGET_FILES.actors += [
     "AboutNewInstallChild.jsm",
     "AboutNewInstallParent.jsm",
     "AboutNewTabChild.jsm",
     "AboutNewTabParent.jsm",
     "AboutPluginsChild.jsm",
     "AboutPluginsParent.jsm",
+    "AboutPocketChild.jsm",
+    "AboutPocketParent.jsm",
     "AboutPrivateBrowsingChild.jsm",
     "AboutPrivateBrowsingParent.jsm",
     "AboutProtectionsChild.jsm",
     "AboutProtectionsParent.jsm",
     "AboutReaderChild.jsm",
     "AboutReaderParent.jsm",
     "AboutTabCrashedChild.jsm",
     "AboutTabCrashedParent.jsm",
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -256,16 +256,31 @@ let JSWINDOWACTORS = {
       events: {
         DOMWindowCreated: { capture: true },
       },
     },
 
     matches: ["about:plugins"],
   },
 
+  AboutPocket: {
+    parent: {
+      moduleURI: "resource:///actors/AboutPocketParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///actors/AboutPocketChild.jsm",
+
+      events: {
+        DOMWindowCreated: { capture: true },
+      },
+    },
+
+    matches: ["about:pocket-saved*", "about:pocket-signup*"],
+  },
+
   AboutPrivateBrowsing: {
     parent: {
       moduleURI: "resource:///actors/AboutPrivateBrowsingParent.jsm",
     },
     child: {
       moduleURI: "resource:///actors/AboutPrivateBrowsingChild.jsm",
 
       events: {
--- a/browser/components/pocket/content/main.js
+++ b/browser/components/pocket/content/main.js
@@ -71,16 +71,19 @@ const POCKET_ONSAVERECS_LOCLES_PREF = "e
 var pktUI = (function() {
   // -- Initialization (on startup and new windows) -- //
 
   // Init panel id at 0. The first actual panel id will have the number 1 so
   // in case at some point any panel has the id 0 we know there is something
   // wrong
   var _panelId = 0;
 
+  let _titleToSave = "";
+  let _urlToSave = "";
+
   var overflowMenuWidth = 230;
   var overflowMenuHeight = 475;
   var savePanelWidth = 350;
   var savePanelHeights = { collapsed: 153, expanded: 272 };
   var onSaveRecsEnabledPref;
   var onSaveRecsLocalesPref;
 
   function initPrefs() {
@@ -100,19 +103,26 @@ var pktUI = (function() {
   /**
    * Either save or attempt to log the user in
    */
   function tryToSaveCurrentPage() {
     tryToSaveUrl(getCurrentUrl(), getCurrentTitle());
   }
 
   function tryToSaveUrl(url, title) {
+    // Validate input parameter
+    if (typeof url !== "undefined" && url.startsWith("about:reader?url=")) {
+      url = ReaderMode.getOriginalUrl(url);
+    }
+
     // If the user is logged in, go ahead and save the current page
     if (pktApi.isUserLoggedIn()) {
-      saveAndShowConfirmation(url, title);
+      _titleToSave = title;
+      _urlToSave = url;
+      saveAndShowConfirmation();
       return;
     }
 
     // If the user is not logged in, show the logged-out state to prompt them to authenticate
     showSignUp();
   }
 
   // -- Panel UI -- //
@@ -176,29 +186,16 @@ var pktUI = (function() {
           controlvariant +
           "&inoverflowmenu=" +
           inOverflowMenu +
           "&locale=" +
           getUILocale(),
         {
           width: inOverflowMenu ? overflowMenuWidth : 300,
           height: startheight,
-          onShow() {
-            // A successful button click, for logged out users.
-            pktTelemetry.sendStructuredIngestionEvent(
-              pktTelemetry.createPingPayload({
-                events: [
-                  {
-                    action: "click",
-                    source: "save_button",
-                  },
-                ],
-              })
-            );
-          },
         }
       );
     });
   }
 
   /**
    * Get a list of recs for item and show them in the panel.
    */
@@ -214,172 +211,39 @@ var pktUI = (function() {
     ) {
       pktApi.getRecsForItem(item.resolved_id, options);
     }
   }
 
   /**
    * Show the logged-out state / sign-up panel
    */
-  function saveAndShowConfirmation(url, title) {
-    // Validate input parameter
-    if (typeof url !== "undefined" && url.startsWith("about:reader?url=")) {
-      url = ReaderMode.getOriginalUrl(url);
-    }
-
-    var isValidURL =
-      typeof url !== "undefined" &&
-      (url.startsWith("http") || url.startsWith("https"));
-
+  function saveAndShowConfirmation() {
     var inOverflowMenu = isInOverflowMenu();
     var startheight =
-      pktApi.isPremiumUser() && isValidURL
+      pktApi.isPremiumUser() && isValidURL()
         ? savePanelHeights.expanded
         : savePanelHeights.collapsed;
     if (inOverflowMenu) {
       startheight = overflowMenuHeight;
     }
 
     getFirefoxAccountSignedInUser(function(userdata) {
-      var panelId = showPanel(
+      showPanel(
         "about:pocket-saved?pockethost=" +
           Services.prefs.getCharPref("extensions.pocket.site") +
           "&premiumStatus=" +
           (pktApi.isPremiumUser() ? "1" : "0") +
           "&fxasignedin=" +
           (typeof userdata == "object" && userdata !== null ? "1" : "0") +
           "&inoverflowmenu=" +
           inOverflowMenu +
           "&locale=" +
           getUILocale(),
         {
-          onShow() {
-            var saveLinkMessageId = "saveLink";
-            getPanelFrame().setAttribute("itemAdded", "false");
-
-            // Send error message for invalid url
-            if (!isValidURL) {
-              // TODO: Pass key for localized error in error object
-              let error = {
-                message: "Only links can be saved",
-                localizedKey: "onlylinkssaved",
-              };
-              pktUIMessaging.sendErrorMessageToPanel(
-                panelId,
-                saveLinkMessageId,
-                error
-              );
-              return;
-            }
-
-            // Check online state
-            if (!navigator.onLine) {
-              // TODO: Pass key for localized error in error object
-              let error = {
-                message:
-                  "You must be connected to the Internet in order to save to Pocket. Please connect to the Internet and try again.",
-              };
-              pktUIMessaging.sendErrorMessageToPanel(
-                panelId,
-                saveLinkMessageId,
-                error
-              );
-              return;
-            }
-
-            // A successful button click, for logged in users.
-            pktTelemetry.sendStructuredIngestionEvent(
-              pktTelemetry.createPingPayload({
-                events: [
-                  {
-                    action: "click",
-                    source: "save_button",
-                  },
-                ],
-              })
-            );
-
-            // Add url
-            var options = {
-              success(data, request) {
-                var item = data.item;
-                var ho2 = data.ho2;
-                var accountState = data.account_state;
-                var displayName = data.display_name;
-                var successResponse = {
-                  status: "success",
-                  accountState,
-                  displayName,
-                  item,
-                  ho2,
-                };
-                pktUIMessaging.sendMessageToPanel(
-                  panelId,
-                  saveLinkMessageId,
-                  successResponse
-                );
-                getPanelFrame().setAttribute("itemAdded", "true");
-
-                getAndShowRecsForItem(item, {
-                  success(data) {
-                    pktUIMessaging.sendMessageToPanel(
-                      panelId,
-                      "renderItemRecs",
-                      data
-                    );
-                    if (data?.recommendations?.[0]?.experiment) {
-                      const payload = pktTelemetry.createPingPayload({
-                        // This is the ML model used to recommend the story.
-                        // Right now this value is the same for all three items returned together,
-                        // so we can just use the first item's value for all.
-                        model: data.recommendations[0].experiment,
-                        // Create an impression event for each item rendered.
-                        events: data.recommendations.map((item, index) => ({
-                          action: "impression",
-                          position: index,
-                          source: "on_save_recs",
-                        })),
-                      });
-                      // Send view impression ping.
-                      pktTelemetry.sendStructuredIngestionEvent(payload);
-                    }
-                  },
-                });
-              },
-              error(error, request) {
-                // If user is not authorized show singup page
-                if (request.status === 401) {
-                  showSignUp();
-                  return;
-                }
-
-                // If there is no error message in the error use a
-                // complete catch-all
-                var errorMessage =
-                  error.message ||
-                  "There was an error when trying to save to Pocket.";
-                var panelError = { message: errorMessage };
-
-                // Send error message to panel
-                pktUIMessaging.sendErrorMessageToPanel(
-                  panelId,
-                  saveLinkMessageId,
-                  panelError
-                );
-              },
-            };
-
-            // Add title if given
-            if (typeof title !== "undefined") {
-              options.title = title;
-            }
-
-            // Send the link
-            pktApi.addLink(url, options);
-          },
           width: inOverflowMenu ? overflowMenuWidth : savePanelWidth,
           height: startheight,
         }
       );
     });
   }
 
   /**
@@ -389,391 +253,186 @@ var pktUI = (function() {
     // Add new panel id
     _panelId += 1;
     url += "&panelId=" + _panelId;
 
     // We don't have to hide and show the panel again if it's already shown
     // as if the user tries to click again on the toolbar button the overlay
     // will close instead of the button will be clicked
     var iframe = getPanelFrame();
-    options.onShow = options.onShow || (() => {});
-
-    // Register event handlers
-    registerEventMessages();
 
     // Load the iframe
     iframe.setAttribute("src", url);
 
-    if (
-      iframe.contentDocument &&
-      iframe.contentDocument.readyState == "complete" &&
-      iframe.contentDocument.documentURI != "about:blank"
-    ) {
-      options.onShow();
-    } else {
-      // iframe didn't load yet. This seems to always be the case when in
-      // the toolbar panel, but never the case for a subview.
-      // XXX this only being fired when it's a _capturing_ listener!
-      iframe.addEventListener("load", options.onShow, {
-        once: true,
-        capture: true,
-      });
-    }
-
     resizePanel({
       width: options.width,
       height: options.height,
     });
-    return _panelId;
+  }
+
+  function onShowSignup() {
+    // A successful button click, for logged out users.
+    pktTelemetry.sendStructuredIngestionEvent(
+      pktTelemetry.createPingPayload({
+        events: [
+          {
+            action: "click",
+            source: "save_button",
+          },
+        ],
+      })
+    );
+  }
+
+  function onShowSaved() {
+    var saveLinkMessageId = "PKT_saveLink";
+    getPanelFrame().setAttribute("itemAdded", "false");
+
+    // Send error message for invalid url
+    if (!isValidURL()) {
+      // TODO: Pass key for localized error in error object
+      let error = {
+        message: "Only links can be saved",
+        localizedKey: "onlylinkssaved",
+      };
+      pktUIMessaging.sendErrorMessageToPanel(
+        saveLinkMessageId,
+        _panelId,
+        error
+      );
+      return;
+    }
+
+    // Check online state
+    if (!navigator.onLine) {
+      // TODO: Pass key for localized error in error object
+      let error = {
+        message:
+          "You must be connected to the Internet in order to save to Pocket. Please connect to the Internet and try again.",
+      };
+      pktUIMessaging.sendErrorMessageToPanel(
+        saveLinkMessageId,
+        _panelId,
+        error
+      );
+      return;
+    }
+
+    // A successful button click, for logged in users.
+    pktTelemetry.sendStructuredIngestionEvent(
+      pktTelemetry.createPingPayload({
+        events: [
+          {
+            action: "click",
+            source: "save_button",
+          },
+        ],
+      })
+    );
+
+    // Add url
+    var options = {
+      success(data, request) {
+        var item = data.item;
+        var ho2 = data.ho2;
+        var accountState = data.account_state;
+        var displayName = data.display_name;
+        var successResponse = {
+          status: "success",
+          accountState,
+          displayName,
+          item,
+          ho2,
+        };
+        pktUIMessaging.sendMessageToPanel(
+          saveLinkMessageId,
+          _panelId,
+          successResponse
+        );
+        getPanelFrame().setAttribute("itemAdded", "true");
+
+        getAndShowRecsForItem(item, {
+          success(data) {
+            pktUIMessaging.sendMessageToPanel(
+              "PKT_renderItemRecs",
+              _panelId,
+              data
+            );
+            if (data?.recommendations?.[0]?.experiment) {
+              const payload = pktTelemetry.createPingPayload({
+                // This is the ML model used to recommend the story.
+                // Right now this value is the same for all three items returned together,
+                // so we can just use the first item's value for all.
+                model: data.recommendations[0].experiment,
+                // Create an impression event for each item rendered.
+                events: data.recommendations.map((item, index) => ({
+                  action: "impression",
+                  position: index,
+                  source: "on_save_recs",
+                })),
+              });
+              // Send view impression ping.
+              pktTelemetry.sendStructuredIngestionEvent(payload);
+            }
+          },
+        });
+      },
+      error(error, request) {
+        // If user is not authorized show singup page
+        if (request.status === 401) {
+          showSignUp();
+          return;
+        }
+
+        // If there is no error message in the error use a
+        // complete catch-all
+        var errorMessage =
+          error.message || "There was an error when trying to save to Pocket.";
+        var panelError = { message: errorMessage };
+
+        // Send error message to panel
+        pktUIMessaging.sendErrorMessageToPanel(
+          saveLinkMessageId,
+          _panelId,
+          panelError
+        );
+      },
+    };
+
+    // Add title if given
+    if (typeof _titleToSave !== "undefined") {
+      options.title = _titleToSave;
+    }
+
+    // Send the link
+    pktApi.addLink(_urlToSave, options);
   }
 
   /**
    * Resize the panel
    * options = {
    *  width: ,
    *  height: ,
-   *  animate [default false]
    * }
    */
   function resizePanel(options) {
     var iframe = getPanelFrame();
 
     // Set an explicit size, panel will adapt.
     iframe.style.width = options.width + "px";
     iframe.style.height = options.height + "px";
   }
 
-  /**
-   * Register all of the messages needed for the panels
-   */
-  function registerEventMessages() {
-    var iframe = getPanelFrame();
-
-    // Only register the messages once
-    var didInitAttributeKey = "did_init";
-    var didInitMessageListener = iframe.getAttribute(didInitAttributeKey);
-    if (
-      typeof didInitMessageListener !== "undefined" &&
-      didInitMessageListener == 1
-    ) {
-      return;
+  function expandSavePanel(data) {
+    if (!isInOverflowMenu()) {
+      resizePanel({
+        width: savePanelWidth,
+        height: savePanelHeights.expanded,
+      });
     }
-    iframe.setAttribute(didInitAttributeKey, 1);
-
-    // When the panel is displayed it generated an event called
-    // "show": we will listen for that event and when it happens,
-    // send our own "show" event to the panel's script, so the
-    // script can prepare the panel for display.
-    var _showMessageId = "show";
-    pktUIMessaging.addMessageListener(iframe, _showMessageId, function(
-      panelId,
-      data
-    ) {
-      // Let panel know that it is ready
-      pktUIMessaging.sendMessageToPanel(panelId, _showMessageId);
-    });
-
-    // Open a new tab with a given url
-    var _openTabWithUrlMessageId = "openTabWithUrl";
-    pktUIMessaging.addMessageListener(
-      iframe,
-      _openTabWithUrlMessageId,
-      function(panelId, data, contentPrincipal, csp) {
-        try {
-          urlSecurityCheck(
-            data.url,
-            contentPrincipal,
-            Services.scriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
-          );
-        } catch (ex) {
-          return;
-        }
-
-        // We don't track every click, only clicks with a known source.
-        if (data.source) {
-          const payload = pktTelemetry.createPingPayload({
-            events: [
-              {
-                action: "click",
-                source: data.source,
-              },
-            ],
-          });
-          // Send click event ping.
-          pktTelemetry.sendStructuredIngestionEvent(payload);
-        }
-
-        var url = data.url;
-        openTabWithUrl(url, contentPrincipal, csp);
-        pktUIMessaging.sendResponseMessageToPanel(
-          panelId,
-          _openTabWithUrlMessageId,
-          url
-        );
-      }
-    );
-
-    // Open a new tab with a Pocket story url
-    var _openTabWithPocketUrlMessageId = "openTabWithPocketUrl";
-    pktUIMessaging.addMessageListener(
-      iframe,
-      _openTabWithPocketUrlMessageId,
-      function(panelId, data, contentPrincipal, csp) {
-        try {
-          urlSecurityCheck(
-            data.url,
-            contentPrincipal,
-            Services.scriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
-          );
-        } catch (ex) {
-          return;
-        }
-
-        const { url, position, model } = data;
-        // Check to see if we need to and can fire valid telemetry.
-        if (model && (position || position === 0)) {
-          const payload = pktTelemetry.createPingPayload({
-            model,
-            events: [
-              {
-                action: "click",
-                position,
-                source: "on_save_recs",
-              },
-            ],
-          });
-          // Send click event ping.
-          pktTelemetry.sendStructuredIngestionEvent(payload);
-        }
-
-        openTabWithUrl(url, contentPrincipal, csp);
-      }
-    );
-
-    // Close the panel
-    var _closeMessageId = "close";
-    pktUIMessaging.addMessageListener(iframe, _closeMessageId, function(
-      panelId,
-      data
-    ) {
-      getPanel().hidePopup();
-    });
-
-    // Send the current url to the panel
-    var _getCurrentURLMessageId = "getCurrentURL";
-    pktUIMessaging.addMessageListener(iframe, _getCurrentURLMessageId, function(
-      panelId,
-      data
-    ) {
-      pktUIMessaging.sendResponseMessageToPanel(
-        panelId,
-        _getCurrentURLMessageId,
-        getCurrentUrl()
-      );
-    });
-
-    // Get article info
-    var _getArticleInfoMessageId = "getArticleInfo";
-    pktUIMessaging.addMessageListener(
-      iframe,
-      _getArticleInfoMessageId,
-      function(panelId, data) {
-        pktApi.getArticleInfo(getCurrentUrl(), {
-          success(res, req) {
-            pktUIMessaging.sendResponseMessageToPanel(
-              panelId,
-              _getArticleInfoMessageId,
-              res
-            );
-          },
-          error(err, req) {
-            err.fallback_title = getCurrentTitle();
-            err.fallback_domain = new URL(getCurrentUrl()).hostname;
-            pktUIMessaging.sendResponseMessageToPanel(
-              panelId,
-              _getArticleInfoMessageId,
-              err
-            );
-          },
-        });
-      }
-    );
-
-    // getMobileDownload
-    var _getMobileDownloadMessageId = "getMobileDownload";
-    pktUIMessaging.addMessageListener(
-      iframe,
-      _getMobileDownloadMessageId,
-      function(panelId, data) {
-        pktApi.getMobileDownload({
-          success(res, req) {
-            pktUIMessaging.sendResponseMessageToPanel(
-              panelId,
-              _getMobileDownloadMessageId,
-              res
-            );
-          },
-          error(err, req) {
-            pktUIMessaging.sendResponseMessageToPanel(
-              panelId,
-              _getMobileDownloadMessageId,
-              err
-            );
-          },
-        });
-      }
-    );
-
-    var _resizePanelMessageId = "resizePanel";
-    pktUIMessaging.addMessageListener(iframe, _resizePanelMessageId, function(
-      panelId,
-      data
-    ) {
-      resizePanel(data);
-    });
-
-    // Callback post initialization to tell background script that panel is "ready" for communication.
-    pktUIMessaging.addMessageListener(iframe, "listenerReady", function(
-      panelId,
-      data
-    ) {});
-
-    pktUIMessaging.addMessageListener(iframe, "expandSavePanel", function(
-      panelId,
-      data
-    ) {
-      if (!isInOverflowMenu()) {
-        resizePanel({
-          width: savePanelWidth,
-          height: savePanelHeights.expanded,
-        });
-      }
-    });
-
-    // Ask for recently accessed/used tags for auto complete
-    var _getTagsMessageId = "getTags";
-    pktUIMessaging.addMessageListener(iframe, _getTagsMessageId, function(
-      panelId,
-      data
-    ) {
-      pktApi.getTags(function(tags, usedTags) {
-        pktUIMessaging.sendResponseMessageToPanel(panelId, _getTagsMessageId, {
-          tags,
-          usedTags,
-        });
-      });
-    });
-
-    // Ask for suggested tags based on passed url
-    var _getSuggestedTagsMessageId = "getSuggestedTags";
-    pktUIMessaging.addMessageListener(
-      iframe,
-      _getSuggestedTagsMessageId,
-      function(panelId, data) {
-        pktApi.getSuggestedTagsForURL(data.url, {
-          success(data, response) {
-            var suggestedTags = data.suggested_tags;
-            var successResponse = {
-              status: "success",
-              value: {
-                suggestedTags,
-              },
-            };
-            pktUIMessaging.sendResponseMessageToPanel(
-              panelId,
-              _getSuggestedTagsMessageId,
-              successResponse
-            );
-          },
-          error(error, response) {
-            pktUIMessaging.sendErrorResponseMessageToPanel(
-              panelId,
-              _getSuggestedTagsMessageId,
-              error
-            );
-          },
-        });
-      }
-    );
-
-    // Pass url and array list of tags, add to existing save item accordingly
-    var _addTagsMessageId = "addTags";
-    pktUIMessaging.addMessageListener(iframe, _addTagsMessageId, function(
-      panelId,
-      data
-    ) {
-      pktApi.addTagsToURL(data.url, data.tags, {
-        success(data, response) {
-          var successResponse = { status: "success" };
-          pktUIMessaging.sendResponseMessageToPanel(
-            panelId,
-            _addTagsMessageId,
-            successResponse
-          );
-        },
-        error(error, response) {
-          pktUIMessaging.sendErrorResponseMessageToPanel(
-            panelId,
-            _addTagsMessageId,
-            error
-          );
-        },
-      });
-    });
-
-    // Based on clicking "remove page" CTA, and passed unique item id, remove the item
-    var _deleteItemMessageId = "deleteItem";
-    pktUIMessaging.addMessageListener(iframe, _deleteItemMessageId, function(
-      panelId,
-      data
-    ) {
-      pktApi.deleteItem(data.itemId, {
-        success(data, response) {
-          var successResponse = { status: "success" };
-          pktUIMessaging.sendResponseMessageToPanel(
-            panelId,
-            _deleteItemMessageId,
-            successResponse
-          );
-          getPanelFrame().setAttribute("itemAdded", "false");
-        },
-        error(error, response) {
-          pktUIMessaging.sendErrorResponseMessageToPanel(
-            panelId,
-            _deleteItemMessageId,
-            error
-          );
-        },
-      });
-    });
-
-    var _initL10NMessageId = "initL10N";
-    pktUIMessaging.addMessageListener(iframe, _initL10NMessageId, function(
-      panelId,
-      data
-    ) {
-      var strings = {};
-      var bundle = Services.strings.createBundle(
-        "chrome://browser/locale/pocket.properties"
-      );
-      for (let str of bundle.getSimpleEnumeration()) {
-        if (str.key in data) {
-          strings[str.key] = bundle.formatStringFromName(
-            str.key,
-            data[str.key]
-          );
-        } else {
-          strings[str.key] = str.value;
-        }
-      }
-      pktUIMessaging.sendResponseMessageToPanel(panelId, _initL10NMessageId, {
-        strings,
-        dir: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
-      });
-    });
   }
 
   // -- Browser Navigation -- //
 
   /**
    * Open a new tab with a given url and notify the iframe panel that it was opened
    */
 
@@ -810,26 +469,92 @@ var pktUI = (function() {
 
     // If there were no non-private windows opened already.
     recentWindow.openWebLinkIn(url, "window", {
       triggeringPrincipal: aTriggeringPrincipal,
       csp: aCsp,
     });
   }
 
+  // Open a new tab with a given url
+  function onOpenTabWithUrl(panelId, data, contentPrincipal, csp) {
+    try {
+      urlSecurityCheck(
+        data.url,
+        contentPrincipal,
+        Services.scriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
+      );
+    } catch (ex) {
+      return;
+    }
+
+    // We don't track every click, only clicks with a known source.
+    if (data.source) {
+      const payload = pktTelemetry.createPingPayload({
+        events: [
+          {
+            action: "click",
+            source: data.source,
+          },
+        ],
+      });
+      // Send click event ping.
+      pktTelemetry.sendStructuredIngestionEvent(payload);
+    }
+
+    var url = data.url;
+    openTabWithUrl(url, contentPrincipal, csp);
+  }
+
+  // Open a new tab with a Pocket story url
+  function onOpenTabWithPocketUrl(panelId, data, contentPrincipal, csp) {
+    try {
+      urlSecurityCheck(
+        data.url,
+        contentPrincipal,
+        Services.scriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
+      );
+    } catch (ex) {
+      return;
+    }
+
+    const { url, position, model } = data;
+    // Check to see if we need to and can fire valid telemetry.
+    if (model && (position || position === 0)) {
+      const payload = pktTelemetry.createPingPayload({
+        model,
+        events: [
+          {
+            action: "click",
+            position,
+            source: "on_save_recs",
+          },
+        ],
+      });
+      // Send click event ping.
+      pktTelemetry.sendStructuredIngestionEvent(payload);
+    }
+
+    openTabWithUrl(url, contentPrincipal, csp);
+  }
+
   // -- Helper Functions -- //
 
   function getCurrentUrl() {
     return gBrowser.currentURI.spec;
   }
 
   function getCurrentTitle() {
     return gBrowser.contentTitle;
   }
 
+  function closePanel() {
+    getPanel().hidePopup();
+  }
+
   function getPanel() {
     var frame = getPanelFrame();
     var panel = frame;
     while (panel && panel.localName != "panel") {
       panel = panel.parentNode;
     }
     return panel;
   }
@@ -843,16 +568,23 @@ var pktUI = (function() {
   function getPanelFrame() {
     return photonPageActionPanelFrame;
   }
 
   function isInOverflowMenu() {
     return false;
   }
 
+  function isValidURL() {
+    return (
+      typeof _urlToSave !== "undefined" &&
+      (_urlToSave.startsWith("http") || _urlToSave.startsWith("https"))
+    );
+  }
+
   function getFirefoxAccountSignedInUser(callback) {
     fxAccounts
       .getSignedInUser()
       .then(userData => {
         callback(userData);
       })
       .then(null, error => {
         callback();
@@ -867,106 +599,60 @@ var pktUI = (function() {
    * Public functions
    */
   return {
     setPhotonPageActionPanelFrame,
     getPanelFrame,
     initPrefs,
 
     openTabWithUrl,
+    onOpenTabWithUrl,
+    onOpenTabWithPocketUrl,
+    onShowSaved,
+    onShowSignup,
 
     getAndShowRecsForItem,
     tryToSaveUrl,
     tryToSaveCurrentPage,
+    resizePanel,
+    expandSavePanel,
+    closePanel,
   };
 })();
 
 // -- Communication to Background -- //
-// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Interaction_between_privileged_and_non-privileged_pages
 var pktUIMessaging = (function() {
   /**
-   * Prefix message id for message listening
-   */
-  function prefixedMessageId(messageId) {
-    return "PKT_" + messageId;
-  }
-
-  /**
-   * Register a listener and callback for a specific messageId
-   */
-  function addMessageListener(iframe, messageId, callback) {
-    iframe.addEventListener(
-      prefixedMessageId(messageId),
-      function(e) {
-        var nodePrincipal = e.target.nodePrincipal;
-        // ignore to ensure we do not pick up other events in the browser
-        if (!nodePrincipal || !nodePrincipal.spec.startsWith("about:pocket")) {
-          return;
-        }
-
-        // Pass in information to callback
-        var payload = JSON.parse(e.target.getAttribute("payload"))[0];
-        var panelId = payload.panelId;
-        var data = payload.data;
-        var csp = e.target.ownerDocument.csp;
-        callback(panelId, data, nodePrincipal, csp);
-
-        // Cleanup the element
-        e.target.remove();
-      },
-      false,
-      true
-    );
-  }
-
-  /**
    * Send a message to the panel's iframe
    */
-  function sendMessageToPanel(panelId, messageId, payload) {
+  function sendMessageToPanel(messageId, panelId, payload) {
     if (!isPanelIdValid(panelId)) {
       return;
     }
 
     var panelFrame = pktUI.getPanelFrame();
     if (!isPocketPanelFrameValid(panelFrame)) {
       return;
     }
 
-    var doc = panelFrame.contentWindow.document;
-    var documentElement = doc.documentElement;
+    const aboutPocketActor = panelFrame?.browsingContext?.currentWindowGlobal?.getActor(
+      "AboutPocket"
+    );
 
     // Send message to panel
-    var panelMessageId = prefixedMessageId(panelId + "_" + messageId);
-
-    var AnswerEvt = doc.createElement("PKTMessage");
-    AnswerEvt.setAttribute("payload", JSON.stringify([payload]));
-    documentElement.appendChild(AnswerEvt);
-
-    var event = doc.createEvent("HTMLEvents");
-    event.initEvent(panelMessageId, true, false);
-    AnswerEvt.dispatchEvent(event);
-  }
-
-  function sendResponseMessageToPanel(panelId, messageId, payload) {
-    var responseMessageId = messageId + "Response";
-    sendMessageToPanel(panelId, responseMessageId, payload);
+    aboutPocketActor?.sendAsyncMessage(`${messageId}_${panelId}`, payload);
   }
 
   /**
    * Helper function to package an error object and send it to the panel
    * iframe as a message response
    */
-  function sendErrorMessageToPanel(panelId, messageId, error) {
+  function sendErrorMessageToPanel(messageId, panelId, error) {
     var errorResponse = { status: "error", error };
-    sendMessageToPanel(panelId, messageId, errorResponse);
-  }
-
-  function sendErrorResponseMessageToPanel(panelId, messageId, error) {
-    var errorResponse = { status: "error", error };
-    sendResponseMessageToPanel(panelId, messageId, errorResponse);
+    sendMessageToPanel(messageId, panelId, errorResponse);
   }
 
   /**
    * Validation
    */
 
   function isPanelIdValid(panelId) {
     // First check if panelId has a valid value > 0. We set the panelId to
@@ -1013,15 +699,12 @@ var pktUIMessaging = (function() {
 
     return true;
   }
 
   /**
    * Public
    */
   return {
-    addMessageListener,
     sendMessageToPanel,
-    sendResponseMessageToPanel,
     sendErrorMessageToPanel,
-    sendErrorResponseMessageToPanel,
   };
 })();
--- a/browser/components/pocket/content/panels/css/saved.css
+++ b/browser/components/pocket/content/panels/css/saved.css
@@ -914,17 +914,17 @@ button {
 .pkt_ext_subshell hr {
     display: none;
 }
 
 .recs_enabled .pkt_ext_subshell hr {
     display: block;
     border: 0;
     border-top: 1px solid #D7D7DB;
-    margin: 0 -16px 0;
+    margin: 0;
 }
 
 .pkt_ext_item_recs {
     text-align: start;
     margin: 0 auto;
     padding: 0.25em 1em;
 }
 
--- a/browser/components/pocket/content/panels/js/messages.js
+++ b/browser/components/pocket/content/panels/js/messages.js
@@ -1,79 +1,60 @@
-// Documentation of methods used here are at:
-// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Interaction_between_privileged_and_non-privileged_pages
+/* global RPMRemoveMessageListener:false, RPMAddMessageListener:false, RPMSendAsyncMessage:false */
 
 var pktPanelMessaging = (function() {
   function panelIdFromURL(url) {
     var panelId = url.match(/panelId=([\w|\d|\.]*)&?/);
     if (panelId && panelId.length > 1) {
       return panelId[1];
     }
 
     return 0;
   }
 
-  function prefixedMessageId(messageId) {
-    return "PKT_" + messageId;
-  }
-
-  function panelPrefixedMessageId(panelId, messageId) {
-    return prefixedMessageId(panelId + "_" + messageId);
+  function removeMessageListener(messageId, panelId, callback) {
+    RPMRemoveMessageListener(`${messageId}_${panelId}`, callback);
   }
 
-  function addMessageListener(panelId, messageId, callback) {
-    document.addEventListener(
-      panelPrefixedMessageId(panelId, messageId),
-      function(e) {
-        callback(JSON.parse(e.target.getAttribute("payload"))[0]);
-
-        // TODO: Figure out why e.target.parentNode is null
-        // e.target.parentNode.removeChild(e.target);
-      }
-    );
+  function addMessageListener(messageId, panelId, callback = () => {}) {
+    RPMAddMessageListener(`${messageId}_${panelId}`, callback);
   }
 
-  function removeMessageListener(panelId, messageId, callback) {
-    document.removeEventListener(
-      panelPrefixedMessageId(panelId, messageId),
-      callback
-    );
-  }
-
-  function sendMessage(panelId, messageId, payload, callback) {
+  function sendMessage(messageId, panelId, payload = {}, callback) {
     // Payload needs to be an object in format:
-    // { panelId: panelId, data: {} }
+    // { panelId: panelId, payload: {} }
     var messagePayload = {
       panelId,
-      data: payload || {},
+      payload,
     };
 
-    // Create a callback to listen for a response
     if (callback) {
-      var messageResponseId = messageId + "Response";
+      // If we expect something back, we use RPMSendAsyncMessage and not RPMSendQuery.
+      // Even though RPMSendQuery returns something, our frame could be closed at any moment,
+      // and we don't want to close a RPMSendQuery promise loop unexpectedly.
+      // So instead we setup a response event.
+      const responseMessageId = `${messageId}_response`;
       var responseListener = function(responsePayload) {
         callback(responsePayload);
-        removeMessageListener(panelId, messageResponseId, responseListener);
+        removeMessageListener(responseMessageId, panelId, responseListener);
       };
 
-      addMessageListener(panelId, messageResponseId, responseListener);
+      addMessageListener(responseMessageId, panelId, responseListener);
     }
 
     // Send message
-    var element = document.createElement("PKTMessageFromPanelElement");
-    element.setAttribute("payload", JSON.stringify([messagePayload]));
-    document.documentElement.appendChild(element);
+    RPMSendAsyncMessage(messageId, messagePayload);
+  }
 
-    var evt = document.createEvent("Events");
-    evt.initEvent(prefixedMessageId(messageId), true, false);
-    element.dispatchEvent(evt);
+  function log() {
+    RPMSendAsyncMessage("PKT_log", arguments);
   }
 
   /**
    * Public functions
    */
   return {
+    log,
     panelIdFromURL,
     addMessageListener,
-    removeMessageListener,
     sendMessage,
   };
 })();
--- a/browser/components/pocket/content/panels/js/saved.js
+++ b/browser/components/pocket/content/panels/js/saved.js
@@ -28,69 +28,70 @@ var PKT_SAVED_OVERLAY = function(options
   this.userTags = [];
   this.cxt_suggested_available = 0;
   this.cxt_entered = 0;
   this.cxt_suggested = 0;
   this.cxt_removed = 0;
   this.justaddedsuggested = false;
   this.fxasignedin = false;
   this.premiumDetailsAdded = false;
-  this.ho2 = false;
   this.fillTagContainer = function(tags, container, tagclass) {
     container.children().remove();
     for (var i = 0; i < tags.length; i++) {
       var newtag = $('<li><a href="#" class="token_tag"></a></li>');
       newtag.find("a").text(tags[i]);
       newtag.addClass(tagclass);
       container.append(newtag);
       this.cxt_suggested_available++;
     }
   };
   this.fillUserTags = function() {
-    thePKT_SAVED.sendMessage("getTags", {}, function(resp) {
-      if (typeof resp == "object" && typeof resp.tags == "object") {
-        myself.userTags = resp.tags;
+    thePKT_SAVED.sendMessage("PKT_getTags", {}, function(resp) {
+      const { data } = resp;
+      if (typeof data == "object" && typeof data.tags == "object") {
+        myself.userTags = data.tags;
       }
     });
   };
   this.fillSuggestedTags = function() {
     if (!$(".pkt_ext_suggestedtag_detail").length) {
       myself.suggestedTagsLoaded = true;
       myself.startCloseTimer();
       return;
     }
 
     $(".pkt_ext_subshell").show();
 
     thePKT_SAVED.sendMessage(
-      "getSuggestedTags",
+      "PKT_getSuggestedTags",
       {
         url: myself.savedUrl,
       },
       function(resp) {
+        const { data } = resp;
         $(".pkt_ext_suggestedtag_detail").removeClass(
           "pkt_ext_suggestedtag_detail_loading"
         );
-        if (resp.status == "success") {
+        if (data.status == "success") {
           var newtags = [];
-          for (var i = 0; i < resp.value.suggestedTags.length; i++) {
-            newtags.push(resp.value.suggestedTags[i].tag);
+          for (let i = 0; i < data.value.suggestedTags.length; i++) {
+            newtags.push(data.value.suggestedTags[i].tag);
           }
           myself.suggestedTagsLoaded = true;
           if (!myself.mouseInside) {
             myself.startCloseTimer();
           }
           myself.fillTagContainer(
             newtags,
             $(".pkt_ext_suggestedtag_detail ul"),
             "token_suggestedtag"
           );
-        } else if (resp.status == "error") {
+        } else if (data.status == "error") {
           var msg = $('<p class="suggestedtag_msg">');
-          msg.text(resp.error.message);
+          msg.text(data.error.message);
           $(".pkt_ext_suggestedtag_detail").append(msg);
           this.suggestedTagsLoaded = true;
           if (!myself.mouseInside) {
             myself.startCloseTimer();
           }
         }
       }
     );
@@ -134,17 +135,17 @@ var PKT_SAVED_OVERLAY = function(options
   this.stopCloseTimer = function() {
     if (myself.preventCloseTimerCancel) {
       return;
     }
     clearTimeout(myself.autocloseTimer);
   };
   this.closePopup = function() {
     myself.stopCloseTimer();
-    thePKT_SAVED.sendMessage("close");
+    thePKT_SAVED.sendMessage("PKT_close");
   };
   this.checkValidTagSubmit = function() {
     var inputlength = $.trim(
       $(".pkt_ext_tag_input_wrapper")
         .find(".token-input-input-token")
         .children("input")
         .val()
     ).length;
@@ -303,20 +304,18 @@ var PKT_SAVED_OVERLAY = function(options
       },
       onDelete() {
         myself.checkValidTagSubmit();
         this.changestamp = Date.now();
         myself.showActiveTags();
         myself.checkPlaceholderStatus();
       },
       onShowDropdown() {
-        if (myself.ho2 !== "show_prompt_preview") {
-          $(".pkt_ext_item_recs").hide(); // hide recs when tag input begins
-          thePKT_SAVED.sendMessage("expandSavePanel");
-        }
+        $(".pkt_ext_item_recs").hide(); // hide recs when tag input begins
+        thePKT_SAVED.sendMessage("PKT_expandSavePanel");
       },
     });
     $("body").on("keydown", function(e) {
       var key = e.keyCode || e.which;
       if (key == 8) {
         var selected = $(".token-input-selected-token");
         if (selected.length) {
           e.preventDefault();
@@ -383,28 +382,29 @@ var PKT_SAVED_OVERLAY = function(options
             .text()
         );
         if (text.length) {
           originaltags.push(text);
         }
       });
 
       thePKT_SAVED.sendMessage(
-        "addTags",
+        "PKT_addTags",
         {
           url: myself.savedUrl,
           tags: originaltags,
         },
         function(resp) {
-          if (resp.status == "success") {
+          const { data } = resp;
+          if (data.status == "success") {
             myself.showStateFinalMsg(myself.dictJSON.tagssaved);
-          } else if (resp.status == "error") {
+          } else if (data.status == "error") {
             $(".pkt_ext_edit_msg")
               .addClass("pkt_ext_edit_msg_error pkt_ext_edit_msg_active")
-              .text(resp.error.message);
+              .text(data.error.message);
           }
         }
       );
     });
   };
   this.initRemovePageInput = function() {
     $(".pkt_ext_removeitem").click(function(e) {
       $(".pkt_ext_subshell").hide();
@@ -415,41 +415,41 @@ var PKT_SAVED_OVERLAY = function(options
       if ($(this).hasClass("pkt_ext_removeitem")) {
         e.preventDefault();
         myself.disableInput();
         $(".pkt_ext_containersaved")
           .find(".pkt_ext_detail h2")
           .text(myself.dictJSON.processingremove);
 
         thePKT_SAVED.sendMessage(
-          "deleteItem",
+          "PKT_deleteItem",
           {
             itemId: myself.savedItemId,
           },
           function(resp) {
-            if (resp.status == "success") {
+            const { data } = resp;
+            if (data.status == "success") {
               myself.showStateFinalMsg(myself.dictJSON.pageremoved);
-            } else if (resp.status == "error") {
+            } else if (data.status == "error") {
               $(".pkt_ext_edit_msg")
                 .addClass("pkt_ext_edit_msg_error pkt_ext_edit_msg_active")
-                .text(resp.error.message);
+                .text(data.error.message);
             }
           }
         );
       }
     });
   };
   this.initOpenListInput = function() {
     $(".pkt_ext_openpocket").click(function(e) {
       e.preventDefault();
-      thePKT_SAVED.sendMessage("openTabWithUrl", {
+      thePKT_SAVED.sendMessage("PKT_openTabWithUrl", {
         url: $(this).attr("href"),
         activate: true,
       });
-      myself.closePopup();
     });
   };
   this.showTagsError = function(msg) {
     $(".pkt_ext_edit_msg")
       .addClass("pkt_ext_edit_msg_error pkt_ext_edit_msg_active")
       .text(msg);
     $(".pkt_ext_tag_detail").addClass("pkt_ext_tag_error");
   };
@@ -504,37 +504,25 @@ var PKT_SAVED_OVERLAY = function(options
     if (typeof initobj.item == "object") {
       this.savedItemId = initobj.item.item_id;
       this.savedUrl = initobj.item.given_url;
     }
     $(".pkt_ext_containersaved")
       .addClass("pkt_ext_container_detailactive")
       .removeClass("pkt_ext_container_finalstate");
 
-    if (
-      initobj.ho2 &&
-      initobj.ho2 != "control" &&
-      !initobj.accountState.has_mobile &&
-      !myself.savedUrl.includes("getpocket.com")
-    ) {
-      myself.createSendToMobilePanel(initobj.ho2, initobj.displayName);
-      myself.ho2 = initobj.ho2;
-    }
-
     myself.fillUserTags();
     if (myself.suggestedTagsLoaded) {
       myself.startCloseTimer();
     } else {
       myself.fillSuggestedTags();
     }
   };
   this.renderItemRecs = function(data) {
     if (data?.recommendations?.length) {
-      $("body").addClass("recs_enabled");
-      $(".pkt_ext_subshell").show();
       // URL encode and append raw image source for Thumbor + CDN
       data.recommendations = data.recommendations.map(rec => {
         // Using array notation because there is a key titled `1` (`images` is an object)
         let rawSource = rec?.item?.top_image_url || rec?.item?.images["1"]?.src;
 
         // Append UTM to rec URLs (leave existing query strings intact)
         if (rec?.item?.resolved_url && !rec.item.resolved_url.match(/\?/)) {
           rec.item.resolved_url = `${rec.item.resolved_url}?utm_source=pocket-ff-recs`;
@@ -546,40 +534,50 @@ var PKT_SAVED_OVERLAY = function(options
 
         return rec;
       });
 
       // This is the ML model used to recommend the story.
       // Right now this value is the same for all three items returned together,
       // so we can just use the first item's value for all.
       const model = data.recommendations[0].experiment;
-      $(".pkt_ext_item_recs").append(Handlebars.templates.item_recs(data));
+      const renderedRecs = Handlebars.templates.item_recs(data);
 
       // Resize popover to accomodate recs:
-      thePKT_SAVED.sendMessage("resizePanel", {
-        width: 350,
-        height: this.premiumStatus ? 535 : 424, // TODO: Dynamic height based on number of recs
-      });
+      thePKT_SAVED.sendMessage(
+        "PKT_resizePanel",
+        {
+          width: 350,
+          height: this.premiumStatus ? 535 : 424, // TODO: Dynamic height based on number of recs
+        },
+        () => {
+          // Recs are fixed positioned at the bottom of the container,
+          // so if we show recs before height is updated,
+          // it shows recs above main saved message. sendMessage is not sync.
+          // This can cause a flash of recs at the top of the doorhanger as the page loads.
+          // So instead we update and show the recs only when the height is done.
+          // This way we don't get the flash of recs.
+          $("body").addClass("recs_enabled");
+          $(".pkt_ext_subshell").show();
+          $(".pkt_ext_item_recs").append(renderedRecs);
+        }
+      );
 
       $(".pkt_ext_item_recs_link").click(function(e) {
         e.preventDefault();
         const url = $(this).attr("href");
         const position = $(".pkt_ext_item_recs_link").index(this);
-        thePKT_SAVED.sendMessage("openTabWithPocketUrl", {
+        thePKT_SAVED.sendMessage("PKT_openTabWithPocketUrl", {
           url,
           model,
           position,
         });
-        myself.closePopup();
       });
     }
   };
-  this.createSendToMobilePanel = function(ho2, displayName) {
-    PKT_SENDTOMOBILE.create(ho2, displayName, myself.premiumDetailsAdded);
-  };
   this.sanitizeText = function(s) {
     var sanitizeMap = {
       "&": "&amp;",
       "<": "&lt;",
       ">": "&gt;",
       '"': "&quot;",
       "'": "&#39;",
     };
@@ -683,21 +681,21 @@ PKT_SAVED.prototype = {
     }
     this.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
     this.overlay = new PKT_SAVED_OVERLAY();
 
     this.inited = true;
   },
 
   addMessageListener(messageId, callback) {
-    pktPanelMessaging.addMessageListener(this.panelId, messageId, callback);
+    pktPanelMessaging.addMessageListener(messageId, this.panelId, callback);
   },
 
   sendMessage(messageId, payload, callback) {
-    pktPanelMessaging.sendMessage(this.panelId, messageId, payload, callback);
+    pktPanelMessaging.sendMessage(messageId, this.panelId, payload, callback);
   },
 
   create() {
     var myself = this;
     var url = window.location.href.match(/premiumStatus=([\w|\d|\.]*)&?/);
     if (url && url.length > 1) {
       myself.overlay.premiumStatus = url[1] == "1";
     }
@@ -717,72 +715,75 @@ PKT_SAVED.prototype = {
     }
     var locale = window.location.href.match(/locale=([\w|\.]*)&?/);
     if (locale && locale.length > 1) {
       myself.overlay.locale = locale[1].toLowerCase();
     }
 
     myself.overlay.create();
 
-    // tell back end we're ready
-    thePKT_SAVED.sendMessage("show");
-
     // wait confirmation of save before flipping to final saved state
-    thePKT_SAVED.addMessageListener("saveLink", function(resp) {
-      if (resp.status == "error") {
-        if (typeof resp.error == "object") {
-          if (resp.error.localizedKey) {
+    thePKT_SAVED.addMessageListener("PKT_saveLink", function(resp) {
+      const { data } = resp;
+      if (data.status == "error") {
+        if (typeof data.error == "object") {
+          if (data.error.localizedKey) {
             myself.overlay.showStateError(
               myself.overlay.dictJSON.pagenotsaved,
-              myself.overlay.dictJSON[resp.error.localizedKey]
+              myself.overlay.dictJSON[data.error.localizedKey]
             );
           } else {
             myself.overlay.showStateError(
               myself.overlay.dictJSON.pagenotsaved,
-              resp.error.message
+              data.error.message
             );
           }
         } else {
           myself.overlay.showStateError(
             myself.overlay.dictJSON.pagenotsaved,
             myself.overlay.dictJSON.errorgeneric
           );
         }
         return;
       }
 
-      myself.overlay.showStateSaved(resp);
+      myself.overlay.showStateSaved(data);
     });
 
-    thePKT_SAVED.addMessageListener("renderItemRecs", function(payload) {
-      myself.overlay.renderItemRecs(payload);
+    thePKT_SAVED.addMessageListener("PKT_renderItemRecs", function(resp) {
+      const { data } = resp;
+      myself.overlay.renderItemRecs(data);
     });
+
+    // tell back end we're ready
+    thePKT_SAVED.sendMessage("PKT_show_saved");
   },
 };
 
 $(function() {
   if (!window.thePKT_SAVED) {
     var thePKT_SAVED = new PKT_SAVED();
     /* global thePKT_SAVED */
     window.thePKT_SAVED = thePKT_SAVED;
     thePKT_SAVED.init();
   }
 
   var pocketHost = thePKT_SAVED.overlay.pockethost;
   // send an async message to get string data
   thePKT_SAVED.sendMessage(
-    "initL10N",
+    "PKT_initL10N",
     {
       tos: [
         "https://" + pocketHost + "/tos?s=ffi&t=tos&tv=panel_tryit",
         "https://" +
           pocketHost +
           "/privacy?s=ffi&t=privacypolicy&tv=panel_tryit",
       ],
     },
     function(resp) {
-      window.pocketStrings = resp.strings;
+      const { data } = resp;
+      window.pocketStrings = data.strings;
       // Set the writing system direction
-      document.documentElement.setAttribute("dir", resp.dir);
+      document.documentElement.setAttribute("dir", data.dir);
       window.thePKT_SAVED.create();
     }
   );
 });
deleted file mode 100644
--- a/browser/components/pocket/content/panels/js/sendtomobile.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* global $:false, Handlebars:false, thePKT_SAVED:false, */
-
-var PKT_SENDTOMOBILE = (function() {
-  var width = 350;
-  var ctaHeight = 200; // iframe height
-  var confirmHeight = 275;
-  var premDetailsHeight = 110;
-  var email = null;
-
-  function _initPanelOneClicks() {
-    $("#pkt_ext_sendtomobile_button").click(function() {
-      $("#pkt_ext_sendtomobile_button").replaceWith(
-        '<div class="pkt_ext_loadingspinner"><div></div></div>'
-      );
-
-      thePKT_SAVED.sendMessage("getMobileDownload", {}, function(data) {
-        if (data.status == 1) {
-          $("body").html(Handlebars.templates.ho2_download({ email }));
-          thePKT_SAVED.sendMessage("resizePanel", {
-            width,
-            height: confirmHeight,
-          });
-        } else {
-          $("body").html(Handlebars.templates.ho2_download_error({ email }));
-          thePKT_SAVED.sendMessage("resizePanel", {
-            width,
-            height: confirmHeight,
-          });
-        }
-      });
-    });
-  }
-
-  function create(ho2, displayName, adjustHeight) {
-    email = displayName;
-    $("body").addClass("pkt_ext_ho2_experiment");
-    var height = adjustHeight ? premDetailsHeight : 0;
-
-    // Show "Send to your phone" CTA
-    height += ctaHeight;
-    $("body").append(Handlebars.templates.ho2_sharebutton());
-    thePKT_SAVED.sendMessage("resizePanel", { width, height });
-
-    _initPanelOneClicks();
-  }
-
-  /**
-   * Public functions
-   */
-  return {
-    create,
-  };
-})();
--- a/browser/components/pocket/content/panels/js/signup.js
+++ b/browser/components/pocket/content/panels/js/signup.js
@@ -2,17 +2,16 @@
 /* import-globals-from messages.js */
 
 /*
 PKT_SIGNUP_OVERLAY is the view itself and contains all of the methods to manipute the overlay and messaging.
 It does not contain any logic for saving or communication with the extension or server.
 */
 
 var PKT_SIGNUP_OVERLAY = function(options) {
-  var myself = this;
   this.inited = false;
   this.active = false;
   this.delayedStateSaved = false;
   this.wrapper = null;
   this.variant = window.___PKT__SIGNUP_VARIANT;
   this.tagline = window.___PKT__SIGNUP_TAGLINE || "";
   this.preventCloseTimerCancel = false;
   this.translations = {};
@@ -23,22 +22,21 @@ var PKT_SIGNUP_OVERLAY = function(option
   this.inoverflowmenu = false;
   this.controlvariant;
   this.pockethost = "getpocket.com";
   this.loggedOutVariant = "control";
   this.dictJSON = {};
   this.initCloseTabEvents = function() {
     function clickHelper(e, linkData) {
       e.preventDefault();
-      thePKT_SIGNUP.sendMessage("openTabWithUrl", {
+      thePKT_SIGNUP.sendMessage("PKT_openTabWithUrl", {
         url: linkData.url,
         activate: true,
         source: linkData.source || "",
       });
-      myself.closePopup();
     }
     $(".pkt_ext_learnmore").click(function(e) {
       clickHelper(e, {
         source: "learn_more",
         url: $(this).attr("href"),
       });
     });
     $(".signup-btn-firefox").click(function(e) {
@@ -62,19 +60,16 @@ var PKT_SIGNUP_OVERLAY = function(option
     // A generic click we don't do anything special for.
     // Was used for an experiment, possibly not needed anymore.
     $(".signup-btn-tryitnow").click(function(e) {
       clickHelper(e, {
         url: $(this).attr("href"),
       });
     });
   };
-  this.closePopup = function() {
-    thePKT_SIGNUP.sendMessage("close");
-  };
   this.sanitizeText = function(s) {
     var sanitizeMap = {
       "&": "&amp;",
       "<": "&lt;",
       ">": "&gt;",
       '"': "&quot;",
       "'": "&#39;",
     };
@@ -183,19 +178,16 @@ PKT_SIGNUP_OVERLAY.prototype = {
 
       $("body").append(
         Handlebars.templates[loggedOutVariantTemplate || variants.control](
           this.dictJSON
         )
       );
     }
 
-    // tell background we're ready
-    thePKT_SIGNUP.sendMessage("show");
-
     // close events
     this.initCloseTabEvents();
   },
 };
 
 // Layer between Bookmarklet and Extensions
 var PKT_SIGNUP = function() {};
 
@@ -205,52 +197,49 @@ PKT_SIGNUP.prototype = {
       return;
     }
     this.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
     this.overlay = new PKT_SIGNUP_OVERLAY();
 
     this.inited = true;
   },
 
-  addMessageListener(messageId, callback) {
-    pktPanelMessaging.addMessageListener(this.panelId, messageId, callback);
-  },
-
   sendMessage(messageId, payload, callback) {
-    pktPanelMessaging.sendMessage(this.panelId, messageId, payload, callback);
+    pktPanelMessaging.sendMessage(messageId, this.panelId, payload, callback);
   },
 
   create() {
     this.overlay.create();
 
     // tell back end we're ready
-    thePKT_SIGNUP.sendMessage("show");
+    thePKT_SIGNUP.sendMessage("PKT_show_signup");
   },
 };
 
 $(function() {
   if (!window.thePKT_SIGNUP) {
     var thePKT_SIGNUP = new PKT_SIGNUP();
     /* global thePKT_SIGNUP */
     window.thePKT_SIGNUP = thePKT_SIGNUP;
     thePKT_SIGNUP.init();
   }
 
   var pocketHost = thePKT_SIGNUP.overlay.pockethost;
   // send an async message to get string data
   thePKT_SIGNUP.sendMessage(
-    "initL10N",
+    "PKT_initL10N",
     {
       tos: [
         "https://" + pocketHost + "/tos?s=ffi&t=tos&tv=panel_tryit",
         "https://" +
           pocketHost +
           "/privacy?s=ffi&t=privacypolicy&tv=panel_tryit",
       ],
     },
     function(resp) {
-      window.pocketStrings = resp.strings;
+      const { data } = resp;
+      window.pocketStrings = data.strings;
       // Set the writing system direction
-      document.documentElement.setAttribute("dir", resp.dir);
+      document.documentElement.setAttribute("dir", data.dir);
       window.thePKT_SIGNUP.create();
     }
   );
 });
--- a/browser/components/pocket/content/panels/saved.html
+++ b/browser/components/pocket/content/panels/saved.html
@@ -10,12 +10,11 @@
         <link rel="stylesheet" href="css/sendtomobile.css">
     </head>
     <body class="pkt_ext_containersaved" aria-live="polite">
         <script src="js/vendor/jquery-2.1.1.min.js"></script>
         <script src="js/vendor/handlebars.runtime.js"></script>
         <script src="js/vendor/jquery.tokeninput.min.js"></script>
         <script src="js/tmpl.js"></script>
         <script src="js/messages.js"></script>
-        <script src="js/sendtomobile.js"></script>
         <script src="js/saved.js"></script>
     </body>
 </html>
--- a/browser/components/pocket/content/pktApi.jsm
+++ b/browser/components/pocket/content/pktApi.jsm
@@ -606,21 +606,19 @@ var pktApi = (function() {
       }
     };
 
     // Execute the action
     return sendAction(action, options);
   }
 
   /**
-   * Get all cached tags and used tags within the callback
-   * @param {function(Array, Array, Boolean)} callback
-   *                           Function with tags and used tags as parameter.
+   * Return all cached tags and used tags.
    */
-  function getTags(callback) {
+  function getTags() {
     var tagsFromSettings = function() {
       var tagsJSON = getSetting("tags");
       if (typeof tagsJSON !== "undefined") {
         return JSON.parse(tagsJSON);
       }
       return [];
     };
 
@@ -650,21 +648,20 @@ var pktApi = (function() {
 
         // Reverse to set the last recent used tags to the front
         usedTags.reverse();
       }
 
       return usedTags;
     };
 
-    if (callback) {
-      var tags = tagsFromSettings();
-      var usedTags = sortedUsedTagsFromSettings();
-      callback(tags, usedTags);
-    }
+    return {
+      tags: tagsFromSettings(),
+      usedTags: sortedUsedTagsFromSettings(),
+    };
   }
 
   /**
    * Fetch suggested tags for a given item id
    * @param  {string} itemId Item id of
    * @param  {Object | undefined} options Can provide an actionInfo object
    *                                      with further data to send to the API.
    *                                      Can have success and error callbacks
--- a/browser/components/pocket/test/unit/browser.ini
+++ b/browser/components/pocket/test/unit/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_pocket_main.js]
 [browser_pocket_pktTelemetry.js]
+[browser_pocket_AboutPocketParent.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js
@@ -0,0 +1,481 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { AboutPocketParent } = ChromeUtils.import(
+  "resource:///actors/AboutPocketParent.jsm"
+);
+const { pktApi } = ChromeUtils.import("chrome://pocket/content/pktApi.jsm");
+let aboutPocketParent;
+
+function test_runner(test) {
+  let testTask = async () => {
+    // Before each
+    const sandbox = sinon.createSandbox();
+    aboutPocketParent = new AboutPocketParent();
+
+    const manager = {
+      isClosed: false,
+    };
+    const browsingContext = {
+      topChromeWindow: {
+        pktUI: {
+          onShowSignup: sandbox.spy(),
+          onShowSaved: sandbox.spy(),
+          closePanel: sandbox.spy(),
+          onOpenTabWithUrl: sandbox.spy(),
+          onOpenTabWithPocketUrl: sandbox.spy(),
+          resizePanel: sandbox.spy(),
+          expandSavePanel: sandbox.spy(),
+          getPanelFrame: () => ({ setAttribute: () => {} }),
+        },
+      },
+      embedderElement: {
+        contentDocument: {
+          nodePrincipal: "nodePrincipal",
+          csp: "csp",
+        },
+      },
+    };
+
+    sandbox.stub(aboutPocketParent, "manager").get(() => manager);
+    sandbox
+      .stub(aboutPocketParent, "browsingContext")
+      .get(() => browsingContext);
+
+    try {
+      await test({ sandbox });
+    } finally {
+      // After each
+      sandbox.restore();
+    }
+  };
+
+  // Copy the name of the test function to identify the test
+  Object.defineProperty(testTask, "name", { value: test.name });
+  add_task(testTask);
+}
+
+test_runner(async function test_AboutPocketParent_sendResponseMessageToPanel({
+  sandbox,
+}) {
+  const sendAsyncMessage = sandbox.stub(aboutPocketParent, "sendAsyncMessage");
+
+  aboutPocketParent.sendResponseMessageToPanel("PKT_testMessage", "1", {
+    foo: 1,
+  });
+
+  const { args } = sendAsyncMessage.firstCall;
+
+  Assert.ok(
+    sendAsyncMessage.calledOnce,
+    "Should fire sendAsyncMessage once with sendResponseMessageToPanel"
+  );
+  Assert.deepEqual(
+    args,
+    ["PKT_testMessage_response_1", { foo: 1 }],
+    "Should fire sendAsyncMessage with proper args from sendResponseMessageToPanel"
+  );
+});
+
+test_runner(async function test_AboutPocketParent_receiveMessage_PKT_initL10N({
+  sandbox,
+}) {
+  const sendResponseMessageToPanel = sandbox.stub(
+    aboutPocketParent,
+    "sendResponseMessageToPanel"
+  );
+
+  await aboutPocketParent.receiveMessage({
+    name: "PKT_initL10N",
+    data: {
+      payload: {
+        tos: ["https://foo.com", "https://bar.com"],
+      },
+      panelId: 1,
+    },
+  });
+
+  const { args } = sendResponseMessageToPanel.firstCall;
+
+  Assert.ok(
+    sendResponseMessageToPanel.calledOnce,
+    "Should fire sendResponseMessageToPanel once with PKT_initL10N"
+  );
+  Assert.equal(
+    args[0],
+    "PKT_initL10N",
+    "Should fire sendResponseMessageToPanel with proper PKT_initL10N messageId"
+  );
+  Assert.equal(
+    args[1],
+    1,
+    "Should fire sendResponseMessageToPanel with proper PKT_initL10N panelId"
+  );
+  Assert.equal(
+    args[2].dir,
+    "ltr",
+    "Should fire sendResponseMessageToPanel with proper PKT_initL10N payload dir"
+  );
+  Assert.ok(
+    args[2].strings,
+    "Should fire sendResponseMessageToPanel with PKT_initL10N payload strings"
+  );
+  Assert.equal(
+    args[2].strings.tos,
+    'By continuing, you agree to Pocket’s <a href="https://foo.com" target="_blank">Terms of Service</a> and <a href="https://bar.com" target="_blank">Privacy Policy</a>',
+    "Should fire sendResponseMessageToPanel with PKT_initL10N payload strings tos"
+  );
+});
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_show_signup({
+    sandbox,
+  }) {
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_show_signup",
+    });
+
+    const {
+      onShowSignup,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+
+    Assert.ok(
+      onShowSignup.calledOnce,
+      "Should fire onShowSignup once with PKT_show_signup"
+    );
+  }
+);
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_show_saved({
+    sandbox,
+  }) {
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_show_saved",
+    });
+
+    const {
+      onShowSaved,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+
+    Assert.ok(
+      onShowSaved.calledOnce,
+      "Should fire onShowSaved once with PKT_show_saved"
+    );
+  }
+);
+
+test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close({
+  sandbox,
+}) {
+  await aboutPocketParent.receiveMessage({
+    name: "PKT_close",
+  });
+
+  const {
+    closePanel,
+  } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+
+  Assert.ok(
+    closePanel.calledOnce,
+    "Should fire closePanel once with PKT_close"
+  );
+});
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_openTabWithUrl({
+    sandbox,
+  }) {
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_openTabWithUrl",
+      data: {
+        payload: { foo: 1 },
+        panelId: 1,
+      },
+    });
+
+    const {
+      onOpenTabWithUrl,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+    const { args } = onOpenTabWithUrl.firstCall;
+
+    Assert.ok(
+      onOpenTabWithUrl.calledOnce,
+      "Should fire onOpenTabWithUrl once with PKT_openTabWithUrl"
+    );
+    Assert.deepEqual(
+      args,
+      [1, { foo: 1 }, "nodePrincipal", "csp"],
+      "Should fire onOpenTabWithUrl with proper args from PKT_openTabWithUrl"
+    );
+  }
+);
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_openTabWithPocketUrl({
+    sandbox,
+  }) {
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_openTabWithPocketUrl",
+      data: {
+        payload: { foo: 1 },
+        panelId: 1,
+      },
+    });
+
+    const {
+      onOpenTabWithPocketUrl,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+    const { args } = onOpenTabWithPocketUrl.firstCall;
+
+    Assert.ok(
+      onOpenTabWithPocketUrl.calledOnce,
+      "Should fire onOpenTabWithPocketUrl once with PKT_openTabWithPocketUrl"
+    );
+    Assert.deepEqual(
+      args,
+      [1, { foo: 1 }, "nodePrincipal", "csp"],
+      "Should fire onOpenTabWithPocketUrl with proper args from PKT_openTabWithPocketUrl"
+    );
+  }
+);
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_resizePanel({
+    sandbox,
+  }) {
+    const sendResponseMessageToPanel = sandbox.stub(
+      aboutPocketParent,
+      "sendResponseMessageToPanel"
+    );
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_resizePanel",
+      data: {
+        payload: { foo: 1 },
+        panelId: 1,
+      },
+    });
+
+    const {
+      resizePanel,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+    const { args } = resizePanel.firstCall;
+
+    Assert.ok(
+      resizePanel.calledOnce,
+      "Should fire resizePanel once with PKT_resizePanel"
+    );
+    Assert.deepEqual(
+      args,
+      [{ foo: 1 }],
+      "Should fire resizePanel with proper args from PKT_resizePanel"
+    );
+    Assert.ok(
+      sendResponseMessageToPanel.calledOnce,
+      "Should fire sendResponseMessageToPanel once with PKT_resizePanel"
+    );
+    Assert.deepEqual(
+      sendResponseMessageToPanel.firstCall.args,
+      ["PKT_resizePanel", 1],
+      "Should fire sendResponseMessageToPanel with proper args from PKT_resizePanel"
+    );
+  }
+);
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_expandSavePanel({
+    sandbox,
+  }) {
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_expandSavePanel",
+      data: {
+        payload: { foo: 1 },
+      },
+    });
+
+    const {
+      expandSavePanel,
+    } = aboutPocketParent.browsingContext.topChromeWindow.pktUI;
+    const { args } = expandSavePanel.firstCall;
+
+    Assert.ok(
+      expandSavePanel.calledOnce,
+      "Should fire expandSavePanel once with PKT_expandSavePanel"
+    );
+    Assert.deepEqual(
+      args,
+      [{ foo: 1 }],
+      "Should fire expandSavePanel with proper args from PKT_expandSavePanel"
+    );
+  }
+);
+
+test_runner(async function test_AboutPocketParent_receiveMessage_PKT_getTags({
+  sandbox,
+}) {
+  const sendResponseMessageToPanel = sandbox.stub(
+    aboutPocketParent,
+    "sendResponseMessageToPanel"
+  );
+  await aboutPocketParent.receiveMessage({
+    name: "PKT_getTags",
+    data: {
+      panelId: 1,
+    },
+  });
+  Assert.ok(
+    sendResponseMessageToPanel.calledOnce,
+    "Should fire sendResponseMessageToPanel once with PKT_getTags"
+  );
+  Assert.deepEqual(
+    sendResponseMessageToPanel.firstCall.args,
+    ["PKT_getTags", 1, { tags: [], usedTags: [] }],
+    "Should fire sendResponseMessageToPanel with proper args from PKT_getTags"
+  );
+});
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_getSuggestedTags({
+    sandbox,
+  }) {
+    const sendResponseMessageToPanel = sandbox.stub(
+      aboutPocketParent,
+      "sendResponseMessageToPanel"
+    );
+    sandbox.stub(pktApi, "getSuggestedTagsForURL").callsFake((url, options) => {
+      options.success({ suggested_tags: "foo" });
+    });
+
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_getSuggestedTags",
+      data: {
+        panelId: 1,
+        payload: { url: "https://foo.com" },
+      },
+    });
+
+    Assert.ok(
+      pktApi.getSuggestedTagsForURL.calledOnce,
+      "Should fire getSuggestedTagsForURL once with PKT_getSuggestedTags"
+    );
+    Assert.equal(
+      pktApi.getSuggestedTagsForURL.firstCall.args[0],
+      "https://foo.com",
+      "Should fire getSuggestedTagsForURL with proper url from PKT_getSuggestedTags"
+    );
+    Assert.ok(
+      sendResponseMessageToPanel.calledOnce,
+      "Should fire sendResponseMessageToPanel once with PKT_getSuggestedTags"
+    );
+    Assert.deepEqual(
+      sendResponseMessageToPanel.firstCall.args,
+      [
+        "PKT_getSuggestedTags",
+        1,
+        {
+          status: "success",
+          value: { suggestedTags: "foo" },
+        },
+      ],
+      "Should fire sendResponseMessageToPanel with proper args from PKT_getSuggestedTags"
+    );
+  }
+);
+
+test_runner(async function test_AboutPocketParent_receiveMessage_PKT_addTags({
+  sandbox,
+}) {
+  const sendResponseMessageToPanel = sandbox.stub(
+    aboutPocketParent,
+    "sendResponseMessageToPanel"
+  );
+  sandbox.stub(pktApi, "addTagsToURL").callsFake((url, tags, options) => {
+    options.success();
+  });
+
+  await aboutPocketParent.receiveMessage({
+    name: "PKT_addTags",
+    data: {
+      panelId: 1,
+      payload: { url: "https://foo.com", tags: "tags" },
+    },
+  });
+
+  Assert.ok(
+    pktApi.addTagsToURL.calledOnce,
+    "Should fire addTagsToURL once with PKT_addTags"
+  );
+  Assert.equal(
+    pktApi.addTagsToURL.firstCall.args[0],
+    "https://foo.com",
+    "Should fire addTagsToURL with proper url from PKT_addTags"
+  );
+  Assert.equal(
+    pktApi.addTagsToURL.firstCall.args[1],
+    "tags",
+    "Should fire addTagsToURL with proper tags from PKT_addTags"
+  );
+  Assert.ok(
+    sendResponseMessageToPanel.calledOnce,
+    "Should fire sendResponseMessageToPanel once with PKT_addTags"
+  );
+  Assert.deepEqual(
+    sendResponseMessageToPanel.firstCall.args,
+    [
+      "PKT_addTags",
+      1,
+      {
+        status: "success",
+      },
+    ],
+    "Should fire sendResponseMessageToPanel with proper args from PKT_addTags"
+  );
+});
+
+test_runner(
+  async function test_AboutPocketParent_receiveMessage_PKT_deleteItem({
+    sandbox,
+  }) {
+    const sendResponseMessageToPanel = sandbox.stub(
+      aboutPocketParent,
+      "sendResponseMessageToPanel"
+    );
+    sandbox.stub(pktApi, "deleteItem").callsFake((itemId, options) => {
+      options.success();
+    });
+
+    await aboutPocketParent.receiveMessage({
+      name: "PKT_deleteItem",
+      data: {
+        panelId: 1,
+        payload: { itemId: "itemId" },
+      },
+    });
+
+    Assert.ok(
+      pktApi.deleteItem.calledOnce,
+      "Should fire deleteItem once with PKT_deleteItem"
+    );
+    Assert.equal(
+      pktApi.deleteItem.firstCall.args[0],
+      "itemId",
+      "Should fire deleteItem with proper itemId from PKT_deleteItem"
+    );
+    Assert.ok(
+      sendResponseMessageToPanel.calledOnce,
+      "Should fire sendResponseMessageToPanel once with PKT_deleteItem"
+    );
+    Assert.deepEqual(
+      sendResponseMessageToPanel.firstCall.args,
+      [
+        "PKT_deleteItem",
+        1,
+        {
+          status: "success",
+        },
+      ],
+      "Should fire sendResponseMessageToPanel with proper args from PKT_deleteItem"
+    );
+  }
+);
--- a/toolkit/modules/RemotePageAccessManager.jsm
+++ b/toolkit/modules/RemotePageAccessManager.jsm
@@ -99,16 +99,26 @@ let RemotePageAccessManager = {
     },
     "about:newinstall": {
       RPMGetUpdateChannel: ["*"],
       RPMGetFxAccountsEndpoint: ["*"],
     },
     "about:plugins": {
       RPMSendQuery: ["RequestPlugins"],
     },
+    "about:pocket-saved": {
+      RPMSendAsyncMessage: ["*"],
+      RPMAddMessageListener: ["*"],
+      RPMRemoveMessageListener: ["*"],
+    },
+    "about:pocket-signup": {
+      RPMSendAsyncMessage: ["*"],
+      RPMAddMessageListener: ["*"],
+      RPMRemoveMessageListener: ["*"],
+    },
     "about:privatebrowsing": {
       RPMSendAsyncMessage: [
         "OpenPrivateWindow",
         "SearchBannerDismissed",
         "OpenSearchPreferences",
         "SearchHandoff",
       ],
       RPMSendQuery: ["ShouldShowSearchBanner", "ShouldShowVPNPromo"],