Bug 1576284 - Add Firefox wordmark, protection template and bug fixes to New Tab Page r=pdahiya
☠☠ backed out by 4f018843d990 ☠ ☠
authorEd Lee <edilee@mozilla.com>
Sat, 24 Aug 2019 00:54:23 +0000
changeset 553439 2aa1a97003cb26bc0fe0aa35f46ba979c70ca09a
parent 553438 faf094c9cd08f5ad349b3bb8ee6d9b9c36061f15
child 553440 3177490c6c0fba5c703a08d129ec55f3fd53bece
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspdahiya
bugs1576284
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1576284 - Add Firefox wordmark, protection template and bug fixes to New Tab Page r=pdahiya Differential Revision: https://phabricator.services.mozilla.com/D43310
browser/components/newtab/common/Reducers.jsm
browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
browser/components/newtab/content-src/asrouter/templates/FirstRun/Interrupt.jsx
browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json
browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss
browser/components/newtab/content-src/components/Search/_Search.scss
browser/components/newtab/content-src/styles/_activity-stream.scss
browser/components/newtab/content-src/styles/_variables.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/data/content/assets/firefox-wordmark.svg
browser/components/newtab/data/content/assets/glyph-caret-right.svg
browser/components/newtab/data/content/assets/protection-report-icon.png
browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ASRouterTargeting.jsm
browser/components/newtab/lib/DiscoveryStreamFeed.jsm
browser/components/newtab/lib/DownloadsManager.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/lib/PanelTestProvider.jsm
browser/components/newtab/lib/ToolbarPanelHub.jsm
browser/components/newtab/mochitest.sh
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
browser/components/newtab/test/unit/common/Reducers.test.js
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx
browser/components/newtab/test/unit/content-src/components/ReturnToAMO.test.jsx
browser/components/newtab/test/unit/content-src/components/StartupOverlay.test.jsx
browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
--- a/browser/components/newtab/common/Reducers.jsm
+++ b/browser/components/newtab/common/Reducers.jsm
@@ -57,16 +57,17 @@ const INITIAL_STATE = {
     feeds: {
       data: {
         // "https://foo.com/feed1": {lastUpdated: 123, data: []}
       },
       loaded: false,
     },
     spocs: {
       spocs_endpoint: "",
+      spocs_per_domain: 1,
       lastUpdated: null,
       data: {}, // {spocs: []}
       loaded: false,
       frequency_caps: [],
       blocked: [],
     },
   },
   Search: {
@@ -592,17 +593,21 @@ function DiscoveryStream(prevState = INI
         },
       };
     case at.DISCOVERY_STREAM_SPOCS_ENDPOINT:
       return {
         ...prevState,
         spocs: {
           ...INITIAL_STATE.DiscoveryStream.spocs,
           spocs_endpoint:
-            action.data || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
+            action.data.url ||
+            INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
+          spocs_per_domain:
+            action.data.spocs_per_domain ||
+            INITIAL_STATE.DiscoveryStream.spocs.spocs_per_domain,
         },
       };
     case at.DISCOVERY_STREAM_SPOCS_UPDATE:
       if (action.data) {
         return {
           ...prevState,
           spocs: {
             ...prevState.spocs,
--- a/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
+++ b/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
@@ -33,16 +33,17 @@ Please note that some targeting attribut
 * [usesFirefoxSync](#usesfirefoxsync)
 * [isFxAEnabled](#isFxAEnabled)
 * [xpinstallEnabled](#xpinstallEnabled)
 * [hasPinnedTabs](#haspinnedtabs)
 * [hasAccessedFxAPanel](#hasaccessedfxapanel)
 * [isWhatsNewPanelEnabled](#iswhatsnewpanelenabled)
 * [earliestFirefoxVersion](#earliestfirefoxversion)
 * [isFxABadgeEnabled](#isfxabadgeenabled)
+* [totalBlockedCount](#totalblockedcount)
 
 ## Detailed usage
 
 ### `addonsInfo`
 Provides information about the add-ons the user has installed.
 
 Note that the `name`, `userDisabled`, and `installDate` is only available if `isFullData` is `true` (this is usually not the case right at start-up).
 
@@ -513,8 +514,18 @@ declare const earliestFirefoxVersion: bo
 
 Boolean pref that controls if the FxA toolbar button is badged by Messaging System.
 
 #### Definition
 
 ```ts
 declare const isFxABadgeEnabled: boolean;
 ```
+
+### `totalBlockedCount`
+
+Total number of events from the content blocking database
+
+#### Definition
+
+```ts
+declare const totalBlockedCount: number;
+```
--- a/browser/components/newtab/content-src/asrouter/templates/FirstRun/Interrupt.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/Interrupt.jsx
@@ -26,26 +26,28 @@ export class Interrupt extends React.Pur
     switch (message.template) {
       case "return_to_amo_overlay":
         return (
           <LocalizationProvider
             bundles={generateBundles({ amo_html: message.content.text })}
           >
             <ReturnToAMO
               {...message}
+              document={this.props.document}
               UISurface="NEWTAB_OVERLAY"
               onBlock={onDismiss}
               onAction={executeAction}
               sendUserActionTelemetry={sendUserActionTelemetry}
             />
           </LocalizationProvider>
         );
       case "fxa_overlay":
         return (
           <StartupOverlay
+            document={this.props.document}
             onBlock={onDismiss}
             dispatch={dispatch}
             fxa_endpoint={fxaEndpoint}
           />
         );
       case "trailhead":
         return (
           <Trailhead
--- a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json
@@ -19,26 +19,36 @@
           },
           "required": ["string_id"],
           "description": "Id of localized string to be rendered."
         }
       ]
     }
   },
   "properties": {
+    "layout": {
+      "description": "Different message layouts",
+     "enum": ["tracking-protections"]
+    },
     "published_date": {
       "type": "integer",
       "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
     },
     "title": {
       "allOf": [
         {"$ref": "#/definitions/localizableText"},
         {"description": "Id of localized string or message override of What's New message title"}
       ]
     },
+    "subtitle": {
+      "allOf": [
+        {"$ref": "#/definitions/localizableText"},
+        {"description": "Id of localized string or message override of What's New message subtitle"}
+      ]
+    },
     "body": {
       "allOf": [
         {"$ref": "#/definitions/localizableText"},
         {"description": "Id of localized string or message override of What's New message body"}
       ]
     },
     "link_text": {
       "allOf": [
@@ -46,16 +56,20 @@
         {"description": "(optional) Id of localized string or message override of What's New message link text"}
       ]
     },
     "cta_url": {
       "description": "Target URL for the What's New message.",
       "type": "string",
       "format": "uri"
     },
+    "cta_type": {
+      "description": "Type of url open action",
+      "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE"]
+    },
     "icon_url": {
       "description": "(optional) URL for the What's New message icon.",
       "type": "string",
       "format": "uri"
     },
     "icon_alt": {
       "description": "Alt text for image.",
       "type": "string"
--- a/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
@@ -19,16 +19,20 @@ export class ReturnToAMO extends React.P
     global.document.body.classList.add("amo");
   }
 
   componentDidMount() {
     this.props.sendUserActionTelemetry({
       event: "IMPRESSION",
       id: this.props.UISurface,
     });
+    // Hide the page content from screen readers while the modal is open
+    this.props.document
+      .getElementById("root")
+      .setAttribute("aria-hidden", "true");
   }
 
   onClickAddExtension() {
     this.props.onAction(this.props.content.primary_button.action);
     this.props.sendUserActionTelemetry({
       event: "INSTALL",
       id: this.props.UISurface,
     });
@@ -36,16 +40,20 @@ export class ReturnToAMO extends React.P
 
   onBlockButton() {
     this.props.onBlock();
     document.body.classList.remove("welcome", "hide-main", "amo");
     this.props.sendUserActionTelemetry({
       event: "BLOCK",
       id: this.props.UISurface,
     });
+    // Re-enable the document for screen readers
+    this.props.document
+      .getElementById("root")
+      .setAttribute("aria-hidden", "false");
   }
 
   renderText() {
     const customElement = (
       <img
         src={this.props.content.addon_icon}
         width="20px"
         height="20px"
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss
@@ -86,17 +86,22 @@
       @include full-width-styles;
     }
 
     @media (max-width: 1120px) {
       margin: 0 60px;
     }
 
     @media (max-width: 865px) {
-      margin: 0 60px 0 0;
+      margin-inline-start: 0;
+    }
+
+    // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 610px.
+    @media (max-width: $break-point-medium - 1px) {
+      margin: auto;
     }
 
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
       margin: auto;
     }
   }
@@ -137,16 +142,20 @@
       height: 24px;
       width: 24px;
     }
 
     @media (min-width: $break-point-medium) {
       @include full-width-styles;
     }
 
+    @media (max-width: $break-point-medium) {
+      margin: auto;
+    }
+
     // Disable breakpoints for now if discovery stream is enabled.
     .ds-outer-wrapper-breakpoint-override & {
       @include full-width-styles;
     }
   }
 
   &.withButton {
     line-height: 20px;
@@ -165,41 +174,51 @@
         opacity: 1;
         box-shadow: none;
       }
 
       @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;
+
+      @media (max-width: $break-point-medium) {
+        margin: auto;
+      }
     }
 
     .buttonContainer {
       margin: auto;
       margin-inline-end: 0;
+
+      @media (max-width: $break-point-medium) {
+        margin: auto;
+      }
+    }
+  }
+
+  button {
+    @media (max-width: $break-point-medium) {
+      margin: auto;
     }
   }
 
   .body {
     display: inline;
     position: sticky;
     transform: translateY(-50%);
     margin: 8px 0 0;
--- a/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
@@ -28,22 +28,30 @@ export class StartupOverlay extends Reac
   }
 
   componentDidMount() {
     // Timeout to allow the scene to render once before attaching the attribute
     // to trigger the animation.
     setTimeout(() => {
       this.setState({ show: true });
     }, 10);
+    // Hide the page content from screen readers while the modal is open
+    this.props.document
+      .getElementById("root")
+      .setAttribute("aria-hidden", "true");
   }
 
   removeOverlay() {
     window.removeEventListener("visibilitychange", this.removeOverlay);
     document.body.classList.remove("hide-main", "fxa");
     this.setState({ show: false });
+    // Re-enable the document for screen readers
+    this.props.document
+      .getElementById("root")
+      .setAttribute("aria-hidden", "false");
 
     setTimeout(() => {
       // Allow scrolling and fully remove overlay after animation finishes.
       this.props.onBlock();
       document.body.classList.remove("welcome");
     }, 400);
   }
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
@@ -2,16 +2,17 @@
  * 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 } from "common/Actions.jsm";
 import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
 import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection";
 import { connect } from "react-redux";
 import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage";
+import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo";
 import { Hero } from "content-src/components/DiscoveryStreamComponents/Hero/Hero";
 import { Highlights } from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights";
 import { HorizontalRule } from "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule";
 import { List } from "content-src/components/DiscoveryStreamComponents/List/List";
 import { Navigation } from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation";
 import React from "react";
 import { SectionTitle } from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle";
 import { selectLayoutRender } from "content-src/lib/selectLayoutRender";
@@ -109,16 +110,27 @@ export class _DiscoveryStreamBase extend
   }
 
   renderComponent(component, embedWidth) {
     switch (component.type) {
       case "Highlights":
         return <Highlights />;
       case "TopSites":
         return <TopSites header={component.header} />;
+      case "TextPromo":
+        return (
+          <DSTextPromo
+            image={component.properties.image_src}
+            alt_text={component.properties.alt_text}
+            header={component.properties.excerpt}
+            cta_text={component.properties.cta_text}
+            cta_url={component.properties.cta_url}
+            subtitle={component.properties.context}
+          />
+        );
       case "Message":
         return (
           <DSMessage
             title={component.header && component.header.title}
             subtitle={component.header && component.header.subtitle}
             link_text={component.header && component.header.link_text}
             link_url={component.header && component.header.link_url}
             icon={component.header && component.header.icon}
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -5,45 +5,47 @@
 import { actionCreators as ac } from "common/Actions.jsm";
 import { DSImage } from "../DSImage/DSImage.jsx";
 import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu";
 import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
 import React from "react";
 import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
 import { DSContextFooter } from "../DSContextFooter/DSContextFooter.jsx";
 
+// Default Meta that displays CTA as link if cta_variant in layout is set as "link"
 export const DefaultMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
   engagement,
+  cta_variant,
 }) => (
   <div className="meta">
     <div className="info-wrap">
       <p className="source clamp">{source}</p>
       <header className="title clamp">{title}</header>
       {excerpt && <p className="excerpt clamp">{excerpt}</p>}
-      {cta && (
+      {cta_variant === "link" && cta && (
         <div role="link" className="cta-link icon icon-arrow" tabIndex="0">
           {cta}
         </div>
       )}
     </div>
     <DSContextFooter
       context_type={context_type}
       context={context}
       engagement={engagement}
     />
   </div>
 );
 
-export const VariantMeta = ({
+export const CTAButtonMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
   engagement,
   sponsor,
@@ -142,52 +144,54 @@ export class DSCard extends React.PureCo
   }
 
   render() {
     if (this.props.placeholder || !this.state.isSeen) {
       return (
         <div className="ds-card placeholder" ref={this.setPlaceholderRef} />
       );
     }
+    const isButtonCTA = this.props.cta_variant === "button";
+
     return (
       <div className="ds-card">
         <SafeAnchor
           className="ds-card-link"
           dispatch={this.props.dispatch}
           onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined}
           url={this.props.url}
         >
           <div className="img-wrapper">
             <DSImage
               extraClassNames="img"
               source={this.props.image_src}
               rawSource={this.props.raw_image_src}
             />
           </div>
-          {this.props.cta_variant && (
-            <VariantMeta
+          {isButtonCTA ? (
+            <CTAButtonMeta
               source={this.props.source}
               title={this.props.title}
               excerpt={this.props.excerpt}
               context={this.props.context}
               context_type={this.props.context_type}
               engagement={this.props.engagement}
               cta={this.props.cta}
               sponsor={this.props.sponsor}
             />
-          )}
-          {!this.props.cta_variant && (
+          ) : (
             <DefaultMeta
               source={this.props.source}
               title={this.props.title}
               excerpt={this.props.excerpt}
               context={this.props.context}
               engagement={this.props.engagement}
               context_type={this.props.context_type}
               cta={this.props.cta}
+              cta_variant={this.props.cta_variant}
             />
           )}
           <ImpressionStats
             campaignId={this.props.campaignId}
             rows={[
               {
                 id: this.props.id,
                 pos: this.props.pos,
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
@@ -0,0 +1,25 @@
+/* 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 React from "react";
+import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
+
+export class DSTextPromo extends React.PureComponent {
+  render() {
+    return (
+      <div className="ds-text-promo">
+        <img src={this.props.image} alt={this.props.alt_text} />
+        <div className="text">
+          <h3>
+            {`${this.props.header}\u2003`}
+            <SafeAnchor className="ds-chevron-link" url={this.props.cta_url}>
+              {this.props.cta_text}
+            </SafeAnchor>
+          </h3>
+          <p className="subtitle">{this.props.subtitle}</p>
+        </div>
+      </div>
+    );
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss
@@ -0,0 +1,87 @@
+.ds-text-promo {
+  display: flex;
+  max-width: 744px;
+  margin: 16px auto;
+
+  img {
+    width: 40px;
+    height: 40px;
+    margin: 0 12px 0 0;
+    border-radius: 4px;
+  }
+
+  .text {
+    line-height: 24px;
+    margin: -4.5px 0 0;
+  }
+
+  h3 {
+    @include dark-theme-only {
+      color: $grey-10;
+    }
+
+    margin: 0;
+    font-weight: 600;
+    font-size: 15px;
+  }
+
+  .subtitle {
+    @include dark-theme-only {
+      color: $grey-40;
+    }
+
+    font-size: 13px;
+    margin: 0;
+    color: $grey-50;
+  }
+}
+
+.ds-chevron-link {
+  color: $blue-60;
+  display: inline-block;
+  outline: 0;
+
+  &:hover {
+    text-decoration: underline;
+  }
+
+  &:active {
+    @include dark-theme-only {
+      color: $blue-50;
+    }
+
+    color: $blue-70;
+
+    &::after {
+      @include dark-theme-only {
+        background-color: $blue-50;
+      }
+
+      background-color: $blue-70;
+    }
+  }
+
+  &:focus {
+    @include dark-theme-only {
+      box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50;
+    }
+
+    box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50;
+    border-radius: 2px;
+  }
+
+  &::after {
+    @include dark-theme-only {
+      background-color: $blue-40;
+    }
+
+    content: ' ';
+    mask: url('#{$image-path}glyph-caret-right.svg') 0 -8px no-repeat;
+    background-color: $blue-60;
+    margin: 0 0 0 4px;
+    width: 5px;
+    height: 8px;
+    text-decoration: none;
+    display: inline-block;
+  }
+}
--- a/browser/components/newtab/content-src/components/Search/_Search.scss
+++ b/browser/components/newtab/content-src/components/Search/_Search.scss
@@ -8,18 +8,18 @@
 .search-wrapper {
   padding: 34px 0 64px;
 
   .only-search & {
     padding: 0 0 64px;
   }
 
   .logo-and-wordmark {
-    $logo-size: 97px;
-    $wordmark-size: 142px;
+    $logo-size: 96px;
+    $wordmark-size: 172px;
 
     align-items: center;
     display: flex;
     justify-content: center;
     margin-bottom: 49px;
 
     .logo {
       background: url('chrome://branding/content/icon128.png') no-repeat center center;
--- a/browser/components/newtab/content-src/styles/_activity-stream.scss
+++ b/browser/components/newtab/content-src/styles/_activity-stream.scss
@@ -157,16 +157,17 @@ input {
 @import '../components/DiscoveryStreamComponents/TopSites/TopSites';
 @import '../components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu';
 @import '../components/DiscoveryStreamComponents/DSCard/DSCard';
 @import '../components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter';
 @import '../components/DiscoveryStreamComponents/DSImage/DSImage';
 @import '../components/DiscoveryStreamComponents/DSMessage/DSMessage';
 @import '../components/DiscoveryStreamImpressionStats/ImpressionStats';
 @import '../components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState';
+@import '../components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo';
 
 // AS Router
 @import '../asrouter/components/Button/Button';
 @import '../asrouter/components/SnippetBase/SnippetBase';
 @import '../asrouter/components/ModalOverlay/ModalOverlay';
 @import '../asrouter/templates/ReturnToAMO/ReturnToAMO';
 @import '../asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet';
 @import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
--- a/browser/components/newtab/content-src/styles/_variables.scss
+++ b/browser/components/newtab/content-src/styles/_variables.scss
@@ -48,16 +48,17 @@
 $grey-90-60: rgba($grey-90, 0.6);
 $grey-90-70: rgba($grey-90, 0.7);
 $grey-90-80: rgba($grey-90, 0.8);
 $grey-90-90: rgba($grey-90, 0.9);
 
 $blue-40-40: rgba($blue-40, 0.4);
 $blue-50-50: rgba($blue-50, 0.5);
 $blue-50-30: rgba($blue-50, 0.3);
+$blue-50-50: rgba($blue-50, 0.5);
 
 $black: #000;
 $black-5: rgba($black, 0.05);
 $black-10: rgba($black, 0.1);
 $black-12: rgba($black, 0.12);
 $black-15: rgba($black, 0.15);
 $black-20: rgba($black, 0.2);
 $black-25: rgba($black, 0.25);
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -1015,29 +1015,29 @@ main {
     padding: 0 0 64px; }
   .search-wrapper .logo-and-wordmark {
     align-items: center;
     display: flex;
     justify-content: center;
     margin-bottom: 49px; }
     .search-wrapper .logo-and-wordmark .logo {
       background: url("chrome://branding/content/icon128.png") no-repeat center center;
-      background-size: 97px;
+      background-size: 96px;
       display: inline-block;
-      height: 97px;
-      width: 97px; }
+      height: 96px;
+      width: 96px; }
     .search-wrapper .logo-and-wordmark .wordmark {
       background: url("../data/content/assets/firefox-wordmark.svg") no-repeat center center;
-      background-size: 142px;
+      background-size: 172px;
       -moz-context-properties: fill;
       display: inline-block;
       fill: var(--newtab-search-wordmark-color);
-      height: 97px;
+      height: 96px;
       margin-inline-start: 15px;
-      width: 142px; }
+      width: 172px; }
     @media (max-width: 609px) {
       .search-wrapper .logo-and-wordmark .logo {
         background-size: 64px;
         height: 64px;
         width: 64px; }
       .search-wrapper .logo-and-wordmark .wordmark {
         background-size: 100px;
         height: 64px;
@@ -2971,16 +2971,72 @@ main {
     margin: 0; }
   .section-empty-state p {
     margin: 0; }
 
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
+.ds-text-promo {
+  display: flex;
+  max-width: 744px;
+  margin: 16px auto; }
+  .ds-text-promo img {
+    width: 40px;
+    height: 40px;
+    margin: 0 12px 0 0;
+    border-radius: 4px; }
+  .ds-text-promo .text {
+    line-height: 24px;
+    margin: -4.5px 0 0; }
+  .ds-text-promo h3 {
+    margin: 0;
+    font-weight: 600;
+    font-size: 15px; }
+    [lwt-newtab-brighttext] .ds-text-promo h3 {
+      color: #F9F9FA; }
+  .ds-text-promo .subtitle {
+    font-size: 13px;
+    margin: 0;
+    color: #737373; }
+    [lwt-newtab-brighttext] .ds-text-promo .subtitle {
+      color: #B1B1B3; }
+
+.ds-chevron-link {
+  color: #0060DF;
+  display: inline-block;
+  outline: 0; }
+  .ds-chevron-link:hover {
+    text-decoration: underline; }
+  .ds-chevron-link:active {
+    color: #003EAA; }
+    [lwt-newtab-brighttext] .ds-chevron-link:active {
+      color: #0A84FF; }
+    .ds-chevron-link:active::after {
+      background-color: #003EAA; }
+      [lwt-newtab-brighttext] .ds-chevron-link:active::after {
+        background-color: #0A84FF; }
+  .ds-chevron-link:focus {
+    box-shadow: 0 0 0 2px #FFF, 0 0 0 5px rgba(10, 132, 255, 0.5);
+    border-radius: 2px; }
+    [lwt-newtab-brighttext] .ds-chevron-link:focus {
+      box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
+  .ds-chevron-link::after {
+    content: ' ';
+    mask: url("../data/content/assets/glyph-caret-right.svg") 0 -8px no-repeat;
+    background-color: #0060DF;
+    margin: 0 0 0 4px;
+    width: 5px;
+    height: 8px;
+    text-decoration: none;
+    display: inline-block; }
+    [lwt-newtab-brighttext] .ds-chevron-link::after {
+      background-color: #45A1FF; }
+
 .ASRouterButton {
   font-weight: 600;
   font-size: 14px;
   white-space: nowrap;
   border-radius: 2px;
   border: 0;
   font-family: inherit;
   padding: 8px 15px;
@@ -3354,17 +3410,20 @@ body[lwt-newtab-brighttext] .scene2Icon 
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
     @media (max-width: 1120px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
-        margin: 0 60px 0 0; } }
+        margin-inline-start: 0; } }
+    @media (max-width: 609px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: transparent;
       border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
       text-align: inherit;
@@ -3389,16 +3448,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
     margin-top: 8px;
     margin-inline-start: 12px;
     height: 32px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         width: 24px; } }
+    @media (max-width: 610px) {
+      .SimpleBelowSearchSnippet .icon {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
@@ -3409,34 +3471,40 @@ body[lwt-newtab-brighttext] .scene2Icon 
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
       @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; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          margin: auto; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto;
       margin-inline-end: 0; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .buttonContainer {
+          margin: auto; } }
+  @media (max-width: 610px) {
+    .SimpleBelowSearchSnippet button {
+      margin: auto; } }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     position: sticky;
     transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -1018,29 +1018,29 @@ main {
     padding: 0 0 64px; }
   .search-wrapper .logo-and-wordmark {
     align-items: center;
     display: flex;
     justify-content: center;
     margin-bottom: 49px; }
     .search-wrapper .logo-and-wordmark .logo {
       background: url("chrome://branding/content/icon128.png") no-repeat center center;
-      background-size: 97px;
+      background-size: 96px;
       display: inline-block;
-      height: 97px;
-      width: 97px; }
+      height: 96px;
+      width: 96px; }
     .search-wrapper .logo-and-wordmark .wordmark {
       background: url("../data/content/assets/firefox-wordmark.svg") no-repeat center center;
-      background-size: 142px;
+      background-size: 172px;
       -moz-context-properties: fill;
       display: inline-block;
       fill: var(--newtab-search-wordmark-color);
-      height: 97px;
+      height: 96px;
       margin-inline-start: 15px;
-      width: 142px; }
+      width: 172px; }
     @media (max-width: 609px) {
       .search-wrapper .logo-and-wordmark .logo {
         background-size: 64px;
         height: 64px;
         width: 64px; }
       .search-wrapper .logo-and-wordmark .wordmark {
         background-size: 100px;
         height: 64px;
@@ -2974,16 +2974,72 @@ main {
     margin: 0; }
   .section-empty-state p {
     margin: 0; }
 
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
+.ds-text-promo {
+  display: flex;
+  max-width: 744px;
+  margin: 16px auto; }
+  .ds-text-promo img {
+    width: 40px;
+    height: 40px;
+    margin: 0 12px 0 0;
+    border-radius: 4px; }
+  .ds-text-promo .text {
+    line-height: 24px;
+    margin: -4.5px 0 0; }
+  .ds-text-promo h3 {
+    margin: 0;
+    font-weight: 600;
+    font-size: 15px; }
+    [lwt-newtab-brighttext] .ds-text-promo h3 {
+      color: #F9F9FA; }
+  .ds-text-promo .subtitle {
+    font-size: 13px;
+    margin: 0;
+    color: #737373; }
+    [lwt-newtab-brighttext] .ds-text-promo .subtitle {
+      color: #B1B1B3; }
+
+.ds-chevron-link {
+  color: #0060DF;
+  display: inline-block;
+  outline: 0; }
+  .ds-chevron-link:hover {
+    text-decoration: underline; }
+  .ds-chevron-link:active {
+    color: #003EAA; }
+    [lwt-newtab-brighttext] .ds-chevron-link:active {
+      color: #0A84FF; }
+    .ds-chevron-link:active::after {
+      background-color: #003EAA; }
+      [lwt-newtab-brighttext] .ds-chevron-link:active::after {
+        background-color: #0A84FF; }
+  .ds-chevron-link:focus {
+    box-shadow: 0 0 0 2px #FFF, 0 0 0 5px rgba(10, 132, 255, 0.5);
+    border-radius: 2px; }
+    [lwt-newtab-brighttext] .ds-chevron-link:focus {
+      box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
+  .ds-chevron-link::after {
+    content: ' ';
+    mask: url("../data/content/assets/glyph-caret-right.svg") 0 -8px no-repeat;
+    background-color: #0060DF;
+    margin: 0 0 0 4px;
+    width: 5px;
+    height: 8px;
+    text-decoration: none;
+    display: inline-block; }
+    [lwt-newtab-brighttext] .ds-chevron-link::after {
+      background-color: #45A1FF; }
+
 .ASRouterButton {
   font-weight: 600;
   font-size: 14px;
   white-space: nowrap;
   border-radius: 2px;
   border: 0;
   font-family: inherit;
   padding: 8px 15px;
@@ -3357,17 +3413,20 @@ body[lwt-newtab-brighttext] .scene2Icon 
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
     @media (max-width: 1120px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
-        margin: 0 60px 0 0; } }
+        margin-inline-start: 0; } }
+    @media (max-width: 609px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: transparent;
       border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
       text-align: inherit;
@@ -3392,16 +3451,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
     margin-top: 8px;
     margin-inline-start: 12px;
     height: 32px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         width: 24px; } }
+    @media (max-width: 610px) {
+      .SimpleBelowSearchSnippet .icon {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
@@ -3412,34 +3474,40 @@ body[lwt-newtab-brighttext] .scene2Icon 
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
       @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; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          margin: auto; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto;
       margin-inline-end: 0; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .buttonContainer {
+          margin: auto; } }
+  @media (max-width: 610px) {
+    .SimpleBelowSearchSnippet button {
+      margin: auto; } }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     position: sticky;
     transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -1015,29 +1015,29 @@ main {
     padding: 0 0 64px; }
   .search-wrapper .logo-and-wordmark {
     align-items: center;
     display: flex;
     justify-content: center;
     margin-bottom: 49px; }
     .search-wrapper .logo-and-wordmark .logo {
       background: url("chrome://branding/content/icon128.png") no-repeat center center;
-      background-size: 97px;
+      background-size: 96px;
       display: inline-block;
-      height: 97px;
-      width: 97px; }
+      height: 96px;
+      width: 96px; }
     .search-wrapper .logo-and-wordmark .wordmark {
       background: url("../data/content/assets/firefox-wordmark.svg") no-repeat center center;
-      background-size: 142px;
+      background-size: 172px;
       -moz-context-properties: fill;
       display: inline-block;
       fill: var(--newtab-search-wordmark-color);
-      height: 97px;
+      height: 96px;
       margin-inline-start: 15px;
-      width: 142px; }
+      width: 172px; }
     @media (max-width: 609px) {
       .search-wrapper .logo-and-wordmark .logo {
         background-size: 64px;
         height: 64px;
         width: 64px; }
       .search-wrapper .logo-and-wordmark .wordmark {
         background-size: 100px;
         height: 64px;
@@ -2971,16 +2971,72 @@ main {
     margin: 0; }
   .section-empty-state p {
     margin: 0; }
 
 @keyframes spinner {
   to {
     transform: rotate(360deg); } }
 
+.ds-text-promo {
+  display: flex;
+  max-width: 744px;
+  margin: 16px auto; }
+  .ds-text-promo img {
+    width: 40px;
+    height: 40px;
+    margin: 0 12px 0 0;
+    border-radius: 4px; }
+  .ds-text-promo .text {
+    line-height: 24px;
+    margin: -4.5px 0 0; }
+  .ds-text-promo h3 {
+    margin: 0;
+    font-weight: 600;
+    font-size: 15px; }
+    [lwt-newtab-brighttext] .ds-text-promo h3 {
+      color: #F9F9FA; }
+  .ds-text-promo .subtitle {
+    font-size: 13px;
+    margin: 0;
+    color: #737373; }
+    [lwt-newtab-brighttext] .ds-text-promo .subtitle {
+      color: #B1B1B3; }
+
+.ds-chevron-link {
+  color: #0060DF;
+  display: inline-block;
+  outline: 0; }
+  .ds-chevron-link:hover {
+    text-decoration: underline; }
+  .ds-chevron-link:active {
+    color: #003EAA; }
+    [lwt-newtab-brighttext] .ds-chevron-link:active {
+      color: #0A84FF; }
+    .ds-chevron-link:active::after {
+      background-color: #003EAA; }
+      [lwt-newtab-brighttext] .ds-chevron-link:active::after {
+        background-color: #0A84FF; }
+  .ds-chevron-link:focus {
+    box-shadow: 0 0 0 2px #FFF, 0 0 0 5px rgba(10, 132, 255, 0.5);
+    border-radius: 2px; }
+    [lwt-newtab-brighttext] .ds-chevron-link:focus {
+      box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
+  .ds-chevron-link::after {
+    content: ' ';
+    mask: url("../data/content/assets/glyph-caret-right.svg") 0 -8px no-repeat;
+    background-color: #0060DF;
+    margin: 0 0 0 4px;
+    width: 5px;
+    height: 8px;
+    text-decoration: none;
+    display: inline-block; }
+    [lwt-newtab-brighttext] .ds-chevron-link::after {
+      background-color: #45A1FF; }
+
 .ASRouterButton {
   font-weight: 600;
   font-size: 14px;
   white-space: nowrap;
   border-radius: 2px;
   border: 0;
   font-family: inherit;
   padding: 8px 15px;
@@ -3354,17 +3410,20 @@ body[lwt-newtab-brighttext] .scene2Icon 
         flex-direction: row;
         padding: 0;
         text-align: inherit; } }
     @media (max-width: 1120px) {
       .SimpleBelowSearchSnippet .innerWrapper {
         margin: 0 60px; } }
     @media (max-width: 865px) {
       .SimpleBelowSearchSnippet .innerWrapper {
-        margin: 0 60px 0 0; } }
+        margin-inline-start: 0; } }
+    @media (max-width: 609px) {
+      .SimpleBelowSearchSnippet .innerWrapper {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .innerWrapper {
       align-items: flex-start;
       background-color: transparent;
       border-radius: 4px;
       box-shadow: none;
       flex-direction: row;
       padding: 0;
       text-align: inherit;
@@ -3389,16 +3448,19 @@ body[lwt-newtab-brighttext] .scene2Icon 
     margin-top: 8px;
     margin-inline-start: 12px;
     height: 32px;
     width: 32px; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .icon {
         height: 24px;
         width: 24px; } }
+    @media (max-width: 610px) {
+      .SimpleBelowSearchSnippet .icon {
+        margin: auto; } }
     .ds-outer-wrapper-breakpoint-override .SimpleBelowSearchSnippet .icon {
       height: 24px;
       width: 24px; }
   .SimpleBelowSearchSnippet.withButton {
     line-height: 20px;
     margin-bottom: 10px;
     min-height: 60px;
     background-color: transparent; }
@@ -3409,34 +3471,40 @@ body[lwt-newtab-brighttext] .scene2Icon 
       margin: auto;
       top: unset; }
       .SimpleBelowSearchSnippet.withButton .blockButton:focus {
         opacity: 1;
         box-shadow: none; }
       @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; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .icon {
+          margin: auto; } }
     .SimpleBelowSearchSnippet.withButton .buttonContainer {
       margin: auto;
       margin-inline-end: 0; }
+      @media (max-width: 610px) {
+        .SimpleBelowSearchSnippet.withButton .buttonContainer {
+          margin: auto; } }
+  @media (max-width: 610px) {
+    .SimpleBelowSearchSnippet button {
+      margin: auto; } }
   .SimpleBelowSearchSnippet .body {
     display: inline;
     position: sticky;
     transform: translateY(-50%);
     margin: 8px 0 0; }
     @media (min-width: 610px) {
       .SimpleBelowSearchSnippet .body {
         margin: 12px 0; } }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -3137,34 +3137,38 @@ class ReturnToAMO extends react__WEBPACK
   componentWillMount() {
     global.document.body.classList.add("amo");
   }
 
   componentDidMount() {
     this.props.sendUserActionTelemetry({
       event: "IMPRESSION",
       id: this.props.UISurface
-    });
+    }); // Hide the page content from screen readers while the modal is open
+
+    this.props.document.getElementById("root").setAttribute("aria-hidden", "true");
   }
 
   onClickAddExtension() {
     this.props.onAction(this.props.content.primary_button.action);
     this.props.sendUserActionTelemetry({
       event: "INSTALL",
       id: this.props.UISurface
     });
   }
 
   onBlockButton() {
     this.props.onBlock();
     document.body.classList.remove("welcome", "hide-main", "amo");
     this.props.sendUserActionTelemetry({
       event: "BLOCK",
       id: this.props.UISurface
-    });
+    }); // Re-enable the document for screen readers
+
+    this.props.document.getElementById("root").setAttribute("aria-hidden", "false");
   }
 
   renderText() {
     const customElement = react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
       src: this.props.content.addon_icon,
       width: "20px",
       height: "20px",
       alt: ICON_ALT_TEXT
@@ -3242,25 +3246,29 @@ class StartupOverlay extends react__WEBP
 
   componentDidMount() {
     // Timeout to allow the scene to render once before attaching the attribute
     // to trigger the animation.
     setTimeout(() => {
       this.setState({
         show: true
       });
-    }, 10);
+    }, 10); // Hide the page content from screen readers while the modal is open
+
+    this.props.document.getElementById("root").setAttribute("aria-hidden", "true");
   }
 
   removeOverlay() {
     window.removeEventListener("visibilitychange", this.removeOverlay);
     document.body.classList.remove("hide-main", "fxa");
     this.setState({
       show: false
-    });
+    }); // Re-enable the document for screen readers
+
+    this.props.document.getElementById("root").setAttribute("aria-hidden", "false");
     setTimeout(() => {
       // Allow scrolling and fully remove overlay after animation finishes.
       this.props.onBlock();
       document.body.classList.remove("welcome");
     }, 400);
   }
 
   onInputChange(e) {
@@ -7926,45 +7934,47 @@ class DSContextFooter_DSContextFooter ex
  * 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/. */
 
 
 
 
 
 
+ // Default Meta that displays CTA as link if cta_variant in layout is set as "link"
 
 const DefaultMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
-  engagement
+  engagement,
+  cta_variant
 }) => external_React_default.a.createElement("div", {
   className: "meta"
 }, external_React_default.a.createElement("div", {
   className: "info-wrap"
 }, external_React_default.a.createElement("p", {
   className: "source clamp"
 }, source), external_React_default.a.createElement("header", {
   className: "title clamp"
 }, title), excerpt && external_React_default.a.createElement("p", {
   className: "excerpt clamp"
-}, excerpt), cta && external_React_default.a.createElement("div", {
+}, excerpt), cta_variant === "link" && cta && external_React_default.a.createElement("div", {
   role: "link",
   className: "cta-link icon icon-arrow",
   tabIndex: "0"
 }, cta)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
   context_type: context_type,
   context: context,
   engagement: engagement
 }));
-const VariantMeta = ({
+const CTAButtonMeta = ({
   source,
   title,
   excerpt,
   context,
   context_type,
   cta,
   engagement,
   sponsor
@@ -8054,46 +8064,48 @@ class DSCard_DSCard extends external_Rea
   render() {
     if (this.props.placeholder || !this.state.isSeen) {
       return external_React_default.a.createElement("div", {
         className: "ds-card placeholder",
         ref: this.setPlaceholderRef
       });
     }
 
+    const isButtonCTA = this.props.cta_variant === "button";
     return external_React_default.a.createElement("div", {
       className: "ds-card"
     }, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
       className: "ds-card-link",
       dispatch: this.props.dispatch,
       onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined,
       url: this.props.url
     }, external_React_default.a.createElement("div", {
       className: "img-wrapper"
     }, external_React_default.a.createElement(DSImage_DSImage, {
       extraClassNames: "img",
       source: this.props.image_src,
       rawSource: this.props.raw_image_src
-    })), this.props.cta_variant && external_React_default.a.createElement(VariantMeta, {
+    })), isButtonCTA ? external_React_default.a.createElement(CTAButtonMeta, {
       source: this.props.source,
       title: this.props.title,
       excerpt: this.props.excerpt,
       context: this.props.context,
       context_type: this.props.context_type,
       engagement: this.props.engagement,
       cta: this.props.cta,
       sponsor: this.props.sponsor
-    }), !this.props.cta_variant && external_React_default.a.createElement(DefaultMeta, {
+    }) : external_React_default.a.createElement(DefaultMeta, {
       source: this.props.source,
       title: this.props.title,
       excerpt: this.props.excerpt,
       context: this.props.context,
       engagement: this.props.engagement,
       context_type: this.props.context_type,
-      cta: this.props.cta
+      cta: this.props.cta,
+      cta_variant: this.props.cta_variant
     }), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
       campaignId: this.props.campaignId,
       rows: [{
         id: this.props.id,
         pos: this.props.pos,
         ...(this.props.shim && this.props.shim.impression ? {
           shim: this.props.shim.impression
         } : {})
@@ -8321,16 +8333,40 @@ class DSMessage_DSMessage extends extern
       className: "title-text"
     }, this.props.title), this.props.link_text && this.props.link_url && external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
       className: "link",
       url: this.props.link_url
     }, this.props.link_text)));
   }
 
 }
+// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx
+/* 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/. */
+
+
+class DSTextPromo_DSTextPromo extends external_React_default.a.PureComponent {
+  render() {
+    return external_React_default.a.createElement("div", {
+      className: "ds-text-promo"
+    }, external_React_default.a.createElement("img", {
+      src: this.props.image,
+      alt: this.props.alt_text
+    }), external_React_default.a.createElement("div", {
+      className: "text"
+    }, external_React_default.a.createElement("h3", null, `${this.props.header}\u2003`, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
+      className: "ds-chevron-link",
+      url: this.props.cta_url
+    }, this.props.cta_text)), external_React_default.a.createElement("p", {
+      className: "subtitle"
+    }, this.props.subtitle)));
+  }
+
+}
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/List/List.jsx
 /* 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/. */
 
 
 
 
@@ -9031,16 +9067,17 @@ const TopSites_TopSites_TopSites = Objec
 
 
 
 
 
 
 
 
+
 const ALLOWED_CSS_URL_PREFIXES = ["chrome://", "resource://", "https://img-getpocket.cdn.mozilla.net/"];
 const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR";
 let rickRollCache = []; // Cache of random probability values for a spoc position
 
 /**
  * Validate a CSS declaration. The values are assumed to be normalized by CSSOM.
  */
 
@@ -9115,16 +9152,26 @@ class DiscoveryStreamBase_DiscoveryStrea
       case "Highlights":
         return external_React_default.a.createElement(Highlights, null);
 
       case "TopSites":
         return external_React_default.a.createElement(TopSites_TopSites_TopSites, {
           header: component.header
         });
 
+      case "TextPromo":
+        return external_React_default.a.createElement(DSTextPromo_DSTextPromo, {
+          image: component.properties.image_src,
+          alt_text: component.properties.alt_text,
+          header: component.properties.excerpt,
+          cta_text: component.properties.cta_text,
+          cta_url: component.properties.cta_url,
+          subtitle: component.properties.context
+        });
+
       case "Message":
         return external_React_default.a.createElement(DSMessage_DSMessage, {
           title: component.header && component.header.title,
           subtitle: component.header && component.header.subtitle,
           link_text: component.header && component.header.link_text,
           link_url: component.header && component.header.link_url,
           icon: component.header && component.header.icon
         });
@@ -12818,16 +12865,17 @@ const INITIAL_STATE = {
     lastUpdated: null,
     feeds: {
       data: {// "https://foo.com/feed1": {lastUpdated: 123, data: []}
       },
       loaded: false
     },
     spocs: {
       spocs_endpoint: "",
+      spocs_per_domain: 1,
       lastUpdated: null,
       data: {},
       // {spocs: []}
       loaded: false,
       frequency_caps: [],
       blocked: []
     }
   },
@@ -13434,17 +13482,18 @@ function DiscoveryStream(prevState = INI
         spocs: { ...prevState.spocs,
           frequency_caps: [...prevState.spocs.frequency_caps, ...action.data]
         }
       };
 
     case Actions["actionTypes"].DISCOVERY_STREAM_SPOCS_ENDPOINT:
       return { ...prevState,
         spocs: { ...INITIAL_STATE.DiscoveryStream.spocs,
-          spocs_endpoint: action.data || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint
+          spocs_endpoint: action.data.url || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
+          spocs_per_domain: action.data.spocs_per_domain || INITIAL_STATE.DiscoveryStream.spocs.spocs_per_domain
         }
       };
 
     case Actions["actionTypes"].DISCOVERY_STREAM_SPOCS_UPDATE:
       if (action.data) {
         return { ...prevState,
           spocs: { ...prevState.spocs,
             lastUpdated: action.data.lastUpdated,
@@ -13621,24 +13670,26 @@ class Interrupt_Interrupt extends extern
 
     switch (message.template) {
       case "return_to_amo_overlay":
         return external_React_default.a.createElement(src["LocalizationProvider"], {
           bundles: Object(rich_text_strings["generateBundles"])({
             amo_html: message.content.text
           })
         }, external_React_default.a.createElement(ReturnToAMO["ReturnToAMO"], _extends({}, message, {
+          document: this.props.document,
           UISurface: "NEWTAB_OVERLAY",
           onBlock: onDismiss,
           onAction: executeAction,
           sendUserActionTelemetry: sendUserActionTelemetry
         })));
 
       case "fxa_overlay":
         return external_React_default.a.createElement(StartupOverlay["StartupOverlay"], {
+          document: this.props.document,
           onBlock: onDismiss,
           dispatch: dispatch,
           fxa_endpoint: fxaEndpoint
         });
 
       case "trailhead":
         return external_React_default.a.createElement(Trailhead["Trailhead"], {
           document: this.props.document,
--- a/browser/components/newtab/data/content/assets/firefox-wordmark.svg
+++ b/browser/components/newtab/data/content/assets/firefox-wordmark.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="138" height="38"><path fill="context-fill" d="M136.837 10.212H131.5l-5.7 9.9-5.644-9.9h-5.542l8.364 12.778-9.438 14.265h5.337l6.723-11.443 6.62 11.443h5.7l-9.391-14.471zM22.844 37.255h4.721V10.212h-4.721zm15.42-21.339l-.462-5.7h-4.054v27.039h4.721V23.306c0-4.205 3.079-8.878 6.466-8.878a8.5 8.5 0 0 1 2.361.308l.872-4.618A11.516 11.516 0 0 0 45.5 9.81c-3.284 0-5.8 2.053-7.236 6.106zM0 37.255h4.875V21.707h11.546v-3.849H4.875V5.8h13.342l.565-3.9H0zM25.153.163A3.139 3.139 0 0 0 21.869 3.4a3.129 3.129 0 0 0 3.284 3.182A3.143 3.143 0 0 0 28.489 3.4 3.153 3.153 0 0 0 25.153.163zm76.491 9.647c-7.7 0-12.11 5.585-12.11 13.949 0 8.57 4.362 14.112 12.059 14.112 7.646 0 12.059-5.8 12.059-14.163 0-8.57-4.31-13.898-12.008-13.898zm-.051 24.264c-4.516 0-6.979-3.284-6.979-10.315 0-7.081 2.515-10.152 7.03-10.152 4.465 0 6.928 3.071 6.928 10.1 0 7.083-2.463 10.367-6.979 10.367zM82.47 8.339c0-2.617 1.027-4.542 4-4.542a11.567 11.567 0 0 1 4.721 1.027l1.488-3.438A14.907 14.907 0 0 0 86.216 0c-5.49 0-8.467 3.447-8.467 7.963v2.249h-4.824v3.643h4.824v23.4h4.721v-23.4h6.056l.513-3.643H82.47zM59.952 9.81c-6.979 0-11.238 5.79-11.238 14.206 0 8.57 4.413 13.855 11.957 13.855a14.741 14.741 0 0 0 9.442-3.387l-2.053-2.822a11.384 11.384 0 0 1-7.03 2.361c-3.9 0-6.825-2.412-7.287-8.673h17.242c.052-.616.1-1.488.1-2.412.003-8.262-3.846-13.128-11.133-13.128zm6.466 12.051H53.743c.359-6 2.72-8.305 6.312-8.305 4.259 0 6.363 2.711 6.363 8z"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="172" height="42"><path fill="context-fill #20123a" d="M.19 2.82h25.72v7H7.57v9.43h18.34v6.9H7.57v15.14H.19zM34.65.13a4.14 4.14 0 0 1 4.27 4.33 4.12 4.12 0 0 1-4.32 4.32 4.09 4.09 0 0 1-4.27-4.22A4.27 4.27 0 0 1 34.65.13zM31 12.83h7.27v28.46H31zm28.35 7.91a5.89 5.89 0 0 0-3.53-1.27c-3 0-4.64 1.9-4.64 6.06v15.76H44V12.83h6.9v4.11a6.79 6.79 0 0 1 6.8-4.37A8.69 8.69 0 0 1 62.53 14zm3 6.48c0-8.17 6.06-15 14.65-15s14.59 6.06 14.59 14.49v3H69.48c.79 3.58 3.58 6 7.85 6a7.62 7.62 0 0 0 7.06-4.21l6.06 3.63c-3 4.43-7.27 6.75-13.33 6.75-9.22-.01-14.75-6.18-14.75-14.66zM69.59 24h15c-.79-3.63-3.74-5.63-7.59-5.63A7.31 7.31 0 0 0 69.59 24zM93.4 12.83h5.11v-1.42c0-7.75 3.27-11 10.44-11h2.53v6.31h-2.06c-3.37 0-4.11 1.16-4.11 4.69v1.42h6.17v6.54h-6v21.92h-7V19.37H93.4zm19.45 14.23a14.56 14.56 0 0 1 14.85-14.81 14.81 14.81 0 1 1 0 29.62c-8.85 0-14.85-6.49-14.85-14.81zm22.65 0a7.8 7.8 0 1 0-15.59 0 7.8 7.8 0 1 0 15.59 0zm16.86-.32l-10.27-13.91h8.53l6.06 8.75 6.22-8.75h8.38l-10.43 13.86 11 14.6h-8.49L156.53 32l-6.59 9.28h-8.48z"/></svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-caret-right.svg
@@ -0,0 +1,1 @@
+<svg width="5" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M.248 8.204a.77.77 0 0 1 1.088.044L4.8 12l-3.464 3.752a.77.77 0 1 1-1.132-1.045L2.704 12l-2.5-2.707a.77.77 0 0 1 .044-1.089z" fill="#0C0C0D"/></svg>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..05bd7c237615b520f09eef0bdb5999b4aca0ed68
GIT binary patch
literal 4309
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
D=gK3}
--- a/browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
+++ b/browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
@@ -31,17 +31,17 @@ NOT against Mozilla central.
 ### Operating system and software
 
 The Activity Stream development environment is designed to work on Mac and Linux.
 If you need to develop on Windows, you might want to reach out on IRC (#activity-stream)
 if you run into any problems.
 
 You will also need to install:
 
-- Node.js 7+ (On Mac, the best way to install Node.js is to use the [install link on the Node.js homepage](https://nodejs.org/en/))
+- Node.js version 8 (To install this legacy version of Node, [use this URL](https://nodejs.org/en/download/releases/))
 - npm (packaged with Node.js)
 
 ### Activity Stream Github repository
 
 You will need to to clone Activity Stream to a local directory from the `master`
 branch of our Github repository: https://github.com/mozilla/activity-stream
 
 ```
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -737,16 +737,17 @@ class _ASRouter {
       addImpression: this.addImpression,
       blockMessageById: this.blockMessageById,
       unblockMessageById: this.unblockMessageById,
       dispatch: this.dispatch,
     });
     ToolbarPanelHub.init(this.waitForInitialized, {
       getMessages: this.handleMessageRequest,
       dispatch: this.dispatch,
+      handleUserAction: this.handleUserAction,
     });
 
     this._loadLocalProviders();
 
     // Instead of setupTrailhead, which adds experiments, just load override pref values
     await this.setFirstRunStateFromPref();
 
     const messageBlockList =
@@ -1526,16 +1527,17 @@ class _ASRouter {
       this._storage.set(impressionsString, impressions);
     }
     return impressions;
   }
 
   handleMessageRequest({
     triggerId,
     triggerParam,
+    triggerContext,
     template,
     provider,
     returnAll = false,
   }) {
     const msgs = this._getUnblockedMessages().filter(m => {
       if (provider && m.provider !== provider) {
         return false;
       }
@@ -1547,23 +1549,31 @@ class _ASRouter {
       }
 
       return true;
     });
 
     if (returnAll) {
       return this._findAllMessages(
         msgs,
-        triggerId && { id: triggerId, param: triggerParam }
+        triggerId && {
+          id: triggerId,
+          param: triggerParam,
+          context: triggerContext,
+        }
       );
     }
 
     return this._findMessage(
       msgs,
-      triggerId && { id: triggerId, param: triggerParam }
+      triggerId && {
+        id: triggerId,
+        param: triggerParam,
+        context: triggerContext,
+      }
     );
   }
 
   async setMessageById(id, target, force = true, action = {}) {
     await this.setState({ lastMessageId: id });
     const newMessage = this.getMessageById(id);
 
     await this._sendMessageToTarget(newMessage, target, action.data, force);
@@ -1898,16 +1908,17 @@ class _ASRouter {
         );
         await this.setupTrailhead();
       }
     }
 
     const message = await this.handleMessageRequest({
       triggerId: trigger.id,
       triggerParam: trigger.param,
+      triggerContext: trigger.context,
     });
 
     await this.setState({ lastMessageId: message ? message.id : null });
     await this._sendMessageToTarget(message, target, trigger);
   }
 
   /* eslint-disable complexity */
   async onMessage({ data: action, target }) {
--- a/browser/components/newtab/lib/ASRouterTargeting.jsm
+++ b/browser/components/newtab/lib/ASRouterTargeting.jsm
@@ -51,16 +51,22 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/AttributionCode.jsm"
 );
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "UpdateManager",
   "@mozilla.org/updates/update-manager;1",
   "nsIUpdateManager"
 );
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "TrackingDBService",
+  "@mozilla.org/tracking-db-service;1",
+  "nsITrackingDBService"
+);
 
 const FXA_USERNAME_PREF = "services.sync.username";
 const FXA_ENABLED_PREF = "identity.fxaccounts.enabled";
 const SEARCH_REGION_PREF = "browser.search.region";
 const MOZ_JEXL_FILEPATH = "mozjexl";
 
 const { activityStreamProvider: asProvider } = NewTabUtils;
 
@@ -417,16 +423,19 @@ const TargetingGetters = {
     return null;
   },
   get isFxABadgeEnabled() {
     return Services.prefs.getBoolPref(
       "browser.messaging-system.fxatoolbarbadge.enabled",
       false
     );
   },
+  get totalBlockedCount() {
+    return TrackingDBService.sumAllEvents();
+  },
 };
 
 this.ASRouterTargeting = {
   Environment: TargetingGetters,
 
   ERROR_TYPES: {
     MALFORMED_EXPRESSION: "MALFORMED_EXPRESSION",
     OTHER_ERROR: "OTHER_ERROR",
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -365,17 +365,17 @@ this.DiscoveryStreamFeed = class Discove
     if (
       layout.spocs &&
       layout.spocs.url &&
       layout.spocs.url !==
         this.store.getState().DiscoveryStream.spocs.spocs_endpoint
     ) {
       sendUpdate({
         type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,
-        data: layout.spocs.url,
+        data: layout.spocs,
       });
     }
   }
 
   /**
    * buildFeedPromise - Adds the promise result to newFeeds and
    *                    pushes a promise to newsFeedsPromises.
    * @param {Object} Has both newFeedsPromises (Array) and newFeeds (Object)
@@ -1336,17 +1336,16 @@ defaultLayoutResp = {
         },
       ],
     },
     {
       width: 12,
       components: [
         {
           type: "CardGrid",
-          cta_variant: false,
           properties: {
             items: 21,
           },
           header: {
             title: "",
           },
           feed: {
             embed_reference: null,
--- a/browser/components/newtab/lib/DownloadsManager.jsm
+++ b/browser/components/newtab/lib/DownloadsManager.jsm
@@ -40,17 +40,17 @@ this.DownloadsManager = class DownloadsM
       hostname: new URL(download.source.url).hostname,
       url: download.source.url,
       path: download.target.path,
       title: DownloadsViewUI.getDisplayName(download),
       description:
         DownloadsViewUI.getSizeWithUnits(download) ||
         DownloadsCommon.strings.sizeUnknown,
       referrer: download.source.referrerInfo
-        ? download.source.referrerInfo.originalReferrer
+        ? download.source.referrerInfo.originalReferrer.spec
         : null,
       date_added: download.endTime,
     };
   }
 
   init(store) {
     this._store = store;
     this._downloadData = DownloadsCommon.getData(
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -9,16 +9,18 @@ ChromeUtils.defineModuleGetter(
   "resource:///modules/AttributionCode.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "AddonRepository",
   "resource://gre/modules/addons/AddonRepository.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const FIREFOX_VERSION = parseInt(Services.appinfo.version.match(/\d+/), 10);
+const ONE_MINUTE = 60 * 1000;
 
 const L10N = new Localization([
   "branding/brand.ftl",
   "browser/branding/brandings.ftl",
   "browser/branding/sync-brand.ftl",
   "browser/newtab/onboarding.ftl",
 ]);
 
@@ -400,19 +402,42 @@ const ONBOARDING_MESSAGES = () => [
     template: "protections_panel",
     content: {
       title: { string_id: "cfr-protections-panel-header" },
       body: { string_id: "cfr-protections-panel-body" },
       link_text: { string_id: "cfr-protections-panel-link-text" },
       cta_url: `${Services.urlFormatter.formatURLPref(
         "app.support.baseURL"
       )}etp-promotions?as=u&utm_source=inproduct`,
+      cta_type: "OPEN_URL",
     },
     trigger: { id: "protectionsPanelOpen" },
   },
+  {
+    id: `WHATS_NEW_BADGE_${FIREFOX_VERSION}`,
+    template: "toolbar_badge",
+    content: {
+      delay: 5 * ONE_MINUTE,
+      target: "whats-new-menu-button",
+      action: { id: "show-whatsnew-button" },
+    },
+    priority: 1,
+    trigger: { id: "toolbarBadgeUpdate" },
+    frequency: {
+      // Makes it so that we track impressions for this message while at the
+      // same time it can have unlimited impressions
+      lifetime: Infinity,
+    },
+    // Never saw this message or saw it in the past 4 days or more recent
+    targeting: `isWhatsNewPanelEnabled &&
+      (earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
+        (!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
+      (messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
+        currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
+  },
 ];
 
 const OnboardingMessageProvider = {
   async getExtraAttributes() {
     const [header, button_label] = await L10N.formatMessages([
       { id: "onboarding-welcome-header" },
       { id: "onboarding-start-browsing-button-label" },
     ]);
--- a/browser/components/newtab/lib/PanelTestProvider.jsm
+++ b/browser/components/newtab/lib/PanelTestProvider.jsm
@@ -1,16 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const FIREFOX_VERSION = parseInt(Services.appinfo.version.match(/\d+/), 10);
 const TWO_DAYS = 2 * 24 * 3600 * 1000;
 
 const MESSAGES = () => [
   {
     id: "SIMPLE_FXA_BOOKMARK_TEST_FLUENT",
     template: "fxa_bookmark_panel",
     content: {
       title: { string_id: "cfr-doorhanger-bookmark-fxa-header" },
@@ -62,82 +59,85 @@ const MESSAGES = () => [
             "https://www.mozilla.org/%LOCALE%/etc/firefox/retention/thank-you-a/",
           expireDelta: TWO_DAYS,
         },
       },
     },
     trigger: { id: "momentsUpdate" },
   },
   {
-    id: `WHATS_NEW_BADGE_${FIREFOX_VERSION}`,
-    template: "toolbar_badge",
-    content: {
-      // delay: 5 * 3600 * 1000,
-      delay: 5000,
-      target: "whats-new-menu-button",
-      action: { id: "show-whatsnew-button" },
-    },
-    priority: 1,
-    trigger: { id: "toolbarBadgeUpdate" },
-    frequency: {
-      // Makes it so that we track impressions for this message while at the
-      // same time it can have unlimited impressions
-      lifetime: Infinity,
-    },
-    // Never saw this message or saw it in the past 4 days or more recent
-    targeting: `isWhatsNewPanelEnabled &&
-      (earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
-        (!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
-      (messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
-        currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
-  },
-  {
     id: "WHATS_NEW_70_1",
     template: "whatsnew_panel_message",
+    order: 3,
     content: {
       published_date: 1560969794394,
       title: "Protection Is Our Focus",
       icon_url:
         "resource://activity-stream/data/content/assets/whatsnew-send-icon.png",
       icon_alt: "Firefox Send Logo",
       body:
         "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
       cta_url: "https://blog.mozilla.org/",
+      cta_type: "OPEN_URL",
     },
     targeting: `firefoxVersion > 69`,
     trigger: { id: "whatsNewPanelOpened" },
   },
   {
     id: "WHATS_NEW_70_2",
     template: "whatsnew_panel_message",
+    order: 1,
     content: {
       published_date: 1560969794394,
       title: "Another thing new in Firefox 70",
       body:
         "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
       link_text: "Learn more on our blog",
       cta_url: "https://blog.mozilla.org/",
+      cta_type: "OPEN_URL",
     },
     targeting: `firefoxVersion > 69`,
     trigger: { id: "whatsNewPanelOpened" },
   },
   {
     id: "WHATS_NEW_69_1",
     template: "whatsnew_panel_message",
+    order: 1,
     content: {
       published_date: 1557346235089,
       title: "Something new in Firefox 69",
       body:
         "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.",
       link_text: "Learn more on our blog",
       cta_url: "https://blog.mozilla.org/",
+      cta_type: "OPEN_URL",
     },
     targeting: `firefoxVersion > 68`,
     trigger: { id: "whatsNewPanelOpened" },
   },
+  {
+    id: "WHATS_NEW_70_3",
+    template: "whatsnew_panel_message",
+    order: 2,
+    content: {
+      published_date: 1560969794394,
+      layout: "tracking-protections",
+      title: { string_id: "cfr-whatsnew-tracking-blocked-title" },
+      subtitle: { string_id: "cfr-whatsnew-tracking-blocked-subtitle" },
+      icon_url:
+        "resource://activity-stream/data/content/assets/protection-report-icon.png",
+      icon_alt: "Protection Report icon",
+      body: { string_id: "cfr-whatsnew-tracking-protect-body" },
+      link_text: { string_id: "cfr-whatsnew-tracking-blocked-link-text" },
+      cta_url: "protections",
+      cta_type: "OPEN_ABOUT_PAGE",
+    },
+    targeting: `firefoxVersion > 69 && totalBlockedCount > 0`,
+    trigger: { id: "whatsNewPanelOpened" },
+  },
 ];
 
 const PanelTestProvider = {
   getMessages() {
     return MESSAGES().map(message => ({
       ...message,
       targeting: `providerCohorts.panel_local_testing == "SHOW_TEST"`,
     }));
--- a/browser/components/newtab/lib/ToolbarPanelHub.jsm
+++ b/browser/components/newtab/lib/ToolbarPanelHub.jsm
@@ -1,27 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-ChromeUtils.defineModuleGetter(
-  this,
-  "Services",
-  "resource://gre/modules/Services.jsm"
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
 );
-ChromeUtils.defineModuleGetter(
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+  EveryWindow: "resource:///modules/EveryWindow.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+});
+XPCOMUtils.defineLazyServiceGetter(
   this,
-  "EveryWindow",
-  "resource:///modules/EveryWindow.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "PrivateBrowsingUtils",
-  "resource://gre/modules/PrivateBrowsingUtils.jsm"
+  "TrackingDBService",
+  "@mozilla.org/tracking-db-service;1",
+  "nsITrackingDBService"
 );
 
 const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
 const PROTECTIONS_PANEL_INFOMSG_PREF =
   "browser.protections_panel.infoMessage.seen";
 
 const TOOLBAR_BUTTON_ID = "whats-new-menu-button";
 const APPMENU_BUTTON_ID = "appMenu-whatsnew-button";
@@ -38,19 +37,20 @@ class _ToolbarPanelHub {
     this._hideToolbarButton = this._hideToolbarButton.bind(this);
     this.insertProtectionPanelMessage = this.insertProtectionPanelMessage.bind(
       this
     );
 
     this.state = null;
   }
 
-  async init(waitForInitialized, { getMessages, dispatch }) {
+  async init(waitForInitialized, { getMessages, dispatch, handleUserAction }) {
     this._getMessages = getMessages;
     this._dispatch = dispatch;
+    this._handleUserAction = handleUserAction;
     // Wait for ASRouter messages to become available in order to know
     // if we can show the What's New panel
     await waitForInitialized;
     if (this.whatsNewPanelEnabled) {
       // Enable the application menu button so that the user can access
       // the panel outside of the toolbar button
       this.enableAppmenuButton();
     }
@@ -132,33 +132,55 @@ class _ToolbarPanelHub {
     if (!panelContainer) {
       return;
     }
     panelContainer.addEventListener("popuphidden", removeToolbarButton, {
       once: true,
     });
   }
 
+  // Newer messages first and use `order` field to decide between messages
+  // with the same timestamp
+  _sortWhatsNewMessages(m1, m2) {
+    // Sort by published_date in descending order.
+    if (m1.content.published_date === m2.content.published_date) {
+      // Ascending order
+      return m1.order - m2.order;
+    }
+    if (m1.content.published_date > m2.content.published_date) {
+      return -1;
+    }
+    return 1;
+  }
+
   // Render what's new messages into the panel.
   async renderMessages(win, doc, containerId) {
-    const messages = (await this.messages).sort((m1, m2) => {
-      // Sort by published_date in descending order.
-      if (m1.content.published_date === m2.content.published_date) {
-        return 0;
-      }
-      if (m1.content.published_date > m2.content.published_date) {
-        return -1;
-      }
-      return 1;
-    });
+    const messages = (await this.messages).sort(this._sortWhatsNewMessages);
     const container = doc.getElementById(containerId);
 
     if (messages && !container.querySelector(".whatsNew-message")) {
       let previousDate = 0;
+      // Get and store any variable part of the message content
+      this.state.contentArguments = await this._contentArguments();
       for (let message of messages) {
+        // Only render date if it is different from the one rendered before.
+        if (message.content.published_date !== previousDate) {
+          container.appendChild(
+            this._createElement(doc, "p", {
+              classList: "whatsNew-message-date",
+              content: new Date(
+                message.content.published_date
+              ).toLocaleDateString("default", {
+                month: "long",
+                day: "numeric",
+                year: "numeric",
+              }),
+            })
+          );
+        }
         container.appendChild(
           this._createMessageElements(win, doc, message, previousDate)
         );
         previousDate = message.content.published_date;
       }
     }
 
     this._onPanelHidden(win);
@@ -176,128 +198,191 @@ class _ToolbarPanelHub {
     // subview (inside the application menu) or as a toolbar dropdown.
     // https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268
     const mainview = win.PanelUI.whatsNewPanel.hasAttribute("mainview");
     this.sendUserEventTelemetry(win, "IMPRESSION", eventId, {
       value: { view: mainview ? "toolbar_dropdown" : "application_menu" },
     });
   }
 
+  /**
+   * Attach click event listener defined in message payload
+   */
+  _attachClickListener(win, element, message) {
+    element.addEventListener("click", () => {
+      this._handleUserAction({
+        target: win,
+        data: {
+          type: message.content.cta_type,
+          data: {
+            args: message.content.cta_url,
+            where: "tabshifted",
+          },
+        },
+      });
+
+      this.sendUserEventTelemetry(win, "CLICK", message);
+    });
+  }
+
   _createMessageElements(win, doc, message, previousDate) {
     const { content } = message;
     const messageEl = this._createElement(doc, "div");
     messageEl.classList.add("whatsNew-message");
 
-    // Only render date if it is different from the one rendered before.
-    if (content.published_date !== previousDate) {
-      messageEl.appendChild(
-        this._createDateElement(doc, content.published_date)
-      );
-    }
-
     const wrapperEl = this._createElement(doc, "button");
+    // istanbul ignore next
     wrapperEl.doCommand = () => {};
     wrapperEl.classList.add("whatsNew-message-body");
     messageEl.appendChild(wrapperEl);
-    wrapperEl.addEventListener("click", () => {
-      win.ownerGlobal.openLinkIn(content.cta_url, "tabshifted", {
-        private: false,
-        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
-          {}
-        ),
-        csp: null,
-      });
-
-      this.sendUserEventTelemetry(win, "CLICK", message);
-    });
 
     if (content.icon_url) {
       wrapperEl.classList.add("has-icon");
       const iconEl = this._createElement(doc, "img");
       iconEl.src = content.icon_url;
       iconEl.classList.add("whatsNew-message-icon");
       this._setTextAttribute(doc, iconEl, "alt", content.icon_alt);
       wrapperEl.appendChild(iconEl);
     }
 
-    const titleEl = this._createElement(doc, "h2");
-    titleEl.classList.add("whatsNew-message-title");
-    this._setString(doc, titleEl, content.title);
-    wrapperEl.appendChild(titleEl);
-
-    const bodyEl = this._createElement(doc, "p");
-    this._setString(doc, bodyEl, content.body);
-    wrapperEl.appendChild(bodyEl);
+    wrapperEl.appendChild(this._createMessageContent(win, doc, content));
 
     if (content.link_text) {
-      const linkEl = this._createElement(doc, "a");
-      linkEl.classList.add("text-link");
-      this._setString(doc, linkEl, content.link_text);
-      wrapperEl.appendChild(linkEl);
+      wrapperEl.appendChild(
+        this._createElement(doc, "a", {
+          classList: "text-link",
+          content: content.link_text,
+        })
+      );
     }
 
+    // Attach event listener on entire message container
+    this._attachClickListener(win, wrapperEl, message);
+
     return messageEl;
   }
 
-  _createHeroElement(win, doc, content) {
+  /**
+   * Return message title (optional subtitle) and body
+   */
+  _createMessageContent(win, doc, content) {
+    const wrapperEl = new win.DocumentFragment();
+
+    wrapperEl.appendChild(
+      this._createElement(doc, "h2", {
+        classList: "whatsNew-message-title",
+        content: content.title,
+      })
+    );
+
+    switch (content.layout) {
+      case "tracking-protections":
+        wrapperEl.appendChild(
+          this._createElement(doc, "h4", {
+            classList: "whatsNew-message-subtitle",
+            content: content.subtitle,
+          })
+        );
+        wrapperEl.appendChild(
+          this._createElement(doc, "h2", {
+            classList: "whatsNew-message-title-large",
+            content: this.state.contentArguments.blockedCount,
+          })
+        );
+        break;
+    }
+
+    wrapperEl.appendChild(
+      this._createElement(doc, "p", { content: content.body })
+    );
+
+    return wrapperEl;
+  }
+
+  _createHeroElement(win, doc, message) {
     const messageEl = this._createElement(doc, "div");
     messageEl.setAttribute("id", "protections-popup-message");
     messageEl.classList.add("whatsNew-hero-message");
     const wrapperEl = this._createElement(doc, "div");
     wrapperEl.classList.add("whatsNew-message-body");
     messageEl.appendChild(wrapperEl);
-    wrapperEl.addEventListener("click", () => {
-      win.ownerGlobal.openLinkIn(content.cta_url, "tabshifted", {
-        private: false,
-        relatedToCurrent: true,
-        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
-          {}
-        ),
-        csp: null,
-      });
-    });
-    const titleEl = this._createElement(doc, "h2");
-    titleEl.classList.add("whatsNew-message-title");
-    this._setString(doc, titleEl, content.title);
-    wrapperEl.appendChild(titleEl);
+
+    this._attachClickListener(win, wrapperEl, message);
 
-    const bodyEl = this._createElement(doc, "p");
-    this._setString(doc, bodyEl, content.body);
-    wrapperEl.appendChild(bodyEl);
+    wrapperEl.appendChild(
+      this._createElement(doc, "h2", {
+        classList: "whatsNew-message-title",
+        content: message.content.title,
+      })
+    );
+    wrapperEl.appendChild(
+      this._createElement(doc, "p", { content: message.content.body })
+    );
 
-    if (content.link_text) {
-      const linkEl = this._createElement(doc, "a");
-      linkEl.classList.add("text-link");
-      this._setString(doc, linkEl, content.link_text);
-      wrapperEl.appendChild(linkEl);
+    if (message.content.link_text) {
+      wrapperEl.appendChild(
+        this._createElement(doc, "a", {
+          classList: "text-link",
+          content: message.content.link_text,
+        })
+      );
     }
 
     return messageEl;
   }
 
-  _createElement(doc, elem) {
-    return doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
+  _createElement(doc, elem, options = {}) {
+    const node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
+    if (options.classList) {
+      node.classList.add(options.classList);
+    }
+    if (options.content) {
+      this._setString(doc, node, options.content);
+    }
+
+    return node;
   }
 
-  _createDateElement(doc, date) {
-    const dateEl = this._createElement(doc, "p");
-    dateEl.classList.add("whatsNew-message-date");
-    dateEl.textContent = new Date(date).toLocaleDateString("default", {
-      month: "long",
-      day: "numeric",
-      year: "numeric",
-    });
-    return dateEl;
+  async _contentArguments() {
+    // Between now and 6 weeks ago
+    const dateTo = new Date();
+    const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);
+    const eventsByDate = await TrackingDBService.getEventsByDateRange(
+      dateFrom,
+      dateTo
+    );
+    // Count all events in the past 6 weeks
+    const totalEvents = eventsByDate.reduce(
+      (acc, day) => acc + day.getResultByName("count"),
+      0
+    );
+    return {
+      // Keys need to match variable names used in asrouter.ftl
+      // `earliestDate` will be either 6 weeks ago or when tracking recording
+      // started. Whichever is more recent.
+      earliestDate: new Date(
+        Math.max(
+          new Date(await TrackingDBService.getEarliestRecordedDate()),
+          dateFrom
+        )
+      ).getTime(),
+      blockedCount: totalEvents.toLocaleString(),
+    };
   }
 
   // If `string_id` is present it means we are relying on fluent for translations.
   // Otherwise, we have a vanilla string.
   _setString(doc, el, stringObj) {
     if (stringObj.string_id) {
-      doc.l10n.setAttributes(el, stringObj.string_id);
+      doc.l10n.setAttributes(
+        el,
+        stringObj.string_id,
+        // Pass all available arguments to Fluent
+        this.state.contentArguments
+      );
     } else {
       el.textContent = stringObj;
     }
   }
 
   // If `string_id` is present it means we are relying on fluent for translations.
   // Otherwise, we have a vanilla string.
   _setTextAttribute(doc, el, attr, stringObj) {
@@ -390,17 +475,17 @@ class _ToolbarPanelHub {
       infoButton.toggleAttribute("checked");
     };
     if (!container.childElementCount) {
       const message = await this._getMessages({
         template: "protections_panel",
         triggerId: "protectionsPanelOpen",
       });
       if (message) {
-        const messageEl = this._createHeroElement(win, doc, message.content);
+        const messageEl = this._createHeroElement(win, doc, message);
         container.appendChild(messageEl);
         infoButton.addEventListener("click", toggleMessage);
         this.sendUserEventTelemetry(win, "IMPRESSION", message.id);
       }
     }
     // Message is collapsed by default. If it was never shown before we want
     // to expand it
     if (
--- a/browser/components/newtab/mochitest.sh
+++ b/browser/components/newtab/mochitest.sh
@@ -8,17 +8,17 @@ export DISPLAY=:99.0
 
 # Pull latest m-c and update tip
 cd /mozilla-central && hg pull && hg update -C
 
 # Build Activity Stream and copy the output to m-c
 cd /activity-stream && npm install . && npm run buildmc
 
 # Build latest m-c with Activity Stream changes
-cd /mozilla-central && ./mach build \
+cd /mozilla-central && rm -rf ./objdir-frontend && ./mach build \
   && ./mach lint browser/components/newtab \
   && ./mach lint -l codespell browser/locales/en-US/browser/newtab \
   && ./mach test browser/components/newtab/test/browser --headless \
   && ./mach test browser/components/newtab/test/xpcshell \
   && ./mach test --log-tbpl test_run_log \
     browser/base/content/test/about/browser_aboutHome_search_telemetry.js \
     browser/base/content/test/static/browser_parsable_css.js \
     browser/base/content/test/tabs/browser_new_tab_in_privileged_process_pref.js \
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -209,16 +209,17 @@ describe("ASRouter", () => {
       );
 
       assert.calledWithExactly(
         FakeToolbarPanelHub.init,
         Router.waitForInitialized,
         {
           getMessages: Router.handleMessageRequest,
           dispatch: Router.dispatch,
+          handleUserAction: Router.handleUserAction,
         }
       );
 
       assert.calledWithExactly(
         FakeBookmarkPanelHub.init,
         Router.handleMessageRequest,
         Router.addImpression,
         Router.dispatch
@@ -925,17 +926,21 @@ describe("ASRouter", () => {
         template: "whatsnew-panel",
         triggerId: "whatsNewPanelOpened",
         returnAll: true,
       });
 
       assert.deepEqual(result, [message2, message1]);
     });
     it("should forward trigger param info", async () => {
-      const trigger = { triggerId: "foo", triggerParam: "bar" };
+      const trigger = {
+        triggerId: "foo",
+        triggerParam: "bar",
+        triggerContext: "context",
+      };
       const message1 = {
         id: "1",
         campaign: "foocampaign",
         trigger: { id: "foo" },
       };
       const message2 = {
         id: "2",
         campaign: "foocampaign",
@@ -946,16 +951,17 @@ describe("ASRouter", () => {
       const stub = sandbox.stub(Router, "_findMessage");
 
       Router.handleMessageRequest(trigger);
 
       assert.calledOnce(stub);
       assert.calledWithExactly(stub, sinon.match.array, {
         id: trigger.triggerId,
         param: trigger.triggerParam,
+        context: trigger.triggerContext,
       });
     });
   });
 
   describe("#uninit", () => {
     it("should remove the message listener on the RemotePageManager", () => {
       const [, listenerAdded] = channel.addMessageListener.firstCall.args;
       assert.isFunction(listenerAdded);
@@ -1619,16 +1625,17 @@ describe("ASRouter", () => {
           data: { trigger: { id: "firstRun" } },
         });
         await Router.onMessage(msg);
 
         assert.calledOnce(Router._findMessage);
         assert.deepEqual(Router._findMessage.firstCall.args[1], {
           id: "firstRun",
           param: undefined,
+          context: undefined,
         });
       });
       it("consider the trigger when picking a message", async () => {
         const messages = [
           {
             id: "foo1",
             template: "simple_template",
             bundled: 1,
--- a/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
@@ -1,11 +1,12 @@
 import { PanelTestProvider } from "lib/PanelTestProvider.jsm";
 import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json";
 import update_schema from "content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json";
+import whats_new_schema from "content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json";
 const messages = PanelTestProvider.getMessages();
 
 describe("PanelTestProvider", () => {
   it("should have a message", () => {
     // Careful: when changing this number make sure that new messages also go
     // through schema verifications.
     assert.lengthOf(messages, 7);
   });
@@ -20,9 +21,19 @@ describe("PanelTestProvider", () => {
   it("should be a valid message", () => {
     const updateMessages = messages.filter(
       ({ template }) => template === "update_action"
     );
     for (let message of updateMessages) {
       assert.jsonSchema(message.content, update_schema);
     }
   });
+  it("should be a valid message", () => {
+    const whatsNewMessages = messages.filter(
+      ({ template }) => template === "whatsnew_panel_message"
+    );
+    for (let message of whatsNewMessages) {
+      assert.jsonSchema(message.content, whats_new_schema);
+      // Not part of `message.content` so it can't be enforced through schema
+      assert.property(message, "order");
+    }
+  });
 });
--- a/browser/components/newtab/test/unit/common/Reducers.test.js
+++ b/browser/components/newtab/test/unit/common/Reducers.test.js
@@ -943,34 +943,36 @@ describe("Reducers", () => {
       assert.deepEqual(state.config, { enabled: true });
     });
     it("should set feeds as loaded with DISCOVERY_STREAM_FEEDS_UPDATE", () => {
       const state = DiscoveryStream(undefined, {
         type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
       });
       assert.isTrue(state.feeds.loaded);
     });
-    it("should set spoc_endpoint with DISCOVERY_STREAM_SPOCS_ENDPOINT", () => {
+    it("should set spoc_endpoint and spocs_per_domain with DISCOVERY_STREAM_SPOCS_ENDPOINT", () => {
       const state = DiscoveryStream(undefined, {
         type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,
-        data: "foo.com",
+        data: { url: "foo.com", spocs_per_domain: 2 },
       });
       assert.equal(state.spocs.spocs_endpoint, "foo.com");
+      assert.equal(state.spocs.spocs_per_domain, 2);
     });
     it("should set spocs with DISCOVERY_STREAM_SPOCS_UPDATE", () => {
       const data = {
         lastUpdated: 123,
         spocs: [1, 2, 3],
       };
       const state = DiscoveryStream(undefined, {
         type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
         data,
       });
       assert.deepEqual(state.spocs, {
         spocs_endpoint: "",
+        spocs_per_domain: 1,
         data: [1, 2, 3],
         lastUpdated: 123,
         loaded: true,
         frequency_caps: [],
         blocked: [],
       });
     });
     it("should handle no data from DISCOVERY_STREAM_SPOCS_UPDATE", () => {
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx
@@ -1,13 +1,13 @@
 import {
   DSCard,
   DefaultMeta,
   PlaceholderDSCard,
-  VariantMeta,
+  CTAButtonMeta,
 } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
 import {
   DSContextFooter,
   StatusMessage,
 } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter";
 import { actionCreators as ac } from "common/Actions.jsm";
 import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu";
 import React from "react";
@@ -172,58 +172,64 @@ describe("<DSCard>", () => {
       assert.ok(default_meta.exists());
     });
 
     it("should not render cta-link for item with no cta", () => {
       const meta = wrapper.find(DefaultMeta);
       assert.notOk(meta.find(".cta-link").exists());
     });
 
-    it("should render cta-link by default when item has cta", () => {
+    it("should not render cta-link by default when item has cta and cta_variant not link", () => {
       wrapper.setProps({ cta: "test" });
       const meta = wrapper.find(DefaultMeta);
+      assert.notOk(meta.find(".cta-link").exists());
+    });
+
+    it("should render cta-link by default when item has cta and cta_variant as link", () => {
+      wrapper.setProps({ cta: "test", cta_variant: "link" });
+      const meta = wrapper.find(DefaultMeta);
       assert.equal(meta.find(".cta-link").text(), "test");
     });
 
     it("should not render cta-button for non spoc content", () => {
-      wrapper.setProps({ cta: "test", cta_variant: true });
-      const meta = wrapper.find(VariantMeta);
+      wrapper.setProps({ cta: "test", cta_variant: "button" });
+      const meta = wrapper.find(CTAButtonMeta);
       assert.lengthOf(meta.find(".cta-button"), 0);
     });
 
-    it("should render cta-button when item has cta and cta button variant is true and is spoc", () => {
+    it("should render cta-button when item has cta and cta_variant is button and is spoc", () => {
       wrapper.setProps({
         cta: "test",
-        cta_variant: true,
+        cta_variant: "button",
         context: "Sponsored by Foo",
       });
-      const meta = wrapper.find(VariantMeta);
+      const meta = wrapper.find(CTAButtonMeta);
       assert.equal(meta.find(".cta-button").text(), "test");
     });
 
-    it("should not render Sponsored by label in footer for spoc item with cta button variant", () => {
+    it("should not render Sponsored by label in footer for spoc item with cta_variant button", () => {
       wrapper.setProps({
         cta: "test",
         context: "Sponsored by test",
-        cta_variant: true,
+        cta_variant: "button",
       });
 
-      assert.ok(wrapper.find(VariantMeta).exists());
+      assert.ok(wrapper.find(CTAButtonMeta).exists());
       assert.notOk(wrapper.find(DSContextFooter).exists());
     });
 
-    it("should render sponsor text on top for spoc item and cta_variant true", () => {
+    it("should render sponsor text on top for spoc item and cta button variant", () => {
       wrapper.setProps({
         sponsor: "Test",
         context: "Sponsored by test",
-        cta_variant: true,
+        cta_variant: "button",
       });
 
-      assert.ok(wrapper.find(VariantMeta).exists());
-      const meta = wrapper.find(VariantMeta);
+      assert.ok(wrapper.find(CTAButtonMeta).exists());
+      const meta = wrapper.find(CTAButtonMeta);
       assert.equal(meta.find(".source").text(), "Test ยท Sponsored");
     });
   });
   describe("DSCard with Intersection Observer", () => {
     beforeEach(() => {
       wrapper = shallow(<DSCard />);
     });
 
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx
@@ -0,0 +1,26 @@
+import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<DSTextPromo>", () => {
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = shallow(<DSTextPromo />);
+  });
+
+  it("should render", () => {
+    assert.ok(wrapper.exists());
+    assert.ok(wrapper.find(".ds-text-promo").exists());
+  });
+
+  it("should render a header", () => {
+    wrapper.setProps({ header: "foo" });
+    assert.ok(wrapper.find(".text").exists());
+  });
+
+  it("should render a subtitle", () => {
+    wrapper.setProps({ subtitle: "foo" });
+    assert.ok(wrapper.find(".subtitle").exists());
+  });
+});
--- a/browser/components/newtab/test/unit/content-src/components/ReturnToAMO.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ReturnToAMO.test.jsx
@@ -2,39 +2,55 @@ import { mount } from "enzyme";
 import React from "react";
 import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
 
 describe("<ReturnToAMO>", () => {
   let dispatch;
   let onReady;
   let sandbox;
   let wrapper;
+  let dummyNode;
+  let fakeDocument;
   let sendUserActionTelemetryStub;
   let content;
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     dispatch = sandbox.stub();
     onReady = sandbox.stub();
     sendUserActionTelemetryStub = sandbox.stub();
     content = {
       primary_button: {},
       secondary_button: {},
     };
+    dummyNode = document.createElement("body");
+    sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
+    fakeDocument = {
+      get activeElement() {
+        return dummyNode;
+      },
+      get body() {
+        return dummyNode;
+      },
+      getElementById() {
+        return dummyNode;
+      },
+    };
   });
 
   afterEach(() => {
     sandbox.restore();
   });
 
   describe("not mounted", () => {
     it("should send an IMPRESSION on mount", () => {
       assert.notCalled(sendUserActionTelemetryStub);
 
       wrapper = mount(
         <ReturnToAMO
+          document={fakeDocument}
           onReady={onReady}
           dispatch={dispatch}
           content={content}
           onBlock={sandbox.stub()}
           onAction={sandbox.stub()}
           UISurface="NEWTAB_OVERLAY"
           sendUserActionTelemetry={sendUserActionTelemetryStub}
         />
@@ -47,16 +63,17 @@ describe("<ReturnToAMO>", () => {
       });
     });
   });
 
   describe("mounted", () => {
     beforeEach(() => {
       wrapper = mount(
         <ReturnToAMO
+          document={fakeDocument}
           onReady={onReady}
           dispatch={dispatch}
           content={content}
           onBlock={sandbox.stub()}
           onAction={sandbox.stub()}
           UISurface="NEWTAB_OVERLAY"
           sendUserActionTelemetry={sendUserActionTelemetryStub}
         />
--- a/browser/components/newtab/test/unit/content-src/components/StartupOverlay.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/StartupOverlay.test.jsx
@@ -1,33 +1,62 @@
 import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
 import { mount } from "enzyme";
 import React from "react";
 import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
 
 describe("<StartupOverlay>", () => {
   let wrapper;
+  let fakeDocument;
+  let dummyNode;
   let dispatch;
   let onBlock;
   let sandbox;
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     dispatch = sandbox.stub();
     onBlock = sandbox.stub();
 
-    wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
+    dummyNode = document.createElement("body");
+    sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
+    fakeDocument = {
+      get activeElement() {
+        return dummyNode;
+      },
+      get body() {
+        return dummyNode;
+      },
+      getElementById() {
+        return dummyNode;
+      },
+    };
+
+    wrapper = mount(
+      <StartupOverlay
+        onBlock={onBlock}
+        dispatch={dispatch}
+        document={fakeDocument}
+      />
+    );
   });
 
   afterEach(() => {
     sandbox.restore();
   });
 
   it("should add show class after mount and timeout", async () => {
     const clock = sandbox.useFakeTimers();
-    wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
+    // We need to mount here to trigger ComponentDidMount after the FakeTimers are added.
+    wrapper = mount(
+      <StartupOverlay
+        onBlock={onBlock}
+        dispatch={dispatch}
+        document={fakeDocument}
+      />
+    );
     assert.isFalse(
       wrapper.find(".overlay-wrapper").hasClass("show"),
       ".overlay-wrapper does not have .show class"
     );
 
     clock.tick(10);
     wrapper.update();
 
--- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js
@@ -1,11 +1,10 @@
 import { _ToolbarBadgeHub } from "lib/ToolbarBadgeHub.jsm";
 import { GlobalOverrider } from "test/unit/utils";
-import { PanelTestProvider } from "lib/PanelTestProvider.jsm";
 import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
 
 describe("ToolbarBadgeHub", () => {
   let sandbox;
   let instance;
   let fakeAddImpression;
   let fakeDispatch;
@@ -26,20 +25,19 @@ describe("ToolbarBadgeHub", () => {
   let requestIdleCallbackStub;
   beforeEach(async () => {
     globals = new GlobalOverrider();
     sandbox = sinon.createSandbox();
     instance = new _ToolbarBadgeHub();
     fakeAddImpression = sandbox.stub();
     fakeDispatch = sandbox.stub();
     isBrowserPrivateStub = sandbox.stub();
-    const panelTestMsgs = await PanelTestProvider.getMessages();
     const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
     fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE");
-    whatsnewMessage = panelTestMsgs.find(({ id }) =>
+    whatsnewMessage = onboardingMsgs.find(({ id }) =>
       id.includes("WHATS_NEW_BADGE_")
     );
     fakeElement = {
       classList: {
         add: sandbox.stub(),
         remove: sandbox.stub(),
       },
       setAttribute: sandbox.stub(),
--- a/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
+++ b/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js
@@ -15,16 +15,19 @@ describe("ToolbarPanelHub", () => {
   let eventListeners = {};
   let addObserverStub;
   let removeObserverStub;
   let getBoolPrefStub;
   let setBoolPrefStub;
   let waitForInitializedStub;
   let isBrowserPrivateStub;
   let fakeDispatch;
+  let getEarliestRecordedDateStub;
+  let getEventsByDateRangeStub;
+  let handleUserActionStub;
 
   beforeEach(async () => {
     sandbox = sinon.createSandbox();
     globals = new GlobalOverrider();
     instance = new _ToolbarPanelHub();
     waitForInitializedStub = sandbox.stub().resolves();
     fakeElementById = {
       setAttribute: sandbox.stub(),
@@ -53,16 +56,20 @@ describe("ToolbarPanelHub", () => {
           appendChild: sandbox.stub(),
           setAttribute: sandbox.stub(),
         };
         createdElements.push(element);
         return element;
       },
     };
     fakeWindow = {
+      // eslint-disable-next-line object-shorthand
+      DocumentFragment: function() {
+        return fakeElementById;
+      },
       document: fakeDocument,
       browser: {
         ownerDocument: fakeDocument,
       },
       MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
       ownerGlobal: {
         openLinkIn: sandbox.stub(),
         gBrowser: "gBrowser",
@@ -89,16 +96,26 @@ describe("ToolbarPanelHub", () => {
         removeObserver: removeObserverStub,
         getBoolPref: getBoolPrefStub,
         setBoolPref: setBoolPrefStub,
       },
     });
     globals.set("PrivateBrowsingUtils", {
       isBrowserPrivate: isBrowserPrivateStub,
     });
+    getEarliestRecordedDateStub = sandbox.stub();
+    getEventsByDateRangeStub = sandbox.stub();
+    globals.set("TrackingDBService", {
+      getEarliestRecordedDate: getEarliestRecordedDateStub.returns(
+        // A random date that's not the current timestamp
+        new Date() - 500
+      ),
+      getEventsByDateRange: getEventsByDateRangeStub.returns([]),
+    });
+    handleUserActionStub = sandbox.stub();
   });
   afterEach(() => {
     instance.uninit();
     sandbox.restore();
     globals.restore();
     eventListeners = {};
     createdElements = [];
   });
@@ -237,87 +254,148 @@ describe("ToolbarPanelHub", () => {
   });
   describe("#renderMessages", () => {
     let getMessagesStub;
     beforeEach(() => {
       getMessagesStub = sandbox.stub();
       instance.init(waitForInitializedStub, {
         getMessages: getMessagesStub,
         dispatch: fakeDispatch,
+        handleUserAction: handleUserActionStub,
       });
     });
     it("should render messages to the panel on renderMessages()", async () => {
       const messages = (await PanelTestProvider.getMessages()).filter(
         m => m.template === "whatsnew_panel_message"
       );
       messages[0].content.link_text = { string_id: "link_text_id" };
 
-      getMessagesStub.returns([messages[0], messages[2], messages[1]]);
+      getMessagesStub.returns(messages);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
       for (let message of messages) {
-        assert.ok(
-          createdElements.find(
-            el =>
-              el.tagName === "h2" && el.textContent === message.content.title
-          )
-        );
-        assert.ok(
-          createdElements.find(
-            el => el.tagName === "p" && el.textContent === message.content.body
-          )
-        );
+        assert.ok(createdElements.find(el => el.tagName === "h2"));
+        if (message.content.layout === "tracking-protections") {
+          assert.ok(createdElements.find(el => el.tagName === "h4"));
+        }
+        assert.ok(createdElements.find(el => el.tagName === "p"));
       }
       // Call the click handler to make coverage happy.
       eventListeners.click();
-      assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
+      assert.calledOnce(handleUserActionStub);
+    });
+    it("should sort based on order field value", async () => {
+      const messages = (await PanelTestProvider.getMessages()).filter(
+        m =>
+          m.template === "whatsnew_panel_message" &&
+          m.content.published_date === 1560969794394
+      );
+
+      messages.forEach(m => (m.content.title = m.order));
+
+      getMessagesStub.returns(messages);
+
+      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
+
+      // Select the title elements that are supposed to be set to the same
+      // value as the `order` field of the message
+      const titleEls = createdElements
+        .filter(
+          el =>
+            el.classList.add.firstCall &&
+            el.classList.add.firstCall.args[0] === "whatsNew-message-title"
+        )
+        .map(el => el.textContent);
+      assert.deepEqual(titleEls, [1, 2, 3]);
     });
     it("should accept string for image attributes", async () => {
       const messages = (await PanelTestProvider.getMessages()).filter(
-        m => m.template === "whatsnew_panel_message"
+        m => m.id === "WHATS_NEW_70_1"
       );
-      getMessagesStub.returns([messages[0], messages[2], messages[1]]);
+      getMessagesStub.returns(messages);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
       const imageEl = createdElements.find(el => el.tagName === "img");
       assert.calledOnce(imageEl.setAttribute);
       assert.calledWithExactly(
         imageEl.setAttribute,
         "alt",
         "Firefox Send Logo"
       );
     });
     it("should accept fluent ids for image attributes", async () => {
       const messages = (await PanelTestProvider.getMessages()).filter(
-        m => m.template === "whatsnew_panel_message"
+        m => m.id === "WHATS_NEW_70_1"
       );
       messages[0].content.icon_alt = { string_id: "foo" };
-      getMessagesStub.returns([messages[0], messages[2], messages[1]]);
+      getMessagesStub.returns(messages);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
       const imageEl = createdElements.find(el => el.tagName === "img");
-      assert.calledOnce(fakeDocument.l10n.setAttributes);
       assert.calledWithExactly(fakeDocument.l10n.setAttributes, imageEl, "foo");
     });
+    it("should accept fluent ids for elements attributes", async () => {
+      const [message] = (await PanelTestProvider.getMessages()).filter(
+        m =>
+          m.template === "whatsnew_panel_message" &&
+          m.content.layout === "tracking-protections"
+      );
+      getMessagesStub.returns([message]);
+      instance.state.contentArguments = { foo: "foo", bar: "bar" };
+
+      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
+
+      const subtitle = createdElements.find(el => el.tagName === "h4");
+      assert.calledWithExactly(
+        fakeDocument.l10n.setAttributes,
+        subtitle,
+        message.content.subtitle.string_id,
+        instance.state.contentArguments
+      );
+    });
+    it("should correctly compute blocker trackers and date", async () => {
+      const messages = (await PanelTestProvider.getMessages()).filter(
+        m => m.template === "whatsnew_panel_message"
+      );
+      getMessagesStub.returns(messages);
+      getEventsByDateRangeStub.returns([
+        { getResultByName: sandbox.stub().returns(2) },
+        { getResultByName: sandbox.stub().returns(3) },
+      ]);
+
+      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
+
+      assert.calledWithExactly(
+        fakeDocument.l10n.setAttributes,
+        sinon.match.object,
+        sinon.match.string,
+        { blockedCount: "5", earliestDate: getEarliestRecordedDateStub() }
+      );
+    });
     it("should only render unique dates (no duplicates)", async () => {
-      instance._createDateElement = sandbox.stub();
       const messages = (await PanelTestProvider.getMessages()).filter(
         m => m.template === "whatsnew_panel_message"
       );
       const uniqueDates = [
         ...new Set(messages.map(m => m.content.published_date)),
       ];
       getMessagesStub.returns(messages);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
 
-      assert.callCount(instance._createDateElement, uniqueDates.length);
+      const dateElements = createdElements.filter(
+        el =>
+          el.tagName === "p" &&
+          el.classList.add.firstCall &&
+          el.classList.add.firstCall.args[0] === "whatsNew-message-date"
+      );
+      assert.lengthOf(dateElements, uniqueDates.length);
     });
     it("should listen for panelhidden and remove the toolbar button", async () => {
       getMessagesStub.returns([]);
       fakeDocument.getElementById
         .withArgs("customizationui-widget-panel")
         .returns(null);
 
       await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
@@ -487,16 +565,17 @@ describe("ToolbarPanelHub", () => {
         target: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument },
       });
     beforeEach(async () => {
       const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();
       await instance.init(waitForInitializedStub, {
         dispatch: fakeDispatch,
         getMessages: () =>
           onboardingMsgs.find(msg => msg.template === "protections_panel"),
+        handleUserAction: handleUserActionStub,
       });
     });
     it("should remember it showed", async () => {
       await fakeInsert();
 
       assert.calledWithExactly(
         setBoolPrefStub,
         "browser.protections_panel.infoMessage.seen",
@@ -517,12 +596,22 @@ describe("ToolbarPanelHub", () => {
 
       assert.callCount(fakeElementById.toggleAttribute, 4);
     });
     it("should open link on click", async () => {
       await fakeInsert();
 
       eventListeners.click();
 
-      assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
+      assert.calledOnce(handleUserActionStub);
+      assert.calledWithExactly(handleUserActionStub, {
+        target: fakeWindow,
+        data: {
+          type: "OPEN_URL",
+          data: {
+            args: sinon.match.string,
+            where: "tabshifted",
+          },
+        },
+      });
     });
   });
 });