Bug 1569303 - Extended First Run Triplets. r=k88hudson, a=RyanVM
authorEd Lee <edilee@mozilla.com>
Wed, 14 Aug 2019 03:23:04 +0000
changeset 541951 c2aa1c31c07e4427bea5ec58432f8806dbe4d973
parent 541950 ea6a6a73f704a455ac0983c20b57524d96322261
child 541952 151bfae582afadf93c61e55e1fd4f980a716756f
push id11792
push userryanvm@gmail.com
push dateThu, 15 Aug 2019 16:05:04 +0000
treeherdermozilla-beta@c2aa1c31c07e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson, RyanVM
bugs1569303, 1560065, 1568909, 1570026, 1571817, 1572378, 243975, 1570754, 1570481, 1571442, 1573209
milestone69.0
Bug 1569303 - Extended First Run Triplets. r=k88hudson, a=RyanVM Includes 10 activity-stream commits: fe05904e Bug 1560065 - Refactor first run to separate Interrupts and Triplets cdd41d99 Bug 1560065 - Converted hooks to class component due to failing tests d40f56fd Bug 1568909 - Show triplets for extended period after first run 82187f91 Bug 1570026 - Part 2. Refactor FirstRun to use generic fetchFlowParams 2fa2908c Bug 1571817 - Hold back rollout test for Extended Triplets (#5233) 469c7ee7 Bug 1572378 - Unable to install extension from RTAMO overlay (#5234) 00243975 Bug 1570754 - Add delay to clearing triplets and always block extended 0e377da0 Port 1570481 - Update and reorder set of first run cards r=Mardak 7af1293f Bug 1571442 - Search box shouldn't move down with extended Triplets on d9e0fa13 Bug 1573209 - Installing addon from ReturntoAMO shouldnt hide triplets (#5248) Differential Revision: https://phabricator.services.mozilla.com/D41888
browser/app/profile/firefox.js
browser/components/newtab/.eslintrc.js
browser/components/newtab/content-src/asrouter/asrouter-content.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/FirstRun.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/Interrupt.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/Triplets.jsx
browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js
browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/_OnboardingMessage.scss
browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.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/docs/v2-system-addon/data_events.md
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/test/browser/browser_aboutwelcome.js
browser/components/newtab/test/browser/browser_onboarding_rtamo.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
browser/components/newtab/test/unit/asrouter/constants.js
browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
browser/components/newtab/test/unit/asrouter/templates/Interrupt.test.jsx
browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
browser/components/newtab/test/unit/asrouter/templates/Triplets.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/content-src/components/addUtmParams.test.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1314,17 +1314,17 @@ pref("browser.newtabpage.activity-stream
 
 // The pref controls if search hand-off is enabled for Activity Stream.
 #ifdef NIGHTLY_BUILD
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true);
 #else
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
 #endif
 
-pref("trailhead.firstrun.branches", "join-privacy");
+pref("trailhead.firstrun.branches", "join-supercharge");
 
 // The pref that controls if the What's New panel is enabled.
 pref("browser.messaging-system.whatsNewPanel.enabled", false);
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -39,16 +39,17 @@ module.exports = {
     "RPMSendAsyncMessage": true,
     "NewTabPagePreloading": true,
   },
   "overrides": [
     {
       // These files use fluent-dom to insert content
       "files": [
         "content-src/asrouter/templates/OnboardingMessage/**",
+        "content-src/asrouter/templates/FirstRun/**",
         "content-src/asrouter/templates/Trailhead/**",
         "content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
         "content-src/components/TopSites/**",
         "content-src/components/MoreRecommendations/MoreRecommendations.jsx",
         "content-src/components/CollapsibleSection/CollapsibleSection.jsx"
       ],
       "rules": {
         "jsx-a11y/anchor-has-content": 0,
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -7,27 +7,30 @@ import {
   actionTypes as at,
   ASRouterActions as ra,
 } from "common/Actions.jsm";
 import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
 import { generateBundles } from "./rich-text-strings";
 import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
 import { LocalizationProvider } from "fluent-react";
 import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
-import { OnboardingMessage } from "./templates/OnboardingMessage/OnboardingMessage";
 import React from "react";
 import ReactDOM from "react-dom";
-import { ReturnToAMO } from "./templates/ReturnToAMO/ReturnToAMO";
 import { SnippetsTemplates } from "./templates/template-manifest";
-import { StartupOverlay } from "./templates/StartupOverlay/StartupOverlay";
-import { Trailhead } from "./templates/Trailhead/Trailhead";
+import { FirstRun } from "./templates/FirstRun/FirstRun";
 
 const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
 const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
-const TEMPLATES_ABOVE_PAGE = ["trailhead"];
+const TEMPLATES_ABOVE_PAGE = [
+  "trailhead",
+  "fxa_overlay",
+  "return_to_amo_overlay",
+  "extended_triplets",
+];
+const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
 const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
 
 export const ASRouterUtils = {
   addListener(listener) {
     if (global.RPMAddMessageListener) {
       global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, listener);
     }
   },
@@ -45,19 +48,16 @@ export const ASRouterUtils = {
     ASRouterUtils.sendMessage({
       type: "BLOCK_MESSAGE_BY_ID",
       data: { id, ...options },
     });
   },
   dismissById(id) {
     ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
   },
-  dismissBundle(bundle) {
-    ASRouterUtils.sendMessage({ type: "DISMISS_BUNDLE", data: { bundle } });
-  },
   executeAction(button_action) {
     ASRouterUtils.sendMessage({
       type: "USER_ACTION",
       data: button_action,
     });
   },
   unblockById(id) {
     ASRouterUtils.sendMessage({ type: "UNBLOCK_MESSAGE_BY_ID", data: { id } });
@@ -109,17 +109,19 @@ function shouldSendImpressionOnUpdate(ne
 export class ASRouterUISurface extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
     this.onUserAction = this.onUserAction.bind(this);
-    this.state = { message: {}, bundle: {} };
+    this.fetchFlowParams = this.fetchFlowParams.bind(this);
+
+    this.state = { message: {}, interruptCleared: false };
     if (props.document) {
       this.headerPortal = props.document.getElementById(
         "header-asrouter-container"
       );
       this.footerPortal = props.document.getElementById(
         "footer-asrouter-container"
       );
     }
@@ -152,36 +154,32 @@ export class ASRouterUISurface extends R
             data: {
               event: "FXA_METRICS_FETCH_ERROR",
               value: response.status,
             },
           })
         );
       }
     } catch (error) {
-      console.error(error);
+      console.error(error); // eslint-disable-line no-console
       dispatch(
         ac.OnlyToMain({
           type: at.TELEMETRY_UNDESIRED_EVENT,
           data: { event: "FXA_METRICS_ERROR" },
         })
       );
     }
     return result;
   }
 
   sendUserActionTelemetry(extraProps = {}) {
-    const { message, bundle } = this.state;
-    if (!message && !extraProps.message_id) {
-      throw new Error(`You must provide a message_id for bundled messages`);
-    }
-    // snippets_user_event, onboarding_user_event
-    const eventType = `${message.provider || bundle.provider}_user_event`;
+    const { message } = this.state;
+    const eventType = `${message.provider}_user_event`;
     ASRouterUtils.sendTelemetry({
-      message_id: message.id || extraProps.message_id,
+      message_id: message.id,
       source: extraProps.id,
       action: eventType,
       ...extraProps,
     });
   }
 
   sendImpression(extraProps) {
     if (this.state.message.provider === "preview") {
@@ -223,67 +221,42 @@ export class ASRouterUISurface extends R
   onBlockById(id) {
     return options => ASRouterUtils.blockById(id, options);
   }
 
   onDismissById(id) {
     return () => ASRouterUtils.dismissById(id);
   }
 
-  dismissBundle(bundle) {
-    return () => {
-      ASRouterUtils.dismissBundle(bundle);
-      this.sendUserActionTelemetry({
-        event: "DISMISS",
-        id: "onboarding-cards",
-        message_id: bundle.map(m => m.id).join(","),
-        // Passing the action because some bundles (Trailhead) don't have a provider set
-        action: "onboarding_user_event",
-      });
-    };
-  }
-
-  triggerOnboarding() {
-    ASRouterUtils.sendMessage({
-      type: "TRIGGER",
-      data: { trigger: { id: "showOnboarding" } },
-    });
-  }
-
   clearMessage(id) {
     if (id === this.state.message.id) {
       this.setState({ message: {} });
       // Remove any styles related to the RTAMO message
       document.body.classList.remove("welcome", "hide-main", "amo");
     }
   }
 
   onMessageFromParent({ data: action }) {
     switch (action.type) {
       case "SET_MESSAGE":
         this.setState({ message: action.data });
         break;
-      case "SET_BUNDLED_MESSAGES":
-        this.setState({ bundle: action.data });
+      case "CLEAR_INTERRUPT":
+        this.setState({ interruptCleared: true });
         break;
       case "CLEAR_MESSAGE":
         this.clearMessage(action.data.id);
         break;
       case "CLEAR_PROVIDER":
         if (action.data.id === this.state.message.provider) {
           this.setState({ message: {} });
         }
         break;
-      case "CLEAR_BUNDLE":
-        if (this.state.bundle.bundle) {
-          this.setState({ bundle: {} });
-        }
-        break;
       case "CLEAR_ALL":
-        this.setState({ message: {}, bundle: {} });
+        this.setState({ message: {} });
         break;
       case "AS_ROUTER_TARGETING_UPDATE":
         action.data.forEach(id => this.clearMessage(id));
         break;
     }
   }
 
   componentWillMount() {
@@ -303,17 +276,17 @@ export class ASRouterUISurface extends R
       this.props.document.location.href === "about:welcome"
     ) {
       ASRouterUtils.sendMessage({
         type: "TRIGGER",
         data: { trigger: { id: "firstRun" } },
       });
     } else {
       ASRouterUtils.sendMessage({
-        type: "SNIPPETS_REQUEST",
+        type: "NEWTAB_MESSAGE_REQUEST",
         data: { endpoint },
       });
     }
   }
 
   componentWillUnmount() {
     ASRouterUtils.removeListener(this.onMessageFromParent);
   }
@@ -340,25 +313,21 @@ export class ASRouterUISurface extends R
         ASRouterUtils.executeAction({ type: ra.OPEN_URL, data: { args: url } });
         break;
       default:
         ASRouterUtils.executeAction(action);
     }
   }
 
   renderSnippets() {
-    if (
-      this.state.bundle.template === "onboarding" ||
-      this.state.message.template === "fxa_overlay" ||
-      this.state.message.template === "return_to_amo_overlay" ||
-      this.state.message.template === "trailhead"
-    ) {
+    const { message } = this.state;
+    if (!SnippetsTemplates[message.template]) {
       return null;
     }
-    const SnippetComponent = SnippetsTemplates[this.state.message.template];
+    const SnippetComponent = SnippetsTemplates[message.template];
     const { content } = this.state.message;
 
     return (
       <ImpressionsWrapper
         id="NEWTAB_FOOTER_BAR"
         message={this.state.message}
         sendImpression={this.sendImpression}
         shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}
@@ -375,96 +344,62 @@ export class ASRouterUISurface extends R
             sendClick={this.sendClick}
             sendUserActionTelemetry={this.sendUserActionTelemetry}
           />
         </LocalizationProvider>
       </ImpressionsWrapper>
     );
   }
 
-  renderOnboarding() {
-    if (this.state.bundle.template === "onboarding") {
-      return (
-        <OnboardingMessage
-          {...this.state.bundle}
-          UISurface="NEWTAB_OVERLAY"
-          onAction={ASRouterUtils.executeAction}
-          onDismissBundle={this.dismissBundle(this.state.bundle.bundle)}
-          sendUserActionTelemetry={this.sendUserActionTelemetry}
-        />
-      );
-    }
-    return null;
-  }
-
-  renderFirstRunOverlay() {
-    const { message } = this.state;
-    if (message.template === "fxa_overlay") {
-      global.document.body.classList.add("fxa");
-      return (
-        <StartupOverlay
-          onReady={this.triggerOnboarding}
-          onBlock={this.onDismissById(message.id)}
-          dispatch={this.props.dispatch}
-        />
-      );
-    } else if (message.template === "return_to_amo_overlay") {
-      global.document.body.classList.add("amo");
-      return (
-        <LocalizationProvider
-          bundles={generateBundles({ amo_html: message.content.text })}
-        >
-          <ReturnToAMO
-            {...message}
-            UISurface="NEWTAB_OVERLAY"
-            onReady={this.triggerOnboarding}
-            onBlock={this.onDismissById(message.id)}
-            onAction={ASRouterUtils.executeAction}
-            sendUserActionTelemetry={this.sendUserActionTelemetry}
-          />
-        </LocalizationProvider>
-      );
-    }
-    return null;
-  }
-
-  renderTrailhead() {
-    const { message } = this.state;
-    if (message.template === "trailhead") {
-      return (
-        <Trailhead
-          document={this.props.document}
-          message={message}
-          onAction={ASRouterUtils.executeAction}
-          onDismissBundle={this.dismissBundle(this.state.message.bundle)}
-          sendUserActionTelemetry={this.sendUserActionTelemetry}
-          dispatch={this.props.dispatch}
-          fxaEndpoint={this.props.fxaEndpoint}
-        />
-      );
-    }
-    return null;
-  }
-
   renderPreviewBanner() {
     if (this.state.message.provider !== "preview") {
       return null;
     }
 
     return (
       <div className="snippets-preview-banner">
         <span className="icon icon-small-spacer icon-info" />
         <span>Preview Purposes Only</span>
       </div>
     );
   }
 
+  renderFirstRun() {
+    const { message } = this.state;
+    if (FIRST_RUN_TEMPLATES.includes(message.template)) {
+      return (
+        <ImpressionsWrapper
+          id="FIRST_RUN"
+          message={this.state.message}
+          sendImpression={this.sendImpression}
+          shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}
+          // This helps with testing
+          document={this.props.document}
+        >
+          <FirstRun
+            document={this.props.document}
+            interruptCleared={this.state.interruptCleared}
+            message={message}
+            sendUserActionTelemetry={this.sendUserActionTelemetry}
+            executeAction={ASRouterUtils.executeAction}
+            dispatch={this.props.dispatch}
+            onBlockById={ASRouterUtils.blockById}
+            onDismiss={this.onDismissById(this.state.message.id)}
+            fxaEndpoint={this.props.fxaEndpoint}
+            fetchFlowParams={this.fetchFlowParams}
+          />
+        </ImpressionsWrapper>
+      );
+    }
+    return null;
+  }
+
   render() {
-    const { message, bundle } = this.state;
-    if (!message.id && !bundle.template) {
+    const { message } = this.state;
+    if (!message.id) {
       return null;
     }
     const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
       message.template
     );
     const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(
       message.template
     );
@@ -475,19 +410,17 @@ export class ASRouterUISurface extends R
         {this.renderSnippets()}
       </div>
     ) : (
       // For onboarding, regular snippets etc. we should render
       // everything in our footer container.
       ReactDOM.createPortal(
         <>
           {this.renderPreviewBanner()}
-          {this.renderTrailhead()}
-          {this.renderFirstRunOverlay()}
-          {this.renderOnboarding()}
+          {this.renderFirstRun()}
           {this.renderSnippets()}
         </>,
         shouldRenderInHeader ? this.headerPortal : this.footerPortal
       )
     );
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/FirstRun.jsx
@@ -0,0 +1,210 @@
+/* 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 { Interrupt } from "./Interrupt";
+import { Triplets } from "./Triplets";
+import { BASE_PARAMS } from "./addUtmParams";
+
+// Note: should match the transition time on .trailheadCards in _Trailhead.scss
+const TRANSITION_LENGTH = 500;
+
+export const FLUENT_FILES = [
+  "branding/brand.ftl",
+  "browser/branding/brandings.ftl",
+  "browser/branding/sync-brand.ftl",
+  "browser/newtab/onboarding.ftl",
+];
+
+export const helpers = {
+  selectInterruptAndTriplets(message = {}, interruptCleared) {
+    const hasInterrupt =
+      interruptCleared === true ? false : Boolean(message.content);
+    const hasTriplets = Boolean(message.bundle && message.bundle.length);
+    const UTMTerm = message.utm_term || "";
+    return {
+      hasTriplets,
+      hasInterrupt,
+      interrupt: hasInterrupt ? message : null,
+      triplets: hasTriplets ? message.bundle : null,
+      UTMTerm,
+    };
+  },
+
+  addFluent(document) {
+    FLUENT_FILES.forEach(file => {
+      const link = document.head.appendChild(document.createElement("link"));
+      link.href = file;
+      link.rel = "localization";
+    });
+  },
+};
+
+export class FirstRun extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.didLoadFlowParams = false;
+
+    this.state = {
+      prevMessage: undefined,
+
+      hasInterrupt: false,
+      hasTriplets: false,
+
+      interrupt: undefined,
+      triplets: undefined,
+
+      isInterruptVisible: false,
+      isTripletsContainerVisible: false,
+      isTripletsContentVisible: false,
+
+      UTMTerm: "",
+
+      flowParams: undefined,
+    };
+
+    this.closeInterrupt = this.closeInterrupt.bind(this);
+    this.closeTriplets = this.closeTriplets.bind(this);
+
+    helpers.addFluent(this.props.document);
+  }
+
+  static getDerivedStateFromProps(props, state) {
+    const { message, interruptCleared } = props;
+    if (
+      interruptCleared !== state.prevInterruptCleared ||
+      (message && message.id !== state.prevMessageId)
+    ) {
+      const {
+        hasTriplets,
+        hasInterrupt,
+        interrupt,
+        triplets,
+        UTMTerm,
+      } = helpers.selectInterruptAndTriplets(message, interruptCleared);
+
+      return {
+        prevMessageId: message.id,
+        prevInterruptCleared: interruptCleared,
+
+        hasInterrupt,
+        hasTriplets,
+
+        interrupt,
+        triplets,
+
+        isInterruptVisible: hasInterrupt,
+        isTripletsContainerVisible: hasTriplets,
+        isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
+
+        UTMTerm,
+      };
+    }
+    return null;
+  }
+
+  async fetchFlowParams() {
+    const { fxaEndpoint, fetchFlowParams } = this.props;
+    const { UTMTerm } = this.state;
+    if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
+      this.didLoadFlowParams = true;
+      const flowParams = await fetchFlowParams({
+        ...BASE_PARAMS,
+        entrypoint: "activity-stream-firstrun",
+        form_type: "email",
+        utm_term: UTMTerm,
+      });
+      this.setState({ flowParams });
+    }
+  }
+
+  removeHideMain() {
+    if (!this.state.hasInterrupt) {
+      // We need to remove hide-main since we should show it underneath everything that has rendered
+      this.props.document.body.classList.remove("hide-main", "welcome");
+    }
+  }
+
+  componentDidMount() {
+    this.fetchFlowParams();
+    this.removeHideMain();
+  }
+
+  componentDidUpdate() {
+    // In case we didn't have FXA info immediately, try again when we receive it.
+    this.fetchFlowParams();
+    this.removeHideMain();
+  }
+
+  closeInterrupt() {
+    this.setState(prevState => ({
+      isInterruptVisible: false,
+      isTripletsContainerVisible: prevState.hasTriplets,
+      isTripletsContentVisible: prevState.hasTriplets,
+    }));
+  }
+
+  closeTriplets() {
+    this.setState({ isTripletsContainerVisible: false });
+
+    // Closing triplets should prevent any future extended triplets from showing up
+    setTimeout(() => {
+      this.props.onBlockById("EXTENDED_TRIPLETS_1");
+    }, TRANSITION_LENGTH);
+  }
+
+  render() {
+    const { props } = this;
+    const {
+      sendUserActionTelemetry,
+      fxaEndpoint,
+      dispatch,
+      executeAction,
+    } = props;
+
+    const {
+      interrupt,
+      triplets,
+      isInterruptVisible,
+      isTripletsContainerVisible,
+      isTripletsContentVisible,
+      hasTriplets,
+      UTMTerm,
+      flowParams,
+    } = this.state;
+
+    return (
+      <>
+        {isInterruptVisible ? (
+          <Interrupt
+            document={props.document}
+            message={interrupt}
+            onNextScene={this.closeInterrupt}
+            UTMTerm={UTMTerm}
+            sendUserActionTelemetry={sendUserActionTelemetry}
+            executeAction={executeAction}
+            dispatch={dispatch}
+            flowParams={flowParams}
+            onDismiss={this.closeInterrupt}
+            fxaEndpoint={fxaEndpoint}
+          />
+        ) : null}
+        {hasTriplets ? (
+          <Triplets
+            document={props.document}
+            cards={triplets}
+            showCardPanel={isTripletsContainerVisible}
+            showContent={isTripletsContentVisible}
+            hideContainer={this.closeTriplets}
+            sendUserActionTelemetry={sendUserActionTelemetry}
+            UTMTerm={`${UTMTerm}-card`}
+            flowParams={flowParams}
+            onAction={executeAction}
+          />
+        ) : null}
+      </>
+    );
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/Interrupt.jsx
@@ -0,0 +1,67 @@
+/* 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 { Trailhead } from "../Trailhead/Trailhead";
+import { ReturnToAMO } from "../ReturnToAMO/ReturnToAMO";
+import { StartupOverlay } from "../StartupOverlay/StartupOverlay";
+import { LocalizationProvider } from "fluent-react";
+import { generateBundles } from "../../rich-text-strings";
+
+export class Interrupt extends React.PureComponent {
+  render() {
+    const {
+      onDismiss,
+      onNextScene,
+      message,
+      sendUserActionTelemetry,
+      executeAction,
+      dispatch,
+      fxaEndpoint,
+      UTMTerm,
+      flowParams,
+    } = this.props;
+
+    switch (message.template) {
+      case "return_to_amo_overlay":
+        return (
+          <LocalizationProvider
+            bundles={generateBundles({ amo_html: message.content.text })}
+          >
+            <ReturnToAMO
+              {...message}
+              UISurface="NEWTAB_OVERLAY"
+              onBlock={onDismiss}
+              onAction={executeAction}
+              sendUserActionTelemetry={sendUserActionTelemetry}
+            />
+          </LocalizationProvider>
+        );
+      case "fxa_overlay":
+        return (
+          <StartupOverlay
+            onBlock={onDismiss}
+            dispatch={dispatch}
+            fxa_endpoint={fxaEndpoint}
+          />
+        );
+      case "trailhead":
+        return (
+          <Trailhead
+            document={this.props.document}
+            message={message}
+            onNextScene={onNextScene}
+            onAction={executeAction}
+            sendUserActionTelemetry={sendUserActionTelemetry}
+            dispatch={dispatch}
+            fxaEndpoint={fxaEndpoint}
+            UTMTerm={UTMTerm}
+            flowParams={flowParams}
+          />
+        );
+      default:
+        throw new Error(`${message.template} is not a valid FirstRun message`);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/Triplets.jsx
@@ -0,0 +1,91 @@
+/* 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 { OnboardingCard } from "../../templates/OnboardingMessage/OnboardingMessage";
+import { addUtmParams } from "./addUtmParams";
+
+export class Triplets extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onCardAction = this.onCardAction.bind(this);
+    this.onHideContainer = this.onHideContainer.bind(this);
+  }
+
+  componentWillMount() {
+    global.document.body.classList.add("inline-onboarding");
+  }
+
+  componentWillUnmount() {
+    this.props.document.body.classList.remove("inline-onboarding");
+  }
+
+  onCardAction(action) {
+    let actionUpdates = {};
+    const { flowParams, UTMTerm } = this.props;
+
+    if (action.type === "OPEN_URL") {
+      let url = new URL(action.data.args);
+      addUtmParams(url, UTMTerm);
+
+      if (action.addFlowParams) {
+        url.searchParams.append("device_id", flowParams.deviceId);
+        url.searchParams.append("flow_id", flowParams.flowId);
+        url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
+      }
+
+      actionUpdates = { data: { ...action.data, args: url.toString() } };
+    }
+
+    this.props.onAction({ ...action, ...actionUpdates });
+  }
+
+  onHideContainer() {
+    const { sendUserActionTelemetry, cards, hideContainer } = this.props;
+    hideContainer();
+    sendUserActionTelemetry({
+      event: "DISMISS",
+      id: "onboarding-cards",
+      message_id: cards.map(m => m.id).join(","),
+      action: "onboarding_user_event",
+    });
+  }
+
+  render() {
+    const {
+      cards,
+      showCardPanel,
+      showContent,
+      sendUserActionTelemetry,
+    } = this.props;
+    return (
+      <div
+        className={`trailheadCards ${showCardPanel ? "expanded" : "collapsed"}`}
+      >
+        <div className="trailheadCardsInner" aria-hidden={!showContent}>
+          <h1 data-l10n-id="onboarding-welcome-header" />
+          <div className={`trailheadCardGrid${showContent ? " show" : ""}`}>
+            {cards.map(card => (
+              <OnboardingCard
+                key={card.id}
+                className="trailheadCard"
+                sendUserActionTelemetry={sendUserActionTelemetry}
+                onAction={this.onCardAction}
+                UISurface="TRAILHEAD"
+                {...card}
+              />
+            ))}
+          </div>
+          {showCardPanel && (
+            <button
+              className="icon icon-dismiss"
+              onClick={this.onHideContainer}
+              data-l10n-id="onboarding-cards-dismiss"
+            />
+          )}
+        </div>
+      </div>
+    );
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/FirstRun/addUtmParams.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+export const BASE_PARAMS = {
+  utm_source: "activity-stream",
+  utm_campaign: "firstrun",
+  utm_medium: "referral",
+};
+
+/**
+ * Takes in a url as a string or URL object and returns a URL object with the
+ * utm_* parameters added to it. If a URL object is passed in, the paraemeters
+ * are added to it (the return value can be ignored in that case as it's the
+ * same object).
+ */
+export function addUtmParams(url, utmTerm) {
+  let returnUrl = url;
+  if (typeof returnUrl === "string") {
+    returnUrl = new URL(url);
+  }
+  Object.keys(BASE_PARAMS).forEach(key => {
+    returnUrl.searchParams.append(key, BASE_PARAMS[key]);
+  });
+  returnUrl.searchParams.append("utm_term", utmTerm);
+  return returnUrl;
+}
--- a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx
@@ -1,16 +1,13 @@
-import { ModalOverlay } from "../../components/ModalOverlay/ModalOverlay";
-import React from "react";
+/* 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/. */
 
-const FLUENT_FILES = [
-  "branding/brand.ftl",
-  "browser/branding/sync-brand.ftl",
-  "browser/newtab/onboarding.ftl",
-];
+import React from "react";
 
 export class OnboardingCard extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
   }
 
   onClick() {
@@ -48,38 +45,8 @@ export class OnboardingCard extends Reac
               onClick={this.onClick}
             />
           </span>
         </div>
       </div>
     );
   }
 }
-
-export class OnboardingMessage extends React.PureComponent {
-  componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
-  }
-
-  render() {
-    const { props } = this;
-    const { button_label, header } = props.extraTemplateStrings;
-    return (
-      <ModalOverlay {...props} button_label={button_label} title={header}>
-        <div className="onboardingMessageContainer">
-          {props.bundle.map(message => (
-            <OnboardingCard
-              key={message.id}
-              sendUserActionTelemetry={props.sendUserActionTelemetry}
-              onAction={props.onAction}
-              UISurface={props.UISurface}
-              {...message}
-            />
-          ))}
-        </div>
-      </ModalOverlay>
-    );
-  }
-}
--- a/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/_OnboardingMessage.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/_OnboardingMessage.scss
@@ -1,25 +1,8 @@
-.onboardingMessageContainer {
-  display: grid;
-  grid-column-gap: 21px;
-  grid-template-columns: auto auto auto;
-  padding-left: 30px;
-  padding-right: 30px;
-  min-height: 500px;
-
-  // at 850px, the cards go from vertical layout to horizontal layout
-  @media(max-width: 850px) {
-    grid-template-columns: none;
-    grid-template-rows: auto auto auto;
-    padding-left: 110px;
-    padding-right: 110px;
-  }
-}
-
 .onboardingMessage {
   height: 340px;
   text-align: center;
   padding: 13px;
   font-weight: 200;
 
   // at 850px, img floats left, content floats right next to it
   @media(max-width: 850px) {
--- a/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
@@ -6,18 +6,21 @@ const ICON_ALT_TEXT = "";
 
 export class ReturnToAMO extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onClickAddExtension = this.onClickAddExtension.bind(this);
     this.onBlockButton = this.onBlockButton.bind(this);
   }
 
+  componentWillMount() {
+    global.document.body.classList.add("amo");
+  }
+
   componentDidMount() {
-    this.props.onReady();
     this.props.sendUserActionTelemetry({
       event: "IMPRESSION",
       id: this.props.UISurface,
     });
   }
 
   onClickAddExtension() {
     this.props.onAction(this.props.content.primary_button.action);
--- a/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
@@ -1,109 +1,54 @@
-import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
-import { connect } from "react-redux";
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { actionCreators as ac } from "common/Actions.jsm";
 import React from "react";
 
-const FLUENT_FILES = [
-  "branding/brand.ftl",
-  "browser/branding/sync-brand.ftl",
-  "browser/newtab/onboarding.ftl",
-];
-
-export class _StartupOverlay extends React.PureComponent {
+export class StartupOverlay extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onInputChange = this.onInputChange.bind(this);
     this.onSubmit = this.onSubmit.bind(this);
     this.clickSkip = this.clickSkip.bind(this);
-    this.initScene = this.initScene.bind(this);
     this.removeOverlay = this.removeOverlay.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
 
     this.utmParams =
       "utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
 
     this.state = {
+      show: false,
       emailInput: "",
-      overlayRemoved: false,
-      deviceId: "",
-      flowId: "",
-      flowBeginTime: 0,
     };
-    this.didFetch = false;
   }
 
-  async componentWillUpdate() {
-    if (this.props.fxa_endpoint && !this.didFetch) {
-      try {
-        this.didFetch = true;
-        const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
-        const response = await fetch(
-          `${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${
-            this.utmParams
-          }`,
-          { credentials: "omit" }
-        );
-        if (response.status === 200) {
-          const { deviceId, flowId, flowBeginTime } = await response.json();
-          this.setState({ deviceId, flowId, flowBeginTime });
-        } else {
-          this.props.dispatch(
-            ac.OnlyToMain({
-              type: at.TELEMETRY_UNDESIRED_EVENT,
-              data: {
-                event: "FXA_METRICS_FETCH_ERROR",
-                value: response.status,
-              },
-            })
-          );
-        }
-      } catch (error) {
-        this.props.dispatch(
-          ac.OnlyToMain({
-            type: at.TELEMETRY_UNDESIRED_EVENT,
-            data: { event: "FXA_METRICS_ERROR" },
-          })
-        );
-      }
-    }
-  }
-
-  async componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
-
-    await this.componentWillUpdate(this.props);
+  componentWillMount() {
+    global.document.body.classList.add("fxa");
   }
 
   componentDidMount() {
-    this.initScene();
-  }
-
-  initScene() {
     // Timeout to allow the scene to render once before attaching the attribute
     // to trigger the animation.
     setTimeout(() => {
       this.setState({ show: true });
-      this.props.onReady();
     }, 10);
   }
 
   removeOverlay() {
     window.removeEventListener("visibilitychange", this.removeOverlay);
     document.body.classList.remove("hide-main", "fxa");
     this.setState({ show: false });
-    this.props.onBlock();
+
     setTimeout(() => {
       // Allow scrolling and fully remove overlay after animation finishes.
+      this.props.onBlock();
       document.body.classList.remove("welcome");
-      this.setState({ overlayRemoved: true });
     }, 400);
   }
 
   onInputChange(e) {
     let error = e.target.previousSibling;
     this.setState({ emailInput: e.target.value });
     error.classList.remove("active");
     e.target.classList.remove("invalid");
@@ -123,35 +68,31 @@ export class _StartupOverlay extends Rea
     );
     this.removeOverlay();
   }
 
   /**
    * Report to telemetry additional information about the form submission.
    */
   _getFormInfo() {
-    const value = { has_flow_params: this.state.flowId.length > 0 };
+    const value = {
+      has_flow_params: this.props.flowParams.flowId.length > 0,
+    };
     return { value };
   }
 
   onInputInvalid(e) {
     let error = e.target.previousSibling;
     error.classList.add("active");
     e.target.classList.add("invalid");
     e.preventDefault(); // Override built-in form validation popup
     e.target.focus();
   }
 
   render() {
-    // When skipping the onboarding tour we show AS but we are still on
-    // about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
-    if (this.state.overlayRemoved) {
-      return null;
-    }
-
     return (
       <div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
         <div className="background" />
         <div className="firstrun-scene">
           <div className="fxaccounts-container">
             <div className="firstrun-left-divider">
               <h1
                 className="firstrun-title"
@@ -204,33 +145,37 @@ export class _StartupOverlay extends Rea
                 <input
                   name="utm_term"
                   type="hidden"
                   value="trailhead-control"
                 />
                 <input
                   name="device_id"
                   type="hidden"
-                  value={this.state.deviceId}
+                  value={this.props.flowParams.deviceId}
                 />
-                <input name="flow_id" type="hidden" value={this.state.flowId} />
+                <input
+                  name="flow_id"
+                  type="hidden"
+                  value={this.props.flowParams.flowId}
+                />
                 <input
                   name="flow_begin_time"
                   type="hidden"
-                  value={this.state.flowBeginTime}
+                  value={this.props.flowParams.flowBeginTime}
                 />
                 <span
                   className="error"
                   data-l10n-id="onboarding-sync-form-invalid-input"
                 />
                 <input
                   className="email-input"
                   name="email"
                   type="email"
-                  required="true"
+                  required={true}
                   onInvalid={this.onInputInvalid}
                   onChange={this.onInputChange}
                   data-l10n-id="onboarding-sync-form-input"
                 />
                 <div className="extra-links">
                   <p data-l10n-id="onboarding-sync-legal-notice">
                     <a
                       data-l10n-name="terms"
@@ -265,10 +210,11 @@ export class _StartupOverlay extends Rea
             </div>
           </div>
         </div>
       </div>
     );
   }
 }
 
-const getState = state => ({ fxa_endpoint: state.Prefs.values.fxa_endpoint });
-export const StartupOverlay = connect(getState)(_StartupOverlay);
+StartupOverlay.defaultProps = {
+  flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
+};
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
@@ -1,135 +1,61 @@
-import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
-import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
-import { OnboardingCard } from "../OnboardingMessage/OnboardingMessage";
-import React from "react";
+/* 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/. */
 
-const FLUENT_FILES = [
-  "branding/brand.ftl",
-  "browser/branding/brandings.ftl",
-  "browser/branding/sync-brand.ftl",
-  "browser/newtab/onboarding.ftl",
-];
+import { actionCreators as ac } from "common/Actions.jsm";
+import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
+import { addUtmParams } from "../FirstRun/addUtmParams";
+import React from "react";
 
 // From resource://devtools/client/shared/focus.js
 const FOCUSABLE_SELECTOR = [
   "a[href]:not([tabindex='-1'])",
   "button:not([disabled]):not([tabindex='-1'])",
   "iframe:not([tabindex='-1'])",
   "input:not([disabled]):not([tabindex='-1'])",
   "select:not([disabled]):not([tabindex='-1'])",
   "textarea:not([disabled]):not([tabindex='-1'])",
   "[tabindex]:not([tabindex='-1'])",
 ].join(", ");
 
 export class Trailhead extends React.PureComponent {
   constructor(props) {
     super(props);
     this.closeModal = this.closeModal.bind(this);
-    this.hideCardPanel = this.hideCardPanel.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.onStartBlur = this.onStartBlur.bind(this);
     this.onSubmit = this.onSubmit.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
-    this.onCardAction = this.onCardAction.bind(this);
 
     this.state = {
       emailInput: "",
-      isModalOpen: true,
-      showCardPanel: true,
-      showCards: false,
-      // The params below are for FxA metrics
-      deviceId: "",
-      flowId: "",
-      flowBeginTime: 0,
     };
-    this.fxaMetricsInitialized = false;
   }
 
   get dialog() {
     return this.props.document.getElementById("trailheadDialog");
   }
 
-  async componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
-
-    await this.componentWillUpdate(this.props);
-  }
-
-  // Get the fxa data if we don't have it yet from mount or update
-  async componentWillUpdate(props) {
-    if (props.fxaEndpoint && !this.fxaMetricsInitialized) {
-      try {
-        this.fxaMetricsInitialized = true;
-        const url = new URL(
-          `${
-            props.fxaEndpoint
-          }/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
-        );
-        this.addUtmParams(url);
-        const response = await fetch(url, { credentials: "omit" });
-        if (response.status === 200) {
-          const { deviceId, flowId, flowBeginTime } = await response.json();
-          this.setState({ deviceId, flowId, flowBeginTime });
-        } else {
-          props.dispatch(
-            ac.OnlyToMain({
-              type: at.TELEMETRY_UNDESIRED_EVENT,
-              data: {
-                event: "FXA_METRICS_FETCH_ERROR",
-                value: response.status,
-              },
-            })
-          );
-        }
-      } catch (error) {
-        props.dispatch(
-          ac.OnlyToMain({
-            type: at.TELEMETRY_UNDESIRED_EVENT,
-            data: { event: "FXA_METRICS_ERROR" },
-          })
-        );
-      }
-    }
-  }
-
   componentDidMount() {
     // We need to remove hide-main since we should show it underneath everything that has rendered
     this.props.document.body.classList.remove("hide-main");
 
-    // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
-    this.props.document.body.classList.add("inline-onboarding");
-
-    // The rest of the page is "hidden" when the modal is open
-    if (this.props.message.content) {
-      this.props.document
-        .getElementById("root")
-        .setAttribute("aria-hidden", "true");
-
-      // Start with focus in the email input box
-      this.dialog.querySelector("input[name=email]").focus();
-    } else {
-      // No modal overlay, let the user scroll and deal them some cards.
-      this.props.document.body.classList.remove("welcome");
-
-      if (this.props.message.includeBundle || this.props.message.cards) {
-        this.revealCards();
-      }
+    // The rest of the page is "hidden" to screen readers when the modal is open
+    this.props.document
+      .getElementById("root")
+      .setAttribute("aria-hidden", "true");
+    // Start with focus in the email input box
+    const input = this.dialog.querySelector("input[name=email]");
+    if (input) {
+      input.focus();
     }
   }
 
-  componentWillUnmount() {
-    this.props.document.body.classList.remove("inline-onboarding");
-  }
-
   onInputChange(e) {
     let error = e.target.previousSibling;
     this.setState({ emailInput: e.target.value });
     error.classList.remove("active");
     e.target.classList.remove("invalid");
   }
 
   onStartBlur(event) {
@@ -163,18 +89,17 @@ export class Trailhead extends React.Pur
 
     global.addEventListener("visibilitychange", this.closeModal);
   }
 
   closeModal(ev) {
     global.removeEventListener("visibilitychange", this.closeModal);
     this.props.document.body.classList.remove("welcome");
     this.props.document.getElementById("root").removeAttribute("aria-hidden");
-    this.setState({ isModalOpen: false });
-    this.revealCards();
+    this.props.onNextScene();
 
     // If closeModal() was triggered by a visibilitychange event, the user actually
     // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
     if (!ev || ev.type !== "visibilitychange") {
       this.props.dispatch(
         ac.UserEvent({ event: "SKIPPED_SIGNIN", ...this._getFormInfo() })
       );
     }
@@ -182,251 +107,158 @@ export class Trailhead extends React.Pur
     // Bug 1190882 - Focus in a disappearing dialog confuses screen readers
     this.props.document.activeElement.blur();
   }
 
   /**
    * Report to telemetry additional information about the form submission.
    */
   _getFormInfo() {
-    const value = { has_flow_params: this.state.flowId.length > 0 };
+    const value = { has_flow_params: this.props.flowParams.flowId.length > 0 };
     return { value };
   }
 
   onInputInvalid(e) {
     let error = e.target.previousSibling;
     error.classList.add("active");
     e.target.classList.add("invalid");
     e.preventDefault(); // Override built-in form validation popup
     e.target.focus();
   }
 
-  hideCardPanel() {
-    this.setState({ showCardPanel: false });
-    this.props.onDismissBundle();
-  }
-
-  revealCards() {
-    this.setState({ showCards: true });
-  }
-
-  /**
-   * Takes in a url as a string or URL object and returns a URL object with the
-   * utm_* parameters added to it. If a URL object is passed in, the paraemeters
-   * are added to it (the return value can be ignored in that case as it's the
-   * same object).
-   */
-  addUtmParams(url, isCard = false) {
-    let returnUrl = url;
-    if (typeof returnUrl === "string") {
-      returnUrl = new URL(url);
-    }
-    returnUrl.searchParams.append("utm_source", "activity-stream");
-    returnUrl.searchParams.append("utm_campaign", "firstrun");
-    returnUrl.searchParams.append("utm_medium", "referral");
-    returnUrl.searchParams.append(
-      "utm_term",
-      `${this.props.message.utm_term}${isCard ? "-card" : ""}`
-    );
-    return returnUrl;
-  }
-
-  onCardAction(action) {
-    let actionUpdates = {};
-
-    if (action.type === "OPEN_URL") {
-      let url = new URL(action.data.args);
-      this.addUtmParams(url, true);
-
-      if (action.addFlowParams) {
-        url.searchParams.append("device_id", this.state.deviceId);
-        url.searchParams.append("flow_id", this.state.flowId);
-        url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
-      }
-
-      actionUpdates = { data: { ...action.data, args: url } };
-    }
-
-    this.props.onAction({ ...action, ...actionUpdates });
-  }
-
   render() {
     const { props } = this;
-    const { bundle: cards, content, utm_term } = props.message;
+    const { UTMTerm } = props;
+    const { content } = props.message;
     const innerClassName = ["trailhead", content && content.className]
       .filter(v => v)
       .join(" ");
 
     return (
-      <>
-        {this.state.isModalOpen && content ? (
-          <ModalOverlayWrapper
-            innerClassName={innerClassName}
-            onClose={this.closeModal}
-            id="trailheadDialog"
-            headerId="trailheadHeader"
+      <ModalOverlayWrapper
+        innerClassName={innerClassName}
+        onClose={this.closeModal}
+        id="trailheadDialog"
+        headerId="trailheadHeader"
+      >
+        <div className="trailheadInner">
+          <div className="trailheadContent">
+            <h1 data-l10n-id={content.title.string_id} id="trailheadHeader" />
+            {content.subtitle && (
+              <p data-l10n-id={content.subtitle.string_id} />
+            )}
+            <ul className="trailheadBenefits">
+              {content.benefits.map(item => (
+                <li key={item.id} className={item.id}>
+                  <h3 data-l10n-id={item.title.string_id} />
+                  <p data-l10n-id={item.text.string_id} />
+                </li>
+              ))}
+            </ul>
+            <a
+              className="trailheadLearn"
+              data-l10n-id={content.learn.text.string_id}
+              href={addUtmParams(content.learn.url, UTMTerm)}
+              target="_blank"
+              rel="noopener noreferrer"
+            />
+          </div>
+          <div
+            role="group"
+            aria-labelledby="joinFormHeader"
+            aria-describedby="joinFormBody"
+            className="trailheadForm"
           >
-            <div className="trailheadInner">
-              <div className="trailheadContent">
-                <h1
-                  data-l10n-id={content.title.string_id}
-                  id="trailheadHeader"
-                />
-                {content.subtitle && (
-                  <p data-l10n-id={content.subtitle.string_id} />
-                )}
-                <ul className="trailheadBenefits">
-                  {content.benefits.map(item => (
-                    <li key={item.id} className={item.id}>
-                      <h3 data-l10n-id={item.title.string_id} />
-                      <p data-l10n-id={item.text.string_id} />
-                    </li>
-                  ))}
-                </ul>
+            <h3
+              id="joinFormHeader"
+              data-l10n-id={content.form.title.string_id}
+            />
+            <p id="joinFormBody" data-l10n-id={content.form.text.string_id} />
+            <form
+              method="get"
+              action={this.props.fxaEndpoint}
+              target="_blank"
+              rel="noopener noreferrer"
+              onSubmit={this.onSubmit}
+            >
+              <input name="service" type="hidden" value="sync" />
+              <input name="action" type="hidden" value="email" />
+              <input name="context" type="hidden" value="fx_desktop_v3" />
+              <input
+                name="entrypoint"
+                type="hidden"
+                value="activity-stream-firstrun"
+              />
+              <input name="utm_source" type="hidden" value="activity-stream" />
+              <input name="utm_campaign" type="hidden" value="firstrun" />
+              <input name="utm_term" type="hidden" value={UTMTerm} />
+              <input
+                name="device_id"
+                type="hidden"
+                value={this.props.flowParams.deviceId}
+              />
+              <input
+                name="flow_id"
+                type="hidden"
+                value={this.props.flowParams.flowId}
+              />
+              <input
+                name="flow_begin_time"
+                type="hidden"
+                value={this.props.flowParams.flowBeginTime}
+              />
+              <input name="style" type="hidden" value="trailhead" />
+              <p
+                data-l10n-id="onboarding-join-form-email-error"
+                className="error"
+              />
+              <input
+                data-l10n-id={content.form.email.string_id}
+                name="email"
+                type="email"
+                onInvalid={this.onInputInvalid}
+                onChange={this.onInputChange}
+              />
+              <p
+                className="trailheadTerms"
+                data-l10n-id="onboarding-join-form-legal"
+              >
                 <a
-                  className="trailheadLearn"
-                  data-l10n-id={content.learn.text.string_id}
-                  href={this.addUtmParams(content.learn.url)}
-                  target="_blank"
-                  rel="noopener noreferrer"
-                />
-              </div>
-              <div
-                role="group"
-                aria-labelledby="joinFormHeader"
-                aria-describedby="joinFormBody"
-                className="trailheadForm"
-              >
-                <h3
-                  id="joinFormHeader"
-                  data-l10n-id={content.form.title.string_id}
-                />
-                <p
-                  id="joinFormBody"
-                  data-l10n-id={content.form.text.string_id}
-                />
-                <form
-                  method="get"
-                  action={this.props.fxaEndpoint}
+                  data-l10n-name="terms"
                   target="_blank"
                   rel="noopener noreferrer"
-                  onSubmit={this.onSubmit}
-                >
-                  <input name="service" type="hidden" value="sync" />
-                  <input name="action" type="hidden" value="email" />
-                  <input name="context" type="hidden" value="fx_desktop_v3" />
-                  <input
-                    name="entrypoint"
-                    type="hidden"
-                    value="activity-stream-firstrun"
-                  />
-                  <input
-                    name="utm_source"
-                    type="hidden"
-                    value="activity-stream"
-                  />
-                  <input name="utm_campaign" type="hidden" value="firstrun" />
-                  <input name="utm_term" type="hidden" value={utm_term} />
-                  <input
-                    name="device_id"
-                    type="hidden"
-                    value={this.state.deviceId}
-                  />
-                  <input
-                    name="flow_id"
-                    type="hidden"
-                    value={this.state.flowId}
-                  />
-                  <input
-                    name="flow_begin_time"
-                    type="hidden"
-                    value={this.state.flowBeginTime}
-                  />
-                  <input name="style" type="hidden" value="trailhead" />
-                  <p
-                    data-l10n-id="onboarding-join-form-email-error"
-                    className="error"
-                  />
-                  <input
-                    data-l10n-id={content.form.email.string_id}
-                    name="email"
-                    type="email"
-                    onInvalid={this.onInputInvalid}
-                    onChange={this.onInputChange}
-                  />
-                  <p
-                    className="trailheadTerms"
-                    data-l10n-id="onboarding-join-form-legal"
-                  >
-                    <a
-                      data-l10n-name="terms"
-                      target="_blank"
-                      rel="noopener noreferrer"
-                      href={this.addUtmParams(
-                        "https://accounts.firefox.com/legal/terms"
-                      )}
-                    />
-                    <a
-                      data-l10n-name="privacy"
-                      target="_blank"
-                      rel="noopener noreferrer"
-                      href={this.addUtmParams(
-                        "https://accounts.firefox.com/legal/privacy"
-                      )}
-                    />
-                  </p>
-                  <button
-                    data-l10n-id={content.form.button.string_id}
-                    type="submit"
-                  />
-                </form>
-              </div>
-            </div>
+                  href={addUtmParams(
+                    "https://accounts.firefox.com/legal/terms",
+                    UTMTerm
+                  )}
+                />
+                <a
+                  data-l10n-name="privacy"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  href={addUtmParams(
+                    "https://accounts.firefox.com/legal/privacy",
+                    UTMTerm
+                  )}
+                />
+              </p>
+              <button
+                data-l10n-id={content.form.button.string_id}
+                type="submit"
+              />
+            </form>
+          </div>
+        </div>
 
-            <button
-              className="trailheadStart"
-              data-l10n-id={content.skipButton.string_id}
-              onBlur={this.onStartBlur}
-              onClick={this.closeModal}
-            />
-          </ModalOverlayWrapper>
-        ) : null}
-        {cards && cards.length ? (
-          <div
-            className={`trailheadCards ${
-              this.state.showCardPanel ? "expanded" : "collapsed"
-            }`}
-          >
-            <div
-              className="trailheadCardsInner"
-              aria-hidden={!this.state.showCards}
-            >
-              <h1 data-l10n-id="onboarding-welcome-header" />
-              <div
-                className={`trailheadCardGrid${
-                  this.state.showCards ? " show" : ""
-                }`}
-              >
-                {cards.map(card => (
-                  <OnboardingCard
-                    key={card.id}
-                    className="trailheadCard"
-                    sendUserActionTelemetry={props.sendUserActionTelemetry}
-                    onAction={this.onCardAction}
-                    UISurface="TRAILHEAD"
-                    {...card}
-                  />
-                ))}
-              </div>
-              {this.state.showCardPanel && (
-                <button
-                  className="icon icon-dismiss"
-                  onClick={this.hideCardPanel}
-                  data-l10n-id="onboarding-cards-dismiss"
-                />
-              )}
-            </div>
-          </div>
-        ) : null}
-      </>
+        <button
+          className="trailheadStart"
+          data-l10n-id={content.skipButton.string_id}
+          onBlur={this.onStartBlur}
+          onClick={this.closeModal}
+        />
+      </ModalOverlayWrapper>
     );
   }
 }
+
+Trailhead.defaultProps = {
+  flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
+};
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
@@ -260,18 +260,23 @@
     animation: fadeIn 0.4s;
   }
 }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
   overflow: hidden;
   text-align: center;
+  // Note: should match TRANSITION_LENGTH in FirstRun.jsx
   transition: max-height 0.5s $photon-easing;
 
+  // This is needed for the transition to work, but will cut off content at the smallest breakpoint
+  @media (min-width: $break-point-medium) {
+    max-height: 1000px;
+  }
 
   &.collapsed {
     max-height: 0;
   }
 
   h1 {
     font-size: 36px;
     font-weight: 200;
@@ -404,16 +409,17 @@
   }
 
   .modalOverlayInner {
     position: absolute;
   }
 
   .outer-wrapper {
     position: relative;
+    display: block;
 
     .prefs-button {
       button {
         position: absolute;
       }
     }
   }
 
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -3478,30 +3478,16 @@ body[lwt-newtab-brighttext] .scene2Icon 
 
 .submissionStatus {
   text-align: center;
   font-size: 14px;
   padding: 20px 0; }
   .submissionStatus .submitStatusTitle {
     font-size: 20px; }
 
-.onboardingMessageContainer {
-  display: grid;
-  grid-column-gap: 21px;
-  grid-template-columns: auto auto auto;
-  padding-left: 30px;
-  padding-right: 30px;
-  min-height: 500px; }
-  @media (max-width: 850px) {
-    .onboardingMessageContainer {
-      grid-template-columns: none;
-      grid-template-rows: auto auto auto;
-      padding-left: 110px;
-      padding-right: 110px; } }
-
 .onboardingMessage {
   height: 340px;
   text-align: center;
   padding: 13px;
   font-weight: 200; }
   @media (max-width: 850px) {
     .onboardingMessage {
       height: 170px;
@@ -4079,16 +4065,19 @@ a.firstrun-link {
   .trailhead .trailheadStart {
     animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
   overflow: hidden;
   text-align: center;
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  @media (min-width: 610px) {
+    .trailheadCards {
+      max-height: 1000px; } }
   .trailheadCards.collapsed {
     max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
@@ -4179,14 +4168,15 @@ a.firstrun-link {
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
   position: absolute; }
 
 .inline-onboarding .outer-wrapper {
-  position: relative; }
+  position: relative;
+  display: block; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -3481,30 +3481,16 @@ body[lwt-newtab-brighttext] .scene2Icon 
 
 .submissionStatus {
   text-align: center;
   font-size: 14px;
   padding: 20px 0; }
   .submissionStatus .submitStatusTitle {
     font-size: 20px; }
 
-.onboardingMessageContainer {
-  display: grid;
-  grid-column-gap: 21px;
-  grid-template-columns: auto auto auto;
-  padding-left: 30px;
-  padding-right: 30px;
-  min-height: 500px; }
-  @media (max-width: 850px) {
-    .onboardingMessageContainer {
-      grid-template-columns: none;
-      grid-template-rows: auto auto auto;
-      padding-left: 110px;
-      padding-right: 110px; } }
-
 .onboardingMessage {
   height: 340px;
   text-align: center;
   padding: 13px;
   font-weight: 200; }
   @media (max-width: 850px) {
     .onboardingMessage {
       height: 170px;
@@ -4082,16 +4068,19 @@ a.firstrun-link {
   .trailhead .trailheadStart {
     animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
   overflow: hidden;
   text-align: center;
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  @media (min-width: 610px) {
+    .trailheadCards {
+      max-height: 1000px; } }
   .trailheadCards.collapsed {
     max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
@@ -4182,14 +4171,15 @@ a.firstrun-link {
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
   position: absolute; }
 
 .inline-onboarding .outer-wrapper {
-  position: relative; }
+  position: relative;
+  display: block; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -3478,30 +3478,16 @@ body[lwt-newtab-brighttext] .scene2Icon 
 
 .submissionStatus {
   text-align: center;
   font-size: 14px;
   padding: 20px 0; }
   .submissionStatus .submitStatusTitle {
     font-size: 20px; }
 
-.onboardingMessageContainer {
-  display: grid;
-  grid-column-gap: 21px;
-  grid-template-columns: auto auto auto;
-  padding-left: 30px;
-  padding-right: 30px;
-  min-height: 500px; }
-  @media (max-width: 850px) {
-    .onboardingMessageContainer {
-      grid-template-columns: none;
-      grid-template-rows: auto auto auto;
-      padding-left: 110px;
-      padding-right: 110px; } }
-
 .onboardingMessage {
   height: 340px;
   text-align: center;
   padding: 13px;
   font-weight: 200; }
   @media (max-width: 850px) {
     .onboardingMessage {
       height: 170px;
@@ -4079,16 +4065,19 @@ a.firstrun-link {
   .trailhead .trailheadStart {
     animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
   overflow: hidden;
   text-align: center;
   transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  @media (min-width: 610px) {
+    .trailheadCards {
+      max-height: 1000px; } }
   .trailheadCards.collapsed {
     max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
@@ -4179,14 +4168,15 @@ a.firstrun-link {
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
   position: absolute; }
 
 .inline-onboarding .outer-wrapper {
-  position: relative; }
+  position: relative;
+  display: block; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
 .inline-onboarding .asrouter-toggle {
   position: absolute; }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -87,25 +87,25 @@
 /******/ ([
 /* 0 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
-/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(48);
+/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(50);
 /* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(24);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);
-/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(14);
+/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(12);
 /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_6__);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(53);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(55);
 
 
 
 
 
 
 
 
@@ -553,25 +553,25 @@ var actionUtils = {
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Base", function() { return _Base; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BaseContent", function() { return BaseContent; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Base", function() { return Base; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
 /* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
-/* harmony import */ var content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(24);
+/* harmony import */ var content_src_components_ConfirmDialog_ConfirmDialog__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(29);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var content_src_components_DiscoveryStreamBase_DiscoveryStreamBase__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(49);
-/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(31);
+/* harmony import */ var content_src_components_DiscoveryStreamBase_DiscoveryStreamBase__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(51);
+/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(33);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_7__);
-/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(47);
-/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(36);
+/* harmony import */ var content_src_components_Search_Search__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(49);
+/* harmony import */ var content_src_components_Sections_Sections__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(38);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -746,22 +746,22 @@ const Base = Object(react_redux__WEBPACK
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ToggleStoryButton", function() { return ToggleStoryButton; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DiscoveryStreamAdmin", function() { return DiscoveryStreamAdmin; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterAdminInner", function() { return ASRouterAdminInner; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CollapseToggle", function() { return CollapseToggle; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterAdmin", function() { return ASRouterAdmin; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(24);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
+/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var _SimpleHashRouter__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(26);
+/* harmony import */ var _SimpleHashRouter__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(28);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -1704,50 +1704,45 @@ const ASRouterAdmin = Object(react_redux
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUtils", function() { return ASRouterUtils; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUISurface", function() { return ASRouterUISurface; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
-/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(52);
+/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(54);
 /* harmony import */ var _components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
-/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(50);
+/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(52);
 /* harmony import */ var content_src_lib_constants__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(11);
-/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(12);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(9);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_7__);
-/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(14);
-/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_8__);
-/* harmony import */ var _templates_ReturnToAMO_ReturnToAMO__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(15);
-/* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(51);
-/* harmony import */ var _templates_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(23);
-/* harmony import */ var _templates_Trailhead_Trailhead__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(25);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
+/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(12);
+/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
+/* harmony import */ var _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(53);
+/* harmony import */ var _templates_FirstRun_FirstRun__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(57);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
 
 
 
 
 
 
-
-
-
 const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
 const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
-const TEMPLATES_ABOVE_PAGE = ["trailhead"];
+const TEMPLATES_ABOVE_PAGE = ["trailhead", "fxa_overlay", "return_to_amo_overlay", "extended_triplets"];
+const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
 const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
 const ASRouterUtils = {
   addListener(listener) {
     if (global.RPMAddMessageListener) {
       global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, listener);
     }
   },
 
@@ -1777,25 +1772,16 @@ const ASRouterUtils = {
     ASRouterUtils.sendMessage({
       type: "DISMISS_MESSAGE_BY_ID",
       data: {
         id
       }
     });
   },
 
-  dismissBundle(bundle) {
-    ASRouterUtils.sendMessage({
-      type: "DISMISS_BUNDLE",
-      data: {
-        bundle
-      }
-    });
-  },
-
   executeAction(button_action) {
     ASRouterUtils.sendMessage({
       type: "USER_ACTION",
       data: button_action
     });
   },
 
   unblockById(id) {
@@ -1854,27 +1840,28 @@ const ASRouterUtils = {
   }
 
 }; // Note: nextProps/prevProps refer to props passed to <ImpressionsWrapper />, not <ASRouterUISurface />
 
 function shouldSendImpressionOnUpdate(nextProps, prevProps) {
   return nextProps.message.id && (!prevProps.message || prevProps.message.id !== nextProps.message.id);
 }
 
-class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_7___default.a.PureComponent {
+class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onMessageFromParent = this.onMessageFromParent.bind(this);
     this.sendClick = this.sendClick.bind(this);
     this.sendImpression = this.sendImpression.bind(this);
     this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
     this.onUserAction = this.onUserAction.bind(this);
+    this.fetchFlowParams = this.fetchFlowParams.bind(this);
     this.state = {
       message: {},
-      bundle: {}
+      interruptCleared: false
     };
 
     if (props.document) {
       this.headerPortal = props.document.getElementById("header-asrouter-container");
       this.footerPortal = props.document.getElementById("footer-asrouter-container");
     }
   }
 
@@ -1918,42 +1905,36 @@ class ASRouterUISurface extends react__W
           type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
           data: {
             event: "FXA_METRICS_FETCH_ERROR",
             value: response.status
           }
         }));
       }
     } catch (error) {
-      console.error(error);
+      console.error(error); // eslint-disable-line no-console
+
       dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
         type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
         data: {
           event: "FXA_METRICS_ERROR"
         }
       }));
     }
 
     return result;
   }
 
   sendUserActionTelemetry(extraProps = {}) {
     const {
-      message,
-      bundle
+      message
     } = this.state;
-
-    if (!message && !extraProps.message_id) {
-      throw new Error(`You must provide a message_id for bundled messages`);
-    } // snippets_user_event, onboarding_user_event
-
-
-    const eventType = `${message.provider || bundle.provider}_user_event`;
+    const eventType = `${message.provider}_user_event`;
     ASRouterUtils.sendTelemetry({
-      message_id: message.id || extraProps.message_id,
+      message_id: message.id,
       source: extraProps.id,
       action: eventType,
       ...extraProps
     });
   }
 
   sendImpression(extraProps) {
     if (this.state.message.provider === "preview") {
@@ -2006,40 +1987,16 @@ class ASRouterUISurface extends react__W
   onBlockById(id) {
     return options => ASRouterUtils.blockById(id, options);
   }
 
   onDismissById(id) {
     return () => ASRouterUtils.dismissById(id);
   }
 
-  dismissBundle(bundle) {
-    return () => {
-      ASRouterUtils.dismissBundle(bundle);
-      this.sendUserActionTelemetry({
-        event: "DISMISS",
-        id: "onboarding-cards",
-        message_id: bundle.map(m => m.id).join(","),
-        // Passing the action because some bundles (Trailhead) don't have a provider set
-        action: "onboarding_user_event"
-      });
-    };
-  }
-
-  triggerOnboarding() {
-    ASRouterUtils.sendMessage({
-      type: "TRIGGER",
-      data: {
-        trigger: {
-          id: "showOnboarding"
-        }
-      }
-    });
-  }
-
   clearMessage(id) {
     if (id === this.state.message.id) {
       this.setState({
         message: {}
       }); // Remove any styles related to the RTAMO message
 
       document.body.classList.remove("welcome", "hide-main", "amo");
     }
@@ -2050,48 +2007,38 @@ class ASRouterUISurface extends react__W
   }) {
     switch (action.type) {
       case "SET_MESSAGE":
         this.setState({
           message: action.data
         });
         break;
 
-      case "SET_BUNDLED_MESSAGES":
+      case "CLEAR_INTERRUPT":
         this.setState({
-          bundle: action.data
+          interruptCleared: true
         });
         break;
 
       case "CLEAR_MESSAGE":
         this.clearMessage(action.data.id);
         break;
 
       case "CLEAR_PROVIDER":
         if (action.data.id === this.state.message.provider) {
           this.setState({
             message: {}
           });
         }
 
         break;
 
-      case "CLEAR_BUNDLE":
-        if (this.state.bundle.bundle) {
-          this.setState({
-            bundle: {}
-          });
-        }
-
-        break;
-
       case "CLEAR_ALL":
         this.setState({
-          message: {},
-          bundle: {}
+          message: {}
         });
         break;
 
       case "AS_ROUTER_TARGETING_UPDATE":
         action.data.forEach(id => this.clearMessage(id));
         break;
     }
   }
@@ -2115,17 +2062,17 @@ class ASRouterUISurface extends react__W
         data: {
           trigger: {
             id: "firstRun"
           }
         }
       });
     } else {
       ASRouterUtils.sendMessage({
-        type: "SNIPPETS_REQUEST",
+        type: "NEWTAB_MESSAGE_REQUEST",
         data: {
           endpoint
         }
       });
     }
   }
 
   componentWillUnmount() {
@@ -2161,136 +2108,106 @@ class ASRouterUISurface extends react__W
         break;
 
       default:
         ASRouterUtils.executeAction(action);
     }
   }
 
   renderSnippets() {
-    if (this.state.bundle.template === "onboarding" || this.state.message.template === "fxa_overlay" || this.state.message.template === "return_to_amo_overlay" || this.state.message.template === "trailhead") {
+    const {
+      message
+    } = this.state;
+
+    if (!_templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__["SnippetsTemplates"][message.template]) {
       return null;
     }
 
-    const SnippetComponent = _templates_template_manifest__WEBPACK_IMPORTED_MODULE_10__["SnippetsTemplates"][this.state.message.template];
+    const SnippetComponent = _templates_template_manifest__WEBPACK_IMPORTED_MODULE_8__["SnippetsTemplates"][message.template];
     const {
       content
     } = this.state.message;
-    return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__["ImpressionsWrapper"], {
+    return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__["ImpressionsWrapper"], {
       id: "NEWTAB_FOOTER_BAR",
       message: this.state.message,
       sendImpression: this.sendImpression,
       shouldSendImpressionOnUpdate: shouldSendImpressionOnUpdate // This helps with testing
       ,
       document: this.props.document
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"], {
+    }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"], {
       bundles: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["generateBundles"])(content)
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(SnippetComponent, _extends({}, this.state.message, {
+    }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(SnippetComponent, _extends({}, this.state.message, {
       UISurface: "NEWTAB_FOOTER_BAR",
       onBlock: this.onBlockById(this.state.message.id),
       onDismiss: this.onDismissById(this.state.message.id),
       onAction: this.onUserAction,
       sendClick: this.sendClick,
       sendUserActionTelemetry: this.sendUserActionTelemetry
     }))));
   }
 
-  renderOnboarding() {
-    if (this.state.bundle.template === "onboarding") {
-      return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_6__["OnboardingMessage"], _extends({}, this.state.bundle, {
-        UISurface: "NEWTAB_OVERLAY",
-        onAction: ASRouterUtils.executeAction,
-        onDismissBundle: this.dismissBundle(this.state.bundle.bundle),
-        sendUserActionTelemetry: this.sendUserActionTelemetry
+  renderPreviewBanner() {
+    if (this.state.message.provider !== "preview") {
+      return null;
+    }
+
+    return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("div", {
+      className: "snippets-preview-banner"
+    }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("span", {
+      className: "icon icon-small-spacer icon-info"
+    }), react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("span", null, "Preview Purposes Only"));
+  }
+
+  renderFirstRun() {
+    const {
+      message
+    } = this.state;
+
+    if (FIRST_RUN_TEMPLATES.includes(message.template)) {
+      return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__["ImpressionsWrapper"], {
+        id: "FIRST_RUN",
+        message: this.state.message,
+        sendImpression: this.sendImpression,
+        shouldSendImpressionOnUpdate: shouldSendImpressionOnUpdate // This helps with testing
+        ,
+        document: this.props.document
+      }, react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(_templates_FirstRun_FirstRun__WEBPACK_IMPORTED_MODULE_9__["FirstRun"], {
+        document: this.props.document,
+        interruptCleared: this.state.interruptCleared,
+        message: message,
+        sendUserActionTelemetry: this.sendUserActionTelemetry,
+        executeAction: ASRouterUtils.executeAction,
+        dispatch: this.props.dispatch,
+        onBlockById: ASRouterUtils.blockById,
+        onDismiss: this.onDismissById(this.state.message.id),
+        fxaEndpoint: this.props.fxaEndpoint,
+        fetchFlowParams: this.fetchFlowParams
       }));
     }
 
     return null;
   }
 
-  renderFirstRunOverlay() {
-    const {
-      message
-    } = this.state;
-
-    if (message.template === "fxa_overlay") {
-      global.document.body.classList.add("fxa");
-      return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_templates_StartupOverlay_StartupOverlay__WEBPACK_IMPORTED_MODULE_11__["StartupOverlay"], {
-        onReady: this.triggerOnboarding,
-        onBlock: this.onDismissById(message.id),
-        dispatch: this.props.dispatch
-      });
-    } else if (message.template === "return_to_amo_overlay") {
-      global.document.body.classList.add("amo");
-      return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_4__["LocalizationProvider"], {
-        bundles: Object(_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["generateBundles"])({
-          amo_html: message.content.text
-        })
-      }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_templates_ReturnToAMO_ReturnToAMO__WEBPACK_IMPORTED_MODULE_9__["ReturnToAMO"], _extends({}, message, {
-        UISurface: "NEWTAB_OVERLAY",
-        onReady: this.triggerOnboarding,
-        onBlock: this.onDismissById(message.id),
-        onAction: ASRouterUtils.executeAction,
-        sendUserActionTelemetry: this.sendUserActionTelemetry
-      })));
-    }
-
-    return null;
-  }
-
-  renderTrailhead() {
+  render() {
     const {
       message
     } = this.state;
 
-    if (message.template === "trailhead") {
-      return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(_templates_Trailhead_Trailhead__WEBPACK_IMPORTED_MODULE_12__["Trailhead"], {
-        document: this.props.document,
-        message: message,
-        onAction: ASRouterUtils.executeAction,
-        onDismissBundle: this.dismissBundle(this.state.message.bundle),
-        sendUserActionTelemetry: this.sendUserActionTelemetry,
-        dispatch: this.props.dispatch,
-        fxaEndpoint: this.props.fxaEndpoint
-      });
-    }
-
-    return null;
-  }
-
-  renderPreviewBanner() {
-    if (this.state.message.provider !== "preview") {
-      return null;
-    }
-
-    return react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
-      className: "snippets-preview-banner"
-    }, react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("span", {
-      className: "icon icon-small-spacer icon-info"
-    }), react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("span", null, "Preview Purposes Only"));
-  }
-
-  render() {
-    const {
-      message,
-      bundle
-    } = this.state;
-
-    if (!message.id && !bundle.template) {
+    if (!message.id) {
       return null;
     }
 
     const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(message.template);
     const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(message.template);
     return shouldRenderBelowSearch ? // Render special below search snippets in place;
-    react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement("div", {
+    react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement("div", {
       className: "below-search-snippet-wrapper"
     }, this.renderSnippets()) : // For onboarding, regular snippets etc. we should render
     // everything in our footer container.
-    react_dom__WEBPACK_IMPORTED_MODULE_8___default.a.createPortal(react__WEBPACK_IMPORTED_MODULE_7___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_7___default.a.Fragment, null, this.renderPreviewBanner(), this.renderTrailhead(), this.renderFirstRunOverlay(), this.renderOnboarding(), this.renderSnippets()), shouldRenderInHeader ? this.headerPortal : this.footerPortal);
+    react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.createPortal(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_6___default.a.Fragment, null, this.renderPreviewBanner(), this.renderFirstRun(), this.renderSnippets()), shouldRenderInHeader ? this.headerPortal : this.footerPortal);
   }
 
 }
 ASRouterUISurface.defaultProps = {
   document: global.document
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
@@ -2583,110 +2500,425 @@ const NEWTAB_DARK_THEME = {
     b: 250,
     a: 1
   }
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 12 */
+/***/ (function(module, exports) {
+
+module.exports = ReactDOM;
+
+/***/ }),
+/* 13 */
+/***/ (function(module) {
+
+module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text for accessibility","default":""},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
+
+/***/ }),
+/* 14 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertLinks", function() { return convertLinks; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RichText", function() { return RichText; });
+/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(52);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
+/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(54);
+/* harmony import */ var _template_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(15);
+function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
+
+
+
+
+ // Elements allowed in snippet content
+
+const ALLOWED_TAGS = {
+  b: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("b", null),
+  i: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("i", null),
+  u: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("u", null),
+  strong: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("strong", null),
+  em: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("em", null),
+  br: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null)
+};
+/**
+ * Transform an object (tag name: {url}) into (tag name: anchor) where the url
+ * is used as href, in order to render links inside a Fluent.Localized component.
+ */
+
+function convertLinks(links, sendClick, doNotAutoBlock, openNewWindow = false) {
+  if (links) {
+    return Object.keys(links).reduce((acc, linkTag) => {
+      const {
+        action
+      } = links[linkTag]; // Setting the value to false will not include the attribute in the anchor
+
+      const url = action ? false : Object(_template_utils__WEBPACK_IMPORTED_MODULE_3__["safeURI"])(links[linkTag].url);
+      acc[linkTag] = // eslint was getting a false positive caused by the dynamic injection
+      // of content.
+      // eslint-disable-next-line jsx-a11y/anchor-has-content
+      react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
+        href: url,
+        target: openNewWindow ? "_blank" : "",
+        "data-metric": links[linkTag].metric,
+        "data-action": action,
+        "data-args": links[linkTag].args,
+        "data-do_not_autoblock": doNotAutoBlock,
+        onClick: sendClick
+      });
+      return acc;
+    }, {});
+  }
+
+  return null;
+}
+/**
+ * Message wrapper used to sanitize markup and render HTML.
+ */
+
+function RichText(props) {
+  if (!_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["RICH_TEXT_KEYS"].includes(props.localization_id)) {
+    throw new Error(`ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`);
+  }
+
+  return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_0__["Localized"], _extends({
+    id: props.localization_id
+  }, ALLOWED_TAGS, props.customElements, convertLinks(props.links, props.sendClick, props.doNotAutoBlock, props.openNewWindow)), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", null, props.text));
+}
+
+/***/ }),
+/* 15 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingCard", function() { return OnboardingCard; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingMessage", function() { return OnboardingMessage; });
-/* harmony import */ var _components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(13);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
-
-
-
-const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"];
-class OnboardingCard extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "safeURI", function() { return safeURI; });
+function safeURI(url) {
+  if (!url) {
+    return "";
+  }
+
+  const {
+    protocol
+  } = new URL(url);
+  const isAllowed = ["http:", "https:", "data:", "resource:", "chrome:"].includes(protocol);
+
+  if (!isAllowed) {
+    console.warn(`The protocol ${protocol} is not allowed for template URLs.`); // eslint-disable-line no-console
+  }
+
+  return isAllowed ? url : "";
+}
+
+/***/ }),
+/* 16 */
+/***/ (function(module) {
+
+module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text describing icon for screen readers","default":""},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_alt_text":{"type":"string","description":"Alt text describing title icon for screen readers","default":""},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
+
+/***/ }),
+/* 17 */
+/***/ (function(module) {
+
+module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+
+/***/ }),
+/* 18 */
+/***/ (function(module) {
+
+module.exports = {"title":"NewsletterSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_privacy_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"fmt":{"type":"string","description":"","default":"H"}}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"scene2_newsletter":{"type":"string","description":"Newsletter/basket id user is subscribing to.","default":"mozilla-foundation"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+
+/***/ }),
+/* 19 */
+/***/ (function(module) {
+
+module.exports = {"title":"SendToDeviceSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"country":{"type":"string","description":"Two character string for the country code (used for SMS)","default":"us"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene2_icon":{"type":"string","description":"(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."},"scene2_icon_dark_theme":{"type":"string","description":"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Send"},"scene2_input_placeholder":{"type":"string","description":"(send to device) Value to show while input is empty.","default":"Your email here"},"scene2_disclaimer_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"string","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_title":{"type":"string","description":"(send to device) Title shown before text on successful registration."},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"include_sms":{"type":"boolean","description":"(send to device) Allow users to send an SMS message with the form?","default":false},"message_id_sms":{"type":"string","description":"(send to device) Newsletter/basket id representing the SMS message to be sent."},"message_id_email":{"type":"string","description":"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+
+/***/ }),
+/* 20 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Trailhead", function() { return Trailhead; });
+/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21);
+/* harmony import */ var _FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(22);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
+/* 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/. */
+
+
+
+ // From resource://devtools/client/shared/focus.js
+
+const FOCUSABLE_SELECTOR = ["a[href]:not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])", "iframe:not([tabindex='-1'])", "input:not([disabled]):not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])", "[tabindex]:not([tabindex='-1'])"].join(", ");
+class Trailhead extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
   constructor(props) {
     super(props);
-    this.onClick = this.onClick.bind(this);
-  }
-
-  onClick() {
-    const {
-      props
-    } = this;
-    const ping = {
-      event: "CLICK_BUTTON",
-      message_id: props.id,
-      id: props.UISurface
+    this.closeModal = this.closeModal.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onStartBlur = this.onStartBlur.bind(this);
+    this.onSubmit = this.onSubmit.bind(this);
+    this.onInputInvalid = this.onInputInvalid.bind(this);
+    this.state = {
+      emailInput: ""
     };
-    props.sendUserActionTelemetry(ping);
-    props.onAction(props.content.primary_button.action);
-  }
-
-  render() {
-    const {
-      content
-    } = this.props;
-    const className = this.props.className || "onboardingMessage";
-    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
-      className: className
-    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
-      className: `onboardingMessageImage ${content.icon}`
-    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
-      className: "onboardingContent"
-    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h3", {
-      className: "onboardingTitle",
-      "data-l10n-id": content.title.string_id
-    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("p", {
-      className: "onboardingText",
-      "data-l10n-id": content.text.string_id
-    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
-      className: "onboardingButtonContainer"
-    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("button", {
-      "data-l10n-id": content.primary_button.label.string_id,
-      className: "button onboardingButton",
-      onClick: this.onClick
-    }))));
-  }
-
-}
-class OnboardingMessage extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
-  componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
+  }
+
+  get dialog() {
+    return this.props.document.getElementById("trailheadDialog");
+  }
+
+  componentDidMount() {
+    // We need to remove hide-main since we should show it underneath everything that has rendered
+    this.props.document.body.classList.remove("hide-main"); // The rest of the page is "hidden" to screen readers when the modal is open
+
+    this.props.document.getElementById("root").setAttribute("aria-hidden", "true"); // Start with focus in the email input box
+
+    const input = this.dialog.querySelector("input[name=email]");
+
+    if (input) {
+      input.focus();
+    }
+  }
+
+  onInputChange(e) {
+    let error = e.target.previousSibling;
+    this.setState({
+      emailInput: e.target.value
+    });
+    error.classList.remove("active");
+    e.target.classList.remove("invalid");
+  }
+
+  onStartBlur(event) {
+    // Make sure focus stays within the dialog when tabbing from the button
+    const {
+      dialog
+    } = this;
+
+    if (event.relatedTarget && !(dialog.compareDocumentPosition(event.relatedTarget) & dialog.DOCUMENT_POSITION_CONTAINED_BY)) {
+      dialog.querySelector(FOCUSABLE_SELECTOR).focus();
+    }
+  }
+
+  onSubmit(event) {
+    // Dynamically require the email on submission so screen readers don't read
+    // out it's always required because there's also ways to skip the modal
+    const {
+      email
+    } = event.target.elements;
+
+    if (!email.value.length) {
+      email.required = true;
+      email.checkValidity();
+      event.preventDefault();
+      return;
+    }
+
+    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
+      event: "SUBMIT_EMAIL",
+      ...this._getFormInfo()
+    }));
+    global.addEventListener("visibilitychange", this.closeModal);
+  }
+
+  closeModal(ev) {
+    global.removeEventListener("visibilitychange", this.closeModal);
+    this.props.document.body.classList.remove("welcome");
+    this.props.document.getElementById("root").removeAttribute("aria-hidden");
+    this.props.onNextScene(); // If closeModal() was triggered by a visibilitychange event, the user actually
+    // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
+
+    if (!ev || ev.type !== "visibilitychange") {
+      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
+        event: "SKIPPED_SIGNIN",
+        ...this._getFormInfo()
+      }));
+    } // Bug 1190882 - Focus in a disappearing dialog confuses screen readers
+
+
+    this.props.document.activeElement.blur();
+  }
+  /**
+   * Report to telemetry additional information about the form submission.
+   */
+
+
+  _getFormInfo() {
+    const value = {
+      has_flow_params: this.props.flowParams.flowId.length > 0
+    };
+    return {
+      value
+    };
+  }
+
+  onInputInvalid(e) {
+    let error = e.target.previousSibling;
+    error.classList.add("active");
+    e.target.classList.add("invalid");
+    e.preventDefault(); // Override built-in form validation popup
+
+    e.target.focus();
   }
 
   render() {
     const {
       props
     } = this;
     const {
-      button_label,
-      header
-    } = props.extraTemplateStrings;
-    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_0__["ModalOverlay"], _extends({}, props, {
-      button_label: button_label,
-      title: header
-    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
-      className: "onboardingMessageContainer"
-    }, props.bundle.map(message => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(OnboardingCard, _extends({
-      key: message.id,
-      sendUserActionTelemetry: props.sendUserActionTelemetry,
-      onAction: props.onAction,
-      UISurface: props.UISurface
-    }, message)))));
-  }
-
-}
+      UTMTerm
+    } = props;
+    const {
+      content
+    } = props.message;
+    const innerClassName = ["trailhead", content && content.className].filter(v => v).join(" ");
+    return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_1__["ModalOverlayWrapper"], {
+      innerClassName: innerClassName,
+      onClose: this.closeModal,
+      id: "trailheadDialog",
+      headerId: "trailheadHeader"
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+      className: "trailheadInner"
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+      className: "trailheadContent"
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h1", {
+      "data-l10n-id": content.title.string_id,
+      id: "trailheadHeader"
+    }), content.subtitle && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+      "data-l10n-id": content.subtitle.string_id
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
+      className: "trailheadBenefits"
+    }, content.benefits.map(item => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("li", {
+      key: item.id,
+      className: item.id
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
+      "data-l10n-id": item.title.string_id
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+      "data-l10n-id": item.text.string_id
+    })))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+      className: "trailheadLearn",
+      "data-l10n-id": content.learn.text.string_id,
+      href: Object(_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_2__["addUtmParams"])(content.learn.url, UTMTerm),
+      target: "_blank",
+      rel: "noopener noreferrer"
+    })), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+      role: "group",
+      "aria-labelledby": "joinFormHeader",
+      "aria-describedby": "joinFormBody",
+      className: "trailheadForm"
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
+      id: "joinFormHeader",
+      "data-l10n-id": content.form.title.string_id
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+      id: "joinFormBody",
+      "data-l10n-id": content.form.text.string_id
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("form", {
+      method: "get",
+      action: this.props.fxaEndpoint,
+      target: "_blank",
+      rel: "noopener noreferrer",
+      onSubmit: this.onSubmit
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "service",
+      type: "hidden",
+      value: "sync"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "action",
+      type: "hidden",
+      value: "email"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "context",
+      type: "hidden",
+      value: "fx_desktop_v3"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "entrypoint",
+      type: "hidden",
+      value: "activity-stream-firstrun"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "utm_source",
+      type: "hidden",
+      value: "activity-stream"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "utm_campaign",
+      type: "hidden",
+      value: "firstrun"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "utm_term",
+      type: "hidden",
+      value: UTMTerm
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "device_id",
+      type: "hidden",
+      value: this.props.flowParams.deviceId
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "flow_id",
+      type: "hidden",
+      value: this.props.flowParams.flowId
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "flow_begin_time",
+      type: "hidden",
+      value: this.props.flowParams.flowBeginTime
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      name: "style",
+      type: "hidden",
+      value: "trailhead"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+      "data-l10n-id": "onboarding-join-form-email-error",
+      className: "error"
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+      "data-l10n-id": content.form.email.string_id,
+      name: "email",
+      type: "email",
+      onInvalid: this.onInputInvalid,
+      onChange: this.onInputChange
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+      className: "trailheadTerms",
+      "data-l10n-id": "onboarding-join-form-legal"
+    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+      "data-l10n-name": "terms",
+      target: "_blank",
+      rel: "noopener noreferrer",
+      href: Object(_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_2__["addUtmParams"])("https://accounts.firefox.com/legal/terms", UTMTerm)
+    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+      "data-l10n-name": "privacy",
+      target: "_blank",
+      rel: "noopener noreferrer",
+      href: Object(_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_2__["addUtmParams"])("https://accounts.firefox.com/legal/privacy", UTMTerm)
+    })), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
+      "data-l10n-id": content.form.button.string_id,
+      type: "submit"
+    })))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
+      className: "trailheadStart",
+      "data-l10n-id": content.skipButton.string_id,
+      onBlur: this.onStartBlur,
+      onClick: this.closeModal
+    }));
+  }
+
+}
+Trailhead.defaultProps = {
+  flowParams: {
+    deviceId: "",
+    flowId: "",
+    flowBeginTime: ""
+  }
+};
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 13 */
+/* 21 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ModalOverlayWrapper", function() { return ModalOverlayWrapper; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ModalOverlay", function() { return ModalOverlay; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
@@ -2755,44 +2987,78 @@ class ModalOverlay extends react__WEBPAC
       onClick: this.props.onDismissBundle
     }, " ", button_label, " ")));
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 14 */
-/***/ (function(module, exports) {
-
-module.exports = ReactDOM;
-
-/***/ }),
-/* 15 */
+/* 22 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReturnToAMO", function() { return ReturnToAMO; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BASE_PARAMS", function() { return BASE_PARAMS; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addUtmParams", function() { return addUtmParams; });
+/* 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/. */
+const BASE_PARAMS = {
+  utm_source: "activity-stream",
+  utm_campaign: "firstrun",
+  utm_medium: "referral"
+};
+/**
+ * Takes in a url as a string or URL object and returns a URL object with the
+ * utm_* parameters added to it. If a URL object is passed in, the paraemeters
+ * are added to it (the return value can be ignored in that case as it's the
+ * same object).
+ */
+
+function addUtmParams(url, utmTerm) {
+  let returnUrl = url;
+
+  if (typeof returnUrl === "string") {
+    returnUrl = new URL(url);
+  }
+
+  Object.keys(BASE_PARAMS).forEach(key => {
+    returnUrl.searchParams.append(key, BASE_PARAMS[key]);
+  });
+  returnUrl.searchParams.append("utm_term", utmTerm);
+  return returnUrl;
+}
+
+/***/ }),
+/* 23 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReturnToAMO", function() { return ReturnToAMO; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
-/* harmony import */ var _components_RichText_RichText__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16);
+/* harmony import */ var _components_RichText_RichText__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
 
  // Alt text if available; in the future this should come from the server. See bug 1551711
 
 const ICON_ALT_TEXT = "";
 class ReturnToAMO extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onClickAddExtension = this.onClickAddExtension.bind(this);
     this.onBlockButton = this.onBlockButton.bind(this);
   }
 
+  componentWillMount() {
+    global.document.body.classList.add("amo");
+  }
+
   componentDidMount() {
-    this.props.onReady();
     this.props.sendUserActionTelemetry({
       event: "IMPRESSION",
       id: this.props.UISurface
     });
   }
 
   onClickAddExtension() {
     this.props.onAction(this.props.content.primary_button.action);
@@ -2848,258 +3114,72 @@ class ReturnToAMO extends react__WEBPACK
       className: "ReturnToAMOIcon"
     })), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
       onClick: this.onBlockButton,
       className: "default grey ReturnToAMOGetStarted"
     }, " ", content.secondary_button.label, " ")));
   }
 
 }
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 16 */
+/* 24 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertLinks", function() { return convertLinks; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RichText", function() { return RichText; });
-/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50);
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "StartupOverlay", function() { return StartupOverlay; });
+/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var _rich_text_strings__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(52);
-/* harmony import */ var _template_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(17);
-function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
-
-
-
-
- // Elements allowed in snippet content
-
-const ALLOWED_TAGS = {
-  b: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("b", null),
-  i: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("i", null),
-  u: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("u", null),
-  strong: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("strong", null),
-  em: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("em", null),
-  br: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null)
-};
-/**
- * Transform an object (tag name: {url}) into (tag name: anchor) where the url
- * is used as href, in order to render links inside a Fluent.Localized component.
- */
-
-function convertLinks(links, sendClick, doNotAutoBlock, openNewWindow = false) {
-  if (links) {
-    return Object.keys(links).reduce((acc, linkTag) => {
-      const {
-        action
-      } = links[linkTag]; // Setting the value to false will not include the attribute in the anchor
-
-      const url = action ? false : Object(_template_utils__WEBPACK_IMPORTED_MODULE_3__["safeURI"])(links[linkTag].url);
-      acc[linkTag] = // eslint was getting a false positive caused by the dynamic injection
-      // of content.
-      // eslint-disable-next-line jsx-a11y/anchor-has-content
-      react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
-        href: url,
-        target: openNewWindow ? "_blank" : "",
-        "data-metric": links[linkTag].metric,
-        "data-action": action,
-        "data-args": links[linkTag].args,
-        "data-do_not_autoblock": doNotAutoBlock,
-        onClick: sendClick
-      });
-      return acc;
-    }, {});
-  }
-
-  return null;
-}
-/**
- * Message wrapper used to sanitize markup and render HTML.
- */
-
-function RichText(props) {
-  if (!_rich_text_strings__WEBPACK_IMPORTED_MODULE_2__["RICH_TEXT_KEYS"].includes(props.localization_id)) {
-    throw new Error(`ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`);
-  }
-
-  return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(fluent_react__WEBPACK_IMPORTED_MODULE_0__["Localized"], _extends({
-    id: props.localization_id
-  }, ALLOWED_TAGS, props.customElements, convertLinks(props.links, props.sendClick, props.doNotAutoBlock, props.openNewWindow)), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", null, props.text));
-}
-
-/***/ }),
-/* 17 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "safeURI", function() { return safeURI; });
-function safeURI(url) {
-  if (!url) {
-    return "";
-  }
-
-  const {
-    protocol
-  } = new URL(url);
-  const isAllowed = ["http:", "https:", "data:", "resource:", "chrome:"].includes(protocol);
-
-  if (!isAllowed) {
-    console.warn(`The protocol ${protocol} is not allowed for template URLs.`); // eslint-disable-line no-console
-  }
-
-  return isAllowed ? url : "";
-}
-
-/***/ }),
-/* 18 */
-/***/ (function(module) {
-
-module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text for accessibility","default":""},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
-
-/***/ }),
-/* 19 */
-/***/ (function(module) {
-
-module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"icon_alt_text":{"type":"string","description":"Alt text describing icon for screen readers","default":""},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_alt_text":{"type":"string","description":"Alt text describing title icon for screen readers","default":""},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
-
-/***/ }),
-/* 20 */
-/***/ (function(module) {
-
-module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
-
-/***/ }),
-/* 21 */
-/***/ (function(module) {
-
-module.exports = {"title":"NewsletterSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_privacy_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"fmt":{"type":"string","description":"","default":"H"}}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"scene2_newsletter":{"type":"string","description":"Newsletter/basket id user is subscribing to.","default":"mozilla-foundation"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
-
-/***/ }),
-/* 22 */
-/***/ (function(module) {
-
-module.exports = {"title":"SendToDeviceSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"country":{"type":"string","description":"Two character string for the country code (used for SMS)","default":"us"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene2_icon":{"type":"string","description":"(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."},"scene2_icon_dark_theme":{"type":"string","description":"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Send"},"scene2_input_placeholder":{"type":"string","description":"(send to device) Value to show while input is empty.","default":"Your email here"},"scene2_disclaimer_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"string","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_title":{"type":"string","description":"(send to device) Title shown before text on successful registration."},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"include_sms":{"type":"boolean","description":"(send to device) Allow users to send an SMS message with the form?","default":false},"message_id_sms":{"type":"string","description":"(send to device) Newsletter/basket id representing the SMS message to be sent."},"message_id_email":{"type":"string","description":"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
-
-/***/ }),
-/* 23 */
-/***/ (function(module, __webpack_exports__, __webpack_require__) {
-
-"use strict";
-__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_StartupOverlay", function() { return _StartupOverlay; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "StartupOverlay", function() { return StartupOverlay; });
-/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(24);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
-
-
-
-const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"];
-class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
+/* 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 StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onInputChange = this.onInputChange.bind(this);
     this.onSubmit = this.onSubmit.bind(this);
     this.clickSkip = this.clickSkip.bind(this);
-    this.initScene = this.initScene.bind(this);
     this.removeOverlay = this.removeOverlay.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
     this.utmParams = "utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
     this.state = {
-      emailInput: "",
-      overlayRemoved: false,
-      deviceId: "",
-      flowId: "",
-      flowBeginTime: 0
+      show: false,
+      emailInput: ""
     };
-    this.didFetch = false;
-  }
-
-  async componentWillUpdate() {
-    if (this.props.fxa_endpoint && !this.didFetch) {
-      try {
-        this.didFetch = true;
-        const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
-        const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${this.utmParams}`, {
-          credentials: "omit"
-        });
-
-        if (response.status === 200) {
-          const {
-            deviceId,
-            flowId,
-            flowBeginTime
-          } = await response.json();
-          this.setState({
-            deviceId,
-            flowId,
-            flowBeginTime
-          });
-        } else {
-          this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
-            type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-            data: {
-              event: "FXA_METRICS_FETCH_ERROR",
-              value: response.status
-            }
-          }));
-        }
-      } catch (error) {
-        this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
-          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-          data: {
-            event: "FXA_METRICS_ERROR"
-          }
-        }));
-      }
-    }
-  }
-
-  async componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
-    await this.componentWillUpdate(this.props);
+  }
+
+  componentWillMount() {
+    global.document.body.classList.add("fxa");
   }
 
   componentDidMount() {
-    this.initScene();
-  }
-
-  initScene() {
     // Timeout to allow the scene to render once before attaching the attribute
     // to trigger the animation.
     setTimeout(() => {
       this.setState({
         show: true
       });
-      this.props.onReady();
     }, 10);
   }
 
   removeOverlay() {
     window.removeEventListener("visibilitychange", this.removeOverlay);
     document.body.classList.remove("hide-main", "fxa");
     this.setState({
       show: false
     });
-    this.props.onBlock();
     setTimeout(() => {
       // Allow scrolling and fully remove overlay after animation finishes.
+      this.props.onBlock();
       document.body.classList.remove("welcome");
-      this.setState({
-        overlayRemoved: true
-      });
     }, 400);
   }
 
   onInputChange(e) {
     let error = e.target.previousSibling;
     this.setState({
       emailInput: e.target.value
     });
@@ -3124,598 +3204,343 @@ class _StartupOverlay extends react__WEB
   }
   /**
    * Report to telemetry additional information about the form submission.
    */
 
 
   _getFormInfo() {
     const value = {
-      has_flow_params: this.state.flowId.length > 0
+      has_flow_params: this.props.flowParams.flowId.length > 0
     };
     return {
       value
     };
   }
 
   onInputInvalid(e) {
     let error = e.target.previousSibling;
     error.classList.add("active");
     e.target.classList.add("invalid");
     e.preventDefault(); // Override built-in form validation popup
 
     e.target.focus();
   }
 
   render() {
-    // When skipping the onboarding tour we show AS but we are still on
-    // about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
-    if (this.state.overlayRemoved) {
-      return null;
-    }
-
-    return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: `overlay-wrapper ${this.state.show ? "show" : ""}`
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "background"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "firstrun-scene"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "fxaccounts-container"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "firstrun-left-divider"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("h1", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h1", {
       className: "firstrun-title",
       "data-l10n-id": "onboarding-sync-welcome-header"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("p", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("p", {
       className: "firstrun-content",
       "data-l10n-id": "onboarding-sync-welcome-content"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
       className: "firstrun-link",
       href: `https://www.mozilla.org/firefox/features/sync/?${this.utmParams}`,
       target: "_blank",
       rel: "noopener noreferrer",
       "data-l10n-id": "onboarding-sync-welcome-learn-more-link"
-    })), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "firstrun-sign-in"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("p", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("p", {
       className: "form-header"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
       "data-l10n-id": "onboarding-sync-form-header"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
       className: "sub-header",
       "data-l10n-id": "onboarding-sync-form-sub-header"
-    })), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("form", {
+    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("form", {
       method: "get",
       action: this.props.fxa_endpoint,
       target: "_blank",
       rel: "noopener noreferrer",
       onSubmit: this.onSubmit
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "service",
       type: "hidden",
       value: "sync"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "action",
       type: "hidden",
       value: "email"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "context",
       type: "hidden",
       value: "fx_desktop_v3"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "entrypoint",
       type: "hidden",
       value: "activity-stream-firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "utm_source",
       type: "hidden",
       value: "activity-stream"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "utm_campaign",
       type: "hidden",
       value: "firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "utm_medium",
       type: "hidden",
       value: "referral"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "utm_term",
       type: "hidden",
       value: "trailhead-control"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "device_id",
       type: "hidden",
-      value: this.state.deviceId
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+      value: this.props.flowParams.deviceId
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "flow_id",
       type: "hidden",
-      value: this.state.flowId
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+      value: this.props.flowParams.flowId
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       name: "flow_begin_time",
       type: "hidden",
-      value: this.state.flowBeginTime
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("span", {
+      value: this.props.flowParams.flowBeginTime
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", {
       className: "error",
       "data-l10n-id": "onboarding-sync-form-invalid-input"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("input", {
       className: "email-input",
       name: "email",
       type: "email",
-      required: "true",
+      required: true,
       onInvalid: this.onInputInvalid,
       onChange: this.onInputChange,
       "data-l10n-id": "onboarding-sync-form-input"
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("div", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", {
       className: "extra-links"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("p", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("p", {
       "data-l10n-id": "onboarding-sync-legal-notice"
-    }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
+    }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
       "data-l10n-name": "terms",
       target: "_blank",
       rel: "noopener noreferrer",
       href: `${this.props.fxa_endpoint}/legal/terms?${this.utmParams}`
-    }), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("a", {
+    }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
       "data-l10n-name": "privacy",
       target: "_blank",
       rel: "noopener noreferrer",
       href: `${this.props.fxa_endpoint}/legal/privacy?${this.utmParams}`
-    }))), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("button", {
+    }))), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("button", {
       className: "continue-button",
       type: "submit",
       "data-l10n-id": "onboarding-sync-form-continue-button"
-    })), react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement("button", {
+    })), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("button", {
       className: "skip-button",
       disabled: !!this.state.emailInput,
       onClick: this.clickSkip,
       "data-l10n-id": "onboarding-sync-form-skip-login-button"
     })))));
   }
 
 }
-
-const getState = state => ({
-  fxa_endpoint: state.Prefs.values.fxa_endpoint
-});
-
-const StartupOverlay = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])(getState)(_StartupOverlay);
-
-/***/ }),
-/* 24 */
-/***/ (function(module, exports) {
-
-module.exports = ReactRedux;
+StartupOverlay.defaultProps = {
+  flowParams: {
+    deviceId: "",
+    flowId: "",
+    flowBeginTime: ""
+  }
+};
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 25 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Trailhead", function() { return Trailhead; });
-/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
-/* harmony import */ var _OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(12);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Triplets", function() { return Triplets; });
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(26);
+/* harmony import */ var _addUtmParams__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(22);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
-
-
-
-
-const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/brandings.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"]; // From resource://devtools/client/shared/focus.js
-
-const FOCUSABLE_SELECTOR = ["a[href]:not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])", "iframe:not([tabindex='-1'])", "input:not([disabled]):not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])", "[tabindex]:not([tabindex='-1'])"].join(", ");
-class Trailhead extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
+/* 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 Triplets extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
-    this.closeModal = this.closeModal.bind(this);
-    this.hideCardPanel = this.hideCardPanel.bind(this);
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onStartBlur = this.onStartBlur.bind(this);
-    this.onSubmit = this.onSubmit.bind(this);
-    this.onInputInvalid = this.onInputInvalid.bind(this);
     this.onCardAction = this.onCardAction.bind(this);
-    this.state = {
-      emailInput: "",
-      isModalOpen: true,
-      showCardPanel: true,
-      showCards: false,
-      // The params below are for FxA metrics
-      deviceId: "",
-      flowId: "",
-      flowBeginTime: 0
-    };
-    this.fxaMetricsInitialized = false;
-  }
-
-  get dialog() {
-    return this.props.document.getElementById("trailheadDialog");
-  }
-
-  async componentWillMount() {
-    FLUENT_FILES.forEach(file => {
-      const link = document.head.appendChild(document.createElement("link"));
-      link.href = file;
-      link.rel = "localization";
-    });
-    await this.componentWillUpdate(this.props);
-  } // Get the fxa data if we don't have it yet from mount or update
-
-
-  async componentWillUpdate(props) {
-    if (props.fxaEndpoint && !this.fxaMetricsInitialized) {
-      try {
-        this.fxaMetricsInitialized = true;
-        const url = new URL(`${props.fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`);
-        this.addUtmParams(url);
-        const response = await fetch(url, {
-          credentials: "omit"
-        });
-
-        if (response.status === 200) {
-          const {
-            deviceId,
-            flowId,
-            flowBeginTime
-          } = await response.json();
-          this.setState({
-            deviceId,
-            flowId,
-            flowBeginTime
-          });
-        } else {
-          props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
-            type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-            data: {
-              event: "FXA_METRICS_FETCH_ERROR",
-              value: response.status
-            }
-          }));
-        }
-      } catch (error) {
-        props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({
-          type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT,
-          data: {
-            event: "FXA_METRICS_ERROR"
-          }
-        }));
-      }
-    }
-  }
-
-  componentDidMount() {
-    // We need to remove hide-main since we should show it underneath everything that has rendered
-    this.props.document.body.classList.remove("hide-main"); // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
-
-    this.props.document.body.classList.add("inline-onboarding"); // The rest of the page is "hidden" when the modal is open
-
-    if (this.props.message.content) {
-      this.props.document.getElementById("root").setAttribute("aria-hidden", "true"); // Start with focus in the email input box
-
-      this.dialog.querySelector("input[name=email]").focus();
-    } else {
-      // No modal overlay, let the user scroll and deal them some cards.
-      this.props.document.body.classList.remove("welcome");
-
-      if (this.props.message.includeBundle || this.props.message.cards) {
-        this.revealCards();
-      }
-    }
+    this.onHideContainer = this.onHideContainer.bind(this);
+  }
+
+  componentWillMount() {
+    global.document.body.classList.add("inline-onboarding");
   }
 
   componentWillUnmount() {
     this.props.document.body.classList.remove("inline-onboarding");
   }
 
-  onInputChange(e) {
-    let error = e.target.previousSibling;
-    this.setState({
-      emailInput: e.target.value
-    });
-    error.classList.remove("active");
-    e.target.classList.remove("invalid");
-  }
-
-  onStartBlur(event) {
-    // Make sure focus stays within the dialog when tabbing from the button
-    const {
-      dialog
-    } = this;
-
-    if (event.relatedTarget && !(dialog.compareDocumentPosition(event.relatedTarget) & dialog.DOCUMENT_POSITION_CONTAINED_BY)) {
-      dialog.querySelector(FOCUSABLE_SELECTOR).focus();
-    }
-  }
-
-  onSubmit(event) {
-    // Dynamically require the email on submission so screen readers don't read
-    // out it's always required because there's also ways to skip the modal
-    const {
-      email
-    } = event.target.elements;
-
-    if (!email.value.length) {
-      email.required = true;
-      email.checkValidity();
-      event.preventDefault();
-      return;
-    }
-
-    this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
-      event: "SUBMIT_EMAIL",
-      ...this._getFormInfo()
-    }));
-    global.addEventListener("visibilitychange", this.closeModal);
-  }
-
-  closeModal(ev) {
-    global.removeEventListener("visibilitychange", this.closeModal);
-    this.props.document.body.classList.remove("welcome");
-    this.props.document.getElementById("root").removeAttribute("aria-hidden");
-    this.setState({
-      isModalOpen: false
-    });
-    this.revealCards(); // If closeModal() was triggered by a visibilitychange event, the user actually
-    // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
-
-    if (!ev || ev.type !== "visibilitychange") {
-      this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
-        event: "SKIPPED_SIGNIN",
-        ...this._getFormInfo()
-      }));
-    } // Bug 1190882 - Focus in a disappearing dialog confuses screen readers
-
-
-    this.props.document.activeElement.blur();
-  }
-  /**
-   * Report to telemetry additional information about the form submission.
-   */
-
-
-  _getFormInfo() {
-    const value = {
-      has_flow_params: this.state.flowId.length > 0
-    };
-    return {
-      value
-    };
-  }
-
-  onInputInvalid(e) {
-    let error = e.target.previousSibling;
-    error.classList.add("active");
-    e.target.classList.add("invalid");
-    e.preventDefault(); // Override built-in form validation popup
-
-    e.target.focus();
-  }
-
-  hideCardPanel() {
-    this.setState({
-      showCardPanel: false
-    });
-    this.props.onDismissBundle();
-  }
-
-  revealCards() {
-    this.setState({
-      showCards: true
-    });
-  }
-  /**
-   * Takes in a url as a string or URL object and returns a URL object with the
-   * utm_* parameters added to it. If a URL object is passed in, the paraemeters
-   * are added to it (the return value can be ignored in that case as it's the
-   * same object).
-   */
-
-
-  addUtmParams(url, isCard = false) {
-    let returnUrl = url;
-
-    if (typeof returnUrl === "string") {
-      returnUrl = new URL(url);
-    }
-
-    returnUrl.searchParams.append("utm_source", "activity-stream");
-    returnUrl.searchParams.append("utm_campaign", "firstrun");
-    returnUrl.searchParams.append("utm_medium", "referral");
-    returnUrl.searchParams.append("utm_term", `${this.props.message.utm_term}${isCard ? "-card" : ""}`);
-    return returnUrl;
-  }
-
   onCardAction(action) {
     let actionUpdates = {};
+    const {
+      flowParams,
+      UTMTerm
+    } = this.props;
 
     if (action.type === "OPEN_URL") {
       let url = new URL(action.data.args);
-      this.addUtmParams(url, true);
+      Object(_addUtmParams__WEBPACK_IMPORTED_MODULE_2__["addUtmParams"])(url, UTMTerm);
 
       if (action.addFlowParams) {
-        url.searchParams.append("device_id", this.state.deviceId);
-        url.searchParams.append("flow_id", this.state.flowId);
-        url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
+        url.searchParams.append("device_id", flowParams.deviceId);
+        url.searchParams.append("flow_id", flowParams.flowId);
+        url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
       }
 
       actionUpdates = {
         data: { ...action.data,
-          args: url
+          args: url.toString()
         }
       };
     }
 
     this.props.onAction({ ...action,
       ...actionUpdates
     });
   }
 
+  onHideContainer() {
+    const {
+      sendUserActionTelemetry,
+      cards,
+      hideContainer
+    } = this.props;
+    hideContainer();
+    sendUserActionTelemetry({
+      event: "DISMISS",
+      id: "onboarding-cards",
+      message_id: cards.map(m => m.id).join(","),
+      action: "onboarding_user_event"
+    });
+  }
+
   render() {
     const {
-      props
-    } = this;
-    const {
-      bundle: cards,
-      content,
-      utm_term
-    } = props.message;
-    const innerClassName = ["trailhead", content && content.className].filter(v => v).join(" ");
-    return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_3___default.a.Fragment, null, this.state.isModalOpen && content ? react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_1__["ModalOverlayWrapper"], {
-      innerClassName: innerClassName,
-      onClose: this.closeModal,
-      id: "trailheadDialog",
-      headerId: "trailheadHeader"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: "trailheadInner"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: "trailheadContent"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h1", {
-      "data-l10n-id": content.title.string_id,
-      id: "trailheadHeader"
-    }), content.subtitle && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
-      "data-l10n-id": content.subtitle.string_id
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
-      className: "trailheadBenefits"
-    }, content.benefits.map(item => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("li", {
-      key: item.id,
-      className: item.id
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
-      "data-l10n-id": item.title.string_id
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
-      "data-l10n-id": item.text.string_id
-    })))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
-      className: "trailheadLearn",
-      "data-l10n-id": content.learn.text.string_id,
-      href: this.addUtmParams(content.learn.url),
-      target: "_blank",
-      rel: "noopener noreferrer"
-    })), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      role: "group",
-      "aria-labelledby": "joinFormHeader",
-      "aria-describedby": "joinFormBody",
-      className: "trailheadForm"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
-      id: "joinFormHeader",
-      "data-l10n-id": content.form.title.string_id
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
-      id: "joinFormBody",
-      "data-l10n-id": content.form.text.string_id
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("form", {
-      method: "get",
-      action: this.props.fxaEndpoint,
-      target: "_blank",
-      rel: "noopener noreferrer",
-      onSubmit: this.onSubmit
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "service",
-      type: "hidden",
-      value: "sync"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "action",
-      type: "hidden",
-      value: "email"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "context",
-      type: "hidden",
-      value: "fx_desktop_v3"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "entrypoint",
-      type: "hidden",
-      value: "activity-stream-firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "utm_source",
-      type: "hidden",
-      value: "activity-stream"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "utm_campaign",
-      type: "hidden",
-      value: "firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "utm_term",
-      type: "hidden",
-      value: utm_term
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "device_id",
-      type: "hidden",
-      value: this.state.deviceId
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "flow_id",
-      type: "hidden",
-      value: this.state.flowId
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "flow_begin_time",
-      type: "hidden",
-      value: this.state.flowBeginTime
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      name: "style",
-      type: "hidden",
-      value: "trailhead"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
-      "data-l10n-id": "onboarding-join-form-email-error",
-      className: "error"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
-      "data-l10n-id": content.form.email.string_id,
-      name: "email",
-      type: "email",
-      onInvalid: this.onInputInvalid,
-      onChange: this.onInputChange
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
-      className: "trailheadTerms",
-      "data-l10n-id": "onboarding-join-form-legal"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
-      "data-l10n-name": "terms",
-      target: "_blank",
-      rel: "noopener noreferrer",
-      href: this.addUtmParams("https://accounts.firefox.com/legal/terms")
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
-      "data-l10n-name": "privacy",
-      target: "_blank",
-      rel: "noopener noreferrer",
-      href: this.addUtmParams("https://accounts.firefox.com/legal/privacy")
-    })), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
-      "data-l10n-id": content.form.button.string_id,
-      type: "submit"
-    })))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
-      className: "trailheadStart",
-      "data-l10n-id": content.skipButton.string_id,
-      onBlur: this.onStartBlur,
-      onClick: this.closeModal
-    })) : null, cards && cards.length ? react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: `trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+      cards,
+      showCardPanel,
+      showContent,
+      sendUserActionTelemetry
+    } = this.props;
+    return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
+      className: `trailheadCards ${showCardPanel ? "expanded" : "collapsed"}`
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
       className: "trailheadCardsInner",
-      "aria-hidden": !this.state.showCards
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h1", {
+      "aria-hidden": !showContent
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
       "data-l10n-id": "onboarding-welcome-header"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: `trailheadCardGrid${this.state.showCards ? " show" : ""}`
-    }, cards.map(card => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__["OnboardingCard"], _extends({
+    }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
+      className: `trailheadCardGrid${showContent ? " show" : ""}`
+    }, cards.map(card => react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_1__["OnboardingCard"], _extends({
       key: card.id,
       className: "trailheadCard",
-      sendUserActionTelemetry: props.sendUserActionTelemetry,
+      sendUserActionTelemetry: sendUserActionTelemetry,
       onAction: this.onCardAction,
       UISurface: "TRAILHEAD"
-    }, card)))), this.state.showCardPanel && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
+    }, card)))), showCardPanel && react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
       className: "icon icon-dismiss",
-      onClick: this.hideCardPanel,
+      onClick: this.onHideContainer,
       "data-l10n-id": "onboarding-cards-dismiss"
-    }))) : null);
+    })));
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 26 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OnboardingCard", function() { return OnboardingCard; });
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
+/* 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 OnboardingCard extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.onClick = this.onClick.bind(this);
+  }
+
+  onClick() {
+    const {
+      props
+    } = this;
+    const ping = {
+      event: "CLICK_BUTTON",
+      message_id: props.id,
+      id: props.UISurface
+    };
+    props.sendUserActionTelemetry(ping);
+    props.onAction(props.content.primary_button.action);
+  }
+
+  render() {
+    const {
+      content
+    } = this.props;
+    const className = this.props.className || "onboardingMessage";
+    return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
+      className: className
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
+      className: `onboardingMessageImage ${content.icon}`
+    }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
+      className: "onboardingContent"
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h3", {
+      className: "onboardingTitle",
+      "data-l10n-id": content.title.string_id
+    }), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("p", {
+      className: "onboardingText",
+      "data-l10n-id": content.text.string_id
+    })), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("span", {
+      className: "onboardingButtonContainer"
+    }, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
+      "data-l10n-id": content.primary_button.label.string_id,
+      className: "button onboardingButton",
+      onClick: this.onClick
+    }))));
+  }
+
+}
+
+/***/ }),
+/* 27 */
+/***/ (function(module, exports) {
+
+module.exports = ReactRedux;
+
+/***/ }),
+/* 28 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SimpleHashRouter", function() { return SimpleHashRouter; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 
 class SimpleHashRouter extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onHashChange = this.onHashChange.bind(this);
@@ -3747,25 +3572,25 @@ class SimpleHashRouter extends react__WE
       }
     });
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 27 */
+/* 29 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_ConfirmDialog", function() { return _ConfirmDialog; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ConfirmDialog", function() { return ConfirmDialog; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(24);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
 
 
 
 /**
  * ConfirmDialog component.
@@ -3849,17 +3674,17 @@ class _ConfirmDialog extends react__WEBP
       "data-l10n-id": this.props.data.confirm_button_string_id
     }))));
   }
 
 }
 const ConfirmDialog = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])(state => state.Dialog)(_ConfirmDialog);
 
 /***/ }),
-/* 28 */
+/* 30 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ContextMenu", function() { return ContextMenu; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ContextMenuItem", function() { return ContextMenuItem; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
@@ -4002,17 +3827,17 @@ class ContextMenuItem extends react__WEB
       "data-l10n-id": option.string_id || option.id
     })));
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 29 */
+/* 31 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "INTERSECTION_RATIO", function() { return INTERSECTION_RATIO; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ImpressionStats", function() { return ImpressionStats; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
@@ -4226,29 +4051,29 @@ ImpressionStats.defaultProps = {
   IntersectionObserver: global.IntersectionObserver,
   document: global.document,
   rows: [],
   source: ""
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 30 */
+/* 32 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CollapsibleSection", function() { return CollapsibleSection; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(31);
-/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(33);
+/* harmony import */ var content_src_components_ErrorBoundary_ErrorBoundary__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(33);
+/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(35);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
-/* harmony import */ var content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(34);
-/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(35);
+/* harmony import */ var content_src_components_SectionMenu_SectionMenu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(36);
+/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(37);
 
 
 
 
 
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
@@ -4525,24 +4350,24 @@ CollapsibleSection.defaultProps = {
   },
   Prefs: {
     values: {}
   }
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 31 */
+/* 33 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundaryFallback", function() { return ErrorBoundaryFallback; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ErrorBoundary", function() { return ErrorBoundary; });
-/* harmony import */ var content_src_components_A11yLinkButton_A11yLinkButton__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32);
+/* harmony import */ var content_src_components_A11yLinkButton_A11yLinkButton__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(34);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
 
 
 class ErrorBoundaryFallback extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.windowObj = this.props.windowObj || window;
@@ -4609,17 +4434,17 @@ class ErrorBoundary extends react__WEBPA
   }
 
 }
 ErrorBoundary.defaultProps = {
   FallbackComponent: ErrorBoundaryFallback
 };
 
 /***/ }),
-/* 32 */
+/* 34 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A11yLinkButton", function() { return A11yLinkButton; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
@@ -4636,17 +4461,17 @@ function A11yLinkButton(props) {
   return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", _extends({
     type: "button"
   }, props, {
     className: className
   }), props.children);
 }
 
 /***/ }),
-/* 33 */
+/* 35 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FluentOrText", function() { return FluentOrText; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 
@@ -4679,28 +4504,28 @@ class FluentOrText extends react__WEBPAC
 
 
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.cloneElement(child, extraProps, grandChildren);
   }
 
 }
 
 /***/ }),
-/* 34 */
+/* 36 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_SectionMenu", function() { return _SectionMenu; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenu", function() { return SectionMenu; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(28);
+/* harmony import */ var content_src_components_ContextMenu_ContextMenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(35);
+/* harmony import */ var content_src_lib_section_menu_options__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(37);
 
 
 
 
 const DEFAULT_SECTION_MENU_OPTIONS = ["MoveUp", "MoveDown", "Separator", "RemoveSection", "CheckCollapsed", "Separator", "ManageSection"];
 const WEBEXT_SECTION_MENU_OPTIONS = ["MoveUp", "MoveDown", "Separator", "CheckCollapsed", "Separator", "ManageWebExtension"];
 class _SectionMenu extends react__WEBPACK_IMPORTED_MODULE_2___default.a.PureComponent {
   getOptions() {
@@ -4760,17 +4585,17 @@ class _SectionMenu extends react__WEBPAC
       options: this.getOptions()
     });
   }
 
 }
 const SectionMenu = _SectionMenu;
 
 /***/ }),
-/* 35 */
+/* 37 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionMenuOptions", function() { return SectionMenuOptions; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 
 /**
@@ -4887,38 +4712,38 @@ const SectionMenuOptions = {
       }
     }),
     userEvent: "MENU_PRIVACY_NOTICE"
   }),
   CheckCollapsed: section => section.collapsed ? SectionMenuOptions.ExpandSection(section) : SectionMenuOptions.CollapseSection(section)
 };
 
 /***/ }),
-/* 36 */
+/* 38 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Section", function() { return Section; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(54);
-/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(30);
-/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(38);
-/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(33);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(24);
+/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(56);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(32);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(40);
+/* harmony import */ var content_src_components_FluentOrText_FluentOrText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(35);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
-/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(40);
-/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(41);
+/* harmony import */ var content_src_components_MoreRecommendations_MoreRecommendations__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(42);
+/* harmony import */ var content_src_components_PocketLoggedInCta_PocketLoggedInCta__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(43);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
-/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(42);
-/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(43);
+/* harmony import */ var content_src_components_Topics_Topics__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(44);
+/* harmony import */ var content_src_components_TopSites_TopSites__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(45);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -5255,17 +5080,17 @@ class _Sections extends react__WEBPACK_I
 }
 const Sections = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({
   Sections: state.Sections,
   Prefs: state.Prefs
 }))(_Sections);
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 37 */
+/* 39 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ScreenshotUtils", function() { return ScreenshotUtils; });
 /**
  * List of helper functions for screenshot-based images.
  *
@@ -5320,24 +5145,24 @@ const ScreenshotUtils = {
 
     return !remoteImage && !localImage;
   }
 
 };
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 38 */
+/* 40 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ComponentPerfTimer", function() { return ComponentPerfTimer; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(39);
+/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(41);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);
 
 
  // Currently record only a fixed set of sections. This will prevent data
 // from custom sections from showing up or from topstories.
 
 const RECORDED_SECTIONS = ["highlights", "topsites"];
@@ -5497,17 +5322,17 @@ class ComponentPerfTimer extends react__
     }
 
     return this.props.children;
   }
 
 }
 
 /***/ }),
-/* 39 */
+/* 41 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PerfService", function() { return _PerfService; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "perfService", function() { return perfService; });
 
 
@@ -5626,17 +5451,17 @@ function _PerfService(options) {
     let mostRecentEntry = entries[entries.length - 1];
     return this._perf.timeOrigin + mostRecentEntry.startTime;
   }
 
 };
 var perfService = new _PerfService();
 
 /***/ }),
-/* 40 */
+/* 42 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MoreRecommendations", function() { return MoreRecommendations; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 
@@ -5655,24 +5480,24 @@ class MoreRecommendations extends react_
     }
 
     return null;
   }
 
 }
 
 /***/ }),
-/* 41 */
+/* 43 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_PocketLoggedInCta", function() { return _PocketLoggedInCta; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PocketLoggedInCta", function() { return PocketLoggedInCta; });
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(24);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
 
 
 class _PocketLoggedInCta extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   render() {
     const {
@@ -5695,17 +5520,17 @@ class _PocketLoggedInCta extends react__
   }
 
 }
 const PocketLoggedInCta = Object(react_redux__WEBPACK_IMPORTED_MODULE_0__["connect"])(state => ({
   Pocket: state.Pocket
 }))(_PocketLoggedInCta);
 
 /***/ }),
-/* 42 */
+/* 44 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topic", function() { return Topic; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Topics", function() { return Topics; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
@@ -5737,36 +5562,36 @@ class Topics extends react__WEBPACK_IMPO
       url: t.url,
       name: t.name
     }))));
   }
 
 }
 
 /***/ }),
-/* 43 */
+/* 45 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_TopSites", function() { return _TopSites; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSites", function() { return TopSites; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(44);
-/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(30);
-/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(38);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(24);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_CollapsibleSection_CollapsibleSection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(32);
+/* harmony import */ var content_src_components_ComponentPerfTimer_ComponentPerfTimer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(40);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_4__);
-/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(13);
+/* harmony import */ var _asrouter_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
-/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(45);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(53);
-/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(55);
-/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(46);
+/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(47);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(55);
+/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(58);
+/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(48);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -5960,17 +5785,17 @@ class _TopSites extends react__WEBPACK_I
 const TopSites = Object(react_redux__WEBPACK_IMPORTED_MODULE_4__["connect"])(state => ({
   TopSites: state.TopSites,
   Prefs: state.Prefs,
   TopSitesRows: state.Prefs.values.topSitesRows
 }))(_TopSites);
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 44 */
+/* 46 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SOURCE", function() { return TOP_SITES_SOURCE; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_CONTEXT_MENU_OPTIONS; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS", function() { return TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MIN_RICH_FAVICON_SIZE", function() { return MIN_RICH_FAVICON_SIZE; });
@@ -5980,27 +5805,27 @@ const TOP_SITES_CONTEXT_MENU_OPTIONS = [
 
 const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"]; // minimum size necessary to show a rich icon instead of a screenshot
 
 const MIN_RICH_FAVICON_SIZE = 96; // minimum size necessary to show any icon in the top left corner with a screenshot
 
 const MIN_CORNER_FAVICON_SIZE = 16;
 
 /***/ }),
-/* 45 */
+/* 47 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SelectableSearchShortcut", function() { return SelectableSearchShortcut; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SearchShortcutsForm", function() { return SearchShortcutsForm; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(44);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(46);
 
 
 
 class SelectableSearchShortcut extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureComponent {
   render() {
     const {
       shortcut,
       selected
@@ -6168,32 +5993,32 @@ class SearchShortcutsForm extends react_
       onClick: this.onSaveButtonClick,
       "data-l10n-id": "newtab-topsites-save-button"
     })));
   }
 
 }
 
 /***/ }),
-/* 46 */
+/* 48 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteLink", function() { return TopSiteLink; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSite", function() { return TopSite; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSitePlaceholder", function() { return TopSitePlaceholder; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteList", function() { return TopSiteList; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(44);
-/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(56);
+/* harmony import */ var _TopSitesConstants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(46);
+/* harmony import */ var content_src_components_LinkMenu_LinkMenu__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(59);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
-/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37);
-/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(53);
+/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(39);
+/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(55);
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
@@ -6815,25 +6640,25 @@ class TopSiteList extends react__WEBPACK
     return react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
       className: `top-sites-list${this.state.draggedSite ? " dnd-active" : ""}`
     }, topSitesUI);
   }
 
 }
 
 /***/ }),
-/* 47 */
+/* 49 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Search", function() { return _Search; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Search", function() { return Search; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(24);
+/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27);
 /* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_1__);
 /* harmony import */ var content_src_lib_constants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(11);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
 /* globals ContentSearchUIController */
 
 
 
@@ -6999,24 +6824,24 @@ class _Search extends react__WEBPACK_IMP
       ref: this.onInputMount
     })));
   }
 
 }
 const Search = Object(react_redux__WEBPACK_IMPORTED_MODULE_1__["connect"])()(_Search);
 
 /***/ }),
-/* 48 */
+/* 50 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DetectUserSessionStart", function() { return DetectUserSessionStart; });
 /* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
-/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(39);
+/* harmony import */ var common_PerfService_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(41);
 
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 class DetectUserSessionStart {
   constructor(store, options = {}) {
     this._store = store; // Overrides for testing
 
@@ -7078,31 +6903,31 @@ class DetectUserSessionStart {
       this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
   }
 
 }
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
-/* 49 */
+/* 51 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: external "ReactDOM"
-var external_ReactDOM_ = __webpack_require__(14);
+var external_ReactDOM_ = __webpack_require__(12);
 var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
 
 
 class DSImage_DSImage extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
@@ -7215,17 +7040,17 @@ DSImage_DSImage.defaultProps = {
   rawSource: null,
   // Unadulterated image URL to filter through Thumbor
   extraClassNames: null,
   // Additional classnames to append to component
   optimize: true // Measure parent container to request exact sizes
 
 };
 // EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx + 1 modules
-var LinkMenu = __webpack_require__(56);
+var LinkMenu = __webpack_require__(59);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
 
 
 class DSLinkMenu_DSLinkMenu extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
@@ -7311,17 +7136,17 @@ class DSLinkMenu_DSLinkMenu extends exte
         shim: this.props.shim,
         bookmarkGuid: this.props.bookmarkGuid
       }
     }));
   }
 
 }
 // EXTERNAL MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx
-var ImpressionStats = __webpack_require__(29);
+var ImpressionStats = __webpack_require__(31);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx
 
 
 class SafeAnchor_SafeAnchor extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
@@ -7636,20 +7461,20 @@ class CardGrid_CardGrid extends external
 
 }
 CardGrid_CardGrid.defaultProps = {
   border: `border`,
   items: 4 // Number of stories to display
 
 };
 // EXTERNAL MODULE: ./content-src/components/CollapsibleSection/CollapsibleSection.jsx
-var CollapsibleSection = __webpack_require__(30);
+var CollapsibleSection = __webpack_require__(32);
 
 // EXTERNAL MODULE: external "ReactRedux"
-var external_ReactRedux_ = __webpack_require__(24);
+var external_ReactRedux_ = __webpack_require__(27);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx
 
 
 class DSMessage_DSMessage extends external_React_default.a.PureComponent {
   render() {
     return external_React_default.a.createElement("div", {
       className: "ds-message"
@@ -8000,17 +7825,17 @@ class Hero_Hero extends external_React_d
 }
 Hero_Hero.defaultProps = {
   data: {},
   border: `border`,
   items: 1 // Number of stories to display
 
 };
 // EXTERNAL MODULE: ./content-src/components/Sections/Sections.jsx
-var Sections = __webpack_require__(36);
+var Sections = __webpack_require__(38);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 class Highlights_Highlights extends external_React_default.a.PureComponent {
@@ -8296,17 +8121,17 @@ const selectLayoutRender = (state, prefs
   }
 
   return {
     spocsFill,
     layoutRender
   };
 };
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSites.jsx
-var TopSites = __webpack_require__(43);
+var TopSites = __webpack_require__(45);
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopSites/TopSites.jsx
 
 
 
 class TopSites_TopSites extends external_React_default.a.PureComponent {
   render() {
     const header = this.props.header || {};
@@ -8619,17 +8444,17 @@ class DiscoveryStreamBase_DiscoveryStrea
 }
 const DiscoveryStreamBase = Object(external_ReactRedux_["connect"])(state => ({
   DiscoveryStream: state.DiscoveryStream,
   Prefs: state.Prefs,
   Sections: state.Sections
 }))(DiscoveryStreamBase_DiscoveryStreamBase);
 
 /***/ }),
-/* 50 */
+/* 52 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 
@@ -9416,28 +9241,28 @@ localized_Localized.propTypes = {
  * components for more information.
  */
 
 
 
 
 
 /***/ }),
-/* 51 */
+/* 53 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: ./content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
-var EOYSnippet_schema = __webpack_require__(18);
+var EOYSnippet_schema = __webpack_require__(13);
 
 // CONCATENATED MODULE: ./content-src/asrouter/components/Button/Button.jsx
 
 const ALLOWED_STYLE_TAGS = ["color", "backgroundColor"];
 const Button = props => {
   const style = {}; // Add allowed style tags from props, e.g. props.color becomes style={color: props.color}
 
   for (const tag of ALLOWED_STYLE_TAGS) {
@@ -9460,23 +9285,23 @@ const Button = props => {
 // CONCATENATED MODULE: ./content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx
 // lifted from https://gist.github.com/kitze/23d82bb9eb0baabfd03a6a720b1d637f
 const ConditionalWrapper = ({
   condition,
   wrap,
   children
 }) => condition ? wrap(children) : children;
 // EXTERNAL MODULE: ./content-src/asrouter/components/RichText/RichText.jsx
-var RichText = __webpack_require__(16);
+var RichText = __webpack_require__(14);
 
 // EXTERNAL MODULE: ./content-src/asrouter/template-utils.js
-var template_utils = __webpack_require__(17);
+var template_utils = __webpack_require__(15);
 
 // EXTERNAL MODULE: ./content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
-var SimpleSnippet_schema = __webpack_require__(19);
+var SimpleSnippet_schema = __webpack_require__(16);
 
 // CONCATENATED MODULE: ./content-src/asrouter/components/SnippetBase/SnippetBase.jsx
 
 
 class SnippetBase_SnippetBase extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onBlockClicked = this.onBlockClicked.bind(this);
@@ -9916,17 +9741,17 @@ const EOYSnippet = props => {
     ...props.content
   };
   return external_React_default.a.createElement(EOYSnippet_EOYSnippetBase, EOYSnippet_extends({}, props, {
     content: extendedContent,
     form_method: "GET"
   }));
 };
 // EXTERNAL MODULE: ./content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json
-var FXASignupSnippet_schema = __webpack_require__(20);
+var FXASignupSnippet_schema = __webpack_require__(17);
 
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
 function SubmitFormSnippet_extends() { SubmitFormSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SubmitFormSnippet_extends.apply(this, arguments); }
 
 
 
 
 
@@ -10258,17 +10083,17 @@ const FXASignupSnippet = props => {
   };
   return external_React_default.a.createElement(SubmitFormSnippet_SubmitFormSnippet, FXASignupSnippet_extends({}, props, {
     content: extendedContent,
     form_action: "https://accounts.firefox.com/",
     form_method: "GET"
   }));
 };
 // EXTERNAL MODULE: ./content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json
-var NewsletterSnippet_schema = __webpack_require__(21);
+var NewsletterSnippet_schema = __webpack_require__(18);
 
 // CONCATENATED MODULE: ./content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx
 function NewsletterSnippet_extends() { NewsletterSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return NewsletterSnippet_extends.apply(this, arguments); }
 
 
 
 
 const NewsletterSnippet = props => {
@@ -10331,17 +10156,17 @@ function isEmailOrPhoneNumber(val, conte
     return "email";
   } else if (check_phone) {
     return "phone";
   }
 
   return "";
 }
 // EXTERNAL MODULE: ./content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json
-var SendToDeviceSnippet_schema = __webpack_require__(22);
+var SendToDeviceSnippet_schema = __webpack_require__(19);
 
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx
 function SendToDeviceSnippet_extends() { SendToDeviceSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SendToDeviceSnippet_extends.apply(this, arguments); }
 
 
 
 
 
@@ -10543,17 +10368,17 @@ const SnippetsTemplates = {
   newsletter_snippet: NewsletterSnippet,
   fxa_signup_snippet: FXASignupSnippet,
   send_to_device_snippet: SendToDeviceSnippet,
   eoy_snippet: EOYSnippet,
   simple_below_search_snippet: SimpleBelowSearchSnippet_SimpleBelowSearchSnippet
 };
 
 /***/ }),
-/* 52 */
+/* 54 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // CONCATENATED MODULE: ./node_modules/fluent/src/types.js
 /* global Intl */
 
@@ -11960,17 +11785,17 @@ function generateBundles(content) {
     }
 
     bundle.addMessages(`${key} = ${string}`);
   });
   return [bundle];
 }
 
 /***/ }),
-/* 53 */
+/* 55 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
@@ -12816,17 +12641,17 @@ var reducers = {
   Dialog,
   Sections,
   Pocket,
   DiscoveryStream,
   Search
 };
 
 /***/ }),
-/* 54 */
+/* 56 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
@@ -12849,27 +12674,27 @@ const cardContextTypes = {
     icon: "pocket"
   },
   download: {
     fluentID: "newtab-label-download",
     icon: "download"
   }
 };
 // EXTERNAL MODULE: external "ReactRedux"
-var external_ReactRedux_ = __webpack_require__(24);
+var external_ReactRedux_ = __webpack_require__(27);
 
 // EXTERNAL MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx + 1 modules
-var LinkMenu = __webpack_require__(56);
+var LinkMenu = __webpack_require__(59);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: ./content-src/lib/screenshot-utils.js
-var screenshot_utils = __webpack_require__(37);
+var screenshot_utils = __webpack_require__(39);
 
 // CONCATENATED MODULE: ./content-src/components/Card/Card.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Card", function() { return Card_Card; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Card", function() { return Card; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PlaceholderCard", function() { return PlaceholderCard; });
 
 
 
@@ -13196,34 +13021,324 @@ const Card = Object(external_ReactRedux_
   platform: state.Prefs.values.platform
 }))(Card_Card);
 const PlaceholderCard = props => external_React_default.a.createElement(Card, {
   placeholder: true,
   className: props.className
 });
 
 /***/ }),
-/* 55 */
+/* 57 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+
+// EXTERNAL MODULE: external "React"
+var external_React_ = __webpack_require__(9);
+var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
+
+// EXTERNAL MODULE: ./content-src/asrouter/templates/Trailhead/Trailhead.jsx
+var Trailhead = __webpack_require__(20);
+
+// EXTERNAL MODULE: ./content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
+var ReturnToAMO = __webpack_require__(23);
+
+// EXTERNAL MODULE: ./content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
+var StartupOverlay = __webpack_require__(24);
+
+// EXTERNAL MODULE: ./node_modules/fluent-react/src/index.js + 14 modules
+var src = __webpack_require__(52);
+
+// EXTERNAL MODULE: ./content-src/asrouter/rich-text-strings.js + 8 modules
+var rich_text_strings = __webpack_require__(54);
+
+// CONCATENATED MODULE: ./content-src/asrouter/templates/FirstRun/Interrupt.jsx
+function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+class Interrupt_Interrupt extends external_React_default.a.PureComponent {
+  render() {
+    const {
+      onDismiss,
+      onNextScene,
+      message,
+      sendUserActionTelemetry,
+      executeAction,
+      dispatch,
+      fxaEndpoint,
+      UTMTerm,
+      flowParams
+    } = this.props;
+
+    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, {
+          UISurface: "NEWTAB_OVERLAY",
+          onBlock: onDismiss,
+          onAction: executeAction,
+          sendUserActionTelemetry: sendUserActionTelemetry
+        })));
+
+      case "fxa_overlay":
+        return external_React_default.a.createElement(StartupOverlay["StartupOverlay"], {
+          onBlock: onDismiss,
+          dispatch: dispatch,
+          fxa_endpoint: fxaEndpoint
+        });
+
+      case "trailhead":
+        return external_React_default.a.createElement(Trailhead["Trailhead"], {
+          document: this.props.document,
+          message: message,
+          onNextScene: onNextScene,
+          onAction: executeAction,
+          sendUserActionTelemetry: sendUserActionTelemetry,
+          dispatch: dispatch,
+          fxaEndpoint: fxaEndpoint,
+          UTMTerm: UTMTerm,
+          flowParams: flowParams
+        });
+
+      default:
+        throw new Error(`${message.template} is not a valid FirstRun message`);
+    }
+  }
+
+}
+// EXTERNAL MODULE: ./content-src/asrouter/templates/FirstRun/Triplets.jsx
+var Triplets = __webpack_require__(25);
+
+// EXTERNAL MODULE: ./content-src/asrouter/templates/FirstRun/addUtmParams.js
+var addUtmParams = __webpack_require__(22);
+
+// CONCATENATED MODULE: ./content-src/asrouter/templates/FirstRun/FirstRun.jsx
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FLUENT_FILES", function() { return FLUENT_FILES; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helpers", function() { return helpers; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FirstRun", function() { return FirstRun_FirstRun; });
+/* 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/. */
+
+
+
+ // Note: should match the transition time on .trailheadCards in _Trailhead.scss
+
+const TRANSITION_LENGTH = 500;
+const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/brandings.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"];
+const helpers = {
+  selectInterruptAndTriplets(message = {}, interruptCleared) {
+    const hasInterrupt = interruptCleared === true ? false : Boolean(message.content);
+    const hasTriplets = Boolean(message.bundle && message.bundle.length);
+    const UTMTerm = message.utm_term || "";
+    return {
+      hasTriplets,
+      hasInterrupt,
+      interrupt: hasInterrupt ? message : null,
+      triplets: hasTriplets ? message.bundle : null,
+      UTMTerm
+    };
+  },
+
+  addFluent(document) {
+    FLUENT_FILES.forEach(file => {
+      const link = document.head.appendChild(document.createElement("link"));
+      link.href = file;
+      link.rel = "localization";
+    });
+  }
+
+};
+class FirstRun_FirstRun extends external_React_default.a.PureComponent {
+  constructor(props) {
+    super(props);
+    this.didLoadFlowParams = false;
+    this.state = {
+      prevMessage: undefined,
+      hasInterrupt: false,
+      hasTriplets: false,
+      interrupt: undefined,
+      triplets: undefined,
+      isInterruptVisible: false,
+      isTripletsContainerVisible: false,
+      isTripletsContentVisible: false,
+      UTMTerm: "",
+      flowParams: undefined
+    };
+    this.closeInterrupt = this.closeInterrupt.bind(this);
+    this.closeTriplets = this.closeTriplets.bind(this);
+    helpers.addFluent(this.props.document);
+  }
+
+  static getDerivedStateFromProps(props, state) {
+    const {
+      message,
+      interruptCleared
+    } = props;
+
+    if (interruptCleared !== state.prevInterruptCleared || message && message.id !== state.prevMessageId) {
+      const {
+        hasTriplets,
+        hasInterrupt,
+        interrupt,
+        triplets,
+        UTMTerm
+      } = helpers.selectInterruptAndTriplets(message, interruptCleared);
+      return {
+        prevMessageId: message.id,
+        prevInterruptCleared: interruptCleared,
+        hasInterrupt,
+        hasTriplets,
+        interrupt,
+        triplets,
+        isInterruptVisible: hasInterrupt,
+        isTripletsContainerVisible: hasTriplets,
+        isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
+        UTMTerm
+      };
+    }
+
+    return null;
+  }
+
+  async fetchFlowParams() {
+    const {
+      fxaEndpoint,
+      fetchFlowParams
+    } = this.props;
+    const {
+      UTMTerm
+    } = this.state;
+
+    if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
+      this.didLoadFlowParams = true;
+      const flowParams = await fetchFlowParams({ ...addUtmParams["BASE_PARAMS"],
+        entrypoint: "activity-stream-firstrun",
+        form_type: "email",
+        utm_term: UTMTerm
+      });
+      this.setState({
+        flowParams
+      });
+    }
+  }
+
+  removeHideMain() {
+    if (!this.state.hasInterrupt) {
+      // We need to remove hide-main since we should show it underneath everything that has rendered
+      this.props.document.body.classList.remove("hide-main", "welcome");
+    }
+  }
+
+  componentDidMount() {
+    this.fetchFlowParams();
+    this.removeHideMain();
+  }
+
+  componentDidUpdate() {
+    // In case we didn't have FXA info immediately, try again when we receive it.
+    this.fetchFlowParams();
+    this.removeHideMain();
+  }
+
+  closeInterrupt() {
+    this.setState(prevState => ({
+      isInterruptVisible: false,
+      isTripletsContainerVisible: prevState.hasTriplets,
+      isTripletsContentVisible: prevState.hasTriplets
+    }));
+  }
+
+  closeTriplets() {
+    this.setState({
+      isTripletsContainerVisible: false
+    }); // Closing triplets should prevent any future extended triplets from showing up
+
+    setTimeout(() => {
+      this.props.onBlockById("EXTENDED_TRIPLETS_1");
+    }, TRANSITION_LENGTH);
+  }
+
+  render() {
+    const {
+      props
+    } = this;
+    const {
+      sendUserActionTelemetry,
+      fxaEndpoint,
+      dispatch,
+      executeAction
+    } = props;
+    const {
+      interrupt,
+      triplets,
+      isInterruptVisible,
+      isTripletsContainerVisible,
+      isTripletsContentVisible,
+      hasTriplets,
+      UTMTerm,
+      flowParams
+    } = this.state;
+    return external_React_default.a.createElement(external_React_default.a.Fragment, null, isInterruptVisible ? external_React_default.a.createElement(Interrupt_Interrupt, {
+      document: props.document,
+      message: interrupt,
+      onNextScene: this.closeInterrupt,
+      UTMTerm: UTMTerm,
+      sendUserActionTelemetry: sendUserActionTelemetry,
+      executeAction: executeAction,
+      dispatch: dispatch,
+      flowParams: flowParams,
+      onDismiss: this.closeInterrupt,
+      fxaEndpoint: fxaEndpoint
+    }) : null, hasTriplets ? external_React_default.a.createElement(Triplets["Triplets"], {
+      document: props.document,
+      cards: triplets,
+      showCardPanel: isTripletsContainerVisible,
+      showContent: isTripletsContentVisible,
+      hideContainer: this.closeTriplets,
+      sendUserActionTelemetry: sendUserActionTelemetry,
+      UTMTerm: `${UTMTerm}-card`,
+      flowParams: flowParams,
+      onAction: executeAction
+    }) : null);
+  }
+
+}
+
+/***/ }),
+/* 58 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
 // EXTERNAL MODULE: ./content-src/components/A11yLinkButton/A11yLinkButton.jsx
-var A11yLinkButton = __webpack_require__(32);
+var A11yLinkButton = __webpack_require__(34);
 
 // EXTERNAL MODULE: external "React"
 var external_React_ = __webpack_require__(9);
 var external_React_default = /*#__PURE__*/__webpack_require__.n(external_React_);
 
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSitesConstants.js
-var TopSitesConstants = __webpack_require__(44);
+var TopSitesConstants = __webpack_require__(46);
 
 // CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
 
 class TopSiteFormInput_TopSiteFormInput extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       validationError: this.props.validationError
@@ -13325,17 +13440,17 @@ class TopSiteFormInput_TopSiteFormInput 
 
 }
 TopSiteFormInput_TopSiteFormInput.defaultProps = {
   showClearButton: false,
   value: "",
   validationError: false
 };
 // EXTERNAL MODULE: ./content-src/components/TopSites/TopSite.jsx
-var TopSite = __webpack_require__(46);
+var TopSite = __webpack_require__(48);
 
 // CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteForm.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TopSiteForm", function() { return TopSiteForm_TopSiteForm; });
 
 
 
 
 
@@ -13619,30 +13734,30 @@ class TopSiteForm_TopSiteForm extends ex
 
 }
 TopSiteForm_TopSiteForm.defaultProps = {
   site: null,
   index: -1
 };
 
 /***/ }),
-/* 56 */
+/* 59 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 
 // EXTERNAL MODULE: ./common/Actions.jsm
 var Actions = __webpack_require__(2);
 
 // EXTERNAL MODULE: external "ReactRedux"
-var external_ReactRedux_ = __webpack_require__(24);
+var external_ReactRedux_ = __webpack_require__(27);
 
 // EXTERNAL MODULE: ./content-src/components/ContextMenu/ContextMenu.jsx
-var ContextMenu = __webpack_require__(28);
+var ContextMenu = __webpack_require__(30);
 
 // CONCATENATED MODULE: ./content-src/lib/link-menu-options.js
 
 
 const _OpenInPrivateWindow = site => ({
   id: "newtab-menu-open-new-private-window",
   icon: "new-window-private",
   action: Actions["actionCreators"].OnlyToMain({
--- a/browser/components/newtab/docs/v2-system-addon/data_events.md
+++ b/browser/components/newtab/docs/v2-system-addon/data_events.md
@@ -950,16 +950,30 @@ CFR impression ping has two forms, in wh
   "source": "CFR",
   // message_id should be a bucket ID in the release channel, we may not use the
   // individual ID, such as addon ID, per legal's request
   "message_id": "bucket_id",
   "event": "IMPRESSION"
 }
 ```
 
+#### Onboarding impression
+```js
+{
+  "client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
+  "action": "onboarding_user_event",
+  "impression_id": "n/a",
+  "source": "FIRST_RUN",
+  "addon_version": "20180710100040",
+  "locale": "en-US",
+  "message_id": "EXTENDED_TRIPLETS_1",
+  "event": "IMPRESSION"
+}
+```
+
 ### User interaction pings
 
 This reports the user's interaction with Activity Stream Router.
 
 #### Snippets interaction pings
 ```js
 {
   "client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -88,16 +88,21 @@ const TRAILHEAD_CONFIG = {
   INTERRUPTS_EXPERIMENT_PREF: "trailhead.firstrun.interruptsExperiment",
   TRIPLETS_ENROLLED_PREF: "trailhead.firstrun.tripletsEnrolled",
   BRANCHES: {
     interrupts: [["control"], ["join"], ["sync"], ["nofirstrun"], ["cards"]],
     triplets: [["supercharge"], ["payoff"], ["multidevice"], ["privacy"]],
   },
   LOCALES: ["en-US", "en-GB", "en-CA", "de", "de-DE", "fr", "fr-FR"],
   EXPERIMENT_RATIOS: [["", 0], ["interrupts", 1], ["triplets", 3]],
+  // Per bug 1571817, for those who meet the targeting criteria of extended
+  // triplets, 99% users (control group) will see the extended triplets, and
+  // the rest 1% (holdback group) won't.
+  EXPERIMENT_RATIOS_FOR_EXTENDED_TRIPLETS: [["control", 99], ["holdback", 1]],
+  EXTENDED_TRIPLETS_EXPERIMENT_PREF: "trailhead.extendedTriplets.experiment",
 };
 
 const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
 const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
 const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
 // List of hosts for endpoints that serve router messages.
 // Key is allowed host, value is a name for the endpoint host.
 const DEFAULT_WHITELIST_HOSTS = {
@@ -484,16 +489,18 @@ class _ASRouter {
       providerBlockList: [],
       messageImpressions: {},
       providerImpressions: {},
       trailheadInitialized: false,
       trailheadInterrupt: "",
       trailheadTriplet: "",
       messages: [],
       errors: [],
+      extendedTripletsInitialized: false,
+      showExtendedTriplets: true,
     };
     this._triggerHandler = this._triggerHandler.bind(this);
     this._localProviders = localProviders;
     this.blockMessageById = this.blockMessageById.bind(this);
     this.unblockMessageById = this.unblockMessageById.bind(this);
     this.onMessage = this.onMessage.bind(this);
     this.handleMessageRequest = this.handleMessageRequest.bind(this);
     this.addImpression = this.addImpression.bind(this);
@@ -729,18 +736,18 @@ class _ASRouter {
       addImpression: this.addImpression,
       blockMessageById: this.blockMessageById,
       unblockMessageById: this.unblockMessageById,
       dispatch: this.dispatch,
     });
 
     this._loadLocalProviders();
 
-    // We need to check whether to set up telemetry for trailhead
-    await this.setupTrailhead();
+    // Instead of setupTrailhead, which adds experiments, just load override pref values
+    await this.setFirstRunStateFromPref();
 
     const messageBlockList =
       (await this._storage.get("messageBlockList")) || [];
     const providerBlockList =
       (await this._storage.get("providerBlockList")) || [];
     const messageImpressions =
       (await this._storage.get("messageImpressions")) || {};
     const providerImpressions =
@@ -881,38 +888,64 @@ class _ASRouter {
     try {
       const data = (await AttributionCode.getAttrDataAsync()) || {};
       return data.source === "addons.mozilla.org";
     } catch (e) {
       return false;
     }
   }
 
+  async setFirstRunStateFromPref() {
+    let interrupt;
+    let triplet;
+
+    const overrideValue = Services.prefs.getStringPref(
+      TRAILHEAD_CONFIG.OVERRIDE_PREF,
+      ""
+    );
+
+    if (overrideValue) {
+      [interrupt, triplet] = overrideValue.split("-");
+    }
+
+    await this.setState({
+      trailheadInterrupt: interrupt,
+      trailheadTriplet: triplet,
+    });
+  }
+
   /**
    * _generateTrailheadBranches - Generates and returns Trailhead configuration and chooses an experiment
    *                             based on clientID and locale.
    * @returns {{experiment: string, interrupt: string, triplet: string}}
    */
   async _generateTrailheadBranches() {
     let experiment = "";
     let interrupt;
     let triplet;
 
-    // Use control Trailhead Branch (for cards) if we are showing RTAMO.
-    if (await this._hasAddonAttributionData()) {
-      return { experiment, interrupt: "control", triplet: "" };
-    }
-
-    // If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
     const overrideValue = Services.prefs.getStringPref(
       TRAILHEAD_CONFIG.OVERRIDE_PREF,
       ""
     );
     if (overrideValue) {
       [interrupt, triplet] = overrideValue.split("-");
+    }
+
+    // Use control Trailhead Branch (for cards) if we are showing RTAMO.
+    if (await this._hasAddonAttributionData()) {
+      return {
+        experiment,
+        interrupt: "control",
+        triplet: triplet || "privacy",
+      };
+    }
+
+    // If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
+    if (overrideValue) {
       return { experiment, interrupt, triplet: triplet || "" };
     }
 
     const locale = Services.locale.appLocaleAsLangTag;
 
     if (TRAILHEAD_CONFIG.LOCALES.includes(locale)) {
       const { userId } = ClientEnvironment;
       experiment = await chooseBranch(
@@ -951,16 +984,50 @@ class _ASRouter {
   // Dispatch a TRAILHEAD_ENROLL_EVENT action
   _sendTrailheadEnrollEvent(data) {
     this.dispatchToAS({
       type: at.TRAILHEAD_ENROLL_EVENT,
       data,
     });
   }
 
+  async setupExtendedTriplets() {
+    // Don't re-initialize
+    if (this.state.extendedTripletsInitialized) {
+      return;
+    }
+
+    let branch = Services.prefs.getStringPref(
+      TRAILHEAD_CONFIG.EXTENDED_TRIPLETS_EXPERIMENT_PREF,
+      ""
+    );
+    if (!branch) {
+      const { userId } = ClientEnvironment;
+      branch = await chooseBranch(
+        `${userId}-extended-triplets-experiment`,
+        TRAILHEAD_CONFIG.EXPERIMENT_RATIOS_FOR_EXTENDED_TRIPLETS
+      );
+      Services.prefs.setStringPref(
+        TRAILHEAD_CONFIG.EXTENDED_TRIPLETS_EXPERIMENT_PREF,
+        branch
+      );
+    }
+
+    // In order for ping centre to pick this up, it MUST contain a substring activity-stream
+    const experimentName = `activity-stream-extended-triplets`;
+    TelemetryEnvironment.setExperimentActive(experimentName, branch);
+
+    const state = { extendedTripletsInitialized: true };
+    // Disable the extended triplets for the "holdback" group.
+    if (branch === "holdback") {
+      state.showExtendedTriplets = false;
+    }
+    await this.setState(state);
+  }
+
   async setupTrailhead() {
     // Don't initialize
     if (
       this.state.trailheadInitialized ||
       !Services.prefs.getBoolPref(
         TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
         false
       )
@@ -968,16 +1035,17 @@ class _ASRouter {
       return;
     }
 
     const {
       experiment,
       interrupt,
       triplet,
     } = await this._generateTrailheadBranches();
+
     await this.setState({
       trailheadInitialized: true,
       trailheadInterrupt: interrupt,
       trailheadTriplet: triplet,
     });
 
     if (experiment) {
       // In order for ping centre to pick this up, it MUST contain a substring activity-stream
@@ -1435,43 +1503,21 @@ class _ASRouter {
       }
     });
     if (needsUpdate) {
       this._storage.set(impressionsString, impressions);
     }
     return impressions;
   }
 
-  async sendNextMessage(target, trigger) {
-    const msgs = this._getUnblockedMessages();
-    let message = null;
-    const previewMsgs = this.state.messages.filter(
-      item => item.provider === "preview"
-    );
-    // Always send preview messages when available
-    if (previewMsgs.length) {
-      [message] = previewMsgs;
-    } else {
-      message = await this._findMessage(msgs, trigger);
-    }
-
-    if (previewMsgs.length) {
-      // We don't want to cache preview messages, remove them after we selected the message to show
-      await this.setState(state => ({
-        lastMessageId: message.id,
-        messages: state.messages.filter(m => m.id !== message.id),
-      }));
-    } else {
-      await this.setState({ lastMessageId: message ? message.id : null });
-    }
-    await this._sendMessageToTarget(message, target, trigger);
-  }
-
-  handleMessageRequest({ triggerId, triggerParam, template }) {
+  handleMessageRequest({ triggerId, triggerParam, provider, template }) {
     const msgs = this._getUnblockedMessages().filter(m => {
+      if (provider && m.provider !== provider) {
+        return false;
+      }
       if (template && m.template !== template) {
         return false;
       }
       if (m.trigger && m.trigger.id !== triggerId) {
         return false;
       }
 
       return true;
@@ -1560,20 +1606,18 @@ class _ASRouter {
   // Ensure we switch to the Onboarding message after RTAMO addon was installed
   _updateOnboardingState() {
     let addonInstallObs = (subject, topic) => {
       Services.obs.removeObserver(
         addonInstallObs,
         "webextension-install-notify"
       );
       this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
-        type: "CLEAR_MESSAGE",
-        data: { id: "RETURN_TO_AMO_1" },
+        type: "CLEAR_INTERRUPT",
       });
-      this.blockMessageById("RETURN_TO_AMO_1");
     };
     Services.obs.addObserver(addonInstallObs, "webextension-install-notify");
   }
 
   _loadSnippetsWhitelistHosts() {
     let additionalHosts = [];
     const whitelistPrefValue = Services.prefs.getStringPref(
       SNIPPETS_ENDPOINT_WHITELIST,
@@ -1757,55 +1801,104 @@ class _ASRouter {
         break;
     }
   }
 
   dispatch(action, target) {
     this.onMessage({ data: action, target });
   }
 
+  async sendNewTabMessage(target, options = {}) {
+    const { endpoint } = options;
+    let message;
+
+    // Load preview endpoint for snippets if one is sent
+    if (endpoint) {
+      await this._addPreviewEndpoint(endpoint.url, target.portID);
+    }
+
+    // Load all messages
+    await this.loadMessagesFromAllProviders();
+
+    if (endpoint) {
+      message = await this.handleMessageRequest({ provider: "preview" });
+      // We don't want to cache preview messages, remove them after we selected the message to show
+      await this.setState(state => ({
+        lastMessageId: message ? message.id : null,
+        messages: message
+          ? state.messages.filter(m => m.id !== message.id)
+          : state.messages,
+      }));
+    } else {
+      // On new tab, send cards if they match; othwerise send a snippet
+      message = await this.handleMessageRequest({
+        provider: "onboarding",
+        template: "extended_triplets",
+      });
+
+      // Set up the experiment for extended triplets. It's done here because we
+      // only want to enroll users (for both control and holdback) if they meet
+      // the targeting criteria.
+      if (message) {
+        await this.setupExtendedTriplets();
+      }
+
+      // If no extended triplets message was returned, or the holdback experiment
+      // is active, show snippets instead
+      if (!message || !this.state.showExtendedTriplets) {
+        message = await this.handleMessageRequest({ provider: "snippets" });
+      }
+
+      await this.setState({ lastMessageId: message ? message.id : null });
+    }
+
+    await this._sendMessageToTarget(message, target);
+  }
+
+  async sendTriggerMessage(target, trigger) {
+    await this.loadMessagesFromAllProviders();
+
+    if (trigger.id === "firstRun") {
+      // On about welcome, set up trailhead experiments
+      if (!this.state.trailheadInitialized) {
+        Services.prefs.setBoolPref(
+          TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
+          true
+        );
+        await this.setupTrailhead();
+      }
+    }
+
+    const message = await this.handleMessageRequest({
+      triggerId: trigger.id,
+      triggerParam: trigger.param,
+    });
+
+    await this.setState({ lastMessageId: message ? message.id : null });
+    await this._sendMessageToTarget(message, target, trigger);
+  }
+
   /* eslint-disable complexity */
   async onMessage({ data: action, target }) {
     switch (action.type) {
       case "USER_ACTION":
         if (action.data.type in ra) {
           await this.handleUserAction({ data: action.data, target });
         }
         break;
-      case "SNIPPETS_REQUEST":
-      case "TRIGGER":
-        // Wait for our initial message loading to be done before responding to any UI requests
+      case "NEWTAB_MESSAGE_REQUEST":
         await this.waitForInitialized;
-        if (action.data && action.data.endpoint) {
-          await this._addPreviewEndpoint(
-            action.data.endpoint.url,
-            target.portID
-          );
-        }
-
-        // Special experiment intialization for trailhead
-        if (
-          action.data &&
-          action.data.trigger &&
-          action.data.trigger.id === "firstRun"
-        ) {
-          Services.prefs.setBoolPref(
-            TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
-            true
-          );
-          await this.setupTrailhead();
-        }
-
-        // Check if any updates are needed first
-        await this.loadMessagesFromAllProviders();
-        await this.sendNextMessage(
+        await this.sendNewTabMessage(target, action.data);
+        break;
+      case "TRIGGER":
+        await this.waitForInitialized;
+        await this.sendTriggerMessage(
           target,
-          (action.data && action.data.trigger) || {}
+          action.data && action.data.trigger
         );
-        break;
       case "BLOCK_MESSAGE_BY_ID":
         await this.blockMessageById(action.data.id);
         // Block the message but don't dismiss it in case the action taken has
         // another state that needs to be visible
         if (action.data.preventDismiss) {
           break;
         }
         this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
@@ -1821,21 +1914,16 @@ class _ASRouter {
         break;
       case "BLOCK_PROVIDER_BY_ID":
         await this.blockProviderById(action.data.id);
         this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
           type: "CLEAR_PROVIDER",
           data: { id: action.data.id },
         });
         break;
-      case "DISMISS_BUNDLE":
-        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
-          type: "CLEAR_BUNDLE",
-        });
-        break;
       case "BLOCK_BUNDLE":
         await this.blockMessageById(action.data.bundle.map(b => b.id));
         this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
           type: "CLEAR_BUNDLE",
         });
         break;
       case "UNBLOCK_MESSAGE_BY_ID":
         this.unblockMessageById(action.data.id);
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -1,139 +1,29 @@
 /* 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";
 /* globals Localization */
-const { FxAccountsConfig } = ChromeUtils.import(
-  "resource://gre/modules/FxAccountsConfig.jsm"
-);
 const { AttributionCode } = ChromeUtils.import(
   "resource:///modules/AttributionCode.jsm"
 );
 const { AddonRepository } = ChromeUtils.import(
   "resource://gre/modules/addons/AddonRepository.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const L10N = new Localization([
   "branding/brand.ftl",
   "browser/branding/brandings.ftl",
   "browser/branding/sync-brand.ftl",
   "browser/newtab/onboarding.ftl",
 ]);
 
-const ONBOARDING_MESSAGES = async () => [
-  {
-    id: "ONBOARDING_1",
-    template: "onboarding",
-    bundled: 3,
-    order: 2,
-    content: {
-      title: { string_id: "onboarding-private-browsing-title" },
-      text: { string_id: "onboarding-private-browsing-text" },
-      icon: "privatebrowsing",
-      primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
-        action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
-      },
-    },
-    trigger: { id: "showOnboarding" },
-  },
-  {
-    id: "ONBOARDING_2",
-    template: "onboarding",
-    bundled: 3,
-    order: 3,
-    content: {
-      title: { string_id: "onboarding-screenshots-title" },
-      text: { string_id: "onboarding-screenshots-text" },
-      icon: "screenshots",
-      primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
-        action: {
-          type: "OPEN_URL",
-          data: {
-            args: "https://screenshots.firefox.com/#tour",
-            where: "tabshifted",
-          },
-        },
-      },
-    },
-    trigger: { id: "showOnboarding" },
-  },
-  {
-    id: "ONBOARDING_3",
-    template: "onboarding",
-    bundled: 3,
-    order: 1,
-    content: {
-      title: { string_id: "onboarding-addons-title" },
-      text: { string_id: "onboarding-addons-text" },
-      icon: "addons",
-      primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
-        action: {
-          type: "OPEN_ABOUT_PAGE",
-          data: { args: "addons" },
-        },
-      },
-    },
-    targeting:
-      "trailheadInterrupt == 'control' && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
-    trigger: { id: "showOnboarding" },
-  },
-  {
-    id: "ONBOARDING_4",
-    template: "onboarding",
-    bundled: 3,
-    order: 1,
-    content: {
-      title: { string_id: "onboarding-ghostery-title" },
-      text: { string_id: "onboarding-ghostery-text" },
-      icon: "gift",
-      primary_button: {
-        label: { string_id: "onboarding-button-label-try-now" },
-        action: {
-          type: "OPEN_URL",
-          data: {
-            args: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/",
-            where: "tabshifted",
-          },
-        },
-      },
-    },
-    targeting:
-      "trailheadInterrupt == 'control' && providerCohorts.onboarding == 'ghostery'",
-    trigger: { id: "showOnboarding" },
-  },
-  {
-    id: "ONBOARDING_5",
-    template: "onboarding",
-    bundled: 3,
-    order: 4,
-    content: {
-      title: { string_id: "onboarding-fxa-title" },
-      text: { string_id: "onboarding-fxa-text" },
-      icon: "sync",
-      primary_button: {
-        label: { string_id: "onboarding-button-label-get-started" },
-        action: {
-          type: "OPEN_URL",
-          data: {
-            args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"),
-            where: "tabshifted",
-          },
-        },
-      },
-    },
-    targeting:
-      "trailheadInterrupt == 'control' && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
-    trigger: { id: "showOnboarding" },
-  },
+const ONBOARDING_MESSAGES = () => [
   {
     id: "TRAILHEAD_1",
     template: "trailhead",
     targeting: "trailheadInterrupt == 'join'",
     trigger: { id: "firstRun" },
     includeBundle: {
       length: 3,
       template: "onboarding",
@@ -204,16 +94,30 @@ const ONBOARDING_MESSAGES = async () => 
   },
   {
     id: "TRAILHEAD_4",
     template: "trailhead",
     targeting: "trailheadInterrupt == 'nofirstrun'",
     trigger: { id: "firstRun" },
   },
   {
+    id: "EXTENDED_TRIPLETS_1",
+    template: "extended_triplets",
+    campaign: "firstrun_triplets",
+    targeting:
+      "trailheadTriplet && ((currentDate|date - profileAgeCreated) / 86400000) < 7",
+    includeBundle: {
+      length: 3,
+      template: "onboarding",
+      trigger: { id: "showOnboarding" },
+    },
+    frequency: { lifetime: 20 },
+    utm_term: "trailhead-cards",
+  },
+  {
     id: "TRAILHEAD_CARD_1",
     template: "onboarding",
     bundled: 3,
     order: 2,
     content: {
       title: { string_id: "onboarding-tracking-protection-title2" },
       text: { string_id: "onboarding-tracking-protection-text2" },
       icon: "tracking",
@@ -236,17 +140,17 @@ const ONBOARDING_MESSAGES = async () => 
     },
     targeting: "trailheadTriplet == 'privacy'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_2",
     template: "onboarding",
     bundled: 3,
-    order: 2,
+    order: 1,
     content: {
       title: { string_id: "onboarding-data-sync-title" },
       text: { string_id: "onboarding-data-sync-text2" },
       icon: "devices",
       primary_button: {
         label: { string_id: "onboarding-data-sync-button2" },
         action: {
           type: "OPEN_URL",
@@ -261,17 +165,17 @@ const ONBOARDING_MESSAGES = async () => 
     },
     targeting: "trailheadTriplet == 'supercharge'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_3",
     template: "onboarding",
     bundled: 3,
-    order: 3,
+    order: 2,
     content: {
       title: { string_id: "onboarding-firefox-monitor-title" },
       text: { string_id: "onboarding-firefox-monitor-text" },
       icon: "ffmonitor",
       primary_button: {
         label: { string_id: "onboarding-firefox-monitor-button" },
         action: {
           type: "OPEN_URL",
@@ -318,17 +222,17 @@ const ONBOARDING_MESSAGES = async () => 
     },
     targeting: "trailheadTriplet == 'payoff'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "TRAILHEAD_CARD_6",
     template: "onboarding",
     bundled: 3,
-    order: 1,
+    order: 3,
     content: {
       title: { string_id: "onboarding-mobile-phone-title" },
       text: { string_id: "onboarding-mobile-phone-text" },
       icon: "mobile",
       primary_button: {
         label: { string_id: "onboarding-mobile-phone-button" },
         action: {
           type: "OPEN_URL",
@@ -431,17 +335,23 @@ const ONBOARDING_MESSAGES = async () => 
       },
     },
     targeting: "trailheadTriplet == 'payoff'",
     trigger: { id: "showOnboarding" },
   },
   {
     id: "FXA_1",
     template: "fxa_overlay",
+    content: {},
     trigger: { id: "firstRun" },
+    includeBundle: {
+      length: 3,
+      template: "onboarding",
+      trigger: { id: "showOnboarding" },
+    },
   },
   {
     id: "RETURN_TO_AMO_1",
     template: "return_to_amo_overlay",
     content: {
       header: { string_id: "onboarding-welcome-header" },
       title: { string_id: "return-to-amo-sub-header" },
       addon_icon: null,
@@ -456,16 +366,21 @@ const ONBOARDING_MESSAGES = async () => 
           type: "INSTALL_ADDON_FROM_URL",
           data: { url: null, telemetrySource: "rtamo" },
         },
       },
       secondary_button: {
         label: { string_id: "return-to-amo-get-started-button" },
       },
     },
+    includeBundle: {
+      length: 3,
+      template: "onboarding",
+      trigger: { id: "showOnboarding" },
+    },
     targeting:
       "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
     trigger: { id: "firstRun" },
   },
 ];
 
 const OnboardingMessageProvider = {
   async getExtraAttributes() {
--- a/browser/components/newtab/test/browser/browser_aboutwelcome.js
+++ b/browser/components/newtab/test/browser/browser_aboutwelcome.js
@@ -80,19 +80,19 @@ add_task(async function test_trailhead_b
     ]
   );
 
   await test_trailhead_branch(
     "sync-supercharge",
     // Expected selectors:
     [
       ".trailhead.syncCohort",
-      "button[data-l10n-id=onboarding-mobile-phone-button]",
       "button[data-l10n-id=onboarding-data-sync-button2]",
       "button[data-l10n-id=onboarding-firefox-monitor-button]",
+      "button[data-l10n-id=onboarding-mobile-phone-button]",
     ]
   );
 
   await test_trailhead_branch(
     "cards-multidevice",
     // Expected selectors:
     [
       "button[data-l10n-id=onboarding-mobile-phone-button]",
--- a/browser/components/newtab/test/browser/browser_onboarding_rtamo.js
+++ b/browser/components/newtab/test/browser/browser_onboarding_rtamo.js
@@ -78,19 +78,16 @@ add_task(async () => {
       "You shouldn't be able to see newtabpage content"
     );
     for (let selector of [
       // ReturnToAMO elements
       ".ReturnToAMOOverlay",
       ".ReturnToAMOContainer",
       ".ReturnToAMOAddonContents",
       ".ReturnToAMOIcon",
-      // Regular onboarding cards
-      ".onboardingMessageContainer",
-      ".onboardingMessage",
     ]) {
       ok(content.document.querySelector(selector), `Should render ${selector}`);
     }
     // Make sure strings are properly shown
     Assert.equal(
       content.document.querySelector(".ReturnToAMOText").innerText,
       "Now let’s get you mochitest_name."
     );
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -28,19 +28,16 @@ import { SnippetsTestMessageProvider } f
 
 const MESSAGE_PROVIDER_PREF_NAME =
   "browser.newtabpage.activity-stream.asrouter.providers.snippets";
 const FAKE_PROVIDERS = [
   FAKE_LOCAL_PROVIDER,
   FAKE_REMOTE_PROVIDER,
   FAKE_REMOTE_SETTINGS_PROVIDER,
 ];
-const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(
-  message => message.id
-);
 const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
 const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
 const FAKE_RESPONSE_HEADERS = { get() {} };
 
 // Creates a message object that looks like messages returned by
 // RemotePageManager listeners
 function fakeAsyncMessage(action) {
   return { data: action, target: new FakeRemotePageManager() };
@@ -786,16 +783,64 @@ describe("ASRouter", () => {
         .withArgs("cfrAddons")
         .returns(false);
       Router._updateMessageProviders();
       assert.equal(Router.state.providers.length, 0);
     });
   });
 
   describe("#handleMessageRequest", () => {
+    it("should not return a blocked message", async () => {
+      // Block all messages except the first
+      await Router.setState(() => ({
+        messages: [
+          { id: "foo", provider: "snippets" },
+          { id: "bar", provider: "snippets" },
+        ],
+        messageBlockList: ["foo"],
+      }));
+      const result = await Router.handleMessageRequest({
+        provider: "snippets",
+      });
+      assert.equal(result.id, "bar");
+    });
+    it("should not return a message from a blocked campaign", async () => {
+      // Block all messages except the first
+      await Router.setState(() => ({
+        messages: [
+          { id: "foo", provider: "snippets", campaign: "foocampaign" },
+          { id: "bar", provider: "snippets" },
+        ],
+        messageBlockList: ["foocampaign"],
+      }));
+
+      const result = await Router.handleMessageRequest({
+        provider: "snippets",
+      });
+
+      assert.equal(result.id, "bar");
+    });
+    it("should not return a message from a blocked provider", async () => {
+      // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
+      // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
+      await Router.setState(() => ({
+        providerBlockList: ["snippets"],
+      }));
+
+      await Router.setState(() => ({
+        messages: [{ id: "foo", provider: "snippets" }],
+        messageBlockList: ["foocampaign"],
+      }));
+
+      const result = await Router.handleMessageRequest({
+        provider: "snippets",
+      });
+
+      assert.isNull(result);
+    });
     it("should get unblocked messages that match the trigger", async () => {
       const message1 = {
         id: "1",
         campaign: "foocampaign",
         trigger: { id: "foo" },
       };
       const message2 = {
         id: "2",
@@ -829,97 +874,23 @@ describe("ASRouter", () => {
 
       const result = Router.handleMessageRequest({
         triggerId: "foo",
         template: "badge",
       });
 
       assert.deepEqual(result, message1);
     });
-    it("should get unblocked messages that match trigger and template", async () => {
-      const message1 = {
-        id: "1",
-        campaign: "foocampaign",
-        template: "badge",
-        trigger: { id: "foo" },
-      };
-      const message2 = {
-        id: "2",
-        campaign: "foocampaign",
-        template: "snippet",
-        trigger: { id: "foo" },
-      };
-      await Router.setState({ messages: [message2, message1] });
-      // Just return the first message provided as arg
-      sandbox.stub(Router, "_findMessage").callsFake(messages => messages[0]);
-
-      const result = Router.handleMessageRequest({
-        triggerId: "foo",
-        template: "badge",
-      });
-
-      assert.deepEqual(result, message1);
-    });
     it("should have messageImpressions in the message context", () => {
       assert.propertyVal(
         Router._getMessagesContext(),
         "messageImpressions",
         Router.state.messageImpressions
       );
     });
-  });
-
-  describe("blocking", () => {
-    it("should not return a blocked message", async () => {
-      // Block all messages except the first
-      await Router.setState(() => ({
-        messageBlockList: ALL_MESSAGE_IDS.slice(1),
-      }));
-      const targetStub = { sendAsyncMessage: sandbox.stub() };
-
-      await Router.sendNextMessage(targetStub);
-
-      assert.calledOnce(targetStub.sendAsyncMessage);
-      assert.equal(Router.state.lastMessageId, ALL_MESSAGE_IDS[0]);
-    });
-    it("should not return a message from a blocked campaign", async () => {
-      // Block all messages except the first
-      await Router.setState(() => ({
-        messages: [{ id: "foo", campaign: "foocampaign" }, { id: "bar" }],
-        messageBlockList: ["foocampaign"],
-      }));
-      const targetStub = { sendAsyncMessage: sandbox.stub() };
-
-      await Router.sendNextMessage(targetStub);
-
-      assert.calledOnce(targetStub.sendAsyncMessage);
-      assert.equal(Router.state.lastMessageId, "bar");
-    });
-    it("should not return a message from a blocked provider", async () => {
-      // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
-      // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
-      await Router.setState(() => ({
-        providerBlockList: [FAKE_LOCAL_PROVIDER.id],
-      }));
-      const targetStub = { sendAsyncMessage: sandbox.stub() };
-
-      await Router.sendNextMessage(targetStub);
-
-      assert.calledOnce(targetStub.sendAsyncMessage);
-      assert.equal(Router.state.lastMessageId, FAKE_REMOTE_MESSAGES[0].id);
-    });
-    it("should not return a message if all messages are blocked", async () => {
-      await Router.setState(() => ({ messageBlockList: ALL_MESSAGE_IDS }));
-      const targetStub = { sendAsyncMessage: sandbox.stub() };
-
-      await Router.sendNextMessage(targetStub);
-
-      assert.calledOnce(targetStub.sendAsyncMessage);
-      assert.equal(Router.state.lastMessageId, null);
-    });
     it("should forward trigger param info", async () => {
       const trigger = { triggerId: "foo", triggerParam: "bar" };
       const message1 = {
         id: "1",
         campaign: "foocampaign",
         trigger: { id: "foo" },
       };
       const message2 = {
@@ -977,34 +948,33 @@ describe("ASRouter", () => {
         Router._storage.set,
         "previousSessionEnd",
         sinon.match.number
       );
     });
   });
 
   describe("onMessage", () => {
-    describe("#onMessage: SNIPPETS_REQUEST", () => {
+    describe("#onMessage: NEWTAB_MESSAGE_REQUEST", () => {
       it("should set state.lastMessageId to a message id", async () => {
-        await Router.onMessage(fakeAsyncMessage({ type: "SNIPPETS_REQUEST" }));
+        await Router.setState({
+          messages: [{ id: "foo", provider: "snippets" }],
+        });
+        await Router.onMessage(
+          fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" })
+        );
 
-        assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
+        assert.equal(Router.state.lastMessageId, "foo");
       });
       it("should send a message back to the to the target", async () => {
         // force the only message to be a regular message so getRandomItemFromArray picks it
         await Router.setState({
-          messages: [
-            {
-              id: "foo",
-              template: "simple_template",
-              content: { title: "Foo", body: "Foo123" },
-            },
-          ],
+          messages: [{ id: "foo", provider: "snippets" }],
         });
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         await Router.onMessage(msg);
         const [currentMessage] = Router.state.messages.filter(
           message => message.id === Router.state.lastMessageId
         );
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "SET_MESSAGE", data: currentMessage }
@@ -1012,23 +982,24 @@ describe("ASRouter", () => {
       });
       it("should send a message back to the to the target if there is a bundle, too", async () => {
         // force the only message to be a bundled message so getRandomItemFromArray picks it
         sandbox.stub(Router, "_findProvider").returns(null);
         await Router.setState({
           messages: [
             {
               id: "foo1",
+              provider: "snippets",
               template: "simple_template",
               bundled: 1,
               content: { title: "Foo1", body: "Foo123-1" },
             },
           ],
         });
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         await Router.onMessage(msg);
         const [currentMessage] = Router.state.messages.filter(
           message => message.id === Router.state.lastMessageId
         );
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME
         );
@@ -1041,30 +1012,32 @@ describe("ASRouter", () => {
           currentMessage.content
         );
       });
       it("should properly order the message's bundle if specified", async () => {
         // force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
         sandbox.stub(Router, "_findProvider").returns(null);
         const firstMessage = {
           id: "foo2",
+          provider: "snippets",
           template: "simple_template",
           bundled: 2,
           order: 1,
           content: { title: "Foo2", body: "Foo123-2" },
         };
         const secondMessage = {
           id: "foo1",
+          provider: "snippets",
           template: "simple_template",
           bundled: 2,
           order: 2,
           content: { title: "Foo1", body: "Foo123-1" },
         };
         await Router.setState({ messages: [secondMessage, firstMessage] });
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         await Router.onMessage(msg);
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME
         );
         assert.equal(
           msg.target.sendAsyncMessage.firstCall.args[1].type,
           "SET_BUNDLED_MESSAGES"
@@ -1117,45 +1090,46 @@ describe("ASRouter", () => {
         assert.equal(result.extraTemplateStrings.header, "header");
       });
       it("should send a CLEAR_ALL message if no bundle available", async () => {
         // force the only message to be a bundled message that needs 2 messages in the bundle
         await Router.setState({
           messages: [
             {
               id: "foo1",
+              provider: "snippets",
               template: "simple_template",
               bundled: 2,
               content: { title: "Foo1", body: "Foo123-1" },
             },
           ],
         });
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         await Router.onMessage(msg);
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "CLEAR_ALL" }
         );
       });
       it("should send a CLEAR_ALL message if no messages are available", async () => {
         await Router.setState({ messages: [] });
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         await Router.onMessage(msg);
 
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "CLEAR_ALL" }
         );
       });
-      it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
+      it("should make a request to the provided endpoint on NEWTAB_MESSAGE_REQUEST", async () => {
         const url = "https://snippets-admin.mozilla.org/foo";
         const msg = fakeAsyncMessage({
-          type: "SNIPPETS_REQUEST",
+          type: "NEWTAB_MESSAGE_REQUEST",
           data: { endpoint: { url } },
         });
         await Router.onMessage(msg);
 
         assert.calledWith(global.fetch, url);
         assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
       });
       it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
@@ -1167,49 +1141,124 @@ describe("ASRouter", () => {
         await Router.onMessage(msg);
 
         assert.calledWith(global.fetch, url);
         assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
       });
       it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
         const url = "https://snippets-admin.mozilla.org/foo";
         const msg = fakeAsyncMessage({
-          type: "SNIPPETS_REQUEST",
+          type: "NEWTAB_MESSAGE_REQUEST",
           data: { endpoint: { url } },
         });
         await Router.onMessage(msg);
 
         assert.calledWithExactly(
           Router.dispatchToAS,
           ac.OnlyToOneContent(
             { type: "SNIPPETS_PREVIEW_MODE" },
             msg.target.portID
           )
         );
       });
       it("should not add a url that is not from a whitelisted host", async () => {
         const url = "https://mozilla.org";
         const msg = fakeAsyncMessage({
-          type: "SNIPPETS_REQUEST",
+          type: "NEWTAB_MESSAGE_REQUEST",
           data: { endpoint: { url } },
         });
         await Router.onMessage(msg);
 
         assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
       });
       it("should reject bad urls", async () => {
         const url = "foo";
         const msg = fakeAsyncMessage({
-          type: "SNIPPETS_REQUEST",
+          type: "NEWTAB_MESSAGE_REQUEST",
           data: { endpoint: { url } },
         });
         await Router.onMessage(msg);
 
         assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
       });
+      it("should handle onboarding message provider", async () => {
+        const handleMessageRequestStub = sandbox.stub(
+          Router,
+          "handleMessageRequest"
+        );
+        handleMessageRequestStub
+          .withArgs({
+            provider: "onboarding",
+            template: "extended_triplets",
+          })
+          .resolves({ id: "foo" });
+        const spy = sandbox.spy(Router, "setupExtendedTriplets");
+        const msg = fakeAsyncMessage({
+          type: "NEWTAB_MESSAGE_REQUEST",
+          data: {},
+        });
+        await Router.onMessage(msg);
+
+        assert.calledOnce(spy);
+      });
+      it("should fallback to snippets if one was assigned to the holdback experiment", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = holdback branch
+        const handleMessageRequestStub = sandbox.stub(
+          Router,
+          "handleMessageRequest"
+        );
+        handleMessageRequestStub
+          .withArgs({
+            provider: "onboarding",
+            template: "extended_triplets",
+          })
+          .resolves({ id: "foo" });
+        const msg = fakeAsyncMessage({
+          type: "NEWTAB_MESSAGE_REQUEST",
+          data: {},
+        });
+        await Router.onMessage(msg);
+
+        assert.calledTwice(handleMessageRequestStub);
+        assert.calledWithExactly(handleMessageRequestStub, {
+          provider: "onboarding",
+          template: "extended_triplets",
+        });
+        assert.calledWithExactly(handleMessageRequestStub, {
+          provider: "snippets",
+        });
+      });
+      it("should fallback to snippets if onboarding message provider returned none", async () => {
+        const handleMessageRequestStub = sandbox.stub(
+          Router,
+          "handleMessageRequest"
+        );
+        handleMessageRequestStub
+          .withArgs({
+            provider: "onboarding",
+            template: "extended_triplets",
+          })
+          .resolves(null);
+        const spy = sandbox.spy(Router, "setupExtendedTriplets");
+        const msg = fakeAsyncMessage({
+          type: "NEWTAB_MESSAGE_REQUEST",
+          data: {},
+        });
+        await Router.onMessage(msg);
+
+        assert.notCalled(spy);
+        assert.calledTwice(handleMessageRequestStub);
+        assert.calledWithExactly(handleMessageRequestStub, {
+          provider: "onboarding",
+          template: "extended_triplets",
+        });
+        assert.calledWithExactly(handleMessageRequestStub, {
+          provider: "snippets",
+        });
+      });
     });
 
     describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
       it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
         await Router.setState({ lastMessageId: "foo" });
         const msg = fakeAsyncMessage({
           type: "BLOCK_MESSAGE_BY_ID",
           data: { id: "foo" },
@@ -1279,33 +1328,16 @@ describe("ASRouter", () => {
         assert.calledWith(
           channel.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "CLEAR_PROVIDER", data: { id: "bar" } }
         );
       });
     });
 
-    describe("#onMessage: DISMISS_BUNDLE", () => {
-      it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
-        await Router.setState({ lastMessageId: "foo" });
-        const msg = fakeAsyncMessage({
-          type: "DISMISS_BUNDLE",
-          data: { bundle: FAKE_BUNDLE },
-        });
-        await Router.onMessage(msg);
-
-        assert.calledWith(
-          channel.sendAsyncMessage,
-          PARENT_TO_CHILD_MESSAGE_NAME,
-          { type: "CLEAR_BUNDLE" }
-        );
-      });
-    });
-
     describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
       it("should remove the id from the messageBlockList", async () => {
         await Router.onMessage(
           fakeAsyncMessage({ type: "BLOCK_MESSAGE_BY_ID", data: { id: "foo" } })
         );
         assert.isTrue(Router.state.messageBlockList.includes("foo"));
         await Router.onMessage(
           fakeAsyncMessage({
@@ -1430,35 +1462,36 @@ describe("ASRouter", () => {
             userPrefs: ASRouterPreferences.getAllUserPreferences(),
             targetingParameters: {},
             errors: Router.errors,
           }),
         });
       });
     });
 
-    describe("#onMessage: SNIPPETS_REQUEST", () => {
-      it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
-        sandbox.stub(Router, "sendNextMessage").resolves();
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+    describe("#onMessage: NEWTAB_MESSAGE_REQUEST", () => {
+      it("should call sendNewTabMessage on NEWTAB_MESSAGE_REQUEST", async () => {
+        sandbox.stub(Router, "sendNewTabMessage").resolves();
+        const data = { endpoint: "foo" };
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST", data });
 
         await Router.onMessage(msg);
 
-        assert.calledOnce(Router.sendNextMessage);
+        assert.calledOnce(Router.sendNewTabMessage);
         assert.calledWithExactly(
-          Router.sendNextMessage,
+          Router.sendNewTabMessage,
           sinon.match.instanceOf(FakeRemotePageManager),
-          {}
+          data
         );
       });
       it("should return the preview message if that's available and remove it from Router.state", async () => {
         const expectedObj = { provider: "preview" };
         Router.setState({ messages: [expectedObj] });
 
-        await Router.sendNextMessage(channel);
+        await Router.sendNewTabMessage(channel, { endpoint: "foo.com" });
 
         assert.calledWith(
           channel.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "SET_MESSAGE", data: expectedObj }
         );
         assert.isUndefined(
           Router.state.messages.find(m => m.provider === "preview")
@@ -1522,36 +1555,37 @@ describe("ASRouter", () => {
         };
         assert.calledWith(
           msg.target.sendAsyncMessage,
           PARENT_TO_CHILD_MESSAGE_NAME,
           { type: "SET_BUNDLED_MESSAGES", data: expectedObj }
         );
       });
       it("should get the bundle and send the message if the message has a bundle", async () => {
-        sandbox.stub(Router, "sendNextMessage").resolves();
-        const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+        sandbox.stub(Router, "sendNewTabMessage").resolves();
+        const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
         msg.bundled = 2; // force this message to want to be bundled
         await Router.onMessage(msg);
-        assert.calledOnce(Router.sendNextMessage);
+        assert.calledOnce(Router.sendNewTabMessage);
       });
     });
 
     describe("#onMessage: TRIGGER", () => {
       it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
         sandbox.stub(Router, "_findMessage").resolves();
         const msg = fakeAsyncMessage({
           type: "TRIGGER",
           data: { trigger: { id: "firstRun" } },
         });
         await Router.onMessage(msg);
 
         assert.calledOnce(Router._findMessage);
         assert.deepEqual(Router._findMessage.firstCall.args[1], {
           id: "firstRun",
+          param: undefined,
         });
       });
       it("consider the trigger when picking a message", async () => {
         const messages = [
           {
             id: "foo1",
             template: "simple_template",
             bundled: 1,
@@ -1837,17 +1871,16 @@ describe("ASRouter", () => {
           "foo.com",
           "foo"
         );
       });
 
       it("should add/remove observers for `webextension-install-notify`", async () => {
         sandbox.spy(global.Services.obs, "addObserver");
         sandbox.spy(global.Services.obs, "removeObserver");
-        sandbox.spy(Router, "blockMessageById");
 
         sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
         const msg = fakeExecuteUserAction({
           type: "INSTALL_ADDON_FROM_URL",
           data: { url: "foo.com" },
         });
 
         await Router.onMessage(msg);
@@ -1855,18 +1888,16 @@ describe("ASRouter", () => {
         assert.calledOnce(global.Services.obs.addObserver);
 
         const [cb] = global.Services.obs.addObserver.firstCall.args;
 
         cb();
 
         assert.calledOnce(global.Services.obs.removeObserver);
         assert.calledOnce(channel.sendAsyncMessage);
-        assert.calledOnce(Router.blockMessageById);
-        assert.calledWithExactly(Router.blockMessageById, "RETURN_TO_AMO_1");
       });
     });
 
     describe("#onMessage: PIN_CURRENT_TAB", () => {
       it("should call pin tab with the selectedTab", async () => {
         const msg = fakeExecuteUserAction({ type: "PIN_CURRENT_TAB" });
         const { gBrowser, ConfirmationHint } = msg.target.browser.ownerGlobal;
 
@@ -2545,61 +2576,73 @@ describe("ASRouter", () => {
   });
 
   describe("handle targeting errors", () => {
     it("should dispatch an event when a targeting expression throws an error", async () => {
       sandbox
         .stub(global.FilterExpressions, "eval")
         .returns(Promise.reject(new Error("fake error")));
       await Router.setState({
-        messages: [{ id: "foo", targeting: "foo2.[[(" }],
+        messages: [{ id: "foo", provider: "snippets", targeting: "foo2.[[(" }],
       });
-      const msg = fakeAsyncMessage({ type: "SNIPPETS_REQUEST" });
+      const msg = fakeAsyncMessage({ type: "NEWTAB_MESSAGE_REQUEST" });
       dispatchStub.reset();
 
       await Router.onMessage(msg);
 
       assert.calledOnce(dispatchStub);
       const [action] = dispatchStub.firstCall.args;
       assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
       assert.equal(action.data.message_id, "foo");
     });
   });
 
   describe("trailhead", () => {
-    it("should call .setupTrailhead on init", async () => {
+    it("should call .setFirstRunStateFromPref and initialize trailhead branches on init", async () => {
+      sandbox.spy(Router, "setFirstRunStateFromPref");
+      getStringPrefStub
+        .withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF)
+        .returns("join-supercharge");
+
+      await Router.init(channel, createFakeStorage(), dispatchStub);
+
+      assert.calledOnce(Router.setFirstRunStateFromPref);
+      assert.equal(Router.state.trailheadInterrupt, "join");
+      assert.equal(Router.state.trailheadTriplet, "supercharge");
+    });
+    it.skip("should call .setupTrailhead on init", async () => {
       sandbox.spy(Router, "setupTrailhead");
       sandbox
         .stub(Router, "_generateTrailheadBranches")
         .resolves({ experiment: "", interrupt: "join", triplet: "privacy" });
       sandbox
         .stub(global.Services.prefs, "getBoolPref")
         .withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF)
         .returns(true);
 
       await Router.init(channel, createFakeStorage(), dispatchStub);
 
       assert.calledOnce(Router.setupTrailhead);
       assert.propertyVal(Router.state, "trailheadInitialized", true);
     });
-    it("should call .setupTrailhead on init but return early if the DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
+    it.skip("should call .setupTrailhead on init but return early if the DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
       sandbox.spy(Router, "setupTrailhead");
       sandbox.spy(Router, "_generateTrailheadBranches");
       sandbox
         .stub(global.Services.prefs, "getBoolPref")
         .withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF)
         .returns(false);
 
       await Router.init(channel, createFakeStorage(), dispatchStub);
 
       assert.calledOnce(Router.setupTrailhead);
       assert.notCalled(Router._generateTrailheadBranches);
       assert.propertyVal(Router.state, "trailheadInitialized", false);
     });
-    it("should call .setupTrailhead and set the DID_SEE_ABOUT_WELCOME_PREF on a firstRun TRIGGER message", async () => {
+    it("should call .setupTrailhead and set the DID_SEE_ABOUT_WELCOME_PREF on a firstRun message", async () => {
       sandbox.spy(Router, "setupTrailhead");
       const msg = fakeAsyncMessage({
         type: "TRIGGER",
         data: { trigger: { id: "firstRun" } },
       });
       await Router.onMessage(msg);
 
       assert.calledOnce(Router.setupTrailhead);
@@ -2857,16 +2900,85 @@ describe("ASRouter", () => {
           .get(() => "en-US");
         checkReturnValue({
           experiment: "",
           interrupt: "join",
           triplet: "supercharge",
         });
       });
     });
+
+    describe(".setupExtendedTriplets", () => {
+      let setStringPrefStub;
+      let setExperimentActiveStub;
+
+      beforeEach(() => {
+        setStringPrefStub = sandbox.stub(
+          global.Services.prefs,
+          "setStringPref"
+        );
+        setExperimentActiveStub = sandbox.stub(
+          global.TelemetryEnvironment,
+          "setExperimentActive"
+        );
+      });
+
+      it("should generates a control branch configuration and update Router.state", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(0); // 0 = control branch
+
+        await Router.setupExtendedTriplets();
+
+        assert.propertyVal(Router.state, "extendedTripletsInitialized", true);
+        assert.propertyVal(Router.state, "showExtendedTriplets", true);
+        assert.calledWith(
+          setStringPrefStub,
+          TRAILHEAD_CONFIG.EXTENDED_TRIPLETS_EXPERIMENT_PREF,
+          "control"
+        );
+        assert.calledWith(
+          setExperimentActiveStub,
+          "activity-stream-extended-triplets",
+          "control"
+        );
+      });
+      it("should generates a test branch configuration and update Router.state", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = holdback branch
+
+        await Router.setupExtendedTriplets();
+
+        assert.propertyVal(Router.state, "extendedTripletsInitialized", true);
+        assert.propertyVal(Router.state, "showExtendedTriplets", false);
+        assert.calledWith(
+          setStringPrefStub,
+          TRAILHEAD_CONFIG.EXTENDED_TRIPLETS_EXPERIMENT_PREF,
+          "holdback"
+        );
+        assert.calledWith(
+          setExperimentActiveStub,
+          "activity-stream-extended-triplets",
+          "holdback"
+        );
+      });
+      it("should reuse the existing branch if it's already defined", async () => {
+        getStringPrefStub.returns("control");
+
+        await Router.setupExtendedTriplets();
+
+        assert.notCalled(setStringPrefStub);
+      });
+      it("should only run once", async () => {
+        sandbox.spy(Router, "setState");
+
+        await Router.setupExtendedTriplets();
+        await Router.setupExtendedTriplets();
+        await Router.setupExtendedTriplets();
+
+        assert.calledOnce(Router.setState);
+      });
+    });
   });
 
   describe("chooseBranch", () => {
     it("should call .ratioSample with the second value in each branch and return one of the first values", async () => {
       sandbox.stub(global.Sampling, "ratioSample").resolves(0);
       const result = await chooseBranch("bleep", [["foo", 14], ["bar", 42]]);
 
       assert.calledWith(global.Sampling.ratioSample, "bleep", [14, 42]);
--- a/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
@@ -16,31 +16,16 @@ const FAKE_NEWSLETTER_SNIPPET = FAKE_LOC
   msg => msg.id === "newsletter"
 );
 const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
 const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
   msg => msg.id === "belowsearch"
 );
 
 FAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: "fakeprovider" });
-const FAKE_BUNDLED_MESSAGE = {
-  bundle: [
-    {
-      id: "foo",
-      template: "onboarding",
-      content: {
-        title: "Foo",
-        primary_button: { label: "Bar" },
-        text: "Foo123",
-      },
-    },
-  ],
-  extraTemplateStrings: {},
-  template: "onboarding",
-};
 
 describe("ASRouterUtils", () => {
   let global;
   let sandbox;
   let fakeSendAsyncMessage;
   beforeEach(() => {
     global = new GlobalOverrider();
     sandbox = sinon.createSandbox();
@@ -80,16 +65,21 @@ describe("ASRouterUISurface", () => {
       ok: true,
       status: 200,
       json: () => Promise.resolve({}),
     });
     fakeDocument = {
       location: { href: "" },
       _listeners: new Set(),
       _visibilityState: "hidden",
+      head: {
+        appendChild(el) {
+          return el;
+        },
+      },
       get visibilityState() {
         return this._visibilityState;
       },
       set visibilityState(value) {
         if (this._visibilityState === value) {
           return;
         }
         this._visibilityState = value;
@@ -100,17 +90,25 @@ describe("ASRouterUISurface", () => {
       },
       removeEventListener(event, listener) {
         this._listeners.delete(listener);
       },
       get body() {
         return document.createElement("body");
       },
       getElementById(id) {
-        return id === "header-asrouter-container" ? headerPortal : footerPortal;
+        switch (id) {
+          case "header-asrouter-container":
+            return headerPortal;
+          default:
+            return footerPortal;
+        }
+      },
+      createElement(tag) {
+        return document.createElement(tag);
       },
     };
     globalO = new GlobalOverrider();
     globalO.set({
       RPMAddMessageListener: sandbox.stub(),
       RPMRemoveMessageListener: sandbox.stub(),
       RPMSendAsyncMessage: sandbox.stub(),
     });
@@ -147,21 +145,16 @@ describe("ASRouterUISurface", () => {
     assert.isTrue(wrapper.find("SubmitFormSnippet").exists());
     assert.propertyVal(
       wrapper.find("SubmitFormSnippet").props(),
       "form_method",
       "GET"
     );
   });
 
-  it("should render the component if a bundle of messages is defined", () => {
-    wrapper.setState({ bundle: FAKE_BUNDLED_MESSAGE });
-    assert.isTrue(wrapper.exists());
-  });
-
   it("should render a preview banner if message provider is preview", () => {
     wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } });
     assert.isTrue(wrapper.find(".snippets-preview-banner").exists());
   });
 
   it("should not render a preview banner if message provider is not preview", () => {
     wrapper.setState({ message: FAKE_MESSAGE });
     assert.isFalse(wrapper.find(".snippets-preview-banner").exists());
@@ -175,20 +168,23 @@ describe("ASRouterUISurface", () => {
 
   it("should not render a SimpleBelowSearchSnippet in a portal", () => {
     wrapper.setState({ message: FAKE_BELOW_SEARCH_SNIPPET });
     assert.equal(headerPortal.childElementCount, 0);
     assert.equal(footerPortal.childElementCount, 0);
   });
 
   it("should render a trailhead message in the header portal", async () => {
+    // wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);
     const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
       msg => msg.template === "trailhead"
     );
+
     wrapper.setState({ message });
+
     assert.isTrue(headerPortal.childElementCount > 0);
     assert.equal(footerPortal.childElementCount, 0);
   });
 
   it("should dispatch an event to select the correct theme", () => {
     const stub = sandbox.stub(window, "dispatchEvent");
     sandbox
       .stub(ASRouterUtils, "getPreviewEndpoint")
@@ -372,29 +368,16 @@ describe("ASRouterUISurface", () => {
       assert.propertyVal(payload, "event", "IMPRESSION");
       assert.propertyVal(
         payload,
         "action",
         `${FAKE_MESSAGE.provider}_user_event`
       );
       assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR");
     });
-
-    it("should call .sendTelemetry with the right message data when a bundle is dismissed", () => {
-      wrapper.instance().dismissBundle([{ id: 1 }, { id: 2 }, { id: 3 }])();
-
-      assert.calledOnce(ASRouterUtils.sendTelemetry);
-      assert.calledWith(ASRouterUtils.sendTelemetry, {
-        action: "onboarding_user_event",
-        event: "DISMISS",
-        id: "onboarding-cards",
-        message_id: "1,2,3",
-        source: "onboarding-cards",
-      });
-    });
   });
 
   describe(".fetchFlowParams", () => {
     let dispatchStub;
     const assertCalledWithURL = url =>
       assert.calledWith(fetchStub, new URL(url).toString(), {
         credentials: "omit",
       });
--- a/browser/components/newtab/test/unit/asrouter/constants.js
+++ b/browser/components/newtab/test/unit/asrouter/constants.js
@@ -1,49 +1,55 @@
 export const CHILD_TO_PARENT_MESSAGE_NAME = "ASRouter:child-to-parent";
 export const PARENT_TO_CHILD_MESSAGE_NAME = "ASRouter:parent-to-child";
 
 export const FAKE_LOCAL_MESSAGES = [
   {
     id: "foo",
+    provider: "snippets",
     template: "simple_snippet",
     content: { title: "Foo", body: "Foo123" },
   },
   {
     id: "foo1",
     template: "simple_snippet",
+    provider: "snippets",
     bundled: 2,
     order: 1,
     content: { title: "Foo1", body: "Foo123-1" },
   },
   {
     id: "foo2",
     template: "simple_snippet",
+    provider: "snippets",
     bundled: 2,
     order: 2,
     content: { title: "Foo2", body: "Foo123-2" },
   },
   {
     id: "bar",
     template: "fancy_template",
     content: { title: "Foo", body: "Foo123" },
   },
   { id: "baz", content: { title: "Foo", body: "Foo123" } },
   {
     id: "newsletter",
+    provider: "snippets",
     template: "newsletter_snippet",
     content: { title: "Foo", body: "Foo123" },
   },
   {
     id: "fxa",
+    provider: "snippets",
     template: "fxa_signup_snippet",
     content: { title: "Foo", body: "Foo123" },
   },
   {
     id: "belowsearch",
+    provider: "snippets",
     template: "simple_below_search_snippet",
     content: { text: "Foo" },
   },
 ];
 export const FAKE_LOCAL_PROVIDER = {
   id: "onboarding",
   type: "local",
   localProvider: "FAKE_LOCAL_PROVIDER",
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx
@@ -0,0 +1,242 @@
+import {
+  FirstRun,
+  FLUENT_FILES,
+} from "content-src/asrouter/templates/FirstRun/FirstRun";
+import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
+import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
+import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
+import { mount } from "enzyme";
+import React from "react";
+
+const FAKE_TRIPLETS = [
+  {
+    id: "CARD_1",
+    content: {
+      title: { string_id: "onboarding-private-browsing-title" },
+      text: { string_id: "onboarding-private-browsing-text" },
+      icon: "icon",
+      primary_button: {
+        label: { string_id: "onboarding-button-label-try-now" },
+        action: {
+          type: "OPEN_URL",
+          data: { args: "https://example.com/" },
+        },
+      },
+    },
+  },
+];
+
+const FAKE_FLOW_PARAMS = {
+  deviceId: "foo",
+  flowId: "abc1",
+  flowBeginTime: 1234,
+};
+
+async function getTestMessage(id) {
+  const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
+    msg => msg.id === id
+  );
+  return { ...message, bundle: FAKE_TRIPLETS };
+}
+
+describe("<FirstRun>", () => {
+  let wrapper;
+  let message;
+  let fakeDoc;
+  let sandbox;
+  let clock;
+  let onBlockByIdStub;
+
+  async function setup() {
+    sandbox = sinon.createSandbox();
+    clock = sandbox.useFakeTimers();
+    message = await getTestMessage("TRAILHEAD_1");
+    fakeDoc = {
+      body: document.createElement("body"),
+      head: document.createElement("head"),
+      createElement: type => document.createElement(type),
+      getElementById: () => document.createElement("div"),
+      activeElement: document.createElement("div"),
+    };
+    onBlockByIdStub = sandbox.stub();
+
+    sandbox
+      .stub(global, "fetch")
+      .withArgs("http://fake.com/endpoint")
+      .resolves({
+        ok: true,
+        status: 200,
+        json: () => Promise.resolve(FAKE_FLOW_PARAMS),
+      });
+
+    wrapper = mount(
+      <FirstRun
+        message={message}
+        document={fakeDoc}
+        dispatch={() => {}}
+        sendUserActionTelemetry={() => {}}
+        onBlockById={onBlockByIdStub}
+      />
+    );
+  }
+
+  beforeEach(setup);
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should render", () => {
+    assert.ok(wrapper);
+  });
+  describe("with both interrupt and triplets", () => {
+    it("should render interrupt and triplets", () => {
+      assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
+      assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
+    });
+    it("should show the card panel and hide the content on the Triplets", () => {
+      // This is so the container shows up in the background but we can fade in the content when intterupt is closed.
+      const tripletsProps = wrapper.find(Triplets).props();
+      assert.propertyVal(tripletsProps, "showCardPanel", true);
+      assert.propertyVal(tripletsProps, "showContent", false);
+    });
+    it("should set the UTM term to trailhead-join (for the traihead-join message)", () => {
+      const iProps = wrapper.find(Interrupt).props();
+      const tProps = wrapper.find(Triplets).props();
+      assert.propertyVal(iProps, "UTMTerm", "trailhead-join");
+      assert.propertyVal(tProps, "UTMTerm", "trailhead-join-card");
+    });
+  });
+
+  describe("with an interrupt but no triplets", () => {
+    beforeEach(() => {
+      message.bundle = []; // Empty triplets
+      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
+    });
+    it("should render interrupt but no triplets", () => {
+      assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
+      assert.lengthOf(wrapper.find(Triplets), 0, "<Triplets>");
+    });
+  });
+
+  describe("with triplets but no interrupt", () => {
+    it("should render interrupt but no triplets", () => {
+      delete message.content; // Empty interrupt
+      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
+
+      assert.lengthOf(wrapper.find(Interrupt), 0, "<Interrupt>");
+      assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
+    });
+  });
+
+  describe("with no triplets or interrupt", () => {
+    it("should render empty", () => {
+      message = { type: "FOO_123" };
+      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
+
+      assert.isTrue(wrapper.isEmptyRender());
+    });
+  });
+
+  it("should pass along executeAction appropriately", () => {
+    const stub = sandbox.stub();
+    wrapper = mount(
+      <FirstRun message={message} document={fakeDoc} executeAction={stub} />
+    );
+
+    assert.propertyVal(wrapper.find(Interrupt).props(), "executeAction", stub);
+    assert.propertyVal(wrapper.find(Triplets).props(), "onAction", stub);
+  });
+
+  it("should load flow params on mount if fxaEndpoint is defined", () => {
+    const stub = sandbox.stub();
+    wrapper = mount(
+      <FirstRun
+        message={message}
+        document={fakeDoc}
+        dispatch={() => {}}
+        fetchFlowParams={stub}
+        fxaEndpoint="https://foo.com"
+      />
+    );
+    assert.calledOnce(stub);
+  });
+
+  it("should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined", () => {
+    const stub = sandbox.stub();
+    wrapper = mount(
+      <FirstRun
+        message={message}
+        document={fakeDoc}
+        fetchFlowParams={stub}
+        dispatch={() => {}}
+      />
+    );
+    assert.notCalled(stub);
+    wrapper.setProps({ fxaEndpoint: "https://foo.com" });
+    assert.calledOnce(stub);
+  });
+
+  it("should not load flow params again onUpdate if they were already set", () => {
+    const stub = sandbox.stub();
+    wrapper = mount(
+      <FirstRun
+        message={message}
+        document={fakeDoc}
+        dispatch={() => {}}
+        fetchFlowParams={stub}
+        fxaEndpoint="https://foo.com"
+      />
+    );
+    wrapper.setProps({ foo: "bar" });
+    wrapper.setProps({ foo: "baz" });
+    assert.calledOnce(stub);
+  });
+
+  it("should load fluent files on mount", () => {
+    assert.lengthOf(fakeDoc.head.querySelectorAll("link"), FLUENT_FILES.length);
+  });
+
+  it("should hide the interrupt and show the triplets when onNextScene is called", () => {
+    // Simulate calling next scene
+    wrapper
+      .find(Interrupt)
+      .find(".trailheadStart")
+      .simulate("click");
+
+    assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
+    assert.isTrue(
+      wrapper
+        .find(Triplets)
+        .find(".trailheadCardGrid")
+        .hasClass("show"),
+      "Show triplet content"
+    );
+  });
+
+  it("should hide the interrupt when props.interruptCleared changes to true", () => {
+    assert.lengthOf(wrapper.find(Interrupt), 1, "Interrupt shown");
+    wrapper.setProps({ interruptCleared: true });
+
+    assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
+  });
+
+  it("should hide triplets when closeTriplets is called and block extended triplets after 500ms", () => {
+    // Simulate calling next scene
+    wrapper
+      .find(Triplets)
+      .find(".icon-dismiss")
+      .simulate("click");
+
+    assert.isFalse(
+      wrapper
+        .find(Triplets)
+        .find(".trailheadCardGrid")
+        .hasClass("show"),
+      "Show triplet content"
+    );
+
+    assert.notCalled(onBlockByIdStub);
+    clock.tick(500);
+    assert.calledWith(onBlockByIdStub, "EXTENDED_TRIPLETS_1");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/Interrupt.test.jsx
@@ -0,0 +1,37 @@
+import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
+import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
+import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
+import { Trailhead } from "content-src/asrouter/templates//Trailhead/Trailhead";
+import { shallow } from "enzyme";
+import React from "react";
+
+describe("<Interrupt>", () => {
+  let wrapper;
+  it("should render Return TO AMO when the message has a template of return_to_amo_overlay", () => {
+    wrapper = shallow(
+      <Interrupt
+        message={{ id: "FOO", content: {}, template: "return_to_amo_overlay" }}
+      />
+    );
+    assert.lengthOf(wrapper.find(ReturnToAMO), 1);
+  });
+  it("should render Trailhead when the message has a template of trailhead", () => {
+    wrapper = shallow(
+      <Interrupt message={{ id: "FOO", content: {}, template: "trailhead" }} />
+    );
+    assert.lengthOf(wrapper.find(Trailhead), 1);
+  });
+  it("should render StartupOverlay when the message has a template of fxa_overlay", () => {
+    wrapper = shallow(
+      <Interrupt message={{ id: "FOO", template: "fxa_overlay" }} />
+    );
+    assert.lengthOf(wrapper.find(StartupOverlay), 1);
+  });
+  it("should throw an error if another type of message is dispatched", () => {
+    assert.throws(() => {
+      wrapper = shallow(
+        <Interrupt message={{ id: "FOO", template: "something" }} />
+      );
+    });
+  });
+});
--- a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
@@ -1,15 +1,15 @@
 import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
 import { mount } from "enzyme";
 import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
 import React from "react";
 import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
 
-const CARDS = [
+export const CARDS = [
   {
     content: {
       title: { string_id: "onboarding-private-browsing-title" },
       text: { string_id: "onboarding-private-browsing-text" },
       icon: "icon",
       primary_button: {
         label: { string_id: "onboarding-button-label-try-now" },
         action: {
@@ -22,21 +22,23 @@ const CARDS = [
 ];
 
 describe("<Trailhead>", () => {
   let wrapper;
   let dummyNode;
   let dispatch;
   let onAction;
   let sandbox;
+  let onNextScene;
 
   beforeEach(async () => {
     sandbox = sinon.sandbox.create();
     dispatch = sandbox.stub();
     onAction = sandbox.stub();
+    onNextScene = sandbox.stub();
     sandbox.stub(global, "fetch").resolves({
       ok: true,
       status: 200,
       json: () => Promise.resolve({ flowId: 123, flowBeginTime: 456 }),
     });
 
     dummyNode = document.createElement("body");
     sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
@@ -54,33 +56,37 @@ describe("<Trailhead>", () => {
 
     const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
       msg => msg.id === "TRAILHEAD_1"
     );
     message.cards = CARDS;
     wrapper = mount(
       <Trailhead
         message={message}
+        UTMTerm={message.utm_term}
         fxaEndpoint="https://accounts.firefox.com/endpoint"
         dispatch={dispatch}
         onAction={onAction}
         document={fakeDocument}
+        onNextScene={onNextScene}
       />
     );
   });
 
   afterEach(() => {
     sandbox.restore();
   });
 
-  it("should emit UserEvent SKIPPED_SIGNIN when you click the start browsing button", () => {
+  it("should emit UserEvent SKIPPED_SIGNIN and call nextScene when you click the start browsing button", () => {
     let skipButton = wrapper.find(".trailheadStart");
     assert.ok(skipButton.exists());
     skipButton.simulate("click");
 
+    assert.calledOnce(onNextScene);
+
     assert.calledOnce(dispatch);
     assert.isUserEventAction(dispatch.firstCall.args[0]);
     assert.calledWith(
       dispatch,
       ac.UserEvent({
         event: at.SKIPPED_SIGNIN,
         value: { has_flow_params: false },
       })
@@ -114,47 +120,16 @@ describe("<Trailhead>", () => {
       dispatch,
       ac.UserEvent({
         event: at.SUBMIT_EMAIL,
         value: { has_flow_params: false },
       })
     );
   });
 
-  it("should add utm_* query params to card actions", () => {
-    let { action } = CARDS[0].content.primary_button;
-    wrapper.instance().onCardAction(action);
-    assert.calledOnce(onAction);
-    const url = onAction.firstCall.args[0].data.args;
-    assert.equal(
-      url,
-      "https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
-    );
-  });
-
-  it("should add flow parameters to card action urls if addFlowParams is true", () => {
-    let action = {
-      type: "OPEN_URL",
-      addFlowParams: true,
-      data: { args: "https://example.com/path?foo=bar" },
-    };
-    wrapper.setState({
-      deviceId: "abc",
-      flowId: "123",
-      flowBeginTime: 456,
-    });
-    wrapper.instance().onCardAction(action);
-    assert.calledOnce(onAction);
-    const url = onAction.firstCall.args[0].data.args;
-    assert.equal(
-      url,
-      "https://example.com/path?foo=bar&utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card&device_id=abc&flow_id=123&flow_begin_time=456"
-    );
-  });
-
   it("should keep focus in dialog when blurring start button", () => {
     const skipButton = wrapper.find(".trailheadStart");
     sandbox.stub(dummyNode, "focus");
 
     skipButton.simulate("blur", { relatedTarget: dummyNode });
 
     assert.calledOnce(dummyNode.focus);
   });
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx
@@ -0,0 +1,96 @@
+import { mount } from "enzyme";
+import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
+import { OnboardingCard } from "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage";
+import React from "react";
+
+const CARDS = [
+  {
+    id: "CARD_1",
+    content: {
+      title: { string_id: "onboarding-private-browsing-title" },
+      text: { string_id: "onboarding-private-browsing-text" },
+      icon: "icon",
+      primary_button: {
+        label: { string_id: "onboarding-button-label-try-now" },
+        action: {
+          type: "OPEN_URL",
+          data: { args: "https://example.com/" },
+        },
+      },
+    },
+  },
+];
+
+describe("<Triplets>", () => {
+  let wrapper;
+  let sandbox;
+  let sendTelemetryStub;
+  let onAction;
+  let onHide;
+
+  async function setup() {
+    sandbox = sinon.createSandbox();
+    sendTelemetryStub = sandbox.stub();
+    onAction = sandbox.stub();
+    onHide = sandbox.stub();
+
+    wrapper = mount(
+      <Triplets
+        cards={CARDS}
+        showCardPanel={true}
+        showContent={true}
+        hideContainer={onHide}
+        onAction={onAction}
+        UTMTerm="trailhead-join-card"
+        sendUserActionTelemetry={sendTelemetryStub}
+      />
+    );
+  }
+
+  beforeEach(setup);
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should add an expanded class to container if props.showCardPanel is true", () => {
+    wrapper.setProps({ showCardPanel: true });
+    assert.isTrue(
+      wrapper.find(".trailheadCards").hasClass("expanded"),
+      "has .expanded)"
+    );
+  });
+  it("should add a collapsed class to container if props.showCardPanel is true", () => {
+    wrapper.setProps({ showCardPanel: false });
+    assert.isFalse(
+      wrapper.find(".trailheadCards").hasClass("expanded"),
+      "has .expanded)"
+    );
+  });
+  it("should send telemetry and call props.hideContainer when the dismiss button is clicked", () => {
+    wrapper.find("button.icon-dismiss").simulate("click");
+    assert.calledOnce(onHide);
+    assert.calledWith(sendTelemetryStub, {
+      event: "DISMISS",
+      message_id: CARDS[0].id,
+      id: "onboarding-cards",
+      action: "onboarding_user_event",
+    });
+  });
+  it("should add utm_* query params to card actions and send the right ping when a card button is clicked", () => {
+    wrapper
+      .find(OnboardingCard)
+      .find("button.onboardingButton")
+      .simulate("click");
+    assert.calledOnce(onAction);
+    const url = onAction.firstCall.args[0].data.args;
+    assert.equal(
+      url,
+      "https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
+    );
+    assert.calledWith(sendTelemetryStub, {
+      event: "CLICK_BUTTON",
+      message_id: CARDS[0].id,
+      id: "TRAILHEAD",
+    });
+  });
+});
--- a/browser/components/newtab/test/unit/content-src/components/ReturnToAMO.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ReturnToAMO.test.jsx
@@ -61,20 +61,16 @@ describe("<ReturnToAMO>", () => {
           sendUserActionTelemetry={sendUserActionTelemetryStub}
         />
       );
 
       // Clear the IMPRESSION ping
       sendUserActionTelemetryStub.reset();
     });
 
-    it("should call onReady on componentDidMount", () => {
-      assert.calledOnce(onReady);
-    });
-
     it("should send telemetry on block", () => {
       wrapper.instance().onBlockButton();
 
       assert.calledOnce(sendUserActionTelemetryStub);
       assert.calledWithExactly(sendUserActionTelemetryStub, {
         event: "BLOCK",
         id: wrapper.instance().props.UISurface,
       });
--- 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,49 +1,45 @@
 import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
 import { mount } from "enzyme";
 import React from "react";
-import { _StartupOverlay as StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
+import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
 
 describe("<StartupOverlay>", () => {
   let wrapper;
   let dispatch;
-  let onReady;
   let onBlock;
   let sandbox;
   beforeEach(() => {
-    sandbox = sinon.sandbox.create();
+    sandbox = sinon.createSandbox();
     dispatch = sandbox.stub();
-    onReady = sandbox.stub();
     onBlock = sandbox.stub();
 
-    wrapper = mount(
-      <StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
-    );
+    wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
   });
 
   afterEach(() => {
     sandbox.restore();
   });
 
-  it("should not render if state.show is false", () => {
-    wrapper.setState({ overlayRemoved: true });
-    assert.isTrue(wrapper.isEmptyRender());
-  });
-
-  it("should call prop.onReady after mount + timeout", async () => {
+  it("should add show class after mount and timeout", async () => {
     const clock = sandbox.useFakeTimers();
-    wrapper = mount(
-      <StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
+    wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
+    assert.isFalse(
+      wrapper.find(".overlay-wrapper").hasClass("show"),
+      ".overlay-wrapper does not have .show class"
     );
-    wrapper.setState({ overlayRemoved: false });
 
     clock.tick(10);
+    wrapper.update();
 
-    assert.calledOnce(onReady);
+    assert.isTrue(
+      wrapper.find(".overlay-wrapper").hasClass("show"),
+      ".overlay-wrapper has .show class"
+    );
   });
 
   it("should emit UserEvent SKIPPED_SIGNIN when you click the skip button", () => {
     let skipButton = wrapper.find(".skip-button");
     assert.ok(skipButton.exists());
     skipButton.simulate("click");
 
     assert.calledOnce(dispatch);
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/addUtmParams.test.js
@@ -0,0 +1,18 @@
+import { addUtmParams } from "content-src/asrouter/templates/FirstRun/addUtmParams";
+
+describe("addUtmParams", () => {
+  it("should convert a string URL", () => {
+    const result = addUtmParams("https://foo.com", "foo");
+    assert.equal(result.hostname, "foo.com");
+  });
+  it("should add all base params", () => {
+    assert.match(
+      addUtmParams(new URL("https://foo.com"), "foo").toString(),
+      /utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/
+    );
+  });
+  it("should add utm_term", () => {
+    const params = addUtmParams(new URL("https://foo.com"), "foo").searchParams;
+    assert.equal(params.get("utm_term"), "foo", "utm_term");
+  });
+});