Bug 1548952 - Add offline fallback, request timeouts and bug fixes to Activity Stream r=k88hudson
authorEd Lee <edilee@mozilla.com>
Sat, 04 May 2019 05:36:06 +0000
changeset 472625 5c493140403539373c1b2b04b75f49255146e72c
parent 472624 3499107fb6fed729654a8e6d719f7bbaff9c5ca1
child 472626 9a2bdd58f6c6e3614ee7a32fcc7f2c78901308f7
push id35969
push userccoroiu@mozilla.com
push dateMon, 06 May 2019 04:24:23 +0000
treeherdermozilla-central@f0566d1a9ab1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1548952
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1548952 - Add offline fallback, request timeouts and bug fixes to Activity Stream r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D29909
browser/app/profile/firefox.js
browser/components/newtab/.eslintrc.js
browser/components/newtab/bin/test-merges.js
browser/components/newtab/bin/vendor-react.js
browser/components/newtab/bin/vendor.js
browser/components/newtab/common/Actions.jsm
browser/components/newtab/common/Reducers.jsm
browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
browser/components/newtab/content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json
browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.scss
browser/components/newtab/content-src/lib/selectLayoutRender.js
browser/components/newtab/content-src/styles/_activity-stream.scss
browser/components/newtab/content-src/styles/_theme.scss
browser/components/newtab/content-src/styles/_variables.scss
browser/components/newtab/css/activity-stream-linux.css
browser/components/newtab/css/activity-stream-mac.css
browser/components/newtab/css/activity-stream-windows.css
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/data/trailhead.wip
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ASRouterPreferences.jsm
browser/components/newtab/lib/ASRouterTargeting.jsm
browser/components/newtab/lib/ActivityStream.jsm
browser/components/newtab/lib/BookmarkPanelHub.jsm
browser/components/newtab/lib/DiscoveryStreamFeed.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/lib/PanelTestProvider.jsm
browser/components/newtab/lib/TelemetryFeed.jsm
browser/components/newtab/locales-src/ach/strings.properties
browser/components/newtab/package-lock.json
browser/components/newtab/package.json
browser/components/newtab/prerendered/locales/ach/activity-stream-strings.js
browser/components/newtab/prerendered/static/activity-stream-initial-state.js
browser/components/newtab/test/browser/browser_asrouter_targeting.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
browser/components/newtab/test/unit/asrouter/TargetingDocs.test.js
browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
browser/components/newtab/test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js
browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
browser/components/newtab/test/unit/common/Reducers.test.js
browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx
browser/components/newtab/test/unit/lib/BookmarkPanelHub.test.js
browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
browser/components/newtab/test/unit/unit-entry.js
browser/components/newtab/vendor/react-dev.js
browser/components/newtab/vendor/react-dom-dev.js
browser/components/newtab/vendor/react-dom.js
browser/components/newtab/vendor/react-intl.js
browser/components/newtab/vendor/react-redux.js
browser/components/newtab/vendor/react.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1320,19 +1320,19 @@ 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
 
 #ifdef NIGHTLY_BUILD
-pref("trailhead.firstrun.cohort", 1);
+pref("trailhead.firstrun.branches", "join-privacy");
 #else
-pref("trailhead.firstrun.cohort", 0);
+pref("trailhead.firstrun.branches", "control");
 #endif
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -63,17 +63,17 @@ module.exports = {
     "react/jsx-equals-spacing": [2, "never"],
     "react/jsx-key": 2,
     "react/jsx-no-bind": 2,
     "react/jsx-no-comment-textnodes": 2,
     "react/jsx-no-duplicate-props": 2,
     "react/jsx-no-target-blank": 2,
     "react/jsx-no-undef": 2,
     "react/jsx-pascal-case": 2,
-    "react/jsx-space-before-closing": [2, "always"],
+    "react/jsx-tag-spacing": 2,
     "react/jsx-uses-react": 2,
     "react/jsx-uses-vars": 2,
     "react/jsx-wrap-multilines": 2,
     "react/no-access-state-in-setstate": 2,
     "react/no-danger": 2,
     "react/no-deprecated": 2,
     "react/no-did-mount-set-state": 2,
     "react/no-did-update-set-state": 2,
deleted file mode 100644
--- a/browser/components/newtab/bin/test-merges.js
+++ /dev/null
@@ -1,344 +0,0 @@
-#! /usr/bin/env node
-"use strict";
-
-/* eslint-disable no-console, mozilla/no-task */
-/* this is a node script; primary interaction is via console */
-
-const Task = require("co-task");
-const process = require("process");
-const path = require("path");
-const GitHubApi = require("@octokit/rest");
-const shelljs = require("shelljs");
-const child_process = require("child_process");
-const github = new GitHubApi();
-
-// some of our API requests need to be authenticated
-let token = process.env.AS_PINE_TOKEN;
-github.authenticate({type: "token", token});
-
-// note that this token MUST have the public_repo scope set in the github API
-
-const AS_REPO_OWNER = process.env.AS_REPO_OWNER || "mozilla";
-const AS_REPO_NAME = process.env.AS_REPO_NAME || "activity-stream";
-const AS_REPO = `${AS_REPO_OWNER}/${AS_REPO_NAME}`;
-const OLDEST_PR_DATE = "2017-03-17";
-const HG = "hg"; // mercurial
-const HG_BRANCH_NAME = "pine";
-const ALREADY_PUSHED_LABEL = "pushed-to-pine";
-const TREEHERDER_PREFIX = "https://treeherder.mozilla.org/#/jobs?repo=pine&revision=";
-
-// Path to the working directory where the export/commit operations will be
-// done.  Highly advisted to be used only for this testing purpose so you don't
-// accidently clobber real work.
-//
-// There will be two child directories:
-//
-// activity-stream - the github repo to be exported from.  MUST
-//
-// * be cloned by hand before running this script
-// * be 'npm install'ed
-// * have the ${ALREADY_PUSHED_LABEL} label created by hand
-// * have the user who has issued AS_PINE_TOKEN as a collaborator for the repo
-//   in order to be able to change labels.
-//
-// mozilla-central - the hg repo for firefox. Will be created if it doesn't
-// already exist.
-const {AS_PINE_TEST_DIR} = process.env;
-
-const TESTING_LOCAL_MC = path.join(AS_PINE_TEST_DIR, "mozilla-central");
-
-const SimpleGit = require("simple-git");
-const TESTING_LOCAL_GIT = path.join(AS_PINE_TEST_DIR, AS_REPO_NAME);
-const git = new SimpleGit(TESTING_LOCAL_GIT);
-
-// Mostly useful to specify during development of the test automation so that
-// prepare-mochitests-dev and friends from the development repo get used
-// instead of from the testing repo, which won't have had any changes checked in
-// just yet.
-const AS_GIT_BIN_REPO = process.env.AS_GIT_BIN_REPO || TESTING_LOCAL_GIT;
-
-const PREPARE_MOCHITESTS_DEV =
-  path.join(AS_GIT_BIN_REPO, "bin", "prepare-mochitests-dev");
-
-/**
- * Find all PRs merged since ${OLDEST_PR_DATE} that don't have
- * ${ALREADY_PUSHED_LABEL}
- *
- * @return {Promise} Promise that resolves with the search results or rejects
- */
-function findNewlyMergedPRs() {
-  const searchTerms = [
-    // closed PRs in our repo
-    `repo:${AS_REPO}`, "type:pr", "state:closed", "is:merged",
-
-    // don't try and mochitest old closed stuff, we don't want to kick off a
-    // zillion test jobs
-    `merged:>=${OLDEST_PR_DATE}`,
-
-    // only look at merges to master
-    "base:master",
-
-    // if it's already been pushed to pine, don't do it again
-    `-label:${ALREADY_PUSHED_LABEL}`,
-  ];
-
-  console.log(`Searching ${AS_REPO} for newly merged PRs`);
-  return github.search.issues({q: searchTerms.join("+")});
-}
-
-/**
- * Return the commitId when the given PR was merged.  This is the one
- * we will want to export and test.
- *
- * @param  {String} prNumber  The number of the PR to export.
- * @return {String}           The commitId associated with the merge of this PR.
- */
-function getPRMergeCommitId(prNumber) {
-  return github.issues.getEvents({
-    owner: AS_REPO_OWNER,
-    repo: AS_REPO_NAME,
-    issue_number: prNumber,
-  }).then(({data}) => {
-    if (data.incomplete_results) {
-      // XXX should handle this case theoretically, but since we'll be running
-      // regularly from cron, it seems unlikely that we'll even hit 30 new
-      // merges (default GitHub page size) in a single run.
-      throw new Error("data.incomplete_results is true, aborting");
-    }
-
-    let mergeEvents = data.filter(item => item.event === "merged");
-    if (mergeEvents.length > 1) {
-      throw new Error("more than one merge event, aborting");
-    } else if (!mergeEvents.length) {
-      throw new Error(`Github returned no merge events for PR ${prNumber}, aborting.  Workaround: mark this PR as pushed-to-pine, so it gets skipped`);
-    }
-    let [mergeEvent] = mergeEvents;
-
-    if (!mergeEvent.commit_id) {
-      throw new Error("merge event has no commit id attached, aborted");
-    }
-
-    return mergeEvent.commit_id;
-  }).catch(err => { throw err; });
-}
-
-/**
- * Checks out the given commit into ${TESTING_LOCAL_GIT}
- *
- * @param  {String} commitId
- * @return {Promise<String[]|?>} Resolves with commit [id, message] on checkout, or
- *                      rejects with error
- */
-function checkoutGitCommit(commitId) {
-  return new Promise((resolve, reject) => {
-    console.log(`Fetching changes from github remote ${AS_REPO}...`);
-    // fetch any changes from the remote
-    git.fetch({}, (err, data) => {
-      if (err) {
-        reject(err);
-        return;
-      }
-      console.log(`Starting github checkout of ${commitId}...`);
-      git.checkout(commitId, (err2, data2) => {
-        if (err2) {
-          reject(err2);
-          return;
-        }
-
-        // Pass along the original commit message
-        git.show(["-s", "--format=%B"], (err3, data3) => {
-          if (err3) {
-            reject(err3);
-            return;
-          }
-          resolve([commitId, data3.trim()]);
-        });
-      });
-    });
-  });
-}
-
-function exportToLocalMC(commitId) {
-  return new Promise((resolve, reject) => {
-    console.log("Preparing mochitest dev environment...");
-    // Weirdly, /bin/yes causes npm-run-all bundle-static to explode, so we
-    // use echo.
-    shelljs.exec(`
-      echo yes | \
-        env AS_GIT_BIN_REPO=${AS_GIT_BIN_REPO} SYMLINK_TESTS=false \
-        ENABLE_MC_AS=1 ${PREPARE_MOCHITESTS_DEV}`,
-      {async: true, cwd: TESTING_LOCAL_GIT, silent: false}, (code, stdout, stderr) => {
-        if (code) {
-          reject(new Error(`${PREPARE_MOCHITESTS_DEV} failed, exit code: ${code}`));
-          return;
-        }
-
-        resolve(commitId);
-      });
-  });
-}
-
-function commitToHg([commitId, commitMsg]) {
-  return new Promise((resolve, reject) => {
-    // we use child_process.execFile here because shelljs.exec goes through
-    // the shell, which means that if the original commit message has shell
-    // quote characters, things can go haywire in weird ways.
-    console.log(`Committing exported ${commitId} to ${AS_REPO_NAME}...`);
-    child_process.execFile(HG,
-      [
-        "commit",
-        "--addremove",
-        "-m",
-        `${commitMsg}\n\nExport of ${commitId} from ${AS_REPO_OWNER}/${AS_REPO_NAME}`,
-        ".",
-      ],
-      {cwd: TESTING_LOCAL_MC, env: process.env, timeout: 5 * 60 * 1000},
-      (code, stdout, stderr) => {
-        if (code) {
-          reject(new Error(`${HG} commit failed, output: ${stderr}`));
-          return;
-        }
-
-        resolve(code);
-      }
-    );
-  });
-}
-
-/**
- * [pushToHgProjectBranch description]
- *
- * @return {Promise<String|Number>} resolves with the text written to XXXstdout, or
- *                                  rejects with the exit code from ${HG}.
- */
-function pushToHgProjectBranch() {
-  return new Promise((resolve, reject) => {
-    shelljs.exec(`${HG} push -f ${HG_BRANCH_NAME}`, {async: true, cwd: TESTING_LOCAL_MC},
-      (code, stdout, stderr) => {
-        if (code) {
-          reject(new Error(`${HG} failed, exit code: ${code}`));
-          return;
-        }
-
-        // Grab the last linked revision from the push output
-        const [rev] = stdout.split(/(?:\/rev\/|changeset=)/).slice(-1)[0].split("\n");
-        resolve(`[Treeherder: ${rev}](${TREEHERDER_PREFIX}${rev})`);
-      }
-    );
-  });
-}
-
-/**
- * Remove last commit from the repo so the next artifact build will work right
- */
-function stripTipFromHg() {
-  return new Promise((resolve, reject) => {
-    console.log("Stripping tip commit from mozilla-central so the next artifact build will work ...");
-    shelljs.exec(`${HG} strip --force --rev -1`,
-      {async: true, cwd: TESTING_LOCAL_MC},
-      (code, stdout, stderr) => {
-        if (code) {
-          reject(new Error(`${HG} strip failed, output: ${stderr}`));
-          return;
-        }
-
-        resolve(code);
-      }
-    );
-  });
-}
-
-function annotateGithubPR(prNumber, annotation) {
-  console.log(`Annotating ${prNumber} with ${annotation}...`);
-
-  // We use createComment from issues instead of pullRequests because we're
-  // not commenting on a particular commit
-  return github.issues.createComment({
-    owner: AS_REPO_OWNER,
-    repo: AS_REPO_NAME,
-    number: prNumber,
-    body: annotation,
-  }).catch(reason => console.log(reason));
-}
-
-/**
- * Labels a given github PR ${ALREADY_PUSHED_LABEL}.
- */
-function labelGithubPR(prNumber) {
-  console.log(`Labeling PR ${prNumber} with ${ALREADY_PUSHED_LABEL}...`);
-
-  return github.issues.addLabels({
-    owner: AS_REPO_OWNER,
-    repo: AS_REPO_NAME,
-    number: prNumber,
-    labels: [ALREADY_PUSHED_LABEL],
-  }).catch(reason => console.log(reason));
-}
-
-function pushPR(pr) {
-  return getPRMergeCommitId(pr.number)
-
-    // get the merged commit to test
-    .then(checkoutGitCommit)
-
-    // use prepare-mochitest-dev to export
-    .then(exportToLocalMC)
-
-    // commit latest export to hg
-    .then(commitToHg)
-
-    // hg push
-    .then(() => pushToHgProjectBranch().catch(() => {
-      stripTipFromHg();
-      throw new Error("pushToHgProjectBranch failed; tip stripped from hg");
-    }))
-
-    // annotate PR with URL to watch
-    .then(annotation => annotateGithubPR(pr.number, annotation))
-
-    // make sure next artifact build doesn't explode
-    .then(() => stripTipFromHg())
-
-    // label with ${ALREADY_PUSHED_LABEL}
-    .then(() => labelGithubPR(pr.number))
-
-    .catch(err => {
-      console.log(err);
-      throw err;
-    });
-}
-
-function main() {
-  findNewlyMergedPRs().then(({data}) => {
-    if (data.incomplete_results) {
-      throw new Error("data.incomplete_results is true, aborting");
-    }
-
-    if (data.items.length === 0) {
-      console.log("No newly merged PRs to test");
-      return;
-    }
-
-    function* executePush() {
-      for (let pr of data.items) {
-        yield pushPR(pr);
-      }
-    }
-
-    // Serialize the execution of the export and pushing tests since each
-    // depend on exclusive access the state of the git and hg repos used to
-    // stage the tests.
-    Task.spawn(executePush).then(() => {
-      console.log("Processed all new merges.");
-    }).catch(reason => {
-      console.log("Something went wrong processing the merges:", reason);
-      process.exitCode = -1;
-    });
-  })
-  .catch(reason => {
-    console.error(reason);
-    process.exitCode = -1;
-  });
-}
-
-main();
rename from browser/components/newtab/bin/vendor-react.js
rename to browser/components/newtab/bin/vendor.js
--- a/browser/components/newtab/bin/vendor-react.js
+++ b/browser/components/newtab/bin/vendor.js
@@ -8,16 +8,20 @@ const path = require("path");
 const filesToVendor = {
   // XXX currently these two licenses are identical.  Perhaps we should check
   // in case that changes at some point in the future.
   "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE",
   "react/umd/react.production.min.js": "react.js",
   "react/umd/react.development.js": "react-dev.js",
   "react-dom/umd/react-dom.production.min.js": "react-dom.js",
   "react-dom/umd/react-dom.development.js": "react-dom-dev.js",
+  "react-intl/LICENSE.md": "REACT_INTL_LICENSE",
+  "react-intl/dist/react-intl.min.js": "react-intl.js",
+  "react-redux/LICENSE.md": "REACT_REDUX_LICENSE",
+  "react-redux/dist/react-redux.min.js": "react-redux.js",
 };
 
 set("-v"); // Echo all the copy commands so the user can see what's going on
 for (let srcPath of Object.keys(filesToVendor)) {
   cp(path.join("node_modules", srcPath),
     path.join("vendor", filesToVendor[srcPath]));
 }
 
--- a/browser/components/newtab/common/Actions.jsm
+++ b/browser/components/newtab/common/Actions.jsm
@@ -291,17 +291,17 @@ function ASRouterUserEvent(data) {
     type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT,
     data,
   });
 }
 
 /**
  * DiscoveryStreamSpocsFill - A telemetry ping indicating a SPOCS Fill event.
  *
- * @param  {object} data Fields to include in the ping (spocs_fills, etc.)
+ * @param  {object} data Fields to include in the ping (spoc_fills, etc.)
  * @param  {int} importContext (For testing) Override the import context for testing.
  * @return {object} An AlsoToMain action
  */
 function DiscoveryStreamSpocsFill(data, importContext = globalImportContext) {
   const action = {
     type: actionTypes.DISCOVERY_STREAM_SPOCS_FILL,
     data,
   };
--- a/browser/components/newtab/common/Reducers.jsm
+++ b/browser/components/newtab/common/Reducers.jsm
@@ -11,20 +11,17 @@ const TOP_SITES_MAX_SITES_PER_ROW = 8;
 
 const dedupe = new Dedupe(site => site && site.url);
 
 const INITIAL_STATE = {
   App: {
     // Have we received real data from the app yet?
     initialized: false,
   },
-  ASRouter: {
-    initialized: false,
-    allowLegacySnippets: null,
-  },
+  ASRouter: {initialized: false},
   Snippets: {initialized: false},
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: [],
     // Used in content only to dispatch action to TopSiteForm.
     editForm: null,
@@ -86,18 +83,16 @@ function App(prevState = INITIAL_STATE.A
       return prevState;
   }
 }
 
 function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
   switch (action.type) {
     case at.AS_ROUTER_INITIALIZED:
       return {...action.data, initialized: true};
-    case at.AS_ROUTER_PREF_CHANGED:
-      return {...prevState, ...action.data};
     default:
       return prevState;
   }
 }
 
 /**
  * insertPinned - Inserts pinned links in their specified slots
  *
--- a/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
+++ b/browser/components/newtab/content-src/asrouter/docs/targeting-attributes.md
@@ -23,18 +23,20 @@ Please note that some targeting attribut
 * [profileAgeCreated](#profileagecreated)
 * [profileAgeReset](#profileagereset)
 * [providerCohorts](#providercohorts)
 * [region](#region)
 * [searchEngines](#searchengines)
 * [sync](#sync)
 * [topFrecentSites](#topfrecentsites)
 * [totalBookmarksCount](#totalbookmarkscount)
-* [trailheadCohort](#trailheadcohort)
+* [trailheadInterrupt](#trailheadinterrupt)
+* [trailheadTriplet](#trailheadtriplet)
 * [usesFirefoxSync](#usesfirefoxsync)
+* [isFxAEnabled](#isFxAEnabled)
 * [xpinstallEnabled](#xpinstallEnabled)
 * [hasPinnedTabs](#haspinnedtabs)
 
 ## Detailed usage
 
 ### `addonsInfo`
 Provides information about the add-ons the user has installed.
 
@@ -420,31 +422,44 @@ type UnixEpochNumber = number;
 Total number of bookmarks.
 
 #### Definition
 
 ```ts
 declare const totalBookmarksCount: number;
 ```
 
-### `trailheadCohort`
+### `trailheadInterrupt`
+
+(67.05+ only) Experiment branch for "interrupt" study
 
-(67+ only) Experiment cohort for special trailhead project
+### `trailheadTriplet`
 
+(67.05+ only) Experiment branch for "triplet" study
 
 ### `usesFirefoxSync`
 
 Does the user use Firefox sync?
 
 #### Definition
 
 ```ts
 declare const usesFirefoxSync: boolean;
 ```
 
+### `isFxAEnabled`
+
+Does the user have Firefox sync enabled? The service could potentially be turned off [for enterprise builds](https://searchfox.org/mozilla-central/rev/b59a99943de4dd314bae4e44ab43ce7687ccbbec/browser/components/enterprisepolicies/Policies.jsm#327).
+
+#### Definition
+
+```ts
+declare const isFxAEnabled: boolean;
+```
+
 ### `xpinstallEnabled`
 
 Pref used by system administrators to disallow add-ons from installed altogether.
 
 #### Definition
 
 ```ts
 declare const xpinstallEnabled: boolean;
--- a/browser/components/newtab/content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json
+++ b/browser/components/newtab/content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json
@@ -56,49 +56,36 @@
               "type": "string",
               "description": "Fluent id of localized string"
             }
           },
           "required": ["string_id"]
         }
       ]
     },
-    "link": {
+    "cta": {
       "description": "Link shown at the bottom of the message, call to action",
-      "properties": {
-        "text": {
-          "description": "Message shown as part of anchor tag",
-          "oneOf": [
-            {
-              "allOf": [
-                {"$ref": "#/definitions/richText"},
-                {"description": "Message to be shown"}
-              ]
-            },
-            {
-              "type": "object",
-              "properties": {
-                "string_id": {
-                  "type": "string",
-                  "description": "Fluent id of localized string"
-                }
-              },
-              "required": ["string_id"]
+      "oneOf": [
+        {
+          "allOf": [
+            {"$ref": "#/definitions/richText"},
+            {"description": "Message to be shown"}
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "string_id": {
+              "type": "string",
+              "description": "Fluent id of localized string"
             }
-          ] 
-        },
-        "url": {
-          "description": "Value for href attribute of the anchor",
-          "allOf": [
-            {"$ref": "#/definitions/link_url"},
-            {"description": "Link that opens in a new tab"}
-          ]
+          },
+          "required": ["string_id"]
         }
-      },
-      "required": ["text", "url"]
+      ]
     },
     "info_icon": {
       "type": "object",
       "description": "The small icon displayed in the top right corner of the panel. Not configurable, only the tooltip text." ,
       "properties": {
         "tooltiptext": {
           "oneOf": [
             {
@@ -116,13 +103,61 @@
                 }
               },
               "required": ["string_id"]
             }
           ]
         }
       },
       "required": ["tooltiptext"]
+    },
+    "close_button": {
+      "type": "object",
+      "description": "The small dissmiss icon displayed in the top right corner of the message. Not configurable, only the tooltip text." ,
+      "properties": {
+        "tooltiptext": {
+          "oneOf": [
+            {
+              "allOf": [
+                {"$ref": "#/definitions/plainText"},
+                {"description": "Message to be shown"}
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "string_id": {
+                  "type": "string",
+                  "description": "Fluent id of localized string"
+                }
+              },
+              "required": ["string_id"]
+            }
+          ]
+        }
+      },
+      "required": ["tooltiptext"]
+    },
+    "color": {
+      "description": "Message text color",
+      "allOf": [
+        {"$ref": "#/definitions/plainText"},
+        {"description": "Valid CSS color"}
+      ]
+    },
+    "background_color_1": {
+      "description": "Configurable background color through CSS gradient",
+      "allOf": [
+        {"$ref": "#/definitions/plainText"},
+        {"description": "Valid CSS color"}
+      ]
+    },
+    "background_color_2": {
+      "description": "Configurable background color through CSS gradient",
+      "allOf": [
+        {"$ref": "#/definitions/plainText"},
+        {"description": "Valid CSS color"}
+      ]
     }
   },
   "additionalProperties": false,
-  "required": ["title", "text", "link", "info_icon"]
+  "required": ["title", "text", "cta", "info_icon"]
 }
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
@@ -1,34 +1,38 @@
 import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
+import {injectIntl} from "react-intl";
 import {ModalOverlayWrapper} from "../../components/ModalOverlay/ModalOverlay";
 import {OnboardingCard} from "../OnboardingMessage/OnboardingMessage";
 import React from "react";
 
 const FLUENT_FILES = [
   "branding/brand.ftl",
   "browser/branding/sync-brand.ftl",
   // These are finalized strings exposed to localizers
   "browser/newtab/onboarding.ftl",
   // These are WIP/in-development strings that only get used if the string
   // doesn't already exist in onboarding.ftl above
   "trailhead.ftl",
 ];
 
-export class Trailhead extends React.PureComponent {
+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.onSubmit = this.onSubmit.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
 
     this.state = {
       emailInput: "",
       isModalOpen: true,
+      showCardPanel: true,
+      showCards: false,
       flowId: "",
       flowBeginTime: 0,
     };
     this.didFetch = false;
   }
 
   async componentWillMount() {
     FLUENT_FILES.forEach(file => {
@@ -55,16 +59,25 @@ export class Trailhead extends React.Pur
   }
 
   componentDidMount() {
     // We need to remove hide-main since we should show it underneath everything that has rendered
     global.document.body.classList.remove("hide-main");
 
     // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
     global.document.body.classList.add("inline-onboarding");
+
+    if (!this.props.message.content) {
+      // No modal overlay, let the user scroll and deal them some cards.
+      global.document.body.classList.remove("welcome");
+
+      if (this.props.message.includeBundle || this.props.message.cards) {
+        this.revealCards();
+      }
+    }
   }
 
   componentDidUnmount() {
     global.document.body.classList.remove("inline-onboarding");
   }
 
   onInputChange(e) {
     let error = e.target.previousSibling;
@@ -78,16 +91,17 @@ export class Trailhead extends React.Pur
 
     global.addEventListener("visibilitychange", this.closeModal);
   }
 
   closeModal() {
     global.removeEventListener("visibilitychange", this.closeModal);
     global.document.body.classList.remove("welcome");
     this.setState({isModalOpen: false});
+    this.revealCards();
     this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN", ...this._getFormInfo()}));
   }
 
   /**
    * Report to telemetry additional information about the form submission.
    */
   _getFormInfo() {
     const value = {has_flow_params: this.state.flowId.length > 0};
@@ -97,88 +111,115 @@ export class Trailhead extends React.Pur
   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});
+  }
+
+  revealCards() {
+    this.setState({showCards: true});
+  }
+
+  getStringValue(str) {
+    if (str.property_id) {
+      str.value = this.props.intl.formatMessage({id: str.property_id});
+    }
+    return str.value;
+  }
+
   render() {
     const {props} = this;
     const {bundle: cards, content} = props.message;
+    const innerClassName = [
+      "trailhead",
+      content && content.className,
+    ].filter(v => v).join(" ");
     return (<>
-    {this.state.isModalOpen && content ? <ModalOverlayWrapper innerClassName={`trailhead ${content.className}`} onClose={this.closeModal}>
+    {this.state.isModalOpen && content ? <ModalOverlayWrapper innerClassName={innerClassName} onClose={this.closeModal}>
       <div className="trailheadInner">
         <div className="trailheadContent">
-          <h1 data-l10n-id={content.title.string_id}>{content.title.value}</h1>
+          <h1 data-l10n-id={content.title.string_id}>{this.getStringValue(content.title)}</h1>
           {content.subtitle &&
-            <p data-l10n-id={content.subtitle.string_id}>{content.subtitle.value}</p>
+            <p data-l10n-id={content.subtitle.string_id}>{this.getStringValue(content.subtitle)}</p>
           }
           <ul className="trailheadBenefits">
             {content.benefits.map(item => (
               <li key={item.id} className={item.id}>
-                <h3 data-l10n-id={item.title.string_id}>{item.title.value}</h3>
-                <p data-l10n-id={item.text.string_id}>{item.text.value}</p>
+                <h3 data-l10n-id={item.title.string_id}>{this.getStringValue(item.title)}</h3>
+                <p data-l10n-id={item.text.string_id}>{this.getStringValue(item.text)}</p>
               </li>
             ))}
           </ul>
           <a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={content.learn.url}>
-            {content.learn.text.value}
+            {this.getStringValue(content.learn.text)}
           </a>
         </div>
         <div className="trailheadForm">
-          <h3 data-l10n-id={content.form.title.string_id}>{content.form.title.value}</h3>
-          <p data-l10n-id={content.form.text.string_id}>{content.form.text.value}</p>
+          <h3 data-l10n-id={content.form.title.string_id}>{this.getStringValue(content.form.title)}</h3>
+          <p data-l10n-id={content.form.text.string_id}>{this.getStringValue(content.form.text)}</p>
           <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="trailhead" />
             <input name="flow_id" type="hidden" value={this.state.flowId} />
             <input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
             <p data-l10n-id="onboarding-join-form-email-error" className="error" />
             <input
               data-l10n-id={content.form.email.string_id}
-              placeholder={content.form.email.placeholder}
+              placeholder={this.getStringValue(content.form.email)}
               name="email"
               type="email"
               required="true"
               onInvalid={this.onInputInvalid}
               onChange={this.onInputChange} />
             <p className="trailheadTerms" data-l10n-id="onboarding-join-form-legal">
               <a data-l10n-name="terms"
                 href="https://accounts.firefox.com/legal/terms" />
               <a data-l10n-name="privacy"
                 href="https://accounts.firefox.com/legal/privacy" />
             </p>
             <button data-l10n-id={content.form.button.string_id} type="submit">
-              {content.form.button.value}
+              {this.getStringValue(content.form.button)}
             </button>
           </form>
         </div>
       </div>
 
       <button className="trailheadStart"
         data-l10n-id={content.skipButton.string_id}
-        onClick={this.closeModal}>{content.skipButton.value}</button>
+        onClick={this.closeModal}>{this.getStringValue(content.skipButton)}</button>
     </ModalOverlayWrapper> : null}
-    {(cards && cards.length) ? <div className="trailheadCards">
+    {(cards && cards.length) ? <div className={`trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`}>
       <div className="trailheadCardsInner">
         <h1 data-l10n-id="onboarding-welcome-header" />
-        <div className="trailheadCardGrid">
+        <div className={`trailheadCardGrid${this.state.showCards ? " show" : ""}`}>
         {cards.map(card => (
           <OnboardingCard key={card.id}
             className="trailheadCard"
             sendUserActionTelemetry={props.sendUserActionTelemetry}
             onAction={props.onAction}
             UISurface="TRAILHEAD"
             {...card} />
         ))}
         </div>
+        {this.state.showCardPanel &&
+          <button
+            className="icon icon-dismiss" onClick={this.hideCardPanel}
+            title={props.intl.formatMessage({id: "menu_action_dismiss"})}
+            aria-label={props.intl.formatMessage({id: "menu_action_dismiss"})} />
+        }
       </div>
     </div> : null}
     </>);
   }
 }
+
+export const Trailhead = injectIntl(_Trailhead);
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
@@ -1,11 +1,14 @@
 .trailhead {
   $benefit-icon-size: 62px;
   $benefit-icon-spacing: $benefit-icon-size + 12px;
+  $benefit-icon-size-small: 40px;
+  $benefit-icon-spacing-small: $benefit-icon-size-small + 12px;
+  $responsive-breakpoint: 850px;
 
   background: url('#{$image-path}trailhead/accounts-form-bg.jpg') bottom / cover;
   color: $white;
   height: auto;
   top: 100px;
 
   @media (max-height: 700px) {
     position: absolute;
@@ -38,17 +41,20 @@
       font-weight: 200;
       line-height: 46px;
       margin: 0;
     }
 
     .trailheadLearn {
       display: block;
       margin-top: 30px;
-      margin-inline-start: $benefit-icon-spacing;
+
+      @media (min-width: $responsive-breakpoint) {
+        margin-inline-start: $benefit-icon-spacing;
+      }
     }
   }
 
   &.syncCohort {
     left: calc(50% - 430px);
     width: 860px;
 
     @media (max-width: 860px) {
@@ -75,23 +81,31 @@
       }
     }
   }
 
   .trailheadBenefits {
     padding: 0;
 
     li {
-      background-position: left 4px;
+      background-position: left 6px;
       background-repeat: no-repeat;
-      background-size: $benefit-icon-size;
+      background-size: $benefit-icon-size-small;
       -moz-context-properties: fill;
       fill: $blue-50;
       list-style: none;
-      padding-inline-start: $benefit-icon-spacing;
+      padding-top: 8px;
+
+
+      @media (min-width: $responsive-breakpoint) {
+        background-position-y: 4px;
+        background-size: $benefit-icon-size;
+        margin-inline-end: 60px;
+        padding-inline-start: $benefit-icon-spacing;
+      }
 
       &:dir(rtl) {
         background-position-x: right;
       }
 
       &.knowledge {
         background-image: url('#{$image-path}trailhead/benefit-knowledge.png');
       }
@@ -104,25 +118,29 @@
         background-image: url('#{$image-path}trailhead/benefit-products.png');
       }
     }
 
     h3 {
       color: $violet-20;
       font-size: 22px;
       font-weight: 400;
-      margin-bottom: 4px;
+      margin: 0 0 4px;
+      padding-inline-start: $benefit-icon-spacing-small;
+
+      @media (min-width: $responsive-breakpoint) {
+        padding-inline-start: 0;
+      }
     }
 
     p {
       color: $white;
       font-size: 15px;
       line-height: 22px;
       margin: 4px 0 15px;
-      margin-inline-end: 60px;
     }
   }
 
   .trailheadForm {
     $logo-size: 100px;
 
     background: url('#{$image-path}trailhead/firefox-logo.png') top center / $logo-size no-repeat;
     min-width: 260px;
@@ -186,16 +204,20 @@
       &:hover,
       &:focus {
         background-color: $trailhead-blue-60;
       }
 
       &:focus {
         outline: dotted 1px;
       }
+
+      &:active {
+        background-color: $trailhead-blue-70;
+      }
     }
   }
 
   .trailheadStart {
     border: 1px solid $white-50;
     cursor: pointer;
     display: block;
     font-size: 15px;
@@ -208,22 +230,39 @@
     &:focus {
       background-color: $trailhead-blue-60;
       border-color: transparent;
     }
 
     &:focus {
       outline: dotted 1px;
     }
+
+    &:active {
+      background-color: $trailhead-blue-70;
+    }
+  }
+
+  .trailheadInner,
+  .trailheadStart {
+    animation: fadeIn 0.4s;
   }
 }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
+  max-height: 1000px;
+  overflow: hidden;
   text-align: center;
+  transition: max-height 0.5s $photon-easing;
+
+
+  &.collapsed {
+    max-height: 0;
+  }
 
   h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color);
   }
 }
@@ -239,22 +278,43 @@
   @media (min-width: $break-point-large) {
     width: $wrapper-max-width-large;
   }
 
   @media (min-width: $break-point-widest) {
     width: $wrapper-max-width-widest;
   }
 
+  .icon-dismiss {
+    border: 0;
+    cursor: pointer;
+    inset-inline-end: 15px;
+    padding: 15px;
+    opacity: 0.75;
+    position: absolute;
+    top: 15px;
+
+    &:hover,
+    &:focus {
+      background-color: var(--newtab-element-hover-color);
+    }
+  }
 }
 
 .trailheadCardGrid {
   display: grid;
   grid-gap: $base-gutter;
   margin: 0;
+  opacity: 0;
+  transition: opacity 0.4s;
+  transition-delay: 0.1s;
+
+  &.show {
+    opacity: 1;
+  }
 
   @media (min-width: $break-point-medium) {
     grid-template-columns: repeat(auto-fit, $card-width);
   }
 
   @media (min-width: $break-point-widest) {
     grid-template-columns: repeat(auto-fit, $card-width-large);
   }
@@ -300,16 +360,24 @@
     min-width: 70%;
     padding: 0 14px;
 
     &:focus,
     &:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color);
     }
+
+    &:focus {
+      outline: dotted 1px;
+    }
+
+    &:active {
+      background: var(--trailhead-card-button-background-active-color);
+    }
   }
 
   .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
@@ -322,16 +390,20 @@
     position: relative;
 
     .prefs-button {
       button {
         position: absolute;
       }
     }
   }
+
+  .asrouter-toggle {
+    position: absolute;
+  }
 }
 
 // If the window is too short, we need to allow scrolling so user can get to Start Browsing button.
 @media (max-height: 700px) {
   .activity-stream.welcome.inline-onboarding {
     overflow: auto;
   }
 }
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -500,17 +500,17 @@ export class ASRouterAdminInner extends 
 
   renderProviders() {
     const providersConfig = this.state.providerPrefs;
     const providerInfo = this.state.providers;
     const userPrefInfo = this.state.userPrefs;
 
     return (<table>{this.renderTableHead()}<tbody>
       {providersConfig.map((provider, i) => {
-        const isTestProvider = provider.id === "snippets_local_testing";
+        const isTestProvider = provider.id.includes("_local_testing");
         const info = providerInfo.find(p => p.id === provider.id) || {};
         const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;
         const isSystemEnabled = (isTestProvider || provider.enabled);
 
         let label = "local";
         if (provider.type === "remote") {
           label = (<span>endpoint (<a className="providerUrl" target="_blank" href={info.url} rel="noopener noreferrer">{info.url}</a>)</span>);
         } else if (provider.type === "remote-settings") {
@@ -694,16 +694,26 @@ export class ASRouterAdminInner extends 
         </thead>
         <tbody>{providersWithErrors.map(this.renderErrorMessage)}</tbody>
         </table>);
     }
 
     return <p>No errors</p>;
   }
 
+  renderTrailheadInfo() {
+    const {trailheadInterrupt, trailheadTriplet, trailheadInitialized} = this.state;
+    return trailheadInitialized ? (<table className="minimal-table">
+      <tbody>
+        <tr><td>Interrupt branch</td><td>{trailheadInterrupt}</td></tr>
+        <tr><td>Triplet branch</td><td>{trailheadTriplet}</td></tr>
+      </tbody>
+    </table>) : <p>Trailhead is not initialized. To update these values, load about:welcome.</p>;
+  }
+
   getSection() {
     const [section] = this.props.location.routes;
     switch (section) {
       case "targeting":
         return (<React.Fragment>
           <h2>Targeting Utilities</h2>
           <button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
           {this.renderTargetingParameters()}
@@ -724,16 +734,18 @@ export class ASRouterAdminInner extends 
           <h2>ASRouter Errors</h2>
           {this.renderErrors()}
           </React.Fragment>
         );
       default:
         return (<React.Fragment>
           <h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restore default prefs</button></h2>
           {this.state.providers ? this.renderProviders() : null}
+          <h2>Trailhead</h2>
+          {this.renderTrailheadInfo()}
           <h2>Messages</h2>
           {this.renderMessageFilter()}
           {this.renderMessages()}
           {this.renderPasteModal()}
         </React.Fragment>);
     }
   }
 
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.scss
@@ -1,16 +1,16 @@
 
 .asrouter-toggle {
   position: fixed;
   top: 15px;
   right: 48px;
   border: 0;
   background: none;
-  z-index: 3000;
+  z-index: 1;
   border-radius: 2px;
 
   .icon-devtools {
     background-image: url('chrome://browser/skin/developer.svg');
     padding: 15px;
   }
 
   &:hover {
@@ -77,16 +77,34 @@
     margin-inline-start: 5px;
     margin-bottom: 0;
   }
 
   table {
     border-collapse: collapse;
     width: 100%;
 
+    &.minimal-table {
+      border-collapse: collapse;
+
+      td {
+        padding: 8px;
+        border: 1px solid $border-color;
+      }
+
+      td:first-child {
+        width: 1%;
+        white-space: nowrap;
+      }
+
+      td:not(:first-child) {
+        font-family: $monospace;
+      }
+    }
+
     &.errorReporting {
       tr {
         border: 1px solid var(--newtab-textbox-background-color);
       }
 
       td {
         padding: 4px;
 
@@ -202,9 +220,8 @@
     margin-bottom: 20px;
   }
 
   .optOutNote {
     font-size: 12px;
     margin-inline-start: 4px;
   }
 }
-
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
@@ -1,16 +1,17 @@
 import React from "react";
 import ReactDOM from "react-dom";
 
 export class DSImage extends React.PureComponent {
   constructor(props) {
     super(props);
 
     this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
+    this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
 
     this.state = {
       isSeen: false,
       optimizedImageFailed: false,
     };
   }
 
   onSeen(entries) {
@@ -74,32 +75,41 @@ export class DSImage extends React.PureC
           source2x = this.reformatImageURL(
             baseSource,
             this.state.containerWidth * 2,
             this.state.containerHeight * 2
           );
 
           img = (<img onError={this.onOptimizedImageError} src={source} srcSet={`${source2x} 2x`} />);
         }
+      } else if (!this.state.nonOptimizedImageFailed) {
+        img = (<img onError={this.onNonOptimizedImageError} src={this.props.source} />);
       } else {
-        img = (<img src={this.props.source} />);
+        // Remove the img element if both sources fail. Render a placeholder instead.
+        img = (<div className="broken-image" />);
       }
     }
 
     return (
       <picture className={classNames}>{img}</picture>
     );
   }
 
   onOptimizedImageError() {
     // This will trigger a re-render and the unoptimized 450px image will be used as a fallback
     this.setState({
       optimizedImageFailed: true,
     });
   }
+
+  onNonOptimizedImageError() {
+    this.setState({
+      nonOptimizedImageFailed: true,
+    });
+  }
 }
 
 DSImage.defaultProps = {
   source: null, // The current source style from Pocket API (always 450px)
   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
 };
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.scss
@@ -1,13 +1,14 @@
 .ds-image {
   display: block;
   position: relative;
 
-  img {
+  img,
+  .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover;
   }
 }
--- a/browser/components/newtab/content-src/lib/selectLayoutRender.js
+++ b/browser/components/newtab/content-src/lib/selectLayoutRender.js
@@ -107,17 +107,17 @@ export const selectLayoutRender = (state
     }),
   })).filter(row => row.components.length);
 
   // Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected
   // by the `probability_selection` first, then gets chosen for the next position. For
   // all other SPOCS that never went through the probabilistic selection, its reason will
   // be "out_of_position".
   let spocsFill = [];
-  if (spocs.data && spocs.data.spocs) {
+  if (spocs.data.spocs) {
     const chosenSpocsFill = [...chosenSpocs]
       .map(spoc => ({id: spoc.id, reason: "n/a", displayed: 1, full_recalc: 0}));
     const unchosenSpocsFill = [...unchosenSpocs]
       .filter(spoc => !chosenSpocs.has(spoc))
       .map(spoc => ({id: spoc.id, reason: "probability_selection", displayed: 0, full_recalc: 0}));
     const outOfPositionSpocsFill = spocs.data.spocs.slice(spocIndex)
       .filter(spoc => !unchosenSpocs.has(spoc))
       .map(spoc => ({id: spoc.id, reason: "out_of_position", displayed: 0, full_recalc: 0}));
--- a/browser/components/newtab/content-src/styles/_activity-stream.scss
+++ b/browser/components/newtab/content-src/styles/_activity-stream.scss
@@ -8,16 +8,20 @@ html {
   height: 100%;
 }
 
 body,
 #root { // sass-lint:disable-line no-ids
   min-height: 100vh;
 }
 
+#root { // sass-lint:disable-line no-ids
+  position: relative;
+}
+
 body {
   background-color: var(--newtab-background-color);
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
   font-size: 16px;
   overflow-y: scroll;
 }
 
 .no-scroll {
--- a/browser/components/newtab/content-src/styles/_theme.scss
+++ b/browser/components/newtab/content-src/styles/_theme.scss
@@ -78,16 +78,17 @@ body {
   --newtab-snippets-background-color: #{$white};
   --newtab-snippets-hairline-color: transparent;
 
   // Trailhead
   --trailhead-header-text-color: #{$trailhead-purple};
   --trailhead-cards-background-color: #{$grey-20};
   --trailhead-card-button-background-color: #{$grey-90-10};
   --trailhead-card-button-background-hover-color: #{$grey-90-20};
+  --trailhead-card-button-background-active-color: #{$grey-90-30};
 
   &[lwt-newtab-brighttext] {
     // General styles
     --newtab-background-color: #{$grey-80};
     --newtab-border-primary-color: #{$grey-10-80};
     --newtab-border-secondary-color: #{$grey-10-10};
     --newtab-button-primary-color: #{$blue-60};
     --newtab-button-secondary-color: #{$grey-70};
@@ -143,10 +144,11 @@ body {
     --newtab-snippets-background-color: #{$grey-70};
     --newtab-snippets-hairline-color: #{$white-10};
 
     // Trailhead
     --trailhead-header-text-color: #{$white-60};
     --trailhead-cards-background-color: #{$grey-90-10};
     --trailhead-card-button-background-color: #{$grey-90-30};
     --trailhead-card-button-background-hover-color: #{$grey-90-40};
+    --trailhead-card-button-background-active-color: #{$grey-90-50};
   }
 }
--- a/browser/components/newtab/content-src/styles/_variables.scss
+++ b/browser/components/newtab/content-src/styles/_variables.scss
@@ -75,16 +75,17 @@
 $aw-extra-blue-2: #0080FF;
 $aw-extra-blue-3: #00C7FF;
 $about-welcome-gradient: linear-gradient(to bottom, $blue-70 40%, $aw-extra-blue-1 60%, $blue-60 80%, $aw-extra-blue-2 90%, $aw-extra-blue-3 100%);
 $about-welcome-extra-links: #676F7E;
 $firefox-wordmark-default-color: #363959;
 $firefox-wordmark-darktheme-color: $white;
 $trailhead-purple: #2B2156;
 $trailhead-blue-60: #0250BB;
+$trailhead-blue-70: #054096;
 
 // Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html
 $photon-easing: cubic-bezier(0.07, 0.95, 0, 1);
 
 $border-radius: 3px;
 
 // Grid related styles
 $base-gutter: 32px;
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -66,17 +66,18 @@ body {
   --newtab-card-hairline-color: rgba(0, 0, 0, 0.1);
   --newtab-card-placeholder-color: #D7D7DB;
   --newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
   --newtab-snippets-background-color: #FFF;
   --newtab-snippets-hairline-color: transparent;
   --trailhead-header-text-color: #2B2156;
   --trailhead-cards-background-color: #EDEDF0;
   --trailhead-card-button-background-color: rgba(12, 12, 13, 0.1);
-  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2); }
+  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2);
+  --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.3); }
   body[lwt-newtab-brighttext] {
     --newtab-background-color: #2A2A2E;
     --newtab-border-primary-color: rgba(249, 249, 250, 0.8);
     --newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
     --newtab-button-primary-color: #0060DF;
     --newtab-button-secondary-color: #38383D;
     --newtab-element-active-color: rgba(249, 249, 250, 0.2);
     --newtab-element-hover-color: rgba(249, 249, 250, 0.1);
@@ -114,17 +115,18 @@ body {
     --newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
     --newtab-card-placeholder-color: #4A4A4F;
     --newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
     --newtab-snippets-background-color: #38383D;
     --newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1);
     --trailhead-header-text-color: rgba(255, 255, 255, 0.6);
     --trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
     --trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
-    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4); }
+    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
+    --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
 
 .icon {
   background-position: center center;
   background-repeat: no-repeat;
   background-size: 16px;
   -moz-context-properties: fill;
   display: inline-block;
   fill: var(--newtab-icon-primary-color);
@@ -237,16 +239,19 @@ body {
 
 html {
   height: 100%; }
 
 body,
 #root {
   min-height: 100vh; }
 
+#root {
+  position: relative; }
+
 body {
   background-color: var(--newtab-background-color);
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
   font-size: 16px;
   overflow-y: scroll; }
 
 .no-scroll {
   overflow: hidden; }
@@ -1628,17 +1633,17 @@ main {
       display: none; } }
 
 .asrouter-toggle {
   position: fixed;
   top: 15px;
   right: 48px;
   border: 0;
   background: none;
-  z-index: 3000;
+  z-index: 1;
   border-radius: 2px; }
   .asrouter-toggle .icon-devtools {
     background-image: url("chrome://browser/skin/developer.svg");
     padding: 15px; }
   .asrouter-toggle:hover {
     background: var(--newtab-element-hover-color); }
   .asrouter-toggle.expanded {
     background: rgba(0, 0, 0, 0.2); }
@@ -1678,16 +1683,26 @@ main {
   .asrouter-admin h2 .button {
     font-size: 14px;
     padding: 6px 12px;
     margin-inline-start: 5px;
     margin-bottom: 0; }
   .asrouter-admin table {
     border-collapse: collapse;
     width: 100%; }
+    .asrouter-admin table.minimal-table {
+      border-collapse: collapse; }
+      .asrouter-admin table.minimal-table td {
+        padding: 8px;
+        border: 1px solid var(--newtab-border-secondary-color); }
+      .asrouter-admin table.minimal-table td:first-child {
+        width: 1%;
+        white-space: nowrap; }
+      .asrouter-admin table.minimal-table td:not(:first-child) {
+        font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", "Source Code Pro", monospace; }
     .asrouter-admin table.errorReporting tr {
       border: 1px solid var(--newtab-textbox-background-color); }
     .asrouter-admin table.errorReporting td {
       padding: 4px; }
       .asrouter-admin table.errorReporting td[rowspan] {
         border: 1px solid var(--newtab-textbox-background-color); }
   .asrouter-admin .sourceLabel {
     background: var(--newtab-textbox-background-color);
@@ -2643,17 +2658,18 @@ main {
     color: #737373;
     margin: 8px 0 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #D7D7DB; }
 
 .ds-image {
   display: block;
   position: relative; }
-  .ds-image img {
+  .ds-image img,
+  .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
 
 .ds-message {
@@ -3711,18 +3727,20 @@ a.firstrun-link {
     padding: 40px 60px; }
   .trailhead .trailheadContent h1 {
     font-size: 36px;
     font-weight: 200;
     line-height: 46px;
     margin: 0; }
   .trailhead .trailheadContent .trailheadLearn {
     display: block;
-    margin-top: 30px;
-    margin-inline-start: 74px; }
+    margin-top: 30px; }
+    @media (min-width: 850px) {
+      .trailhead .trailheadContent .trailheadLearn {
+        margin-inline-start: 74px; } }
   .trailhead.syncCohort {
     left: calc(50% - 430px);
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
@@ -3734,42 +3752,51 @@ a.firstrun-link {
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
     padding: 0; }
     .trailhead .trailheadBenefits li {
-      background-position: left 4px;
+      background-position: left 6px;
       background-repeat: no-repeat;
-      background-size: 62px;
+      background-size: 40px;
       -moz-context-properties: fill;
       fill: #0A84FF;
       list-style: none;
-      padding-inline-start: 74px; }
+      padding-top: 8px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits li {
+          background-position-y: 4px;
+          background-size: 62px;
+          margin-inline-end: 60px;
+          padding-inline-start: 74px; } }
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
     .trailhead .trailheadBenefits h3 {
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
-      margin-bottom: 4px; }
+      margin: 0 0 4px;
+      padding-inline-start: 52px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits h3 {
+          padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
-      margin: 4px 0 15px;
-      margin-inline-end: 60px; }
+      margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
     min-width: 260px;
     padding-top: 100px;
     text-align: center; }
     .trailhead .trailheadForm h3 {
       font-size: 36px;
       font-weight: 200;
@@ -3805,34 +3832,46 @@ a.firstrun-link {
       display: block;
       font-size: 15px;
       font-weight: 400;
       padding: 14px; }
       .trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
         background-color: #0250BB; }
       .trailhead .trailheadForm button:focus {
         outline: dotted 1px; }
+      .trailhead .trailheadForm button:active {
+        background-color: #054096; }
   .trailhead .trailheadStart {
     border: 1px solid rgba(255, 255, 255, 0.5);
     cursor: pointer;
     display: block;
     font-size: 15px;
     font-weight: 400;
     margin: 0 auto 40px;
     min-width: 300px;
     padding: 14px; }
     .trailhead .trailheadStart:hover, .trailhead .trailheadStart:focus {
       background-color: #0250BB;
       border-color: transparent; }
     .trailhead .trailheadStart:focus {
       outline: dotted 1px; }
+    .trailhead .trailheadStart:active {
+      background-color: #054096; }
+  .trailhead .trailheadInner,
+  .trailhead .trailheadStart {
+    animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
-  text-align: center; }
+  max-height: 1000px;
+  overflow: hidden;
+  text-align: center;
+  transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  .trailheadCards.collapsed {
+    max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
 .trailheadCardsInner {
   margin: auto;
@@ -3841,21 +3880,36 @@ a.firstrun-link {
     .trailheadCardsInner {
       width: 530px; } }
   @media (min-width: 866px) {
     .trailheadCardsInner {
       width: 786px; } }
   @media (min-width: 1122px) {
     .trailheadCardsInner {
       width: 1042px; } }
+  .trailheadCardsInner .icon-dismiss {
+    border: 0;
+    cursor: pointer;
+    inset-inline-end: 15px;
+    padding: 15px;
+    opacity: 0.75;
+    position: absolute;
+    top: 15px; }
+    .trailheadCardsInner .icon-dismiss:hover, .trailheadCardsInner .icon-dismiss:focus {
+      background-color: var(--newtab-element-hover-color); }
 
 .trailheadCardGrid {
   display: grid;
   grid-gap: 32px;
-  margin: 0; }
+  margin: 0;
+  opacity: 0;
+  transition: opacity 0.4s;
+  transition-delay: 0.1s; }
+  .trailheadCardGrid.show {
+    opacity: 1; }
   @media (min-width: 610px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 224px); } }
   @media (min-width: 1122px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 309px); } }
 
 .trailheadCard {
@@ -3887,24 +3941,31 @@ a.firstrun-link {
     background: var(--trailhead-card-button-background-color);
     border: 0;
     height: 30px;
     min-width: 70%;
     padding: 0 14px; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
+    .trailheadCard .onboardingButton:focus {
+      outline: dotted 1px; }
+    .trailheadCard .onboardingButton:active {
+      background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
+.inline-onboarding .asrouter-toggle {
+  position: absolute; }
+
 @media (max-height: 700px) {
   .activity-stream.welcome.inline-onboarding {
     overflow: auto; } }
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -69,17 +69,18 @@ body {
   --newtab-card-hairline-color: rgba(0, 0, 0, 0.1);
   --newtab-card-placeholder-color: #D7D7DB;
   --newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
   --newtab-snippets-background-color: #FFF;
   --newtab-snippets-hairline-color: transparent;
   --trailhead-header-text-color: #2B2156;
   --trailhead-cards-background-color: #EDEDF0;
   --trailhead-card-button-background-color: rgba(12, 12, 13, 0.1);
-  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2); }
+  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2);
+  --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.3); }
   body[lwt-newtab-brighttext] {
     --newtab-background-color: #2A2A2E;
     --newtab-border-primary-color: rgba(249, 249, 250, 0.8);
     --newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
     --newtab-button-primary-color: #0060DF;
     --newtab-button-secondary-color: #38383D;
     --newtab-element-active-color: rgba(249, 249, 250, 0.2);
     --newtab-element-hover-color: rgba(249, 249, 250, 0.1);
@@ -117,17 +118,18 @@ body {
     --newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
     --newtab-card-placeholder-color: #4A4A4F;
     --newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
     --newtab-snippets-background-color: #38383D;
     --newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1);
     --trailhead-header-text-color: rgba(255, 255, 255, 0.6);
     --trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
     --trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
-    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4); }
+    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
+    --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
 
 .icon {
   background-position: center center;
   background-repeat: no-repeat;
   background-size: 16px;
   -moz-context-properties: fill;
   display: inline-block;
   fill: var(--newtab-icon-primary-color);
@@ -240,16 +242,19 @@ body {
 
 html {
   height: 100%; }
 
 body,
 #root {
   min-height: 100vh; }
 
+#root {
+  position: relative; }
+
 body {
   background-color: var(--newtab-background-color);
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
   font-size: 16px;
   overflow-y: scroll; }
 
 .no-scroll {
   overflow: hidden; }
@@ -1631,17 +1636,17 @@ main {
       display: none; } }
 
 .asrouter-toggle {
   position: fixed;
   top: 15px;
   right: 48px;
   border: 0;
   background: none;
-  z-index: 3000;
+  z-index: 1;
   border-radius: 2px; }
   .asrouter-toggle .icon-devtools {
     background-image: url("chrome://browser/skin/developer.svg");
     padding: 15px; }
   .asrouter-toggle:hover {
     background: var(--newtab-element-hover-color); }
   .asrouter-toggle.expanded {
     background: rgba(0, 0, 0, 0.2); }
@@ -1681,16 +1686,26 @@ main {
   .asrouter-admin h2 .button {
     font-size: 14px;
     padding: 6px 12px;
     margin-inline-start: 5px;
     margin-bottom: 0; }
   .asrouter-admin table {
     border-collapse: collapse;
     width: 100%; }
+    .asrouter-admin table.minimal-table {
+      border-collapse: collapse; }
+      .asrouter-admin table.minimal-table td {
+        padding: 8px;
+        border: 1px solid var(--newtab-border-secondary-color); }
+      .asrouter-admin table.minimal-table td:first-child {
+        width: 1%;
+        white-space: nowrap; }
+      .asrouter-admin table.minimal-table td:not(:first-child) {
+        font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", "Source Code Pro", monospace; }
     .asrouter-admin table.errorReporting tr {
       border: 1px solid var(--newtab-textbox-background-color); }
     .asrouter-admin table.errorReporting td {
       padding: 4px; }
       .asrouter-admin table.errorReporting td[rowspan] {
         border: 1px solid var(--newtab-textbox-background-color); }
   .asrouter-admin .sourceLabel {
     background: var(--newtab-textbox-background-color);
@@ -2646,17 +2661,18 @@ main {
     color: #737373;
     margin: 8px 0 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #D7D7DB; }
 
 .ds-image {
   display: block;
   position: relative; }
-  .ds-image img {
+  .ds-image img,
+  .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
 
 .ds-message {
@@ -3714,18 +3730,20 @@ a.firstrun-link {
     padding: 40px 60px; }
   .trailhead .trailheadContent h1 {
     font-size: 36px;
     font-weight: 200;
     line-height: 46px;
     margin: 0; }
   .trailhead .trailheadContent .trailheadLearn {
     display: block;
-    margin-top: 30px;
-    margin-inline-start: 74px; }
+    margin-top: 30px; }
+    @media (min-width: 850px) {
+      .trailhead .trailheadContent .trailheadLearn {
+        margin-inline-start: 74px; } }
   .trailhead.syncCohort {
     left: calc(50% - 430px);
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
@@ -3737,42 +3755,51 @@ a.firstrun-link {
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
     padding: 0; }
     .trailhead .trailheadBenefits li {
-      background-position: left 4px;
+      background-position: left 6px;
       background-repeat: no-repeat;
-      background-size: 62px;
+      background-size: 40px;
       -moz-context-properties: fill;
       fill: #0A84FF;
       list-style: none;
-      padding-inline-start: 74px; }
+      padding-top: 8px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits li {
+          background-position-y: 4px;
+          background-size: 62px;
+          margin-inline-end: 60px;
+          padding-inline-start: 74px; } }
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
     .trailhead .trailheadBenefits h3 {
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
-      margin-bottom: 4px; }
+      margin: 0 0 4px;
+      padding-inline-start: 52px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits h3 {
+          padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
-      margin: 4px 0 15px;
-      margin-inline-end: 60px; }
+      margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
     min-width: 260px;
     padding-top: 100px;
     text-align: center; }
     .trailhead .trailheadForm h3 {
       font-size: 36px;
       font-weight: 200;
@@ -3808,34 +3835,46 @@ a.firstrun-link {
       display: block;
       font-size: 15px;
       font-weight: 400;
       padding: 14px; }
       .trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
         background-color: #0250BB; }
       .trailhead .trailheadForm button:focus {
         outline: dotted 1px; }
+      .trailhead .trailheadForm button:active {
+        background-color: #054096; }
   .trailhead .trailheadStart {
     border: 1px solid rgba(255, 255, 255, 0.5);
     cursor: pointer;
     display: block;
     font-size: 15px;
     font-weight: 400;
     margin: 0 auto 40px;
     min-width: 300px;
     padding: 14px; }
     .trailhead .trailheadStart:hover, .trailhead .trailheadStart:focus {
       background-color: #0250BB;
       border-color: transparent; }
     .trailhead .trailheadStart:focus {
       outline: dotted 1px; }
+    .trailhead .trailheadStart:active {
+      background-color: #054096; }
+  .trailhead .trailheadInner,
+  .trailhead .trailheadStart {
+    animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
-  text-align: center; }
+  max-height: 1000px;
+  overflow: hidden;
+  text-align: center;
+  transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  .trailheadCards.collapsed {
+    max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
 .trailheadCardsInner {
   margin: auto;
@@ -3844,21 +3883,36 @@ a.firstrun-link {
     .trailheadCardsInner {
       width: 530px; } }
   @media (min-width: 866px) {
     .trailheadCardsInner {
       width: 786px; } }
   @media (min-width: 1122px) {
     .trailheadCardsInner {
       width: 1042px; } }
+  .trailheadCardsInner .icon-dismiss {
+    border: 0;
+    cursor: pointer;
+    inset-inline-end: 15px;
+    padding: 15px;
+    opacity: 0.75;
+    position: absolute;
+    top: 15px; }
+    .trailheadCardsInner .icon-dismiss:hover, .trailheadCardsInner .icon-dismiss:focus {
+      background-color: var(--newtab-element-hover-color); }
 
 .trailheadCardGrid {
   display: grid;
   grid-gap: 32px;
-  margin: 0; }
+  margin: 0;
+  opacity: 0;
+  transition: opacity 0.4s;
+  transition-delay: 0.1s; }
+  .trailheadCardGrid.show {
+    opacity: 1; }
   @media (min-width: 610px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 224px); } }
   @media (min-width: 1122px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 309px); } }
 
 .trailheadCard {
@@ -3890,24 +3944,31 @@ a.firstrun-link {
     background: var(--trailhead-card-button-background-color);
     border: 0;
     height: 30px;
     min-width: 70%;
     padding: 0 14px; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
+    .trailheadCard .onboardingButton:focus {
+      outline: dotted 1px; }
+    .trailheadCard .onboardingButton:active {
+      background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
+.inline-onboarding .asrouter-toggle {
+  position: absolute; }
+
 @media (max-height: 700px) {
   .activity-stream.welcome.inline-onboarding {
     overflow: auto; } }
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -66,17 +66,18 @@ body {
   --newtab-card-hairline-color: rgba(0, 0, 0, 0.1);
   --newtab-card-placeholder-color: #D7D7DB;
   --newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
   --newtab-snippets-background-color: #FFF;
   --newtab-snippets-hairline-color: transparent;
   --trailhead-header-text-color: #2B2156;
   --trailhead-cards-background-color: #EDEDF0;
   --trailhead-card-button-background-color: rgba(12, 12, 13, 0.1);
-  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2); }
+  --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.2);
+  --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.3); }
   body[lwt-newtab-brighttext] {
     --newtab-background-color: #2A2A2E;
     --newtab-border-primary-color: rgba(249, 249, 250, 0.8);
     --newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
     --newtab-button-primary-color: #0060DF;
     --newtab-button-secondary-color: #38383D;
     --newtab-element-active-color: rgba(249, 249, 250, 0.2);
     --newtab-element-hover-color: rgba(249, 249, 250, 0.1);
@@ -114,17 +115,18 @@ body {
     --newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
     --newtab-card-placeholder-color: #4A4A4F;
     --newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
     --newtab-snippets-background-color: #38383D;
     --newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1);
     --trailhead-header-text-color: rgba(255, 255, 255, 0.6);
     --trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
     --trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
-    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4); }
+    --trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
+    --trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
 
 .icon {
   background-position: center center;
   background-repeat: no-repeat;
   background-size: 16px;
   -moz-context-properties: fill;
   display: inline-block;
   fill: var(--newtab-icon-primary-color);
@@ -237,16 +239,19 @@ body {
 
 html {
   height: 100%; }
 
 body,
 #root {
   min-height: 100vh; }
 
+#root {
+  position: relative; }
+
 body {
   background-color: var(--newtab-background-color);
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
   font-size: 16px;
   overflow-y: scroll; }
 
 .no-scroll {
   overflow: hidden; }
@@ -1628,17 +1633,17 @@ main {
       display: none; } }
 
 .asrouter-toggle {
   position: fixed;
   top: 15px;
   right: 48px;
   border: 0;
   background: none;
-  z-index: 3000;
+  z-index: 1;
   border-radius: 2px; }
   .asrouter-toggle .icon-devtools {
     background-image: url("chrome://browser/skin/developer.svg");
     padding: 15px; }
   .asrouter-toggle:hover {
     background: var(--newtab-element-hover-color); }
   .asrouter-toggle.expanded {
     background: rgba(0, 0, 0, 0.2); }
@@ -1678,16 +1683,26 @@ main {
   .asrouter-admin h2 .button {
     font-size: 14px;
     padding: 6px 12px;
     margin-inline-start: 5px;
     margin-bottom: 0; }
   .asrouter-admin table {
     border-collapse: collapse;
     width: 100%; }
+    .asrouter-admin table.minimal-table {
+      border-collapse: collapse; }
+      .asrouter-admin table.minimal-table td {
+        padding: 8px;
+        border: 1px solid var(--newtab-border-secondary-color); }
+      .asrouter-admin table.minimal-table td:first-child {
+        width: 1%;
+        white-space: nowrap; }
+      .asrouter-admin table.minimal-table td:not(:first-child) {
+        font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", "Source Code Pro", monospace; }
     .asrouter-admin table.errorReporting tr {
       border: 1px solid var(--newtab-textbox-background-color); }
     .asrouter-admin table.errorReporting td {
       padding: 4px; }
       .asrouter-admin table.errorReporting td[rowspan] {
         border: 1px solid var(--newtab-textbox-background-color); }
   .asrouter-admin .sourceLabel {
     background: var(--newtab-textbox-background-color);
@@ -2643,17 +2658,18 @@ main {
     color: #737373;
     margin: 8px 0 0; }
     [lwt-newtab-brighttext] .ds-card p {
       color: #D7D7DB; }
 
 .ds-image {
   display: block;
   position: relative; }
-  .ds-image img {
+  .ds-image img,
+  .ds-image .broken-image {
     background-color: var(--newtab-card-placeholder-color);
     position: absolute;
     top: 0;
     width: 100%;
     height: 100%;
     object-fit: cover; }
 
 .ds-message {
@@ -3711,18 +3727,20 @@ a.firstrun-link {
     padding: 40px 60px; }
   .trailhead .trailheadContent h1 {
     font-size: 36px;
     font-weight: 200;
     line-height: 46px;
     margin: 0; }
   .trailhead .trailheadContent .trailheadLearn {
     display: block;
-    margin-top: 30px;
-    margin-inline-start: 74px; }
+    margin-top: 30px; }
+    @media (min-width: 850px) {
+      .trailhead .trailheadContent .trailheadLearn {
+        margin-inline-start: 74px; } }
   .trailhead.syncCohort {
     left: calc(50% - 430px);
     width: 860px; }
     @media (max-width: 860px) {
       .trailhead.syncCohort {
         left: 0;
         width: 100%; } }
     .trailhead.syncCohort .trailheadInner {
@@ -3734,42 +3752,51 @@ a.firstrun-link {
       background-size: contain;
       height: 200px;
       margin-inline-end: 60px; }
     .trailhead.syncCohort .trailheadContent .trailheadLearn {
       margin-inline-start: 0; }
   .trailhead .trailheadBenefits {
     padding: 0; }
     .trailhead .trailheadBenefits li {
-      background-position: left 4px;
+      background-position: left 6px;
       background-repeat: no-repeat;
-      background-size: 62px;
+      background-size: 40px;
       -moz-context-properties: fill;
       fill: #0A84FF;
       list-style: none;
-      padding-inline-start: 74px; }
+      padding-top: 8px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits li {
+          background-position-y: 4px;
+          background-size: 62px;
+          margin-inline-end: 60px;
+          padding-inline-start: 74px; } }
       .trailhead .trailheadBenefits li:dir(rtl) {
         background-position-x: right; }
       .trailhead .trailheadBenefits li.knowledge {
         background-image: url("../data/content/assets/trailhead/benefit-knowledge.png"); }
       .trailhead .trailheadBenefits li.privacy {
         background-image: url("../data/content/assets/trailhead/benefit-privacy.png"); }
       .trailhead .trailheadBenefits li.products {
         background-image: url("../data/content/assets/trailhead/benefit-products.png"); }
     .trailhead .trailheadBenefits h3 {
       color: #CB9EFF;
       font-size: 22px;
       font-weight: 400;
-      margin-bottom: 4px; }
+      margin: 0 0 4px;
+      padding-inline-start: 52px; }
+      @media (min-width: 850px) {
+        .trailhead .trailheadBenefits h3 {
+          padding-inline-start: 0; } }
     .trailhead .trailheadBenefits p {
       color: #FFF;
       font-size: 15px;
       line-height: 22px;
-      margin: 4px 0 15px;
-      margin-inline-end: 60px; }
+      margin: 4px 0 15px; }
   .trailhead .trailheadForm {
     background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
     min-width: 260px;
     padding-top: 100px;
     text-align: center; }
     .trailhead .trailheadForm h3 {
       font-size: 36px;
       font-weight: 200;
@@ -3805,34 +3832,46 @@ a.firstrun-link {
       display: block;
       font-size: 15px;
       font-weight: 400;
       padding: 14px; }
       .trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
         background-color: #0250BB; }
       .trailhead .trailheadForm button:focus {
         outline: dotted 1px; }
+      .trailhead .trailheadForm button:active {
+        background-color: #054096; }
   .trailhead .trailheadStart {
     border: 1px solid rgba(255, 255, 255, 0.5);
     cursor: pointer;
     display: block;
     font-size: 15px;
     font-weight: 400;
     margin: 0 auto 40px;
     min-width: 300px;
     padding: 14px; }
     .trailhead .trailheadStart:hover, .trailhead .trailheadStart:focus {
       background-color: #0250BB;
       border-color: transparent; }
     .trailhead .trailheadStart:focus {
       outline: dotted 1px; }
+    .trailhead .trailheadStart:active {
+      background-color: #054096; }
+  .trailhead .trailheadInner,
+  .trailhead .trailheadStart {
+    animation: fadeIn 0.4s; }
 
 .trailheadCards {
   background: var(--trailhead-cards-background-color);
-  text-align: center; }
+  max-height: 1000px;
+  overflow: hidden;
+  text-align: center;
+  transition: max-height 0.5s cubic-bezier(0.07, 0.95, 0, 1); }
+  .trailheadCards.collapsed {
+    max-height: 0; }
   .trailheadCards h1 {
     font-size: 36px;
     font-weight: 200;
     margin: 0 0 40px;
     color: var(--trailhead-header-text-color); }
 
 .trailheadCardsInner {
   margin: auto;
@@ -3841,21 +3880,36 @@ a.firstrun-link {
     .trailheadCardsInner {
       width: 530px; } }
   @media (min-width: 866px) {
     .trailheadCardsInner {
       width: 786px; } }
   @media (min-width: 1122px) {
     .trailheadCardsInner {
       width: 1042px; } }
+  .trailheadCardsInner .icon-dismiss {
+    border: 0;
+    cursor: pointer;
+    inset-inline-end: 15px;
+    padding: 15px;
+    opacity: 0.75;
+    position: absolute;
+    top: 15px; }
+    .trailheadCardsInner .icon-dismiss:hover, .trailheadCardsInner .icon-dismiss:focus {
+      background-color: var(--newtab-element-hover-color); }
 
 .trailheadCardGrid {
   display: grid;
   grid-gap: 32px;
-  margin: 0; }
+  margin: 0;
+  opacity: 0;
+  transition: opacity 0.4s;
+  transition-delay: 0.1s; }
+  .trailheadCardGrid.show {
+    opacity: 1; }
   @media (min-width: 610px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 224px); } }
   @media (min-width: 1122px) {
     .trailheadCardGrid {
       grid-template-columns: repeat(auto-fit, 309px); } }
 
 .trailheadCard {
@@ -3887,24 +3941,31 @@ a.firstrun-link {
     background: var(--trailhead-card-button-background-color);
     border: 0;
     height: 30px;
     min-width: 70%;
     padding: 0 14px; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
+    .trailheadCard .onboardingButton:focus {
+      outline: dotted 1px; }
+    .trailheadCard .onboardingButton:active {
+      background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
     height: 60px;
     position: absolute;
     bottom: 0;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding .outer-wrapper {
   position: relative; }
   .inline-onboarding .outer-wrapper .prefs-button button {
     position: absolute; }
 
+.inline-onboarding .asrouter-toggle {
+  position: absolute; }
+
 @media (max-height: 700px) {
   .activity-stream.welcome.inline-onboarding {
     overflow: auto; } }
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -356,17 +356,17 @@ function ASRouterUserEvent(data) {
   return AlsoToMain({
     type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT,
     data
   });
 }
 /**
  * DiscoveryStreamSpocsFill - A telemetry ping indicating a SPOCS Fill event.
  *
- * @param  {object} data Fields to include in the ping (spocs_fills, etc.)
+ * @param  {object} data Fields to include in the ping (spoc_fills, etc.)
  * @param  {int} importContext (For testing) Override the import context for testing.
  * @return {object} An AlsoToMain action
  */
 
 
 function DiscoveryStreamSpocsFill(data, importContext = globalImportContext) {
   const action = {
     type: actionTypes.DISCOVERY_STREAM_SPOCS_FILL,
@@ -1375,17 +1375,17 @@ class ASRouterAdminInner extends react__
     }, "Last Updated")));
   }
 
   renderProviders() {
     const providersConfig = this.state.providerPrefs;
     const providerInfo = this.state.providers;
     const userPrefInfo = this.state.userPrefs;
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", null, this.renderTableHead(), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, providersConfig.map((provider, i) => {
-      const isTestProvider = provider.id === "snippets_local_testing";
+      const isTestProvider = provider.id.includes("_local_testing");
       const info = providerInfo.find(p => p.id === provider.id) || {};
       const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;
       const isSystemEnabled = isTestProvider || provider.enabled;
       let label = "local";
 
       if (provider.type === "remote") {
         label = react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("span", null, "endpoint (", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
           className: "providerUrl",
@@ -1607,16 +1607,27 @@ class ASRouterAdminInner extends react__
       return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", {
         className: "errorReporting"
       }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("thead", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tr", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("th", null, "Provider ID"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("th", null, "Message"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("th", null, "Timestamp"))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, providersWithErrors.map(this.renderErrorMessage)));
     }
 
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "No errors");
   }
 
+  renderTrailheadInfo() {
+    const {
+      trailheadInterrupt,
+      trailheadTriplet,
+      trailheadInitialized
+    } = this.state;
+    return trailheadInitialized ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", {
+      className: "minimal-table"
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tbody", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tr", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Interrupt branch"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, trailheadInterrupt)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tr", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Triplet branch"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, trailheadTriplet)))) : react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "Trailhead is not initialized. To update these values, load about:welcome.");
+  }
+
   getSection() {
     const [section] = this.props.location.routes;
 
     switch (section) {
       case "targeting":
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Targeting Utilities"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
           className: "button",
           onClick: this.expireCache
@@ -1635,17 +1646,17 @@ class ASRouterAdminInner extends react__
       case "errors":
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "ASRouter Errors"), this.renderErrors());
 
       default:
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Message Providers ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
           title: "Restore all provider settings that ship with Firefox",
           className: "button",
           onClick: this.resetPref
-        }, "Restore default prefs")), this.state.providers ? this.renderProviders() : null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Messages"), this.renderMessageFilter(), this.renderMessages(), this.renderPasteModal());
+        }, "Restore default prefs")), this.state.providers ? this.renderProviders() : null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Trailhead"), this.renderTrailheadInfo(), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Messages"), this.renderMessageFilter(), this.renderMessages(), this.renderPasteModal());
     }
   }
 
   render() {
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: `asrouter-admin ${this.props.collapsed ? "collapsed" : "expanded"}`
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("aside", {
       className: "sidebar"
@@ -3247,42 +3258,49 @@ const StartupOverlay = Object(react_redu
 module.exports = ReactRedux;
 
 /***/ }),
 /* 27 */
 /***/ (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; });
+/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Trailhead", function() { return _Trailhead; });
+/* 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__(15);
-/* harmony import */ var _OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11);
-/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_3__);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
+/* harmony import */ var _components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(15);
+/* harmony import */ var _OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(14);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(11);
+/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
 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", // These are finalized strings exposed to localizers
 "browser/newtab/onboarding.ftl", // These are WIP/in-development strings that only get used if the string
 // doesn't already exist in onboarding.ftl above
 "trailhead.ftl"];
-class Trailhead extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureComponent {
+class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___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.onSubmit = this.onSubmit.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
     this.state = {
       emailInput: "",
       isModalOpen: true,
+      showCardPanel: true,
+      showCards: false,
       flowId: "",
       flowBeginTime: 0
     };
     this.didFetch = false;
   }
 
   async componentWillMount() {
     FLUENT_FILES.forEach(file => {
@@ -3328,16 +3346,25 @@ class Trailhead extends react__WEBPACK_I
     }
   }
 
   componentDidMount() {
     // We need to remove hide-main since we should show it underneath everything that has rendered
     global.document.body.classList.remove("hide-main"); // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
 
     global.document.body.classList.add("inline-onboarding");
+
+    if (!this.props.message.content) {
+      // No modal overlay, let the user scroll and deal them some cards.
+      global.document.body.classList.remove("welcome");
+
+      if (this.props.message.includeBundle || this.props.message.cards) {
+        this.revealCards();
+      }
+    }
   }
 
   componentDidUnmount() {
     global.document.body.classList.remove("inline-onboarding");
   }
 
   onInputChange(e) {
     let error = e.target.previousSibling;
@@ -3357,16 +3384,17 @@ class Trailhead extends react__WEBPACK_I
   }
 
   closeModal() {
     global.removeEventListener("visibilitychange", this.closeModal);
     global.document.body.classList.remove("welcome");
     this.setState({
       isModalOpen: false
     });
+    this.revealCards();
     this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
       event: "SKIPPED_SIGNIN",
       ...this._getFormInfo()
     }));
   }
   /**
    * Report to telemetry additional information about the form submission.
    */
@@ -3385,141 +3413,174 @@ class Trailhead extends react__WEBPACK_I
     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
+    });
+  }
+
+  revealCards() {
+    this.setState({
+      showCards: true
+    });
+  }
+
+  getStringValue(str) {
+    if (str.property_id) {
+      str.value = this.props.intl.formatMessage({
+        id: str.property_id
+      });
+    }
+
+    return str.value;
+  }
+
   render() {
     const {
       props
     } = this;
     const {
       bundle: cards,
       content
     } = props.message;
-    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: `trailhead ${content.className}`,
+    const innerClassName = ["trailhead", content && content.className].filter(v => v).join(" ");
+    return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, this.state.isModalOpen && content ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_2__["ModalOverlayWrapper"], {
+      innerClassName: innerClassName,
       onClose: this.closeModal
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "trailheadInner"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "trailheadContent"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h1", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h1", {
       "data-l10n-id": content.title.string_id
-    }, content.title.value), content.subtitle && react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+    }, this.getStringValue(content.title)), content.subtitle && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": content.subtitle.string_id
-    }, content.subtitle.value), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("ul", {
+    }, this.getStringValue(content.subtitle)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("ul", {
       className: "trailheadBenefits"
-    }, content.benefits.map(item => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("li", {
+    }, content.benefits.map(item => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("li", {
       key: item.id,
       className: item.id
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", {
       "data-l10n-id": item.title.string_id
-    }, item.title.value), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+    }, this.getStringValue(item.title)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": item.text.string_id
-    }, item.text.value)))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+    }, this.getStringValue(item.text))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       className: "trailheadLearn",
       "data-l10n-id": content.learn.text.string_id,
       href: content.learn.url
-    }, content.learn.text.value)), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+    }, this.getStringValue(content.learn.text))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "trailheadForm"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h3", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", {
       "data-l10n-id": content.form.title.string_id
-    }, content.form.title.value), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+    }, this.getStringValue(content.form.title)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": content.form.text.string_id
-    }, content.form.text.value), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("form", {
+    }, this.getStringValue(content.form.text)), react__WEBPACK_IMPORTED_MODULE_4___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", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "service",
       type: "hidden",
       value: "sync"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "action",
       type: "hidden",
       value: "email"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "context",
       type: "hidden",
       value: "fx_desktop_v3"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "entrypoint",
       type: "hidden",
       value: "activity-stream-firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "utm_source",
       type: "hidden",
       value: "activity-stream"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "utm_campaign",
       type: "hidden",
       value: "firstrun"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "utm_term",
       type: "hidden",
       value: "trailhead"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "flow_id",
       type: "hidden",
       value: this.state.flowId
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       name: "flow_begin_time",
       type: "hidden",
       value: this.state.flowBeginTime
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       "data-l10n-id": "onboarding-join-form-email-error",
       className: "error"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
       "data-l10n-id": content.form.email.string_id,
-      placeholder: content.form.email.placeholder,
+      placeholder: this.getStringValue(content.form.email),
       name: "email",
       type: "email",
       required: "true",
       onInvalid: this.onInputInvalid,
       onChange: this.onInputChange
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("p", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       className: "trailheadTerms",
       "data-l10n-id": "onboarding-join-form-legal"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       "data-l10n-name": "terms",
       href: "https://accounts.firefox.com/legal/terms"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
       "data-l10n-name": "privacy",
       href: "https://accounts.firefox.com/legal/privacy"
-    })), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
+    })), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
       "data-l10n-id": content.form.button.string_id,
       type: "submit"
-    }, content.form.button.value)))), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("button", {
+    }, this.getStringValue(content.form.button))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
       className: "trailheadStart",
       "data-l10n-id": content.skipButton.string_id,
       onClick: this.closeModal
-    }, content.skipButton.value)) : null, cards && cards.length ? react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: "trailheadCards"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
+    }, this.getStringValue(content.skipButton))) : null, cards && cards.length ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
+      className: `trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "trailheadCardsInner"
-    }, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("h1", {
+    }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h1", {
       "data-l10n-id": "onboarding-welcome-header"
-    }), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("div", {
-      className: "trailheadCardGrid"
-    }, cards.map(card => react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_2__["OnboardingCard"], _extends({
+    }), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
+      className: `trailheadCardGrid${this.state.showCards ? " show" : ""}`
+    }, cards.map(card => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_3__["OnboardingCard"], _extends({
       key: card.id,
       className: "trailheadCard",
       sendUserActionTelemetry: props.sendUserActionTelemetry,
       onAction: props.onAction,
       UISurface: "TRAILHEAD"
-    }, card)))))) : null);
-  }
-
-}
+    }, card)))), this.state.showCardPanel && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
+      className: "icon icon-dismiss",
+      onClick: this.hideCardPanel,
+      title: props.intl.formatMessage({
+        id: "menu_action_dismiss"
+      }),
+      "aria-label": props.intl.formatMessage({
+        id: "menu_action_dismiss"
+      })
+    }))) : null);
+  }
+
+}
+const Trailhead = Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(_Trailhead);
 /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
 
 /***/ }),
 /* 28 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
@@ -7420,16 +7481,17 @@ var external_ReactDOM_default = /*#__PUR
 
 // CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
 
 
 class DSImage_DSImage extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
+    this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
     this.state = {
       isSeen: false,
       optimizedImageFailed: false
     };
   }
 
   onSeen(entries) {
     if (this.state) {
@@ -7483,35 +7545,47 @@ class DSImage_DSImage extends external_R
           source = this.reformatImageURL(baseSource, this.state.containerWidth, this.state.containerHeight);
           source2x = this.reformatImageURL(baseSource, this.state.containerWidth * 2, this.state.containerHeight * 2);
           img = external_React_default.a.createElement("img", {
             onError: this.onOptimizedImageError,
             src: source,
             srcSet: `${source2x} 2x`
           });
         }
-      } else {
+      } else if (!this.state.nonOptimizedImageFailed) {
         img = external_React_default.a.createElement("img", {
+          onError: this.onNonOptimizedImageError,
           src: this.props.source
         });
+      } else {
+        // Remove the img element if both sources fail. Render a placeholder instead.
+        img = external_React_default.a.createElement("div", {
+          className: "broken-image"
+        });
       }
     }
 
     return external_React_default.a.createElement("picture", {
       className: classNames
     }, img);
   }
 
   onOptimizedImageError() {
     // This will trigger a re-render and the unoptimized 450px image will be used as a fallback
     this.setState({
       optimizedImageFailed: true
     });
   }
 
+  onNonOptimizedImageError() {
+    this.setState({
+      nonOptimizedImageFailed: true
+    });
+  }
+
 }
 DSImage_DSImage.defaultProps = {
   source: null,
   // The current source style from Pocket API (always 450px)
   rawSource: null,
   // Unadulterated image URL to filter through Thumbor
   extraClassNames: null,
   // Additional classnames to append to component
@@ -8364,17 +8438,17 @@ const selectLayoutRender = (state, prefs
     })
   })).filter(row => row.components.length); // Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected
   // by the `probability_selection` first, then gets chosen for the next position. For
   // all other SPOCS that never went through the probabilistic selection, its reason will
   // be "out_of_position".
 
   let spocsFill = [];
 
-  if (spocs.data && spocs.data.spocs) {
+  if (spocs.data.spocs) {
     const chosenSpocsFill = [...chosenSpocs].map(spoc => ({
       id: spoc.id,
       reason: "n/a",
       displayed: 1,
       full_recalc: 0
     }));
     const unchosenSpocsFill = [...unchosenSpocs].filter(spoc => !chosenSpocs.has(spoc)).map(spoc => ({
       id: spoc.id,
@@ -11707,17 +11781,17 @@ class CachedIterable {
 
     if (seen.length === 0 || seen[seen.length - 1].done === false) {
       seen.push(iterator.next());
     }
   }
 
 }
 // CONCATENATED MODULE: ./node_modules/fluent/src/fallback.js
-function _asyncIterator(iterable) { var method; if (typeof Symbol === "function") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
+function _asyncIterator(iterable) { var method; if (typeof Symbol !== "undefined") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
 
 /*
  * @overview
  *
  * Functions for managing ordered sequences of MessageContexts.
  *
  * An ordered iterable of MessageContext instances can represent the current
  * negotiated fallback chain of languages.  This iterable can be used to find
@@ -12515,18 +12589,17 @@ const TOP_SITES_DEFAULT_ROWS = 1;
 const TOP_SITES_MAX_SITES_PER_ROW = 8;
 const dedupe = new Dedupe(site => site && site.url);
 const INITIAL_STATE = {
   App: {
     // Have we received real data from the app yet?
     initialized: false
   },
   ASRouter: {
-    initialized: false,
-    allowLegacySnippets: null
+    initialized: false
   },
   Snippets: {
     initialized: false
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
@@ -12600,21 +12673,16 @@ function App(prevState = INITIAL_STATE.A
 
 function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
   switch (action.type) {
     case Actions["actionTypes"].AS_ROUTER_INITIALIZED:
       return { ...action.data,
         initialized: true
       };
 
-    case Actions["actionTypes"].AS_ROUTER_PREF_CHANGED:
-      return { ...prevState,
-        ...action.data
-      };
-
     default:
       return prevState;
   }
 }
 /**
  * insertPinned - Inserts pinned links in their specified slots
  *
  * @param {array} a list of links
--- a/browser/components/newtab/data/trailhead.wip
+++ b/browser/components/newtab/data/trailhead.wip
@@ -72,17 +72,20 @@ onboarding-ghostery-title = Ghostery
 onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
 
 # Note: "Sync" in this case is a generic verb, as in "to synchronize"
 onboarding-fxa-title = Sync
 onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
 
 onboarding-tracking-protection-title = Control How You’re Tracked
 onboarding-tracking-protection-text = Don’t like when ads follow you around? { -brand-short-name } helps you control how advertisers track your activity online.
-onboarding-tracking-protection-button = Learn More
+onboarding-tracking-protection-button = { PLATFORM() ->
+  [windows] Update Options
+  *[other] Update Preferences
+}
 
 onboarding-data-sync-title = Take Your Settings with You
 # "Sync" is short for synchronize.
 onboarding-data-sync-text = Sync your bookmarks and passwords everywhere you use { -brand-product-name }.
 onboarding-data-sync-button = Turn on { -sync-brand-short-name }
 
 onboarding-firefox-monitor-title = Stay Alert to Data Breaches
 onboarding-firefox-monitor-text = { -monitor-brand-name } monitors if your email has appeared in a data breach and alerts you if it appears in a new breach.
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -8,50 +8,94 @@ const {XPCOMUtils} = ChromeUtils.import(
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   UITour: "resource:///modules/UITour.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
+  SnippetsTestMessageProvider: "resource://activity-stream/lib/SnippetsTestMessageProvider.jsm",
+  PanelTestProvider: "resource://activity-stream/lib/PanelTestProvider.jsm",
 });
 const {ASRouterActions: ra, actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
 const {CFRMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/CFRMessageProvider.jsm");
 const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm");
-const {SnippetsTestMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/SnippetsTestMessageProvider.jsm");
 const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js");
 const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm");
+const {AttributionCode} = ChromeUtils.import("resource:///modules/AttributionCode.jsm");
 
 ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
   "resource://activity-stream/lib/ASRouterPreferences.jsm");
 ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
   "resource://activity-stream/lib/ASRouterTargeting.jsm");
 ChromeUtils.defineModuleGetter(this, "QueryCache",
   "resource://activity-stream/lib/ASRouterTargeting.jsm");
 ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
   "resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
-const {AttributionCode} = ChromeUtils.import("resource:///modules/AttributionCode.jsm");
+ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
+  "resource://gre/modules/TelemetryEnvironment.jsm");
+ChromeUtils.defineModuleGetter(this, "ClientEnvironment",
+  "resource://normandy/lib/ClientEnvironment.jsm");
+ChromeUtils.defineModuleGetter(this, "Sampling",
+  "resource://gre/modules/components-utils/Sampling.jsm");
+
+const TRAILHEAD_CONFIG = {
+  OVERRIDE_PREF: "trailhead.firstrun.branches",
+  DID_SEE_ABOUT_WELCOME_PREF: "trailhead.firstrun.didSeeAboutWelcome",
+  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: [
+    ["", 1],
+    ["interrupts", 1],
+    ["triplets", 1],
+  ],
+};
 
 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 = {
   "activity-stream-icons.services.mozilla.com": "production",
   "snippets-admin.mozilla.org": "preview",
 };
 const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
 // Max possible impressions cap for any message
 const MAX_MESSAGE_LIFETIME_CAP = 100;
 
-const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider, SnippetsTestMessageProvider};
+const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider};
 const STARTPAGE_VERSION = "6";
 
+/**
+ * chooseBranch<T> -  Choose an item from a list of "branches" pseudorandomly using a seed / ratio configuration
+ * @param seed {string} A unique seed for the randomizer
+ * @param branches {Array<[T, number?]>} A list of branches, where branch[0] is any item and branch[1] is the ratio
+ * @returns {T} An randomly chosen item in a branch
+ */
+async function chooseBranch(seed, branches) {
+  const ratios = branches.map(([item, ratio]) => ((typeof ratio !== "undefined") ? ratio : 1));
+  return branches[await Sampling.ratioSample(seed, ratios)][0];
+}
+
 const MessageLoaderUtils = {
   STARTPAGE_VERSION,
   REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
   _errors: [],
 
   reportError(e) {
     Cu.reportError(e);
     this._errors.push({timestamp: new Date(), error: {message: e.toString(), stack: e.stack}});
@@ -339,33 +383,36 @@ class _ASRouter {
     this._resetInitialization();
     this._state = {
       lastMessageId: null,
       providers: [],
       messageBlockList: [],
       providerBlockList: [],
       messageImpressions: {},
       providerImpressions: {},
+      trailheadInitialized: false,
+      trailheadInterrupt: "",
+      trailheadTriplet: "",
       messages: [],
       errors: [],
     };
     this._triggerHandler = this._triggerHandler.bind(this);
     this._localProviders = localProviders;
     this.onMessage = this.onMessage.bind(this);
     this.handleMessageRequest = this.handleMessageRequest.bind(this);
     this.addImpression = this.addImpression.bind(this);
     this._handleTargetingError = this._handleTargetingError.bind(this);
     this.onPrefChange = this.onPrefChange.bind(this);
   }
 
   // Update message providers and fetch new messages on pref change
   async onPrefChange() {
+    this._loadLocalProviders();
     this._updateMessageProviders();
     await this.loadMessagesFromAllProviders();
-    this.dispatchToAS(ac.BroadcastToContent({type: at.AS_ROUTER_PREF_CHANGED, data: ASRouterPreferences.specialConditions}));
   }
 
   // Replace all frequency time period aliases with their millisecond values
   // This allows us to avoid accounting for special cases later on
   normalizeItemFrequency({frequency}) {
     if (frequency && frequency.custom) {
       for (const setting of frequency.custom) {
         if (setting.period === "daily") {
@@ -519,16 +566,21 @@ class _ASRouter {
     this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
     this.dispatchToAS = dispatchToAS;
     this.dispatch = this.dispatch.bind(this);
 
     ASRouterPreferences.init();
     ASRouterPreferences.addListener(this.onPrefChange);
     BookmarkPanelHub.init(this.handleMessageRequest, this.addImpression);
 
+    this._loadLocalProviders();
+
+    // We need to check whether to set up telemetry for trailhead
+    await this.setupTrailhead();
+
     const messageBlockList = await this._storage.get("messageBlockList") || [];
     const providerBlockList = await this._storage.get("providerBlockList") || [];
     const messageImpressions = await this._storage.get("messageImpressions") || {};
     const providerImpressions = await this._storage.get("providerImpressions") || {};
     const previousSessionEnd = await this._storage.get("previousSessionEnd") || 0;
     await this.setState({messageBlockList, providerBlockList, messageImpressions, providerImpressions, previousSessionEnd});
     this._updateMessageProviders();
     await this.loadMessagesFromAllProviders();
@@ -576,16 +628,27 @@ class _ASRouter {
   }
 
   _onStateChanged(state) {
     if (ASRouterPreferences.devtoolsEnabled) {
       this._updateAdminState();
     }
   }
 
+  _loadLocalProviders() {
+    // If we're in ASR debug mode add the local test providers
+    if (ASRouterPreferences.devtoolsEnabled) {
+      this._localProviders = {
+        ...this._localProviders,
+        SnippetsTestMessageProvider,
+        PanelTestProvider,
+      };
+    }
+  }
+
   /**
    * Used by ASRouter Admin returns all ASRouterTargeting.Environment
    * and ASRouter._getMessagesContext parameters and values
    */
   async getTargetingParameters(environment, localContext) {
     const targetingParameters = {};
     for (const param of Object.keys(environment)) {
       targetingParameters[param] = await environment[param];
@@ -618,23 +681,94 @@ class _ASRouter {
         message_id: message.id,
         action: "asrouter_undesired_event",
         event: "TARGETING_EXPRESSION_ERROR",
         value: type,
       }));
     }
   }
 
+  /**
+   * _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;
+
+    // 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("-");
+      return {experiment, interrupt, triplet: triplet || ""};
+    }
+
+    const locale = Services.locale.appLocaleAsLangTag;
+
+    if (TRAILHEAD_CONFIG.LOCALES.includes(locale)) {
+      const {userId} = ClientEnvironment;
+      experiment = await chooseBranch(`${userId}-trailhead-experiments`, TRAILHEAD_CONFIG.EXPERIMENT_RATIOS);
+
+      // For the interrupts experiment,
+      // we randomly assign an interrupt and always use the "supercharge" triplet.
+      if (experiment === "interrupts") {
+        interrupt =  await chooseBranch(`${userId}-interrupts-branch`, TRAILHEAD_CONFIG.BRANCHES.interrupts);
+        if (["join", "sync", "cards"].includes(interrupt)) {
+          triplet = "supercharge";
+        }
+
+      // For the triplets experiment or non-experiment experience,
+      // we randomly assign a triplet and always use the "join" interrupt.
+      } else {
+        interrupt = "join";
+        triplet = await chooseBranch(`${userId}-triplets-branch`, TRAILHEAD_CONFIG.BRANCHES.triplets);
+      }
+    } else {
+      // If the user is not in a trailhead-compabtible locale, return the control experience and no experiment.
+      interrupt = "control";
+    }
+
+    return {experiment, interrupt, triplet};
+  }
+
+  async setupTrailhead() {
+    // Don't initialize
+    if (this.state.trailheadInitialized || !Services.prefs.getBoolPref(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF, false)) {
+      return;
+    }
+
+    const {experiment, interrupt, triplet} = await this._generateTrailheadBranches();
+    await this.setState({trailheadInitialized: true, trailheadInterrupt: interrupt, trailheadTriplet: triplet});
+
+    if (experiment) {
+      TelemetryEnvironment.setExperimentActive(
+        // In order for ping centre to pick this up, it MUST start with activity-stream
+        `activity-stream-firstrun-trailhead-${experiment}`,
+        experiment === "interrupts" ? interrupt : triplet,
+        {type: "as-firstrun"}
+      );
+    }
+  }
+
   // Return an object containing targeting parameters used to select messages
   _getMessagesContext() {
-    const {previousSessionEnd} = this.state;
+    const {previousSessionEnd, trailheadInterrupt, trailheadTriplet} = this.state;
+
     return {
       get previousSessionEnd() {
         return previousSessionEnd;
       },
+      get trailheadInterrupt() {
+        return trailheadInterrupt;
+      },
+      get trailheadTriplet() {
+        return trailheadTriplet;
+      },
     };
   }
 
   _findMessage(candidateMessages, trigger) {
     const messages = candidateMessages.filter(m => this.isBelowFrequencyCaps(m));
     const context = this._getMessagesContext();
 
      // Find a message that matches the targeting context as well as the trigger context (if one is provided)
@@ -781,16 +915,39 @@ class _ASRouter {
     let {state} = this;
     return state.messages.filter(item =>
       !state.messageBlockList.includes(item.id) &&
       (!item.campaign || !state.messageBlockList.includes(item.campaign)) &&
       !state.providerBlockList.includes(item.provider)
     );
   }
 
+  /**
+   * Route messages based on template to the correct module that can display them
+   */
+  routeMessageToTarget(message, target, trigger, force = false) {
+    switch (message.template) {
+      case "cfr_doorhanger":
+        if (force) {
+          CFRPageActions.forceRecommendation(target, message, this.dispatch);
+        } else {
+          CFRPageActions.addRecommendation(target, trigger.param.host, message, this.dispatch);
+        }
+        break;
+      case "fxa_bookmark_panel":
+        if (force) {
+          BookmarkPanelHub._forceShowMessage(message);
+        }
+        break;
+      default:
+        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
+        break;
+    }
+  }
+
   async _sendMessageToTarget(message, target, trigger, force = false) {
     // No message is available, so send CLEAR_ALL.
     if (!message) {
       try {
         target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
       } catch (e) {}
 
     // For bundled messages, look for the rest of the bundle or else send CLEAR_ALL
@@ -802,29 +959,19 @@ class _ASRouter {
       } catch (e) {}
 
     // For nested bundled messages, look for the desired bundle
     } else if (message.includeBundle) {
       const bundledMessages = await this._getBundledMessages(message, target, message.includeBundle.trigger, force);
       try {
         target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: {...message, bundle: bundledMessages && bundledMessages.bundle}});
       } catch (e) {}
-
-    // CFR doorhanger
-    } else if (message.template === "cfr_doorhanger") {
-      if (force) {
-        CFRPageActions.forceRecommendation(target, message, this.dispatch);
-      } else {
-        CFRPageActions.addRecommendation(target, trigger.param.host, message, this.dispatch);
-      }
-
-    // New tab single messages
     } else {
       try {
-        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
+        this.routeMessageToTarget(message, target, trigger, force);
       } catch (e) {}
     }
   }
 
   async addImpression(message) {
     const provider = this.state.providers.find(p => p.id === message.provider);
     // We only need to store impressions for messages that have frequency, or
     // that have providers that have frequency
@@ -942,21 +1089,17 @@ class _ASRouter {
     const msgs = this._getUnblockedMessages();
     return this._findMessage(msgs.filter(m => m.trigger && m.trigger.id === trigger.id), trigger);
   }
 
   async setMessageById(id, target, force = true, action = {}) {
     await this.setState({lastMessageId: id});
     const newMessage = this.getMessageById(id);
 
-    if (newMessage && newMessage.provider === "cfr-fxa") {
-      BookmarkPanelHub._forceShowMessage(newMessage);
-    } else {
-      await this._sendMessageToTarget(newMessage, target, action.data, force);
-    }
+    await this._sendMessageToTarget(newMessage, target, action.data, force);
   }
 
   async blockMessageById(idOrIds) {
     const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
 
     await this.setState(state => {
       const messageBlockList = [...state.messageBlockList];
       const messageImpressions = {...state.messageImpressions};
@@ -1155,30 +1298,38 @@ class _ASRouter {
         break;
     }
   }
 
   dispatch(action, target) {
     this.onMessage({data: action, target});
   }
 
+  /* 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
         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(target, (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
@@ -1271,16 +1422,18 @@ class _ASRouter {
         break;
       default:
         Cu.reportError("Unknown message received");
         break;
     }
   }
 }
 this._ASRouter = _ASRouter;
+this.chooseBranch = chooseBranch;
+this.TRAILHEAD_CONFIG = TRAILHEAD_CONFIG;
 
 /**
  * ASRouter - singleton instance of _ASRouter that controls all messages
  * in the new tab page.
  */
 this.ASRouter = new _ASRouter();
 
-const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils"];
+const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils", "chooseBranch", "TRAILHEAD_CONFIG"];
--- a/browser/components/newtab/lib/ASRouterPreferences.jsm
+++ b/browser/components/newtab/lib/ASRouterPreferences.jsm
@@ -22,22 +22,27 @@ const MIGRATE_PREFS = [
 ];
 
 const USER_PREFERENCES = {
   snippets: "browser.newtabpage.activity-stream.feeds.snippets",
   cfrAddons: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
   cfrFeatures: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
 };
 
-const TEST_PROVIDER = {
+const TEST_PROVIDERS = [{
   id: "snippets_local_testing",
   type: "local",
   localProvider: "SnippetsTestMessageProvider",
   enabled: true,
-};
+}, {
+  id: "panel_local_testing",
+  type: "local",
+  localProvider: "PanelTestProvider",
+  enabled: true,
+}];
 
 class _ASRouterPreferences {
   constructor() {
     Object.assign(this, DEFAULT_STATE);
     this._callbacks = new Set();
   }
 
   _getProviderConfig() {
@@ -74,17 +79,17 @@ class _ASRouterPreferences {
     }
   }
 
   get providers() {
     if (!this._initialized || this._providers === null) {
       const config = this._getProviderConfig();
       const providers = config.map(provider => Object.freeze(provider));
       if (this.devtoolsEnabled) {
-        providers.unshift(TEST_PROVIDER);
+        providers.unshift(...TEST_PROVIDERS);
       }
       this._providers = Object.freeze(providers);
     }
 
     return this._providers;
   }
 
   enableOrDisableProvider(id, value) {
@@ -109,26 +114,16 @@ class _ASRouterPreferences {
 
   get devtoolsEnabled() {
     if (!this._initialized || this._devtoolsEnabled === null) {
       this._devtoolsEnabled = Services.prefs.getBoolPref(this._devtoolsPref, false);
     }
     return this._devtoolsEnabled;
   }
 
-  get specialConditions() {
-    let allowLegacySnippets = true;
-    for (const provider of this.providers) {
-      if (provider.id === "snippets" && provider.enabled) {
-        allowLegacySnippets = false;
-      }
-    }
-    return {allowLegacySnippets};
-  }
-
   observe(aSubject, aTopic, aPrefName) {
     if (aPrefName && aPrefName.startsWith(this._providerPrefBranch)) {
       this._providers = null;
     } else if (aPrefName === this._devtoolsPref) {
       this._providers = null;
       this._devtoolsEnabled = null;
     }
     this._callbacks.forEach(cb => cb(aPrefName));
@@ -187,11 +182,11 @@ class _ASRouterPreferences {
     }
     Object.assign(this, DEFAULT_STATE);
     this._callbacks.clear();
   }
 }
 this._ASRouterPreferences = _ASRouterPreferences;
 
 this.ASRouterPreferences = new _ASRouterPreferences();
-this.TEST_PROVIDER = TEST_PROVIDER;
+this.TEST_PROVIDERS = TEST_PROVIDERS;
 
-const EXPORTED_SYMBOLS = ["_ASRouterPreferences", "ASRouterPreferences", "TEST_PROVIDER"];
+const EXPORTED_SYMBOLS = ["_ASRouterPreferences", "ASRouterPreferences", "TEST_PROVIDERS"];
--- a/browser/components/newtab/lib/ASRouterTargeting.jsm
+++ b/browser/components/newtab/lib/ASRouterTargeting.jsm
@@ -14,16 +14,17 @@ ChromeUtils.defineModuleGetter(this, "Sh
 ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
   "resource://gre/modules/TelemetryEnvironment.jsm");
 ChromeUtils.defineModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 ChromeUtils.defineModuleGetter(this, "AttributionCode",
   "resource:///modules/AttributionCode.jsm");
 
 const FXA_USERNAME_PREF = "services.sync.username";
+const FXA_ENABLED_PREF = "identity.fxaccounts.enabled";
 const SEARCH_REGION_PREF = "browser.search.region";
 const MOZ_JEXL_FILEPATH = "mozjexl";
 
 const {activityStreamProvider: asProvider} = NewTabUtils;
 
 const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
 const FRECENT_SITES_IGNORE_BLOCKED = false;
 const FRECENT_SITES_NUM_ITEMS = 25;
@@ -167,19 +168,16 @@ function sortMessagesByTargeting(message
       return 1;
     }
 
     return 0;
   });
 }
 
 const TargetingGetters = {
-  get trailheadCohort() {
-    return Services.prefs.getIntPref("trailhead.firstrun.cohort", 0);
-  },
   get locale() {
     return Services.locale.appLocaleAsLangTag;
   },
   get localeLanguageCode() {
     return Services.locale.appLocaleAsLangTag && Services.locale.appLocaleAsLangTag.substr(0, 2);
   },
   get browserSettings() {
     const {settings} = TelemetryEnvironment.currentEnvironment;
@@ -200,16 +198,19 @@ const TargetingGetters = {
     return ProfileAge().then(times => times.created);
   },
   get profileAgeReset() {
     return ProfileAge().then(times => times.reset);
   },
   get usesFirefoxSync() {
     return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
   },
+  get isFxAEnabled() {
+    return Services.prefs.getBoolPref(FXA_ENABLED_PREF, true);
+  },
   get sync() {
     return {
       desktopDevices: Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0),
       mobileDevices: Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0),
       totalDevices: Services.prefs.getIntPref("services.sync.numClients", 0),
     };
   },
   get xpinstallEnabled() {
--- a/browser/components/newtab/lib/ActivityStream.jsm
+++ b/browser/components/newtab/lib/ActivityStream.jsm
@@ -115,17 +115,17 @@ const PREFS_CONFIG = new Map([
   }],
   ["telemetry.ut.events", {
     title: "Enable Unified Telemetry event data collection",
     value: AppConstants.EARLY_BETA_OR_EARLIER,
     value_local_dev: false,
   }],
   ["telemetry.structuredIngestion", {
     title: "Enable Structured Ingestion Telemetry data collection",
-    value: AppConstants.EARLY_BETA_OR_EARLIER,
+    value: true,
     value_local_dev: false,
   }],
   ["telemetry.structuredIngestion.endpoint", {
     title: "Structured Ingestion telemetry server endpoint",
     value: "https://incoming.telemetry.mozilla.org/submit/activity-stream",
   }],
   ["telemetry.ping.endpoint", {
     title: "Telemetry server endpoint",
--- a/browser/components/newtab/lib/BookmarkPanelHub.jsm
+++ b/browser/components/newtab/lib/BookmarkPanelHub.jsm
@@ -17,16 +17,17 @@ class _BookmarkPanelHub {
     this._handleMessageRequest = null;
     this._addImpression = null;
     this._initalized = false;
     this._response = null;
     this._l10n = null;
 
     this.messageRequest = this.messageRequest.bind(this);
     this.toggleRecommendation = this.toggleRecommendation.bind(this);
+    this.collapseMessage = this.collapseMessage.bind(this);
   }
 
   /**
    * @param {function} handleMessageRequest
    * @param {function} addImpression
    */
   init(handleMessageRequest, addImpression) {
     this._handleMessageRequest = handleMessageRequest;
@@ -51,32 +52,34 @@ class _BookmarkPanelHub {
    * to ASRouter. Caches only 1 request, unique identifier is `request.url`.
    * Caching ensures we don't duplicate requests and telemetry pings.
    * Return value is important for the caller to know if a message will be
    * shown.
    *
    * @returns {obj|null} response object or null if no messages matched
    */
   async messageRequest(target, win) {
-    if (this._response && this._response.url === target.url) {
-      return this.onResponse(this._response, target, win);
+    if (this._response && this._response.url === target.url && this._response.content) {
+      this.showMessage(this._response.content, target, win);
+      return true;
     }
 
     const response = await this._handleMessageRequest(this._trigger);
 
     return this.onResponse(response, target, win);
   }
 
   /**
    * If the response contains a message render it and send an impression.
    * Otherwise we remove the message from the container.
    */
   onResponse(response, target, win) {
     this._response = {
       ...response,
+      collapsed: false,
       target,
       win,
       url: target.url,
     };
 
     if (response && response.content) {
       this.showMessage(response.content, target, win);
       this.sendImpression();
@@ -85,16 +88,21 @@ class _BookmarkPanelHub {
     }
 
     target.infoButton.disabled = !response;
 
     return !!response;
   }
 
   showMessage(message, target, win) {
+    if (this._response.collapsed) {
+      this.toggleRecommendation(false);
+      return;
+    }
+
     const createElement = elem => target.document.createElementNS("http://www.w3.org/1999/xhtml", elem);
 
     if (!target.container.querySelector("#cfrMessageContainer")) {
       const recommendation = createElement("div");
       recommendation.setAttribute("id", "cfrMessageContainer");
       recommendation.addEventListener("click", async e => {
         target.hidePopup();
         const url = await FxAccounts.config.promiseEmailFirstURI("bookmark");
@@ -105,17 +113,20 @@ class _BookmarkPanelHub {
         });
       });
       recommendation.style.color = message.color;
       recommendation.style.background = `-moz-linear-gradient(-45deg, ${message.background_color_1} 0%, ${message.background_color_2} 70%)`;
       const close = createElement("a");
       close.setAttribute("id", "cfrClose");
       close.setAttribute("aria-label", "close");
       this._l10n.setAttributes(close, message.close_button.tooltiptext);
-      close.addEventListener("click", target.close);
+      close.addEventListener("click", e => {
+        this.collapseMessage();
+        target.close(e);
+      });
       const title = createElement("h1");
       title.setAttribute("id", "editBookmarkPanelRecommendationTitle");
       this._l10n.setAttributes(title, message.title);
       const content = createElement("p");
       content.setAttribute("id", "editBookmarkPanelRecommendationContent");
       this._l10n.setAttributes(content, message.text);
       const cta = createElement("button");
       cta.setAttribute("id", "editBookmarkPanelRecommendationCta");
@@ -128,35 +139,48 @@ class _BookmarkPanelHub {
       target.container.appendChild(recommendation);
     }
 
     this.toggleRecommendation(true);
   }
 
   toggleRecommendation(visible) {
     const {target} = this._response;
-    target.infoButton.checked = visible !== undefined ? !!visible : !target.infoButton.checked;
+    if (visible === undefined) {
+      // When called from the info button of the bookmark panel
+      target.infoButton.checked = !target.infoButton.checked;
+    } else {
+      target.infoButton.checked = visible;
+    }
     if (target.infoButton.checked) {
+      // If it was ever collapsed we need to cancel the state
+      this._response.collapsed = false;
       target.recommendationContainer.removeAttribute("disabled");
     } else {
       target.recommendationContainer.setAttribute("disabled", "disabled");
     }
   }
 
+  collapseMessage() {
+    this._response.collapsed = true;
+    this.toggleRecommendation(false);
+  }
+
   hideMessage(target) {
     const container = target.container.querySelector("#cfrMessageContainer");
     if (container) {
       container.remove();
     }
     this.toggleRecommendation(false);
+    this._response = null;
   }
 
   _forceShowMessage(message) {
+    this.toggleRecommendation(true);
     this.showMessage(message.content, this._response.target, this._response.win);
-    this._response.target.infoButton.disabled = false;
   }
 
   sendImpression() {
     this._addImpression(this._response);
   }
 }
 
 this._BookmarkPanelHub = _BookmarkPanelHub;
--- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
+++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 const {NewTabUtils} = ChromeUtils.import("resource://gre/modules/NewTabUtils.jsm");
+const {setTimeout, clearTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 ChromeUtils.defineModuleGetter(this, "perfService", "resource://activity-stream/common/PerfService.jsm");
 const {UserDomainAffinityProvider} = ChromeUtils.import("resource://activity-stream/lib/UserDomainAffinityProvider.jsm");
 
 const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
 const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm");
 
@@ -17,16 +18,17 @@ const CACHE_KEY = "discovery_stream";
 const LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
 const COMPONENT_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const SPOCS_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
 const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour
 const MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours
 const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server
 const DEFAULT_MAX_HISTORY_QUERY_RESULTS = 1000;
+const FETCH_TIMEOUT = 45 * 1000;
 const PREF_CONFIG = "discoverystream.config";
 const PREF_ENDPOINTS = "discoverystream.endpoints";
 const PREF_OPT_OUT = "discoverystream.optOut.0";
 const PREF_SHOW_SPONSORED = "showSponsored";
 const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
 const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
 
 let defaultLayoutResp;
@@ -136,20 +138,27 @@ this.DiscoveryStreamFeed = class Discove
 
     try {
       // Make sure the requested endpoint is allowed
       const allowed = this.store.getState().Prefs.values[PREF_ENDPOINTS].split(",");
       if (!allowed.some(prefix => endpoint.startsWith(prefix))) {
         throw new Error(`Not one of allowed prefixes (${allowed})`);
       }
 
-      const response = await fetch(endpoint, {credentials: "omit"});
+      const controller = new AbortController();
+      const {signal} = controller;
+      const fetchPromise = fetch(endpoint, {credentials: "omit", signal});
+      // istanbul ignore next
+      const timeoutId = setTimeout(() => { controller.abort(); }, FETCH_TIMEOUT);
+
+      const response = await fetchPromise;
       if (!response.ok) {
         throw new Error(`Unexpected status (${response.status})`);
       }
+      clearTimeout(timeoutId);
       return response.json();
     } catch (error) {
       Cu.reportError(`Failed to fetch ${endpoint}: ${error.message}`);
     }
     return null;
   }
 
   /**
@@ -223,29 +232,29 @@ this.DiscoveryStreamFeed = class Discove
         Cu.reportError("No response for response.layout prop");
       }
     }
     return layout;
   }
 
   async loadLayout(sendUpdate, isStartup) {
     let layout = {};
-    if (this.config.hardcoded_layout) {
-      layout = {lastUpdate: Date.now(), ...defaultLayoutResp};
-    } else {
+    if (!this.config.hardcoded_layout) {
       layout = await this.fetchLayout(isStartup);
     }
 
-    if (layout && layout.layout) {
-      sendUpdate({
-        type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
-        data: layout,
-      });
+    if (!layout || !layout.layout) {
+      layout = {lastUpdate: Date.now(), ...defaultLayoutResp};
     }
-    if (layout && layout.spocs && layout.spocs.url) {
+
+    sendUpdate({
+      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
+      data: layout,
+    });
+    if (layout.spocs && layout.spocs.url) {
       sendUpdate({
         type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,
         data: layout.spocs.url,
       });
     }
   }
 
   /**
@@ -383,17 +392,17 @@ this.DiscoveryStreamFeed = class Discove
         }
       }
     }
 
     // Use good data if we have it, otherwise nothing.
     // We can have no data if spocs set to off.
     // We can have no data if request fails and there is no good cache.
     // We want to send an update spocs or not, so client can render something.
-    spocs = spocs || {
+    spocs = spocs && spocs.data ? spocs : {
       lastUpdated: Date.now(),
       data: {},
     };
 
     let {data, filtered: frequencyCapped} = this.frequencyCapSpocs(spocs.data);
     let {data: newSpocs, filtered} = this.transform(data);
 
     sendUpdate({
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -94,17 +94,17 @@ const ONBOARDING_MESSAGES = async () => 
       primary_button: {
         label: {string_id: "onboarding-button-label-try-now"},
         action: {
           type: "OPEN_ABOUT_PAGE",
           data: {args: "addons"},
         },
       },
     },
-    targeting: "trailheadCohort == 0 && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
+    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: {
@@ -114,17 +114,17 @@ const ONBOARDING_MESSAGES = async () => 
       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: "trailheadCohort == 0 && providerCohorts.onboarding == 'ghostery'",
+    targeting: "trailheadInterrupt == 'control' && providerCohorts.onboarding == 'ghostery'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "ONBOARDING_5",
     template: "onboarding",
     bundled: 3,
     order: 4,
     content: {
@@ -134,23 +134,23 @@ const ONBOARDING_MESSAGES = async () => 
       primary_button: {
         label: {string_id: "onboarding-button-label-get-started"},
         action: {
           type: "OPEN_URL",
           data: {args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"), where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort == 0 && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
+    targeting: "trailheadInterrupt == 'control' && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_1",
     template: "trailhead",
-    targeting: "trailheadCohort == 1",
+    targeting: "trailheadInterrupt == 'join'",
     trigger: {id: "firstRun"},
     includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
     content: {
       className: "joinCohort",
       title: {string_id: "onboarding-welcome-body"},
       benefits: ["products", "knowledge", "privacy"].map(id => (
         {
           id,
@@ -169,159 +169,165 @@ const ONBOARDING_MESSAGES = async () => 
         button: {string_id: "onboarding-join-form-continue"},
       },
       skipButton: {string_id: "onboarding-start-browsing-button-label"},
     },
   },
   {
     id: "TRAILHEAD_2",
     template: "trailhead",
-    targeting: "trailheadCohort == 2",
+    targeting: "trailheadInterrupt == 'sync'",
     trigger: {id: "firstRun"},
     includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
     content: {
       className: "syncCohort",
-      title: {value: "Take Firefox with You"},
-      subtitle: {value: "Get your bookmarks, history, passwords and other settings on all your devices."},
+      title: {property_id: "firstrun_title"},
+      subtitle: {property_id: "firstrun_content"},
       benefits: [],
       learn: {
-        text: {string_id: "onboarding-welcome-learn-more"},
+        text: {property_id: "firstrun_learn_more_link"},
         url: "https://www.mozilla.org/firefox/accounts/",
       },
       form: {
-        title: {value: "Enter your email"},
-        text: {value: "to continue to Firefox Sync"},
-        email: {placeholder: "Email"},
-        button: {string_id: "onboarding-join-form-continue"},
+        title: {property_id: "firstrun_form_header"},
+        text: {property_id: "firstrun_form_sub_header"},
+        email: {property_id: "firstrun_email_input_placeholder"},
+        button: {property_id: "firstrun_continue_to_login"},
       },
-      skipButton: {value: "Skip this step"},
+      skipButton: {property_id: "firstrun_skip_login"},
     },
   },
   {
     id: "TRAILHEAD_3",
     template: "trailhead",
-    targeting: "trailheadCohort == 3",
+    targeting: "trailheadInterrupt == 'cards'",
     trigger: {id: "firstRun"},
     includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
   },
   {
     id: "TRAILHEAD_4",
     template: "trailhead",
-    targeting: "trailheadCohort == 4",
+    targeting: "trailheadInterrupt == 'nofirstrun'",
     trigger: {id: "firstRun"},
   },
   {
     id: "TRAILHEAD_CARD_1",
     template: "onboarding",
     bundled: 3,
+    order: 2,
     content: {
       title: {string_id: "onboarding-tracking-protection-title"},
       text: {string_id: "onboarding-tracking-protection-text"},
       icon: "tracking",
       primary_button: {
         label: {string_id: "onboarding-tracking-protection-button"},
         action: {
           type: "OPEN_PREFERENCES_PAGE",
           data: {category: "privacy-trackingprotection"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'privacy'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_2",
     template: "onboarding",
     bundled: 3,
+    order: 2,
     content: {
       title: {string_id: "onboarding-data-sync-title"},
       text: {string_id: "onboarding-data-sync-text"},
       icon: "devices",
       primary_button: {
         label: {string_id: "onboarding-data-sync-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'supercharge'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_3",
     template: "onboarding",
     bundled: 3,
+    order: 3,
     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",
           data: {args: "https://monitor.firefox.com/", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet in ['payoff', 'supercharge']",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_4",
     template: "onboarding",
     bundled: 3,
+    order: 1,
     content: {
       title: {string_id: "onboarding-private-browsing-title"},
       text: {string_id: "onboarding-private-browsing-text"},
       icon: "private",
       primary_button: {
         label: {string_id: "onboarding-private-browsing-button"},
         action: {type: "OPEN_PRIVATE_BROWSER_WINDOW"},
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'privacy'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_5",
     template: "onboarding",
     bundled: 3,
+    order: 5,
     content: {
       title: {string_id: "onboarding-firefox-send-title"},
       text: {string_id: "onboarding-firefox-send-text"},
       icon: "ffsend",
       primary_button: {
         label: {string_id: "onboarding-firefox-send-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://send.firefox.com/?utm_source=activity-stream?utm_medium=referral?utm_campaign=firstrun", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'payoff'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_6",
     template: "onboarding",
     bundled: 3,
+    order: 1,
     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",
           data: {args: "https://www.mozilla.org/firefox/mobile/", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet in ['supercharge', 'multidevice']",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_7",
     template: "onboarding",
     bundled: 3,
     content: {
       title: {string_id: "onboarding-privacy-right-title"},
@@ -330,93 +336,97 @@ const ONBOARDING_MESSAGES = async () => 
       primary_button: {
         label: {string_id: "onboarding-privacy-right-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://www.mozilla.org/?privacy-right", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'unused'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_8",
     template: "onboarding",
     bundled: 3,
+    order: 3,
     content: {
       title: {string_id: "onboarding-send-tabs-title"},
       text: {string_id: "onboarding-send-tabs-text"},
       icon: "sendtab",
       primary_button: {
         label: {string_id: "onboarding-send-tabs-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://blog.mozilla.org/firefox/send-tabs-a-better-way/", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'multidevice'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_9",
     template: "onboarding",
     bundled: 3,
+    order: 2,
     content: {
       title: {string_id: "onboarding-pocket-anywhere-title"},
       text: {string_id: "onboarding-pocket-anywhere-text"},
       icon: "pocket",
       primary_button: {
         label: {string_id: "onboarding-pocket-anywhere-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://getpocket.com/firefox_learnmore", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'multidevice'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_10",
     template: "onboarding",
     bundled: 3,
+    order: 3,
     content: {
       title: {string_id: "onboarding-lockwise-passwords-title"},
       text: {string_id: "onboarding-lockwise-passwords-text"},
       icon: "lockwise",
       primary_button: {
         label: {string_id: "onboarding-lockwise-passwords-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://lockwise.firefox.com/", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'privacy'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "TRAILHEAD_CARD_11",
     template: "onboarding",
     bundled: 3,
+    order: 4,
     content: {
       title: {string_id: "onboarding-facebook-container-title"},
       text: {string_id: "onboarding-facebook-container-text"},
       icon: "fbcont",
       primary_button: {
         label: {string_id: "onboarding-facebook-container-button"},
         action: {
           type: "OPEN_URL",
           data: {args: "https://addons.mozilla.org/firefox/addon/facebook-container/", where: "tabshifted"},
         },
       },
     },
-    targeting: "trailheadCohort > 0",
+    targeting: "trailheadTriplet == 'payoff'",
     trigger: {id: "showOnboarding"},
   },
   {
     id: "FXA_1",
     template: "fxa_overlay",
     trigger: {id: "firstRun"},
   },
   {
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/lib/PanelTestProvider.jsm
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const MESSAGES = () => ([
+  {
+    "id": "SIMPLE_FXA_BOOKMARK_TEST_1",
+    "template": "fxa_bookmark_panel",
+    "content": {
+      "title": "cfr-doorhanger-bookmark-fxa-header",
+      "text": "cfr-doorhanger-bookmark-fxa-body",
+      "cta": "cfr-doorhanger-bookmark-fxa-link-text",
+      "color": "white",
+      "background_color_1": "#7d31ae",
+      "background_color_2": "#5033be",
+      "info_icon": {
+        "tooltiptext": "cfr-doorhanger-bookmark-fxa-info-icon-tooltip",
+      },
+      "close_button": {
+        "tooltiptext": "cfr-doorhanger-bookmark-fxa-close-btn-tooltip",
+      },
+    },
+    "trigger": {"id": "bookmark-panel"},
+  },
+]);
+
+const PanelTestProvider = {
+  getMessages() {
+    return MESSAGES()
+      .map(message => ({...message, targeting: `true`}));
+  },
+};
+this.PanelTestProvider = PanelTestProvider;
+
+const EXPORTED_SYMBOLS = ["PanelTestProvider"];
--- a/browser/components/newtab/lib/TelemetryFeed.jsm
+++ b/browser/components/newtab/lib/TelemetryFeed.jsm
@@ -341,16 +341,20 @@ this.TelemetryFeed = class TelemetryFeed
       return;
     }
 
     this.sendDiscoveryStreamLoadedContent(portID, session);
     this.sendDiscoveryStreamImpressions(portID, session);
 
     if (session.perf.visibility_event_rcvd_ts) {
       session.session_duration = Math.round(perfService.absNow() - session.perf.visibility_event_rcvd_ts);
+    } else {
+      // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either.
+      this.sessions.delete(portID);
+      return;
     }
 
     let sessionEndEvent = this.createSessionEndEvent(session);
     this.sendEvent(sessionEndEvent);
     this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent);
     this.sessions.delete(portID);
   }
 
@@ -839,17 +843,17 @@ this.TelemetryFeed = class TelemetryFeed
    *          reason: "n/a",
    *          full_recalc: 1
    *        }
    *      ]
    *    }
    */
   handleDiscoveryStreamSpocsFill(data) {
     const payload = this.createSpocsFillPing(data);
-    this.sendStructuredIngestionEvent(payload, "spocs-fills", "1");
+    this.sendStructuredIngestionEvent(payload, "spoc-fills", "1");
   }
 
   /**
    * Take all enumerable members of the data object and merge them into
    * the session.perf object for the given port, so that it is sent to the
    * server when the session ends.  All members of the data object should
    * be valid values of the perf object, as defined in pings.js and the
    * data*.md documentation.
--- a/browser/components/newtab/locales-src/ach/strings.properties
+++ b/browser/components/newtab/locales-src/ach/strings.properties
@@ -88,16 +88,17 @@ section_disclaimer_topstories_buttontext
 # in English, while "Home" should be localized matching the about:preferences
 # sidebar mozilla-central string for the panel that has preferences related to
 # what is shown for the homepage, new windows, and new tabs.
 prefs_home_header=Jami me Acakki Firefox
 prefs_home_description=Yer jami ma imito ii kio me Acakki Firefox.
 
 prefs_content_discovery_header=Acakki me Firefox
 
+
 # LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
 # plural forms used in a drop down of multiple row options (1 row, 2 rows).
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 prefs_search_header=Yeny me kakube
 prefs_topsites_description=Kakube ma ilimo loyo
 prefs_topstories_description2=Jami mabeco loyo ki ii kakube, kiyubo piri
 prefs_topstories_options_sponsored_label=Lok ma kicwako
 prefs_topstories_sponsored_learn_more=Nong ngec mapol
@@ -170,16 +171,17 @@ error_fallback_default_info=Aii, gin mo 
 error_fallback_default_refresh_suggestion=Nwo cano potbuk me temo odoco.
 
 # LOCALIZATION NOTE (section_menu_action_*).  These strings are displayed in the section
 # context menu and are meant as a call to action for the given section.
 section_menu_action_remove_section=Kwany bute
 section_menu_action_collapse_section=Kan bute
 section_menu_action_expand_section=Yar bute
 section_menu_action_manage_section=Lo bute
+section_menu_action_manage_webext=Lo Lamed
 section_menu_action_add_topsite=Med Kakube maloyo
 section_menu_action_add_search_engine=Med ingin me yeny
 section_menu_action_move_up=Kob Malo
 section_menu_action_move_down=Kob Piny
 section_menu_action_privacy_notice=Ngec me mung
 
 # LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
 # firstrun of the browser, they give an introduction to Firefox and Sync.
--- a/browser/components/newtab/package-lock.json
+++ b/browser/components/newtab/package-lock.json
@@ -9,35 +9,104 @@
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
       "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
       "dev": true,
       "requires": {
         "@babel/highlight": "^7.0.0"
       }
     },
     "@babel/core": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.3.4.tgz",
-      "integrity": "sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==",
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.4.tgz",
+      "integrity": "sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.3.4",
-        "@babel/helpers": "^7.2.0",
-        "@babel/parser": "^7.3.4",
-        "@babel/template": "^7.2.2",
-        "@babel/traverse": "^7.3.4",
-        "@babel/types": "^7.3.4",
+        "@babel/generator": "^7.4.4",
+        "@babel/helpers": "^7.4.4",
+        "@babel/parser": "^7.4.4",
+        "@babel/template": "^7.4.4",
+        "@babel/traverse": "^7.4.4",
+        "@babel/types": "^7.4.4",
         "convert-source-map": "^1.1.0",
         "debug": "^4.1.0",
         "json5": "^2.1.0",
         "lodash": "^4.17.11",
         "resolve": "^1.3.2",
         "semver": "^5.4.1",
         "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "@babel/generator": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",
+          "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4",
+            "jsesc": "^2.5.1",
+            "lodash": "^4.17.11",
+            "source-map": "^0.5.0",
+            "trim-right": "^1.0.1"
+          }
+        },
+        "@babel/helper-split-export-declaration": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+          "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/parser": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz",
+          "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==",
+          "dev": true
+        },
+        "@babel/template": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/traverse": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz",
+          "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/generator": "^7.4.4",
+            "@babel/helper-function-name": "^7.1.0",
+            "@babel/helper-split-export-declaration": "^7.4.4",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0",
+            "lodash": "^4.17.11"
+          }
+        },
+        "@babel/types": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
+          "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2",
+            "lodash": "^4.17.11",
+            "to-fast-properties": "^2.0.0"
+          }
+        }
       }
     },
     "@babel/generator": {
       "version": "7.3.4",
       "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz",
       "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==",
       "dev": true,
       "requires": {
@@ -123,24 +192,93 @@
       "requires": {
         "@babel/helper-function-name": "^7.1.0",
         "@babel/template": "^7.1.0",
         "@babel/traverse": "^7.1.0",
         "@babel/types": "^7.2.0"
       }
     },
     "@babel/helpers": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz",
-      "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==",
-      "dev": true,
-      "requires": {
-        "@babel/template": "^7.1.2",
-        "@babel/traverse": "^7.1.5",
-        "@babel/types": "^7.3.0"
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz",
+      "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.4.4",
+        "@babel/traverse": "^7.4.4",
+        "@babel/types": "^7.4.4"
+      },
+      "dependencies": {
+        "@babel/generator": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",
+          "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4",
+            "jsesc": "^2.5.1",
+            "lodash": "^4.17.11",
+            "source-map": "^0.5.0",
+            "trim-right": "^1.0.1"
+          }
+        },
+        "@babel/helper-split-export-declaration": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+          "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/parser": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz",
+          "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==",
+          "dev": true
+        },
+        "@babel/template": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/traverse": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz",
+          "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/generator": "^7.4.4",
+            "@babel/helper-function-name": "^7.1.0",
+            "@babel/helper-split-export-declaration": "^7.4.4",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0",
+            "lodash": "^4.17.11"
+          }
+        },
+        "@babel/types": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
+          "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2",
+            "lodash": "^4.17.11",
+            "to-fast-properties": "^2.0.0"
+          }
+        }
       }
     },
     "@babel/highlight": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
       "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
       "dev": true,
       "requires": {
@@ -220,44 +358,52 @@
       "integrity": "sha512-A32OkKTp4i5U6aE88GwwcuV4HAprUgHcTq0sSafLxjr6AW0QahrCRCjxogkbbcdtpbXkuTOlgpjophCxb6sh5g==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.0.0",
         "@babel/plugin-syntax-jsx": "^7.2.0"
       }
     },
     "@babel/polyfill": {
-      "version": "7.2.5",
-      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.2.5.tgz",
-      "integrity": "sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==",
-      "dev": true,
-      "requires": {
-        "core-js": "^2.5.7",
-        "regenerator-runtime": "^0.12.0"
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz",
+      "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.6.5",
+        "regenerator-runtime": "^0.13.2"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.13.2",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+          "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==",
+          "dev": true
+        }
       }
     },
     "@babel/preset-react": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz",
       "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.0.0",
         "@babel/plugin-transform-react-display-name": "^7.0.0",
         "@babel/plugin-transform-react-jsx": "^7.0.0",
         "@babel/plugin-transform-react-jsx-self": "^7.0.0",
         "@babel/plugin-transform-react-jsx-source": "^7.0.0"
       }
     },
     "@babel/runtime": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
-      "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
-      "requires": {
-        "regenerator-runtime": "^0.12.0"
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz",
+      "integrity": "sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==",
+      "requires": {
+        "regenerator-runtime": "^0.13.2"
       }
     },
     "@babel/template": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
       "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
       "dev": true,
       "requires": {
@@ -289,59 +435,16 @@
       "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
       "dev": true,
       "requires": {
         "esutils": "^2.0.2",
         "lodash": "^4.17.11",
         "to-fast-properties": "^2.0.0"
       }
     },
-    "@octokit/endpoint": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-3.2.3.tgz",
-      "integrity": "sha512-yUPCt4vMIOclox13CUxzuKiPJIFo46b/6GhUnUTw5QySczN1L0DtSxgmIZrZV4SAb9EyAqrceoyrWoYVnfF2AA==",
-      "dev": true,
-      "requires": {
-        "deepmerge": "3.2.0",
-        "is-plain-object": "^2.0.4",
-        "universal-user-agent": "^2.0.1",
-        "url-template": "^2.0.8"
-      }
-    },
-    "@octokit/request": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-2.4.1.tgz",
-      "integrity": "sha512-nN8W24ZXEpJQJoVgMsGZeK9FOzxkc39Xn9ykseUpPpPMNEDFSvqfkCeqqKrjUiXRm72ubGLWG1SOz0aJPcgGww==",
-      "dev": true,
-      "requires": {
-        "@octokit/endpoint": "^3.1.1",
-        "deprecation": "^1.0.1",
-        "is-plain-object": "^2.0.4",
-        "node-fetch": "^2.3.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^2.0.1"
-      }
-    },
-    "@octokit/rest": {
-      "version": "16.17.0",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.17.0.tgz",
-      "integrity": "sha512-1RB7e4ptR/M+1Ik3Qn84pbppbSadBaCtpgFqgqsXn6s4ZVE6hqW9SOm6UW5yd3KT7ObVfdYUkhMlgR937oKyDw==",
-      "dev": true,
-      "requires": {
-        "@octokit/request": "2.4.1",
-        "before-after-hook": "^1.4.0",
-        "btoa-lite": "^1.0.0",
-        "lodash.get": "^4.4.2",
-        "lodash.set": "^4.3.2",
-        "lodash.uniq": "^4.5.0",
-        "octokit-pagination-methods": "^1.1.0",
-        "universal-user-agent": "^2.0.0",
-        "url-template": "^2.0.8"
-      }
-    },
     "@sinonjs/commons": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
       "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
       "dev": true,
       "requires": {
         "type-detect": "4.0.8"
       }
@@ -352,19 +455,19 @@
       "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1",
         "@sinonjs/samsam": "^3.1.0"
       }
     },
     "@sinonjs/samsam": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.0.tgz",
-      "integrity": "sha512-beHeJM/RRAaLLsMJhsCvHK31rIqZuobfPLa/80yGH5hnD8PV1hyh9xJBJNFfNmO7yWqm+zomijHsXpI6iTQJfQ==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz",
+      "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.0.2",
         "array-from": "^2.1.1",
         "lodash": "^4.17.11"
       }
     },
     "@sinonjs/text-encoding": {
@@ -569,23 +672,40 @@
     },
     "abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
       "dev": true
     },
     "accepts": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
-      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
-      "dev": true,
-      "requires": {
-        "mime-types": "~2.1.18",
-        "negotiator": "0.6.1"
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.40.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+          "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.24",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+          "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.40.0"
+          }
+        }
       }
     },
     "acorn": {
       "version": "6.1.1",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
       "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
       "dev": true
     },
@@ -602,16 +722,42 @@
       "dev": true
     },
     "after": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
       "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
       "dev": true
     },
+    "airbnb-prop-types": {
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz",
+      "integrity": "sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==",
+      "dev": true,
+      "requires": {
+        "array.prototype.find": "^2.0.4",
+        "function.prototype.name": "^1.1.0",
+        "has": "^1.0.3",
+        "is-regex": "^1.0.4",
+        "object-is": "^1.0.1",
+        "object.assign": "^4.1.0",
+        "object.entries": "^1.1.0",
+        "prop-types": "^15.7.2",
+        "prop-types-exact": "^1.2.0",
+        "react-is": "^16.8.6"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "16.8.6",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
+          "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
+          "dev": true
+        }
+      }
+    },
     "ajv": {
       "version": "6.10.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
       "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
       "dev": true,
       "requires": {
         "fast-deep-equal": "^2.0.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -780,16 +926,26 @@
       "dev": true
     },
     "array-unique": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
       "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
       "dev": true
     },
+    "array.prototype.find": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.0.4.tgz",
+      "integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.7.0"
+      }
+    },
     "array.prototype.flat": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz",
       "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==",
       "dev": true,
       "requires": {
         "define-properties": "^1.1.2",
         "es-abstract": "^1.10.0",
@@ -1282,22 +1438,16 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
       "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
       "dev": true,
       "requires": {
         "tweetnacl": "^0.14.3"
       }
     },
-    "before-after-hook": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-1.4.0.tgz",
-      "integrity": "sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==",
-      "dev": true
-    },
     "better-assert": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
       "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
       "dev": true,
       "requires": {
         "callsite": "1.0.0"
       }
@@ -1337,56 +1487,53 @@
     },
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
       "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
       "dev": true
     },
     "body-parser": {
-      "version": "1.18.3",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
-      "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
-      "dev": true,
-      "requires": {
-        "bytes": "3.0.0",
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.1.0",
         "content-type": "~1.0.4",
         "debug": "2.6.9",
         "depd": "~1.1.2",
-        "http-errors": "~1.6.3",
-        "iconv-lite": "0.4.23",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
         "on-finished": "~2.3.0",
-        "qs": "6.5.2",
-        "raw-body": "2.3.3",
-        "type-is": "~1.6.16"
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
       },
       "dependencies": {
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
             "ms": "2.0.0"
           }
         },
-        "iconv-lite": {
-          "version": "0.4.23",
-          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
-          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
-          "dev": true,
-          "requires": {
-            "safer-buffer": ">= 2.1.2 < 3"
-          }
-        },
         "ms": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
           "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
           "dev": true
+        },
+        "qs": {
+          "version": "6.7.0",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+          "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
+          "dev": true
         }
       }
     },
     "boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
       "dev": true
@@ -1490,22 +1637,16 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
       "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
       "dev": true,
       "requires": {
         "pako": "~1.0.5"
       }
     },
-    "btoa-lite": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
-      "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=",
-      "dev": true
-    },
     "buffer": {
       "version": "4.9.1",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
       "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
       "dev": true,
       "requires": {
         "base64-js": "^1.0.2",
         "ieee754": "^1.1.4",
@@ -1548,19 +1689,19 @@
     },
     "builtin-status-codes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
       "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
       "dev": true
     },
     "bytes": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
-      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+      "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
       "dev": true
     },
     "cacache": {
       "version": "11.3.2",
       "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz",
       "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==",
       "dev": true,
       "requires": {
@@ -1642,19 +1783,19 @@
     },
     "callsite": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
       "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
       "dev": true
     },
     "callsites": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
-      "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
       "dev": true
     },
     "camelcase": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz",
       "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==",
       "dev": true
     },
@@ -1841,31 +1982,16 @@
       "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
       "dev": true,
       "requires": {
         "string-width": "^2.1.1",
         "strip-ansi": "^4.0.0",
         "wrap-ansi": "^2.0.0"
       }
     },
-    "co": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz",
-      "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=",
-      "dev": true
-    },
-    "co-task": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/co-task/-/co-task-1.0.0.tgz",
-      "integrity": "sha1-dJSwCATsga6kiVVuuGsteurCpRU=",
-      "dev": true,
-      "requires": {
-        "co": "^3.1.0"
-      }
-    },
     "code-point-at": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
       "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
       "dev": true
     },
     "collection-visit": {
       "version": "1.0.0",
@@ -2305,22 +2431,16 @@
       }
     },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
       "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
       "dev": true
     },
-    "deepmerge": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.0.tgz",
-      "integrity": "sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==",
-      "dev": true
-    },
     "default-require-extensions": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
       "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
       "dev": true,
       "requires": {
         "strip-bom": "^3.0.0"
       }
@@ -2394,22 +2514,16 @@
       "dev": true
     },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
       "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
       "dev": true
     },
-    "deprecation": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-1.0.1.tgz",
-      "integrity": "sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg==",
-      "dev": true
-    },
     "des.js": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
       "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
       "dev": true,
       "requires": {
         "inherits": "^2.0.1",
         "minimalistic-assert": "^1.0.0"
@@ -2725,35 +2839,45 @@
         "object.entries": "^1.0.4",
         "object.values": "^1.0.4",
         "raf": "^3.4.0",
         "rst-selector-parser": "^2.2.3",
         "string.prototype.trim": "^1.1.2"
       }
     },
     "enzyme-adapter-react-16": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.10.0.tgz",
-      "integrity": "sha512-0QqwEZcBv1xEEla+a3H7FMci+y4ybLia9cZzsdIrId7qcig4MK0kqqf6iiCILH1lsKS6c6AVqL3wGPhCevv5aQ==",
-      "dev": true,
-      "requires": {
-        "enzyme-adapter-utils": "^1.10.0",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.12.1.tgz",
+      "integrity": "sha512-GB61gvY97XvrA6qljExGY+lgI6BBwz+ASLaRKct9VQ3ozu0EraqcNn3CcrUckSGIqFGa1+CxO5gj5is5t3lwrw==",
+      "dev": true,
+      "requires": {
+        "enzyme-adapter-utils": "^1.11.0",
         "object.assign": "^4.1.0",
         "object.values": "^1.1.0",
-        "prop-types": "^15.6.2",
-        "react-is": "^16.7.0",
-        "react-test-renderer": "^16.0.0-0"
+        "prop-types": "^15.7.2",
+        "react-is": "^16.8.6",
+        "react-test-renderer": "^16.0.0-0",
+        "semver": "^5.6.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "16.8.6",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
+          "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
+          "dev": true
+        }
       }
     },
     "enzyme-adapter-utils": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.1.tgz",
-      "integrity": "sha512-oasinhhLoBuZsIkTe8mx0HiudtfErUtG0Ooe1FOplu/t4c9rOmyG5gtrBASK6u4whHIRWvv0cbZMElzNTR21SA==",
-      "dev": true,
-      "requires": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.11.0.tgz",
+      "integrity": "sha512-0VZeoE9MNx+QjTfsjmO1Mo+lMfunucYB4wt5ficU85WB/LoetTJrbuujmHP3PJx6pSoaAuLA+Mq877x4LoxdNg==",
+      "dev": true,
+      "requires": {
+        "airbnb-prop-types": "^2.12.0",
         "function.prototype.name": "^1.1.0",
         "object.assign": "^4.1.0",
         "object.fromentries": "^2.0.0",
         "prop-types": "^15.7.2",
         "semver": "^5.6.0"
       }
     },
     "errno": {
@@ -2890,42 +3014,42 @@
       "requires": {
         "es6-map": "^0.1.3",
         "es6-weak-map": "^2.0.1",
         "esrecurse": "^4.1.0",
         "estraverse": "^4.1.1"
       }
     },
     "eslint": {
-      "version": "5.15.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.15.1.tgz",
-      "integrity": "sha512-NTcm6vQ+PTgN3UBsALw5BMhgO6i5EpIjQF/Xb5tIh3sk9QhrFafujUOczGz4J24JBlzWclSB9Vmx8d+9Z6bFCg==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+      "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
         "ajv": "^6.9.1",
         "chalk": "^2.1.0",
         "cross-spawn": "^6.0.5",
         "debug": "^4.0.1",
         "doctrine": "^3.0.0",
-        "eslint-scope": "^4.0.2",
+        "eslint-scope": "^4.0.3",
         "eslint-utils": "^1.3.1",
         "eslint-visitor-keys": "^1.0.0",
         "espree": "^5.0.1",
         "esquery": "^1.0.1",
         "esutils": "^2.0.2",
         "file-entry-cache": "^5.0.1",
         "functional-red-black-tree": "^1.0.1",
         "glob": "^7.1.2",
         "globals": "^11.7.0",
         "ignore": "^4.0.6",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "inquirer": "^6.2.2",
-        "js-yaml": "^3.12.0",
+        "js-yaml": "^3.13.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.3.0",
         "lodash": "^4.17.11",
         "minimatch": "^3.0.4",
         "mkdirp": "^0.5.1",
         "natural-compare": "^1.4.0",
         "optionator": "^0.8.2",
         "path-is-inside": "^1.0.2",
@@ -2934,24 +3058,34 @@
         "semver": "^5.5.1",
         "strip-ansi": "^4.0.0",
         "strip-json-comments": "^2.0.1",
         "table": "^5.2.3",
         "text-table": "^0.2.0"
       },
       "dependencies": {
         "eslint-scope": {
-          "version": "4.0.2",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.2.tgz",
-          "integrity": "sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==",
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
           "dev": true,
           "requires": {
             "esrecurse": "^4.1.0",
             "estraverse": "^4.1.1"
           }
+        },
+        "js-yaml": {
+          "version": "3.13.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
         }
       }
     },
     "eslint-import-resolver-node": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
       "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==",
       "dev": true,
@@ -2973,19 +3107,19 @@
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
           "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
           "dev": true
         }
       }
     },
     "eslint-module-utils": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz",
-      "integrity": "sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz",
+      "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==",
       "dev": true,
       "requires": {
         "debug": "^2.6.8",
         "pkg-dir": "^2.0.0"
       },
       "dependencies": {
         "debug": {
           "version": "2.6.9",
@@ -3058,31 +3192,32 @@
     },
     "eslint-plugin-fetch-options": {
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/eslint-plugin-fetch-options/-/eslint-plugin-fetch-options-0.0.4.tgz",
       "integrity": "sha512-Cre2qoYkn0OUdavSFb2duRk1sMChwlger01FvRoG5zO9BRgaMSH3kK3eURSlrOGiKbq7oNwl6Ha39eLeXSZ3Sg==",
       "dev": true
     },
     "eslint-plugin-import": {
-      "version": "2.16.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz",
-      "integrity": "sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==",
-      "dev": true,
-      "requires": {
+      "version": "2.17.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz",
+      "integrity": "sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g==",
+      "dev": true,
+      "requires": {
+        "array-includes": "^3.0.3",
         "contains-path": "^0.1.0",
         "debug": "^2.6.9",
         "doctrine": "1.5.0",
         "eslint-import-resolver-node": "^0.3.2",
-        "eslint-module-utils": "^2.3.0",
+        "eslint-module-utils": "^2.4.0",
         "has": "^1.0.3",
         "lodash": "^4.17.11",
         "minimatch": "^3.0.4",
         "read-pkg-up": "^2.0.0",
-        "resolve": "^1.9.0"
+        "resolve": "^1.10.0"
       },
       "dependencies": {
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
@@ -3128,63 +3263,36 @@
         "axobject-query": "^2.0.2",
         "damerau-levenshtein": "^1.0.4",
         "emoji-regex": "^7.0.2",
         "has": "^1.0.3",
         "jsx-ast-utils": "^2.0.1"
       }
     },
     "eslint-plugin-mozilla": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-mozilla/-/eslint-plugin-mozilla-1.1.1.tgz",
-      "integrity": "sha512-ynTwCUOc5vf+ZZKtSfySSGWmPLd/wCCiX+zH2MnMcVUYtV0187cUVJTOcQ0YxOdpcQrfaW8gMraETrXGsdZ5bA==",
-      "dev": true,
-      "requires": {
-        "htmlparser2": "3.10.0",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-mozilla/-/eslint-plugin-mozilla-1.2.1.tgz",
+      "integrity": "sha512-i6SAWTPgKI6T50L6Gy41efR3StjPGR3IWkFOW/4aGsJtpl+OmJVaOmUfhp5PGis3ZbDisNvQ/vzLiNicAWC7nw==",
+      "dev": true,
+      "requires": {
+        "htmlparser2": "3.10.1",
         "ini-parser": "0.0.2",
         "sax": "1.2.4"
-      },
-      "dependencies": {
-        "htmlparser2": {
-          "version": "3.10.0",
-          "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
-          "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
-          "dev": true,
-          "requires": {
-            "domelementtype": "^1.3.0",
-            "domhandler": "^2.3.0",
-            "domutils": "^1.5.1",
-            "entities": "^1.1.1",
-            "inherits": "^2.0.1",
-            "readable-stream": "^3.0.6"
-          }
-        },
-        "readable-stream": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.2.0.tgz",
-          "integrity": "sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==",
-          "dev": true,
-          "requires": {
-            "inherits": "^2.0.3",
-            "string_decoder": "^1.1.1",
-            "util-deprecate": "^1.0.1"
-          }
-        }
       }
     },
     "eslint-plugin-no-unsanitized": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-3.0.2.tgz",
       "integrity": "sha512-JnwpoH8Sv4QOjrTDutENBHzSnyYtspdjtglYtqUtAHe6f6LLKqykJle+UwFPg23GGwt5hI3amS9CRDezW8GAww==",
       "dev": true
     },
     "eslint-plugin-promise": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz",
-      "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz",
+      "integrity": "sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==",
       "dev": true
     },
     "eslint-plugin-react": {
       "version": "7.12.4",
       "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz",
       "integrity": "sha512-1puHJkXJY+oS1t467MjbqjvX53uQ05HXwjqDgdbGBqf5j9eeydI54G3KwiJmWciQ0HTBacIKw2jgwSBSH3yfgQ==",
       "dev": true,
       "requires": {
@@ -3204,19 +3312,19 @@
           "dev": true,
           "requires": {
             "esutils": "^2.0.2"
           }
         }
       }
     },
     "eslint-plugin-react-hooks": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.0.tgz",
-      "integrity": "sha512-iwDuWR2ReRgvJsNm8fXPtTKdg78IVQF8I4+am3ntztPf/+nPnWZfArFu6aXpaC75/iCYRrkqI8nPCYkxJstmpA==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.0.tgz",
+      "integrity": "sha512-lHBVRIaz5ibnIgNG07JNiAuBUeKhEf8l4etNx5vfAEwqQ5tcuK3jV9yjmopPgQDagQb7HwIuQVsE3IVcGrRnag==",
       "dev": true
     },
     "eslint-scope": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
       "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
       "dev": true,
       "requires": {
@@ -3232,26 +3340,28 @@
     },
     "eslint-visitor-keys": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
       "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
       "dev": true
     },
     "eslint-watch": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-watch/-/eslint-watch-5.0.1.tgz",
-      "integrity": "sha512-iFQ+LSsW3uhPlBgcSGR6MogMbaurC2YIMkQWWVA+u32bhWQXBn4z2gQv0tPtsrcJErCflyiU6lv/eOqHP3yojA==",
-      "dev": true,
-      "requires": {
-        "@babel/polyfill": "^7.0.0",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-watch/-/eslint-watch-5.1.2.tgz",
+      "integrity": "sha512-tDUvfcekGxGuyzUPedyQq15X4dUmm9wA35UDi7g1+Cvhf9204jy0iahaEN55Vus1uIaHI9pc14Dixs4Qak3Kvg==",
+      "dev": true,
+      "requires": {
+        "@babel/polyfill": "^7.4.4",
         "chokidar": "^2.0.4",
+        "core-js": "2",
         "debug": "^4.1.0",
         "execa": "^1.0.0",
         "keypress": "^0.2.1",
+        "lodash.debounce": "^4.0.8",
         "lodash.isempty": "^4.4.0",
         "lodash.isequal": "^4.5.0",
         "lodash.kebabcase": "^4.1.1",
         "lodash.unionwith": "^4.6.0",
         "optionator": "^0.8.2",
         "source-map-support": "^0.5.9"
       },
       "dependencies": {
@@ -3313,48 +3423,33 @@
               "dev": true,
               "requires": {
                 "is-extendable": "^0.1.0"
               }
             }
           }
         },
         "chokidar": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
-          "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+          "version": "2.1.5",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+          "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
           "dev": true,
           "requires": {
             "anymatch": "^2.0.0",
             "async-each": "^1.0.1",
             "braces": "^2.3.2",
             "fsevents": "^1.2.7",
             "glob-parent": "^3.1.0",
             "inherits": "^2.0.3",
             "is-binary-path": "^1.0.0",
             "is-glob": "^4.0.0",
             "normalize-path": "^3.0.0",
             "path-is-absolute": "^1.0.0",
             "readdirp": "^2.2.1",
-            "upath": "^1.1.0"
-          }
-        },
-        "execa": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-          "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-          "dev": true,
-          "requires": {
-            "cross-spawn": "^6.0.0",
-            "get-stream": "^4.0.0",
-            "is-stream": "^1.1.0",
-            "npm-run-path": "^2.0.0",
-            "p-finally": "^1.0.0",
-            "signal-exit": "^3.0.0",
-            "strip-eof": "^1.0.0"
+            "upath": "^1.1.1"
           }
         },
         "expand-brackets": {
           "version": "2.1.4",
           "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
           "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
           "dev": true,
           "requires": {
@@ -3507,25 +3602,16 @@
               "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
               "dev": true,
               "requires": {
                 "is-extendable": "^0.1.0"
               }
             }
           }
         },
-        "get-stream": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-          "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-          "dev": true,
-          "requires": {
-            "pump": "^3.0.0"
-          }
-        },
         "glob-parent": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
           "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
           "dev": true,
           "requires": {
             "is-glob": "^3.1.0",
             "path-dirname": "^1.0.0"
@@ -3573,19 +3659,19 @@
         },
         "is-extglob": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
           "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
           "dev": true
         },
         "is-glob": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-          "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+          "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
           "dev": true,
           "requires": {
             "is-extglob": "^2.1.1"
           }
         },
         "is-number": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -3700,19 +3786,19 @@
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
       "dev": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
       }
     },
     "eventemitter3": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
-      "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+      "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
       "dev": true
     },
     "events": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
       "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
       "dev": true
     },
@@ -3722,23 +3808,23 @@
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
       "dev": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
       }
     },
     "execa": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
-      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
       "dev": true,
       "requires": {
         "cross-spawn": "^6.0.0",
-        "get-stream": "^3.0.0",
+        "get-stream": "^4.0.0",
         "is-stream": "^1.1.0",
         "npm-run-path": "^2.0.0",
         "p-finally": "^1.0.0",
         "signal-exit": "^3.0.0",
         "strip-eof": "^1.0.0"
       }
     },
     "exit-hook": {
@@ -5110,20 +5196,23 @@
     },
     "get-stdin": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
       "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
       "dev": true
     },
     "get-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
-      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
-      "dev": true
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+      "dev": true,
+      "requires": {
+        "pump": "^3.0.0"
+      }
     },
     "get-value": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
       "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
       "dev": true
     },
     "getpass": {
@@ -5252,22 +5341,22 @@
       "resolved": "https://registry.npmjs.org/habitat/-/habitat-3.1.2.tgz",
       "integrity": "sha1-5c/V1zTlEiB8Lbut2AYExx0THeA=",
       "dev": true,
       "requires": {
         "xtend": "~2.1.1"
       }
     },
     "handlebars": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz",
-      "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==",
-      "dev": true,
-      "requires": {
-        "async": "^2.5.0",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
+      "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
+      "dev": true,
+      "requires": {
+        "neo-async": "^2.6.0",
         "optimist": "^0.6.1",
         "source-map": "^0.6.1",
         "uglify-js": "^3.1.4"
       },
       "dependencies": {
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -5511,25 +5600,26 @@
             "inherits": "^2.0.3",
             "string_decoder": "^1.1.1",
             "util-deprecate": "^1.0.1"
           }
         }
       }
     },
     "http-errors": {
-      "version": "1.6.3",
-      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
-      "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+      "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
       "dev": true,
       "requires": {
         "depd": "~1.1.2",
         "inherits": "2.0.3",
-        "setprototypeof": "1.1.0",
-        "statuses": ">= 1.4.0 < 2"
+        "setprototypeof": "1.1.1",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.0"
       }
     },
     "http-proxy": {
       "version": "1.17.0",
       "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
       "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
       "dev": true,
       "requires": {
@@ -5631,19 +5721,19 @@
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
       "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
       "dev": true,
       "requires": {
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
     "ieee754": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
-      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
       "dev": true
     },
     "iferr": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
       "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
       "dev": true
     },
@@ -5724,46 +5814,46 @@
     },
     "ini-parser": {
       "version": "0.0.2",
       "resolved": "https://registry.npmjs.org/ini-parser/-/ini-parser-0.0.2.tgz",
       "integrity": "sha1-+kF4flZ3Y7P/Zdel2alO23QHh+8=",
       "dev": true
     },
     "inquirer": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
-      "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
+      "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
       "dev": true,
       "requires": {
         "ansi-escapes": "^3.2.0",
         "chalk": "^2.4.2",
         "cli-cursor": "^2.1.0",
         "cli-width": "^2.0.0",
         "external-editor": "^3.0.3",
         "figures": "^2.0.0",
         "lodash": "^4.17.11",
         "mute-stream": "0.0.7",
         "run-async": "^2.2.0",
         "rxjs": "^6.4.0",
         "string-width": "^2.1.0",
-        "strip-ansi": "^5.0.0",
+        "strip-ansi": "^5.1.0",
         "through": "^2.3.6"
       },
       "dependencies": {
         "ansi-regex": {
           "version": "4.1.0",
           "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
           "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
           "dev": true
         },
         "strip-ansi": {
-          "version": "5.1.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.1.0.tgz",
-          "integrity": "sha512-TjxrkPONqO2Z8QDCpeE2j6n0M6EwxzyDgzEeGp+FbdvaJAt//ClYi6W5my+3ROlC/hZX2KACUwDfK49Ka5eDvg==",
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "dev": true,
           "requires": {
             "ansi-regex": "^4.1.0"
           }
         }
       }
     },
     "interpret": {
@@ -6118,70 +6208,128 @@
     },
     "isstream": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
       "dev": true
     },
     "istanbul-api": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.1.tgz",
-      "integrity": "sha512-kVmYrehiwyeBAk/wE71tW6emzLiHGjYIiDrc8sfyty4F8M02/lrgXSm+R1kXysmF20zArvmZXjlE/mg24TVPJw==",
-      "dev": true,
-      "requires": {
-        "async": "^2.6.1",
-        "compare-versions": "^3.2.1",
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz",
+      "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.6.2",
+        "compare-versions": "^3.4.0",
         "fileset": "^2.0.3",
-        "istanbul-lib-coverage": "^2.0.3",
-        "istanbul-lib-hook": "^2.0.3",
-        "istanbul-lib-instrument": "^3.1.0",
-        "istanbul-lib-report": "^2.0.4",
-        "istanbul-lib-source-maps": "^3.0.2",
-        "istanbul-reports": "^2.1.1",
-        "js-yaml": "^3.12.0",
-        "make-dir": "^1.3.0",
+        "istanbul-lib-coverage": "^2.0.5",
+        "istanbul-lib-hook": "^2.0.7",
+        "istanbul-lib-instrument": "^3.3.0",
+        "istanbul-lib-report": "^2.0.8",
+        "istanbul-lib-source-maps": "^3.0.6",
+        "istanbul-reports": "^2.2.4",
+        "js-yaml": "^3.13.1",
+        "make-dir": "^2.1.0",
         "minimatch": "^3.0.4",
         "once": "^1.4.0"
       },
       "dependencies": {
+        "@babel/generator": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",
+          "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4",
+            "jsesc": "^2.5.1",
+            "lodash": "^4.17.11",
+            "source-map": "^0.5.0",
+            "trim-right": "^1.0.1"
+          }
+        },
+        "@babel/helper-split-export-declaration": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+          "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/parser": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz",
+          "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==",
+          "dev": true
+        },
+        "@babel/template": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4"
+          }
+        },
+        "@babel/traverse": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz",
+          "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "@babel/generator": "^7.4.4",
+            "@babel/helper-function-name": "^7.1.0",
+            "@babel/helper-split-export-declaration": "^7.4.4",
+            "@babel/parser": "^7.4.4",
+            "@babel/types": "^7.4.4",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0",
+            "lodash": "^4.17.11"
+          }
+        },
+        "@babel/types": {
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
+          "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2",
+            "lodash": "^4.17.11",
+            "to-fast-properties": "^2.0.0"
+          }
+        },
         "istanbul-lib-coverage": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
-          "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==",
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
+          "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
           "dev": true
         },
         "istanbul-lib-instrument": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz",
-          "integrity": "sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==",
-          "dev": true,
-          "requires": {
-            "@babel/generator": "^7.0.0",
-            "@babel/parser": "^7.0.0",
-            "@babel/template": "^7.0.0",
-            "@babel/traverse": "^7.0.0",
-            "@babel/types": "^7.0.0",
-            "istanbul-lib-coverage": "^2.0.3",
-            "semver": "^5.5.0"
-          }
-        },
-        "make-dir": {
-          "version": "1.3.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
-          "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
-          "dev": true,
-          "requires": {
-            "pify": "^3.0.0"
-          }
-        },
-        "pify": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz",
+          "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==",
+          "dev": true,
+          "requires": {
+            "@babel/generator": "^7.4.0",
+            "@babel/parser": "^7.4.3",
+            "@babel/template": "^7.4.0",
+            "@babel/traverse": "^7.4.3",
+            "@babel/types": "^7.4.0",
+            "istanbul-lib-coverage": "^2.0.5",
+            "semver": "^6.0.0"
+          }
+        },
+        "semver": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
+          "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==",
           "dev": true
         }
       }
     },
     "istanbul-instrumenter-loader": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz",
       "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==",
@@ -6217,19 +6365,19 @@
     },
     "istanbul-lib-coverage": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz",
       "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==",
       "dev": true
     },
     "istanbul-lib-hook": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz",
-      "integrity": "sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA==",
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz",
+      "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==",
       "dev": true,
       "requires": {
         "append-transform": "^1.0.0"
       }
     },
     "istanbul-lib-instrument": {
       "version": "1.10.2",
       "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz",
@@ -6241,107 +6389,77 @@
         "babel-traverse": "^6.18.0",
         "babel-types": "^6.18.0",
         "babylon": "^6.18.0",
         "istanbul-lib-coverage": "^1.2.1",
         "semver": "^5.3.0"
       }
     },
     "istanbul-lib-report": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz",
-      "integrity": "sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA==",
-      "dev": true,
-      "requires": {
-        "istanbul-lib-coverage": "^2.0.3",
-        "make-dir": "^1.3.0",
-        "supports-color": "^6.0.0"
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz",
+      "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==",
+      "dev": true,
+      "requires": {
+        "istanbul-lib-coverage": "^2.0.5",
+        "make-dir": "^2.1.0",
+        "supports-color": "^6.1.0"
       },
       "dependencies": {
         "istanbul-lib-coverage": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
-          "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==",
-          "dev": true
-        },
-        "make-dir": {
-          "version": "1.3.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
-          "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
-          "dev": true,
-          "requires": {
-            "pify": "^3.0.0"
-          }
-        },
-        "pify": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
+          "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
           "dev": true
         },
         "supports-color": {
           "version": "6.1.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
           "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
           "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
         }
       }
     },
     "istanbul-lib-source-maps": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz",
-      "integrity": "sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ==",
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz",
+      "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
-        "istanbul-lib-coverage": "^2.0.3",
-        "make-dir": "^1.3.0",
-        "rimraf": "^2.6.2",
+        "istanbul-lib-coverage": "^2.0.5",
+        "make-dir": "^2.1.0",
+        "rimraf": "^2.6.3",
         "source-map": "^0.6.1"
       },
       "dependencies": {
         "istanbul-lib-coverage": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
-          "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==",
-          "dev": true
-        },
-        "make-dir": {
-          "version": "1.3.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
-          "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
-          "dev": true,
-          "requires": {
-            "pify": "^3.0.0"
-          }
-        },
-        "pify": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
+          "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
           "dev": true
         },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
           "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
           "dev": true
         }
       }
     },
     "istanbul-reports": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.1.1.tgz",
-      "integrity": "sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw==",
-      "dev": true,
-      "requires": {
-        "handlebars": "^4.1.0"
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.4.tgz",
+      "integrity": "sha512-QCHGyZEK0bfi9GR215QSm+NJwFKEShbtc7tfbUdLAEzn3kKhLDDZqvljn8rPZM9v8CEOhzL1nlYoO4r1ryl67w==",
+      "dev": true,
+      "requires": {
+        "handlebars": "^4.1.2"
       }
     },
     "joi-browser": {
       "version": "13.4.0",
       "resolved": "https://registry.npmjs.org/joi-browser/-/joi-browser-13.4.0.tgz",
       "integrity": "sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==",
       "dev": true
     },
@@ -6352,19 +6470,19 @@
       "dev": true
     },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
     "js-yaml": {
-      "version": "3.12.2",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz",
-      "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==",
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
       "dev": true,
       "requires": {
         "argparse": "^1.0.7",
         "esprima": "^4.0.0"
       }
     },
     "jsbn": {
       "version": "0.1.1",
@@ -6482,19 +6600,19 @@
     },
     "just-extend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
       "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
       "dev": true
     },
     "karma": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/karma/-/karma-4.0.1.tgz",
-      "integrity": "sha512-ind+4s03BqIXas7ZmraV3/kc5+mnqwCd+VDX1FndS6jxbt03kQKX2vXrWxNLuCjVYmhMwOZosAEKMM0a2q7w7A==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-4.1.0.tgz",
+      "integrity": "sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw==",
       "dev": true,
       "requires": {
         "bluebird": "^3.3.0",
         "body-parser": "^1.16.1",
         "braces": "^2.3.2",
         "chokidar": "^2.0.3",
         "colors": "^1.1.0",
         "connect": "^3.6.0",
@@ -6568,33 +6686,33 @@
             "repeat-element": "^1.1.2",
             "snapdragon": "^0.8.1",
             "snapdragon-node": "^2.0.1",
             "split-string": "^3.0.2",
             "to-regex": "^3.0.1"
           }
         },
         "chokidar": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
-          "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+          "version": "2.1.5",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+          "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
           "dev": true,
           "requires": {
             "anymatch": "^2.0.0",
             "async-each": "^1.0.1",
             "braces": "^2.3.2",
             "fsevents": "^1.2.7",
             "glob-parent": "^3.1.0",
             "inherits": "^2.0.3",
             "is-binary-path": "^1.0.0",
             "is-glob": "^4.0.0",
             "normalize-path": "^3.0.0",
             "path-is-absolute": "^1.0.0",
             "readdirp": "^2.2.1",
-            "upath": "^1.1.0"
+            "upath": "^1.1.1"
           }
         },
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
@@ -6808,19 +6926,19 @@
         },
         "is-extglob": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
           "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
           "dev": true
         },
         "is-glob": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-          "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+          "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
           "dev": true,
           "requires": {
             "is-extglob": "^2.1.1"
           }
         },
         "is-number": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -7094,32 +7212,26 @@
       }
     },
     "lodash": {
       "version": "4.17.11",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
       "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
       "dev": true
     },
-    "lodash.assign": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
-      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
-      "dev": true
-    },
     "lodash.capitalize": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz",
       "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=",
       "dev": true
     },
-    "lodash.clonedeep": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
       "dev": true
     },
     "lodash.escape": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
       "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=",
       "dev": true
     },
@@ -7148,87 +7260,58 @@
       "dev": true
     },
     "lodash.kebabcase": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
       "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=",
       "dev": true
     },
-    "lodash.mergewith": {
-      "version": "4.6.1",
-      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
-      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
-      "dev": true
-    },
-    "lodash.set": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
-    },
     "lodash.unionwith": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/lodash.unionwith/-/lodash.unionwith-4.6.0.tgz",
       "integrity": "sha1-dNFAtcqBRubGQ8NyT1FSU42awfA=",
       "dev": true
     },
-    "lodash.uniq": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
-      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
-      "dev": true
-    },
     "log-symbols": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
       "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
       "dev": true,
       "requires": {
         "chalk": "^2.0.1"
       }
     },
     "log4js": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.0.2.tgz",
-      "integrity": "sha512-KE7HjiieVDPPdveA3bJZSuu0n8chMkFl8mIoisBFxwEJ9FmXe4YzNuiqSwYUiR1K8q8/5/8Yd6AClENY1RA9ww==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.1.0.tgz",
+      "integrity": "sha512-eDa+zZPeVEeK6QGJAePyXM6pg4P3n3TO5rX9iZMVY48JshsTyLJZLIL5HipI1kQ2qLsSyOpUqNND/C5H4WhhiA==",
       "dev": true,
       "requires": {
         "date-format": "^2.0.0",
-        "debug": "^3.1.0",
+        "debug": "^4.1.1",
         "flatted": "^2.0.0",
         "rfdc": "^1.1.2",
-        "streamroller": "^1.0.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.2.6",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        }
+        "streamroller": "^1.0.4"
       }
     },
     "loglevelnext": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz",
       "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==",
       "dev": true,
       "requires": {
         "es6-symbol": "^3.1.1",
         "object.assign": "^4.1.0"
       }
     },
     "lolex": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz",
-      "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz",
+      "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==",
       "dev": true
     },
     "loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
       "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
       "requires": {
         "js-tokens": "^3.0.0 || ^4.0.0"
@@ -7249,22 +7332,16 @@
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
       "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
       "dev": true,
       "requires": {
         "pseudomap": "^1.0.2",
         "yallist": "^2.1.2"
       }
     },
-    "macos-release": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.0.0.tgz",
-      "integrity": "sha512-iCM3ZGeqIzlrH7KxYK+fphlJpCCczyHXc+HhRVbEu9uNTCrzYJjvvtefzeKTCVHd5AP/aD/fzC80JZ4ZP+dQ/A==",
-      "dev": true
-    },
     "make-dir": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
       "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
       "dev": true,
       "requires": {
         "pify": "^4.0.1",
         "semver": "^5.6.0"
@@ -7502,28 +7579,28 @@
     },
     "mime": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
       "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
       "dev": true
     },
     "mime-db": {
-      "version": "1.38.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
-      "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+      "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
       "dev": true
     },
     "mime-types": {
-      "version": "2.1.22",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
-      "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
-      "dev": true,
-      "requires": {
-        "mime-db": "~1.38.0"
+      "version": "2.1.24",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+      "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.40.0"
       }
     },
     "mimeparse": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/mimeparse/-/mimeparse-0.1.4.tgz",
       "integrity": "sha1-2vsCdSNw/SJgk64xUsJxrwGsJUo=",
       "dev": true
     },
@@ -7612,73 +7689,140 @@
           "version": "0.0.8",
           "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
           "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
           "dev": true
         }
       }
     },
     "mocha": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.0.2.tgz",
-      "integrity": "sha512-RtTJsmmToGyeTznSOMoM6TPEk1A84FQaHIciKrRqARZx+B5ccJ5tXlmJzEKGBxZdqk9UjpRsesZTUkZmR5YnuQ==",
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz",
+      "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==",
       "dev": true,
       "requires": {
         "ansi-colors": "3.2.3",
         "browser-stdout": "1.3.1",
         "debug": "3.2.6",
         "diff": "3.5.0",
         "escape-string-regexp": "1.0.5",
-        "findup-sync": "2.0.0",
+        "find-up": "3.0.0",
         "glob": "7.1.3",
         "growl": "1.10.5",
         "he": "1.2.0",
-        "js-yaml": "3.12.0",
+        "js-yaml": "3.13.1",
         "log-symbols": "2.2.0",
         "minimatch": "3.0.4",
         "mkdirp": "0.5.1",
         "ms": "2.1.1",
-        "node-environment-flags": "1.0.4",
+        "node-environment-flags": "1.0.5",
         "object.assign": "4.1.0",
         "strip-json-comments": "2.0.1",
         "supports-color": "6.0.0",
         "which": "1.3.1",
         "wide-align": "1.1.3",
-        "yargs": "12.0.5",
-        "yargs-parser": "11.1.1",
+        "yargs": "13.2.2",
+        "yargs-parser": "13.0.0",
         "yargs-unparser": "1.5.0"
       },
       "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
         "debug": {
           "version": "3.2.6",
           "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
           "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
           "dev": true,
           "requires": {
             "ms": "^2.1.1"
           }
         },
+        "get-caller-file": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+          "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+          "dev": true
+        },
         "js-yaml": {
-          "version": "3.12.0",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
-          "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+          "version": "3.13.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
           "dev": true,
           "requires": {
             "argparse": "^1.0.7",
             "esprima": "^4.0.0"
           }
         },
+        "require-main-filename": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+          "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
         "supports-color": {
           "version": "6.0.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
           "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
           "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
+        },
+        "yargs": {
+          "version": "13.2.2",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz",
+          "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^2.0.1",
+            "os-locale": "^3.1.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^2.0.0",
+            "set-blocking": "^2.0.0",
+            "string-width": "^3.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^4.0.0",
+            "yargs-parser": "^13.0.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "13.0.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz",
+          "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
         }
       }
     },
     "mock-raf": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mock-raf/-/mock-raf-1.0.1.tgz",
       "integrity": "sha512-+25y56bblLzEnv+G4ODsHNck07A5uP5HFfu/1VBKeFrUXoFT9oru+R+jLxLz6rwdM5drUHFdqX9LYBsMP4dz/w==",
       "dev": true,
@@ -7725,17 +7869,18 @@
       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
       "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
       "dev": true
     },
     "nan": {
       "version": "2.13.1",
       "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.1.tgz",
       "integrity": "sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
       "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
       "dev": true,
       "requires": {
         "arr-diff": "^4.0.0",
@@ -7786,19 +7931,19 @@
         "commander": "^2.19.0",
         "moo": "^0.4.3",
         "railroad-diagrams": "^1.0.0",
         "randexp": "0.4.6",
         "semver": "^5.4.1"
       }
     },
     "negotiator": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
-      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
       "dev": true
     },
     "neo-async": {
       "version": "2.6.0",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
       "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
       "dev": true
     },
@@ -7831,28 +7976,37 @@
           "version": "2.7.5",
           "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
           "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
           "dev": true
         }
       }
     },
     "node-environment-flags": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
-      "integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==",
-      "dev": true,
-      "requires": {
-        "object.getownpropertydescriptors": "^2.0.3"
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
+      "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
+      "dev": true,
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3",
+        "semver": "^5.7.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
+          "dev": true
+        }
       }
     },
     "node-fetch": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
-      "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==",
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz",
+      "integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==",
       "dev": true
     },
     "node-gyp": {
       "version": "3.8.0",
       "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
       "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
       "dev": true,
       "requires": {
@@ -7922,34 +8076,32 @@
           "dev": true,
           "requires": {
             "inherits": "2.0.3"
           }
         }
       }
     },
     "node-sass": {
-      "version": "4.11.0",
-      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz",
-      "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
+      "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
       "dev": true,
       "requires": {
         "async-foreach": "^0.1.3",
         "chalk": "^1.1.1",
         "cross-spawn": "^3.0.0",
         "gaze": "^1.0.0",
         "get-stdin": "^4.0.1",
         "glob": "^7.0.3",
         "in-publish": "^2.0.0",
-        "lodash.assign": "^4.2.0",
-        "lodash.clonedeep": "^4.3.2",
-        "lodash.mergewith": "^4.6.0",
+        "lodash": "^4.17.11",
         "meow": "^3.7.0",
         "mkdirp": "^0.5.1",
-        "nan": "^2.10.0",
+        "nan": "^2.13.2",
         "node-gyp": "^3.8.0",
         "npmlog": "^4.0.0",
         "request": "^2.88.0",
         "sass-graph": "^2.2.4",
         "stdout-stream": "^1.4.0",
         "true-case-path": "^1.0.2"
       },
       "dependencies": {
@@ -7989,16 +8141,22 @@
           }
         },
         "get-stdin": {
           "version": "4.0.1",
           "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
           "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
           "dev": true
         },
+        "nan": {
+          "version": "2.13.2",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+          "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+          "dev": true
+        },
         "strip-ansi": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -8282,22 +8440,16 @@
       "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.12.0",
         "function-bind": "^1.1.1",
         "has": "^1.0.3"
       }
     },
-    "octokit-pagination-methods": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
-      "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
-      "dev": true
-    },
     "on-finished": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
       "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
       "dev": true,
       "requires": {
         "ee-first": "1.1.1"
       }
@@ -8402,26 +8554,16 @@
           "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
           "dev": true,
           "requires": {
             "pump": "^3.0.0"
           }
         }
       }
     },
-    "os-name": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.0.0.tgz",
-      "integrity": "sha512-7c74tib2FsdFbQ3W+qj8Tyd1R3Z6tuVRNNxXjJcZ4NgjIEQU9N/prVMqcW29XZPXGACqaXN3jq58/6hoaoXH6g==",
-      "dev": true,
-      "requires": {
-        "macos-release": "^2.0.0",
-        "windows-release": "^3.1.0"
-      }
-    },
     "os-tmpdir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
       "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
       "dev": true
     },
     "osenv": {
       "version": "0.1.5",
@@ -8488,19 +8630,19 @@
       "dev": true,
       "requires": {
         "cyclist": "~0.2.2",
         "inherits": "^2.0.3",
         "readable-stream": "^2.1.5"
       }
     },
     "parent-module": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz",
-      "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
       "dev": true,
       "requires": {
         "callsites": "^3.0.0"
       }
     },
     "parse-asn1": {
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
@@ -8565,19 +8707,19 @@
       "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
       "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
       "dev": true,
       "requires": {
         "better-assert": "~1.0.0"
       }
     },
     "parseurl": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
-      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
       "dev": true
     },
     "pascalcase": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
       "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
       "dev": true
     },
@@ -8792,16 +8934,27 @@
       "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
       "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
       "requires": {
         "loose-envify": "^1.4.0",
         "object-assign": "^4.1.1",
         "react-is": "^16.8.1"
       }
     },
+    "prop-types-exact": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz",
+      "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3",
+        "object.assign": "^4.1.0",
+        "reflect.ownkeys": "^0.2.0"
+      }
+    },
     "properties-parser": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.3.1.tgz",
       "integrity": "sha1-ExbpU5/7/ZOEXjabIRAiq9R4dxo=",
       "dev": true,
       "requires": {
         "string.prototype.codepointat": "^0.2.0"
       }
@@ -9006,42 +9159,31 @@
     },
     "range-parser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
       "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
       "dev": true
     },
     "raw-body": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
-      "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
-      "dev": true,
-      "requires": {
-        "bytes": "3.0.0",
-        "http-errors": "1.6.3",
-        "iconv-lite": "0.4.23",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+      "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.1.0",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
         "unpipe": "1.0.0"
-      },
-      "dependencies": {
-        "iconv-lite": {
-          "version": "0.4.23",
-          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
-          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
-          "dev": true,
-          "requires": {
-            "safer-buffer": ">= 2.1.2 < 3"
-          }
-        }
       }
     },
     "raw-loader": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-1.0.0.tgz",
-      "integrity": "sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-2.0.0.tgz",
+      "integrity": "sha512-kZnO5MoIyrojfrPWqrhFNLZemIAX8edMOCp++yC5RKxzFB3m92DqKNhKlU6+FvpOhWtvyh3jOaD7J6/9tpdIKg==",
       "dev": true,
       "requires": {
         "loader-utils": "^1.1.0",
         "schema-utils": "^1.0.0"
       },
       "dependencies": {
         "json5": {
           "version": "1.0.1",
@@ -9072,82 +9214,121 @@
             "ajv": "^6.1.0",
             "ajv-errors": "^1.0.0",
             "ajv-keywords": "^3.1.0"
           }
         }
       }
     },
     "react": {
-      "version": "16.8.3",
-      "resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz",
-      "integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
+      "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
+      "requires": {
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.2",
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
+      }
+    },
+    "react-dom": {
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
+      "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.13.3"
-      }
-    },
-    "react-dom": {
-      "version": "16.8.3",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz",
-      "integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==",
-      "requires": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1",
-        "prop-types": "^15.6.2",
-        "scheduler": "^0.13.3"
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
       }
     },
     "react-intl": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.4.0.tgz",
-      "integrity": "sha1-ZsFNyd+ac7L7v71gIXJugKYT6xU=",
-      "requires": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.8.0.tgz",
+      "integrity": "sha512-1cSasNkHxZOXYYhms9Q1tSEWF8AWZQNq3nPLB/j8mYV0ZTSt2DhGQXHfKrKQMu4cgj9J1Crqg7xFPICTBgzqtQ==",
+      "requires": {
+        "hoist-non-react-statics": "^2.5.5",
         "intl-format-cache": "^2.0.5",
         "intl-messageformat": "^2.1.0",
-        "intl-relativeformat": "^2.0.0",
+        "intl-relativeformat": "^2.1.0",
         "invariant": "^2.1.1"
+      },
+      "dependencies": {
+        "hoist-non-react-statics": {
+          "version": "2.5.5",
+          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
+          "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
+        }
       }
     },
     "react-is": {
       "version": "16.8.4",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz",
       "integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA=="
     },
-    "react-lifecycles-compat": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
-      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
-    },
     "react-redux": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.0.tgz",
-      "integrity": "sha512-CRMpEx8+ccpoVxQrQDkG1obExNYpj6dZ1Ni7lUNFB9wcxOhy02tIqpFo4IUXc0kYvCGMu6ABj9A4imEX2VGZJQ==",
-      "requires": {
-        "@babel/runtime": "^7.1.2",
-        "hoist-non-react-statics": "^3.0.0",
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.0.3.tgz",
+      "integrity": "sha512-vYZA7ftOYlDk3NetitsI7fLjryt/widNl1SLXYvFenIpm7vjb4ryK0EeFrgn62usg5fYkyIAWNUPKnwWPevKLg==",
+      "requires": {
+        "@babel/runtime": "^7.4.3",
+        "hoist-non-react-statics": "^3.3.0",
         "invariant": "^2.2.4",
-        "loose-envify": "^1.1.0",
-        "prop-types": "^15.6.1",
-        "react-is": "^16.6.0",
-        "react-lifecycles-compat": "^3.0.0"
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.8.6"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "16.8.6",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
+          "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
+        }
       }
     },
     "react-test-renderer": {
-      "version": "16.8.4",
-      "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.4.tgz",
-      "integrity": "sha512-jQ9Tf/ilIGSr55Cz23AZ/7H3ABEdo9oy2zF9nDHZyhLHDSLKuoILxw2ifpBfuuwQvj4LCoqdru9iZf7gwFH28A==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz",
+      "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==",
       "dev": true,
       "requires": {
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "react-is": "^16.8.4",
-        "scheduler": "^0.13.4"
+        "react-is": "^16.8.6",
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "16.8.6",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
+          "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
+          "dev": true
+        }
       }
     },
     "read-pkg": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
       "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
       "dev": true,
       "requires": {
@@ -9573,20 +9754,26 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
       "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==",
       "requires": {
         "loose-envify": "^1.4.0",
         "symbol-observable": "^1.2.0"
       }
     },
+    "reflect.ownkeys": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
+      "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=",
+      "dev": true
+    },
     "regenerator-runtime": {
-      "version": "0.12.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
-      "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
+      "version": "0.13.2",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+      "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
     },
     "regex-cache": {
       "version": "0.4.4",
       "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
       "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
       "dev": true,
       "requires": {
         "is-equal-shallow": "^0.1.3"
@@ -9844,19 +10031,19 @@
     },
     "rx-lite": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
       "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
       "dev": true
     },
     "rxjs": {
-      "version": "6.4.0",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
-      "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz",
+      "integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==",
       "dev": true,
       "requires": {
         "tslib": "^1.9.0"
       }
     },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -9874,19 +10061,19 @@
     },
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
     "sass": {
-      "version": "1.17.2",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.17.2.tgz",
-      "integrity": "sha512-TBNcwSIEXpXAIaFxQnWbHzhciwPKpHRprQ+1ww+g9eHCiY3PINJs6vQTu+LcBt1vIhrtQGRFIoxJO39TfLrptA==",
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.19.0.tgz",
+      "integrity": "sha512-8kzKCgxCzh8/zEn3AuRwzLWVSSFj8omkiGwqdJdeOufjM+I88dXxu9LYJ/Gw4rRTHXesN0r1AixBuqM6yLQUJw==",
       "dev": true,
       "requires": {
         "chokidar": "^2.0.0"
       },
       "dependencies": {
         "anymatch": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
@@ -9945,33 +10132,33 @@
               "dev": true,
               "requires": {
                 "is-extendable": "^0.1.0"
               }
             }
           }
         },
         "chokidar": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
-          "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+          "version": "2.1.5",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+          "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
           "dev": true,
           "requires": {
             "anymatch": "^2.0.0",
             "async-each": "^1.0.1",
             "braces": "^2.3.2",
             "fsevents": "^1.2.7",
             "glob-parent": "^3.1.0",
             "inherits": "^2.0.3",
             "is-binary-path": "^1.0.0",
             "is-glob": "^4.0.0",
             "normalize-path": "^3.0.0",
             "path-is-absolute": "^1.0.0",
             "readdirp": "^2.2.1",
-            "upath": "^1.1.0"
+            "upath": "^1.1.1"
           }
         },
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
@@ -10181,19 +10368,19 @@
         },
         "is-extglob": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
           "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
           "dev": true
         },
         "is-glob": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-          "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+          "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
           "dev": true,
           "requires": {
             "is-extglob": "^2.1.1"
           }
         },
         "is-number": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -10462,19 +10649,19 @@
           "dev": true,
           "requires": {
             "camelcase": "^3.0.0"
           }
         }
       }
     },
     "sass-lint": {
-      "version": "1.12.1",
-      "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.12.1.tgz",
-      "integrity": "sha1-Yw9pwhaqIGuCMvsqqQe98zNrbYM=",
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/sass-lint/-/sass-lint-1.13.1.tgz",
+      "integrity": "sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==",
       "dev": true,
       "requires": {
         "commander": "^2.8.1",
         "eslint": "^2.7.0",
         "front-matter": "2.1.2",
         "fs-extra": "^3.0.1",
         "glob": "^7.0.0",
         "globule": "^1.0.0",
@@ -10878,19 +11065,20 @@
     },
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
       "dev": true
     },
     "scheduler": {
-      "version": "0.13.4",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.4.tgz",
-      "integrity": "sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==",
+      "version": "0.13.6",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+      "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+      "dev": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
       }
     },
     "schema-utils": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
@@ -10961,19 +11149,19 @@
     },
     "semver-compare": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
       "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
       "dev": true
     },
     "serialize-javascript": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz",
-      "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
+      "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
       "dev": true
     },
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
       "dev": true
     },
@@ -11002,19 +11190,19 @@
     },
     "setimmediate": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
       "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
       "dev": true
     },
     "setprototypeof": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
-      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+      "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
       "dev": true
     },
     "sha.js": {
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
       "dev": true,
       "requires": {
@@ -11061,36 +11249,27 @@
       }
     },
     "signal-exit": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
       "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
       "dev": true
     },
-    "simple-git": {
-      "version": "1.107.0",
-      "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.107.0.tgz",
-      "integrity": "sha512-t4OK1JRlp4ayKRfcW6owrWcRVLyHRUlhGd0uN6ZZTqfDq8a5XpcUdOKiGRNobHEuMtNqzp0vcJNvhYWwh5PsQA==",
-      "dev": true,
-      "requires": {
-        "debug": "^4.0.1"
-      }
-    },
     "sinon": {
-      "version": "7.2.7",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.7.tgz",
-      "integrity": "sha512-rlrre9F80pIQr3M36gOdoCEWzFAMDgHYD8+tocqOw+Zw9OZ8F84a80Ds69eZfcjnzDqqG88ulFld0oin/6rG/g==",
-      "dev": true,
-      "requires": {
-        "@sinonjs/commons": "^1.3.1",
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz",
+      "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.4.0",
         "@sinonjs/formatio": "^3.2.1",
-        "@sinonjs/samsam": "^3.2.0",
+        "@sinonjs/samsam": "^3.3.1",
         "diff": "^3.5.0",
-        "lolex": "^3.1.0",
+        "lolex": "^4.0.1",
         "nise": "^1.4.10",
         "supports-color": "^5.5.0"
       }
     },
     "slash": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
       "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
@@ -11535,19 +11714,19 @@
     },
     "stream-shift": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
       "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
       "dev": true
     },
     "streamroller": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.3.tgz",
-      "integrity": "sha512-P7z9NwP51EltdZ81otaGAN3ob+/F88USJE546joNq7bqRNTe6jc74fTBDyynxP4qpIfKlt/CesEYicuMzI0yJg==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.4.tgz",
+      "integrity": "sha512-Wc2Gm5ygjSX8ZpW9J7Y9FwiSzTlKSvcl0FTTMd3rn7RoxDXpBW+xD9TY5sWL2n0UR61COB0LG1BQvN6nTUQbLQ==",
       "dev": true,
       "requires": {
         "async": "^2.6.1",
         "date-format": "^2.0.0",
         "debug": "^3.1.0",
         "fs-extra": "^7.0.0",
         "lodash": "^4.17.10"
       },
@@ -11702,19 +11881,19 @@
           "dev": true,
           "requires": {
             "emoji-regex": "^7.0.1",
             "is-fullwidth-code-point": "^2.0.0",
             "strip-ansi": "^5.1.0"
           }
         },
         "strip-ansi": {
-          "version": "5.1.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.1.0.tgz",
-          "integrity": "sha512-TjxrkPONqO2Z8QDCpeE2j6n0M6EwxzyDgzEeGp+FbdvaJAt//ClYi6W5my+3ROlC/hZX2KACUwDfK49Ka5eDvg==",
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "dev": true,
           "requires": {
             "ansi-regex": "^4.1.0"
           }
         }
       }
     },
     "tapable": {
@@ -11891,16 +12070,22 @@
           "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
           "dev": true,
           "requires": {
             "kind-of": "^3.0.2"
           }
         }
       }
     },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+      "dev": true
+    },
     "tough-cookie": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
       "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
       "dev": true,
       "requires": {
         "psl": "^1.1.24",
         "punycode": "^1.4.1"
@@ -11979,46 +12164,63 @@
     },
     "type-detect": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
       "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
       "dev": true
     },
     "type-is": {
-      "version": "1.6.16",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
-      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
       "dev": true,
       "requires": {
         "media-typer": "0.3.0",
-        "mime-types": "~2.1.18"
+        "mime-types": "~2.1.24"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.40.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+          "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.24",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+          "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.40.0"
+          }
+        }
       }
     },
     "typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
       "dev": true
     },
     "uglify-js": {
-      "version": "3.4.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
-      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.10.tgz",
+      "integrity": "sha512-/GTF0nosyPLbdJBd+AwYiZ+Hu5z8KXWnO0WCGt1BQ/u9Iamhejykqmz5o1OHJ53+VAk6xVxychonnApDjuqGsw==",
       "dev": true,
       "optional": true,
       "requires": {
-        "commander": "~2.17.1",
+        "commander": "~2.20.0",
         "source-map": "~0.6.1"
       },
       "dependencies": {
         "commander": {
-          "version": "2.17.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+          "version": "2.20.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+          "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
           "dev": true,
           "optional": true
         },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
           "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
           "dev": true,
@@ -12080,25 +12282,16 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz",
       "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==",
       "dev": true,
       "requires": {
         "imurmurhash": "^0.1.4"
       }
     },
-    "universal-user-agent": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.0.3.tgz",
-      "integrity": "sha512-eRHEHhChCBHrZsA4WEhdgiOKgdvgrMIHwnwnqD0r5C6AO8kwKcG7qSku3iXdhvHL3YvsS9ZkSGN8h/hIpoFC8g==",
-      "dev": true,
-      "requires": {
-        "os-name": "^3.0.0"
-      }
-    },
     "universalify": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
       "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
       "dev": true
     },
     "unpipe": {
       "version": "1.0.0",
@@ -12186,22 +12379,16 @@
       }
     },
     "url-join": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
       "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
       "dev": true
     },
-    "url-template": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
-      "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=",
-      "dev": true
-    },
     "url2": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/url2/-/url2-0.0.0.tgz",
       "integrity": "sha1-Tqq9HVw6yQ1iq0SFyZhCKGWgSxo=",
       "dev": true
     },
     "use": {
       "version": "3.1.1",
@@ -12406,33 +12593,33 @@
               "dev": true,
               "requires": {
                 "is-extendable": "^0.1.0"
               }
             }
           }
         },
         "chokidar": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
-          "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+          "version": "2.1.5",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
+          "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
           "dev": true,
           "requires": {
             "anymatch": "^2.0.0",
             "async-each": "^1.0.1",
             "braces": "^2.3.2",
             "fsevents": "^1.2.7",
             "glob-parent": "^3.1.0",
             "inherits": "^2.0.3",
             "is-binary-path": "^1.0.0",
             "is-glob": "^4.0.0",
             "normalize-path": "^3.0.0",
             "path-is-absolute": "^1.0.0",
             "readdirp": "^2.2.1",
-            "upath": "^1.1.0"
+            "upath": "^1.1.1"
           }
         },
         "debug": {
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
@@ -12642,19 +12829,19 @@
         },
         "is-extglob": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
           "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
           "dev": true
         },
         "is-glob": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-          "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+          "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
           "dev": true,
           "requires": {
             "is-extglob": "^2.1.1"
           }
         },
         "is-number": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -12712,19 +12899,19 @@
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
           "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
           "dev": true
         }
       }
     },
     "webpack": {
-      "version": "4.29.6",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.29.6.tgz",
-      "integrity": "sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==",
+      "version": "4.30.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.30.0.tgz",
+      "integrity": "sha512-4hgvO2YbAFUhyTdlR4FNyt2+YaYBYHavyzjCMbZzgglo02rlKi/pcsEzwCuCpsn1ryzIl1cq/u8ArIKu8JBYMg==",
       "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.8.5",
         "@webassemblyjs/helper-module-context": "1.8.5",
         "@webassemblyjs/wasm-edit": "1.8.5",
         "@webassemblyjs/wasm-parser": "1.8.5",
         "acorn": "^6.0.5",
         "acorn-dynamic-import": "^4.0.0",
@@ -12794,19 +12981,19 @@
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
           "dev": true,
           "requires": {
             "ms": "2.0.0"
           }
         },
         "eslint-scope": {
-          "version": "4.0.2",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.2.tgz",
-          "integrity": "sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==",
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
           "dev": true,
           "requires": {
             "esrecurse": "^4.1.0",
             "estraverse": "^4.1.1"
           }
         },
         "expand-brackets": {
           "version": "2.1.4",
@@ -13070,32 +13257,32 @@
             "ajv": "^6.1.0",
             "ajv-errors": "^1.0.0",
             "ajv-keywords": "^3.1.0"
           }
         }
       }
     },
     "webpack-cli": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.2.3.tgz",
-      "integrity": "sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.1.tgz",
+      "integrity": "sha512-c2inFU7SM0IttEgF7fK6AaUsbBnORRzminvbyRKS+NlbQHVZdCtzKBlavRL5359bFsywXGRAItA5di/IruC8mg==",
       "dev": true,
       "requires": {
         "chalk": "^2.4.1",
         "cross-spawn": "^6.0.5",
         "enhanced-resolve": "^4.1.0",
         "findup-sync": "^2.0.0",
         "global-modules": "^1.0.0",
         "import-local": "^2.0.0",
         "interpret": "^1.1.0",
         "loader-utils": "^1.1.0",
         "supports-color": "^5.5.0",
         "v8-compile-cache": "^2.0.2",
-        "yargs": "^12.0.4"
+        "yargs": "^12.0.5"
       },
       "dependencies": {
         "json5": {
           "version": "1.0.1",
           "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
           "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
           "dev": true,
           "requires": {
@@ -13179,25 +13366,16 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
       "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
       "dev": true,
       "requires": {
         "string-width": "^1.0.2 || 2"
       }
     },
-    "windows-release": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.1.0.tgz",
-      "integrity": "sha512-hBb7m7acFgQPQc222uEQTmdcGLeBmQLNLFIh0rDk3CwFOBrfjefLzEfEfmpMq8Af/n/GnFf3eYf203FY1PmudA==",
-      "dev": true,
-      "requires": {
-        "execa": "^0.10.0"
-      }
-    },
     "wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
       "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
       "dev": true
     },
     "worker-farm": {
       "version": "1.6.0",
--- a/browser/components/newtab/package.json
+++ b/browser/components/newtab/package.json
@@ -4,81 +4,78 @@
   "version": "1.14.3",
   "author": "Mozilla (https://mozilla.org/)",
   "bugs": {
     "url": "https://github.com/mozilla/activity-stream/issues"
   },
   "dependencies": {
     "fluent": "0.6.4",
     "fluent-react": "0.7.0",
-    "react": "16.8.3",
-    "react-dom": "16.8.3",
-    "react-intl": "2.4.0",
-    "react-redux": "5.1.0",
+    "react": "16.8.6",
+    "react-dom": "16.8.6",
+    "react-intl": "2.8.0",
+    "react-redux": "7.0.3",
     "redux": "4.0.1",
     "reselect": "4.0.0"
   },
   "devDependencies": {
-    "@octokit/rest": "16.17.0",
+    "@babel/core": "7.4.4",
+    "@babel/plugin-proposal-async-generator-functions": "7.2.0",
+    "@babel/preset-react": "7.0.0",
     "acorn": "6.1.1",
-    "@babel/core": "7.3.4",
     "babel-eslint": "10.0.1",
     "babel-loader": "8.0.5",
-    "@babel/preset-react": "7.0.0",
     "babel-plugin-jsm-to-commonjs": "0.5.0",
     "babel-plugin-jsm-to-esmodules": "0.6.0",
-    "@babel/plugin-proposal-async-generator-functions": "7.2.0",
     "chai": "4.2.0",
     "chai-json-schema": "1.5.0",
-    "co-task": "1.0.0",
     "cpx": "1.5.0",
     "enzyme": "3.9.0",
-    "enzyme-adapter-react-16": "1.10.0",
-    "eslint": "5.15.1",
+    "enzyme-adapter-react-16": "1.12.1",
+    "eslint": "5.16.0",
     "eslint-plugin-fetch-options": "0.0.4",
-    "eslint-plugin-import": "2.16.0",
+    "eslint-plugin-import": "2.17.2",
     "eslint-plugin-json": "1.4.0",
     "eslint-plugin-jsx-a11y": "6.2.1",
-    "eslint-plugin-mozilla": "1.1.1",
+    "eslint-plugin-mozilla": "1.2.1",
     "eslint-plugin-no-unsanitized": "3.0.2",
-    "eslint-plugin-promise": "4.0.1",
+    "eslint-plugin-promise": "4.1.1",
     "eslint-plugin-react": "7.12.4",
-    "eslint-plugin-react-hooks": "1.5.0",
-    "eslint-watch": "5.0.1",
+    "eslint-plugin-react-hooks": "1.6.0",
+    "eslint-watch": "5.1.2",
     "husky": "1.3.1",
     "istanbul-instrumenter-loader": "3.0.1",
     "joi-browser": "13.4.0",
-    "karma": "4.0.1",
+    "karma": "4.1.0",
     "karma-chai": "0.1.0",
     "karma-coverage-istanbul-reporter": "2.0.5",
     "karma-firefox-launcher": "1.1.0",
     "karma-mocha": "1.3.0",
     "karma-mocha-reporter": "2.2.5",
     "karma-sinon": "1.0.5",
     "karma-sourcemap-loader": "0.3.7",
     "karma-webpack": "3.0.5",
     "loader-utils": "0.2.16",
     "minimist": "1.2.0",
-    "mocha": "6.0.2",
+    "mocha": "6.1.4",
     "mock-raf": "1.0.1",
-    "node-fetch": "2.3.0",
-    "node-sass": "4.11.0",
+    "node-fetch": "2.5.0",
+    "node-sass": "4.12.0",
     "npm-run-all": "4.1.5",
     "pontoon-to-json": "2.0.0",
     "prop-types": "15.7.2",
-    "raw-loader": "1.0.0",
-    "react-test-renderer": "16.8.4",
+    "raw-loader": "2.0.0",
+    "react-test-renderer": "16.8.6",
     "rimraf": "2.6.3",
-    "sass": "1.17.2",
-    "sass-lint": "1.12.1",
+    "sass": "1.19.0",
+    "sass-lint": "1.13.1",
     "shelljs": "0.8.3",
-    "simple-git": "1.107.0",
-    "sinon": "7.2.7",
-    "webpack": "4.29.6",
-    "webpack-cli": "3.2.3",
+    "sinon": "7.3.2",
+    "webpack": "4.30.0",
+    "webpack-cli": "3.3.1",
     "yamscripts": "0.1.0"
   },
   "engines": {
     "firefox": ">=45.0 <=*",
     "//": "when changing node versions, also edit .travis.yml and .nvmrc",
     "node": "8.*",
     "npm": "6.4.1"
   },
--- a/browser/components/newtab/prerendered/locales/ach/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/ach/activity-stream-strings.js
@@ -82,17 +82,17 @@ window.gActivityStreamStrings = {
   "highlights_empty_state": "Cak yeny, ka wa binyuto coc akwana mabeco, video, ki potbuk mukene ma ilimo cokcokki onyo ma kiketo alamabuk kany.",
   "topstories_empty_state": "Ityeko weng. Rot doki lacen pi lok madito mapol ki bot {provider}. Pe itwero kuro? Yer lok macuke lamal me nongo lok mabeco mapol ki i but kakube.",
   "error_fallback_default_info": "Aii, gin mo otime marac i cano jami man.",
   "error_fallback_default_refresh_suggestion": "Nwo cano potbuk me temo odoco.",
   "section_menu_action_remove_section": "Kwany bute",
   "section_menu_action_collapse_section": "Kan bute",
   "section_menu_action_expand_section": "Yar bute",
   "section_menu_action_manage_section": "Lo bute",
-  "section_menu_action_manage_webext": "Manage Extension",
+  "section_menu_action_manage_webext": "Lo Lamed",
   "section_menu_action_add_topsite": "Med Kakube maloyo",
   "section_menu_action_add_search_engine": "Med ingin me yeny",
   "section_menu_action_move_up": "Kob Malo",
   "section_menu_action_move_down": "Kob Piny",
   "section_menu_action_privacy_notice": "Ngec me mung",
   "firstrun_title": "Wot ki Firefox",
   "firstrun_content": "Nong alamabuk mamegi, gin mukato, mung me donyo ki ter mukene i nyonyo ni weng.",
   "firstrun_learn_more_link": "Nong ngec mapol ikom Akaunt me Firefox",
--- a/browser/components/newtab/prerendered/static/activity-stream-initial-state.js
+++ b/browser/components/newtab/prerendered/static/activity-stream-initial-state.js
@@ -6,18 +6,17 @@ window.gActivityStreamPrerenderedState =
     "editForm": null,
     "showSearchShortcutsForm": false,
     "searchShortcuts": []
   },
   "App": {
     "initialized": false
   },
   "ASRouter": {
-    "initialized": false,
-    "allowLegacySnippets": null
+    "initialized": false
   },
   "Snippets": {
     "initialized": false
   },
   "Prefs": {
     "initialized": true,
     "values": {
       "feeds.topsites": true,
--- a/browser/components/newtab/test/browser/browser_asrouter_targeting.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_targeting.js
@@ -144,16 +144,36 @@ add_task(async function check_usesFirefo
   is(await ASRouterTargeting.Environment.usesFirefoxSync, true,
     "should return true if a fx account is set");
 
   const message = {id: "foo", targeting: "usesFirefoxSync"};
   is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
     "should select correct item by usesFirefoxSync");
 });
 
+add_task(async function check_isFxAEnabled() {
+  await pushPrefs(["identity.fxaccounts.enabled", false]);
+  is(await ASRouterTargeting.Environment.isFxAEnabled, false,
+    "should return false if fxa is disabled");
+
+  const message = {id: "foo", targeting: "isFxAEnabled"};
+  is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), undefined,
+    "should not select a message if fxa is disabled");
+});
+
+add_task(async function check_isFxAEnabled() {
+  await pushPrefs(["identity.fxaccounts.enabled", true]);
+  is(await ASRouterTargeting.Environment.isFxAEnabled, true,
+    "should return true if fxa is enabled");
+
+  const message = {id: "foo", targeting: "isFxAEnabled"};
+  is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
+    "should select the correct message");
+});
+
 add_task(async function check_totalBookmarksCount() {
   // Make sure we remove default bookmarks so they don't interfere
   await clearHistoryAndBookmarks();
   const message = {id: "foo", targeting: "totalBookmarksCount > 0"};
 
   const results = await ASRouterTargeting.findMatchingMessage({messages: [message]});
   is(results ? JSON.stringify(results) : results, undefined,
     "Should not select any message because bookmarks count is not 0");
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -1,9 +1,14 @@
-import {_ASRouter, MessageLoaderUtils} from "lib/ASRouter.jsm";
+import {
+  _ASRouter,
+  chooseBranch,
+  MessageLoaderUtils,
+  TRAILHEAD_CONFIG,
+} from "lib/ASRouter.jsm";
 import {ASRouterTargeting, QueryCache} from "lib/ASRouterTargeting.jsm";
 import {
   CHILD_TO_PARENT_MESSAGE_NAME,
   FAKE_LOCAL_MESSAGES,
   FAKE_LOCAL_PROVIDER,
   FAKE_LOCAL_PROVIDERS,
   FAKE_RECOMMENDATION,
   FAKE_REMOTE_MESSAGES,
@@ -12,17 +17,19 @@ import {
   FakeRemotePageManager,
   PARENT_TO_CHILD_MESSAGE_NAME,
 } from "./constants";
 import {actionCreators as ac} from "common/Actions.jsm";
 import {ASRouterPreferences} from "lib/ASRouterPreferences.jsm";
 import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
 import {CFRPageActions} from "lib/CFRPageActions.jsm";
 import {GlobalOverrider} from "test/unit/utils";
+import {PanelTestProvider} from "lib/PanelTestProvider.jsm";
 import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
+import {SnippetsTestMessageProvider} from "lib/SnippetsTestMessageProvider.jsm";
 
 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() {}};
 
@@ -70,17 +77,18 @@ describe("ASRouter", () => {
   function setMessageProviderPref(value) {
     sandbox.stub(ASRouterPreferences, "providers").get(() => value);
   }
 
   async function createRouterAndInit(providers = FAKE_PROVIDERS) {
     setMessageProviderPref(providers);
     channel = new FakeRemotePageManager();
     dispatchStub = sandbox.stub();
-    Router = new _ASRouter(FAKE_LOCAL_PROVIDERS);
+    // `.freeze` to catch any attempts at modifying the object
+    Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
     await Router.init(channel, createFakeStorage(), dispatchStub);
   }
 
   beforeEach(async () => {
     globals = new GlobalOverrider();
     messageBlockList = [];
     providerBlockList = [];
     messageImpressions = {};
@@ -103,18 +111,24 @@ describe("ASRouter", () => {
       _clearCache: () => sinon.stub(),
       getAttrDataAsync: () => (Promise.resolve({content: "addonID"})),
     };
     FakeBookmarkPanelHub = {
       init: sandbox.stub(),
       uninit: sandbox.stub(),
       _forceShowMessage: sandbox.stub(),
     };
-    globals.set("AttributionCode", fakeAttributionCode);
-    globals.set("BookmarkPanelHub", FakeBookmarkPanelHub);
+    globals.set({
+      AttributionCode: fakeAttributionCode,
+      // Testing framework doesn't know how to `defineLazyModuleGetter` so we're
+      // importing these modules into the global scope ourselves.
+      SnippetsTestMessageProvider,
+      PanelTestProvider,
+      BookmarkPanelHub: FakeBookmarkPanelHub,
+    });
     await createRouterAndInit();
   });
   afterEach(() => {
     ASRouterPreferences.uninit();
     sandbox.restore();
     globals.restore();
   });
 
@@ -146,17 +160,17 @@ describe("ASRouter", () => {
       messageImpressions = {foo: [0, 1, 2]};
       setMessageProviderPref([{id: "onboarding", type: "local", messages: [testMessage]}]);
       Router = new _ASRouter();
       await Router.init(channel, createFakeStorage(), dispatchStub);
 
       assert.deepEqual(Router.state.messageImpressions, messageImpressions);
     });
     it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
-      Router = new _ASRouter(FAKE_LOCAL_PROVIDERS);
+      Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
 
       const loadMessagesSpy = sandbox.spy(Router, "loadMessagesFromAllProviders");
       await Router.init(channel, createFakeStorage(), dispatchStub);
 
       assert.calledOnce(loadMessagesSpy);
       assert.isArray(Router.state.messages);
       assert.lengthOf(Router.state.messages, FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length);
     });
@@ -183,16 +197,37 @@ describe("ASRouter", () => {
       previousSessionEnd = 200;
       await createRouterAndInit();
 
       assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
     });
     it("should dispatch a AS_ROUTER_INITIALIZED event to AS with ASRouterPreferences.specialConditions", async () => {
       assert.calledWith(Router.dispatchToAS, ac.BroadcastToContent({type: "AS_ROUTER_INITIALIZED", data: ASRouterPreferences.specialConditions}));
     });
+    describe("lazily loading local test providers", () => {
+      afterEach(() => {
+        Router.uninit();
+      });
+      it("should add the local test providers on init if devtools are enabled", async () => {
+        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
+
+        await createRouterAndInit();
+
+        assert.property(Router._localProviders, "SnippetsTestMessageProvider");
+        assert.property(Router._localProviders, "PanelTestProvider");
+      });
+      it("should not add the local test providers on init if devtools are disabled", async () => {
+        sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
+
+        await createRouterAndInit();
+
+        assert.notProperty(Router._localProviders, "SnippetsTestMessageProvider");
+        assert.notProperty(Router._localProviders, "PanelTestProvider");
+      });
+    });
   });
 
   describe("preference changes", () => {
     it("should call ASRouterPreferences.init and add a listener on init", () => {
       assert.calledOnce(ASRouterPreferences.init);
       assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);
     });
     it("should call ASRouterPreferences.uninit and remove the listener on uninit", () => {
@@ -908,17 +943,17 @@ describe("ASRouter", () => {
         const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
         await Router.onMessage(msg);
 
         assert.notCalled(msg.target.sendAsyncMessage);
         assert.calledOnce(CFRPageActions.forceRecommendation);
       });
 
       it("should call BookmarkPanelHub._forceShowMessage the provider is cfr-fxa", async () => {
-        const testMessage = {id: "foo", template: "cfr_doorhanger", provider: "cfr-fxa"};
+        const testMessage = {id: "foo", template: "fxa_bookmark_panel"};
         await Router.setState({messages: [testMessage]});
         const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
         await Router.onMessage(msg);
 
         assert.notCalled(msg.target.sendAsyncMessage);
         assert.calledOnce(FakeBookmarkPanelHub._forceShowMessage);
       });
 
@@ -1518,9 +1553,164 @@ describe("ASRouter", () => {
       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 () => {
+      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 () => {
+      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 () => {
+      sandbox.spy(Router, "setupTrailhead");
+      const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
+      await Router.onMessage(msg);
+
+      assert.calledOnce(Router.setupTrailhead);
+    });
+
+    it("should have trailheadInterrupt and trailheadTriplet in the message context", async () => {
+      sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
+      sandbox.stub(Router, "_generateTrailheadBranches").resolves({experiment: "", interrupt: "join", triplet: "privacy"});
+      await Router.setupTrailhead();
+
+      assert.propertyVal(Router._getMessagesContext(), "trailheadInterrupt", "join");
+      assert.propertyVal(Router._getMessagesContext(), "trailheadTriplet", "privacy");
+    });
+
+    describe(".setupTrailhead", () => {
+      let getBoolPrefStub;
+
+      beforeEach(() => {
+        getBoolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
+      });
+
+      const configWithExperiment = {experiment: "interrupt", interrupt: "join", triplet: "privacy"};
+      const configWithoutExperiment = {experiment: "", interrupt: "control", triplet: ""};
+
+      it("should generates an experiment/branch configuration and update Router.state", async () => {
+        const config = configWithoutExperiment;
+        sandbox.stub(Router, "_generateTrailheadBranches").resolves(config);
+
+        await Router.setupTrailhead();
+
+        assert.propertyVal(Router.state, "trailheadInitialized", true);
+        assert.propertyVal(Router.state, "trailheadInterrupt", config.interrupt);
+        assert.propertyVal(Router.state, "trailheadTriplet", config.triplet);
+      });
+      it("should only run once", async () => {
+        sandbox.spy(Router, "setState");
+
+        await Router.setupTrailhead();
+        await Router.setupTrailhead();
+        await Router.setupTrailhead();
+
+        assert.calledOnce(Router.setState);
+      });
+      it("should return early if DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
+        getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
+
+        await Router.setupTrailhead();
+
+        sandbox.spy(Router, "setState");
+        assert.notCalled(Router.setState);
+      });
+      it("should set active experiment if one is defined", async () => {
+        sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithExperiment);
+        sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
+
+        await Router.setupTrailhead();
+
+        assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
+      });
+      it("should not set an active experiment if no experiment is defined", async () => {
+        sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithoutExperiment);
+        sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
+
+        await Router.setupTrailhead();
+
+        assert.notCalled(global.TelemetryEnvironment.setExperimentActive);
+      });
+    });
+
+    describe("._generateTrailheadBranches", () => {
+      async function checkReturnValue(expected) {
+        const result = await Router._generateTrailheadBranches();
+        assert.propertyVal(result, "experiment", expected.experiment);
+        assert.propertyVal(result, "interrupt", expected.interrupt);
+        assert.propertyVal(result, "triplet", expected.triplet);
+      }
+      it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
+        checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
+      });
+      it("should use values in override pref if it is set with no experiment", async () => {
+        getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("join-privacy");
+        checkReturnValue({experiment: "", interrupt: "join", triplet: "privacy"});
+
+        getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("nofirstrun");
+        checkReturnValue({experiment: "", interrupt: "nofirstrun", triplet: ""});
+      });
+      it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
+        checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
+      });
+      it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
+        checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
+      });
+      it("should roll for experiment if locale is in TRAILHEAD_LOCALES", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
+        checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
+      });
+      it("should roll a triplet experiment", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(2); // 2 = triplets experiment
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
+        checkReturnValue({experiment: "triplets", interrupt: "join", triplet: "multidevice"});
+      });
+      it("should roll no experiment", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(0); // 0 = no experiment
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
+        checkReturnValue({experiment: "", interrupt: "join", triplet: "supercharge"});
+      });
+    });
+  });
+
+  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]);
+      assert.equal(result, "foo");
+    });
+    it("should use 1 as the default ratio", async () => {
+      sandbox.stub(global.Sampling, "ratioSample").resolves(1);
+      const result = await chooseBranch("bleep", [["foo"], ["bar"]]);
+
+      assert.calledWith(global.Sampling.ratioSample, "bleep", [1, 1]);
+      assert.equal(result, "bar");
+    });
+  });
 });
--- a/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
@@ -1,9 +1,9 @@
-import {_ASRouterPreferences, ASRouterPreferences as ASRouterPreferencesSingleton, TEST_PROVIDER} from "lib/ASRouterPreferences.jsm";
+import {_ASRouterPreferences, ASRouterPreferences as ASRouterPreferencesSingleton, TEST_PROVIDERS} from "lib/ASRouterPreferences.jsm";
 const FAKE_PROVIDERS = [{id: "foo"}, {id: "bar"}];
 
 const PROVIDER_PREF_BRANCH = "browser.newtabpage.activity-stream.asrouter.providers.";
 const DEVTOOLS_PREF = "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
 const SNIPPETS_USER_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
 const CFR_USER_PREF_ADDONS = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons";
 const CFR_USER_PREF_FEATURES = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";
 
@@ -125,21 +125,21 @@ describe("ASRouterPreferences", () => {
       assert.callCount(stringPrefStub, 4);
     });
     it("should skip the pref without throwing if a pref is not parsable", () => {
       stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns("not json");
       ASRouterPreferences.init();
 
       assert.deepEqual(ASRouterPreferences.providers, [{id: "bar"}]);
     });
-    it("should include TEST_PROVIDER if devtools is turned on", () => {
+    it("should include TEST_PROVIDERS if devtools is turned on", () => {
       boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
       ASRouterPreferences.init();
 
-      assert.deepEqual(ASRouterPreferences.providers, [TEST_PROVIDER, ...FAKE_PROVIDERS]);
+      assert.deepEqual(ASRouterPreferences.providers, [...TEST_PROVIDERS, ...FAKE_PROVIDERS]);
     });
   });
   describe(".devtoolsEnabled", () => {
     it("should read the pref the first time .devtoolsEnabled is accessed", () => {
       ASRouterPreferences.init();
 
       const result = ASRouterPreferences.devtoolsEnabled;
       assert.deepEqual(result, false);
@@ -156,35 +156,16 @@ describe("ASRouterPreferences", () => {
       // Intentionally not initialized
       const [firstCall, secondCall] = [ASRouterPreferences.devtoolsEnabled, ASRouterPreferences.devtoolsEnabled];
 
       assert.deepEqual(firstCall, false);
       assert.deepEqual(secondCall, false);
       assert.calledTwice(boolPrefStub);
     });
   });
-  describe(".specialConditions", () => {
-    it("should return .allowLegacySnippets=true if snippets is not present or disabled", () => {
-      ASRouterPreferences.init();
-      let testProviders = [];
-      sandbox.stub(ASRouterPreferences, "providers").get(() => testProviders);
-
-      testProviders = [{id: "foo"}];
-      assert.isTrue(ASRouterPreferences.specialConditions.allowLegacySnippets);
-
-      testProviders = [{id: "snippets", enabled: false}];
-      assert.isTrue(ASRouterPreferences.specialConditions.allowLegacySnippets);
-    });
-    it("should return .allowLegacySnippets=false if snippets is enabled", () => {
-      ASRouterPreferences.init();
-      sandbox.stub(ASRouterPreferences, "providers").get(() => [{id: "snippets", enabled: true}]);
-
-      assert.isFalse(ASRouterPreferences.specialConditions.allowLegacySnippets);
-    });
-  });
   describe("#getUserPreference(providerId)", () => {
     it("should return the user preference for snippets", () => {
       boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
       assert.isTrue(ASRouterPreferences.getUserPreference("snippets"));
     });
   });
   describe("#getAllUserPreferences", () => {
     it("should return all user preferences", () => {
@@ -272,17 +253,17 @@ describe("ASRouterPreferences", () => {
       boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
       global.Services.prefs.getChildList
         .withArgs(PROVIDER_PREF_BRANCH).returns([]);
       ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
 
       // Cache should be invalidated so we access the new value of the pref now
       // Note that providers needs to be invalidated because devtools adds test content to it.
       assert.isTrue(ASRouterPreferences.devtoolsEnabled);
-      assert.deepEqual(ASRouterPreferences.providers, [TEST_PROVIDER]);
+      assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS);
     });
     it("should call listeners added with .addListener", () => {
       const callback1 = sinon.stub();
       const callback2 = sinon.stub();
       ASRouterPreferences.init();
       ASRouterPreferences.addListener(callback1);
       ASRouterPreferences.addListener(callback2);
 
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/PanelTestProvider.test.js
@@ -0,0 +1,12 @@
+import {PanelTestProvider} from "lib/PanelTestProvider.jsm";
+import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json";
+const messages = PanelTestProvider.getMessages();
+
+describe("PanelTestProvider", () => {
+  it("should have a message", () => {
+    assert.lengthOf(messages, 1);
+  });
+  it("should be a valid message", () => {
+    assert.jsonSchema(messages[0].content, schema);
+  });
+});
--- a/browser/components/newtab/test/unit/asrouter/TargetingDocs.test.js
+++ b/browser/components/newtab/test/unit/asrouter/TargetingDocs.test.js
@@ -1,16 +1,18 @@
 import {ASRouterTargeting} from "lib/ASRouterTargeting.jsm";
 import docs from "content-src/asrouter/docs/targeting-attributes.md";
 
 // The following targeting parameters are either deprecated or should not be included in the docs for some reason.
 const SKIP_DOCS = [];
 // These are extra message context attributes via ASRouter.jsm
 const MESSAGE_CONTEXT_ATTRIBUTES = [
   "previousSessionEnd",
+  "trailheadInterrupt",
+  "trailheadTriplet",
 ];
 
 function getHeadingsFromDocs() {
   const re = /### `(\w+)`/g;
   const found = [];
   let match = 1;
   while (match) {
     match = re.exec(docs);
--- a/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/asrouter-content.test.jsx
@@ -1,13 +1,12 @@
 import {ASRouterUISurface, ASRouterUtils} from "content-src/asrouter/asrouter-content";
+import {GlobalOverrider, mountWithIntl} from "test/unit/utils";
 import {OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME} from "content-src/lib/init-store";
 import {FAKE_LOCAL_MESSAGES} from "./constants";
-import {GlobalOverrider} from "test/unit/utils";
-import {mount} from "enzyme";
 import {OnboardingMessageProvider} from "lib/OnboardingMessageProvider.jsm";
 import React from "react";
 import {Trailhead} from "../../../content-src/asrouter/templates/Trailhead/Trailhead";
 
 let [FAKE_MESSAGE] = FAKE_LOCAL_MESSAGES;
 const FAKE_NEWSLETTER_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "newsletter");
 const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
 const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "belowsearch");
@@ -80,17 +79,17 @@ describe("ASRouterUISurface", () => {
     global.set({
       RPMAddMessageListener: sandbox.stub(),
       RPMRemoveMessageListener: sandbox.stub(),
       RPMSendAsyncMessage: sandbox.stub(),
     });
 
     sandbox.stub(ASRouterUtils, "sendTelemetry");
 
-    wrapper = mount(<ASRouterUISurface document={fakeDocument} />);
+    wrapper = mountWithIntl(<ASRouterUISurface document={fakeDocument} />);
   });
 
   afterEach(() => {
     sandbox.restore();
     global.restore();
   });
 
   it("should render the component if a message id is defined", () => {
@@ -145,17 +144,17 @@ describe("ASRouterUISurface", () => {
     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").returns({theme: "dark"});
 
-    wrapper = mount(<ASRouterUISurface document={fakeDocument} />);
+    wrapper = mountWithIntl(<ASRouterUISurface document={fakeDocument} />);
 
     assert.calledOnce(stub);
     assert.property(stub.firstCall.args[0].detail.data, "ntp_background");
     assert.property(stub.firstCall.args[0].detail.data, "ntp_text");
     assert.property(stub.firstCall.args[0].detail.data, "sidebar");
     assert.property(stub.firstCall.args[0].detail.data, "sidebar_text");
   });
 
--- a/browser/components/newtab/test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js
+++ b/browser/components/newtab/test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js
@@ -1,29 +1,23 @@
 import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json";
 
 const DEFAULT_CONTENT = {
   "title": "Sync your bookmarks everywhere",
   "text": "Great find! Now don't be left without this bookmark.",
-  "link": {
-    "text": "Sync bookmarks now",
-    "url": "https://mozilla.com",
-  },
+  "cta": "Sync bookmarks now",
   "info_icon": {
     "tooltiptext": "Learn more",
   },
 };
 
 const L10N_CONTENT = {
   "title": {string_id: "cfr-bookmark-title"},
   "text": {string_id: "cfr-bookmark-body"},
-  "link": {
-    "text": {string_id: "cfr-bookmark-link-text"},
-    "url": "https://mozilla.com",
-  },
+  "cta": {string_id: "cfr-bookmark-link-text"},
   "info_icon": {
     "tooltiptext": {string_id: "cfr-bookmark-tooltip-text"},
   },
 };
 
 describe("CFR FxA Message Schema", () => {
   it("should validate DEFAULT_CONTENT", () => {
     assert.jsonSchema(DEFAULT_CONTENT, schema);
--- a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx
@@ -1,13 +1,13 @@
 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";
+import {_Trailhead as Trailhead} from "content-src/asrouter/templates/Trailhead/Trailhead";
 
 describe("<Trailhead>", () => {
   let wrapper;
   let dispatch;
   let sandbox;
 
   beforeEach(async () => {
     sandbox = sinon.sandbox.create();
--- a/browser/components/newtab/test/unit/common/Reducers.test.js
+++ b/browser/components/newtab/test/unit/common/Reducers.test.js
@@ -1051,22 +1051,13 @@ describe("Reducers", () => {
       assert.propertyVal(nextState, "fakeFocus", true);
     });
     it("should set focus and hide to false on SHOW_SEARCH", () => {
       const nextState = Search(undefined, {type: "SHOW_SEARCH"});
       assert.propertyVal(nextState, "fakeFocus", false);
       assert.propertyVal(nextState, "hide", false);
     });
   });
-  describe("ASRouter", () => {
-    it("should listen for pref changes on AS_ROUTER_PREF_CHANGED", async () => {
-      const action = {data: {foo: "bar"}, type: at.AS_ROUTER_PREF_CHANGED};
-
-      const result = ASRouter({}, action);
-
-      assert.propertyVal(result, "foo", "bar");
-    });
-  });
   it("should set initialized to true on AS_ROUTER_INITIALIZED", () => {
     const nextState = ASRouter(undefined, {type: "AS_ROUTER_INITIALIZED"});
     assert.propertyVal(nextState, "initialized", true);
   });
 });
--- a/browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx
@@ -61,17 +61,17 @@ describe("ASRouterAdmin", () => {
   });
   it("should set a .expanded class on the outer div if props.collapsed is false", () => {
     wrapper.setProps({collapsed: false});
     assert.isTrue(wrapper.find(".asrouter-admin").hasClass("expanded"));
     assert.isFalse(wrapper.find(".asrouter-admin").hasClass("collapsed"));
   });
   describe("#getSection", () => {
     it("should render a message provider section by default", () => {
-      assert.equal(wrapper.find("h2").at(1).text(), "Messages");
+      assert.equal(wrapper.find("h2").at(2).text(), "Messages");
     });
     it("should render a targeting section for targeting route", () => {
       wrapper = shallow(<ASRouterAdminInner location={{routes: ["targeting"]}} />);
       assert.equal(wrapper.find("h2").at(0).text(), "Targeting Utilities");
     });
     it("should render a pocket section for pocket route", () => {
       wrapper = shallow(<ASRouterAdminInner location={{routes: ["pocket"]}} Sections={[]} />);
       assert.equal(wrapper.find("h2").at(0).text(), "Pocket");
--- a/browser/components/newtab/test/unit/lib/BookmarkPanelHub.test.js
+++ b/browser/components/newtab/test/unit/lib/BookmarkPanelHub.test.js
@@ -3,27 +3,63 @@ import {GlobalOverrider} from "test/unit
 
 describe("BookmarkPanelHub", () => {
   let globals;
   let sandbox;
   let instance;
   let fakeAddImpression;
   let fakeHandleMessageRequest;
   let fakeL10n;
+  let fakeMessage;
+  let fakeTarget;
+  let fakeContainer;
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     globals = new GlobalOverrider();
 
     fakeL10n = {setAttributes: sandbox.stub(), translateElements: sandbox.stub()};
     globals.set("DOMLocalization", function() { return fakeL10n; }); // eslint-disable-line prefer-arrow-callback
     globals.set("FxAccounts", {config: {promiseEmailFirstURI: sandbox.stub()}});
 
     instance = new _BookmarkPanelHub();
     fakeAddImpression = sandbox.stub();
     fakeHandleMessageRequest = sandbox.stub();
+    fakeMessage = {
+      text: "text",
+      title: "title",
+      link: {
+        url: "url",
+        text: "text",
+      },
+      color: "white",
+      background_color_1: "#7d31ae",
+      background_color_2: "#5033be",
+      info_icon: {tooltiptext: "cfr-bookmark-tooltip-text"},
+      close_button: {tooltiptext: "cfr-bookmark-tooltip-text"},
+    };
+    fakeContainer = {
+      addEventListener: sandbox.stub(),
+      setAttribute: sandbox.stub(),
+      classList: {add: sandbox.stub()},
+      appendChild: sandbox.stub(),
+      children: [],
+      style: {},
+    };
+    fakeTarget = {
+      document: {
+        createElementNS: sandbox.stub().returns(fakeContainer),
+      },
+      container: {
+        querySelector: sandbox.stub(),
+        appendChild: sandbox.stub(),
+      },
+      hidePopup: sandbox.stub(),
+      infoButton: {},
+      close: sandbox.stub(),
+    };
   });
   afterEach(() => {
     instance.uninit();
     sandbox.restore();
     globals.restore();
   });
   it("should create an instance", () => {
     assert.ok(instance);
@@ -47,46 +83,43 @@ describe("BookmarkPanelHub", () => {
     beforeEach(() => {
       sandbox.stub(instance, "onResponse");
       instance.init(fakeHandleMessageRequest, fakeAddImpression);
     });
     afterEach(() => {
       sandbox.restore();
     });
     it("should not re-request messages for the same URL", async () => {
-      const fakeTarget = {url: "foo.com"};
-      instance._response = {url: "foo.com"};
+      instance._response = {url: "foo.com", content: true};
+      fakeTarget.url = "foo.com";
+      sandbox.stub(instance, "showMessage");
 
       await instance.messageRequest(fakeTarget);
 
       assert.notCalled(fakeHandleMessageRequest);
+      assert.calledOnce(instance.showMessage);
     });
     it("should call handleMessageRequest", async () => {
-      const fakeMessage = {};
-      const fakeTarget = {};
       fakeHandleMessageRequest.resolves(fakeMessage);
 
       await instance.messageRequest(fakeTarget, {});
 
       assert.calledOnce(fakeHandleMessageRequest);
       assert.calledWithExactly(fakeHandleMessageRequest, instance._trigger);
     });
     it("should call onResponse", async () => {
-      const fakeMessage = {};
-      const fakeTarget = {};
       fakeHandleMessageRequest.resolves(fakeMessage);
 
       await instance.messageRequest(fakeTarget, {});
 
       assert.calledOnce(instance.onResponse);
       assert.calledWithExactly(instance.onResponse, fakeMessage, fakeTarget, {});
     });
   });
   describe("#onResponse", () => {
-    let fakeTarget;
     beforeEach(() => {
       sandbox.stub(instance, "showMessage");
       sandbox.stub(instance, "sendImpression");
       sandbox.stub(instance, "hideMessage");
       fakeTarget = {infoButton: {disabled: true}};
     });
     it("should show a message when called with a response", () => {
       instance.onResponse({content: "content"}, fakeTarget, {});
@@ -97,53 +130,21 @@ describe("BookmarkPanelHub", () => {
     });
     it("should hide existing messages if no response is provided", () => {
       instance.onResponse(null, fakeTarget);
 
       assert.calledOnce(instance.hideMessage);
       assert.calledWithExactly(instance.hideMessage, fakeTarget);
     });
   });
-  describe("#showMessage", () => {
-    let fakeTarget;
-    let fakeContainer;
-    const fakeMessage = {
-      text: "text",
-      title: "title",
-      link: {
-        url: "url",
-        text: "text",
-      },
-      color: "white",
-      background_color_1: "#7d31ae",
-      background_color_2: "#5033be",
-      info_icon: {tooltiptext: "cfr-bookmark-tooltip-text"},
-      close_button: {tooltiptext: "cfr-bookmark-tooltip-text"},
-    };
+  describe("#showMessage.collapsed=false", () => {
     beforeEach(() => {
       sandbox.stub(instance, "toggleRecommendation");
+      sandbox.stub(instance, "_response").value({collapsed: false});
       instance.init(fakeHandleMessageRequest, fakeAddImpression);
-      fakeContainer = {
-        addEventListener: sandbox.stub(),
-        setAttribute: sandbox.stub(),
-        classList: {add: sandbox.stub()},
-        appendChild: sandbox.stub(),
-        children: [],
-        style: {},
-      };
-      fakeTarget = {
-        document: {
-          createElementNS: sandbox.stub().returns(fakeContainer),
-        },
-        container: {
-          querySelector: sandbox.stub(),
-          appendChild: sandbox.stub(),
-        },
-        hidePopup: sandbox.stub(),
-      };
     });
     it("should create a container", () => {
       fakeTarget.container.querySelector.returns(false);
 
       instance.showMessage(fakeMessage, fakeTarget);
 
       assert.equal(fakeTarget.document.createElementNS.callCount, 5);
       assert.calledOnce(fakeTarget.container.appendChild);
@@ -160,25 +161,52 @@ describe("BookmarkPanelHub", () => {
       fakeTarget.container.querySelector.returns(false);
 
       instance.showMessage(fakeMessage, fakeTarget, windowStub);
       // Call the event listener cb
       await fakeContainer.addEventListener.firstCall.args[1]();
 
       assert.calledOnce(windowStub.ownerGlobal.openLinkIn);
     });
+    it("should collapse the message", () => {
+      fakeTarget.container.querySelector.returns(false);
+      sandbox.spy(instance, "collapseMessage");
+      instance._response.collapsed = false;
+
+      instance.showMessage(fakeMessage, fakeTarget);
+      // Show message calls it once so we need to reset
+      instance.toggleRecommendation.reset();
+      // Call the event listener cb
+      fakeContainer.addEventListener.secondCall.args[1]();
+
+      assert.calledOnce(instance.collapseMessage);
+      assert.calledOnce(fakeTarget.close);
+      assert.isTrue(instance._response.collapsed);
+      assert.calledOnce(instance.toggleRecommendation);
+    });
     it("should call toggleRecommendation `true`", () => {
       instance.showMessage(fakeMessage, fakeTarget);
 
       assert.calledOnce(instance.toggleRecommendation);
       assert.calledWithExactly(instance.toggleRecommendation, true);
     });
   });
+  describe("#showMessage.collapsed=true", () => {
+    beforeEach(() => {
+      sandbox.stub(instance, "_response").value({collapsed: true, target: fakeTarget});
+      sandbox.stub(instance, "toggleRecommendation");
+    });
+    it("should return early if the message is collapsed", () => {
+      instance.showMessage();
+
+      assert.calledOnce(instance.toggleRecommendation);
+      assert.calledWithExactly(instance.toggleRecommendation, false);
+    });
+  });
   describe("#hideMessage", () => {
-    let fakeTarget;
     let removeStub;
     beforeEach(() => {
       sandbox.stub(instance, "toggleRecommendation");
       removeStub = sandbox.stub();
       fakeTarget = {container: {querySelector: sandbox.stub().returns({remove: removeStub})}};
     });
     it("should remove the message", () => {
       instance.hideMessage(fakeTarget);
@@ -234,25 +262,36 @@ describe("BookmarkPanelHub", () => {
       instance.toggleRecommendation();
 
       assert.calledOnce(target.recommendationContainer.removeAttribute);
     });
   });
   describe("#_forceShowMessage", () => {
     it("should call showMessage with the correct args", () => {
       const msg = {content: "foo"};
-      const target = {infoButton: {disabled: false}};
+      const target = {infoButton: {disabled: false}, recommendationContainer: {removeAttribute: sandbox.stub()}};
       sandbox.stub(instance, "showMessage");
       sandbox.stub(instance, "_response").value({target, win: "win"});
 
       instance._forceShowMessage(msg);
 
       assert.calledOnce(instance.showMessage);
       assert.calledWithExactly(instance.showMessage, "foo", target, "win");
     });
+    it("should call toggleRecommendation with true", () => {
+      const msg = {content: "foo"};
+      sandbox.stub(instance, "showMessage");
+      sandbox.stub(instance, "toggleRecommendation");
+      sandbox.stub(instance, "_response").value({fakeTarget, win: "win"});
+
+      instance._forceShowMessage(msg);
+
+      assert.calledOnce(instance.toggleRecommendation);
+      assert.calledWithExactly(instance.toggleRecommendation, true);
+    });
   });
   describe("#sendImpression", () => {
     beforeEach(() => {
       instance.init(fakeHandleMessageRequest, fakeAddImpression);
       instance._response = "foo";
     });
     it("should dispatch an impression", () => {
       instance.sendImpression();
--- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
@@ -140,17 +140,17 @@ describe("DiscoveryStreamFeed", () => {
 
       assert.equal(response, "hi");
     });
     it("should replace urls with $apiKey", async () => {
       sandbox.stub(global.Services.prefs, "getCharPref").returns("replaced");
 
       await feed.fetchFromEndpoint("https://getpocket.cdn.mozilla.net/dummy?consumer_key=$apiKey");
 
-      assert.calledWith(fetchStub, "https://getpocket.cdn.mozilla.net/dummy?consumer_key=replaced", {credentials: "omit"});
+      assert.calledWithMatch(fetchStub, "https://getpocket.cdn.mozilla.net/dummy?consumer_key=replaced", {credentials: "omit"});
     });
   });
 
   describe("#loadLayout", () => {
     it("should fetch data and populate the cache if it is empty", async () => {
       const resp = {layout: ["foo", "bar"]};
       const fakeCache = {};
       sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
@@ -208,16 +208,25 @@ describe("DiscoveryStreamFeed", () => {
       feed.config.hardcoded_layout = true;
       sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));
 
       await feed.loadLayout(feed.store.dispatch);
 
       assert.notCalled(feed.fetchLayout);
       assert.equal(feed.store.getState().DiscoveryStream.spocs.spocs_endpoint, "https://getpocket.cdn.mozilla.net/v3/firefox/unique-spocs?consumer_key=$apiKey");
     });
+    it("should fetch local layout for invalid layout endpoint or when fetch layout fails", async () => {
+      feed.config.hardcoded_layout = false;
+      fetchStub.resolves({ok: false});
+
+      await feed.loadLayout(feed.store.dispatch, true);
+
+      assert.calledOnce(fetchStub);
+      assert.equal(feed.store.getState().DiscoveryStream.spocs.spocs_endpoint, "https://getpocket.cdn.mozilla.net/v3/firefox/unique-spocs?consumer_key=$apiKey");
+    });
   });
 
   describe("#loadLayoutEndPointUsingPref", () => {
     it("should return endpoint if valid key", async () => {
       const endpoint = feed.finalLayoutEndpoint("https://somedomain.org/stories?consumer_key=$apiKey", "test_key_val");
       assert.equal("https://somedomain.org/stories?consumer_key=test_key_val", endpoint);
     });
 
--- a/browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/TelemetryFeed.test.js
@@ -398,23 +398,24 @@ describe("TelemetryFeed", () => {
       session.perf.visibility_event_rcvd_ts = 444.4732;
 
       instance.endSession("foo");
 
       assert.isNumber(session.session_duration);
       assert.ok(Number.isInteger(session.session_duration),
         "session_duration should be an integer");
     });
-    it("shouldn't add session_duration if there's no visibility_event_rcvd_ts", () => {
+    it("shouldn't send session ping if there's no visibility_event_rcvd_ts", () => {
       sandbox.stub(instance, "sendEvent");
-      const session = instance.addSession("foo");
+      instance.addSession("foo");
 
       instance.endSession("foo");
 
-      assert.notProperty(session, "session_duration");
+      assert.notCalled(instance.sendEvent);
+      assert.isFalse(instance.sessions.has("foo"));
     });
     it("should remove the session from .sessions", () => {
       sandbox.stub(instance, "sendEvent");
       instance.addSession("foo");
 
       instance.endSession("foo");
 
       assert.isFalse(instance.sessions.has("foo"));
@@ -423,16 +424,17 @@ describe("TelemetryFeed", () => {
       FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
       FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = true;
       instance = new TelemetryFeed();
 
       sandbox.stub(instance, "sendEvent");
       sandbox.stub(instance, "createSessionEndEvent");
       sandbox.stub(instance.utEvents, "sendSessionEndEvent");
       const session = instance.addSession("foo");
+      session.perf.visibility_event_rcvd_ts = 444.4732;
 
       instance.endSession("foo");
 
       // Did we call sendEvent with the result of createSessionEndEvent?
       assert.calledWith(instance.createSessionEndEvent, session);
 
       let sessionEndEvent = instance.createSessionEndEvent.firstCall.returnValue;
       assert.calledWith(instance.sendEvent, sessionEndEvent);
--- a/browser/components/newtab/test/unit/unit-entry.js
+++ b/browser/components/newtab/test/unit/unit-entry.js
@@ -35,16 +35,19 @@ const TEST_GLOBAL = {
   AppConstants: {MOZILLA_OFFICIAL: true},
   UpdateUtils: {getUpdateChannel() {}},
   BrowserWindowTracker: {getTopWindow() {}},
   ChromeUtils: {
     defineModuleGetter() {},
     generateQI() { return {}; },
     import() { return global; },
   },
+  ClientEnvironment: {
+    get userId() { return "foo123"; },
+  },
   Components: {isSuccessCode: () => true},
   // eslint-disable-next-line object-shorthand
   ContentSearchUIController: function() {}, // NB: This is a function/constructor
   Cc: {
     "@mozilla.org/browser/nav-bookmarks-service;1": {
       addObserver() {},
       getService() {
         return this;
@@ -273,15 +276,23 @@ const TEST_GLOBAL = {
       return Promise.resolve(stringsIds.map(({id, args}) => ({value: {string_id: id, args}})));
     }
   },
   FxAccountsConfig: {
     promiseEmailFirstURI(id) {
       return Promise.resolve(id);
     },
   },
+  TelemetryEnvironment: {
+    setExperimentActive() {},
+  },
+  Sampling: {
+    ratioSample(seed, ratios) {
+      return 0;
+    },
+  },
 };
 overrider.set(TEST_GLOBAL);
 
 describe("activity-stream", () => {
   after(() => overrider.restore());
   files.forEach(file => req(file));
 });
--- a/browser/components/newtab/vendor/react-dev.js
+++ b/browser/components/newtab/vendor/react-dev.js
@@ -1,9 +1,9 @@
-/** @license React v16.8.3
+/** @license React v16.8.6
  * react.development.js
  *
  * Copyright (c) Facebook, Inc. and its affiliates.
  *
  * This source code is licensed under the MIT license found in the
  * LICENSE file in the root directory of this source tree.
  */
 
@@ -12,17 +12,17 @@
 (function (global, factory) {
 	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
 	typeof define === 'function' && define.amd ? define(factory) :
 	(global.React = factory());
 }(this, (function () { 'use strict';
 
 // TODO: this is special because it gets imported during build.
 
-var ReactVersion = '16.8.3';
+var ReactVersion = '16.8.6';
 
 // The Symbol used to tag the ReactElement-like types. If there is no native Symbol
 // nor polyfill, then a plain number is used for performance.
 var hasSymbol = typeof Symbol === 'function' && Symbol.for;
 
 var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;
 var REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca;
 var REACT_FRAGMENT_TYPE = hasSymbol ? Symbol.for('react.fragment') : 0xeacb;
@@ -2616,17 +2616,17 @@ function memo(type, compare) {
     $$typeof: REACT_MEMO_TYPE,
     type: type,
     compare: compare === undefined ? null : compare
   };
 }
 
 function resolveDispatcher() {
   var dispatcher = ReactCurrentDispatcher.current;
-  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)') : void 0;
+  !(dispatcher !== null) ? invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.') : void 0;
   return dispatcher;
 }
 
 function useContext(Context, unstable_observedBits) {
   var dispatcher = resolveDispatcher();
   {
     !(unstable_observedBits === undefined) ? warning$1(false, 'useContext() second argument is reserved for future ' + 'use in React. Passing it is not supported. ' + 'You passed: %s.%s', unstable_observedBits, typeof unstable_observedBits === 'number' && Array.isArray(arguments[2]) ? '\n\nDid you call array.map(useContext)? ' + 'Calling Hooks inside a loop is not supported. ' + 'Learn more at https://fb.me/rules-of-hooks' : '') : void 0;
 
--- a/browser/components/newtab/vendor/react-dom-dev.js
+++ b/browser/components/newtab/vendor/react-dom-dev.js
@@ -1,9 +1,9 @@
-/** @license React v16.8.3
+/** @license React v16.8.6
  * react-dom.development.js
  *
  * Copyright (c) Facebook, Inc. and its affiliates.
  *
  * This source code is licensed under the MIT license found in the
  * LICENSE file in the root directory of this source tree.
  */
 
@@ -5430,25 +5430,39 @@ function containsNode(outerNode, innerNo
     return false;
   }
 }
 
 function isInDocument(node) {
   return node && node.ownerDocument && containsNode(node.ownerDocument.documentElement, node);
 }
 
+function isSameOriginFrame(iframe) {
+  try {
+    // Accessing the contentDocument of a HTMLIframeElement can cause the browser
+    // to throw, e.g. if it has a cross-origin src attribute.
+    // Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g:
+    // iframe.contentDocument.defaultView;
+    // A safety way is to access one of the cross origin properties: Window or Location
+    // Which might result in "SecurityError" DOM Exception and it is compatible to Safari.
+    // https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl
+
+    return typeof iframe.contentWindow.location.href === 'string';
+  } catch (err) {
+    return false;
+  }
+}
+
 function getActiveElementDeep() {
   var win = window;
   var element = getActiveElement();
   while (element instanceof win.HTMLIFrameElement) {
-    // Accessing the contentDocument of a HTMLIframeElement can cause the browser
-    // to throw, e.g. if it has a cross-origin src attribute
-    try {
-      win = element.contentDocument.defaultView;
-    } catch (e) {
+    if (isSameOriginFrame(element)) {
+      win = element.contentWindow;
+    } else {
       return element;
     }
     element = getActiveElement(win.document);
   }
   return element;
 }
 
 /**
@@ -7728,24 +7742,35 @@ function createElement(type, props, root
     } else if (typeof props.is === 'string') {
       // $FlowIssue `createElement` should be updated for Web Components
       domElement = ownerDocument.createElement(type, { is: props.is });
     } else {
       // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
       // See discussion in https://github.com/facebook/react/pull/6896
       // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
       domElement = ownerDocument.createElement(type);
-      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple`
-      // attribute on `select`s needs to be added before `option`s are inserted. This prevents
-      // a bug where the `select` does not scroll to the correct option because singular
-      // `select` elements automatically pick the first item.
+      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
+      // attributes on `select`s needs to be added before `option`s are inserted.
+      // This prevents:
+      // - a bug where the `select` does not scroll to the correct option because singular
+      //  `select` elements automatically pick the first item #13222
+      // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
       // See https://github.com/facebook/react/issues/13222
-      if (type === 'select' && props.multiple) {
+      // and https://github.com/facebook/react/issues/14239
+      if (type === 'select') {
         var node = domElement;
-        node.multiple = true;
+        if (props.multiple) {
+          node.multiple = true;
+        } else if (props.size) {
+          // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
+          // it is possible that no option is selected.
+          //
+          // This is only necessary when a select in "single selection mode".
+          node.size = props.size;
+        }
       }
     }
   } else {
     domElement = ownerDocument.createElementNS(namespaceURI, type);
   }
 
   {
     if (namespaceURI === HTML_NAMESPACE) {
@@ -10118,16 +10143,17 @@ function FiberNode(tag, pendingProps, ke
     this.treeBaseDuration = 0;
   }
 
   {
     this._debugID = debugCounter++;
     this._debugSource = null;
     this._debugOwner = null;
     this._debugIsCurrentlyTiming = false;
+    this._debugHookTypes = null;
     if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
       Object.preventExtensions(this);
     }
   }
 }
 
 // This is a constructor function, rather than a POJO constructor, still
 // please ensure we do the following:
@@ -10185,16 +10211,17 @@ function createWorkInProgress(current, p
     workInProgress.type = current.type;
     workInProgress.stateNode = current.stateNode;
 
     {
       // DEV-only fields
       workInProgress._debugID = current._debugID;
       workInProgress._debugSource = current._debugSource;
       workInProgress._debugOwner = current._debugOwner;
+      workInProgress._debugHookTypes = current._debugHookTypes;
     }
 
     workInProgress.alternate = current;
     current.alternate = workInProgress;
   } else {
     workInProgress.pendingProps = pendingProps;
 
     // We already have an alternate.
@@ -10452,16 +10479,17 @@ function assignFiberPropertiesInDEV(targ
     target.actualStartTime = source.actualStartTime;
     target.selfBaseDuration = source.selfBaseDuration;
     target.treeBaseDuration = source.treeBaseDuration;
   }
   target._debugID = source._debugID;
   target._debugSource = source._debugSource;
   target._debugOwner = source._debugOwner;
   target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming;
+  target._debugHookTypes = source._debugHookTypes;
   return target;
 }
 
 var ReactInternals$2 = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
 
 var _ReactInternals$Sched$1 = ReactInternals$2.SchedulerTracing;
 var __interactionsRef = _ReactInternals$Sched$1.__interactionsRef;
 var __subscriberRef = _ReactInternals$Sched$1.__subscriberRef;
@@ -11416,24 +11444,45 @@ function adoptClassInstance(workInProgre
   }
 }
 
 function constructClassInstance(workInProgress, ctor, props, renderExpirationTime) {
   var isLegacyContextConsumer = false;
   var unmaskedContext = emptyContextObject;
   var context = null;
   var contextType = ctor.contextType;
-  if (typeof contextType === 'object' && contextType !== null) {
-    {
-      if (contextType.$$typeof !== REACT_CONTEXT_TYPE && !didWarnAboutInvalidateContextType.has(ctor)) {
+
+  {
+    if ('contextType' in ctor) {
+      var isValid =
+      // Allow null for conditional declaration
+      contextType === null || contextType !== undefined && contextType.$$typeof === REACT_CONTEXT_TYPE && contextType._context === undefined; // Not a <Context.Consumer>
+
+      if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) {
         didWarnAboutInvalidateContextType.add(ctor);
-        warningWithoutStack$1(false, '%s defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + 'Did you accidentally pass the Context.Provider instead?', getComponentName(ctor) || 'Component');
-      }
-    }
-
+
+        var addendum = '';
+        if (contextType === undefined) {
+          addendum = ' However, it is set to undefined. ' + 'This can be caused by a typo or by mixing up named and default imports. ' + 'This can also happen due to a circular dependency, so ' + 'try moving the createContext() call to a separate file.';
+        } else if (typeof contextType !== 'object') {
+          addendum = ' However, it is set to a ' + typeof contextType + '.';
+        } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {
+          addendum = ' Did you accidentally pass the Context.Provider instead?';
+        } else if (contextType._context !== undefined) {
+          // <Context.Consumer>
+          addendum = ' Did you accidentally pass the Context.Consumer instead?';
+        } else {
+          addendum = ' However, it is set to an object with keys {' + Object.keys(contextType).join(', ') + '}.';
+        }
+        warningWithoutStack$1(false, '%s defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext().%s', getComponentName(ctor) || 'Component', addendum);
+      }
+    }
+  }
+
+  if (typeof contextType === 'object' && contextType !== null) {
     context = readContext(contextType);
   } else {
     unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
     var contextTypes = ctor.contextTypes;
     isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;
     context = isLegacyContextConsumer ? getMaskedContext(workInProgress, unmaskedContext) : emptyContextObject;
   }
 
@@ -12850,17 +12899,16 @@ var renderExpirationTime = NoWork;
 // The work-in-progress fiber. I've named it differently to distinguish it from
 // the work-in-progress hook.
 var currentlyRenderingFiber$1 = null;
 
 // Hooks are stored as a linked list on the fiber's memoizedState field. The
 // current hook list is the list that belongs to the current fiber. The
 // work-in-progress hook list is a new list that will be added to the
 // work-in-progress fiber.
-var firstCurrentHook = null;
 var currentHook = null;
 var nextCurrentHook = null;
 var firstWorkInProgressHook = null;
 var workInProgressHook = null;
 var nextWorkInProgressHook = null;
 
 var remainingExpirationTime = NoWork;
 var componentUpdateQueue = null;
@@ -12880,55 +12928,83 @@ var didScheduleRenderPhaseUpdate = false
 var renderPhaseUpdates = null;
 // Counter to prevent infinite loops.
 var numberOfReRenders = 0;
 var RE_RENDER_LIMIT = 25;
 
 // In DEV, this is the name of the currently executing primitive hook
 var currentHookNameInDev = null;
 
-function warnOnHookMismatchInDev() {
+// In DEV, this list ensures that hooks are called in the same order between renders.
+// The list stores the order of hooks used during the initial render (mount).
+// Subsequent renders (updates) reference this list.
+var hookTypesDev = null;
+var hookTypesUpdateIndexDev = -1;
+
+function mountHookTypesDev() {
+  {
+    var hookName = currentHookNameInDev;
+
+    if (hookTypesDev === null) {
+      hookTypesDev = [hookName];
+    } else {
+      hookTypesDev.push(hookName);
+    }
+  }
+}
+
+function updateHookTypesDev() {
+  {
+    var hookName = currentHookNameInDev;
+
+    if (hookTypesDev !== null) {
+      hookTypesUpdateIndexDev++;
+      if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
+        warnOnHookMismatchInDev(hookName);
+      }
+    }
+  }
+}
+
+function warnOnHookMismatchInDev(currentHookName) {
   {
     var componentName = getComponentName(currentlyRenderingFiber$1.type);
     if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
       didWarnAboutMismatchedHooksForComponent.add(componentName);
 
-      var secondColumnStart = 22;
-
-      var table = '';
-      var prevHook = firstCurrentHook;
-      var nextHook = firstWorkInProgressHook;
-      var n = 1;
-      while (prevHook !== null && nextHook !== null) {
-        var oldHookName = prevHook._debugType;
-        var newHookName = nextHook._debugType;
-
-        var row = n + '. ' + oldHookName;
-
-        // Extra space so second column lines up
-        // lol @ IE not supporting String#repeat
-        while (row.length < secondColumnStart) {
-          row += ' ';
-        }
-
-        row += newHookName + '\n';
-
-        table += row;
-        prevHook = prevHook.next;
-        nextHook = nextHook.next;
-        n++;
-      }
-
-      warning$1(false, 'React has detected a change in the order of Hooks called by %s. ' + 'This will lead to bugs and errors if not fixed. ' + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + '   Previous render    Next render\n' + '   -------------------------------\n' + '%s' + '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', componentName, table);
+      if (hookTypesDev !== null) {
+        var table = '';
+
+        var secondColumnStart = 30;
+
+        for (var i = 0; i <= hookTypesUpdateIndexDev; i++) {
+          var oldHookName = hookTypesDev[i];
+          var newHookName = i === hookTypesUpdateIndexDev ? currentHookName : oldHookName;
+
+          var row = i + 1 + '. ' + oldHookName;
+
+          // Extra space so second column lines up
+          // lol @ IE not supporting String#repeat
+          while (row.length < secondColumnStart) {
+            row += ' ';
+          }
+
+          row += newHookName + '\n';
+
+          table += row;
+        }
+
+        warning$1(false, 'React has detected a change in the order of Hooks called by %s. ' + 'This will lead to bugs and errors if not fixed. ' + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + '   Previous render            Next render\n' + '   ------------------------------------------------------\n' + '%s' + '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', componentName, table);
+      }
     }
   }
 }
 
 function throwInvalidHookError() {
-  invariant(false, 'Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)');
+  invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.');
 }
 
 function areHookInputsEqual(nextDeps, prevDeps) {
   if (prevDeps === null) {
     {
       warning$1(false, '%s received a final argument during this render, but not during ' + 'the previous render. Even though the final argument is optional, ' + 'its type cannot change between renders.', currentHookNameInDev);
     }
     return false;
@@ -12948,85 +13024,120 @@ function areHookInputsEqual(nextDeps, pr
     return false;
   }
   return true;
 }
 
 function renderWithHooks(current, workInProgress, Component, props, refOrContext, nextRenderExpirationTime) {
   renderExpirationTime = nextRenderExpirationTime;
   currentlyRenderingFiber$1 = workInProgress;
-  firstCurrentHook = nextCurrentHook = current !== null ? current.memoizedState : null;
+  nextCurrentHook = current !== null ? current.memoizedState : null;
+
+  {
+    hookTypesDev = current !== null ? current._debugHookTypes : null;
+    hookTypesUpdateIndexDev = -1;
+  }
 
   // The following should have already been reset
   // currentHook = null;
   // workInProgressHook = null;
 
   // remainingExpirationTime = NoWork;
   // componentUpdateQueue = null;
 
   // didScheduleRenderPhaseUpdate = false;
   // renderPhaseUpdates = null;
   // numberOfReRenders = 0;
   // sideEffectTag = 0;
 
-  {
-    ReactCurrentDispatcher$1.current = nextCurrentHook === null ? HooksDispatcherOnMountInDEV : HooksDispatcherOnUpdateInDEV;
+  // TODO Warn if no hooks are used at all during mount, then some are used during update.
+  // Currently we will identify the update render as a mount because nextCurrentHook === null.
+  // This is tricky because it's valid for certain types of components (e.g. React.lazy)
+
+  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
+  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
+  // so nextCurrentHook would be null during updates and mounts.
+  {
+    if (nextCurrentHook !== null) {
+      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
+    } else if (hookTypesDev !== null) {
+      // This dispatcher handles an edge case where a component is updating,
+      // but no stateful hooks have been used.
+      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
+      // but with the extra DEV validation to ensure hooks ordering hasn't changed.
+      // This dispatcher does that.
+      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
+    } else {
+      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
+    }
   }
 
   var children = Component(props, refOrContext);
 
   if (didScheduleRenderPhaseUpdate) {
     do {
       didScheduleRenderPhaseUpdate = false;
       numberOfReRenders += 1;
 
       // Start over from the beginning of the list
-      firstCurrentHook = nextCurrentHook = current !== null ? current.memoizedState : null;
+      nextCurrentHook = current !== null ? current.memoizedState : null;
       nextWorkInProgressHook = firstWorkInProgressHook;
 
       currentHook = null;
       workInProgressHook = null;
       componentUpdateQueue = null;
 
+      {
+        // Also validate hook order for cascading updates.
+        hookTypesUpdateIndexDev = -1;
+      }
+
       ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
 
       children = Component(props, refOrContext);
     } while (didScheduleRenderPhaseUpdate);
 
     renderPhaseUpdates = null;
     numberOfReRenders = 0;
   }
 
-  {
-    currentHookNameInDev = null;
-  }
-
   // We can assume the previous dispatcher is always this one, since we set it
   // at the beginning of the render ph