summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/aboutwelcome
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx188
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss673
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/FxCards.jsx83
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/HeroText.jsx19
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx50
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx445
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx100
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/SimpleAboutWelcome.jsx35
-rw-r--r--browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx60
9 files changed, 1653 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx
new file mode 100644
index 0000000000..d9503584b9
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.jsx
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import ReactDOM from "react-dom";
+import { MultiStageAboutWelcome } from "./components/MultiStageAboutWelcome";
+import { SimpleAboutWelcome } from "./components/SimpleAboutWelcome";
+import { ReturnToAMO } from "./components/ReturnToAMO";
+
+import {
+ AboutWelcomeUtils,
+ DEFAULT_WELCOME_CONTENT,
+} from "../lib/aboutwelcome-utils";
+
+class AboutWelcome extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = { metricsFlowUri: null };
+ this.fetchFxAFlowUri = this.fetchFxAFlowUri.bind(this);
+ this.handleStartBtnClick = this.handleStartBtnClick.bind(this);
+ }
+
+ async fetchFxAFlowUri() {
+ this.setState({ metricsFlowUri: await window.AWGetFxAMetricsFlowURI() });
+ }
+
+ componentDidMount() {
+ this.fetchFxAFlowUri();
+
+ // Record impression with performance data after allowing the page to load
+ const recordImpression = domState => {
+ const { domComplete, domInteractive } = performance
+ .getEntriesByType("navigation")
+ .pop();
+ window.AWSendEventTelemetry({
+ event: "IMPRESSION",
+ event_context: {
+ domComplete,
+ domInteractive,
+ mountStart: performance.getEntriesByName("mount").pop().startTime,
+ domState,
+ source: this.props.UTMTerm,
+ page: "about:welcome",
+ },
+ message_id: this.props.messageId,
+ });
+ };
+ if (document.readyState === "complete") {
+ // Page might have already triggered a load event because it waited for async data,
+ // e.g., attribution, so the dom load timing could be of a empty content
+ // with domState in telemetry captured as 'complete'
+ recordImpression(document.readyState);
+ } else {
+ window.addEventListener("load", () => recordImpression("load"), {
+ once: true,
+ });
+ }
+
+ // Captures user has seen about:welcome by setting
+ // firstrun.didSeeAboutWelcome pref to true and capturing welcome UI unique messageId
+ window.AWSendToParent("SET_WELCOME_MESSAGE_SEEN", this.props.messageId);
+ }
+
+ handleStartBtnClick() {
+ AboutWelcomeUtils.handleUserAction(this.props.startButton.action);
+ const ping = {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: this.props.startButton.message_id,
+ page: "about:welcome",
+ },
+ message_id: this.props.messageId,
+ id: "ABOUT_WELCOME",
+ };
+ window.AWSendEventTelemetry(ping);
+ }
+
+ render() {
+ const { props } = this;
+ if (props.template === "simplified") {
+ return (
+ <SimpleAboutWelcome
+ metricsFlowUri={this.state.metricsFlowUri}
+ message_id={props.messageId}
+ utm_term={props.UTMTerm}
+ title={props.title}
+ subtitle={props.subtitle}
+ cards={props.cards}
+ startButton={props.startButton}
+ handleStartBtnClick={this.handleStartBtnClick}
+ />
+ );
+ } else if (props.template === "return_to_amo") {
+ return (
+ <ReturnToAMO
+ message_id={props.messageId}
+ name={props.name}
+ url={props.url}
+ iconURL={props.iconURL}
+ />
+ );
+ }
+
+ return (
+ <MultiStageAboutWelcome
+ screens={props.screens}
+ metricsFlowUri={this.state.metricsFlowUri}
+ message_id={props.messageId}
+ utm_term={props.UTMTerm}
+ />
+ );
+ }
+}
+
+AboutWelcome.defaultProps = DEFAULT_WELCOME_CONTENT;
+
+// Computes messageId and UTMTerm info used in telemetry
+function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) {
+ let messageId =
+ welcomeContent.template === "return_to_amo"
+ ? "RTAMO_DEFAULT_WELCOME"
+ : "DEFAULT_ABOUTWELCOME";
+ let UTMTerm = "default";
+
+ if (welcomeContent.id) {
+ messageId = welcomeContent.id.toUpperCase();
+ }
+
+ if (experimentId && branchId) {
+ UTMTerm = `${experimentId}-${branchId}`.toLowerCase();
+ }
+ return {
+ messageId,
+ UTMTerm,
+ };
+}
+
+async function retrieveRenderContent() {
+ // Check for override content in pref browser.aboutwelcome.overrideContent
+ let aboutWelcomeProps = await window.AWGetWelcomeOverrideContent();
+ if (aboutWelcomeProps?.template) {
+ let { messageId, UTMTerm } = ComputeTelemetryInfo(aboutWelcomeProps);
+ return { aboutWelcomeProps, messageId, UTMTerm };
+ }
+
+ // Check for experiment and retrieve content
+ const { slug, branch } = await window.AWGetExperimentData();
+ aboutWelcomeProps = branch?.feature ? branch.feature.value : {};
+
+ // Check if there is any attribution data, this could take a while to await in series
+ // especially when there is an add-on that requires remote lookup
+ // Moving RTAMO as part of another screen of multistage is one option to fix the delay
+ // as it will allow the initial page to be fast while we fetch attribution data in parallel for a later screen.
+ const attribution = await window.AWGetAttributionData();
+ if (attribution?.template) {
+ aboutWelcomeProps = {
+ ...aboutWelcomeProps,
+ // If part of an experiment, render experiment template
+ template: aboutWelcomeProps?.template
+ ? aboutWelcomeProps.template
+ : attribution.template,
+ ...attribution.extraProps,
+ };
+ }
+
+ let { messageId, UTMTerm } = ComputeTelemetryInfo(
+ aboutWelcomeProps,
+ slug,
+ branch && branch.slug
+ );
+ return { aboutWelcomeProps, messageId, UTMTerm };
+}
+
+async function mount() {
+ let { aboutWelcomeProps, messageId, UTMTerm } = await retrieveRenderContent();
+ ReactDOM.render(
+ <AboutWelcome
+ messageId={messageId}
+ UTMTerm={UTMTerm}
+ {...aboutWelcomeProps}
+ />,
+ document.getElementById("root")
+ );
+}
+
+performance.mark("mount");
+mount();
diff --git a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
new file mode 100644
index 0000000000..501b57952f
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss
@@ -0,0 +1,673 @@
+// sass-lint:disable no-css-comments
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+@import '../styles/normalize';
+@import '../styles/OnboardingImages';
+
+$break-point-medium: 610px;
+$break-point-large: 866px;
+$break-point-widest: 1122px;
+$logo-size: 112px;
+
+html {
+ height: 100%;
+}
+
+body {
+ // sass-lint:disable no-color-literals
+ --grey-subtitle: #4A4A4F;
+ --grey-subtitle-1: #696977;
+ --newtab-background-color: #EDEDF0;
+ --newtab-background-color-1: #F9F9FA;
+ --newtab-text-primary-color: #0C0C0D;
+ --newtab-text-conditional-color: #4A4A4F;
+ --newtab-button-primary-color: #0060DF;
+ --newtab-button-secondary-color: #0060DF;
+ --newtab-card-background-color: #FFF;
+ --newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.4);
+ --tiles-theme-section-border-width: 1px;
+ --welcome-header-text-color: #2B2156;
+ --welcome-header-text-color-1: #20133A;
+ --welcome-card-button-background-color: rgba(12, 12, 13, 0.1);
+ --welcome-card-button-background-hover-color: rgba(12, 12, 13, 0.2);
+ --welcome-card-button-background-active-color: rgba(12, 12, 13, 0.3);
+ --welcome-button-box-shadow-color: #0A84FF;
+ --welcome-button-box-shadow-inset-color: rgba(10, 132, 255, 0.3);
+ --welcome-button-text-color: #FFF;
+ --welcome-button-background-hover-color: #003EAA;
+ --welcome-button-background-active-color: #002275;
+ --about-welcome-media-fade: linear-gradient(transparent, transparent 35%, #F9F9FA, #F9F9FA);
+
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu',
+ 'Helvetica Neue', sans-serif;
+ font-size: 16px;
+ position: relative;
+ /* these two rules fix test failures in
+ "browser_ext_themes_ntp_colors" & "browser_ext_themes_ntp_colors_perwindow".*/
+ color: var(--newtab-text-primary-color);
+ background-color: var(--newtab-background-color);
+
+ &[lwt-newtab-brighttext] {
+ --newtab-background-color: #2A2A2E;
+ --newtab-background-color-1: #1D1133;
+ --newtab-text-primary-color: #F9F9FA;
+ --newtab-text-conditional-color: #F9F9FA;
+ --grey-subtitle-1: #FFF;
+ --newtab-button-primary-color: #0060DF;
+ --newtab-button-secondary-color: #FFF;
+ --newtab-card-background-color: #38383D;
+ --newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.4);
+ --welcome-header-text-color: rgba(255, 255, 255, 0.6);
+ --welcome-header-text-color-1: #7542E5;
+ --welcome-card-button-background-color: rgba(12, 12, 13, 0.3);
+ --welcome-card-button-background-hover-color: rgba(12, 12, 13, 0.5);
+ --welcome-card-button-background-active-color: rgba(12, 12, 13, 0.7);
+ --welcome-button-box-shadow-color: #0A84FF;
+ --about-welcome-media-fade: linear-gradient(transparent, transparent 35%, #1D1133, #1D1133);
+ }
+}
+
+.welcomeCardGrid {
+ margin: 0;
+ margin-top: 32px;
+ display: grid;
+ grid-gap: 32px;
+ transition: opacity 0.4s;
+ transition-delay: 0.1s;
+ grid-auto-rows: 1fr;
+
+ @media (min-width: $break-point-medium) {
+ grid-template-columns: repeat(auto-fit, 224px);
+ }
+
+ @media (min-width: $break-point-widest) {
+ grid-template-columns: repeat(auto-fit, 309px);
+ }
+}
+
+.welcomeContainer {
+ text-align: center;
+
+ @media (min-width: $break-point-medium) {
+ max-height: 1000px;
+ }
+
+ h1 {
+ font-size: 36px;
+ font-weight: 200;
+ margin: 0 0 40px;
+ color: var(--welcome-header-text-color);
+ }
+
+ .welcome-title {
+ margin-bottom: 5px;
+ line-height: 52px;
+ }
+
+ .welcome-subtitle {
+ font-size: 28px;
+ font-weight: 200;
+ margin: 6px 0 0;
+ color: var(--grey-subtitle);
+ line-height: 42px;
+ }
+}
+
+.welcomeContainerInner {
+ margin: auto;
+ padding: 40px 25px;
+
+ @media (min-width: $break-point-medium) {
+ width: 530px;
+ }
+
+ @media (min-width: $break-point-large) {
+ width: 786px;
+ }
+
+ @media (min-width: $break-point-widest) {
+ width: 1042px;
+ }
+}
+
+.welcomeCard {
+ position: relative;
+ background: var(--newtab-card-background-color);
+ border-radius: 4px;
+ box-shadow: var(--newtab-card-shadow);
+ font-size: 13px;
+ padding: 20px 20px 60px;
+
+ @media (max-width: $break-point-large) {
+ padding: 20px;
+ }
+
+ @media (min-width: $break-point-widest) {
+ font-size: 15px;
+ }
+}
+
+.welcomeCard .onboardingTitle {
+ font-weight: normal;
+ color: var(--newtab-text-primary-color);
+ margin: 10px 0 4px;
+ font-size: 15px;
+
+ @media (min-width: $break-point-widest) {
+ font-size: 18px;
+ }
+}
+
+.welcomeCard .onboardingText {
+ margin: 0 0 60px;
+ color: var(--newtab-text-conditional-color);
+ line-height: 1.5;
+ font-weight: 200;
+}
+
+.welcomeCard .onboardingButton {
+ color: var(--newtab-text-conditional-color);
+ background: var(--welcome-card-button-background-color);
+ border: 0;
+ border-radius: 4px;
+ margin: 14px;
+ min-width: 70%;
+ padding: 6px 14px;
+ white-space: pre-wrap;
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ box-shadow: none;
+ background: var(--welcome-card-button-background-hover-color);
+ }
+
+ &:focus {
+ outline: dotted 1px;
+ }
+
+ &:active {
+ background: var(--welcome-card-button-background-active-color);
+ }
+}
+
+.welcomeCard .onboardingButtonContainer {
+ position: absolute;
+ bottom: 16px;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+
+.onboardingMessageImage {
+ height: 112px;
+ width: 180px;
+ background-size: auto 140px;
+ background-position: center center;
+ background-repeat: no-repeat;
+ display: inline-block;
+
+ @media (max-width: $break-point-large) {
+ height: 75px;
+ min-width: 80px;
+ background-size: 140px;
+ }
+}
+
+.start-button {
+ border: 0;
+ font-size: 15px;
+ font-family: inherit;
+ font-weight: 200;
+ margin-inline-start: 12px;
+ margin: 30px 0 25px;
+ padding: 8px 16px;
+ white-space: nowrap;
+ background-color: var(--newtab-button-primary-color);
+ color: var(--welcome-button-text-color);
+ cursor: pointer;
+ border-radius: 2px;
+
+ &:focus {
+ background: var(--welcome-button-background-hover-color);
+ box-shadow: 0 0 0 1px var(--welcome-button-box-shadow-inset-color) inset,
+ 0 0 0 1px var(--welcome-button-box-shadow-inset-color),
+ 0 0 0 4px var(--welcome-button-box-shadow-color);
+ }
+
+ &:hover {
+ background: var(--welcome-button-background-hover-color);
+ }
+
+ &:active {
+ background: var(--welcome-button-background-active-color);
+ }
+}
+
+
+.onboardingContainer {
+ text-align: center;
+ overflow-x: auto;
+ height: 100vh;
+ background-color: var(--newtab-background-color-1);
+
+ .screen {
+ display: flex;
+ flex-flow: column nowrap;
+ height: 100%;
+ }
+
+ .brand-logo {
+ background: url('chrome://branding/content/about-logo.svg') top
+ center / $logo-size no-repeat;
+ padding: $logo-size 0 20px;
+ margin-top: 60px;
+
+ &.cta-top {
+ margin-top: 25px;
+ }
+ }
+
+ .welcomeZap {
+ span {
+ position: relative;
+ z-index: 1;
+ white-space: nowrap;
+ }
+
+ .zap {
+ &::after {
+ display: block;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ content: '';
+ position: absolute;
+ top: calc(100% - 0.15em);
+ width: 100%;
+ height: 0.3em;
+ left: 0;
+ z-index: -1;
+ }
+
+ &.short::after {
+ background-image: url('chrome://activity-stream/content/data/content/assets/short-zap.svg');
+ }
+
+ &.long::after {
+ background-image: url('chrome://activity-stream/content/data/content/assets/long-zap.svg');
+ }
+ }
+
+
+ }
+
+ .welcome-text {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 20px;
+
+ h1,
+ h2 {
+ width: 860px;
+ @media (max-width: $break-point-large) {
+ width: 530px;
+ }
+
+ @media (max-width: $break-point-medium) {
+ width: 430px;
+ }
+ }
+
+ h1 {
+ font-size: 48px;
+ line-height: 56px;
+ font-weight: bold;
+ margin: 0 6px;
+ color: var(--welcome-header-text-color-1);
+ }
+
+ h2 {
+ font-size: 18px;
+ font-weight: normal;
+ margin: 10px 6px 0;
+ color: var(--grey-subtitle-1);
+ line-height: 28px;
+ max-width: 750px;
+ letter-spacing: -0.01em;
+ }
+
+ img {
+ margin-inline: 2px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ .tiles-theme-container {
+ margin: 10px auto;
+ border: 0;
+ }
+
+ .sr-only {
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+
+ &.input {
+ height: 1px;
+ width: 1px;
+ }
+ }
+
+ .tiles-theme-section {
+ display: grid;
+ grid-gap: 21px;
+ grid-template-columns: repeat(4, auto);
+
+ /* --newtab-background-color-1 will be invisible, but it's necessary to
+ * keep the content from jumping around when it gets focus-within and
+ * does sprout a dotted border. This way it keeps a 1 pixel wide border
+ * either way so things don't change position.
+ */
+ border: var(--tiles-theme-section-border-width)
+ solid
+ var(--newtab-background-color-1);
+
+ @media (max-width: $break-point-medium) {
+ grid-template-columns: repeat(2, auto);
+ }
+
+ &:focus-within {
+ border: var(--tiles-theme-section-border-width) dotted;
+ }
+
+ .theme {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ width: 180px;
+ height: 145px;
+ color: #000;
+ background-color: #FFF;
+ box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.4);
+ border-radius: 4px;
+ cursor: pointer;
+
+ .icon {
+ background-size: cover;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ height: 91px;
+
+ &:dir(rtl) {
+ transform: scaleX(-1);
+ }
+
+ &.light {
+ background-image: url('chrome://mozapps/content/extensions/firefox-compact-light.svg');
+ }
+
+ &.dark {
+ background-image: url('chrome://mozapps/content/extensions/firefox-compact-dark.svg');
+ }
+
+ &.automatic {
+ background-image: url('chrome://mozapps/content/extensions/default-theme.svg');
+ }
+
+ &.alpenglow {
+ background-image: url('chrome://mozapps/content/extensions/firefox-alpenglow.svg');
+ }
+ }
+
+ .text {
+ display: flex;
+ font-size: 14px;
+ font-weight: bold;
+ line-height: 22px;
+ margin-inline-start: 12px;
+ margin-top: 9px;
+ }
+
+ &.selected {
+ outline: 4px solid #0090ED;
+ outline-offset: -4px;
+ }
+
+ &:focus,
+ &:active {
+ outline: 4px solid #0090ED;
+ outline-offset: -4px;
+ }
+ }
+ }
+
+ .tiles-container {
+ margin: 10px auto;
+
+ &.info {
+ padding: 6px 12px 12px;
+
+ &:hover,
+ &:focus {
+ background-color: rgba(217, 217, 227, 0.3);
+ border-radius: 4px;
+ }
+ }
+ }
+
+ .tiles-topsites-section {
+ $host-size: 12px;
+ $tile-size: 96px;
+
+ display: grid;
+ grid-gap: $tile-size / 4;
+ grid-template-columns: repeat(5, auto);
+
+ @media (max-width: $break-point-medium) {
+ grid-template-columns: repeat(3, auto);
+ }
+
+ .site {
+ width: $tile-size;
+ }
+
+ .icon {
+ background-size: cover;
+ border-radius: 4px;
+ box-shadow: var(--newtab-card-shadow);
+ color: rgba(255, 255, 255, 0.5);
+ font-size: $host-size * 2;
+ font-weight: bold;
+ height: $tile-size;
+ line-height: $tile-size;
+ }
+
+ .host {
+ font-size: $host-size;
+ line-height: $host-size * 3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .site:nth-child(1) .icon {
+ background-color: #7542E5;
+ }
+
+ .site:nth-child(2) .icon {
+ background-color: #952BB9;
+ }
+
+ .site:nth-child(3) .icon {
+ background-color: #E31587;
+ }
+
+ .site:nth-child(4) .icon {
+ background-color: #E25920;
+ }
+
+ .site:nth-child(5) .icon {
+ background-color: #0250BB;
+ }
+ }
+
+ // "tiles-media-section" styles here will support tiles of type
+ // "video" when the screen JSON it set in the below format:
+
+ // "tiles": {
+ // "type": "video",
+ // "source": {
+ // "default" : "<media-file-uri>",
+ // "dark" : "<media-file-dark-mode-uri>"
+ // }
+ // }
+
+ .tiles-media-section {
+ align-self: center;
+ position: relative;
+ margin-top: -12px;
+ margin-bottom: -155px;
+
+ .fade {
+ height: 390px;
+ width: 800px;
+ position: absolute;
+ background-image: var(--about-welcome-media-fade);
+ }
+
+ .media {
+ height: 390px;
+ width: 800px;
+ }
+
+ &.privacy {
+ background: top no-repeat url('chrome://activity-stream/content/data/content/assets/firefox-protections.svg');
+ height: 200px;
+ width: 800px;
+ margin: 0;
+
+ &.media {
+ opacity: 0;
+ }
+ }
+ }
+
+ button {
+ font-family: inherit;
+ cursor: pointer;
+ border: 0;
+ border-radius: 4px;
+
+ &.primary {
+ font-size: 16px;
+ margin-inline-start: 12px;
+ margin: 20px 0 0;
+ padding: 12px 20px;
+ white-space: nowrap;
+ background-color: var(--newtab-button-primary-color);
+ color: var(--welcome-button-text-color);
+ fill: currentColor;
+ position: relative;
+ z-index: 1;
+ // This transparent border will show up in Windows High Contrast Mode to improve accessibility.
+ border: 1px solid transparent;
+
+ &:focus {
+ background: var(--welcome-button-background-hover-color);
+ box-shadow: 0 0 0 4px var(--welcome-button-box-shadow-color);
+ }
+
+ &:hover {
+ background: var(--welcome-button-background-hover-color);
+ }
+
+ &:active {
+ background: var(--welcome-button-background-active-color);
+ }
+ }
+
+ &.secondary {
+ background-color: initial;
+ text-decoration: underline;
+ display: block;
+ padding: 0;
+ width: auto;
+ color: var(--newtab-button-secondary-color);
+ margin-top: 14px;
+
+ &:hover,
+ &:active {
+ background-color: initial;
+ }
+ }
+ }
+
+ .secondary-cta {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ font-size: 14px;
+
+ &.top {
+ justify-content: end;
+ align-items: end;
+ padding-inline-end: 30px;
+ padding-top: 4px;
+
+ @media (max-width: $break-point-medium) {
+ justify-content: center;
+ }
+ }
+
+ span {
+ color: var(--grey-subtitle-1);
+ margin: 0 4px;
+ }
+ }
+
+ .helptext {
+ padding: 1em;
+ text-align: center;
+ color: var(--grey-subtitle-1);
+ font-size: 12px;
+ line-height: 18px;
+
+ &.default {
+ align-self: center;
+ max-width: 40%;
+ }
+ }
+
+ .steps {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ margin-top: auto;
+ padding: 32px 0 66px;
+ z-index: 1;
+
+ &.has-helptext {
+ padding-bottom: 0;
+ }
+
+ .indicator {
+ width: 60px;
+ height: 4px;
+ margin-inline-end: 4px;
+ margin-inline-start: 4px;
+ background: var(--grey-subtitle-1);
+ border-radius: 5px;
+ // This transparent border will show up in Windows High Contrast Mode to improve accessibility.
+ border: 1px solid transparent;
+ opacity: 0.25;
+
+ &.current {
+ opacity: 1;
+ }
+ }
+ }
+}
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/FxCards.jsx b/browser/components/newtab/content-src/aboutwelcome/components/FxCards.jsx
new file mode 100644
index 0000000000..4275c59c8f
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/FxCards.jsx
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { addUtmParams } from "../../asrouter/templates/FirstRun/addUtmParams";
+import { OnboardingCard } from "../../asrouter/templates/OnboardingMessage/OnboardingMessage";
+import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
+
+export class FxCards extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = { flowParams: null };
+
+ this.fetchFxAFlowParams = this.fetchFxAFlowParams.bind(this);
+ this.onCardAction = this.onCardAction.bind(this);
+ }
+
+ componentDidUpdate() {
+ this.fetchFxAFlowParams();
+ }
+
+ componentDidMount() {
+ this.fetchFxAFlowParams();
+ }
+
+ async fetchFxAFlowParams() {
+ if (this.state.flowParams || !this.props.metricsFlowUri) {
+ return;
+ }
+
+ const flowParams = await AboutWelcomeUtils.fetchFlowParams(
+ this.props.metricsFlowUri
+ );
+
+ this.setState({ flowParams });
+ }
+
+ onCardAction(action) {
+ let { type, data } = action;
+ let UTMTerm = `aboutwelcome-${this.props.utm_term}-card`;
+
+ if (action.type === "OPEN_URL") {
+ let url = new URL(action.data.args);
+ addUtmParams(url, UTMTerm);
+
+ if (action.addFlowParams && this.state.flowParams) {
+ url.searchParams.append("device_id", this.state.flowParams.deviceId);
+ url.searchParams.append("flow_id", this.state.flowParams.flowId);
+ url.searchParams.append(
+ "flow_begin_time",
+ this.state.flowParams.flowBeginTime
+ );
+ }
+
+ data = { ...data, args: url.toString() };
+ }
+
+ AboutWelcomeUtils.handleUserAction({ type, data });
+ }
+
+ render() {
+ const { props } = this;
+ return (
+ <React.Fragment>
+ <div className={`welcomeCardGrid show`}>
+ {props.cards.map(card => (
+ <OnboardingCard
+ key={card.id}
+ message={card}
+ className="welcomeCard"
+ sendUserActionTelemetry={props.sendTelemetry}
+ onAction={this.onCardAction}
+ UISurface="ABOUT_WELCOME"
+ {...card}
+ />
+ ))}
+ </div>
+ </React.Fragment>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/HeroText.jsx b/browser/components/newtab/content-src/aboutwelcome/components/HeroText.jsx
new file mode 100644
index 0000000000..bb3a296d54
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/HeroText.jsx
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { Localized } from "./MSLocalized";
+
+export const HeroText = props => {
+ return (
+ <React.Fragment>
+ <Localized text={props.title}>
+ <h1 className="welcome-title" />
+ </Localized>
+ <Localized text={props.subtitle}>
+ <h2 className="welcome-subtitle" />
+ </Localized>
+ </React.Fragment>
+ );
+};
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx
new file mode 100644
index 0000000000..694a294028
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/MSLocalized.jsx
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+const MS_STRING_PROP = "string_id";
+
+/**
+ * Based on the .text prop, localizes an inner element if a string_id
+ * is provided, OR renders plain text, OR hides it if nothing is provided.
+ *
+ * Examples:
+ *
+ * Localized text
+ * ftl:
+ * title = Welcome
+ * jsx:
+ * <Localized text={{string_id: "title"}}><h1 /></Localized>
+ * output:
+ * <h1 data-l10n-id="title">Welcome</h1>
+ *
+ * Unlocalized text
+ * jsx:
+ * <Localized text="Welcome"><h1 /></Localized>
+ * output:
+ * <h1>Welcome</h1>
+ */
+
+export const Localized = ({ text, children }) => {
+ if (!text) {
+ return null;
+ }
+
+ let props = children ? children.props : {};
+ let textNode;
+
+ if (typeof text === "object" && text[MS_STRING_PROP]) {
+ props = { ...props };
+ props["data-l10n-id"] = text[MS_STRING_PROP];
+ } else if (typeof text === "string") {
+ textNode = text;
+ }
+
+ if (!children) {
+ return React.createElement("span", props, textNode);
+ } else if (textNode) {
+ return React.cloneElement(children, props, textNode);
+ }
+ return React.cloneElement(children, props);
+};
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
new file mode 100644
index 0000000000..fde65d3673
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx
@@ -0,0 +1,445 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React, { useState, useEffect, useRef } from "react";
+import { Localized } from "./MSLocalized";
+import { Zap } from "./Zap";
+import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
+import {
+ BASE_PARAMS,
+ addUtmParams,
+} from "../../asrouter/templates/FirstRun/addUtmParams";
+
+export const MultiStageAboutWelcome = props => {
+ const [index, setScreenIndex] = useState(0);
+ useEffect(() => {
+ // Send impression ping when respective screen first renders
+ props.screens.forEach(screen => {
+ if (index === screen.order) {
+ AboutWelcomeUtils.sendImpressionTelemetry(
+ `${props.message_id}_${screen.id}`
+ );
+ }
+ });
+
+ // Remember that a new screen has loaded for browser navigation
+ if (index > window.history.state) {
+ window.history.pushState(index, "");
+ }
+ }, [index]);
+
+ useEffect(() => {
+ // Switch to the screen tracked in state (null for initial state)
+ const handler = ({ state }) => setScreenIndex(Number(state));
+
+ // Handle page load, e.g., going back to about:welcome from about:home
+ handler(window.history);
+
+ // Watch for browser back/forward button navigation events
+ window.addEventListener("popstate", handler);
+ return () => window.removeEventListener("popstate", handler);
+ }, []);
+
+ const [flowParams, setFlowParams] = useState(null);
+ const { metricsFlowUri } = props;
+ useEffect(() => {
+ (async () => {
+ if (metricsFlowUri) {
+ setFlowParams(await AboutWelcomeUtils.fetchFlowParams(metricsFlowUri));
+ }
+ })();
+ }, [metricsFlowUri]);
+
+ // Transition to next screen, opening about:home on last screen button CTA
+ const handleTransition =
+ index < props.screens.length - 1
+ ? () => setScreenIndex(prevState => prevState + 1)
+ : () =>
+ AboutWelcomeUtils.handleUserAction({
+ type: "OPEN_ABOUT_PAGE",
+ data: { args: "home", where: "current" },
+ });
+
+ // Update top sites with default sites by region when region is available
+ const [region, setRegion] = useState(null);
+ useEffect(() => {
+ (async () => {
+ setRegion(await window.AWGetRegion());
+ })();
+ }, []);
+
+ // Get the active theme so the rendering code can make it selected
+ // by default.
+ const [activeTheme, setActiveTheme] = useState(null);
+ const [initialTheme, setInitialTheme] = useState(null);
+ useEffect(() => {
+ (async () => {
+ let theme = await window.AWGetSelectedTheme();
+ setInitialTheme(theme);
+ setActiveTheme(theme);
+ })();
+ }, []);
+
+ const useImportable = props.message_id.includes("IMPORTABLE");
+ // Track whether we have already sent the importable sites impression telemetry
+ const importTelemetrySent = useRef(false);
+ const [topSites, setTopSites] = useState([]);
+ useEffect(() => {
+ (async () => {
+ let DEFAULT_SITES = await window.AWGetDefaultSites();
+ const importable = JSON.parse(await window.AWGetImportableSites());
+ const showImportable = useImportable && importable.length >= 5;
+ if (!importTelemetrySent.current) {
+ AboutWelcomeUtils.sendImpressionTelemetry(`${props.message_id}_SITES`, {
+ display: showImportable ? "importable" : "static",
+ importable: importable.length,
+ });
+ importTelemetrySent.current = true;
+ }
+ setTopSites(
+ showImportable
+ ? { data: importable, showImportable }
+ : { data: DEFAULT_SITES, showImportable }
+ );
+ })();
+ }, [useImportable, region]);
+
+ return (
+ <React.Fragment>
+ <div className={`outer-wrapper onboardingContainer`}>
+ {props.screens.map(screen => {
+ return index === screen.order ? (
+ <WelcomeScreen
+ key={screen.id}
+ id={screen.id}
+ totalNumberOfScreens={props.screens.length}
+ order={screen.order}
+ content={screen.content}
+ navigate={handleTransition}
+ topSites={topSites}
+ messageId={`${props.message_id}_${screen.id}`}
+ UTMTerm={props.utm_term}
+ flowParams={flowParams}
+ activeTheme={activeTheme}
+ initialTheme={initialTheme}
+ setActiveTheme={setActiveTheme}
+ />
+ ) : null;
+ })}
+ </div>
+ </React.Fragment>
+ );
+};
+
+export class WelcomeScreen extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleAction = this.handleAction.bind(this);
+ }
+
+ handleOpenURL(action, flowParams, UTMTerm) {
+ let { type, data } = action;
+ if (type === "SHOW_FIREFOX_ACCOUNTS") {
+ let params = {
+ ...BASE_PARAMS,
+ utm_term: `aboutwelcome-${UTMTerm}-screen`,
+ };
+ if (action.addFlowParams && flowParams) {
+ params = {
+ ...params,
+ ...flowParams,
+ };
+ }
+ data = { ...data, extraParams: params };
+ } else if (type === "OPEN_URL") {
+ let url = new URL(data.args);
+ addUtmParams(url, `aboutwelcome-${UTMTerm}-screen`);
+ if (action.addFlowParams && flowParams) {
+ url.searchParams.append("device_id", flowParams.deviceId);
+ url.searchParams.append("flow_id", flowParams.flowId);
+ url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
+ }
+ data = { ...data, args: url.toString() };
+ }
+ AboutWelcomeUtils.handleUserAction({ type, data });
+ }
+
+ async handleAction(event) {
+ let { props } = this;
+
+ let targetContent =
+ props.content[event.currentTarget.value] || props.content.tiles;
+ if (!(targetContent && targetContent.action)) {
+ return;
+ }
+
+ // Send telemetry before waiting on actions
+ AboutWelcomeUtils.sendActionTelemetry(
+ props.messageId,
+ event.currentTarget.value
+ );
+
+ let { action } = targetContent;
+
+ if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) {
+ this.handleOpenURL(action, props.flowParams, props.UTMTerm);
+ } else if (action.type) {
+ AboutWelcomeUtils.handleUserAction(action);
+ // Wait until migration closes to complete the action
+ if (action.type === "SHOW_MIGRATION_WIZARD") {
+ await window.AWWaitForMigrationClose();
+ AboutWelcomeUtils.sendActionTelemetry(props.messageId, "migrate_close");
+ }
+ }
+
+ // A special tiles.action.theme value indicates we should use the event's value vs provided value.
+ if (action.theme) {
+ let themeToUse =
+ action.theme === "<event>"
+ ? event.currentTarget.value
+ : this.props.initialTheme || action.theme;
+
+ this.props.setActiveTheme(themeToUse);
+ window.AWSelectTheme(themeToUse);
+ }
+
+ if (action.navigate) {
+ props.navigate();
+ }
+ }
+
+ renderSecondaryCTA(className) {
+ return (
+ <div
+ className={className ? `secondary-cta ${className}` : `secondary-cta`}
+ >
+ <Localized text={this.props.content.secondary_button.text}>
+ <span />
+ </Localized>
+ <Localized text={this.props.content.secondary_button.label}>
+ <button
+ className="secondary"
+ value="secondary_button"
+ onClick={this.handleAction}
+ />
+ </Localized>
+ </div>
+ );
+ }
+
+ renderTiles() {
+ switch (this.props.content.tiles.type) {
+ case "topsites":
+ return this.props.topSites && this.props.topSites.data ? (
+ <div
+ className={`tiles-container ${
+ this.props.content.tiles.info ? "info" : ""
+ }`}
+ >
+ <div
+ className="tiles-topsites-section"
+ name="topsites-section"
+ id="topsites-section"
+ aria-labelledby="helptext"
+ role="region"
+ >
+ {this.props.topSites.data
+ .slice(0, 5)
+ .map(({ icon, label, title }) => (
+ <div
+ className="site"
+ key={icon + label}
+ aria-label={title ? title : label}
+ role="img"
+ >
+ <div
+ className="icon"
+ style={
+ icon
+ ? {
+ backgroundColor: "transparent",
+ backgroundImage: `url(${icon})`,
+ }
+ : {}
+ }
+ >
+ {icon ? "" : label && label[0].toUpperCase()}
+ </div>
+ {this.props.content.tiles.showTitles && (
+ <div className="host">{title || label}</div>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ ) : null;
+ case "theme":
+ return this.props.content.tiles.data ? (
+ <div className="tiles-theme-container">
+ <div>
+ <fieldset className="tiles-theme-section">
+ <Localized text={this.props.content.subtitle}>
+ <legend className="sr-only" />
+ </Localized>
+ {this.props.content.tiles.data.map(
+ ({ theme, label, tooltip, description }) => (
+ <Localized
+ key={theme + label}
+ text={typeof tooltip === "object" ? tooltip : {}}
+ >
+ <label
+ className={`theme${
+ theme === this.props.activeTheme ? " selected" : ""
+ }`}
+ title={theme + label}
+ >
+ <Localized
+ text={
+ typeof description === "object" ? description : {}
+ }
+ >
+ <input
+ type="radio"
+ value={theme}
+ name="theme"
+ checked={theme === this.props.activeTheme}
+ className="sr-only input"
+ onClick={this.handleAction}
+ data-l10n-attrs="aria-description"
+ />
+ </Localized>
+ <div className={`icon ${theme}`} />
+ {label && (
+ <Localized text={label}>
+ <div className="text" />
+ </Localized>
+ )}
+ </label>
+ </Localized>
+ )
+ )}
+ </fieldset>
+ </div>
+ </div>
+ ) : null;
+ case "video":
+ return this.props.content.tiles.source ? (
+ <div
+ className={`tiles-media-section ${this.props.content.tiles.media_type}`}
+ >
+ <div className="fade" />
+ <video
+ className="media"
+ autoPlay="true"
+ loop="true"
+ muted="true"
+ src={
+ AboutWelcomeUtils.hasDarkMode()
+ ? this.props.content.tiles.source.dark
+ : this.props.content.tiles.source.default
+ }
+ />
+ </div>
+ ) : null;
+ case "image":
+ return this.props.content.tiles.source ? (
+ <div className={`${this.props.content.tiles.media_type}`}>
+ <img
+ src={
+ AboutWelcomeUtils.hasDarkMode() &&
+ this.props.content.tiles.source.dark
+ ? this.props.content.tiles.source.dark
+ : this.props.content.tiles.source.default
+ }
+ role="presentation"
+ alt=""
+ />
+ </div>
+ ) : null;
+ }
+ return null;
+ }
+
+ renderStepsIndicator() {
+ let steps = [];
+ for (let i = 0; i < this.props.totalNumberOfScreens; i++) {
+ let className = i === this.props.order ? "current" : "";
+ steps.push(<div key={i} className={`indicator ${className}`} />);
+ }
+ return steps;
+ }
+
+ renderHelpText() {
+ return (
+ <Localized text={this.props.content.help_text.text}>
+ <p
+ id="helptext"
+ className={`helptext ${this.props.content.help_text.position}`}
+ />
+ </Localized>
+ );
+ }
+
+ render() {
+ const { content, topSites } = this.props;
+ const hasSecondaryTopCTA =
+ content.secondary_button && content.secondary_button.position === "top";
+ const showImportableSitesDisclaimer =
+ content.tiles &&
+ content.tiles.type === "topsites" &&
+ topSites &&
+ topSites.showImportable;
+
+ return (
+ <main className={`screen ${this.props.id}`}>
+ {hasSecondaryTopCTA ? this.renderSecondaryCTA("top") : null}
+ <div className={`brand-logo ${hasSecondaryTopCTA ? "cta-top" : ""}`} />
+ <div className="welcome-text">
+ <Zap hasZap={content.zap} text={content.title} />
+ <Localized text={content.subtitle}>
+ <h2 />
+ </Localized>
+ </div>
+ {content.tiles ? this.renderTiles() : null}
+ <div>
+ <Localized
+ text={content.primary_button ? content.primary_button.label : null}
+ >
+ <button
+ className="primary"
+ value="primary_button"
+ onClick={this.handleAction}
+ />
+ </Localized>
+ </div>
+ {content.secondary_button && content.secondary_button.position !== "top"
+ ? this.renderSecondaryCTA()
+ : null}
+ {content.help_text && content.help_text.position === "default"
+ ? this.renderHelpText()
+ : null}
+ <nav
+ className={
+ (content.help_text && content.help_text.position === "footer") ||
+ showImportableSitesDisclaimer
+ ? "steps has-helptext"
+ : "steps"
+ }
+ data-l10n-id={"onboarding-welcome-steps-indicator"}
+ data-l10n-args={`{"current": ${parseInt(this.props.order, 10) +
+ 1}, "total": ${this.props.totalNumberOfScreens}}`}
+ >
+ {/* These empty elements are here to help trigger the nav for screen readers. */}
+ <br />
+ <p />
+ {this.renderStepsIndicator()}
+ </nav>
+ {(content.help_text && content.help_text.position === "footer") ||
+ showImportableSitesDisclaimer
+ ? this.renderHelpText()
+ : null}
+ </main>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx b/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx
new file mode 100644
index 0000000000..2d5501dc46
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/ReturnToAMO.jsx
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import {
+ AboutWelcomeUtils,
+ DEFAULT_RTAMO_CONTENT,
+} from "../../lib/aboutwelcome-utils";
+import { Localized } from "./MSLocalized";
+
+export class ReturnToAMO extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.onClickAddExtension = this.onClickAddExtension.bind(this);
+ this.handleStartBtnClick = this.handleStartBtnClick.bind(this);
+ }
+
+ onClickAddExtension() {
+ const { content, message_id, url } = this.props;
+ if (!content?.primary_button?.action?.data) {
+ return;
+ }
+
+ // Set add-on url in action.data.url property from JSON
+ content.primary_button.action.data.url = url;
+ AboutWelcomeUtils.handleUserAction(content.primary_button.action);
+ const ping = {
+ event: "INSTALL",
+ event_context: {
+ source: "ADD_EXTENSION_BUTTON",
+ page: "about:welcome",
+ },
+ message_id,
+ };
+ window.AWSendEventTelemetry(ping);
+ }
+
+ handleStartBtnClick() {
+ const { content, message_id } = this.props;
+ AboutWelcomeUtils.handleUserAction(content.startButton.action);
+ const ping = {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: content.startButton.message_id,
+ page: "about:welcome",
+ },
+ message_id,
+ };
+ window.AWSendEventTelemetry(ping);
+ }
+
+ render() {
+ const { content } = this.props;
+ if (!content) {
+ return null;
+ }
+ // For experiments, when needed below rendered UI allows settings hard coded strings
+ // directly inside JSON except for ReturnToAMOText which picks add-on name and icon from fluent string
+ return (
+ <div className="outer-wrapper onboardingContainer">
+ <main className="screen">
+ <div className="brand-logo" />
+ <div className="welcome-text">
+ <Localized text={content.subtitle}>
+ <h1 />
+ </Localized>
+ <Localized text={content.text}>
+ <h2
+ data-l10n-args={
+ this.props.name
+ ? JSON.stringify({ "addon-name": this.props.name })
+ : null
+ }
+ >
+ <img
+ data-l10n-name="icon"
+ src={this.props.iconURL}
+ role="presentation"
+ alt=""
+ />
+ </h2>
+ </Localized>
+ <Localized text={content.primary_button.label}>
+ <button onClick={this.onClickAddExtension} className="primary" />
+ </Localized>
+ <Localized text={content.startButton.label}>
+ <button
+ onClick={this.handleStartBtnClick}
+ className="secondary"
+ />
+ </Localized>
+ </div>
+ </main>
+ </div>
+ );
+ }
+}
+
+ReturnToAMO.defaultProps = DEFAULT_RTAMO_CONTENT;
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/SimpleAboutWelcome.jsx b/browser/components/newtab/content-src/aboutwelcome/components/SimpleAboutWelcome.jsx
new file mode 100644
index 0000000000..4186cb2d69
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/SimpleAboutWelcome.jsx
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { HeroText } from "./HeroText";
+import { FxCards } from "./FxCards";
+import { Localized } from "./MSLocalized";
+
+export class SimpleAboutWelcome extends React.PureComponent {
+ render() {
+ const { props } = this;
+ return (
+ <div className="outer-wrapper welcomeContainer">
+ <div className="welcomeContainerInner">
+ <main>
+ <HeroText title={props.title} subtitle={props.subtitle} />
+ <FxCards
+ cards={props.cards}
+ metricsFlowUri={this.props.metricsFlowUri}
+ sendTelemetry={window.AWSendEventTelemetry}
+ utm_term={this.props.UTMTerm}
+ />
+ <Localized text={props.startButton.label}>
+ <button
+ className="start-button"
+ onClick={this.props.handleStartBtnClick}
+ />
+ </Localized>
+ </main>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx b/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx
new file mode 100644
index 0000000000..a067c4d7fe
--- /dev/null
+++ b/browser/components/newtab/content-src/aboutwelcome/components/Zap.jsx
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React, { useEffect } from "react";
+import { Localized } from "./MSLocalized";
+const MS_STRING_PROP = "string_id";
+const ZAP_SIZE_THRESHOLD = 160;
+
+function calculateZapLength() {
+ let span = document.querySelector(".zap");
+ if (!span) {
+ return;
+ }
+ let rect = span.getBoundingClientRect();
+ if (rect && rect.width > ZAP_SIZE_THRESHOLD) {
+ span.classList.add("long");
+ } else {
+ span.classList.add("short");
+ }
+}
+
+export const Zap = props => {
+ useEffect(() => {
+ requestAnimationFrame(() => calculateZapLength());
+ });
+
+ if (!props.text) {
+ return null;
+ }
+
+ if (props.hasZap) {
+ if (typeof props.text === "object" && props.text[MS_STRING_PROP]) {
+ return (
+ <Localized text={props.text}>
+ <h1 className="welcomeZap">
+ <span data-l10n-name="zap" className="zap" />
+ </h1>
+ </Localized>
+ );
+ } else if (typeof props.text === "string") {
+ // Parse string to zap style last word of the props.text
+ let titleArray = props.text.split(" ");
+ let lastWord = `${titleArray.pop()}`;
+ return (
+ <h1 className="welcomeZap">
+ {titleArray.join(" ").concat(" ")}
+ <span className="zap">{lastWord}</span>
+ </h1>
+ );
+ }
+ } else {
+ return (
+ <Localized text={props.text}>
+ <h1 />
+ </Localized>
+ );
+ }
+ return null;
+};