Backed out changeset 08f94ba4c50c (bug 1659150) for bc failures on browser_all_files_referenced.js. CLOSED TREE

This commit is contained in:
Cosmin Sabou 2020-09-11 08:32:46 +03:00
Родитель 90d135b86d
Коммит 00b09a0063
67 изменённых файлов: 6430 добавлений и 826 удалений

Просмотреть файл

@ -1386,8 +1386,8 @@ pref("browser.newtabpage.activity-stream.feeds.section.topstories", true);
pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
#endif
// Used to display triplet cards on newtab
pref("trailhead.firstrun.newtab.triplets", "");
pref("trailhead.firstrun.branches", "join-dynamic");
// Separate about welcome
pref("browser.aboutwelcome.enabled", true);
// Used to set multistage welcome UX

Просмотреть файл

@ -567,6 +567,7 @@ var Policies = {
onBeforeAddons(manager, param) {
if (param) {
setAndLockPref("identity.fxaccounts.enabled", false);
setAndLockPref("trailhead.firstrun.branches", "nofirstrun-empty");
setAndLockPref("browser.aboutwelcome.enabled", false);
}
},
@ -1316,6 +1317,7 @@ var Policies = {
onProfileAfterChange(manager, param) {
let url = param ? param.href : "";
setAndLockPref("startup.homepage_welcome_url", url);
setAndLockPref("trailhead.firstrun.branches", "nofirstrun-empty");
setAndLockPref("browser.aboutwelcome.enabled", false);
},
},
@ -1983,6 +1985,7 @@ var Policies = {
manager.disallowFeature("urlbarinterventions");
}
if ("SkipOnboarding") {
setAndLockPref("trailhead.firstrun.branches", "nofirstrun-empty");
setAndLockPref("browser.aboutwelcome.enabled", false);
}
},

Просмотреть файл

@ -43,6 +43,9 @@ module.exports = {
"content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx",
"content-src/asrouter/templates/OnboardingMessage/**",
"content-src/asrouter/templates/FirstRun/**",
"content-src/asrouter/templates/Trailhead/**",
"content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt.jsx",
"content-src/asrouter/components/FxASignupForm/FxASignupForm.jsx",
"content-src/components/TopSites/**",
"content-src/components/MoreRecommendations/MoreRecommendations.jsx",
"content-src/components/CollapsibleSection/CollapsibleSection.jsx",

Просмотреть файл

@ -151,6 +151,7 @@ for (const type of [
"TOP_SITES_UPDATED",
"TOTAL_BOOKMARKS_REQUEST",
"TOTAL_BOOKMARKS_RESPONSE",
"TRAILHEAD_ENROLL_EVENT",
"UNINIT",
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",

Просмотреть файл

@ -11,9 +11,9 @@ import React from "react";
import ReactDOM from "react-dom";
import { reducers } from "common/Reducers.jsm";
export const NewTab = ({ store }) => (
export const NewTab = ({ store, isFirstrun }) => (
<Provider store={store}>
<Base />
<Base isFirstrun={isFirstrun} />
</Provider>
);
@ -43,12 +43,24 @@ export function renderWithoutState() {
doRequest();
}
ReactDOM.hydrate(<NewTab store={store} />, document.getElementById("root"));
ReactDOM.hydrate(
<NewTab
store={store}
isFirstrun={global.document.location.href === "about:welcome"}
/>,
document.getElementById("root")
);
}
export function renderCache(initialState) {
const store = initStore(reducers, initialState);
new DetectUserSessionStart(store).sendEventOrAddListener();
ReactDOM.hydrate(<NewTab store={store} />, document.getElementById("root"));
ReactDOM.hydrate(
<NewTab
store={store}
isFirstrun={global.document.location.href === "about:welcome"}
/>,
document.getElementById("root")
);
}

Просмотреть файл

@ -15,7 +15,13 @@ import { FirstRun } from "./templates/FirstRun/FirstRun";
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
const TEMPLATES_ABOVE_PAGE = ["extended_triplets"];
const TEMPLATES_ABOVE_PAGE = [
"trailhead",
"full_page_interrupt",
"return_to_amo_overlay",
"extended_triplets",
];
const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
export const ASRouterUtils = {
@ -121,7 +127,7 @@ export class ASRouterUISurface extends React.PureComponent {
this.onUserAction = this.onUserAction.bind(this);
this.fetchFlowParams = this.fetchFlowParams.bind(this);
this.state = { message: {} };
this.state = { message: {}, interruptCleared: false };
if (props.document) {
this.headerPortal = props.document.getElementById(
"header-asrouter-container"
@ -253,6 +259,8 @@ export class ASRouterUISurface extends React.PureComponent {
if (id === this.state.message.id) {
this.setState({ message: {} });
// Remove any styles related to the RTAMO message
document.body.classList.remove("welcome", "hide-main", "amo");
}
}
@ -261,6 +269,9 @@ export class ASRouterUISurface extends React.PureComponent {
case "SET_MESSAGE":
this.setState({ message: action.data });
break;
case "CLEAR_INTERRUPT":
this.setState({ interruptCleared: true });
break;
case "CLEAR_MESSAGE":
this.clearMessage(action.data.id);
break;
@ -279,10 +290,21 @@ export class ASRouterUISurface extends React.PureComponent {
}
requestMessage(endpoint) {
ASRouterUtils.sendMessage({
type: "NEWTAB_MESSAGE_REQUEST",
data: { endpoint },
});
// If we are loading about:welcome we want to trigger the onboarding messages
if (
this.props.document &&
this.props.document.location.href === "about:welcome"
) {
ASRouterUtils.sendMessage({
type: "TRIGGER",
data: { trigger: { id: "firstRun" } },
});
} else {
ASRouterUtils.sendMessage({
type: "NEWTAB_MESSAGE_REQUEST",
data: { endpoint },
});
}
}
componentWillMount() {
@ -379,7 +401,7 @@ export class ASRouterUISurface extends React.PureComponent {
renderFirstRun() {
const { message } = this.state;
if (TEMPLATES_ABOVE_PAGE.includes(message.template)) {
if (FIRST_RUN_TEMPLATES.includes(message.template)) {
return (
<ImpressionsWrapper
id="FIRST_RUN"
@ -391,9 +413,11 @@ export class ASRouterUISurface extends React.PureComponent {
>
<FirstRun
document={this.props.document}
interruptCleared={this.state.interruptCleared}
message={message}
sendUserActionTelemetry={this.sendUserActionTelemetry}
executeAction={ASRouterUtils.executeAction}
dispatch={this.props.dispatch}
onBlockById={ASRouterUtils.blockById}
onDismiss={this.onDismissById(this.state.message.id)}
fxaEndpoint={this.props.fxaEndpoint}

Просмотреть файл

@ -0,0 +1,173 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { actionCreators as ac } from "common/Actions.jsm";
import {
addUtmParams,
BASE_PARAMS,
} from "../../templates/FirstRun/addUtmParams";
import React from "react";
export class FxASignupForm extends React.PureComponent {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.handleSignIn = this.handleSignIn.bind(this);
this.state = {
emailInput: "",
};
}
get email() {
return this.props.document
.getElementById("fxaSignupForm")
.querySelector("input[name=email]");
}
onSubmit(event) {
let userEvent = "SUBMIT_EMAIL";
const { email } = event.target.elements;
if (email.disabled) {
userEvent = "SUBMIT_SIGNIN";
} else if (!email.value.length) {
email.required = true;
email.checkValidity();
event.preventDefault();
return;
}
// Report to telemetry additional information about the form submission.
const value = { has_flow_params: !!this.props.flowParams.flowId.length };
this.props.dispatch(ac.UserEvent({ event: userEvent, value }));
global.addEventListener("visibilitychange", this.props.onClose);
}
handleSignIn(event) {
// Set disabled to prevent email from appearing in url resulting in the wrong page
this.email.disabled = true;
}
componentDidMount() {
// Start with focus in the email input box
if (this.email) {
this.email.focus();
}
}
onInputChange(e) {
let error = e.target.previousSibling;
this.setState({ emailInput: e.target.value });
error.classList.remove("active");
e.target.classList.remove("invalid");
}
onInputInvalid(e) {
let error = e.target.previousSibling;
error.classList.add("active");
e.target.classList.add("invalid");
e.preventDefault(); // Override built-in form validation popup
e.target.focus();
}
render() {
const { content, UTMTerm } = this.props;
return (
<div
id="fxaSignupForm"
role="group"
aria-labelledby="joinFormHeader"
aria-describedby="joinFormBody"
className="fxaSignupForm"
>
<h3 id="joinFormHeader" data-l10n-id={content.form.title.string_id} />
<p id="joinFormBody" data-l10n-id={content.form.text.string_id} />
<form
method="get"
action={this.props.fxaEndpoint}
target="_blank"
rel="noopener noreferrer"
onSubmit={this.onSubmit}
>
<input name="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={BASE_PARAMS.utm_campaign}
/>
<input name="utm_term" type="hidden" value={UTMTerm} />
<input
name="device_id"
type="hidden"
value={this.props.flowParams.deviceId}
/>
<input
name="flow_id"
type="hidden"
value={this.props.flowParams.flowId}
/>
<input
name="flow_begin_time"
type="hidden"
value={this.props.flowParams.flowBeginTime}
/>
<input name="style" type="hidden" value="trailhead" />
<p
data-l10n-id="onboarding-join-form-email-error"
className="error"
/>
<input
data-l10n-id={content.form.email.string_id}
name="email"
type="email"
onInvalid={this.onInputInvalid}
onChange={this.onInputChange}
/>
<p className="fxa-terms" data-l10n-id="onboarding-join-form-legal">
<a
data-l10n-name="terms"
target="_blank"
rel="noopener noreferrer"
href={addUtmParams(
"https://accounts.firefox.com/legal/terms",
UTMTerm
)}
/>
<a
data-l10n-name="privacy"
target="_blank"
rel="noopener noreferrer"
href={addUtmParams(
"https://accounts.firefox.com/legal/privacy",
UTMTerm
)}
/>
</p>
<button data-l10n-id={content.form.button.string_id} type="submit" />
{this.props.showSignInLink && (
<div className="fxa-signin">
<span data-l10n-id="onboarding-join-form-signin-label" />
<button
data-l10n-id="onboarding-join-form-signin"
onClick={this.handleSignIn}
/>
</div>
)}
</form>
</div>
);
}
}
FxASignupForm.defaultProps = { document: global.document };

Просмотреть файл

@ -0,0 +1,126 @@
.fxaSignupForm {
min-width: 260px;
text-align: center;
a {
color: $white;
text-decoration: underline;
}
input,
button {
border-radius: 4px;
padding: 10px;
}
h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px;
}
p {
font-size: 15px;
line-height: 22px;
margin: 0 0 20px;
}
.fxa-terms {
margin: 4px 30px 20px;
a,
& {
color: $white-70;
font-size: 12px;
line-height: 20px;
}
}
.fxa-signin {
font-size: 16px;
margin-top: 19px;
span {
margin-inline-end: 5px;
}
button {
background-color: initial;
text-decoration: underline;
color: $white;
display: inline;
padding: 0;
width: auto;
&:hover,
&:focus,
&:active {
background-color: initial;
}
}
}
form {
position: relative;
.error.active {
inset-inline-start: 0;
z-index: 0;
}
}
button,
input {
width: 100%;
}
input {
background-color: $white;
border: 1px solid $grey-50;
box-shadow: none;
color: $grey-70;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms;
&:hover {
border-color: $grey-90;
}
&:focus {
border-color: $blue-50;
box-shadow: 0 0 0 3px $email-input-focus;
}
&.invalid {
border-color: $red-60;
}
&.invalid:focus {
box-shadow: 0 0 0 3px $email-input-invalid;
}
}
button {
background-color: $blue-60;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px;
&:hover,
&:focus {
background-color: $trailhead-blue-60;
}
&:focus {
outline: dotted 1px;
}
&:active {
background-color: $trailhead-blue-70;
}
}
}

Просмотреть файл

@ -1,15 +1,40 @@
# First run on-boarding flow
First Run flow describes the entire experience users have after Firefox has successfully been installed up until the first instance of new tab is shown.
First run help onboard new users by showing relevant messaging on about:welcome and about:newtab using triplets.
First run help onboard new users by showing relevant messaging on about:welcome and about:newtab using interrupts and triplets.
### First Run Multistage
A full-page multistep experience that shows up on first run since Fx80 with browser.aboutwelcome.enabled pref as true.
## Interrupts
A first run experience shown on about:welcome page and decide UI based on messaging template provided. In Firefox 72, interrupt can be one of below three types
Setting browser.aboutwelcome.enabled to false make first run looks like about:newtab and hides about:welcome
### First Run Modal
A modal that shows up on first run, usually the first stage.
In 71+, below modal interrupts are supported:
* join - purple first run modal with "Meet Firefox + Products / Knowledge Privacy + Join Firefox" messaging
* sync - purple first run modal but with 70 fxa messaging
* modal_control - First Run Modal control with same messaging as “join” modal
* modal_variant_a - First Run Modal with "Get the most + Sync/Monitor/Lockwise + Start Here" messaging.
* modal_variant_b - First Run Modal with "Supercharge privacy + Sync/Monitor/Lockwise + Start Here" messaging.
* modal_variant_c - First Run Modal with "Add Privacy + Sync/Monitor/Lockwise + Start Here" messaging.
* modal_variant_f - First Run Modal with "Meet Firefox + Products / Knowledge Privacy + Start Here" messaging.
### First Run Takeover
A full-page experience that shows up on first run, usually the first stage (a previous variant of this was the blue FxA Sync sign-in page).
A modal less page showing signup form and triplet messaging together on the same page.
* full_page_d - FxA signup form on top with triplet messaging on bottom
* full_page_e - FxA signup form on bottom with triplet messaging on top
### First Run Return to AMO
Part of a custom First Run Flow for users that installed Firefox after attempting to add an add-on from another browser. This is a full-page experience on first run.
Please Note: This is unique interrupt experience shown on about:welcome and not controlled by interrupt value of pref 'trailhead.firstrun.branches' and instead uses attribution targeting condition below
``` "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'"```
## Triplets
The cards that show up above the new tab content on the first instance of new tab. Setting browser.aboutwelcome.enabled to false and trailhead.firstrun.newtab.triplets to one of values below hides multistage welcome and takes user straight to triplets on opening about:welcome page
The cards that show up above the new tab content on the first instance of new tab, usually the second stage.
* supercharge - Shows Sync, Monitor and Mobile onboarding cards. Supported in 71+.
* payoff - Shows Monitor, Facbook Container and Firefox Send onboarding cards. Supported in 71 only.
@ -20,3 +45,18 @@ In 72+
* static - same experience as supercharge triplet - with Sync, Monitor and Mobile onboarding cards
* dynamic - Dynamic triplets showing three onboarding cards (Sync, Monitor and Private Browsing) that gets swapped with preselected list of cards that satisfies targeting rules. Preselected cards supported are Send Tab, Mobile and Lockwise.
* dynamic_chrome - Dynamic triplets showing three onboarding cards (Chrome switchers, Sync and Monitor) that gets swapped with preselected list of cards that satisfies targeting rules. Preselected cards supported are Private Browsing, Send Tab, Mobile and Lockwise.
## Misc
Below experiences are controlled by using following interrupt values inside 'trailhead.firstrun.branches'
* nofirstrun - nothing - looks like about:newtab and hides both first and second stage of about:welcome
* cards - no modal straight to triplet. This hides only first stage and takes user straight to triplets on opening about:welcome page.
## How to switch between first run experiences
First run experiences are controlled by pref 'trailhead.firstrun.branches'. This pref value follow format ```'<interrupt>-<triplet>'``` where ```<interrupt>``` is the interrupt message name from interrupt section above and ```<triplet>``` is triplet message name. If no value is set for 'trailhead.firstrun.branches', by default 'join-supercharge' interrupt and triplet experience is used. 'join-supercharge' is default first run experience in 71+.
For Example:
* Open about:config and set preference 'trailhead.firstrun.branches' to string value 'modal_variant_a-supercharge'
* Open about:welcome shows 'modal_variant_a' first run modal stage 1 on welcome screen.
* Dismissing welcome screen by clicking on “Start browsing” shows stage 2 'supercharge' triplets experience on new tab.

Просмотреть файл

@ -28,6 +28,7 @@ Please note that some targeting attributes require stricter controls on the tele
* [sync](#sync)
* [topFrecentSites](#topfrecentsites)
* [totalBookmarksCount](#totalbookmarkscount)
* [trailheadInterrupt](#trailheadinterrupt)
* [trailheadTriplet](#trailheadtriplet)
* [usesFirefoxSync](#usesfirefoxsync)
* [isFxAEnabled](#isFxAEnabled)
@ -442,6 +443,10 @@ Total number of bookmarks.
declare const totalBookmarksCount: number;
```
### `trailheadInterrupt`
(67.05+ only) Experiment branch for "interrupt" study
### `trailheadTriplet`
(67.05+ only) Experiment branch for "triplet" study

Просмотреть файл

@ -3,6 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { Interrupt } from "./Interrupt";
import { Triplets } from "./Triplets";
import { BASE_PARAMS } from "./addUtmParams";
@ -33,10 +34,12 @@ export class FirstRun extends React.PureComponent {
this.didLoadFlowParams = false;
this.state = {
didUserClearInterrupt: false,
didUserClearTriplets: false,
flowParams: undefined,
};
this.closeInterrupt = this.closeInterrupt.bind(this);
this.closeTriplets = this.closeTriplets.bind(this);
helpers.addFluent(this.props.document);
@ -73,13 +76,46 @@ export class FirstRun extends React.PureComponent {
}
}
removeHideMain() {
if (!this.isInterruptVisible) {
// We need to remove hide-main since we should show it underneath everything that has rendered
this.props.document.body.classList.remove("hide-main", "welcome");
}
}
// Is there any interrupt content? This is false for new tab triplets.
get hasInterrupt() {
const { message } = this.props;
return Boolean(message && message.content);
}
// Are all conditions met for the interrupt to actually be visible?
// 1. hasInterrupt - Is there interrupt content?
// 2. state.didUserClearInterrupt - Was it cleared by the user?
// 3. props.interruptCleared - Was it cleared externally?
get isInterruptVisible() {
return (
this.hasInterrupt &&
!this.state.didUserClearInterrupt &&
!this.props.interruptCleared
);
}
componentDidMount() {
this.fetchFlowParams();
this.removeHideMain();
}
componentDidUpdate() {
// In case we didn't have FXA info immediately, try again when we receive it.
this.fetchFlowParams();
this.removeHideMain();
}
closeInterrupt() {
this.setState({
didUserClearInterrupt: true,
});
}
closeTriplets() {
@ -93,15 +129,22 @@ export class FirstRun extends React.PureComponent {
render() {
const { props, state, UTMTerm } = this;
const { sendUserActionTelemetry, executeAction, message } = props;
const {
sendUserActionTelemetry,
fxaEndpoint,
dispatch,
executeAction,
message,
} = props;
const { didUserClearTriplets, flowParams } = state;
const hasTriplets = Boolean(message.bundle && message.bundle.length);
const interrupt = this.hasInterrupt ? message : null;
const triplets = hasTriplets ? message.bundle : null;
const isTripletsContainerVisible = hasTriplets && !didUserClearTriplets;
// Allow 1) falsy to not render a header 2) default welcome header 3) custom header
// Allow 1) falsy to not render a header 2) default welcome 3) custom header
const tripletsHeaderId =
message.tripletsHeaderId === undefined
? "onboarding-welcome-header"
@ -109,12 +152,29 @@ export class FirstRun extends React.PureComponent {
return (
<>
{this.isInterruptVisible ? (
<Interrupt
document={props.document}
cards={triplets}
message={interrupt}
onNextScene={this.closeInterrupt}
UTMTerm={UTMTerm}
sendUserActionTelemetry={sendUserActionTelemetry}
executeAction={executeAction}
dispatch={dispatch}
flowParams={flowParams}
onDismiss={this.closeInterrupt}
fxaEndpoint={fxaEndpoint}
onBlockById={props.onBlockById}
/>
) : null}
{hasTriplets ? (
<Triplets
document={props.document}
cards={triplets}
headerId={tripletsHeaderId}
showCardPanel={isTripletsContainerVisible}
showContent={!this.isInterruptVisible}
hideContainer={this.closeTriplets}
sendUserActionTelemetry={sendUserActionTelemetry}
UTMTerm={`${UTMTerm}-card`}

Просмотреть файл

@ -0,0 +1,77 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { Trailhead } from "../Trailhead/Trailhead";
import { ReturnToAMO } from "../ReturnToAMO/ReturnToAMO";
import { FullPageInterrupt } from "../FullPageInterrupt/FullPageInterrupt";
import { LocalizationProvider } from "fluent-react";
import { generateBundles } from "../../rich-text-strings";
export class Interrupt extends React.PureComponent {
render() {
const {
cards,
onDismiss,
onNextScene,
message,
sendUserActionTelemetry,
executeAction,
dispatch,
fxaEndpoint,
UTMTerm,
flowParams,
} = this.props;
switch (message.template) {
case "return_to_amo_overlay":
return (
<LocalizationProvider
bundles={generateBundles({ amo_html: message.content.text })}
>
<ReturnToAMO
{...message}
document={this.props.document}
UISurface="NEWTAB_OVERLAY"
onBlock={onDismiss}
onAction={executeAction}
sendUserActionTelemetry={sendUserActionTelemetry}
/>
</LocalizationProvider>
);
case "full_page_interrupt":
return (
<FullPageInterrupt
document={this.props.document}
cards={cards}
message={message}
onBlock={onDismiss}
onAction={executeAction}
dispatch={dispatch}
fxaEndpoint={fxaEndpoint}
sendUserActionTelemetry={sendUserActionTelemetry}
UTMTerm={UTMTerm}
flowParams={flowParams}
onBlockById={this.props.onBlockById}
/>
);
case "trailhead":
return (
<Trailhead
document={this.props.document}
message={message}
onNextScene={onNextScene}
onAction={executeAction}
sendUserActionTelemetry={sendUserActionTelemetry}
dispatch={dispatch}
fxaEndpoint={fxaEndpoint}
UTMTerm={UTMTerm}
flowParams={flowParams}
/>
);
default:
throw new Error(`${message.template} is not a valid FirstRun message`);
}
}
}

Просмотреть файл

@ -61,15 +61,16 @@ export class Triplets extends React.PureComponent {
cards,
headerId,
showCardPanel,
showContent,
sendUserActionTelemetry,
} = this.props;
return (
<div
className={`trailheadCards ${showCardPanel ? "expanded" : "collapsed"}`}
>
<div className="trailheadCardsInner" aria-hidden={!showCardPanel}>
<div className="trailheadCardsInner" aria-hidden={!showContent}>
{headerId && <h1 data-l10n-id={headerId} />}
<div className={`trailheadCardGrid${showCardPanel ? " show" : ""}`}>
<div className={`trailheadCardGrid${showContent ? " show" : ""}`}>
{cards.map(card => (
<OnboardingCard
key={card.id}

Просмотреть файл

@ -0,0 +1,189 @@
/* 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 { addUtmParams } from "../FirstRun/addUtmParams";
import { FxASignupForm } from "../../components/FxASignupForm/FxASignupForm";
import { OnboardingCard } from "../../templates/OnboardingMessage/OnboardingMessage";
import React from "react";
export const FxAccounts = ({
document,
content,
dispatch,
fxaEndpoint,
flowParams,
removeOverlay,
url,
UTMTerm,
}) => (
<React.Fragment>
<div
className="fullpage-left-section"
aria-labelledby="fullpage-left-title"
aria-describedby="fullpage-left-content"
>
<h1
id="fullpage-left-title"
className="fullpage-left-title"
data-l10n-id="onboarding-welcome-body"
/>
<p
id="fullpage-left-content"
className="fullpage-left-content"
data-l10n-id="onboarding-benefit-products-text"
/>
<p
className="fullpage-left-content"
data-l10n-id="onboarding-benefit-privacy-text"
/>
<a
className="fullpage-left-link"
href={addUtmParams(url, UTMTerm)}
target="_blank"
rel="noopener noreferrer"
data-l10n-id="onboarding-welcome-learn-more"
/>
<div className="fullpage-icon fx-systems-icons" />
</div>
<div className="fullpage-form">
<FxASignupForm
document={document}
content={content}
dispatch={dispatch}
fxaEndpoint={fxaEndpoint}
UTMTerm={UTMTerm}
flowParams={flowParams}
onClose={removeOverlay}
showSignInLink={true}
/>
</div>
</React.Fragment>
);
export const FxCards = ({ cards, onCardAction, sendUserActionTelemetry }) => (
<React.Fragment>
{cards.map(card => (
<OnboardingCard
key={card.id}
message={card}
className="trailheadCard"
sendUserActionTelemetry={sendUserActionTelemetry}
onAction={onCardAction}
UISurface="TRAILHEAD"
{...card}
/>
))}
</React.Fragment>
);
export class FullPageInterrupt extends React.PureComponent {
constructor(props) {
super(props);
this.removeOverlay = this.removeOverlay.bind(this);
this.onCardAction = this.onCardAction.bind(this);
}
componentWillMount() {
global.document.body.classList.add("trailhead-fullpage");
}
componentDidMount() {
// Hide the page content from screen readers while the full page interrupt is open
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "true");
}
removeOverlay() {
window.removeEventListener("visibilitychange", this.removeOverlay);
document.body.classList.remove("hide-main", "trailhead-fullpage");
// Re-enable the document for screen readers
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "false");
this.props.onBlock();
document.body.classList.remove("welcome");
}
onCardAction(action, message) {
let actionUpdates = {};
const { flowParams, UTMTerm } = this.props;
if (action.type === "OPEN_URL") {
let url = new URL(action.data.args);
addUtmParams(url, UTMTerm);
if (action.addFlowParams) {
url.searchParams.append("device_id", flowParams.deviceId);
url.searchParams.append("flow_id", flowParams.flowId);
url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
}
actionUpdates = { data: { ...action.data, args: url.toString() } };
}
this.props.onAction({ ...action, ...actionUpdates });
// Only block if message is in dynamic triplets experiment
if (message.blockOnClick) {
this.props.onBlockById(message.id, { preloadedOnly: true });
}
this.removeOverlay();
}
render() {
const { props } = this;
const { content } = props.message;
const cards = (
<FxCards
cards={props.cards}
onCardAction={this.onCardAction}
sendUserActionTelemetry={props.sendUserActionTelemetry}
/>
);
const accounts = (
<FxAccounts
document={props.document}
content={content}
dispatch={props.dispatch}
fxaEndpoint={props.fxaEndpoint}
flowParams={props.flowParams}
removeOverlay={this.removeOverlay}
url={content.learn.url}
UTMTerm={props.UTMTerm}
/>
);
// By default we show accounts section on top and
// cards section in bottom half of the full page interrupt
const cardsFirst = content && content.className === "fullPageCardsAtTop";
const firstContainerClassName = [
"container",
content && content.className,
].join(" ");
return (
<div className="fullpage-wrapper">
<div className="fullpage-icon brand-logo" />
<h1
className="welcome-title"
data-l10n-id="onboarding-welcome-header"
/>
<h2
className="welcome-subtitle"
data-l10n-id="onboarding-fullpage-welcome-subheader"
/>
<div className={firstContainerClassName}>
{cardsFirst ? cards : accounts}
</div>
<div className="section-divider" />
<div className="container">{cardsFirst ? accounts : cards}</div>
</div>
);
}
}
FullPageInterrupt.defaultProps = {
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
};

Просмотреть файл

@ -0,0 +1,258 @@
.activity-stream {
&.welcome {
overflow: hidden;
}
&:not(.welcome) {
.fullpage-wrapper {
display: none;
}
}
}
.fullpage-wrapper {
$responsive-breakpoint: 975px;
$responsive-width: 300px;
$header-size: 36px;
$form-text-size: 16px;
align-content: center;
display: flex;
flex-direction: column;
overflow-x: auto;
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 21000;
background-color: $ghost-white;
+ div {
opacity: 0;
}
.fullpage-icon {
background-position-x: left;
background-repeat: no-repeat;
background-size: contain;
&:dir(rtl) {
background-position-x: right;
}
@media screen and (max-width: $responsive-breakpoint) {
background-position: center;
}
}
.brand-logo {
background-image: url('chrome://branding/content/about-logo.png');
margin: 20px 10px 10px 20px;
padding-bottom: 50px;
}
.welcome-title,
.welcome-subtitle {
align-self: center;
margin: 0;
@media screen and (max-width: $responsive-breakpoint) {
text-align: center;
}
}
.welcome-title {
color: $trailhead-purple-80;
font-size: 46px;
font-weight: 600;
line-height: 62px;
}
.welcome-subtitle {
color: $trailhead-violet;
font-size: 20px;
line-height: 27px;
}
.container {
display: flex;
align-self: center;
padding: 50px 0;
@media screen and (max-width: $responsive-breakpoint) {
flex-direction: column;
width: $responsive-width;
text-align: center;
}
}
.fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px;
@media screen and (max-width: $responsive-breakpoint) {
width: $responsive-width;
}
.fullpage-left-content {
color: $grey-60;
display: inline;
margin: 0;
margin-inline-end: 2px;
}
.fullpage-left-link {
color: $blue-60;
display: block;
text-decoration: underline;
margin-bottom: 30px;
&:hover,
&:active,
&:focus {
color: $blue-60;
}
}
.fullpage-left-title {
margin: 0;
color: $trailhead-purple-80;
font-size: $header-size;
line-height: 48px;
}
.fx-systems-icons {
height: 33px;
display: block;
background-image: url('#{$image-path}trailhead/firefox-systems.png');
margin-bottom: 20px;
}
}
.fullpage-form {
position: relative;
text-align: center;
margin-inline-start: $header-size;
@media screen and (max-width: $responsive-breakpoint) {
margin-inline-start: 0;
}
.fxaSignupForm {
width: 356px;
padding: 25px;
box-shadow: 0 0 16px 0 $black-15;
border-radius: 6px;
background: $white;
}
.fxa-terms {
margin: 4px 0 20px;
a,
& {
color: $grey-60;
font-size: 12px;
line-height: $form-text-size;
}
}
.fxa-signin {
color: $grey-60;
line-height: 30px;
opacity: 0.77;
button {
color: $blue-60;
}
}
h3 {
color: $trailhead-purple-80;
font-weight: 400;
font-size: $header-size;
line-height: $header-size;
margin: 0;
padding: 8px;
}
h3 + p {
color: $grey-60;
font-size: $form-text-size;
line-height: 20px;
opacity: 0.77;
}
input {
background: $white;
border: 1px solid $grey-30;
border-radius: 2px;
&:hover {
border-color: $grey-50;
}
&.invalid {
border-color: $red-60;
}
}
button {
color: $white;
font-size: $form-text-size;
&:focus {
outline: dotted 1px $grey-50;
}
}
}
.section-divider::after {
content: '';
display: block;
border-bottom: 0.5px solid $grey-30;
}
.trailheadCard {
box-shadow: none;
background: none;
text-align: center;
width: 320px;
padding: 18px;
.onboardingTitle {
color: $grey-90;
}
.onboardingText {
font-weight: normal;
color: $grey-60;
margin-top: 4px;
}
.onboardingButton {
color: $grey-60;
background: $grey-90-10;
&:focus,
&:hover {
background: $grey-90-20;
}
&:active {
background: $grey-90-30;
}
}
.onboardingMessageImage {
height: 112px;
width: 154px;
}
@media screen and (max-width: $responsive-breakpoint) {
width: $responsive-width;
}
}
}

Просмотреть файл

@ -114,6 +114,7 @@
}
}
// Also used for Trailhead
.onboardingMessageImage {
height: 112px;
width: 180px;

Просмотреть файл

@ -0,0 +1,106 @@
/* 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 { RichText } from "../../components/RichText/RichText";
// Alt text if available; in the future this should come from the server. See bug 1551711
const ICON_ALT_TEXT = "";
export class ReturnToAMO extends React.PureComponent {
constructor(props) {
super(props);
this.onClickAddExtension = this.onClickAddExtension.bind(this);
this.onBlockButton = this.onBlockButton.bind(this);
}
componentWillMount() {
global.document.body.classList.add("amo");
}
componentDidMount() {
this.props.sendUserActionTelemetry({
event: "IMPRESSION",
id: this.props.UISurface,
});
// Hide the page content from screen readers while the modal is open
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "true");
}
onClickAddExtension() {
this.props.onAction(this.props.content.primary_button.action);
this.props.sendUserActionTelemetry({
event: "INSTALL",
id: this.props.UISurface,
});
}
onBlockButton() {
this.props.onBlock();
document.body.classList.remove("welcome", "hide-main", "amo");
this.props.sendUserActionTelemetry({
event: "BLOCK",
id: this.props.UISurface,
});
// Re-enable the document for screen readers
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "false");
}
renderText() {
const customElement = (
<img
src={this.props.content.addon_icon}
width="20px"
height="20px"
alt={ICON_ALT_TEXT}
/>
);
return (
<RichText
customElements={{ icon: customElement }}
amo_html={this.props.content.text}
localization_id="amo_html"
/>
);
}
render() {
const { content } = this.props;
return (
<div className="ReturnToAMOOverlay">
<div>
<h2> {content.header} </h2>
<div className="ReturnToAMOContainer">
<div className="ReturnToAMOAddonContents">
<p> {content.title} </p>
<div className="ReturnToAMOText">
<span> {this.renderText()} </span>
</div>
<button
onClick={this.onClickAddExtension}
className="puffy blue ReturnToAMOAddExtension"
>
{" "}
<span className="icon icon-add" />{" "}
{content.primary_button.label}{" "}
</button>
</div>
<div className="ReturnToAMOIcon" />
</div>
<button
onClick={this.onBlockButton}
className="default grey ReturnToAMOGetStarted"
>
{" "}
{content.secondary_button.label}{" "}
</button>
</div>
</div>
);
}
}

Просмотреть файл

@ -0,0 +1,125 @@
.ReturnToAMOOverlay,
.amo + body.hide-main { // sass-lint:disable-line no-qualifying-elements
background: $grey-10;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 2100;
.ReturnToAMOText {
color: $grey-90;
line-height: 32px;
font-size: 23px;
width: 100%;
img {
margin-inline-start: 6px;
margin-inline-end: 6px;
}
}
h2 {
color: $grey-60;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px;
}
p {
color: $grey-60;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
}
.puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px;
}
.blue {
border: 0;
color: $white;
background-color: $blue-60;
&:hover {
box-shadow: none;
background-color: $blue-70;
}
&:active {
background-color: $blue-80;
}
}
.default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px;
}
.grey {
border: 0;
background-color: $grey-90-10;
&:hover {
box-shadow: none;
background-color: $grey-90-20;
}
&:active {
background-color: $grey-90-30;
}
}
.ReturnToAMOGetStarted {
margin-top: 40px;
float: right;
&:dir(rtl) {
float: left;
}
}
.ReturnToAMOAddExtension {
margin-top: 20px;
}
.ReturnToAMOContainer {
width: 960px;
background: $white;
box-shadow: 0 1px 15px 0 $black-30;
border-radius: 4px;
display: flex;
padding: 64px 64px 72px;
}
.ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px;
}
.ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url('chrome://activity-stream/content/data/content/assets/gift-extension.svg');
}
.icon-add {
fill: $white;
vertical-align: sub;
}
}

Просмотреть файл

@ -0,0 +1,148 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { actionCreators as ac } from "common/Actions.jsm";
import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
import { FxASignupForm } from "../../components/FxASignupForm/FxASignupForm";
import { addUtmParams } from "../FirstRun/addUtmParams";
import React from "react";
// From resource://devtools/client/shared/focus.js
const FOCUSABLE_SELECTOR = [
"a[href]:not([tabindex='-1'])",
"button:not([disabled]):not([tabindex='-1'])",
"iframe:not([tabindex='-1'])",
"input:not([disabled]):not([tabindex='-1'])",
"select:not([disabled]):not([tabindex='-1'])",
"textarea:not([disabled]):not([tabindex='-1'])",
"[tabindex]:not([tabindex='-1'])",
].join(", ");
export class Trailhead extends React.PureComponent {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.onStartBlur = this.onStartBlur.bind(this);
}
get dialog() {
return this.props.document.getElementById("trailheadDialog");
}
componentDidMount() {
// We need to remove hide-main since we should show it underneath everything that has rendered
this.props.document.body.classList.remove("hide-main");
// The rest of the page is "hidden" to screen readers when the modal is open
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "true");
}
onStartBlur(event) {
// Make sure focus stays within the dialog when tabbing from the button
const { dialog } = this;
if (
event.relatedTarget &&
!(
dialog.compareDocumentPosition(event.relatedTarget) &
dialog.DOCUMENT_POSITION_CONTAINED_BY
)
) {
dialog.querySelector(FOCUSABLE_SELECTOR).focus();
}
}
closeModal(ev) {
global.removeEventListener("visibilitychange", this.closeModal);
this.props.document.body.classList.remove("welcome");
this.props.document.getElementById("root").removeAttribute("aria-hidden");
this.props.onNextScene();
// If closeModal() was triggered by a visibilitychange event, the user actually
// submitted the email form so we don't send a SKIPPED_SIGNIN ping.
if (!ev || ev.type !== "visibilitychange") {
this.props.dispatch(
ac.UserEvent({ event: "SKIPPED_SIGNIN", ...this._getFormInfo() })
);
}
// Bug 1190882 - Focus in a disappearing dialog confuses screen readers
this.props.document.activeElement.blur();
}
/**
* Report to telemetry additional information about the form submission.
*/
_getFormInfo() {
const value = { has_flow_params: !!this.props.flowParams.flowId.length };
return { value };
}
render() {
const { props } = this;
const { UTMTerm } = props;
const { content } = props.message;
const innerClassName = ["trailhead", content && content.className]
.filter(v => v)
.join(" ");
return (
<ModalOverlayWrapper
innerClassName={innerClassName}
onClose={this.closeModal}
id="trailheadDialog"
headerId="trailheadHeader"
hasDismissIcon={true}
>
<div className="trailheadInner">
<div className="trailheadContent">
<h1 data-l10n-id={content.title.string_id} id="trailheadHeader" />
{content.subtitle && (
<p data-l10n-id={content.subtitle.string_id} />
)}
<ul className="trailheadBenefits">
{content.benefits.map(item => (
<li key={item.id} className={item.id}>
<h2 data-l10n-id={item.title.string_id} />
<p data-l10n-id={item.text.string_id} />
</li>
))}
</ul>
<a
className="trailheadLearn"
data-l10n-id={content.learn.text.string_id}
href={addUtmParams(content.learn.url, UTMTerm)}
target="_blank"
rel="noopener noreferrer"
/>
</div>
<div className="trailhead-join-form">
<FxASignupForm
document={this.props.document}
content={content}
dispatch={this.props.dispatch}
fxaEndpoint={this.props.fxaEndpoint}
UTMTerm={UTMTerm}
flowParams={this.props.flowParams}
onClose={this.closeModal}
showSignInLink={true}
/>
</div>
</div>
<button
className="trailheadStart"
data-l10n-id={content.skipButton.string_id}
onBlur={this.onStartBlur}
onClick={this.closeModal}
/>
</ModalOverlayWrapper>
);
}
}
Trailhead.defaultProps = {
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
};

Просмотреть файл

@ -1,3 +1,185 @@
.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;
$logo-size: 100px;
background: url('#{$image-path}trailhead/accounts-form-bg.jpg') bottom / cover;
color: $white;
height: auto;
a {
color: $white;
text-decoration: underline;
}
input,
button {
border-radius: 4px;
padding: 10px;
}
.trailheadInner {
$content-spacing: 40px;
display: grid;
grid-column-gap: $content-spacing;
grid-template-columns: 5fr 3fr;
padding: $content-spacing 60px;
}
.trailheadContent {
h1 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 0;
}
.trailheadLearn {
display: block;
margin-top: 30px;
@media (min-width: $responsive-breakpoint) {
margin-inline-start: $benefit-icon-spacing;
}
}
}
.trailhead-join-form {
background: url('#{$image-path}trailhead/firefox-logo.png') top center / $logo-size no-repeat;
color: $white;
min-width: 260px;
padding-top: $logo-size;
}
&.syncCohort {
left: calc(50% - 430px);
width: 860px;
@media (max-width: 860px) {
left: 0;
width: 100%;
}
.trailheadInner {
grid-template-columns: 4fr 3fr;
}
.trailheadContent {
.trailheadBenefits {
background: url('#{$image-path}sync-devices-trailhead.svg');
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
height: 200px;
margin-inline-end: 60px;
}
.trailheadLearn {
margin-inline-start: 0;
}
}
}
.trailheadBenefits {
padding: 0;
li {
background-position: left 6px;
background-repeat: no-repeat;
background-size: $benefit-icon-size-small;
-moz-context-properties: fill;
fill: $blue-50;
list-style: none;
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,
&.monitor {
background-image: url('#{$image-path}trailhead/benefit-knowledge.png');
}
&.lockwise,
&.privacy {
background-image: url('#{$image-path}trailhead/benefit-privacy.png');
}
&.products {
background-image: url('#{$image-path}trailhead/benefit-products.png');
}
&.sync {
background-image: url('#{$image-path}trailhead/benefit-sync.png');
}
}
h2 {
text-align: start;
line-height: inherit;
color: $violet-20;
font-size: 22px;
font-weight: 400;
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;
}
}
.trailheadStart {
border: 1px solid $white-50;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
margin: 0 auto 40px;
min-width: 300px;
padding: 14px;
&:hover,
&: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);
overflow: hidden;

Просмотреть файл

@ -516,7 +516,7 @@ export class ASRouterAdminInner extends React.PureComponent {
collapsedMessages: [],
modifiedMessages: [],
evaluationStatus: {},
trailheadTriplet: "",
trailhead: {},
stringTargetingParameters: null,
newStringTargetingParameters: null,
copiedToClipboard: false,
@ -1624,12 +1624,17 @@ export class ASRouterAdminInner extends React.PureComponent {
}
renderTrailheadInfo() {
const { trailheadInterrupt, trailheadTriplet } = this.state.trailhead;
return (
<table className="minimal-table">
<tbody>
<tr>
<td>Interrupt branch</td>
<td>{trailheadInterrupt}</td>
</tr>
<tr>
<td>Triplet branch</td>
<td>{this.state.trailheadTriplet}</td>
<td>{trailheadTriplet}</td>
</tr>
</tbody>
</table>

Просмотреть файл

@ -42,6 +42,12 @@ function debounce(func, wait) {
}
export class _Base extends React.PureComponent {
componentWillMount() {
if (this.props.isFirstrun) {
global.document.body.classList.add("welcome", "hide-main");
}
}
componentWillUnmount() {
this.updateTheme();
}
@ -55,6 +61,8 @@ export class _Base extends React.PureComponent {
"activity-stream",
// If we skipped the about:welcome overlay and removed the CSS classes
// we don't want to add them back to the Activity Stream view
document.body.classList.contains("welcome") ? "welcome" : "",
document.body.classList.contains("hide-main") ? "hide-main" : "",
document.body.classList.contains("inline-onboarding")
? "inline-onboarding"
: "",

Просмотреть файл

@ -172,9 +172,12 @@ input {
@import '../asrouter/components/Button/Button';
@import '../asrouter/components/SnippetBase/SnippetBase';
@import '../asrouter/components/ModalOverlay/ModalOverlay';
@import '../asrouter/templates/ReturnToAMO/ReturnToAMO';
@import '../asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet';
@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';
@import '../asrouter/templates/FirstRun/Triplets';
@import '../asrouter/templates/EOYSnippet/EOYSnippet';
@import '../asrouter/components/FxASignupForm/FxASignupForm';
@import '../asrouter/templates/Trailhead/Trailhead';
@import '../asrouter/templates/FullPageInterrupt/FullPageInterrupt';

Просмотреть файл

@ -3607,6 +3607,112 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px #D7D7DB;
transition: box-shadow 150ms; }
.ReturnToAMOOverlay,
.amo + body.hide-main {
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 2100; }
.ReturnToAMOOverlay .ReturnToAMOText,
.amo + body.hide-main .ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%; }
.ReturnToAMOOverlay .ReturnToAMOText img,
.amo + body.hide-main .ReturnToAMOText img {
margin-inline-start: 6px;
margin-inline-end: 6px; }
.ReturnToAMOOverlay h2,
.amo + body.hide-main h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px; }
.ReturnToAMOOverlay p,
.amo + body.hide-main p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px; }
.ReturnToAMOOverlay .puffy,
.amo + body.hide-main .puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px; }
.ReturnToAMOOverlay .blue,
.amo + body.hide-main .blue {
border: 0;
color: #FFF;
background-color: #0060DF; }
.ReturnToAMOOverlay .blue:hover,
.amo + body.hide-main .blue:hover {
box-shadow: none;
background-color: #003EAA; }
.ReturnToAMOOverlay .blue:active,
.amo + body.hide-main .blue:active {
background-color: #002275; }
.ReturnToAMOOverlay .default,
.amo + body.hide-main .default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px; }
.ReturnToAMOOverlay .grey,
.amo + body.hide-main .grey {
border: 0;
background-color: rgba(12, 12, 13, 0.1); }
.ReturnToAMOOverlay .grey:hover,
.amo + body.hide-main .grey:hover {
box-shadow: none;
background-color: rgba(12, 12, 13, 0.2); }
.ReturnToAMOOverlay .grey:active,
.amo + body.hide-main .grey:active {
background-color: rgba(12, 12, 13, 0.3); }
.ReturnToAMOOverlay .ReturnToAMOGetStarted,
.amo + body.hide-main .ReturnToAMOGetStarted {
margin-top: 40px;
float: right; }
.ReturnToAMOOverlay .ReturnToAMOGetStarted:dir(rtl),
.amo + body.hide-main .ReturnToAMOGetStarted:dir(rtl) {
float: left; }
.ReturnToAMOOverlay .ReturnToAMOAddExtension,
.amo + body.hide-main .ReturnToAMOAddExtension {
margin-top: 20px; }
.ReturnToAMOOverlay .ReturnToAMOContainer,
.amo + body.hide-main .ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px; }
.ReturnToAMOOverlay .ReturnToAMOAddonContents,
.amo + body.hide-main .ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px; }
.ReturnToAMOOverlay .ReturnToAMOIcon,
.amo + body.hide-main .ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url("chrome://activity-stream/content/data/content/assets/gift-extension.svg"); }
.ReturnToAMOOverlay .icon-add,
.amo + body.hide-main .icon-add {
fill: #FFF;
vertical-align: sub; }
.below-search-snippet {
margin: 0 auto 16px; }
.below-search-snippet.withButton {
@ -4083,6 +4189,233 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-width: 80px;
background-size: 140px; } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
.fxaSignupForm {
min-width: 260px;
text-align: center; }
.fxaSignupForm a {
color: #FFF;
text-decoration: underline; }
.fxaSignupForm input,
.fxaSignupForm button {
border-radius: 4px;
padding: 10px; }
.fxaSignupForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.fxaSignupForm p {
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.fxaSignupForm .fxa-terms {
margin: 4px 30px 20px; }
.fxaSignupForm .fxa-terms a, .fxaSignupForm .fxa-terms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.fxaSignupForm .fxa-signin {
font-size: 16px;
margin-top: 19px; }
.fxaSignupForm .fxa-signin span {
margin-inline-end: 5px; }
.fxaSignupForm .fxa-signin button {
background-color: initial;
text-decoration: underline;
color: #FFF;
display: inline;
padding: 0;
width: auto; }
.fxaSignupForm .fxa-signin button:hover, .fxaSignupForm .fxa-signin button:focus, .fxaSignupForm .fxa-signin button:active {
background-color: initial; }
.fxaSignupForm form {
position: relative; }
.fxaSignupForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.fxaSignupForm button,
.fxaSignupForm input {
width: 100%; }
.fxaSignupForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.fxaSignupForm input:hover {
border-color: #0C0C0D; }
.fxaSignupForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.fxaSignupForm input.invalid {
border-color: #D70022; }
.fxaSignupForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.fxaSignupForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.fxaSignupForm button:hover, .fxaSignupForm button:focus {
background-color: #0250BB; }
.fxaSignupForm button:focus {
outline: dotted 1px; }
.fxaSignupForm button:active {
background-color: #054096; }
.trailhead {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
height: auto; }
.trailhead a {
color: #FFF;
text-decoration: underline; }
.trailhead input,
.trailhead button {
border-radius: 4px;
padding: 10px; }
.trailhead .trailheadInner {
display: grid;
grid-column-gap: 40px;
grid-template-columns: 5fr 3fr;
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; }
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
color: #FFF;
min-width: 260px;
padding-top: 100px; }
.trailhead.syncCohort {
left: calc(50% - 430px);
width: 860px; }
@media (max-width: 860px) {
.trailhead.syncCohort {
left: 0;
width: 100%; } }
.trailhead.syncCohort .trailheadInner {
grid-template-columns: 4fr 3fr; }
.trailhead.syncCohort .trailheadContent .trailheadBenefits {
background: url("chrome://activity-stream/content/data/content/assets/sync-devices-trailhead.svg");
background-position: center center;
background-repeat: no-repeat;
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 6px;
background-repeat: no-repeat;
background-size: 40px;
-moz-context-properties: fill;
fill: #0A84FF;
list-style: none;
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, .trailhead .trailheadBenefits li.monitor {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-knowledge.png"); }
.trailhead .trailheadBenefits li.lockwise, .trailhead .trailheadBenefits li.privacy {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-privacy.png"); }
.trailhead .trailheadBenefits li.products {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-products.png"); }
.trailhead .trailheadBenefits li.sync {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-sync.png"); }
.trailhead .trailheadBenefits h2 {
text-align: start;
line-height: inherit;
color: #CB9EFF;
font-size: 22px;
font-weight: 400;
margin: 0 0 4px;
padding-inline-start: 52px; }
@media (min-width: 850px) {
.trailhead .trailheadBenefits h2 {
padding-inline-start: 0; } }
.trailhead .trailheadBenefits p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.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);
overflow: hidden;
@ -4239,39 +4572,171 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
align-content: center;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
flex-direction: column;
overflow-x: auto;
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 21000;
background-color: #FAFAFC; }
.fullpage-wrapper + div {
opacity: 0; }
.fullpage-wrapper .fullpage-icon {
background-position-x: left;
background-repeat: no-repeat;
background-size: contain; }
.fullpage-wrapper .fullpage-icon:dir(rtl) {
background-position-x: right; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-icon {
background-position: center; } }
.fullpage-wrapper .brand-logo {
background-image: url("chrome://branding/content/about-logo.png");
margin: 20px 10px 10px 20px;
padding-bottom: 50px; }
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
align-self: center;
margin: 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
text-align: center; } }
.fullpage-wrapper .welcome-title {
color: #36296D;
font-size: 46px;
font-weight: 600;
line-height: 62px; }
.fullpage-wrapper .welcome-subtitle {
color: #7542E5;
font-size: 20px;
line-height: 27px; }
.fullpage-wrapper .container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 300px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-left-section {
width: 300px; } }
.fullpage-wrapper .fullpage-left-section .fullpage-left-content {
color: #4A4A4F;
display: inline;
margin: 0;
margin-inline-end: 2px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link {
color: #0060DF;
display: block;
text-decoration: underline;
margin-bottom: 30px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link:hover, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:active, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:focus {
color: #0060DF; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-title {
margin: 0;
color: #36296D;
font-size: 36px;
line-height: 48px; }
.fullpage-wrapper .fullpage-left-section .fx-systems-icons {
height: 33px;
display: block;
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-systems.png");
margin-bottom: 20px; }
.fullpage-wrapper .fullpage-form {
position: relative;
text-align: center;
margin-inline-start: 36px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-form {
margin-inline-start: 0; } }
.fullpage-wrapper .fullpage-form .fxaSignupForm {
width: 356px;
padding: 25px;
box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.15);
border-radius: 6px;
background: #FFF; }
.fullpage-wrapper .fullpage-form .fxa-terms {
margin: 4px 0 20px; }
.fullpage-wrapper .fullpage-form .fxa-terms a, .fullpage-wrapper .fullpage-form .fxa-terms {
color: #4A4A4F;
font-size: 12px;
line-height: 16px; }
.fullpage-wrapper .fullpage-form .fxa-signin {
color: #4A4A4F;
line-height: 30px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form .fxa-signin button {
color: #0060DF; }
.fullpage-wrapper .fullpage-form h3 {
color: #36296D;
font-weight: 400;
font-size: 36px;
line-height: 36px;
margin: 0;
padding: 8px; }
.fullpage-wrapper .fullpage-form h3 + p {
color: #4A4A4F;
font-size: 16px;
line-height: 20px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form input {
background: #FFF;
border: 1px solid #D7D7DB;
border-radius: 2px; }
.fullpage-wrapper .fullpage-form input:hover {
border-color: #737373; }
.fullpage-wrapper .fullpage-form input.invalid {
border-color: #D70022; }
.fullpage-wrapper .fullpage-form button {
color: #FFF;
font-size: 16px; }
.fullpage-wrapper .fullpage-form button:focus {
outline: dotted 1px #737373; }
.fullpage-wrapper .section-divider::after {
content: '';
display: block;
border-bottom: 0.5px solid #D7D7DB; }
.fullpage-wrapper .trailheadCard {
box-shadow: none;
background: none;
text-align: center;
width: 320px;
padding: 18px; }
.fullpage-wrapper .trailheadCard .onboardingTitle {
color: #0C0C0D; }
.fullpage-wrapper .trailheadCard .onboardingText {
font-weight: normal;
color: #4A4A4F;
margin-top: 4px; }
.fullpage-wrapper .trailheadCard .onboardingButton {
color: #4A4A4F;
background: rgba(12, 12, 13, 0.1); }
.fullpage-wrapper .trailheadCard .onboardingButton:focus, .fullpage-wrapper .trailheadCard .onboardingButton:hover {
background: rgba(12, 12, 13, 0.2); }
.fullpage-wrapper .trailheadCard .onboardingButton:active {
background: rgba(12, 12, 13, 0.3); }
.fullpage-wrapper .trailheadCard .onboardingMessageImage {
height: 112px;
width: 154px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .trailheadCard {
width: 300px; } }

Просмотреть файл

@ -3610,6 +3610,112 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px #D7D7DB;
transition: box-shadow 150ms; }
.ReturnToAMOOverlay,
.amo + body.hide-main {
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 2100; }
.ReturnToAMOOverlay .ReturnToAMOText,
.amo + body.hide-main .ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%; }
.ReturnToAMOOverlay .ReturnToAMOText img,
.amo + body.hide-main .ReturnToAMOText img {
margin-inline-start: 6px;
margin-inline-end: 6px; }
.ReturnToAMOOverlay h2,
.amo + body.hide-main h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px; }
.ReturnToAMOOverlay p,
.amo + body.hide-main p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px; }
.ReturnToAMOOverlay .puffy,
.amo + body.hide-main .puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px; }
.ReturnToAMOOverlay .blue,
.amo + body.hide-main .blue {
border: 0;
color: #FFF;
background-color: #0060DF; }
.ReturnToAMOOverlay .blue:hover,
.amo + body.hide-main .blue:hover {
box-shadow: none;
background-color: #003EAA; }
.ReturnToAMOOverlay .blue:active,
.amo + body.hide-main .blue:active {
background-color: #002275; }
.ReturnToAMOOverlay .default,
.amo + body.hide-main .default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px; }
.ReturnToAMOOverlay .grey,
.amo + body.hide-main .grey {
border: 0;
background-color: rgba(12, 12, 13, 0.1); }
.ReturnToAMOOverlay .grey:hover,
.amo + body.hide-main .grey:hover {
box-shadow: none;
background-color: rgba(12, 12, 13, 0.2); }
.ReturnToAMOOverlay .grey:active,
.amo + body.hide-main .grey:active {
background-color: rgba(12, 12, 13, 0.3); }
.ReturnToAMOOverlay .ReturnToAMOGetStarted,
.amo + body.hide-main .ReturnToAMOGetStarted {
margin-top: 40px;
float: right; }
.ReturnToAMOOverlay .ReturnToAMOGetStarted:dir(rtl),
.amo + body.hide-main .ReturnToAMOGetStarted:dir(rtl) {
float: left; }
.ReturnToAMOOverlay .ReturnToAMOAddExtension,
.amo + body.hide-main .ReturnToAMOAddExtension {
margin-top: 20px; }
.ReturnToAMOOverlay .ReturnToAMOContainer,
.amo + body.hide-main .ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px; }
.ReturnToAMOOverlay .ReturnToAMOAddonContents,
.amo + body.hide-main .ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px; }
.ReturnToAMOOverlay .ReturnToAMOIcon,
.amo + body.hide-main .ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url("chrome://activity-stream/content/data/content/assets/gift-extension.svg"); }
.ReturnToAMOOverlay .icon-add,
.amo + body.hide-main .icon-add {
fill: #FFF;
vertical-align: sub; }
.below-search-snippet {
margin: 0 auto 16px; }
.below-search-snippet.withButton {
@ -4086,6 +4192,233 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-width: 80px;
background-size: 140px; } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
.fxaSignupForm {
min-width: 260px;
text-align: center; }
.fxaSignupForm a {
color: #FFF;
text-decoration: underline; }
.fxaSignupForm input,
.fxaSignupForm button {
border-radius: 4px;
padding: 10px; }
.fxaSignupForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.fxaSignupForm p {
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.fxaSignupForm .fxa-terms {
margin: 4px 30px 20px; }
.fxaSignupForm .fxa-terms a, .fxaSignupForm .fxa-terms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.fxaSignupForm .fxa-signin {
font-size: 16px;
margin-top: 19px; }
.fxaSignupForm .fxa-signin span {
margin-inline-end: 5px; }
.fxaSignupForm .fxa-signin button {
background-color: initial;
text-decoration: underline;
color: #FFF;
display: inline;
padding: 0;
width: auto; }
.fxaSignupForm .fxa-signin button:hover, .fxaSignupForm .fxa-signin button:focus, .fxaSignupForm .fxa-signin button:active {
background-color: initial; }
.fxaSignupForm form {
position: relative; }
.fxaSignupForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.fxaSignupForm button,
.fxaSignupForm input {
width: 100%; }
.fxaSignupForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.fxaSignupForm input:hover {
border-color: #0C0C0D; }
.fxaSignupForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.fxaSignupForm input.invalid {
border-color: #D70022; }
.fxaSignupForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.fxaSignupForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.fxaSignupForm button:hover, .fxaSignupForm button:focus {
background-color: #0250BB; }
.fxaSignupForm button:focus {
outline: dotted 1px; }
.fxaSignupForm button:active {
background-color: #054096; }
.trailhead {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
height: auto; }
.trailhead a {
color: #FFF;
text-decoration: underline; }
.trailhead input,
.trailhead button {
border-radius: 4px;
padding: 10px; }
.trailhead .trailheadInner {
display: grid;
grid-column-gap: 40px;
grid-template-columns: 5fr 3fr;
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; }
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
color: #FFF;
min-width: 260px;
padding-top: 100px; }
.trailhead.syncCohort {
left: calc(50% - 430px);
width: 860px; }
@media (max-width: 860px) {
.trailhead.syncCohort {
left: 0;
width: 100%; } }
.trailhead.syncCohort .trailheadInner {
grid-template-columns: 4fr 3fr; }
.trailhead.syncCohort .trailheadContent .trailheadBenefits {
background: url("chrome://activity-stream/content/data/content/assets/sync-devices-trailhead.svg");
background-position: center center;
background-repeat: no-repeat;
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 6px;
background-repeat: no-repeat;
background-size: 40px;
-moz-context-properties: fill;
fill: #0A84FF;
list-style: none;
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, .trailhead .trailheadBenefits li.monitor {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-knowledge.png"); }
.trailhead .trailheadBenefits li.lockwise, .trailhead .trailheadBenefits li.privacy {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-privacy.png"); }
.trailhead .trailheadBenefits li.products {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-products.png"); }
.trailhead .trailheadBenefits li.sync {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-sync.png"); }
.trailhead .trailheadBenefits h2 {
text-align: start;
line-height: inherit;
color: #CB9EFF;
font-size: 22px;
font-weight: 400;
margin: 0 0 4px;
padding-inline-start: 52px; }
@media (min-width: 850px) {
.trailhead .trailheadBenefits h2 {
padding-inline-start: 0; } }
.trailhead .trailheadBenefits p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.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);
overflow: hidden;
@ -4242,39 +4575,171 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
align-content: center;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
flex-direction: column;
overflow-x: auto;
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 21000;
background-color: #FAFAFC; }
.fullpage-wrapper + div {
opacity: 0; }
.fullpage-wrapper .fullpage-icon {
background-position-x: left;
background-repeat: no-repeat;
background-size: contain; }
.fullpage-wrapper .fullpage-icon:dir(rtl) {
background-position-x: right; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-icon {
background-position: center; } }
.fullpage-wrapper .brand-logo {
background-image: url("chrome://branding/content/about-logo.png");
margin: 20px 10px 10px 20px;
padding-bottom: 50px; }
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
align-self: center;
margin: 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
text-align: center; } }
.fullpage-wrapper .welcome-title {
color: #36296D;
font-size: 46px;
font-weight: 600;
line-height: 62px; }
.fullpage-wrapper .welcome-subtitle {
color: #7542E5;
font-size: 20px;
line-height: 27px; }
.fullpage-wrapper .container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 300px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-left-section {
width: 300px; } }
.fullpage-wrapper .fullpage-left-section .fullpage-left-content {
color: #4A4A4F;
display: inline;
margin: 0;
margin-inline-end: 2px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link {
color: #0060DF;
display: block;
text-decoration: underline;
margin-bottom: 30px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link:hover, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:active, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:focus {
color: #0060DF; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-title {
margin: 0;
color: #36296D;
font-size: 36px;
line-height: 48px; }
.fullpage-wrapper .fullpage-left-section .fx-systems-icons {
height: 33px;
display: block;
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-systems.png");
margin-bottom: 20px; }
.fullpage-wrapper .fullpage-form {
position: relative;
text-align: center;
margin-inline-start: 36px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-form {
margin-inline-start: 0; } }
.fullpage-wrapper .fullpage-form .fxaSignupForm {
width: 356px;
padding: 25px;
box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.15);
border-radius: 6px;
background: #FFF; }
.fullpage-wrapper .fullpage-form .fxa-terms {
margin: 4px 0 20px; }
.fullpage-wrapper .fullpage-form .fxa-terms a, .fullpage-wrapper .fullpage-form .fxa-terms {
color: #4A4A4F;
font-size: 12px;
line-height: 16px; }
.fullpage-wrapper .fullpage-form .fxa-signin {
color: #4A4A4F;
line-height: 30px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form .fxa-signin button {
color: #0060DF; }
.fullpage-wrapper .fullpage-form h3 {
color: #36296D;
font-weight: 400;
font-size: 36px;
line-height: 36px;
margin: 0;
padding: 8px; }
.fullpage-wrapper .fullpage-form h3 + p {
color: #4A4A4F;
font-size: 16px;
line-height: 20px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form input {
background: #FFF;
border: 1px solid #D7D7DB;
border-radius: 2px; }
.fullpage-wrapper .fullpage-form input:hover {
border-color: #737373; }
.fullpage-wrapper .fullpage-form input.invalid {
border-color: #D70022; }
.fullpage-wrapper .fullpage-form button {
color: #FFF;
font-size: 16px; }
.fullpage-wrapper .fullpage-form button:focus {
outline: dotted 1px #737373; }
.fullpage-wrapper .section-divider::after {
content: '';
display: block;
border-bottom: 0.5px solid #D7D7DB; }
.fullpage-wrapper .trailheadCard {
box-shadow: none;
background: none;
text-align: center;
width: 320px;
padding: 18px; }
.fullpage-wrapper .trailheadCard .onboardingTitle {
color: #0C0C0D; }
.fullpage-wrapper .trailheadCard .onboardingText {
font-weight: normal;
color: #4A4A4F;
margin-top: 4px; }
.fullpage-wrapper .trailheadCard .onboardingButton {
color: #4A4A4F;
background: rgba(12, 12, 13, 0.1); }
.fullpage-wrapper .trailheadCard .onboardingButton:focus, .fullpage-wrapper .trailheadCard .onboardingButton:hover {
background: rgba(12, 12, 13, 0.2); }
.fullpage-wrapper .trailheadCard .onboardingButton:active {
background: rgba(12, 12, 13, 0.3); }
.fullpage-wrapper .trailheadCard .onboardingMessageImage {
height: 112px;
width: 154px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .trailheadCard {
width: 300px; } }

Просмотреть файл

@ -3607,6 +3607,112 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
box-shadow: 0 0 0 5px #D7D7DB;
transition: box-shadow 150ms; }
.ReturnToAMOOverlay,
.amo + body.hide-main {
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 2100; }
.ReturnToAMOOverlay .ReturnToAMOText,
.amo + body.hide-main .ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%; }
.ReturnToAMOOverlay .ReturnToAMOText img,
.amo + body.hide-main .ReturnToAMOText img {
margin-inline-start: 6px;
margin-inline-end: 6px; }
.ReturnToAMOOverlay h2,
.amo + body.hide-main h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px; }
.ReturnToAMOOverlay p,
.amo + body.hide-main p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px; }
.ReturnToAMOOverlay .puffy,
.amo + body.hide-main .puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px; }
.ReturnToAMOOverlay .blue,
.amo + body.hide-main .blue {
border: 0;
color: #FFF;
background-color: #0060DF; }
.ReturnToAMOOverlay .blue:hover,
.amo + body.hide-main .blue:hover {
box-shadow: none;
background-color: #003EAA; }
.ReturnToAMOOverlay .blue:active,
.amo + body.hide-main .blue:active {
background-color: #002275; }
.ReturnToAMOOverlay .default,
.amo + body.hide-main .default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px; }
.ReturnToAMOOverlay .grey,
.amo + body.hide-main .grey {
border: 0;
background-color: rgba(12, 12, 13, 0.1); }
.ReturnToAMOOverlay .grey:hover,
.amo + body.hide-main .grey:hover {
box-shadow: none;
background-color: rgba(12, 12, 13, 0.2); }
.ReturnToAMOOverlay .grey:active,
.amo + body.hide-main .grey:active {
background-color: rgba(12, 12, 13, 0.3); }
.ReturnToAMOOverlay .ReturnToAMOGetStarted,
.amo + body.hide-main .ReturnToAMOGetStarted {
margin-top: 40px;
float: right; }
.ReturnToAMOOverlay .ReturnToAMOGetStarted:dir(rtl),
.amo + body.hide-main .ReturnToAMOGetStarted:dir(rtl) {
float: left; }
.ReturnToAMOOverlay .ReturnToAMOAddExtension,
.amo + body.hide-main .ReturnToAMOAddExtension {
margin-top: 20px; }
.ReturnToAMOOverlay .ReturnToAMOContainer,
.amo + body.hide-main .ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px; }
.ReturnToAMOOverlay .ReturnToAMOAddonContents,
.amo + body.hide-main .ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px; }
.ReturnToAMOOverlay .ReturnToAMOIcon,
.amo + body.hide-main .ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url("chrome://activity-stream/content/data/content/assets/gift-extension.svg"); }
.ReturnToAMOOverlay .icon-add,
.amo + body.hide-main .icon-add {
fill: #FFF;
vertical-align: sub; }
.below-search-snippet {
margin: 0 auto 16px; }
.below-search-snippet.withButton {
@ -4083,6 +4189,233 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-width: 80px;
background-size: 140px; } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
.fxaSignupForm {
min-width: 260px;
text-align: center; }
.fxaSignupForm a {
color: #FFF;
text-decoration: underline; }
.fxaSignupForm input,
.fxaSignupForm button {
border-radius: 4px;
padding: 10px; }
.fxaSignupForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.fxaSignupForm p {
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.fxaSignupForm .fxa-terms {
margin: 4px 30px 20px; }
.fxaSignupForm .fxa-terms a, .fxaSignupForm .fxa-terms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.fxaSignupForm .fxa-signin {
font-size: 16px;
margin-top: 19px; }
.fxaSignupForm .fxa-signin span {
margin-inline-end: 5px; }
.fxaSignupForm .fxa-signin button {
background-color: initial;
text-decoration: underline;
color: #FFF;
display: inline;
padding: 0;
width: auto; }
.fxaSignupForm .fxa-signin button:hover, .fxaSignupForm .fxa-signin button:focus, .fxaSignupForm .fxa-signin button:active {
background-color: initial; }
.fxaSignupForm form {
position: relative; }
.fxaSignupForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.fxaSignupForm button,
.fxaSignupForm input {
width: 100%; }
.fxaSignupForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.fxaSignupForm input:hover {
border-color: #0C0C0D; }
.fxaSignupForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.fxaSignupForm input.invalid {
border-color: #D70022; }
.fxaSignupForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.fxaSignupForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.fxaSignupForm button:hover, .fxaSignupForm button:focus {
background-color: #0250BB; }
.fxaSignupForm button:focus {
outline: dotted 1px; }
.fxaSignupForm button:active {
background-color: #054096; }
.trailhead {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
height: auto; }
.trailhead a {
color: #FFF;
text-decoration: underline; }
.trailhead input,
.trailhead button {
border-radius: 4px;
padding: 10px; }
.trailhead .trailheadInner {
display: grid;
grid-column-gap: 40px;
grid-template-columns: 5fr 3fr;
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; }
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
color: #FFF;
min-width: 260px;
padding-top: 100px; }
.trailhead.syncCohort {
left: calc(50% - 430px);
width: 860px; }
@media (max-width: 860px) {
.trailhead.syncCohort {
left: 0;
width: 100%; } }
.trailhead.syncCohort .trailheadInner {
grid-template-columns: 4fr 3fr; }
.trailhead.syncCohort .trailheadContent .trailheadBenefits {
background: url("chrome://activity-stream/content/data/content/assets/sync-devices-trailhead.svg");
background-position: center center;
background-repeat: no-repeat;
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 6px;
background-repeat: no-repeat;
background-size: 40px;
-moz-context-properties: fill;
fill: #0A84FF;
list-style: none;
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, .trailhead .trailheadBenefits li.monitor {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-knowledge.png"); }
.trailhead .trailheadBenefits li.lockwise, .trailhead .trailheadBenefits li.privacy {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-privacy.png"); }
.trailhead .trailheadBenefits li.products {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-products.png"); }
.trailhead .trailheadBenefits li.sync {
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/benefit-sync.png"); }
.trailhead .trailheadBenefits h2 {
text-align: start;
line-height: inherit;
color: #CB9EFF;
font-size: 22px;
font-weight: 400;
margin: 0 0 4px;
padding-inline-start: 52px; }
@media (min-width: 850px) {
.trailhead .trailheadBenefits h2 {
padding-inline-start: 0; } }
.trailhead .trailheadBenefits p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.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);
overflow: hidden;
@ -4239,39 +4572,171 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.EOYSnippetForm {
margin: 10px 0 8px;
align-self: start;
font-size: 14px;
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
align-content: center;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 8px 20px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
padding: 5px 14px;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm input[type='radio']:checked:focus + .donation-amount,
.EOYSnippetForm input[type='radio']:not(:checked):focus + .donation-amount {
border: 1px dotted var(--newtab-link-primary-color); }
.EOYSnippetForm .monthly-checkbox-container {
flex-direction: column;
overflow-x: auto;
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 21000;
background-color: #FAFAFC; }
.fullpage-wrapper + div {
opacity: 0; }
.fullpage-wrapper .fullpage-icon {
background-position-x: left;
background-repeat: no-repeat;
background-size: contain; }
.fullpage-wrapper .fullpage-icon:dir(rtl) {
background-position-x: right; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-icon {
background-position: center; } }
.fullpage-wrapper .brand-logo {
background-image: url("chrome://branding/content/about-logo.png");
margin: 20px 10px 10px 20px;
padding-bottom: 50px; }
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
align-self: center;
margin: 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .welcome-title,
.fullpage-wrapper .welcome-subtitle {
text-align: center; } }
.fullpage-wrapper .welcome-title {
color: #36296D;
font-size: 46px;
font-weight: 600;
line-height: 62px; }
.fullpage-wrapper .welcome-subtitle {
color: #7542E5;
font-size: 20px;
line-height: 27px; }
.fullpage-wrapper .container {
display: flex;
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
align-self: flex-end;
display: flex; }
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 300px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-left-section {
width: 300px; } }
.fullpage-wrapper .fullpage-left-section .fullpage-left-content {
color: #4A4A4F;
display: inline;
margin: 0;
margin-inline-end: 2px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link {
color: #0060DF;
display: block;
text-decoration: underline;
margin-bottom: 30px; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-link:hover, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:active, .fullpage-wrapper .fullpage-left-section .fullpage-left-link:focus {
color: #0060DF; }
.fullpage-wrapper .fullpage-left-section .fullpage-left-title {
margin: 0;
color: #36296D;
font-size: 36px;
line-height: 48px; }
.fullpage-wrapper .fullpage-left-section .fx-systems-icons {
height: 33px;
display: block;
background-image: url("chrome://activity-stream/content/data/content/assets/trailhead/firefox-systems.png");
margin-bottom: 20px; }
.fullpage-wrapper .fullpage-form {
position: relative;
text-align: center;
margin-inline-start: 36px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .fullpage-form {
margin-inline-start: 0; } }
.fullpage-wrapper .fullpage-form .fxaSignupForm {
width: 356px;
padding: 25px;
box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.15);
border-radius: 6px;
background: #FFF; }
.fullpage-wrapper .fullpage-form .fxa-terms {
margin: 4px 0 20px; }
.fullpage-wrapper .fullpage-form .fxa-terms a, .fullpage-wrapper .fullpage-form .fxa-terms {
color: #4A4A4F;
font-size: 12px;
line-height: 16px; }
.fullpage-wrapper .fullpage-form .fxa-signin {
color: #4A4A4F;
line-height: 30px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form .fxa-signin button {
color: #0060DF; }
.fullpage-wrapper .fullpage-form h3 {
color: #36296D;
font-weight: 400;
font-size: 36px;
line-height: 36px;
margin: 0;
padding: 8px; }
.fullpage-wrapper .fullpage-form h3 + p {
color: #4A4A4F;
font-size: 16px;
line-height: 20px;
opacity: 0.77; }
.fullpage-wrapper .fullpage-form input {
background: #FFF;
border: 1px solid #D7D7DB;
border-radius: 2px; }
.fullpage-wrapper .fullpage-form input:hover {
border-color: #737373; }
.fullpage-wrapper .fullpage-form input.invalid {
border-color: #D70022; }
.fullpage-wrapper .fullpage-form button {
color: #FFF;
font-size: 16px; }
.fullpage-wrapper .fullpage-form button:focus {
outline: dotted 1px #737373; }
.fullpage-wrapper .section-divider::after {
content: '';
display: block;
border-bottom: 0.5px solid #D7D7DB; }
.fullpage-wrapper .trailheadCard {
box-shadow: none;
background: none;
text-align: center;
width: 320px;
padding: 18px; }
.fullpage-wrapper .trailheadCard .onboardingTitle {
color: #0C0C0D; }
.fullpage-wrapper .trailheadCard .onboardingText {
font-weight: normal;
color: #4A4A4F;
margin-top: 4px; }
.fullpage-wrapper .trailheadCard .onboardingButton {
color: #4A4A4F;
background: rgba(12, 12, 13, 0.1); }
.fullpage-wrapper .trailheadCard .onboardingButton:focus, .fullpage-wrapper .trailheadCard .onboardingButton:hover {
background: rgba(12, 12, 13, 0.2); }
.fullpage-wrapper .trailheadCard .onboardingButton:active {
background: rgba(12, 12, 13, 0.3); }
.fullpage-wrapper .trailheadCard .onboardingMessageImage {
height: 112px;
width: 154px; }
@media screen and (max-width: 975px) {
.fullpage-wrapper .trailheadCard {
width: 300px; } }

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 23 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.2 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.1 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 30 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 17 KiB

Просмотреть файл

@ -1353,6 +1353,23 @@ This reports a failure in the Remote Settings loader to load messages for Activi
}
```
## Trailhead experiment enrollment ping
This reports an enrollment ping when a user gets enrolled in a Trailhead experiment. Note that this ping is only collected through the Mozilla Events telemetry pipeline.
```js
{
"category": "activity_stream",
"method": "enroll",
"object": "preference_study"
"value": "activity-stream-firstup-trailhead-interrupts",
"extra_keys": {
"experimentType": "as-firstrun",
"branch": ["supercharge" | "join" | "sync" | "privacy" ...]
}
}
```
## Feature Callouts interaction pings
This reports when a user has seen or clicked a badge/notification in the browser toolbar in a non-PBM window

Просмотреть файл

@ -549,6 +549,7 @@ class _ASRouter {
providers: [],
messageBlockList: [],
messageImpressions: {},
trailheadInitialized: false,
messages: [],
groups: [],
errors: [],
@ -1037,7 +1038,7 @@ class _ASRouter {
ASRouterTargeting.Environment,
this._getMessagesContext()
),
trailheadTriplet: ASRouterPreferences.trailheadTriplet,
trailhead: ASRouterPreferences.trailhead,
errors: this.errors,
},
});
@ -1056,6 +1057,18 @@ class _ASRouter {
}
}
async setTrailHeadMessageSeen() {
if (!this.state.trailheadInitialized) {
Services.prefs.setBoolPref(
TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,
true
);
await this.setState({
trailheadInitialized: true,
});
}
}
// Return an object containing targeting parameters used to select messages
_getMessagesContext() {
const { messageImpressions, previousSessionEnd } = this.state;
@ -1214,7 +1227,9 @@ class _ASRouter {
// This is used to determine whether to block when action is triggered
// Only block for dynamic triplets experiment and when there are more messages available
blockOnClick:
ASRouterPreferences.trailheadTriplet.startsWith("dynamic") &&
ASRouterPreferences.trailhead.trailheadTriplet.startsWith(
"dynamic"
) &&
allMessages.length >
TRAILHEAD_CONFIG.DYNAMIC_TRIPLET_BUNDLE_LENGTH,
}))
@ -1350,7 +1365,8 @@ class _ASRouter {
type: "SET_MESSAGE",
data: {
...message,
trailheadTriplet: ASRouterPreferences.trailheadTriplet || "",
trailheadTriplet:
ASRouterPreferences.trailhead.trailheadTriplet || "",
bundle: bundledMessages && bundledMessages.bundle,
},
});
@ -1884,6 +1900,11 @@ class _ASRouter {
async sendTriggerMessage(target, trigger) {
await this.loadMessagesFromAllProviders();
if (trigger.id === "firstRun") {
// On about welcome, set trailhead message seen on receiving firstrun trigger
await this.setTrailHeadMessageSeen();
}
const telemetryObject = { port: target.portID };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
// Return all the messages so that it can record the Reach event

Просмотреть файл

@ -13,13 +13,25 @@ const PROVIDER_PREF_BRANCH =
const DEVTOOLS_PREF =
"browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
const FXA_USERNAME_PREF = "services.sync.username";
const FIRST_RUN_TRIPLET_PREF = "trailhead.firstrun.newtab.triplets";
const FIRST_RUN_PREF = "trailhead.firstrun.branches";
const DEFAULT_FIRSTRUN_TRIPLET = "supercharge";
const DEFAULT_FIRSTRUN_INTERRUPT = "join";
function getTrailheadConfigFromPref(value) {
let [interrupt, triplet] = value.split("-");
return {
trailheadInterrupt: interrupt || DEFAULT_FIRSTRUN_INTERRUPT,
trailheadTriplet: triplet || DEFAULT_FIRSTRUN_TRIPLET,
};
}
XPCOMUtils.defineLazyPreferenceGetter(
this,
"trailheadTripletPref",
FIRST_RUN_TRIPLET_PREF,
""
"trailheadPrefs",
FIRST_RUN_PREF,
"",
null,
getTrailheadConfigFromPref
);
const DEFAULT_STATE = {
@ -107,8 +119,8 @@ class _ASRouterPreferences {
}
// istanbul ignore next
get trailheadTriplet() {
return trailheadTripletPref;
get trailhead() {
return trailheadPrefs;
}
get providers() {
@ -232,10 +244,12 @@ this._ASRouterPreferences = _ASRouterPreferences;
this.ASRouterPreferences = new _ASRouterPreferences();
this.TEST_PROVIDERS = TEST_PROVIDERS;
this.TARGETING_PREFERENCES = TARGETING_PREFERENCES;
this.getTrailheadConfigFromPref = getTrailheadConfigFromPref;
const EXPORTED_SYMBOLS = [
"_ASRouterPreferences",
"ASRouterPreferences",
"TEST_PROVIDERS",
"TARGETING_PREFERENCES",
"getTrailheadConfigFromPref",
];

Просмотреть файл

@ -420,8 +420,11 @@ const TargetingGetters = {
get isFxAEnabled() {
return isFxAEnabled;
},
get trailheadInterrupt() {
return ASRouterPreferences.trailhead.trailheadInterrupt;
},
get trailheadTriplet() {
return ASRouterPreferences.trailheadTriplet;
return ASRouterPreferences.trailhead.trailheadTriplet;
},
get sync() {
return {

Просмотреть файл

@ -3,6 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals Localization */
ChromeUtils.defineModuleGetter(
this,
"AttributionCode",
"resource:///modules/AttributionCode.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AddonRepository",
"resource://gre/modules/addons/AddonRepository.jsm"
);
const { FX_MONITOR_OAUTH_CLIENT_ID } = ChromeUtils.import(
"resource://gre/modules/FxAccountsCommon.js"
);
@ -15,7 +25,146 @@ const L10N = new Localization([
"browser/newtab/onboarding.ftl",
]);
const TRAILHEAD_ONBOARDING_TEMPLATE = {
trigger: { id: "firstRun" },
template: "trailhead",
includeBundle: {
length: 3,
template: "onboarding",
trigger: { id: "showOnboarding" },
},
};
const TRAILHEAD_FULL_PAGE_CONTENT = {
title: { string_id: "onboarding-welcome-body" },
learn: {
text: { string_id: "onboarding-welcome-learn-more" },
url: "https://www.mozilla.org/firefox/accounts/",
},
form: {
title: { string_id: "onboarding-welcome-form-header" },
text: { string_id: "onboarding-join-form-body" },
email: { string_id: "onboarding-fullpage-form-email" },
button: { string_id: "onboarding-join-form-continue" },
},
};
const DEFAULT_WELCOME_CONTENT = {
className: "welcomeCohort",
benefits: ["sync", "monitor", "lockwise"].map(id => ({
id,
title: { string_id: `onboarding-benefit-${id}-title` },
text: { string_id: `onboarding-benefit-${id}-text` },
})),
learn: {
text: { string_id: "onboarding-welcome-modal-family-learn-more" },
url: "https://www.mozilla.org/firefox/accounts/",
},
form: {
title: { string_id: "onboarding-welcome-form-header" },
text: { string_id: "onboarding-join-form-body" },
email: { string_id: "onboarding-join-form-email" },
button: { string_id: "onboarding-join-form-continue" },
},
skipButton: { string_id: "onboarding-start-browsing-button-label" },
};
const ONBOARDING_MESSAGES = () => [
{
id: "TRAILHEAD_1",
utm_term: "trailhead-join",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
...DEFAULT_WELCOME_CONTENT,
title: { string_id: "onboarding-welcome-body" },
},
},
{
id: "TRAILHEAD_2",
targeting: "trailheadInterrupt == 'sync'",
utm_term: "trailhead-sync",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
className: "syncCohort",
title: { string_id: "onboarding-sync-welcome-header" },
subtitle: { string_id: "onboarding-sync-welcome-content" },
benefits: [],
learn: {
text: { string_id: "onboarding-sync-welcome-learn-more-link" },
url: "https://www.mozilla.org/firefox/accounts/",
},
form: {
title: { string_id: "onboarding-sync-form-header" },
text: { string_id: "onboarding-sync-form-sub-header" },
email: { string_id: "onboarding-sync-form-input" },
button: { string_id: "onboarding-sync-form-continue-button" },
},
skipButton: { string_id: "onboarding-sync-form-skip-login-button" },
},
},
{
id: "TRAILHEAD_3",
targeting: "trailheadInterrupt == 'cards'",
utm_term: "trailhead-cards",
...TRAILHEAD_ONBOARDING_TEMPLATE,
},
{
id: "TRAILHEAD_4",
template: "trailhead",
targeting: "trailheadInterrupt == 'nofirstrun'",
trigger: { id: "firstRun" },
},
{
id: "TRAILHEAD_6",
targeting: "trailheadInterrupt == 'modal_variant_a'",
utm_term: "trailhead-modal_variant_a",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
...DEFAULT_WELCOME_CONTENT,
title: { string_id: "onboarding-welcome-modal-get-body" },
},
},
{
id: "TRAILHEAD_7",
targeting: "trailheadInterrupt == 'modal_variant_b'",
utm_term: "trailhead-modal_variant_b",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
...DEFAULT_WELCOME_CONTENT,
title: { string_id: "onboarding-welcome-modal-supercharge-body" },
},
},
{
id: "TRAILHEAD_8",
targeting: "trailheadInterrupt == 'modal_variant_c'",
utm_term: "trailhead-modal_variant_c",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
...DEFAULT_WELCOME_CONTENT,
title: { string_id: "onboarding-welcome-modal-privacy-body" },
},
},
{
id: "FULL_PAGE_1",
targeting: "trailheadInterrupt == 'full_page_d'",
utm_term: "trailhead-full_page_d",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
...TRAILHEAD_FULL_PAGE_CONTENT,
},
template: "full_page_interrupt",
},
{
id: "FULL_PAGE_2",
targeting: "trailheadInterrupt == 'full_page_e'",
utm_term: "trailhead-full_page_e",
...TRAILHEAD_ONBOARDING_TEMPLATE,
content: {
className: "fullPageCardsAtTop",
...TRAILHEAD_FULL_PAGE_CONTENT,
},
template: "full_page_interrupt",
},
{
id: "EXTENDED_TRIPLETS_1",
template: "extended_triplets",
@ -299,6 +448,38 @@ const ONBOARDING_MESSAGES = () => [
targeting: "trailheadTriplet == 'privacy'",
trigger: { id: "showOnboarding" },
},
{
id: "RETURN_TO_AMO_1",
template: "return_to_amo_overlay",
content: {
header: { string_id: "onboarding-welcome-header" },
title: { string_id: "return-to-amo-sub-header" },
addon_icon: null,
icon: "gift-extension",
text: {
string_id: "return-to-amo-addon-header",
args: { "addon-name": null },
},
primary_button: {
label: { string_id: "return-to-amo-extension-button" },
action: {
type: "INSTALL_ADDON_FROM_URL",
data: { url: null, telemetrySource: "rtamo" },
},
},
secondary_button: {
label: { string_id: "return-to-amo-get-started-button" },
},
},
includeBundle: {
length: 3,
template: "onboarding",
trigger: { id: "showOnboarding" },
},
targeting:
"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
trigger: { id: "firstRun" },
},
{
id: "FXA_ACCOUNTS_BADGE",
template: "toolbar_badge",
@ -354,6 +535,40 @@ const OnboardingMessageProvider = {
continue;
}
// We need some addon info if we are showing return to amo overlay, so fetch
// that, and update the message accordingly
if (msg.template === "return_to_amo_overlay") {
try {
const { name, iconURL, url } = await this.getAddonInfo();
// If we do not have all the data from the AMO api to indicate to the user
// what they are installing we don't want to show the message
if (!name || !iconURL || !url) {
continue;
}
msg.content.text.args["addon-name"] = name;
msg.content.addon_icon = iconURL;
msg.content.primary_button.action.data.url = url;
} catch (e) {
continue;
}
// We know we want to show this message, so translate message strings
const [
primary_button_string,
title_string,
text_string,
] = await L10N.formatMessages([
{ id: msg.content.primary_button.label.string_id },
{ id: msg.content.title.string_id },
{ id: msg.content.text.string_id, args: msg.content.text.args },
]);
translatedMessage.content.primary_button.label =
primary_button_string.value;
translatedMessage.content.title = title_string.value;
translatedMessage.content.text = text_string.value;
}
// Translate any secondary buttons separately
if (msg.content.secondary_button) {
const [secondary_button_string] = await L10N.formatMessages([
@ -372,6 +587,40 @@ const OnboardingMessageProvider = {
}
return translatedMessages;
},
async getAddonInfo() {
try {
let { content, source } = await AttributionCode.getAttrDataAsync();
if (!content || source !== "addons.mozilla.org") {
return null;
}
// Attribution data can be double encoded
while (content.includes("%")) {
try {
const result = decodeURIComponent(content);
if (result === content) {
break;
}
content = result;
} catch (e) {
break;
}
}
const [addon] = await AddonRepository.getAddonsByIDs([content]);
if (addon.sourceURI.scheme !== "https") {
return null;
}
return {
name: addon.name,
url: addon.sourceURI.spec,
iconURL: addon.icons["64"] || addon.icons["32"],
};
} catch (e) {
Cu.reportError(
"Failed to get the latest add-on version for Return to AMO"
);
return null;
}
},
};
this.OnboardingMessageProvider = OnboardingMessageProvider;

Просмотреть файл

@ -845,6 +845,14 @@ this.TelemetryFeed = class TelemetryFeed {
this.sendEvent(this.createUndesiredEvent(action));
}
handleTrailheadEnrollEvent(action) {
// Unlike `sendUTEvent`, we always send the event if AS's telemetry is enabled
// regardless of `this.eventTelemetryEnabled`.
if (this.telemetryEnabled) {
this.utEvents.sendTrailheadEnrollEvent(action.data);
}
}
async sendPageTakeoverData() {
if (this.telemetryEnabled) {
const value = {};
@ -958,6 +966,9 @@ this.TelemetryFeed = class TelemetryFeed {
case at.TELEMETRY_PERFORMANCE_EVENT:
this.sendEvent(this.createPerformanceEvent(action));
break;
case at.TRAILHEAD_ENROLL_EVENT:
this.handleTrailheadEnrollEvent(action);
break;
case at.UNINIT:
this.uninit();
break;

Просмотреть файл

@ -23,6 +23,7 @@ this.UTEventReporting = class UTEventReporting {
Services.telemetry.setEventRecordingEnabled("activity_stream", true);
this.sendUserEvent = this.sendUserEvent.bind(this);
this.sendSessionEndEvent = this.sendSessionEndEvent.bind(this);
this.sendTrailheadEnrollEvent = this.sendTrailheadEnrollEvent.bind(this);
}
_createExtras(data) {
@ -60,6 +61,19 @@ this.UTEventReporting = class UTEventReporting {
);
}
sendTrailheadEnrollEvent(data) {
Services.telemetry.recordEvent(
"activity_stream",
"enroll",
"preference_study",
data.experiment,
{
experimentType: data.type,
branch: data.branch,
}
);
}
uninit() {
Services.telemetry.setEventRecordingEnabled("activity_stream", false);
}

Просмотреть файл

@ -14,6 +14,7 @@ prefs =
browser.newtabpage.activity-stream.feeds.section.topstories=true
browser.newtabpage.activity-stream.feeds.section.topstories.options={"provider_name":""}
[browser_aboutwelcome.js]
[browser_aboutwelcome_actors.js]
[browser_aboutwelcome_simplified.js]
[browser_aboutwelcome_multistage.js]
@ -29,6 +30,7 @@ prefs =
[browser_discovery_card.js]
[browser_getScreenshots.js]
[browser_newtab_overrides.js]
[browser_onboarding_rtamo.js]
skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
[browser_topsites_contextMenu_options.js]
[browser_topsites_section.js]

Просмотреть файл

@ -0,0 +1,209 @@
"use strict";
const { ASRouter } = ChromeUtils.import(
"resource://activity-stream/lib/ASRouter.jsm"
);
const BRANCH_PREF = "trailhead.firstrun.branches";
const SIMPLIFIED_WELCOME_ENABLED_PREF = "browser.aboutwelcome.enabled";
/**
* Sets the trailhead branch pref to the passed value.
*/
async function setTrailheadBranch(value) {
Services.prefs.setCharPref(BRANCH_PREF, value);
// Set about:welcome to use trailhead flow
Services.prefs.setBoolPref(SIMPLIFIED_WELCOME_ENABLED_PREF, false);
// Reset trailhead so it loads the new branch.
Services.prefs.clearUserPref("trailhead.firstrun.didSeeAboutWelcome");
await ASRouter.setState({ trailheadInitialized: false });
registerCleanupFunction(() => {
Services.prefs.clearUserPref(BRANCH_PREF);
Services.prefs.clearUserPref(SIMPLIFIED_WELCOME_ENABLED_PREF);
});
}
/**
* Test a specific trailhead branch.
*/
async function test_trailhead_branch(
branchName,
expectedSelectors = [],
unexpectedSelectors = []
) {
await setTrailheadBranch(branchName);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:welcome",
false
);
let browser = tab.linkedBrowser;
await ContentTask.spawn(
browser,
{ expectedSelectors, branchName, unexpectedSelectors },
async ({
expectedSelectors: expected,
branchName: branch,
unexpectedSelectors: unexpected,
}) => {
for (let selector of expected) {
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector(selector),
`Should render ${selector} in the ${branch} branch`
);
}
for (let selector of unexpected) {
ok(
!content.document.querySelector(selector),
`Should not render ${selector} in the ${branch} branch`
);
}
}
);
BrowserTestUtils.removeTab(tab);
}
/**
* Test the the various trailhead branches.
*/
add_task(async function test_trailhead_branches() {
await test_trailhead_branch(
"join-dynamic",
// Expected selectors:
[
".trailhead.welcomeCohort",
"button[data-l10n-id=onboarding-data-sync-button2]",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
"button[data-l10n-id=onboarding-browse-privately-button]",
]
);
// Validate sync card is not shown if user usesFirefoxSync
await pushPrefs(["services.sync.username", "someone@foo.com"]);
await test_trailhead_branch(
"join-dynamic",
// Expected selectors:
[
".trailhead.welcomeCohort",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
"button[data-l10n-id=onboarding-browse-privately-button]",
],
// Unexpected selectors:
["button[data-l10n-id=onboarding-data-sync-button2]"]
);
// Validate multidevice card is not shown if user has mobile devices connected
await pushPrefs(["services.sync.clients.devices.mobile", 1]);
await test_trailhead_branch(
"join-dynamic",
// Expected selectors:
[
".trailhead.welcomeCohort",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
],
// Unexpected selectors:
["button[data-l10n-id=onboarding-mobile-phone-button"]
);
await test_trailhead_branch(
"sync-supercharge",
// Expected selectors:
[
".trailhead.syncCohort",
"button[data-l10n-id=onboarding-data-sync-button2]",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
"button[data-l10n-id=onboarding-mobile-phone-button]",
]
);
await test_trailhead_branch(
"modal_variant_a-supercharge",
// Expected selectors:
[
".trailhead.welcomeCohort",
"p[data-l10n-id=onboarding-benefit-sync-text]",
"p[data-l10n-id=onboarding-benefit-monitor-text]",
"p[data-l10n-id=onboarding-benefit-lockwise-text]",
]
);
await test_trailhead_branch(
"modal_variant_f-supercharge",
// Expected selectors:
[
".trailhead.welcomeCohort",
"h3[data-l10n-id=onboarding-welcome-form-header]",
"p[data-l10n-id=onboarding-benefit-sync-text]",
"p[data-l10n-id=onboarding-benefit-monitor-text]",
"p[data-l10n-id=onboarding-benefit-lockwise-text]",
"button[data-l10n-id=onboarding-join-form-signin]",
]
);
await test_trailhead_branch(
"join-supercharge",
// Expected selectors:
[
".trailhead.welcomeCohort",
"h3[data-l10n-id=onboarding-welcome-form-header]",
"p[data-l10n-id=onboarding-benefit-sync-text]",
"p[data-l10n-id=onboarding-benefit-monitor-text]",
"p[data-l10n-id=onboarding-benefit-lockwise-text]",
"button[data-l10n-id=onboarding-join-form-signin]",
]
);
await test_trailhead_branch(
"full_page_d-supercharge",
// Expected selectors:
[
".trailhead-fullpage",
".trailheadCard",
"p[data-l10n-id=onboarding-benefit-products-text]",
"button[data-l10n-id=onboarding-join-form-continue]",
"button[data-l10n-id=onboarding-join-form-signin]",
]
);
await test_trailhead_branch(
"full_page_e-supercharge",
// Expected selectors:
[
".fullPageCardsAtTop",
".trailhead-fullpage",
".trailheadCard",
"p[data-l10n-id=onboarding-benefit-products-text]",
"button[data-l10n-id=onboarding-join-form-continue]",
"button[data-l10n-id=onboarding-join-form-signin]",
]
);
await test_trailhead_branch(
"nofirstrun",
[],
// Unexpected selectors:
["#trailheadDialog", ".trailheadCards"]
);
// Test trailhead default join-supercharge branch renders
// correct when separate about welcome pref is false
await test_trailhead_branch(
"join-supercharge",
// Expected selectors:
[
".trailhead.welcomeCohort",
"h1[data-l10n-id=onboarding-welcome-header]",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
"h3[data-l10n-id=onboarding-welcome-form-header]",
"p[data-l10n-id=onboarding-benefit-sync-text]",
"p[data-l10n-id=onboarding-benefit-monitor-text]",
"p[data-l10n-id=onboarding-benefit-lockwise-text]",
],
["h2[data-l10n-id=onboarding-fullpage-welcome-subheader]"]
);
});

Просмотреть файл

@ -72,6 +72,17 @@ add_task(async function test_Separate_About_Welcome_branches() {
"button.secondary",
],
// Unexpected selectors:
[".trailhead.welcomeCohort", ".welcome-subtitle"]
[
".trailhead.welcomeCohort",
".welcome-subtitle",
"h3[data-l10n-id=onboarding-welcome-form-header]",
"p[data-l10n-id=onboarding-benefit-sync-text]",
"p[data-l10n-id=onboarding-benefit-monitor-text]",
"p[data-l10n-id=onboarding-benefit-lockwise-text]",
"h1[data-l10n-id=onboarding-welcome-header]",
"button[data-l10n-id=onboarding-data-sync-button2]",
"button[data-l10n-id=onboarding-firefox-monitor-button]",
"button[data-l10n-id=onboarding-browse-privately-button",
]
);
});

Просмотреть файл

@ -0,0 +1,113 @@
const { ASRouter } = ChromeUtils.import(
"resource://activity-stream/lib/ASRouter.jsm"
);
const { OnboardingMessageProvider } = ChromeUtils.import(
"resource://activity-stream/lib/OnboardingMessageProvider.jsm"
);
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
const BRANCH_PREF = "trailhead.firstrun.branches";
const SIMPLIFIED_WELCOME_ENABLED_PREF = "browser.aboutwelcome.enabled";
async function setRTAMOOnboarding() {
await ASRouter.forceAttribution({
campaign: "non-fx-button",
source: "addons.mozilla.org",
content: "iridium@particlecore.github.io",
});
AttributionCode._clearCache();
const data = await AttributionCode.getAttrDataAsync();
Assert.equal(
data.source,
"addons.mozilla.org",
"Attribution data should be set"
);
Services.prefs.setCharPref(BRANCH_PREF, "join-supercharge");
// Set about:welcome to use trailhead flow
Services.prefs.setBoolPref(SIMPLIFIED_WELCOME_ENABLED_PREF, false);
// Reset trailhead so it loads the new branch.
Services.prefs.clearUserPref("trailhead.firstrun.didSeeAboutWelcome");
await ASRouter.setState({ trailheadInitialized: false });
ASRouter._updateMessageProviders();
await ASRouter.loadMessagesFromAllProviders();
registerCleanupFunction(async () => {
// Separate cleanup methods between mac and windows
if (AppConstants.platform === "macosx") {
const { path } = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent;
const attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(path, "", true);
}
// Clear cache call is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
Services.prefs.clearUserPref(BRANCH_PREF);
Services.prefs.clearUserPref(SIMPLIFIED_WELCOME_ENABLED_PREF);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
});
}
add_task(async function setup() {
// Store it in order to restore to the original value
const { getAddonInfo } = OnboardingMessageProvider;
// Prevent fetching the real addon url and making a network request
OnboardingMessageProvider.getAddonInfo = () => ({
name: "mochitest_name",
iconURL: "mochitest_iconURL",
url: "https://example.com",
});
registerCleanupFunction(() => {
OnboardingMessageProvider.getAddonInfo = getAddonInfo;
});
});
add_task(async () => {
await setRTAMOOnboarding();
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:welcome" },
async browser => {
let modalText = await SpecialPowers.spawn(browser, [], async () => {
// Wait for Activity Stream to load
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector(".activity-stream"),
`Should render Activity Stream`
);
await ContentTaskUtils.waitForCondition(
() => content.document.body.classList.contains("welcome"),
"The modal setup should be completed"
);
await ContentTaskUtils.waitForCondition(
() => content.document.body.classList.contains("hide-main"),
"You shouldn't be able to see newtabpage content"
);
for (let selector of [
// ReturnToAMO elements
".ReturnToAMOOverlay",
".ReturnToAMOContainer",
".ReturnToAMOAddonContents",
".ReturnToAMOIcon",
]) {
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector(selector) !== null,
`Should render ${selector}`
);
}
return content.document.querySelector(".ReturnToAMOText").innerText;
});
// Make sure strings are properly shown
Assert.equal(modalText, "Now lets get you mochitest_name.");
}
);
});

Просмотреть файл

@ -323,6 +323,27 @@ export const UTSessionPing = Joi.array().items(
eventsTelemetryExtraKeys
);
export const trailheadEnrollExtraKeys = Joi.object()
.keys({
experimentType: Joi.string().required(),
branch: Joi.string().required(),
})
.options({ allowUnknown: false });
export const UTTrailheadEnrollPing = Joi.array().items(
Joi.string()
.required()
.valid("activity_stream"),
Joi.string()
.required()
.valid("enroll"),
Joi.string()
.required()
.valid("preference_study"),
Joi.string().required(),
trailheadEnrollExtraKeys
);
export function chaiAssertions(_chai, utils) {
const { Assertion } = _chai;

Просмотреть файл

@ -121,8 +121,8 @@ describe("ASRouter", () => {
sandbox.spy(ASRouterPreferences, "uninit");
sandbox.spy(ASRouterPreferences, "addListener");
sandbox.spy(ASRouterPreferences, "removeListener");
sandbox.stub(ASRouterPreferences, "trailheadTriplet").get(() => {
return "test";
sandbox.stub(ASRouterPreferences, "trailhead").get(() => {
return { trailheadTriplet: "test" };
});
sandbox.replaceGetter(
ASRouterPreferences,
@ -583,7 +583,7 @@ describe("ASRouter", () => {
providerPrefs: ASRouterPreferences.providers,
userPrefs: ASRouterPreferences.getAllUserPreferences(),
targetingParameters: {},
trailheadTriplet: ASRouterPreferences.trailheadTriplet,
trailhead: ASRouterPreferences.trailhead,
errors: Router.errors,
}),
});
@ -2111,7 +2111,7 @@ describe("ASRouter", () => {
providerPrefs: ASRouterPreferences.providers,
userPrefs: ASRouterPreferences.getAllUserPreferences(),
targetingParameters: {},
trailheadTriplet: ASRouterPreferences.trailheadTriplet,
trailhead: ASRouterPreferences.trailhead,
errors: Router.errors,
}),
});
@ -2524,13 +2524,12 @@ describe("ASRouter", () => {
});
it("should set blockOnClick property true for dynamic triplet and matching messages more than 3", async () => {
sandbox.replaceGetter(
ASRouterPreferences,
"trailheadTriplet",
function() {
return "dynamic";
}
);
sandbox.replaceGetter(ASRouterPreferences, "trailhead", function() {
return {
trailheadInterrupt: "join",
trailheadTriplet: "dynamic",
};
});
await Router.onMessage(msg);
const [, resp] = msg.target.sendAsyncMessage.firstCall.args;
const expectedBundle = [
@ -2560,13 +2559,12 @@ describe("ASRouter", () => {
});
it("should set blockOnClick property true for triplet branch name that starts with 'dynamic' and matching messages more than 3", async () => {
sandbox.replaceGetter(
ASRouterPreferences,
"trailheadTriplet",
function() {
return "dynamic_test";
}
);
sandbox.replaceGetter(ASRouterPreferences, "trailhead", function() {
return {
trailheadInterrupt: "join",
trailheadTriplet: "dynamic_test",
};
});
await Router.onMessage(msg);
const [, resp] = msg.target.sendAsyncMessage.firstCall.args;
const expectedBundle = [

Просмотреть файл

@ -1,6 +1,7 @@
import {
_ASRouterPreferences,
ASRouterPreferences as ASRouterPreferencesSingleton,
getTrailheadConfigFromPref,
TEST_PROVIDERS,
} from "lib/ASRouterPreferences.jsm";
const FAKE_PROVIDERS = [{ id: "foo" }, { id: "bar" }];
@ -374,4 +375,26 @@ describe("ASRouterPreferences", () => {
);
});
});
describe("#getTrailheadConfigFromPref", () => {
it("should return trailHeadTriplet and trailHeadInterrupt", () => {
let result = getTrailheadConfigFromPref("foo-bar");
assert.propertyVal(result, "trailheadInterrupt", "foo");
assert.propertyVal(result, "trailheadTriplet", "bar");
});
it("should return default values when pref is empty", () => {
let result = getTrailheadConfigFromPref("");
assert.propertyVal(result, "trailheadInterrupt", "join");
assert.propertyVal(result, "trailheadTriplet", "supercharge");
});
it("should return default trailHeadTriplet and trailHeadInterrupt when no hyphen", () => {
let result = getTrailheadConfigFromPref("control");
assert.propertyVal(result, "trailheadInterrupt", "control");
assert.propertyVal(result, "trailheadTriplet", "supercharge");
});
it("should return trailHeadTriplet and default trailHeadInterrupt when prefixed with hyphen", () => {
let result = getTrailheadConfigFromPref("-control");
assert.propertyVal(result, "trailheadInterrupt", "join");
assert.propertyVal(result, "trailheadTriplet", "control");
});
});
});

Просмотреть файл

@ -82,6 +82,32 @@ describe("#CachedTargetingGetter", () => {
assert(rejected);
});
it("should check targeted message before message without targeting", async () => {
const messages = await OnboardingMessageProvider.getUntranslatedMessages();
const stub = sandbox
.stub(ASRouterTargeting, "checkMessageTargeting")
.resolves();
const context = {
attributionData: {
campaign: "non-fx-button",
source: "addons.mozilla.org",
},
};
await ASRouterTargeting.findMatchingMessage({
messages,
trigger: { id: "firstRun" },
context,
});
const messageCount = messages.filter(
message => message.trigger && message.trigger.id === "firstRun"
).length;
assert.equal(stub.callCount, messageCount);
const calls = stub.getCalls().map(call => call.args[0]);
const lastCall = calls[calls.length - 1];
assert.equal(lastCall.id, "TRAILHEAD_1");
});
describe("sortMessagesByPriority", () => {
it("should sort messages in descending priority order", async () => {
const [

Просмотреть файл

@ -4,7 +4,11 @@ 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", "trailheadTriplet"];
const MESSAGE_CONTEXT_ATTRIBUTES = [
"previousSessionEnd",
"trailheadInterrupt",
"trailheadTriplet",
];
function getHeadingsFromDocs() {
const re = /### `(\w+)`/g;

Просмотреть файл

@ -5,8 +5,11 @@ import {
import { GlobalOverrider } from "test/unit/utils";
import { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from "content-src/lib/init-store";
import { FAKE_LOCAL_MESSAGES } from "./constants";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import React from "react";
import { mount } from "enzyme";
import { Trailhead } from "../../../content-src/asrouter/templates/Trailhead/Trailhead";
import { Triplets } from "../../../content-src/asrouter/templates/FirstRun/Triplets";
import { actionCreators as ac } from "common/Actions.jsm";
let [FAKE_MESSAGE] = FAKE_LOCAL_MESSAGES;
@ -174,6 +177,18 @@ describe("ASRouterUISurface", () => {
assert.equal(footerPortal.childElementCount, 0);
});
it("should render a trailhead message in the header portal", async () => {
// wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.template === "trailhead");
wrapper.setState({ message });
assert.isTrue(headerPortal.childElementCount > 0);
assert.equal(footerPortal.childElementCount, 0);
});
it("should dispatch an event to select the correct theme", () => {
const stub = sandbox.stub(window, "dispatchEvent");
sandbox
@ -222,7 +237,42 @@ describe("ASRouterUISurface", () => {
});
});
describe("Triplet bundle Card", () => {
describe("trailhead", () => {
it("should render trailhead if a trailhead message is received", async () => {
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.template === "trailhead");
wrapper.setState({ message });
assert.lengthOf(wrapper.find(Trailhead), 1);
});
it("should render Triplets if a trailhead message with bundle is received", async () => {
const FAKE_TRIPLETS_BUNDLE = [
{
id: "test",
content: {
title: { string_id: "foo" },
text: { string_id: "text1" },
icon: "icon",
primary_button: {
label: { string_id: "button1" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },
},
},
},
},
];
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.template === "trailhead");
wrapper.setState({
message: { ...message, bundle: FAKE_TRIPLETS_BUNDLE },
});
assert.lengthOf(wrapper.find(Triplets), 1);
});
it("should send NEW_TAB_MESSAGE_REQUEST if a bundle card id is blocked or cleared", async () => {
sandbox.stub(ASRouterUtils, "sendMessage");
const FAKE_TRIPLETS_BUNDLE_1 = [
@ -242,8 +292,11 @@ describe("ASRouterUISurface", () => {
},
},
];
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.id === "TRAILHEAD_1");
wrapper.setState({
message: { bundle: FAKE_TRIPLETS_BUNDLE_1 },
message: { ...message, bundle: FAKE_TRIPLETS_BUNDLE_1 },
});
wrapper.instance().clearMessage("CARD_1");

Просмотреть файл

@ -2,6 +2,7 @@ import {
FirstRun,
FLUENT_FILES,
} from "content-src/asrouter/templates/FirstRun/FirstRun";
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import { mount } from "enzyme";
@ -72,7 +73,7 @@ describe("<FirstRun>", () => {
async function setup() {
sandbox = sinon.createSandbox();
clock = sandbox.useFakeTimers();
message = await getTestMessage("EXTENDED_TRIPLETS_1");
message = await getTestMessage("TRAILHEAD_1");
fakeDoc = {
body: document.createElement("body"),
head: document.createElement("head"),
@ -110,21 +111,47 @@ describe("<FirstRun>", () => {
it("should render", () => {
assert.ok(wrapper);
});
describe("with triplets", () => {
it("should render triplets", () => {
describe("with both interrupt and triplets", () => {
it("should render interrupt and triplets", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
});
it("should show the card panel and the content on the Triplets", () => {
it("should show the card panel and hide the content on the Triplets", () => {
// This is so the container shows up in the background but we can fade in the content when intterupt is closed.
const tripletsProps = wrapper.find(Triplets).props();
assert.propertyVal(tripletsProps, "showCardPanel", true);
assert.propertyVal(tripletsProps, "showContent", false);
});
it("should set the UTM term to trailhead-cards-card (for the extended-triplet message)", () => {
it("should set the UTM term to trailhead-join (for the traihead-join message)", () => {
const iProps = wrapper.find(Interrupt).props();
const tProps = wrapper.find(Triplets).props();
assert.propertyVal(tProps, "UTMTerm", "trailhead-cards-card");
assert.propertyVal(iProps, "UTMTerm", "trailhead-join");
assert.propertyVal(tProps, "UTMTerm", "trailhead-join-card");
});
});
describe("with no triplets", () => {
describe("with an interrupt but no triplets", () => {
beforeEach(() => {
message.bundle = []; // Empty triplets
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
});
it("should render interrupt but no triplets", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 0, "<Triplets>");
});
});
describe("with triplets but no interrupt", () => {
it("should render interrupt but no triplets", () => {
delete message.content; // Empty interrupt
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
assert.lengthOf(wrapper.find(Interrupt), 0, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
});
});
describe("with no triplets or interrupt", () => {
it("should render empty", () => {
message = { type: "FOO_123" };
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
@ -138,6 +165,8 @@ describe("<FirstRun>", () => {
wrapper = mount(
<FirstRun message={message} document={fakeDoc} executeAction={stub} />
);
assert.propertyVal(wrapper.find(Interrupt).props(), "executeAction", stub);
assert.propertyVal(wrapper.find(Triplets).props(), "onAction", stub);
});
@ -147,6 +176,7 @@ describe("<FirstRun>", () => {
<FirstRun
message={message}
document={fakeDoc}
dispatch={() => {}}
fetchFlowParams={stub}
fxaEndpoint="https://foo.com"
/>
@ -157,7 +187,12 @@ describe("<FirstRun>", () => {
it("should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined", () => {
const stub = sandbox.stub();
wrapper = mount(
<FirstRun message={message} document={fakeDoc} fetchFlowParams={stub} />
<FirstRun
message={message}
document={fakeDoc}
fetchFlowParams={stub}
dispatch={() => {}}
/>
);
assert.notCalled(stub);
wrapper.setProps({ fxaEndpoint: "https://foo.com" });
@ -170,6 +205,7 @@ describe("<FirstRun>", () => {
<FirstRun
message={message}
document={fakeDoc}
dispatch={() => {}}
fetchFlowParams={stub}
fxaEndpoint="https://foo.com"
/>
@ -183,6 +219,28 @@ describe("<FirstRun>", () => {
assert.lengthOf(fakeDoc.head.querySelectorAll("link"), FLUENT_FILES.length);
});
it("should show triplet content only when interrupt is not visible", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "Interrupt shown");
assert.propertyVal(wrapper.find(Triplets).props(), "showContent", false);
assert.propertyVal(wrapper.find(Triplets).props(), "showCardPanel", true);
wrapper.setProps({ interruptCleared: true });
assert.propertyVal(wrapper.find(Triplets).props(), "showContent", true);
assert.propertyVal(wrapper.find(Triplets).props(), "showCardPanel", true);
});
it("should update didUserClearInterrupt state to false on close of interrupt", () => {
assert.isFalse(wrapper.state().didUserClearInterrupt);
// Simulate calling close interrupt
wrapper
.find(Interrupt)
.find(".trailheadStart")
.simulate("click");
assert.isTrue(wrapper.state().didUserClearInterrupt);
});
it("should update didUserClearTriplets state to true on close of triplet", () => {
assert.isFalse(wrapper.state().didUserClearTriplets);
// Simulate calling close Triplets
@ -193,6 +251,30 @@ describe("<FirstRun>", () => {
assert.isTrue(wrapper.state().didUserClearTriplets);
});
it("should hide the interrupt and show the triplets when onNextScene is called", () => {
// Simulate calling next scene
wrapper
.find(Interrupt)
.find(".trailheadStart")
.simulate("click");
assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
assert.isTrue(
wrapper
.find(Triplets)
.find(".trailheadCardGrid")
.hasClass("show"),
"Show triplet content"
);
});
it("should hide the interrupt when props.interruptCleared changes to true", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "Interrupt shown");
wrapper.setProps({ interruptCleared: true });
assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
});
it("should hide triplets when closeTriplets is called and block extended triplets after 500ms", () => {
// Simulate calling next scene
wrapper

Просмотреть файл

@ -0,0 +1,139 @@
import { mount } from "enzyme";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import React from "react";
import {
FullPageInterrupt,
FxAccounts,
FxCards,
} from "content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt";
import { FxASignupForm } from "content-src/asrouter/components/FxASignupForm/FxASignupForm";
import { OnboardingCard } from "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage";
const CARDS = [
{
id: "CARD_1",
content: {
title: { string_id: "onboarding-private-browsing-title" },
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },
},
},
},
},
];
const FAKE_FLOW_PARAMS = {
deviceId: "foo",
flowId: "abc1",
flowBeginTime: 1234,
};
describe("<FullPageInterrupt>", () => {
let wrapper;
let dummyNode;
let dispatch;
let onBlock;
let sandbox;
let onAction;
let onBlockById;
let sendTelemetryStub;
beforeEach(async () => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
onBlock = sandbox.stub();
onAction = sandbox.stub();
onBlockById = sandbox.stub();
sendTelemetryStub = sandbox.stub();
dummyNode = document.createElement("body");
sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
const fakeDocument = {
getElementById() {
return dummyNode;
},
};
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.id === "FULL_PAGE_1");
wrapper = mount(
<FullPageInterrupt
message={message}
UTMTerm={message.utm_term}
fxaEndpoint="https://accounts.firefox.com/endpoint/"
dispatch={dispatch}
onBlock={onBlock}
onAction={onAction}
onBlockById={onBlockById}
cards={CARDS}
document={fakeDocument}
sendUserActionTelemetry={sendTelemetryStub}
flowParams={FAKE_FLOW_PARAMS}
/>
);
});
afterEach(() => {
sandbox.restore();
});
it("should trigger onBlock on removeOverlay", () => {
wrapper.instance().removeOverlay();
assert.calledOnce(onBlock);
});
it("should render Full Page interrupt with accounts and triplet cards section", () => {
assert.lengthOf(wrapper.find(FxAccounts), 1);
assert.lengthOf(wrapper.find(FxCards), 1);
});
it("should render FxASignupForm inside FxAccounts", () => {
assert.lengthOf(wrapper.find(FxASignupForm), 1);
});
it("should display learn more link on full page", () => {
assert.ok(wrapper.find("a.fullpage-left-link").exists());
});
it("should add utm_* query params to card actions and send the right ping when a card button is clicked", () => {
wrapper
.find(OnboardingCard)
.find("button.onboardingButton")
.simulate("click");
assert.calledOnce(onAction);
const url = onAction.firstCall.args[0].data.args;
assert.equal(
url,
"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-full_page_d"
);
assert.calledWith(sendTelemetryStub, {
event: "CLICK_BUTTON",
message_id: CARDS[0].id,
id: "TRAILHEAD",
});
});
it("should not call blockById by default when a card button is clicked", () => {
wrapper
.find(OnboardingCard)
.find("button.onboardingButton")
.simulate("click");
assert.notCalled(onBlockById);
});
it("should call blockById when blockOnClick on message is true", () => {
CARDS[0].blockOnClick = true;
wrapper
.find(OnboardingCard)
.find("button.onboardingButton")
.simulate("click");
assert.calledOnce(onBlockById);
assert.calledWith(onBlockById, CARDS[0].id);
});
});

Просмотреть файл

@ -0,0 +1,121 @@
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { FxASignupForm } from "content-src/asrouter/components/FxASignupForm/FxASignupForm";
import { mount } from "enzyme";
import React from "react";
describe("<FxASignupForm>", () => {
let wrapper;
let dummyNode;
let dispatch;
let onClose;
let sandbox;
const FAKE_FLOW_PARAMS = {
deviceId: "foo",
flowId: "abc1",
flowBeginTime: 1234,
};
const FAKE_MESSAGE_CONTENT = {
title: { string_id: "onboarding-welcome-body" },
learn: {
text: { string_id: "onboarding-welcome-learn-more" },
url: "https://www.mozilla.org/firefox/accounts/",
},
form: {
title: { string_id: "onboarding-welcome-form-header" },
text: { string_id: "onboarding-join-form-body" },
email: { string_id: "onboarding-fullpage-form-email" },
button: { string_id: "onboarding-join-form-continue" },
},
};
beforeEach(async () => {
sandbox = sinon.sandbox.create();
dispatch = sandbox.stub();
onClose = sandbox.stub();
dummyNode = document.createElement("body");
sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
const fakeDocument = {
getElementById() {
return dummyNode;
},
};
wrapper = mount(
<FxASignupForm
document={fakeDocument}
content={FAKE_MESSAGE_CONTENT}
dispatch={dispatch}
fxaEndpoint="https://accounts.firefox.com/endpoint/"
UTMTerm="test-utm-term"
flowParams={FAKE_FLOW_PARAMS}
onClose={onClose}
/>
);
});
afterEach(() => {
sandbox.restore();
});
it("should prevent submissions with no email", () => {
const form = wrapper.find("form");
const preventDefault = sandbox.stub();
form.simulate("submit", { preventDefault });
assert.calledOnce(preventDefault);
assert.notCalled(dispatch);
});
it("should not display signin link by default", () => {
assert.notOk(
wrapper
.find("button[data-l10n-id='onboarding-join-form-signin']")
.exists()
);
});
it("should display signin when showSignInLink is true", () => {
wrapper.setProps({ showSignInLink: true });
let signIn = wrapper.find(
"button[data-l10n-id='onboarding-join-form-signin']"
);
assert.exists(signIn);
});
it("should emit UserEvent SUBMIT_EMAIL when you submit a valid email", () => {
let form = wrapper.find("form");
assert.ok(form.exists());
form.getDOMNode().elements.email.value = "a@b.c";
form.simulate("submit");
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(
dispatch,
ac.UserEvent({
event: at.SUBMIT_EMAIL,
value: { has_flow_params: true },
})
);
});
it("should emit UserEvent SUBMIT_SIGNIN when submit with email disabled", () => {
let form = wrapper.find("form");
form.getDOMNode().elements.email.disabled = true;
form.simulate("submit");
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(
dispatch,
ac.UserEvent({
event: at.SUBMIT_SIGNIN,
value: { has_flow_params: true },
})
);
});
});

Просмотреть файл

@ -0,0 +1,39 @@
import { FullPageInterrupt } from "content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt";
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
import { Trailhead } from "content-src/asrouter/templates//Trailhead/Trailhead";
import { shallow } from "enzyme";
import React from "react";
describe("<Interrupt>", () => {
let wrapper;
it("should render Return TO AMO when the message has a template of return_to_amo_overlay", () => {
wrapper = shallow(
<Interrupt
message={{ id: "FOO", content: {}, template: "return_to_amo_overlay" }}
/>
);
assert.lengthOf(wrapper.find(ReturnToAMO), 1);
});
it("should render Trailhead when the message has a template of trailhead", () => {
wrapper = shallow(
<Interrupt message={{ id: "FOO", content: {}, template: "trailhead" }} />
);
assert.lengthOf(wrapper.find(Trailhead), 1);
});
it("should render Full Page interrupt when the message has a template of full_page_interrupt", () => {
wrapper = shallow(
<Interrupt
message={{ id: "FOO", content: {}, template: "full_page_interrupt" }}
/>
);
assert.lengthOf(wrapper.find(FullPageInterrupt), 1);
});
it("should throw an error if another type of message is dispatched", () => {
assert.throws(() => {
wrapper = shallow(
<Interrupt message={{ id: "FOO", template: "something" }} />
);
});
});
});

Просмотреть файл

@ -77,4 +77,220 @@ describe("OnboardingMessage", () => {
.filter(msg => msg.template === "whatsnew_panel_message")
.forEach(msg => assert.jsonSchema(msg.content, whatsNewSchema));
});
it("should decode the content field (double decoding)", async () => {
const fakeContent = "foo%2540bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const [
translatedMessage,
] = await OnboardingMessageProvider.translateMessages(msgs);
assert.propertyVal(
translatedMessage.content.text.args,
"addon-name",
"foo@bar.org"
);
});
it("should catch any decoding exceptions", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const [
translatedMessage,
] = await OnboardingMessageProvider.translateMessages(msgs);
assert.propertyVal(
translatedMessage.content.text.args,
"addon-name",
fakeContent
);
});
it("should ignore attribution from sources other than mozilla.org", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.allizom.org" }),
});
const [returnToAMOMsg] = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
assert.propertyVal(returnToAMOMsg.content.text.args, "addon-name", null);
});
it("should correctly add all addon information to the message after translation", async () => {
const fakeContent = "foo%2540bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const [
translatedMessage,
] = await OnboardingMessageProvider.translateMessages(msgs);
assert.propertyVal(
translatedMessage.content.text.args,
"addon-name",
"foo@bar.org"
);
assert.propertyVal(translatedMessage.content, "addon_icon", "icon");
assert.propertyVal(
translatedMessage.content.primary_button.action.data,
"url",
"foo"
);
assert.propertyVal(
translatedMessage.content.primary_button.action.data,
"telemetrySource",
"rtamo"
);
});
it("should skip return_to_amo_overlay if any addon fields are missing", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
globals.set("AddonRepository", {
getAddonsByIDs: ([content]) => [
{
name: content,
sourceURI: { spec: "foo", scheme: "https" },
icons: { 64: null },
},
],
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
assert.lengthOf(translatedMessages, 0);
});
it("should skip return_to_amo_overlay if any addon fields are missing", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
globals.set("AddonRepository", {
getAddonsByIDs: ([content]) => [
{
name: content,
sourceURI: { spec: null, scheme: "https" },
icons: { 64: "icon" },
},
],
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
assert.lengthOf(translatedMessages, 0);
});
it("should skip return_to_amo_overlay if any addon fields are missing", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
globals.set("AddonRepository", {
getAddonsByIDs: ([content]) => [
{
name: null,
sourceURI: { spec: "foo", scheme: "https" },
icons: { 64: "icon" },
},
],
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
assert.lengthOf(translatedMessages, 0);
});
it("should skip return_to_amo_overlay if addon scheme is not https", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox
.stub()
.resolves({ content: fakeContent, source: "addons.mozilla.org" }),
});
globals.set("AddonRepository", {
getAddonsByIDs: ([content]) => [
{
name: content,
sourceURI: { spec: "foo", scheme: "http" },
icons: { 64: "icon" },
},
],
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
assert.lengthOf(translatedMessages, 0);
});
it("should skip return_to_amo_overlay if getAddonInfo fails", async () => {
globals.set("AttributionCode", {
getAttrDataAsync: sandbox.stub().rejects(),
});
const msgs = (
await OnboardingMessageProvider.getUntranslatedMessages()
).filter(({ id }) => id === "RETURN_TO_AMO_1");
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
assert.lengthOf(translatedMessages, 0);
});
it("should catch any exceptions fetching the addon information", async () => {
const fakeContent = "foo%bar.org";
globals.set("AttributionCode", {
getAttrDataAsync: sandbox.stub().resolves({ content: fakeContent }),
});
globals.set("AddonRepository", {
getAddonsByIDs: sandbox.stub().rejects(),
});
const msgs = await OnboardingMessageProvider.getUntranslatedMessages();
const translatedMessages = await OnboardingMessageProvider.translateMessages(
msgs
);
const returnToAMOMsgs = translatedMessages.filter(
({ id }) => id === "RETURN_TO_AMO_1"
);
assert.lengthOf(translatedMessages, msgs.length - 1);
assert.lengthOf(returnToAMOMsgs, 0);
});
});

Просмотреть файл

@ -0,0 +1,141 @@
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { FxASignupForm } from "content-src/asrouter/components/FxASignupForm/FxASignupForm";
import { mount } from "enzyme";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import React from "react";
import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
export const CARDS = [
{
content: {
title: { string_id: "onboarding-private-browsing-title" },
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },
},
},
},
},
];
describe("<Trailhead>", () => {
let wrapper;
let dummyNode;
let dispatch;
let onAction;
let sandbox;
let onNextScene;
beforeEach(async () => {
sandbox = sinon.sandbox.create();
dispatch = sandbox.stub();
onAction = sandbox.stub();
onNextScene = sandbox.stub();
sandbox.stub(global, "fetch").resolves({
ok: true,
status: 200,
json: () => Promise.resolve({ flowId: 123, flowBeginTime: 456 }),
});
dummyNode = document.createElement("body");
sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
const fakeDocument = {
get activeElement() {
return dummyNode;
},
get body() {
return dummyNode;
},
getElementById() {
return dummyNode;
},
};
const message = (
await OnboardingMessageProvider.getUntranslatedMessages()
).find(msg => msg.id === "TRAILHEAD_1");
message.cards = CARDS;
wrapper = mount(
<Trailhead
message={message}
UTMTerm={message.utm_term}
fxaEndpoint="https://accounts.firefox.com/endpoint"
dispatch={dispatch}
onAction={onAction}
document={fakeDocument}
onNextScene={onNextScene}
/>
);
});
afterEach(() => {
sandbox.restore();
});
it("should render FxASignupForm with signup email", () => {
assert.lengthOf(wrapper.find(FxASignupForm), 1);
});
it("should emit UserEvent SKIPPED_SIGNIN and call nextScene when you click the start browsing button", () => {
let skipButton = wrapper.find(".trailheadStart");
assert.ok(skipButton.exists());
skipButton.simulate("click");
assert.calledOnce(onNextScene);
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(
dispatch,
ac.UserEvent({
event: at.SKIPPED_SIGNIN,
value: { has_flow_params: false },
})
);
});
it("should NOT emit UserEvent SKIPPED_SIGNIN when closeModal is triggered by visibilitychange event", () => {
wrapper.instance().closeModal({ type: "visibilitychange" });
assert.notCalled(dispatch);
});
it("should prevent submissions with no email", () => {
const form = wrapper.find("form");
const preventDefault = sandbox.stub();
form.simulate("submit", { preventDefault });
assert.calledOnce(preventDefault);
assert.notCalled(dispatch);
});
it("should emit UserEvent SUBMIT_EMAIL when you submit a valid email", () => {
let form = wrapper.find("form");
assert.ok(form.exists());
form.getDOMNode().elements.email.value = "a@b.c";
form.simulate("submit");
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(
dispatch,
ac.UserEvent({
event: at.SUBMIT_EMAIL,
value: { has_flow_params: false },
})
);
});
it("should keep focus in dialog when blurring start button", () => {
const skipButton = wrapper.find(".trailheadStart");
sandbox.stub(dummyNode, "focus");
skipButton.simulate("blur", { relatedTarget: dummyNode });
assert.calledOnce(dummyNode.focus);
});
});

Просмотреть файл

@ -0,0 +1,105 @@
import { mount } from "enzyme";
import React from "react";
import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
describe("<ReturnToAMO>", () => {
let dispatch;
let onReady;
let sandbox;
let wrapper;
let dummyNode;
let fakeDocument;
let sendUserActionTelemetryStub;
let content;
beforeEach(() => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
onReady = sandbox.stub();
sendUserActionTelemetryStub = sandbox.stub();
content = {
primary_button: {},
secondary_button: {},
};
dummyNode = document.createElement("body");
sandbox.stub(dummyNode, "querySelector").returns(dummyNode);
fakeDocument = {
get activeElement() {
return dummyNode;
},
get body() {
return dummyNode;
},
getElementById() {
return dummyNode;
},
};
});
afterEach(() => {
sandbox.restore();
});
describe("not mounted", () => {
it("should send an IMPRESSION on mount", () => {
assert.notCalled(sendUserActionTelemetryStub);
wrapper = mount(
<ReturnToAMO
document={fakeDocument}
onReady={onReady}
dispatch={dispatch}
content={content}
onBlock={sandbox.stub()}
onAction={sandbox.stub()}
UISurface="NEWTAB_OVERLAY"
sendUserActionTelemetry={sendUserActionTelemetryStub}
/>
);
assert.calledOnce(sendUserActionTelemetryStub);
assert.calledWithExactly(sendUserActionTelemetryStub, {
event: "IMPRESSION",
id: wrapper.instance().props.UISurface,
});
});
});
describe("mounted", () => {
beforeEach(() => {
wrapper = mount(
<ReturnToAMO
document={fakeDocument}
onReady={onReady}
dispatch={dispatch}
content={content}
onBlock={sandbox.stub()}
onAction={sandbox.stub()}
UISurface="NEWTAB_OVERLAY"
sendUserActionTelemetry={sendUserActionTelemetryStub}
/>
);
// Clear the IMPRESSION ping
sendUserActionTelemetryStub.reset();
});
it("should send telemetry on block", () => {
wrapper.instance().onBlockButton();
assert.calledOnce(sendUserActionTelemetryStub);
assert.calledWithExactly(sendUserActionTelemetryStub, {
event: "BLOCK",
id: wrapper.instance().props.UISurface,
});
});
it("should send telemetry on install", () => {
wrapper.instance().onClickAddExtension();
assert.calledWithExactly(sendUserActionTelemetryStub, {
event: "INSTALL",
id: wrapper.instance().props.UISurface,
});
});
});
});

Просмотреть файл

@ -45,6 +45,7 @@ describe("TelemetryFeed", () => {
class UTEventReporting {
sendUserEvent() {}
sendSessionEndEvent() {}
sendTrailheadEnrollEvent() {}
uninit() {}
}
@ -1544,6 +1545,15 @@ describe("TelemetryFeed", () => {
assert.calledWith(eventCreator, action.data);
assert.calledWith(sendEvent, eventCreator.returnValue);
});
it("should call .handleTrailheadEnrollEvent on a TRAILHEAD_ENROLL_EVENT action", () => {
const data = { experiment: "foo", type: "bar", branch: "baz" };
const action = { type: at.TRAILHEAD_ENROLL_EVENT, data };
sandbox.spy(instance, "handleTrailheadEnrollEvent");
instance.onAction(action);
assert.calledWith(instance.handleTrailheadEnrollEvent, action);
});
});
describe("#handleNewTabInit", () => {
it("should set the session as preloaded if the browser is preloaded", () => {
@ -1828,6 +1838,28 @@ describe("TelemetryFeed", () => {
);
});
});
describe("#handleTrailheadEnrollEvent", () => {
it("should send a TRAILHEAD_ENROLL_EVENT if the telemetry is enabled", () => {
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
const data = { experiment: "foo", type: "bar", branch: "baz" };
instance = new TelemetryFeed();
sandbox.stub(instance.utEvents, "sendTrailheadEnrollEvent");
instance.handleTrailheadEnrollEvent({ data });
assert.calledWith(instance.utEvents.sendTrailheadEnrollEvent, data);
});
it("should not send TRAILHEAD_ENROLL_EVENT if the telemetry is disabled", () => {
FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
const data = { experiment: "foo", type: "bar", branch: "baz" };
instance = new TelemetryFeed();
sandbox.stub(instance.utEvents, "sendTrailheadEnrollEvent");
instance.handleTrailheadEnrollEvent({ data });
assert.notCalled(instance.utEvents.sendTrailheadEnrollEvent);
});
});
describe("#handleASRouterUserEvent", () => {
it("should call sendStructuredIngestionEvent on known pingTypes", async () => {
const data = {

Просмотреть файл

@ -1,4 +1,8 @@
import { UTSessionPing, UTUserEventPing } from "test/schemas/pings";
import {
UTSessionPing,
UTTrailheadEnrollPing,
UTUserEventPing,
} from "test/schemas/pings";
import { GlobalOverrider } from "test/unit/utils";
import { UTEventReporting } from "lib/UTEventReporting.jsm";
@ -45,6 +49,21 @@ const FAKE_SESSION_PING_UT = [
page: "about:newtab",
},
];
const FAKE_TRAILHEAD_ENROLL_EVENT = {
experiment: "activity-stream-trailhead-firstrun-interrupts",
type: "as-firstrun",
branch: "supercharge",
};
const FAKE_TRAILHEAD_ENROLL_EVENT_UT = [
"activity_stream",
"enroll",
"preference_study",
"activity-stream-trailhead-firstrun-interrupts",
{
experimentType: "as-firstrun",
branch: "supercharge",
},
];
describe("UTEventReporting", () => {
let globals;
@ -90,6 +109,19 @@ describe("UTEventReporting", () => {
});
});
describe("#sendTrailheadEnrollEvent()", () => {
it("should queue up the correct data to send to Events Telemetry", async () => {
utEvents.sendTrailheadEnrollEvent(FAKE_TRAILHEAD_ENROLL_EVENT);
assert.calledWithExactly(
global.Services.telemetry.recordEvent,
...FAKE_TRAILHEAD_ENROLL_EVENT_UT
);
let ping = global.Services.telemetry.recordEvent.firstCall.args;
assert.validate(ping, UTTrailheadEnrollPing);
});
});
describe("#uninit()", () => {
it("should call setEventRecordingEnabled with a false value", () => {
assert.equal(

Просмотреть файл

@ -288,7 +288,7 @@ addTestsWithPrivilegedContentProcessPref(async function test_default_url() {
});
addTestsWithPrivilegedContentProcessPref(async function test_welcome_url() {
// Disable about:welcome to load newtab
// Set about:welcome to use trailhead flow
Services.prefs.setBoolPref(SIMPLIFIED_WELCOME_ENABLED_PREF, false);
Assert.equal(
aboutNewTabService.welcomeURL,

Просмотреть файл

@ -2,13 +2,40 @@
# 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/.
### UI strings for the simplified onboarding / multistage about:welcome
### UI strings for the simplified onboarding modal / about:welcome
### Various strings use a non-breaking space to avoid a single dangling /
### widowed word, so test on various window sizes if you also want this.
## Welcome page strings
## These button action text can be split onto multiple lines, so use explicit
## newlines in translations to control where the line break appears (e.g., to
## avoid breaking quoted text).
onboarding-button-label-learn-more = Learn More
onboarding-button-label-get-started = Get Started
## Welcome modal dialog strings
onboarding-welcome-header = Welcome to { -brand-short-name }
onboarding-welcome-body = Youve got the browser.<br/>Meet the rest of { -brand-product-name }.
onboarding-welcome-learn-more = Learn more about the benefits.
onboarding-welcome-modal-get-body = Youve got the browser.<br/>Now get the most out of { -brand-product-name }.
onboarding-welcome-modal-supercharge-body = Supercharge your privacy protection.
onboarding-welcome-modal-privacy-body = Youve got the browser. Lets add more privacy protection.
onboarding-welcome-modal-family-learn-more = Learn about the { -brand-product-name } family of products.
onboarding-welcome-form-header = Start Here
onboarding-join-form-body = Enter your email address to get started.
onboarding-join-form-email =
.placeholder = Enter email
onboarding-join-form-email-error = Valid email required
onboarding-join-form-legal = By proceeding, you agree to the <a data-l10n-name="terms">Terms of Service</a> and <a data-l10n-name="privacy">Privacy Notice</a>.
onboarding-join-form-continue = Continue
# This message is followed by a link using onboarding-join-form-signin ("Sign In") as text.
onboarding-join-form-signin-label = Already have an account?
# Text for link to submit the sign in form
onboarding-join-form-signin = Sign In
onboarding-start-browsing-button-label = Start Browsing
onboarding-cards-dismiss =
.title = Dismiss
@ -110,6 +137,52 @@ onboarding-multistage-theme-description-alpenglow =
Use a colorful appearance for buttons,
menus, and windows.
## Welcome full page string
onboarding-fullpage-welcome-subheader = Lets start exploring everything you can do.
onboarding-fullpage-form-email =
.placeholder = Your email address…
## Firefox Sync modal dialog strings.
onboarding-sync-welcome-header = Take { -brand-product-name } with You
onboarding-sync-welcome-content = Get your bookmarks, history, passwords and other settings on all your devices.
onboarding-sync-welcome-learn-more-link = Learn more about Firefox Accounts
onboarding-sync-form-input =
.placeholder = Email
onboarding-sync-form-continue-button = Continue
onboarding-sync-form-skip-login-button = Skip this step
## This is part of the line "Enter your email to continue to Firefox Sync"
onboarding-sync-form-header = Enter your email
onboarding-sync-form-sub-header = to continue to { -sync-brand-name }
## These are individual benefit messages shown with an image, title and
## description.
onboarding-benefit-products-text = Get things done with a family of tools that respects your privacy across your devices.
# "Personal Data Promise" is a concept that should be translated consistently
# across the product. It refers to a concept shown elsewhere to the user: "The
# Firefox Personal Data Promise is the way we honor your data in everything we
# make and do. We take less data. We keep it safe. And we make sure that we are
# transparent about how we use it."
onboarding-benefit-privacy-text = Everything we do honors our Personal Data Promise: Take less. Keep it safe. No secrets.
onboarding-benefit-sync-title = { -sync-brand-short-name }
onboarding-benefit-sync-text = Take your bookmarks, passwords, history, and more everywhere you use { -brand-product-name }.
onboarding-benefit-monitor-title = { -monitor-brand-short-name }
onboarding-benefit-monitor-text = Get notified when your personal info is in a known data breach.
onboarding-benefit-lockwise-title = { -lockwise-brand-short-name }
onboarding-benefit-lockwise-text = Manage passwords that are protected and portable.
## These strings belong to the individual onboarding messages.
## Each message has a title and a description of what the browser feature is.
@ -168,3 +241,15 @@ onboarding-import-browser-settings-button = Import Chrome Data
onboarding-personal-data-promise-title = Private by Design
onboarding-personal-data-promise-text = { -brand-product-name } treats your data with respect by taking less of it, protecting it, and being clear about how we use it.
onboarding-personal-data-promise-button = Read our Promise
## Message strings belonging to the Return to AMO flow
return-to-amo-sub-header = Great, youve got { -brand-short-name }
# <icon></icon> will be replaced with the icon belonging to the extension
#
# Variables:
# $addon-name (String) - Name of the add-on
return-to-amo-addon-header = Now lets get you <icon></icon><b>{ $addon-name }.</b>
return-to-amo-extension-button = Add the Extension
return-to-amo-get-started-button = Get Started with { -brand-short-name }

Просмотреть файл

@ -9,5 +9,6 @@
"network.proxy.type": 1,
"plugin.disable": true,
"startup.homepage_override_url": "",
"startup.homepage_welcome_url": ""
"startup.homepage_welcome_url": "",
"trailhead.firstrun.branches": "join-none"
}

Просмотреть файл

@ -11,5 +11,6 @@
"network.proxy.type": 1,
"plugin.disable": true,
"startup.homepage_override_url": "",
"startup.homepage_welcome_url": ""
"startup.homepage_welcome_url": "",
"trailhead.firstrun.branches": "join-none"
}

Просмотреть файл

@ -85,3 +85,4 @@ user_pref("toolkit.telemetry.server", "https://127.0.0.1/telemetry-dummy/");
user_pref("telemetry.fog.test.localhost_port", -1);
user_pref("startup.homepage_welcome_url", "");
user_pref("startup.homepage_welcome_url.additional", "");
user_pref("trailhead.firstrun.branches", "join");

Просмотреть файл

@ -103,6 +103,25 @@ activity_stream:
session_id: The ID of the Activity Stream session in which the event occurred
page: about:home or about_newtab - the page where the event occurred
user_prefs: An integer representaing a user's A-S settings.
enroll:
objects: ["preference_study"]
release_channel_collection: opt-out
description: >
Sent when a user gets enrolled in a preference study.
bug_numbers: [1549784]
notification_emails:
- "najiang@mozilla.com"
- "edilee@mozilla.com"
products:
- "firefox"
- "fennec"
record_in_processes: ["main"]
expiry_version: never
extra_keys:
experimentType: >
For preference_study recipes, the type of experiment this is ("exp" or "exp-highpop").
branch: >
For preference_study recipes, the slug of the branch that was chosen for this client.
addonsManager:
install: