Bug 1569300 - Special Monitor Snippet. r=k88hudson, a=RyanVM
authorEd Lee <edilee@mozilla.com>
Tue, 13 Aug 2019 22:01:35 +0000
changeset 541948 194b831613666fa13fb024c934197ee3e776e368
parent 541947 feeaf170fca669a675eff2cb16bd56b0a329b508
child 541949 aa1646f240499fee211a3600fca1249cd01466f0
push id11790
push userryanvm@gmail.com
push dateThu, 15 Aug 2019 15:15:21 +0000
treeherdermozilla-beta@194b83161366 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson, RyanVM
bugs1569300, 1569597, 1571435, 1571441, 1570026, 46821636, 1572463
milestone69.0
Bug 1569300 - Special Monitor Snippet. r=k88hudson, a=RyanVM Includes 5 activity-stream commits: 5118b78c Bug 1569597 - Updated schema & template, added test snippet with corr… (#5208) 5ea99ba1 Bug 1571435 - Overrode the .active class for snippets with buttons, to prevent a double highlight. (#5224) 70b63230 Bug 1571441 - Fixed snippet width and block button position on smaller screens (#5227) 9e70a342 Bug 1570026 - Part 1. Add Monitor action for snippets 46821636 Bug 1572463 - Fixes for the Special Monitor Snippet (#5235) Differential Revision: https://phabricator.services.mozilla.com/D41857
browser/components/newtab/common/Actions.jsm
browser/components/newtab/content-src/asrouter/asrouter-content.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
browser/components/newtab/content-src/components/Base/_Base.scss
browser/components/newtab/content-src/components/Search/_Search.scss
browser/components/newtab/css/activity-stream-linux.css
browser/components/newtab/css/activity-stream-mac.css
browser/components/newtab/css/activity-stream-windows.css
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
--- a/browser/components/newtab/common/Actions.jsm
+++ b/browser/components/newtab/common/Actions.jsm
@@ -152,16 +152,17 @@ for (const type of [
   "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",
 ]) {
   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 } : {};
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -1,9 +1,17 @@
-import { actionCreators as ac } from "common/Actions.jsm";
+/* 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 { 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 { OnboardingMessage } from "./templates/OnboardingMessage/OnboardingMessage";
 import React from "react";
 import ReactDOM from "react-dom";
@@ -100,27 +108,71 @@ function shouldSendImpressionOnUpdate(ne
 
 export class ASRouterUISurface extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
+    this.onUserAction = this.onUserAction.bind(this);
     this.state = { message: {}, bundle: {} };
     if (props.document) {
       this.headerPortal = props.document.getElementById(
         "header-asrouter-container"
       );
       this.footerPortal = props.document.getElementById(
         "footer-asrouter-container"
       );
     }
   }
 
+  async fetchFlowParams(params = {}) {
+    let result = {};
+    const { fxaEndpoint, dispatch } = this.props;
+    if (!fxaEndpoint) {
+      const err =
+        "Tried to fetch flow params before fxaEndpoint pref was ready";
+      console.error(err); // eslint-disable-line no-console
+    }
+
+    try {
+      const urlObj = new URL(fxaEndpoint);
+      urlObj.pathname = "metrics-flow";
+      Object.keys(params).forEach(key => {
+        urlObj.searchParams.append(key, params[key]);
+      });
+      const response = await fetch(urlObj.toString(), { credentials: "omit" });
+      if (response.status === 200) {
+        const { deviceId, flowId, flowBeginTime } = await response.json();
+        result = { deviceId, flowId, flowBeginTime };
+      } else {
+        console.error("Non-200 response", response); // eslint-disable-line no-console
+        dispatch(
+          ac.OnlyToMain({
+            type: at.TELEMETRY_UNDESIRED_EVENT,
+            data: {
+              event: "FXA_METRICS_FETCH_ERROR",
+              value: response.status,
+            },
+          })
+        );
+      }
+    } catch (error) {
+      console.error(error);
+      dispatch(
+        ac.OnlyToMain({
+          type: at.TELEMETRY_UNDESIRED_EVENT,
+          data: { event: "FXA_METRICS_ERROR" },
+        })
+      );
+    }
+    return result;
+  }
+
   sendUserActionTelemetry(extraProps = {}) {
     const { message, bundle } = this.state;
     if (!message && !extraProps.message_id) {
       throw new Error(`You must provide a message_id for bundled messages`);
     }
     // snippets_user_event, onboarding_user_event
     const eventType = `${message.provider || bundle.provider}_user_event`;
     ASRouterUtils.sendTelemetry({
@@ -261,16 +313,42 @@ export class ASRouterUISurface extends R
       });
     }
   }
 
   componentWillUnmount() {
     ASRouterUtils.removeListener(this.onMessageFromParent);
   }
 
+  async getMonitorUrl({ url, flowRequestParams = {} }) {
+    const flowValues = await this.fetchFlowParams(flowRequestParams);
+
+    // Note that flowParams are actually added dynamically on the page
+    const urlObj = new URL(url);
+    ["deviceId", "flowId", "flowBeginTime"].forEach(key => {
+      if (key in flowValues) {
+        urlObj.searchParams.append(key, flowValues[key]);
+      }
+    });
+
+    return urlObj.toString();
+  }
+
+  async onUserAction(action) {
+    switch (action.type) {
+      // This needs to be handled locally because its
+      case ra.ENABLE_FIREFOX_MONITOR:
+        const url = await this.getMonitorUrl(action.data.args);
+        ASRouterUtils.executeAction({ type: ra.OPEN_URL, data: { args: url } });
+        break;
+      default:
+        ASRouterUtils.executeAction(action);
+    }
+  }
+
   renderSnippets() {
     if (
       this.state.bundle.template === "onboarding" ||
       this.state.message.template === "fxa_overlay" ||
       this.state.message.template === "return_to_amo_overlay" ||
       this.state.message.template === "trailhead"
     ) {
       return null;
@@ -288,17 +366,17 @@ export class ASRouterUISurface extends R
         document={this.props.document}
       >
         <LocalizationProvider bundles={generateBundles(content)}>
           <SnippetComponent
             {...this.state.message}
             UISurface="NEWTAB_FOOTER_BAR"
             onBlock={this.onBlockById(this.state.message.id)}
             onDismiss={this.onDismissById(this.state.message.id)}
-            onAction={ASRouterUtils.executeAction}
+            onAction={this.onUserAction}
             sendClick={this.sendClick}
             sendUserActionTelemetry={this.sendUserActionTelemetry}
           />
         </LocalizationProvider>
       </ImpressionsWrapper>
     );
   }
 
@@ -388,17 +466,19 @@ export class ASRouterUISurface extends R
       message.template
     );
     const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(
       message.template
     );
 
     return shouldRenderBelowSearch ? (
       // Render special below search snippets in place;
-      <div className="below-search-snippet">{this.renderSnippets()}</div>
+      <div className="below-search-snippet-wrapper">
+        {this.renderSnippets()}
+      </div>
     ) : (
       // For onboarding, regular snippets etc. we should render
       // everything in our footer container.
       ReactDOM.createPortal(
         <>
           {this.renderPreviewBanner()}
           {this.renderTrailhead()}
           {this.renderFirstRunOverlay()}
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -1,58 +1,129 @@
 import React from "react";
+import { Button } from "../../components/Button/Button";
 import { RichText } from "../../components/RichText/RichText";
 import { safeURI } from "../../template-utils";
 import { SnippetBase } from "../../components/SnippetBase/SnippetBase";
 
 const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
 // Alt text placeholder in case the prop from the server isn't available
 const ICON_ALT_TEXT = "";
 
 export class SimpleBelowSearchSnippet extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onButtonClick = this.onButtonClick.bind(this);
+  }
+
   renderText() {
     const { props } = this;
-    return (
+    return props.content.text ? (
       <RichText
         text={props.content.text}
         customElements={this.props.customElements}
         localization_id="text"
         links={props.content.links}
         sendClick={props.sendClick}
       />
+    ) : null;
+  }
+
+  renderTitle() {
+    const { title } = this.props.content;
+    return title ? (
+      <h3 className={"title title-inline"}>
+        {title}
+        <br />
+      </h3>
+    ) : null;
+  }
+
+  async onButtonClick() {
+    if (this.props.provider !== "preview") {
+      this.props.sendUserActionTelemetry({
+        event: "CLICK_BUTTON",
+        id: this.props.UISurface,
+      });
+    }
+    const { button_url } = this.props.content;
+    // If button_url is defined handle it as OPEN_URL action
+    const type = this.props.content.button_action || (button_url && "OPEN_URL");
+    await this.props.onAction({
+      type,
+      data: { args: this.props.content.button_action_args || button_url },
+    });
+    if (!this.props.content.do_not_autoblock) {
+      this.props.onBlock();
+    }
+  }
+
+  _shouldRenderButton() {
+    return (
+      this.props.content.button_action ||
+      this.props.onButtonClick ||
+      this.props.content.button_url
+    );
+  }
+
+  renderButton() {
+    const { props } = this;
+    if (!this._shouldRenderButton()) {
+      return null;
+    }
+
+    return (
+      <Button
+        onClick={props.onButtonClick || this.onButtonClick}
+        color={props.content.button_color}
+        backgroundColor={props.content.button_background_color}
+      >
+        {props.content.button_label}
+      </Button>
     );
   }
 
   render() {
     const { props } = this;
     let className = "SimpleBelowSearchSnippet";
+    let containerName = "below-search-snippet";
 
     if (props.className) {
       className += ` ${props.className}`;
     }
+    if (this._shouldRenderButton()) {
+      className += " withButton";
+      containerName += " withButton";
+    }
 
     return (
-      <SnippetBase
-        {...props}
-        className={className}
-        textStyle={this.props.textStyle}
-      >
-        <img
-          src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
-          className="icon icon-light-theme"
-          alt={props.content.icon_alt_text || ICON_ALT_TEXT}
-        />
-        <img
-          src={
-            safeURI(props.content.icon_dark_theme || props.content.icon) ||
-            DEFAULT_ICON_PATH
-          }
-          className="icon icon-dark-theme"
-          alt={props.content.icon_alt_text || ICON_ALT_TEXT}
-        />
-        <div>
-          <p className="body">{this.renderText()}</p>
-          {this.props.extraContent}
+      <div className={containerName}>
+        <div className="snippet-hover-wrapper">
+          <SnippetBase
+            {...props}
+            className={className}
+            textStyle={this.props.textStyle}
+          >
+            <img
+              src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
+              className="icon icon-light-theme"
+              alt={props.content.icon_alt_text || ICON_ALT_TEXT}
+            />
+            <img
+              src={
+                safeURI(props.content.icon_dark_theme || props.content.icon) ||
+                DEFAULT_ICON_PATH
+              }
+              className="icon icon-dark-theme"
+              alt={props.content.icon_alt_text || ICON_ALT_TEXT}
+            />
+            <div className="textContainer">
+              {this.renderTitle()}
+              <p className="body">{this.renderText()}</p>
+              {this.props.extraContent}
+            </div>
+            {<div className="buttonContainer">{this.renderButton()}</div>}
+          </SnippetBase>
         </div>
-      </SnippetBase>
+      </div>
     );
   }
 }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -1,25 +1,35 @@
 {
   "title": "SimpleBelowSearchSnippet",
-  "description": "A simple template with just an icon and rich text. It gets inserted below the Activity Stream search box.",
+  "description": "A simple template with an icon, rich text and an optional button. It gets inserted below the Activity Stream search box.",
   "version": "1.2.0",
   "type": "object",
   "definitions": {
+    "plainText": {
+      "description": "Plain text (no HTML allowed)",
+      "type": "string"
+    },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
       "type": "string"
     },
     "link_url": {
       "description": "Target for links or buttons",
       "type": "string",
       "format": "uri"
     }
   },
   "properties": {
+    "title": {
+      "allOf": [
+        {"$ref": "#/definitions/plainText"},
+        {"description": "Snippet title displayed before snippet text"}
+      ]
+    },
     "text": {
       "allOf": [
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "icon": {
       "type": "string",
@@ -34,16 +44,43 @@
       "description": "Alt text describing icon for screen readers",
       "default": ""
     },
     "block_button_text": {
       "type": "string",
       "description": "Tooltip text used for dismiss button.",
       "default": "Remove this"
     },
+    "button_action": {
+      "type": "string",
+      "description": "The type of action the button should trigger."
+    },
+    "button_url": {
+      "allOf": [
+        {"$ref": "#/definitions/link_url"},
+        {"description": "A url, button_label links to this"}
+      ]
+    },
+    "button_action_args": {
+      "description": "Additional parameters for button action, example which specific menu the button should open"
+    },
+    "button_label": {
+      "allOf": [
+        {"$ref": "#/definitions/plainText"},
+        {"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."}
+      ]
+    },
+    "button_color": {
+      "type": "string",
+      "description": "The text color of the button. Valid CSS color."
+    },
+    "button_background_color": {
+      "type": "string",
+      "description": "The background color of the button. Valid CSS color."
+    },
     "do_not_autoblock": {
       "type": "boolean",
       "description": "Used to prevent blocking the snippet after the CTA link has been clicked"
     },
     "links": {
       "additionalProperties": {
         "url": {
           "allOf": [
@@ -59,10 +96,15 @@
           "type": "string",
           "description": "Additional parameters for link action, example which specific menu the button should open"
         }
       }
     }
   },
   "additionalProperties": false,
   "required": ["text"],
-  "dependencies": {}
+  "dependencies": {
+    "button_action": ["button_label"],
+    "button_url": ["button_label"],
+    "button_color": ["button_label"],
+    "button_background_color": ["button_label"]
+  }
 }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -1,80 +1,201 @@
+
+.below-search-snippet {
+  margin: 0 auto 16px;
+
+  &.withButton {
+    padding: 0 25px;
+    margin: auto;
+    min-height: 60px;
+    background-color: transparent;
+
+    // Add more padding if discovery stream is enabled.
+    .ds-outer-wrapper-breakpoint-override & {
+      padding: 0 50px;
+
+      @media (max-width: 865px) {
+
+        .buttonContainer {
+          margin: auto;
+        }
+      }
+    }
+
+    .snippet-hover-wrapper {
+      min-height: 60px;
+      border-radius: 4px;
+
+      &:hover {
+        background-color: var(--newtab-element-hover-color);
+
+        .blockButton {
+          display: block;
+
+          // larger inset if discovery stream is enabled.
+          .ds-outer-wrapper-breakpoint-override & {
+            inset-inline-end: -8%;
+
+            @media (max-width: 865px) {
+              inset-inline-end: 2%;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 .SimpleBelowSearchSnippet {
-  background-color: inherit;
+  background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
+  margin: auto;
   z-index: auto;
 
+  @media (min-width: $break-point-large) {
+    width: 736px;
+  }
+
+  &.active {
+    background-color: var(--newtab-element-hover-color);
+    border-radius: 4px;
+  }
+
   .innerWrapper {
     align-items: center;
-    background-color: var(--newtab-card-background-color);
+    background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
     padding: 16px;
     text-align: center;
     width: 100%;
 
     @mixin full-width-styles {
       align-items: flex-start;
-      background-color: inherit;
+      background-color: transparent;
+      border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      padding-inline-end: 36px;
       text-align: inherit;
     }
 
     @media (min-width: $break-point-medium) {
       @include full-width-styles;
     }
 
+    @media (max-width: 1120px) {
+      margin: 0 60px;
+    }
+
+    @media (max-width: 865px) {
+      margin: 0 60px 0 0;
+    }
+
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
-    }
-  }
-
-  &.active {
-    .innerWrapper {
-      background-color: var(--newtab-element-hover-color);
+      margin: auto;
     }
   }
 
   .blockButton {
     display: block;
-    inset-inline-end: 15px;
+    inset-inline-end: 20px;
     opacity: 1;
-    top: 24px;
+    top: 50%;
+  }
+
+  .title {
+    font-size: inherit;
+    margin: 0;
+  }
+
+  .title-inline {
+    display: inline;
+  }
+
+  .textContainer {
+    margin: 10px;
+    margin-inline-start: 0;
   }
 
   .icon {
+    margin-top: 8px;
+    margin-inline-start: 12px;
     height: 32px;
-    margin-inline-start: 12px;
     width: 32px;
 
     @mixin full-width-styles {
       height: 24px;
-      margin-top: 10px;
       width: 24px;
     }
 
     @media (min-width: $break-point-medium) {
       @include full-width-styles;
     }
 
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
     }
   }
 
+  &.withButton {
+    line-height: 20px;
+    margin-bottom: 10px;
+    min-height: 60px;
+    background-color: transparent;
+
+    .blockButton {
+      display: none;
+      inset-inline-end: -15%;
+      opacity: 1;
+      margin: auto;
+      top: unset;
+
+      @media (max-width: 1120px) {
+        inset-inline-end: 2%;
+      }
+
+      @media (max-width: 865px) {
+        margin-top: 10px;
+      }
+
+      .ds-outer-wrapper-breakpoint-override & {
+        inset-inline-end: -10%;
+        margin: auto;
+
+        @media (max-width: 865px) {
+          inset-inline-end: 2%;
+        }
+      }
+    }
+
+    .icon {
+      width: 42px;
+      height: 42px;
+      flex-shrink: 0;
+      margin: auto 0;
+      margin-inline-end: 10px;
+    }
+
+    .buttonContainer {
+      margin: auto;
+      margin-inline-end: 0;
+    }
+  }
+
   .body {
+    display: inline;
+    position: sticky;
+    transform: translateY(-50%);
     margin: 8px 0 0;
 
     @media (min-width: $break-point-medium) {
       margin: 12px 0;
     }
 
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
@@ -166,41 +166,43 @@ export class SimpleSnippet extends React
     if (props.content.tall) {
       className += " tall";
     }
     if (sectionHeader) {
       className += " has-section-header";
     }
 
     return (
-      <SnippetBase
-        {...props}
-        className={className}
-        textStyle={this.props.textStyle}
-      >
-        {sectionHeader}
-        <ConditionalWrapper
-          condition={sectionHeader}
-          wrap={this.wrapSnippetContent}
+      <div className="snippet-hover-wrapper">
+        <SnippetBase
+          {...props}
+          className={className}
+          textStyle={this.props.textStyle}
         >
-          <img
-            src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
-            className="icon icon-light-theme"
-            alt={props.content.icon_alt_text || ICON_ALT_TEXT}
-          />
-          <img
-            src={
-              safeURI(props.content.icon_dark_theme || props.content.icon) ||
-              DEFAULT_ICON_PATH
-            }
-            className="icon icon-dark-theme"
-            alt={props.content.icon_alt_text || ICON_ALT_TEXT}
-          />
-          <div>
-            {this.renderTitle()} <p className="body">{this.renderText()}</p>
-            {this.props.extraContent}
-          </div>
-          {<div>{this.renderButton()}</div>}
-        </ConditionalWrapper>
-      </SnippetBase>
+          {sectionHeader}
+          <ConditionalWrapper
+            condition={sectionHeader}
+            wrap={this.wrapSnippetContent}
+          >
+            <img
+              src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}
+              className="icon icon-light-theme"
+              alt={props.content.icon_alt_text || ICON_ALT_TEXT}
+            />
+            <img
+              src={
+                safeURI(props.content.icon_dark_theme || props.content.icon) ||
+                DEFAULT_ICON_PATH
+              }
+              className="icon icon-dark-theme"
+              alt={props.content.icon_alt_text || ICON_ALT_TEXT}
+            />
+            <div>
+              {this.renderTitle()} <p className="body">{this.renderText()}</p>
+              {this.props.extraContent}
+            </div>
+            {<div>{this.renderButton()}</div>}
+          </ConditionalWrapper>
+        </SnippetBase>
+      </div>
     );
   }
 }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
@@ -63,17 +63,16 @@
     },
     "button_url": {
       "allOf": [
         {"$ref": "#/definitions/link_url"},
         {"description": "A url, button_label links to this"}
       ]
     },
     "button_action_args": {
-      "type": "string",
       "description": "Additional parameters for button action, example which specific menu the button should open"
     },
     "button_label": {
       "allOf": [
         {"$ref": "#/definitions/plainText"},
         {"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."}
       ]
     },
--- a/browser/components/newtab/content-src/components/Base/_Base.scss
+++ b/browser/components/newtab/content-src/components/Base/_Base.scss
@@ -12,42 +12,47 @@
 
   a {
     color: var(--newtab-link-primary-color);
   }
 }
 
 main {
   margin: auto;
+  width: $wrapper-default-width;
   // Offset the snippets container so things at the bottom of the page are still
   // visible when snippets are visible. Adjust for other spacing.
   padding-bottom: $snippets-container-height - $section-spacing - $base-gutter;
-  width: $wrapper-default-width;
+
+  section {
+    margin-bottom: $section-spacing;
+    position: relative;
+  }
+
+  .hide-main & {
+    visibility: hidden;
+  }
 
   @media (min-width: $break-point-medium) {
     width: $wrapper-max-width-medium;
   }
 
   @media (min-width: $break-point-large) {
     width: $wrapper-max-width-large;
   }
 
   @media (min-width: $break-point-widest) {
     width: $wrapper-max-width-widest;
   }
 
-  section {
-    margin-bottom: $section-spacing;
-    position: relative;
-  }
+}
 
-  .hide-main & {
-    visibility: hidden;
-  }
-
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%;
 }
 
 .ds-outer-wrapper-search-alignment {
   main {
     // This override is to ensure while Discovery Stream loads,
     // the search bar does not jump around. (it sticks to the top)
     margin: 0 auto;
   }
--- a/browser/components/newtab/content-src/components/Search/_Search.scss
+++ b/browser/components/newtab/content-src/components/Search/_Search.scss
@@ -126,50 +126,32 @@
     }
 
     &:dir(rtl) {
       transform: scaleX(-1);
     }
   }
 }
 
-.below-search-snippet {
-  margin: 0 auto 16px;
-  width: $searchbar-width-small;
-
-  @media (min-width: $break-point-medium) {
-    width: $searchbar-width-medium;
-  }
-
-  @media (min-width: $break-point-large) {
-    width: $searchbar-width-large;
-  }
-
-  // Disable breakpoints for now if discovery stream is enabled.
-  .ds-outer-wrapper-breakpoint-override & {
-    width: $searchbar-width-large;
-  }
-}
-
-.non-collapsible-section + .below-search-snippet {
+.non-collapsible-section + .below-search-snippet-wrapper {
   // If search is enabled, we need to invade its large bottom padding.
   margin-top: -48px;
 }
 
 @media (max-height: 700px) {
   .search-wrapper {
     padding: 0 0 30px;
   }
 
-  .non-collapsible-section + .below-search-snippet {
+  .non-collapsible-section + .below-search-snippet-wrapper {
     // In shorter windows, search doesn't have such a large padding.
     margin-top: -14px;
   }
 
-  .below-search-snippet {
+  .below-search-snippet-wrapper {
     min-height: 0;
   }
 }
 
 .search-handoff-button {
   background: var(--newtab-textbox-background-color) var(--newtab-search-icon) $search-icon-padding center no-repeat;
   background-size: $search-icon-size;
   border: solid 1px var(--newtab-search-border-color);
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -365,32 +365,36 @@ input[type='text'], input[type='search']
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
 main {
   margin: auto;
-  padding-bottom: 68px;
-  width: 274px; }
+  width: 274px;
+  padding-bottom: 68px; }
+  main section {
+    margin-bottom: 20px;
+    position: relative; }
+  .hide-main main {
+    visibility: hidden; }
   @media (min-width: 610px) {
     main {
       width: 530px; } }
   @media (min-width: 866px) {
     main {
       width: 786px; } }
   @media (min-width: 1122px) {
     main {
       width: 1042px; } }
-  main section {
-    margin-bottom: 20px;
-    position: relative; }
-  .hide-main main {
-    visibility: hidden; }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1083,37 +1087,25 @@ main {
     .search-wrapper .search-button:focus, .search-wrapper .search-button:hover {
       background-color: rgba(12, 12, 13, 0.1);
       cursor: pointer; }
     .search-wrapper .search-button:active {
       background-color: rgba(12, 12, 13, 0.2); }
     .search-wrapper .search-button:dir(rtl) {
       transform: scaleX(-1); }
 
-.below-search-snippet {
-  margin: 0 auto 16px;
-  width: 224px; }
-  @media (min-width: 610px) {
-    .below-search-snippet {
-      width: 480px; } }
-  @media (min-width: 866px) {
-    .below-search-snippet {
-      width: 736px; } }
-  .ds-outer-wrapper-breakpoint-override .below-search-snippet {
-    width: 736px; }
-
-.non-collapsible-section + .below-search-snippet {
+.non-collapsible-section + .below-search-snippet-wrapper {
   margin-top: -48px; }
 
 @media (max-height: 700px) {
   .search-wrapper {
     padding: 0 0 30px; }
-  .non-collapsible-section + .below-search-snippet {
+  .non-collapsible-section + .below-search-snippet-wrapper {
     margin-top: -14px; }
-  .below-search-snippet {
+  .below-search-snippet-wrapper {
     min-height: 0; } }
 
 .search-handoff-button {
   background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
   background-size: 24px;
   border: solid 1px var(--newtab-search-border-color);
   border-radius: 3px;
   box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
@@ -3166,69 +3158,148 @@ body[lwt-newtab-brighttext] .scene2Icon 
     background-position: center center;
     background-repeat: no-repeat;
     background-image: url("resource://activity-stream/data/content/assets/gift-extension.svg"); }
   .ReturnToAMOOverlay .icon-add,
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
+.below-search-snippet {
+  margin: 0 auto 16px; }
+  .below-search-snippet.withButton {
+    padding: 0 25px;
+    margin: auto;
+    min-height: 60px;
+    background-color: transparent; }
+    .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
+      padding: 0 50px; }
+      @media (max-width: 865px) {
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
+          margin: auto; } }
+    .below-search-snippet.withButton .snippet-hover-wrapper {
+      min-height: 60px;
+      border-radius: 4px; }
+      .below-search-snippet.withButton .snippet-hover-wrapper:hover {
+        background-color: var(--newtab-element-hover-color); }
+        .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          display: block; }
+          .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+            inset-inline-end: -8%; }
+            @media (max-width: 865px) {
+              .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+                inset-inline-end: 2%; } }
+
 .SimpleBelowSearchSnippet {
-  background-color: inherit;
+  background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
+  margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
+  .SimpleBelowSearchSnippet.active {
+    background-color: var(--newtab-element-hover-color);
+    border-radius: 4px; }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
-    background-color: var(--newtab-card-background-color);
+    background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
     padding: 16px;
     text-align: center;
     width: 100%; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
-        background-color: inherit;
+        background-color: transparent;
+        border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
-        padding-inline-end: 36px;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
-      background-color: inherit;
+      background-color: transparent;
+      border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      padding-inline-end: 36px;
-      text-align: inherit; }
-  .SimpleBelowSearchSnippet.active .innerWrapper {
-    background-color: var(--newtab-element-hover-color); }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
-    inset-inline-end: 15px;
+    inset-inline-end: 20px;
     opacity: 1;
-    top: 24px; }
+    top: 50%; }
+  .SimpleBelowSearchSnippet .title {
+    font-size: inherit;
+    margin: 0; }
+  .SimpleBelowSearchSnippet .title-inline {
+    display: inline; }
+  .SimpleBelowSearchSnippet .textContainer {
+    margin: 10px;
+    margin-inline-start: 0; }
   .SimpleBelowSearchSnippet .icon {
+    margin-top: 8px;
+    margin-inline-start: 12px;
     height: 32px;
-    margin-inline-start: 12px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
-        margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
-      margin-top: 10px;
       width: 24px; }
+  .SimpleBelowSearchSnippet.withButton {
+    line-height: 20px;
+    margin-bottom: 10px;
+    min-height: 60px;
+    background-color: transparent; }
+    .SimpleBelowSearchSnippet.withButton .blockButton {
+      display: none;
+      inset-inline-end: -15%;
+      opacity: 1;
+      margin: auto;
+      top: unset; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
+        @media (max-width: 865px) {
+          .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+            inset-inline-end: 2%; } }
+    .SimpleBelowSearchSnippet.withButton .icon {
+      width: 42px;
+      height: 42px;
+      flex-shrink: 0;
+      margin: auto 0;
+      margin-inline-end: 10px; }
+    .SimpleBelowSearchSnippet.withButton .buttonContainer {
+      margin: auto;
+      margin-inline-end: 0; }
   .SimpleBelowSearchSnippet .body {
+    display: inline;
+    position: sticky;
+    transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .body {
       margin: 12px 0; }
     .SimpleBelowSearchSnippet .body a {
       font-weight: 600; }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -368,32 +368,36 @@ input[type='text'], input[type='search']
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
 main {
   margin: auto;
-  padding-bottom: 68px;
-  width: 274px; }
+  width: 274px;
+  padding-bottom: 68px; }
+  main section {
+    margin-bottom: 20px;
+    position: relative; }
+  .hide-main main {
+    visibility: hidden; }
   @media (min-width: 610px) {
     main {
       width: 530px; } }
   @media (min-width: 866px) {
     main {
       width: 786px; } }
   @media (min-width: 1122px) {
     main {
       width: 1042px; } }
-  main section {
-    margin-bottom: 20px;
-    position: relative; }
-  .hide-main main {
-    visibility: hidden; }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1086,37 +1090,25 @@ main {
     .search-wrapper .search-button:focus, .search-wrapper .search-button:hover {
       background-color: rgba(12, 12, 13, 0.1);
       cursor: pointer; }
     .search-wrapper .search-button:active {
       background-color: rgba(12, 12, 13, 0.2); }
     .search-wrapper .search-button:dir(rtl) {
       transform: scaleX(-1); }
 
-.below-search-snippet {
-  margin: 0 auto 16px;
-  width: 224px; }
-  @media (min-width: 610px) {
-    .below-search-snippet {
-      width: 480px; } }
-  @media (min-width: 866px) {
-    .below-search-snippet {
-      width: 736px; } }
-  .ds-outer-wrapper-breakpoint-override .below-search-snippet {
-    width: 736px; }
-
-.non-collapsible-section + .below-search-snippet {
+.non-collapsible-section + .below-search-snippet-wrapper {
   margin-top: -48px; }
 
 @media (max-height: 700px) {
   .search-wrapper {
     padding: 0 0 30px; }
-  .non-collapsible-section + .below-search-snippet {
+  .non-collapsible-section + .below-search-snippet-wrapper {
     margin-top: -14px; }
-  .below-search-snippet {
+  .below-search-snippet-wrapper {
     min-height: 0; } }
 
 .search-handoff-button {
   background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
   background-size: 24px;
   border: solid 1px var(--newtab-search-border-color);
   border-radius: 3px;
   box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
@@ -3169,69 +3161,148 @@ body[lwt-newtab-brighttext] .scene2Icon 
     background-position: center center;
     background-repeat: no-repeat;
     background-image: url("resource://activity-stream/data/content/assets/gift-extension.svg"); }
   .ReturnToAMOOverlay .icon-add,
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
+.below-search-snippet {
+  margin: 0 auto 16px; }
+  .below-search-snippet.withButton {
+    padding: 0 25px;
+    margin: auto;
+    min-height: 60px;
+    background-color: transparent; }
+    .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
+      padding: 0 50px; }
+      @media (max-width: 865px) {
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
+          margin: auto; } }
+    .below-search-snippet.withButton .snippet-hover-wrapper {
+      min-height: 60px;
+      border-radius: 4px; }
+      .below-search-snippet.withButton .snippet-hover-wrapper:hover {
+        background-color: var(--newtab-element-hover-color); }
+        .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          display: block; }
+          .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+            inset-inline-end: -8%; }
+            @media (max-width: 865px) {
+              .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+                inset-inline-end: 2%; } }
+
 .SimpleBelowSearchSnippet {
-  background-color: inherit;
+  background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
+  margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
+  .SimpleBelowSearchSnippet.active {
+    background-color: var(--newtab-element-hover-color);
+    border-radius: 4px; }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
-    background-color: var(--newtab-card-background-color);
+    background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
     padding: 16px;
     text-align: center;
     width: 100%; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
-        background-color: inherit;
+        background-color: transparent;
+        border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
-        padding-inline-end: 36px;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
-      background-color: inherit;
+      background-color: transparent;
+      border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      padding-inline-end: 36px;
-      text-align: inherit; }
-  .SimpleBelowSearchSnippet.active .innerWrapper {
-    background-color: var(--newtab-element-hover-color); }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
-    inset-inline-end: 15px;
+    inset-inline-end: 20px;
     opacity: 1;
-    top: 24px; }
+    top: 50%; }
+  .SimpleBelowSearchSnippet .title {
+    font-size: inherit;
+    margin: 0; }
+  .SimpleBelowSearchSnippet .title-inline {
+    display: inline; }
+  .SimpleBelowSearchSnippet .textContainer {
+    margin: 10px;
+    margin-inline-start: 0; }
   .SimpleBelowSearchSnippet .icon {
+    margin-top: 8px;
+    margin-inline-start: 12px;
     height: 32px;
-    margin-inline-start: 12px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
-        margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
-      margin-top: 10px;
       width: 24px; }
+  .SimpleBelowSearchSnippet.withButton {
+    line-height: 20px;
+    margin-bottom: 10px;
+    min-height: 60px;
+    background-color: transparent; }
+    .SimpleBelowSearchSnippet.withButton .blockButton {
+      display: none;
+      inset-inline-end: -15%;
+      opacity: 1;
+      margin: auto;
+      top: unset; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
+        @media (max-width: 865px) {
+          .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+            inset-inline-end: 2%; } }
+    .SimpleBelowSearchSnippet.withButton .icon {
+      width: 42px;
+      height: 42px;
+      flex-shrink: 0;
+      margin: auto 0;
+      margin-inline-end: 10px; }
+    .SimpleBelowSearchSnippet.withButton .buttonContainer {
+      margin: auto;
+      margin-inline-end: 0; }
   .SimpleBelowSearchSnippet .body {
+    display: inline;
+    position: sticky;
+    transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .body {
       margin: 12px 0; }
     .SimpleBelowSearchSnippet .body a {
       font-weight: 600; }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -365,32 +365,36 @@ input[type='text'], input[type='search']
   .outer-wrapper.only-search {
     display: block;
     padding-top: 134px; }
   .outer-wrapper a {
     color: var(--newtab-link-primary-color); }
 
 main {
   margin: auto;
-  padding-bottom: 68px;
-  width: 274px; }
+  width: 274px;
+  padding-bottom: 68px; }
+  main section {
+    margin-bottom: 20px;
+    position: relative; }
+  .hide-main main {
+    visibility: hidden; }
   @media (min-width: 610px) {
     main {
       width: 530px; } }
   @media (min-width: 866px) {
     main {
       width: 786px; } }
   @media (min-width: 1122px) {
     main {
       width: 1042px; } }
-  main section {
-    margin-bottom: 20px;
-    position: relative; }
-  .hide-main main {
-    visibility: hidden; }
+
+.below-search-snippet.withButton {
+  margin: auto;
+  width: 100%; }
 
 .ds-outer-wrapper-search-alignment main {
   margin: 0 auto; }
 
 .ds-outer-wrapper-breakpoint-override main {
   width: 1042px; }
 
 .ds-outer-wrapper-breakpoint-override:not(.fixed-search) .search-wrapper .search-inner-wrapper {
@@ -1083,37 +1087,25 @@ main {
     .search-wrapper .search-button:focus, .search-wrapper .search-button:hover {
       background-color: rgba(12, 12, 13, 0.1);
       cursor: pointer; }
     .search-wrapper .search-button:active {
       background-color: rgba(12, 12, 13, 0.2); }
     .search-wrapper .search-button:dir(rtl) {
       transform: scaleX(-1); }
 
-.below-search-snippet {
-  margin: 0 auto 16px;
-  width: 224px; }
-  @media (min-width: 610px) {
-    .below-search-snippet {
-      width: 480px; } }
-  @media (min-width: 866px) {
-    .below-search-snippet {
-      width: 736px; } }
-  .ds-outer-wrapper-breakpoint-override .below-search-snippet {
-    width: 736px; }
-
-.non-collapsible-section + .below-search-snippet {
+.non-collapsible-section + .below-search-snippet-wrapper {
   margin-top: -48px; }
 
 @media (max-height: 700px) {
   .search-wrapper {
     padding: 0 0 30px; }
-  .non-collapsible-section + .below-search-snippet {
+  .non-collapsible-section + .below-search-snippet-wrapper {
     margin-top: -14px; }
-  .below-search-snippet {
+  .below-search-snippet-wrapper {
     min-height: 0; } }
 
 .search-handoff-button {
   background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
   background-size: 24px;
   border: solid 1px var(--newtab-search-border-color);
   border-radius: 3px;
   box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
@@ -3166,69 +3158,148 @@ body[lwt-newtab-brighttext] .scene2Icon 
     background-position: center center;
     background-repeat: no-repeat;
     background-image: url("resource://activity-stream/data/content/assets/gift-extension.svg"); }
   .ReturnToAMOOverlay .icon-add,
   .amo + body.hide-main .icon-add {
     fill: #FFF;
     vertical-align: sub; }
 
+.below-search-snippet {
+  margin: 0 auto 16px; }
+  .below-search-snippet.withButton {
+    padding: 0 25px;
+    margin: auto;
+    min-height: 60px;
+    background-color: transparent; }
+    .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton {
+      padding: 0 50px; }
+      @media (max-width: 865px) {
+        .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .buttonContainer {
+          margin: auto; } }
+    .below-search-snippet.withButton .snippet-hover-wrapper {
+      min-height: 60px;
+      border-radius: 4px; }
+      .below-search-snippet.withButton .snippet-hover-wrapper:hover {
+        background-color: var(--newtab-element-hover-color); }
+        .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+          display: block; }
+          .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+            inset-inline-end: -8%; }
+            @media (max-width: 865px) {
+              .ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
+                inset-inline-end: 2%; } }
+
 .SimpleBelowSearchSnippet {
-  background-color: inherit;
+  background-color: transparent;
   border: 0;
   box-shadow: none;
   position: relative;
+  margin: auto;
   z-index: auto; }
+  @media (min-width: 866px) {
+    .SimpleBelowSearchSnippet {
+      width: 736px; } }
+  .SimpleBelowSearchSnippet.active {
+    background-color: var(--newtab-element-hover-color);
+    border-radius: 4px; }
   .SimpleBelowSearchSnippet .innerWrapper {
     align-items: center;
-    background-color: var(--newtab-card-background-color);
+    background-color: transparent;
     border-radius: 4px;
     box-shadow: var(--newtab-card-shadow);
     flex-direction: column;
     padding: 16px;
     text-align: center;
     width: 100%; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         align-items: flex-start;
-        background-color: inherit;
+        background-color: transparent;
+        border-radius: 4px;
         box-shadow: none;
         flex-direction: row;
         padding: 0;
-        padding-inline-end: 36px;
         text-align: inherit; } }
+    @media (max-width: 1120px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px; } }
+    @media (max-width: 865px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: 0 60px 0 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
-      background-color: inherit;
+      background-color: transparent;
+      border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
-      padding-inline-end: 36px;
-      text-align: inherit; }
-  .SimpleBelowSearchSnippet.active .innerWrapper {
-    background-color: var(--newtab-element-hover-color); }
+      text-align: inherit;
+      margin: auto; }
   .SimpleBelowSearchSnippet .blockButton {
     display: block;
-    inset-inline-end: 15px;
+    inset-inline-end: 20px;
     opacity: 1;
-    top: 24px; }
+    top: 50%; }
+  .SimpleBelowSearchSnippet .title {
+    font-size: inherit;
+    margin: 0; }
+  .SimpleBelowSearchSnippet .title-inline {
+    display: inline; }
+  .SimpleBelowSearchSnippet .textContainer {
+    margin: 10px;
+    margin-inline-start: 0; }
   .SimpleBelowSearchSnippet .icon {
+    margin-top: 8px;
+    margin-inline-start: 12px;
     height: 32px;
-    margin-inline-start: 12px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
-        margin-top: 10px;
         width: 24px; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
-      margin-top: 10px;
       width: 24px; }
+  .SimpleBelowSearchSnippet.withButton {
+    line-height: 20px;
+    margin-bottom: 10px;
+    min-height: 60px;
+    background-color: transparent; }
+    .SimpleBelowSearchSnippet.withButton .blockButton {
+      display: none;
+      inset-inline-end: -15%;
+      opacity: 1;
+      margin: auto;
+      top: unset; }
+      @media (max-width: 1120px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          inset-inline-end: 2%; } }
+      @media (max-width: 865px) {
+        .SimpleBelowSearchSnippet.withButton .blockButton {
+          margin-top: 10px; } }
+      .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+        inset-inline-end: -10%;
+        margin: auto; }
+        @media (max-width: 865px) {
+          .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet.withButton .blockButton {
+            inset-inline-end: 2%; } }
+    .SimpleBelowSearchSnippet.withButton .icon {
+      width: 42px;
+      height: 42px;
+      flex-shrink: 0;
+      margin: auto 0;
+      margin-inline-end: 10px; }
+    .SimpleBelowSearchSnippet.withButton .buttonContainer {
+      margin: auto;
+      margin-inline-end: 0; }
   .SimpleBelowSearchSnippet .body {
+    display: inline;
+    position: sticky;
+    transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .body {
       margin: 12px 0; }
     .SimpleBelowSearchSnippet .body a {
       font-weight: 600; }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -193,17 +193,17 @@ 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", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "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_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "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_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_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "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 ["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"]) {
+for (const type of ["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"]) {
   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
   } : {};
@@ -1719,16 +1719,19 @@ const ASRouterAdmin = Object(react_redux
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(14);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_8__);
 /* harmony import */ var _templates_ReturnToAMO_ReturnToAMO__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(15);
 /* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(51);
 /* harmony import */ var _templates_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(23);
 /* harmony import */ var _templates_Trailhead_Trailhead__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(25);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
+/* 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/. */
 
 
 
 
 
 
 
 
@@ -1858,27 +1861,85 @@ function shouldSendImpressionOnUpdate(ne
 
 class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_7___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
+    this.onUserAction = this.onUserAction.bind(this);
     this.state = {
       message: {},
       bundle: {}
     };
 
     if (props.document) {
       this.headerPortal = props.document.getElementById("header-asrouter-container");
       this.footerPortal = props.document.getElementById("footer-asrouter-container");
     }
   }
 
+  async fetchFlowParams(params = {}) {
+    let result = {};
+    const {
+      fxaEndpoint,
+      dispatch
+    } = this.props;
+
+    if (!fxaEndpoint) {
+      const err = "Tried to fetch flow params before fxaEndpoint pref was ready";
+      console.error(err); // eslint-disable-line no-console
+    }
+
+    try {
+      const urlObj = new URL(fxaEndpoint);
+      urlObj.pathname = "metrics-flow";
+      Object.keys(params).forEach(key => {
+        urlObj.searchParams.append(key, params[key]);
+      });
+      const response = await fetch(urlObj.toString(), {
+        credentials: "omit"
+      });
+
+      if (response.status === 200) {
+        const {
+          deviceId,
+          flowId,
+          flowBeginTime
+        } = await response.json();
+        result = {
+          deviceId,
+          flowId,
+          flowBeginTime
+        };
+      } else {
+        console.error("Non-200 response", response); // eslint-disable-line no-console
+
+        dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
+          data: {
+            event: "FXA_METRICS_FETCH_ERROR",
+            value: response.status
+          }
+        }));
+      }
+    } catch (error) {
+      console.error(error);
+      dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
+        type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
+        data: {
+          event: "FXA_METRICS_ERROR"
+        }
+      }));
+    }
+
+    return result;
+  }
+
   sendUserActionTelemetry(extraProps = {}) {
     const {
       message,
       bundle
     } = this.state;
 
     if (!message && !extraProps.message_id) {
       throw new Error(`You must provide a message_id for bundled messages`);
@@ -2066,16 +2127,49 @@ class ASRouterUISurface extends react__W
       });
     }
   }
 
   componentWillUnmount() {
     ASRouterUtils.removeListener(this.onMessageFromParent);
   }
 
+  async getMonitorUrl({
+    url,
+    flowRequestParams = {}
+  }) {
+    const flowValues = await this.fetchFlowParams(flowRequestParams); // Note that flowParams are actually added dynamically on the page
+
+    const urlObj = new URL(url);
+    ["deviceId", "flowId", "flowBeginTime"].forEach(key => {
+      if (key in flowValues) {
+        urlObj.searchParams.append(key, flowValues[key]);
+      }
+    });
+    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:
+        const url = await this.getMonitorUrl(action.data.args);
+        ASRouterUtils.executeAction({
+          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["ASRouterActions"].OPEN_URL,
+          data: {
+            args: url
+          }
+        });
+        break;
+
+      default:
+        ASRouterUtils.executeAction(action);
+    }
+  }
+
   renderSnippets() {
     if (this.state.bundle.template === "onboarding" || this.state.message.template === "fxa_overlay" || this.state.message.template === "return_to_amo_overlay" || this.state.message.template === "trailhead") {
       return null;
     }
 
     const SnippetComponent = _templates_template_manifest__WEBPACK_IMPORTED_MODULE_10__["SnippetsTemplates"][this.state.message.template];
     const {
       content
@@ -2088,17 +2182,17 @@ class ASRouterUISurface extends react__W
       ,
       document: this.props.document
     }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"], {
       bundles: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["generateBundles"])(content)
     }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(SnippetComponent, _extends({}, this.state.message, {
       UISurface: "NEWTAB_FOOTER_BAR",
       onBlock: this.onBlockById(this.state.message.id),
       onDismiss: this.onDismissById(this.state.message.id),
-      onAction: ASRouterUtils.executeAction,
+      onAction: this.onUserAction,
       sendClick: this.sendClick,
       sendUserActionTelemetry: this.sendUserActionTelemetry
     }))));
   }
 
   renderOnboarding() {
     if (this.state.bundle.template === "onboarding") {
       return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_6__["OnboardingMessage"], _extends({}, this.state.bundle, {
@@ -2183,17 +2277,17 @@ class ASRouterUISurface extends react__W
     if (!message.id && !bundle.template) {
       return null;
     }
 
     const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(message.template);
     const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(message.template);
     return shouldRenderBelowSearch ? // Render special below search snippets in place;
     react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
-      className: "below-search-snippet"
+      className: "below-search-snippet-wrapper"
     }, this.renderSnippets()) : // For onboarding, regular snippets etc. we should render
     // everything in our footer container.
     react_dom__WEBPACK_IMPORTED_MODULE_8___default.a.createPortal(react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_7___default.a.Fragment, null, this.renderPreviewBanner(), this.renderTrailhead(), this.renderFirstRunOverlay(), this.renderOnboarding(), this.renderSnippets()), shouldRenderInHeader ? this.headerPortal : this.footerPortal);
   }
 
 }
 ASRouterUISurface.defaultProps = {
   document: global.document
@@ -2862,17 +2956,17 @@ function safeURI(url) {
 /***/ (function(module) {
 
 module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text for accessibility","default":""},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
 
 /***/ }),
 /* 19 */
 /***/ (function(module) {
 
-module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text describing icon for screen readers","default":""},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_alt_text":{"type":"string","description":"Alt text describing title icon for screen readers","default":""},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"type":"string","description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
+module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text describing icon for screen readers","default":""},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_alt_text":{"type":"string","description":"Alt text describing title icon for screen readers","default":""},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
 
 /***/ }),
 /* 20 */
 /***/ (function(module) {
 
 module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
 
 /***/ }),
@@ -9654,33 +9748,35 @@ class SimpleSnippet_SimpleSnippet extend
     if (props.content.tall) {
       className += " tall";
     }
 
     if (sectionHeader) {
       className += " has-section-header";
     }
 
-    return external_React_default.a.createElement(SnippetBase_SnippetBase, _extends({}, props, {
+    return external_React_default.a.createElement("div", {
+      className: "snippet-hover-wrapper"
+    }, external_React_default.a.createElement(SnippetBase_SnippetBase, _extends({}, props, {
       className: className,
       textStyle: this.props.textStyle
     }), sectionHeader, external_React_default.a.createElement(ConditionalWrapper, {
       condition: sectionHeader,
       wrap: this.wrapSnippetContent
     }, external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || DEFAULT_ICON_PATH,
       className: "icon icon-light-theme",
       alt: props.content.icon_alt_text || ICON_ALT_TEXT
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH,
       className: "icon icon-dark-theme",
       alt: props.content.icon_alt_text || ICON_ALT_TEXT
     }), external_React_default.a.createElement("div", null, this.renderTitle(), " ", external_React_default.a.createElement("p", {
       className: "body"
-    }, this.renderText()), this.props.extraContent), external_React_default.a.createElement("div", null, this.renderButton())));
+    }, this.renderText()), this.props.extraContent), external_React_default.a.createElement("div", null, this.renderButton()))));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx
 function EOYSnippet_extends() { EOYSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return EOYSnippet_extends.apply(this, arguments); }
 
 
 
@@ -10310,57 +10406,131 @@ const SendToDeviceSnippet = props => {
 };
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
 function SimpleBelowSearchSnippet_extends() { SimpleBelowSearchSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SimpleBelowSearchSnippet_extends.apply(this, arguments); }
 
 
 
 
 
+
 const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text placeholder in case the prop from the server isn't available
 
 const SimpleBelowSearchSnippet_ICON_ALT_TEXT = "";
 class SimpleBelowSearchSnippet_SimpleBelowSearchSnippet extends external_React_default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onButtonClick = this.onButtonClick.bind(this);
+  }
+
   renderText() {
     const {
       props
     } = this;
-    return external_React_default.a.createElement(RichText["RichText"], {
+    return props.content.text ? external_React_default.a.createElement(RichText["RichText"], {
       text: props.content.text,
       customElements: this.props.customElements,
       localization_id: "text",
       links: props.content.links,
       sendClick: props.sendClick
-    });
+    }) : null;
+  }
+
+  renderTitle() {
+    const {
+      title
+    } = this.props.content;
+    return title ? external_React_default.a.createElement("h3", {
+      className: "title title-inline"
+    }, title, external_React_default.a.createElement("br", null)) : null;
+  }
+
+  async onButtonClick() {
+    if (this.props.provider !== "preview") {
+      this.props.sendUserActionTelemetry({
+        event: "CLICK_BUTTON",
+        id: this.props.UISurface
+      });
+    }
+
+    const {
+      button_url
+    } = this.props.content; // If button_url is defined handle it as OPEN_URL action
+
+    const type = this.props.content.button_action || button_url && "OPEN_URL";
+    await this.props.onAction({
+      type,
+      data: {
+        args: this.props.content.button_action_args || button_url
+      }
+    });
+
+    if (!this.props.content.do_not_autoblock) {
+      this.props.onBlock();
+    }
+  }
+
+  _shouldRenderButton() {
+    return this.props.content.button_action || this.props.onButtonClick || this.props.content.button_url;
+  }
+
+  renderButton() {
+    const {
+      props
+    } = this;
+
+    if (!this._shouldRenderButton()) {
+      return null;
+    }
+
+    return external_React_default.a.createElement(Button, {
+      onClick: props.onButtonClick || this.onButtonClick,
+      color: props.content.button_color,
+      backgroundColor: props.content.button_background_color
+    }, props.content.button_label);
   }
 
   render() {
     const {
       props
     } = this;
     let className = "SimpleBelowSearchSnippet";
+    let containerName = "below-search-snippet";
 
     if (props.className) {
       className += ` ${props.className}`;
     }
 
-    return external_React_default.a.createElement(SnippetBase_SnippetBase, SimpleBelowSearchSnippet_extends({}, props, {
+    if (this._shouldRenderButton()) {
+      className += " withButton";
+      containerName += " withButton";
+    }
+
+    return external_React_default.a.createElement("div", {
+      className: containerName
+    }, external_React_default.a.createElement("div", {
+      className: "snippet-hover-wrapper"
+    }, external_React_default.a.createElement(SnippetBase_SnippetBase, SimpleBelowSearchSnippet_extends({}, props, {
       className: className,
       textStyle: this.props.textStyle
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
       className: "icon icon-light-theme",
       alt: props.content.icon_alt_text || SimpleBelowSearchSnippet_ICON_ALT_TEXT
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
       className: "icon icon-dark-theme",
       alt: props.content.icon_alt_text || SimpleBelowSearchSnippet_ICON_ALT_TEXT
-    }), external_React_default.a.createElement("div", null, external_React_default.a.createElement("p", {
+    }), external_React_default.a.createElement("div", {
+      className: "textContainer"
+    }, this.renderTitle(), external_React_default.a.createElement("p", {
       className: "body"
-    }, this.renderText()), this.props.extraContent));
+    }, this.renderText()), this.props.extraContent), external_React_default.a.createElement("div", {
+      className: "buttonContainer"
+    }, this.renderButton()))));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/template-manifest.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsTemplates", function() { return SnippetsTemplates; });
 
 
 
--- a/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
+++ b/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
@@ -86,16 +86,29 @@ const MESSAGES = () => [
       button_label: "Get one now!",
       button_url: "https://www.mozilla.org/en-US/firefox/accounts",
       text:
         "Sync it, link it, take it with you. All this and more with a Firefox Account.",
       block_button_text: "Block",
     },
   },
   {
+    id: "SIMPLE_TEST_BUTTON_ACTION_1",
+    template: "simple_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      button_label: "Open about:config",
+      button_action: "OPEN_ABOUT_PAGE",
+      button_action_args: "config",
+      text: "Testing the OPEN_ABOUT_PAGE action",
+      block_button_text: "Block",
+    },
+  },
+  {
     id: "SIMPLE_WITH_TITLE_TEST_1",
     template: "simple_snippet",
     content: {
       icon: TEST_ICON,
       icon_dark_theme: TEST_ICON_BW,
       title: "Ready to sync?",
       text: "Get connected with a <syncLink>Firefox account</syncLink>.",
       links: {
@@ -384,16 +397,91 @@ const MESSAGES = () => [
       text:
         "Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>",
       links: {
         syncLink: { url: "https://www.mozilla.org/en-US/firefox/accounts" },
       },
       block_button_text: "Block",
     },
   },
+  {
+    id: "SIMPLE_BELOW_SEARCH_TEST_TITLE",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      title: "See if you've been part of an online data breach.",
+      text:
+        "Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>",
+      links: {
+        syncLink: { url: "https://www.mozilla.org/en-US/firefox/accounts" },
+      },
+      block_button_text: "Block",
+    },
+  },
+  {
+    id: "SPECIAL_SNIPPET_BUTTON_1",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      button_label: "Find Out Now",
+      button_url: "https://www.mozilla.org/en-US/firefox/accounts",
+      title: "See if you've been part of an online data breach.",
+      text: "Firefox Monitor tells you what hackers already know about you.",
+      block_button_text: "Block",
+    },
+  },
+  {
+    id: "SPECIAL_SNIPPET_LONG_CONTENT",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      button_label: "Find Out Now",
+      button_url: "https://www.mozilla.org/en-US/firefox/accounts",
+      title: "See if you've been part of an online data breach.",
+      text:
+        "Firefox Monitor tells you what hackers already know about you. Here's some extra text to make the content really long.",
+      block_button_text: "Block",
+    },
+  },
+  {
+    id: "SPECIAL_SNIPPET_NO_TITLE",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      icon_dark_theme: TEST_ICON_BW,
+      button_label: "Find Out Now",
+      button_url: "https://www.mozilla.org/en-US/firefox/accounts",
+      text: "Firefox Monitor tells you what hackers already know about you.",
+      block_button_text: "Block",
+    },
+  },
+  {
+    id: "SPECIAL_SNIPPET_MONITOR",
+    template: "simple_below_search_snippet",
+    content: {
+      icon: TEST_ICON,
+      title: "See if you've been part of an online data breach.",
+      text: "Firefox Monitor tells you what hackers already know about you.",
+      button_label: "Get monitor",
+      button_action: "ENABLE_FIREFOX_MONITOR",
+      button_action_args: {
+        url:
+          "https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab",
+        flowRequestParams: {
+          entrypoint: "snippets",
+          utm_term: "monitor",
+          form_type: "email",
+        },
+      },
+      block_button_text: "Block",
+    },
+  },
 ];
 
 const SnippetsTestMessageProvider = {
   getMessages() {
     return (
       MESSAGES()
         // Ensures we never actually show test except when triggered by debug tools
         .map(message => ({
--- a/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
@@ -4,16 +4,17 @@ import {
 } from "content-src/asrouter/asrouter-content";
 import { GlobalOverrider } from "test/unit/utils";
 import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
 import { FAKE_LOCAL_MESSAGES } from "./constants";
 import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import React from "react";
 import { mount } from "enzyme";
 import { Trailhead } from "../../../content-src/asrouter/templates/Trailhead/Trailhead";
+import { actionCreators as ac } from "common/Actions.jsm";
 
 let [FAKE_MESSAGE] = FAKE_LOCAL_MESSAGES;
 const FAKE_NEWSLETTER_SNIPPET = FAKE_LOCAL_MESSAGES.find(
   msg => msg.id === "newsletter"
 );
 const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
 const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
   msg => msg.id === "belowsearch"
@@ -58,27 +59,33 @@ describe("ASRouterUtils", () => {
     const [, payload] = fakeSendAsyncMessage.firstCall.args;
     assert.propertyVal(payload.data, "id", 1);
     assert.propertyVal(payload.data, "event", "CLICK");
   });
 });
 
 describe("ASRouterUISurface", () => {
   let wrapper;
-  let global;
+  let globalO;
   let sandbox;
   let headerPortal;
   let footerPortal;
   let fakeDocument;
+  let fetchStub;
 
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     headerPortal = document.createElement("div");
     footerPortal = document.createElement("div");
     sandbox.stub(footerPortal, "querySelector").returns(footerPortal);
+    fetchStub = sandbox.stub(global, "fetch").resolves({
+      ok: true,
+      status: 200,
+      json: () => Promise.resolve({}),
+    });
     fakeDocument = {
       location: { href: "" },
       _listeners: new Set(),
       _visibilityState: "hidden",
       get visibilityState() {
         return this._visibilityState;
       },
       set visibilityState(value) {
@@ -96,31 +103,31 @@ describe("ASRouterUISurface", () => {
       },
       get body() {
         return document.createElement("body");
       },
       getElementById(id) {
         return id === "header-asrouter-container" ? headerPortal : footerPortal;
       },
     };
-    global = new GlobalOverrider();
-    global.set({
+    globalO = new GlobalOverrider();
+    globalO.set({
       RPMAddMessageListener: sandbox.stub(),
       RPMRemoveMessageListener: sandbox.stub(),
       RPMSendAsyncMessage: sandbox.stub(),
     });
 
     sandbox.stub(ASRouterUtils, "sendTelemetry");
 
     wrapper = mount(<ASRouterUISurface document={fakeDocument} />);
   });
 
   afterEach(() => {
     sandbox.restore();
-    global.restore();
+    globalO.restore();
   });
 
   it("should render the component if a message id is defined", () => {
     wrapper.setState({ message: FAKE_MESSAGE });
     assert.isTrue(wrapper.exists());
   });
 
   it("should pass in the correct form_method for newsletter snippets", () => {
@@ -379,9 +386,149 @@ describe("ASRouterUISurface", () => {
         action: "onboarding_user_event",
         event: "DISMISS",
         id: "onboarding-cards",
         message_id: "1,2,3",
         source: "onboarding-cards",
       });
     });
   });
+
+  describe(".fetchFlowParams", () => {
+    let dispatchStub;
+    const assertCalledWithURL = url =>
+      assert.calledWith(fetchStub, new URL(url).toString(), {
+        credentials: "omit",
+      });
+    beforeEach(() => {
+      dispatchStub = sandbox.stub();
+      wrapper = mount(
+        <ASRouterUISurface
+          dispatch={dispatchStub}
+          fxaEndpoint="https://accounts.firefox.com"
+        />
+      );
+    });
+    it("should use the base url returned from the endpoint pref", async () => {
+      wrapper = mount(
+        <ASRouterUISurface
+          dispatch={dispatchStub}
+          fxaEndpoint="https://foo.com"
+        />
+      );
+      await wrapper.instance().fetchFlowParams();
+
+      assertCalledWithURL("https://foo.com/metrics-flow");
+    });
+    it("should add given search params to the URL", async () => {
+      const params = { foo: "1", bar: "2" };
+
+      await wrapper.instance().fetchFlowParams(params);
+
+      assertCalledWithURL(
+        "https://accounts.firefox.com/metrics-flow?foo=1&bar=2"
+      );
+    });
+    it("should return flowId, flowBeginTime, deviceId on a 200 response", async () => {
+      const flowInfo = { flowId: "foo", flowBeginTime: 123, deviceId: "bar" };
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: true,
+        status: 200,
+        json: () => Promise.resolve(flowInfo),
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, flowInfo);
+    });
+    it("should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a non-200 response", async () => {
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: false,
+        status: 400,
+        statusText: "Client error",
+        url: "https://accounts.firefox.com/metrics-flow",
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, {});
+      assert.calledWith(
+        dispatchStub,
+        ac.OnlyToMain({
+          type: "TELEMETRY_UNDESIRED_EVENT",
+          data: {
+            event: "FXA_METRICS_FETCH_ERROR",
+            value: 400,
+          },
+        })
+      );
+    });
+    it("should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a parsing erorr", async () => {
+      fetchStub.withArgs("https://accounts.firefox.com/metrics-flow").resolves({
+        ok: false,
+        status: 200,
+        // No json to parse, throws an error
+      });
+
+      const result = await wrapper.instance().fetchFlowParams();
+      assert.deepEqual(result, {});
+      assert.calledWith(
+        dispatchStub,
+        ac.OnlyToMain({
+          type: "TELEMETRY_UNDESIRED_EVENT",
+          data: { event: "FXA_METRICS_ERROR" },
+        })
+      );
+    });
+
+    describe(".onUserAction", () => {
+      it("if the action.type is ENABLE_FIREFOX_MONITOR, it should generate the right monitor URL given some flowParams", async () => {
+        const flowInfo = { flowId: "foo", flowBeginTime: 123, deviceId: "bar" };
+        fetchStub
+          .withArgs(
+            "https://accounts.firefox.com/metrics-flow?utm_term=avocado"
+          )
+          .resolves({
+            ok: true,
+            status: 200,
+            json: () => Promise.resolve(flowInfo),
+          });
+
+        sandbox.spy(ASRouterUtils, "executeAction");
+
+        const msg = {
+          type: "ENABLE_FIREFOX_MONITOR",
+          data: {
+            args: {
+              url: "https://monitor.firefox.com?foo=bar",
+              flowRequestParams: {
+                utm_term: "avocado",
+              },
+            },
+          },
+        };
+
+        await wrapper.instance().onUserAction(msg);
+
+        assertCalledWithURL(
+          "https://accounts.firefox.com/metrics-flow?utm_term=avocado"
+        );
+        assert.calledWith(ASRouterUtils.executeAction, {
+          type: "OPEN_URL",
+          data: {
+            args: new URL(
+              "https://monitor.firefox.com?foo=bar&deviceId=bar&flowId=foo&flowBeginTime=123"
+            ).toString(),
+          },
+        });
+      });
+      it("if the action.type is not ENABLE_FIREFOX_MONITOR, it should just call ASRouterUtils.executeAction", async () => {
+        const msg = {
+          type: "FOO",
+          data: {
+            args: "bar",
+          },
+        };
+        sandbox.spy(ASRouterUtils, "executeAction");
+        await wrapper.instance().onUserAction(msg);
+        assert.calledWith(ASRouterUtils.executeAction, msg);
+      });
+    });
+  });
 });