Bug 1588215 - Add modal-less welcome, send-tab recipes and bug fixes to New Tab Page r=Mardak,fluent-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D49020

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Punam Dahiya 2019-10-12 07:01:16 +00:00
Родитель e54b007827
Коммит d16004d6cb
61 изменённых файлов: 3301 добавлений и 939 удалений

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

@ -45,6 +45,8 @@ module.exports = {
"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",

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

@ -123,6 +123,7 @@ for (const type of [
"SNIPPETS_RESET",
"SNIPPET_BLOCKED",
"SUBMIT_EMAIL",
"SUBMIT_SIGNIN",
"SYSTEM_TICK",
"TELEMETRY_IMPRESSION_STATS",
"TELEMETRY_PERFORMANCE_EVENT",
@ -154,6 +155,7 @@ for (const type of [
// as call-to-action buttons in snippets, onboarding tour, etc.
const ASRouterActions = {};
for (const type of [
"HIGHLIGHT_FEATURE",
"INSTALL_ADDON_FROM_URL",
"OPEN_APPLICATIONS_MENU",
"OPEN_PRIVATE_BROWSER_WINDOW",

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

@ -21,6 +21,7 @@ const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
const TEMPLATES_ABOVE_PAGE = [
"trailhead",
"full_page_interrupt",
"return_to_amo_overlay",
"extended_triplets",
];

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

@ -0,0 +1,167 @@
/* 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 } 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="service" type="hidden" value="sync" />
<input name="action" type="hidden" value="email" />
<input name="context" type="hidden" value="fx_desktop_v3" />
<input
name="entrypoint"
type="hidden"
value="activity-stream-firstrun"
/>
<input name="utm_source" type="hidden" value="activity-stream" />
<input name="utm_campaign" type="hidden" value="firstrun" />
<input name="utm_term" type="hidden" value={UTMTerm} />
<input
name="device_id"
type="hidden"
value={this.props.flowParams.deviceId}
/>
<input
name="flow_id"
type="hidden"
value={this.props.flowParams.flowId}
/>
<input
name="flow_begin_time"
type="hidden"
value={this.props.flowParams.flowBeginTime}
/>
<input name="style" type="hidden" value="trailhead" />
<p
data-l10n-id="onboarding-join-form-email-error"
className="error"
/>
<input
data-l10n-id={content.form.email.string_id}
name="email"
type="email"
onInvalid={this.onInputInvalid}
onChange={this.onInputChange}
/>
<p className="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;
}
}
}

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

@ -29,9 +29,9 @@ To test telemetry pings, complete the the following steps:
- In about:config, set:
- `browser.newtabpage.activity-stream.telemetry` to `true`
- `browser.ping-centre.log` to `true`
- Open the Browser Toolbox devtools (Tools > Developer > Browser Toolbox) and switch to the console tab. Add a filter for for `activity-stream-router` to only display relevant pings:
- Open the Browser Toolbox devtools (Tools > Web Developer > Browser Toolbox) and switch to the console tab. Add a filter for for `activity-stream` to only display relevant pings:
![Devtools telemetry pong](./telemetry-screenshot.png)
![Devtools telemetry ping](./telemetry-screenshot.png)
You should now see pings show up as you view/interact with ASR messages/templates.

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

@ -40,6 +40,8 @@ Please note that some targeting attributes require stricter controls on the tele
* [totalBlockedCount](#totalblockedcount)
* [recentBookmarks](#recentbookmarks)
* [userPrefs](#userprefs)
* [attachedFxAOAuthClients](#attachedfxaoauthclients)
* [platformName](#platformname)
## Detailed usage
@ -555,3 +557,39 @@ declare const userPrefs: {
snippets: boolean;
}
```
### `attachedFxAOAuthClients`
Information about connected services associated with the FxA Account.
#### Definition
```
interface OAuthClient {
id: string;
// FxA service name
name: string;
lastAccessTime: UnixEpochNumber;
}
declare const attachedFxAOAuthClients: Array<OAuthClient>
```
#### Examples
```javascript
{
id: "7377719276ad44ee",
name: "Pocket",
lastAccessTime: 1513599164000
}
```
### `platformName`
[Platform information](https://searchfox.org/mozilla-central/rev/05a22d864814cb1e4352faa4004e1f975c7d2eb9/toolkit/modules/AppConstants.jsm#156).
#### Definition
```
declare const platformName = "linux" | "win" | "macosx" | "android" | "other";
```

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

@ -180,6 +180,7 @@ export class FirstRun extends React.PureComponent {
{isInterruptVisible ? (
<Interrupt
document={props.document}
cards={triplets}
message={interrupt}
onNextScene={this.closeInterrupt}
UTMTerm={UTMTerm}

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

@ -5,12 +5,14 @@
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,
@ -38,6 +40,21 @@ export class Interrupt extends React.PureComponent {
/>
</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}
/>
);
case "trailhead":
return (
<Trailhead

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

@ -0,0 +1,184 @@
/* 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}
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) {
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 });
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,259 @@
.activity-stream {
&.welcome {
overflow: hidden;
}
&:not(.welcome) {
.fullpage-wrapper {
display: none;
}
}
}
.fullpage-wrapper {
$responsive-breakpoint: 850px;
$responsive-width: 352px;
$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;
background-size: 154px;
}
@media screen and (max-width: $responsive-breakpoint) {
width: $responsive-width;
}
}
}

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

@ -1,7 +1,7 @@
{
"title": "ToolbarBadgeMessage",
"description": "A template that specifies to which element in the browser toolbar to add a notification.",
"version": "1.0.0",
"version": "1.1.0",
"type": "object",
"properties": {
"target": {
@ -21,6 +21,17 @@
"delay": {
"type": "number",
"description": "Optional delay in ms after which to show the notification"
},
"badgeDescription": {
"type": "object",
"description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'",
"properties": {
"string_id": {
"type": "string",
"description": "Fluent string id"
}
},
"required": ["string_id"]
}
},
"additionalProperties": false,

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

@ -4,6 +4,7 @@
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";
@ -22,14 +23,7 @@ export class Trailhead extends React.PureComponent {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.onStartBlur = this.onStartBlur.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.state = {
emailInput: "",
};
}
get dialog() {
@ -44,18 +38,6 @@ export class Trailhead extends React.PureComponent {
this.props.document
.getElementById("root")
.setAttribute("aria-hidden", "true");
// Start with focus in the email input box
const input = this.dialog.querySelector("input[name=email]");
if (input) {
input.focus();
}
}
onInputChange(e) {
let error = e.target.previousSibling;
this.setState({ emailInput: e.target.value });
error.classList.remove("active");
e.target.classList.remove("invalid");
}
onStartBlur(event) {
@ -72,24 +54,6 @@ export class Trailhead extends React.PureComponent {
}
}
onSubmit(event) {
// Dynamically require the email on submission so screen readers don't read
// out it's always required because there's also ways to skip the modal
const { email } = event.target.elements;
if (!email.value.length) {
email.required = true;
email.checkValidity();
event.preventDefault();
return;
}
this.props.dispatch(
ac.UserEvent({ event: "SUBMIT_EMAIL", ...this._getFormInfo() })
);
global.addEventListener("visibilitychange", this.closeModal);
}
closeModal(ev) {
global.removeEventListener("visibilitychange", this.closeModal);
this.props.document.body.classList.remove("welcome");
@ -116,14 +80,6 @@ export class Trailhead extends React.PureComponent {
return { value };
}
onInputInvalid(e) {
let error = e.target.previousSibling;
error.classList.add("active");
e.target.classList.add("invalid");
e.preventDefault(); // Override built-in form validation popup
e.target.focus();
}
render() {
const { props } = this;
const { UTMTerm } = props;
@ -162,90 +118,16 @@ export class Trailhead extends React.PureComponent {
rel="noopener noreferrer"
/>
</div>
<div
role="group"
aria-labelledby="joinFormHeader"
aria-describedby="joinFormBody"
className="trailheadForm"
>
<h3
id="joinFormHeader"
data-l10n-id={content.form.title.string_id}
<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}
/>
<p id="joinFormBody" data-l10n-id={content.form.text.string_id} />
<form
method="get"
action={this.props.fxaEndpoint}
target="_blank"
rel="noopener noreferrer"
onSubmit={this.onSubmit}
>
<input name="service" type="hidden" value="sync" />
<input name="action" type="hidden" value="email" />
<input name="context" type="hidden" value="fx_desktop_v3" />
<input
name="entrypoint"
type="hidden"
value="activity-stream-firstrun"
/>
<input name="utm_source" type="hidden" value="activity-stream" />
<input name="utm_campaign" type="hidden" value="firstrun" />
<input name="utm_term" type="hidden" value={UTMTerm} />
<input
name="device_id"
type="hidden"
value={this.props.flowParams.deviceId}
/>
<input
name="flow_id"
type="hidden"
value={this.props.flowParams.flowId}
/>
<input
name="flow_begin_time"
type="hidden"
value={this.props.flowParams.flowBeginTime}
/>
<input name="style" type="hidden" value="trailhead" />
<p
data-l10n-id="onboarding-join-form-email-error"
className="error"
/>
<input
data-l10n-id={content.form.email.string_id}
name="email"
type="email"
onInvalid={this.onInputInvalid}
onChange={this.onInputChange}
/>
<p
className="trailheadTerms"
data-l10n-id="onboarding-join-form-legal"
>
<a
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"
/>
</form>
</div>
</div>

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

@ -4,6 +4,7 @@
$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;
@ -47,6 +48,13 @@
}
}
.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;
@ -141,103 +149,6 @@
}
}
.trailheadForm {
$logo-size: 100px;
background: url('#{$image-path}trailhead/firefox-logo.png') top center / $logo-size no-repeat;
min-width: 260px;
padding-top: $logo-size;
text-align: center;
h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px;
}
p {
color: $white;
font-size: 15px;
line-height: 22px;
margin: 0 0 20px;
}
.trailheadTerms {
margin: 4px 30px 20px;
a,
& {
color: $white-70;
font-size: 12px;
line-height: 20px;
}
}
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;
}
}
}
.trailheadStart {
border: 1px solid $white-50;
cursor: pointer;
@ -484,19 +395,3 @@
transform: translateY(0);
}
}
.firstrun-title {
background: url('chrome://branding/content/about-logo.png') top left no-repeat;
background-size: 90px 90px;
margin: 40px 0 10px;
padding-top: 110px;
@media screen and (max-width: 790px) {
background: url('chrome://branding/content/about-logo.png') top center no-repeat;
background-size: 90px 90px;
}
&:dir(rtl) {
background-position: top right;
}
}

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

@ -238,6 +238,7 @@ export class DSCard extends React.PureComponent {
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
campaignId={this.props.campaignId}
/>
</div>
);

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

@ -125,6 +125,7 @@ $excerpt-line-height: 20;
border-radius: 4px;
height: 32px;
font-size: 14px;
font-weight: 600;
padding: 5px 8px 7px;
border: 0;
color: $grey-90;
@ -164,6 +165,7 @@ $excerpt-line-height: 20;
}
font-size: 15px;
font-weight: 600;
line-height: 24px;
height: 24px;
width: auto;

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

@ -60,7 +60,7 @@ export class DSImage extends React.PureComponent {
}
componentDidMount() {
this.idleCallbackId = window.requestIdleCallback(
this.idleCallbackId = this.props.windowObj.requestIdleCallback(
this.onIdleCallback.bind(this)
);
this.observer = new IntersectionObserver(this.onSeen.bind(this), {
@ -77,6 +77,9 @@ export class DSImage extends React.PureComponent {
if (this.observer) {
this.observer.unobserve(ReactDOM.findDOMNode(this));
}
if (this.idleCallbackId) {
this.props.windowObj.cancelIdleCallback(this.idleCallbackId);
}
}
render() {
@ -160,4 +163,5 @@ DSImage.defaultProps = {
extraClassNames: null, // Additional classnames to append to component
optimize: true, // Measure parent container to request exact sizes
alt_text: null,
windowObj: window, // Added to support unit tests
};

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

@ -76,6 +76,7 @@ export class DSLinkMenu extends React.PureComponent {
pocket_id: this.props.pocket_id,
shim: this.props.shim,
bookmarkGuid: this.props.bookmarkGuid,
campaign_id: this.props.campaignId,
}}
/>
</ContextMenuButton>

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

@ -3,6 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { actionCreators as ac } from "common/Actions.jsm";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { ModalOverlayWrapper } from "content-src/asrouter/components/ModalOverlay/ModalOverlay";
@ -10,6 +11,16 @@ export class DSPrivacyModal extends React.PureComponent {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.onLinkClick = this.onLinkClick.bind(this);
}
onLinkClick(event) {
this.props.dispatch(
ac.UserEvent({
event: "CLICK_PRIVACY_INFO",
source: "DS_PRIVACY_MODAL",
})
);
}
closeModal() {
@ -28,7 +39,10 @@ export class DSPrivacyModal extends React.PureComponent {
<div className="privacy-notice">
<h3 data-l10n-id="newtab-privacy-modal-header" />
<p data-l10n-id="newtab-privacy-modal-paragraph" />
<SafeAnchor url="https://www.mozilla.org/en-US/privacy/firefox/">
<SafeAnchor
onLinkClick={this.onLinkClick}
url="https://www.mozilla.org/en-US/privacy/firefox/"
>
<span data-l10n-id="newtab-privacy-modal-link" />
</SafeAnchor>
</div>

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

@ -142,6 +142,7 @@ export class Hero extends React.PureComponent {
pocket_id={heroRec.pocket_id}
shim={heroRec.shim}
bookmarkGuid={heroRec.bookmarkGuid}
campaignId={heroRec.campaign_id}
/>
</div>
);

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

@ -116,6 +116,7 @@ export class ListItem extends React.PureComponent {
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
campaignId={this.props.campaignId}
/>
)}
</li>

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

@ -28,6 +28,7 @@ export const LinkMenuOptions = {
action: {
type: at.SHOW_PRIVACY_INFO,
},
userEvent: "SHOW_PRIVACY_INFO",
}),
RemoveBookmark: site => ({
id: "newtab-menu-remove-bookmark",
@ -60,12 +61,20 @@ export const LinkMenuOptions = {
}),
userEvent: "OPEN_NEW_WINDOW",
}),
// This blocks the url for regular stories,
// but also sends a message to DiscoveryStream with campaign_id.
// If DiscoveryStream sees this message for a campaign_id
// it also blocks it on the campaign_id.
BlockUrl: (site, index, eventSource) => ({
id: "newtab-menu-dismiss",
icon: "dismiss",
action: ac.AlsoToMain({
type: at.BLOCK_URL,
data: { url: site.open_url || site.url, pocket_id: site.pocket_id },
data: {
url: site.open_url || site.url,
pocket_id: site.pocket_id,
...(site.campaign_id ? { campaign_id: site.campaign_id } : {}),
},
}),
impression: ac.ImpressionStats({
source: eventSource,

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

@ -176,4 +176,6 @@ input {
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';
@import '../asrouter/templates/EOYSnippet/EOYSnippet';
@import '../asrouter/components/FxASignupForm/FxASignupForm';
@import '../asrouter/templates/Trailhead/Trailhead';
@import '../asrouter/templates/FullPageInterrupt/FullPageInterrupt';

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

@ -70,6 +70,7 @@ $white-10: rgba($white, 0.1);
$white-50: rgba($white, 0.5);
$white-60: rgba($white, 0.6);
$white-70: rgba($white, 0.7);
$ghost-white: #FAFAFC;
$pocket-teal: #50BCB6;
$pocket-red: #EF4056;
$shadow-10: rgba(12, 12, 13, 0.1);
@ -85,7 +86,9 @@ $about-welcome-gradient: linear-gradient(to bottom, $blue-70 40%, $aw-extra-blue
$about-welcome-extra-links: #676F7E;
$firefox-wordmark-default-color: #363959;
$firefox-wordmark-darktheme-color: $white;
$trailhead-violet: #7542E5;
$trailhead-purple: #2B2156;
$trailhead-purple-80: #36296D;
$trailhead-blue-60: #0250BB;
$trailhead-blue-70: #054096;

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

@ -2725,6 +2725,7 @@ main {
border-radius: 4px;
height: 32px;
font-size: 14px;
font-weight: 600;
padding: 5px 8px 7px;
border: 0;
color: #0C0C0D;
@ -2747,6 +2748,7 @@ main {
box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
.ds-card .meta .cta-link {
font-size: 15px;
font-weight: 600;
line-height: 24px;
height: 24px;
width: auto;
@ -3936,6 +3938,84 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
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("../data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
@ -3963,6 +4043,11 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("../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; }
@ -4023,65 +4108,6 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.trailhead .trailheadForm {
background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
min-width: 260px;
padding-top: 100px;
text-align: center; }
.trailhead .trailheadForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.trailhead .trailheadForm p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.trailhead .trailheadForm .trailheadTerms {
margin: 4px 30px 20px; }
.trailhead .trailheadForm .trailheadTerms a, .trailhead .trailheadForm .trailheadTerms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.trailhead .trailheadForm form {
position: relative; }
.trailhead .trailheadForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
background-color: #0250BB; }
.trailhead .trailheadForm button:focus {
outline: dotted 1px; }
.trailhead .trailheadForm button:active {
background-color: #054096; }
.trailhead .trailheadStart {
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
@ -4258,14 +4284,172 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top left no-repeat;
background-size: 90px 90px;
margin: 40px 0 10px;
padding-top: 110px; }
@media screen and (max-width: 790px) {
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top center no-repeat;
background-size: 90px 90px; } }
.firstrun-title:dir(rtl) {
background-position: top right; }
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
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: #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: 850px) {
.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: 850px) {
.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;
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 352px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .fullpage-left-section {
width: 352px; } }
.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("../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: 850px) {
.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;
background-size: 154px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .trailheadCard {
width: 352px; } }

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

@ -2728,6 +2728,7 @@ main {
border-radius: 4px;
height: 32px;
font-size: 14px;
font-weight: 600;
padding: 5px 8px 7px;
border: 0;
color: #0C0C0D;
@ -2750,6 +2751,7 @@ main {
box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
.ds-card .meta .cta-link {
font-size: 15px;
font-weight: 600;
line-height: 24px;
height: 24px;
width: auto;
@ -3939,6 +3941,84 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
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("../data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
@ -3966,6 +4046,11 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("../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; }
@ -4026,65 +4111,6 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.trailhead .trailheadForm {
background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
min-width: 260px;
padding-top: 100px;
text-align: center; }
.trailhead .trailheadForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.trailhead .trailheadForm p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.trailhead .trailheadForm .trailheadTerms {
margin: 4px 30px 20px; }
.trailhead .trailheadForm .trailheadTerms a, .trailhead .trailheadForm .trailheadTerms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.trailhead .trailheadForm form {
position: relative; }
.trailhead .trailheadForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
background-color: #0250BB; }
.trailhead .trailheadForm button:focus {
outline: dotted 1px; }
.trailhead .trailheadForm button:active {
background-color: #054096; }
.trailhead .trailheadStart {
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
@ -4261,14 +4287,172 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top left no-repeat;
background-size: 90px 90px;
margin: 40px 0 10px;
padding-top: 110px; }
@media screen and (max-width: 790px) {
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top center no-repeat;
background-size: 90px 90px; } }
.firstrun-title:dir(rtl) {
background-position: top right; }
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
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: #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: 850px) {
.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: 850px) {
.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;
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 352px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .fullpage-left-section {
width: 352px; } }
.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("../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: 850px) {
.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;
background-size: 154px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .trailheadCard {
width: 352px; } }

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

@ -2725,6 +2725,7 @@ main {
border-radius: 4px;
height: 32px;
font-size: 14px;
font-weight: 600;
padding: 5px 8px 7px;
border: 0;
color: #0C0C0D;
@ -2747,6 +2748,7 @@ main {
box-shadow: 0 0 0 2px #2A2A2E, 0 0 0 5px rgba(10, 132, 255, 0.5); }
.ds-card .meta .cta-link {
font-size: 15px;
font-weight: 600;
line-height: 24px;
height: 24px;
width: auto;
@ -3936,6 +3938,84 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
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("../data/content/assets/trailhead/accounts-form-bg.jpg") bottom/cover;
color: #FFF;
@ -3963,6 +4043,11 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
@media (min-width: 850px) {
.trailhead .trailheadContent .trailheadLearn {
margin-inline-start: 74px; } }
.trailhead .trailhead-join-form {
background: url("../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; }
@ -4023,65 +4108,6 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
font-size: 15px;
line-height: 22px;
margin: 4px 0 15px; }
.trailhead .trailheadForm {
background: url("../data/content/assets/trailhead/firefox-logo.png") top center/100px no-repeat;
min-width: 260px;
padding-top: 100px;
text-align: center; }
.trailhead .trailheadForm h3 {
font-size: 36px;
font-weight: 200;
line-height: 46px;
margin: 12px 0 4px; }
.trailhead .trailheadForm p {
color: #FFF;
font-size: 15px;
line-height: 22px;
margin: 0 0 20px; }
.trailhead .trailheadForm .trailheadTerms {
margin: 4px 30px 20px; }
.trailhead .trailheadForm .trailheadTerms a, .trailhead .trailheadForm .trailheadTerms {
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
line-height: 20px; }
.trailhead .trailheadForm form {
position: relative; }
.trailhead .trailheadForm form .error.active {
inset-inline-start: 0;
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
font-weight: 400;
padding: 14px; }
.trailhead .trailheadForm button:hover, .trailhead .trailheadForm button:focus {
background-color: #0250BB; }
.trailhead .trailheadForm button:focus {
outline: dotted 1px; }
.trailhead .trailheadForm button:active {
background-color: #054096; }
.trailhead .trailheadStart {
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
@ -4258,14 +4284,172 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
opacity: 1;
transform: translateY(0); } }
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top left no-repeat;
background-size: 90px 90px;
margin: 40px 0 10px;
padding-top: 110px; }
@media screen and (max-width: 790px) {
.firstrun-title {
background: url("chrome://branding/content/about-logo.png") top center no-repeat;
background-size: 90px 90px; } }
.firstrun-title:dir(rtl) {
background-position: top right; }
.activity-stream.welcome {
overflow: hidden; }
.activity-stream:not(.welcome) .fullpage-wrapper {
display: none; }
.fullpage-wrapper {
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: #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: 850px) {
.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: 850px) {
.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;
align-self: center;
padding: 50px 0; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .container {
flex-direction: column;
width: 352px;
text-align: center; } }
.fullpage-wrapper .fullpage-left-section {
position: relative;
width: 538px;
font-size: 18px;
line-height: 30px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .fullpage-left-section {
width: 352px; } }
.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("../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: 850px) {
.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;
background-size: 154px; }
@media screen and (max-width: 850px) {
.fullpage-wrapper .trailheadCard {
width: 352px; } }

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

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

После

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

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

@ -1,6 +1,6 @@
# Metrics we collect
By default, the about:newtab and about:home pages in Firefox (the pages you see when you open a new tab and when you start the browser), will send data back to Mozilla servers about usage of these pages. The intent is to collect data in order to improve the user's experience while using Activity Stream. Data about your specific browsing behaior or the sites you visit is **never transmitted to any Mozilla server**. At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-firefox).
By default, the about:newtab, about:welcome and about:home pages in Firefox (the pages you see when you open a new tab and when you start the browser), will send data back to Mozilla servers about usage of these pages. The intent is to collect data in order to improve the user's experience while using Activity Stream. Data about your specific browsing behaior or the sites you visit is **never transmitted to any Mozilla server**. At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-firefox).
Data is sent to our servers in the form of discreet HTTPS 'pings' or messages whenever you do some action on the Activity Stream about:home, about:newtab or about:welcome pages. We try to minimize the amount and frequency of pings by batching them together. Pings are sent in [JSON serialized format](http://www.json.org/).
@ -169,6 +169,43 @@ A user event ping includes some basic metadata (tab id, addon version, etc.) as
}
```
#### Showing privacy information
```js
{
  "event": "SHOW_PRIVACY_INFO",
"source": "TOP_SITES",
  "action_position": 2,
// Basic metadata
"action": "activity_stream_event",
"page": ["about:newtab" | "about:home" | "about:welcome" | "unknown"],
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
"session_id": "005deed0-e3e4-4c02-a041-17405fd703f6",
  "addon_version": "20180710100040",
  "locale": "en-US",
"user_prefs": 7
}
```
#### Clicking on privacy information link
```js
{
  "event": "CLICK_PRIVACY_INFO",
"source": "DS_PRIVACY_MODAL",
// Basic metadata
"action": "activity_stream_event",
"page": ["about:newtab" | "about:home" | "about:welcome" | "unknown"],
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
"session_id": "005deed0-e3e4-4c02-a041-17405fd703f6",
  "addon_version": "20180710100040",
  "locale": "en-US",
"user_prefs": 7
}
```
#### Deleting a search shortcut
```js
{
@ -409,7 +446,7 @@ A user event ping includes some basic metadata (tab id, addon version, etc.) as
```js
{
"event": ["SUBMIT_EMAIL" | "SKIPPED_SIGNIN"],
"event": ["SUBMIT_EMAIL" | "SUBMIT_SIGNIN" | "SKIPPED_SIGNIN"],
"value": {
"has_flow_params": false,
}

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

@ -1893,6 +1893,20 @@ class _ASRouter {
case ra.OPEN_APPLICATIONS_MENU:
UITour.showMenu(target.browser.ownerGlobal, action.data.args);
break;
case ra.HIGHLIGHT_FEATURE:
const highlight = await UITour.getTarget(
target.browser.ownerGlobal,
action.data.args
);
if (highlight) {
await UITour.showHighlight(
target.browser.ownerGlobal,
highlight,
"none",
{ autohide: true }
);
}
break;
case ra.INSTALL_ADDON_FROM_URL:
this._updateOnboardingState();
await MessageLoaderUtils.installAddonFromURL(

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

@ -21,6 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AttributionCode: "resource:///modules/AttributionCode.jsm",
FilterExpressions:
"resource://gre/modules/components-utils/FilterExpressions.jsm",
fxAccounts: "resource://gre/modules/FxAccounts.jsm",
});
XPCOMUtils.defineLazyPreferenceGetter(
@ -460,6 +461,12 @@ const TargetingGetters = {
get totalBlockedCount() {
return TrackingDBService.sumAllEvents();
},
get attachedFxAOAuthClients() {
return this.usesFirefoxSync ? fxAccounts.listAttachedOAuthClients() : [];
},
get platformName() {
return AppConstants.platform;
},
};
this.ASRouterTargeting = {

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

@ -77,6 +77,64 @@ function checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) {
* have idempotent `init` and `uninit` methods.
*/
this.ASRouterTriggerListeners = new Map([
[
"openArticleURL",
{
id: "openArticleURL",
_initialized: false,
_triggerHandler: null,
_hosts: new Set(),
_matchPatternSet: null,
readerModeEvent: "Reader:UpdateReaderButton",
init(triggerHandler, hosts, patterns) {
if (!this._initialized) {
this.receiveMessage = this.receiveMessage.bind(this);
Services.mm.addMessageListener(this.readerModeEvent, this);
this._triggerHandler = triggerHandler;
this._initialized = true;
}
if (patterns) {
if (this._matchPatternSet) {
this._matchPatternSet = new MatchPatternSet(
new Set([...this._matchPatternSet.patterns, ...patterns]),
MATCH_PATTERN_OPTIONS
);
} else {
this._matchPatternSet = new MatchPatternSet(
patterns,
MATCH_PATTERN_OPTIONS
);
}
}
if (hosts) {
hosts.forEach(h => this._hosts.add(h));
}
},
receiveMessage({ data, target }) {
if (data && data.isArticle) {
const match = checkURLMatch(target.currentURI, {
hosts: this._hosts,
matchPatternSet: this._matchPatternSet,
});
if (match) {
this._triggerHandler(target, { id: this.id, param: match });
}
}
},
uninit() {
if (this._initialized) {
Services.mm.removeMessageListener(this.readerModeEvent, this);
this._initialized = false;
this._triggerHandler = null;
this._hosts = new Set();
this._matchPatternSet = null;
}
},
},
],
[
"openBookmarkedURL",
{
@ -435,14 +493,7 @@ this.ASRouterTriggerListeners = new Map([
this,
"SiteProtection:ContentBlockingEvent"
);
for (let win of Services.wm.getEnumerator("navigator:browser")) {
if (isPrivateWindow(win)) {
continue;
}
win.gBrowser.removeTabsProgressListener(this);
}
EveryWindow.unregisterCallback(this.id);
this.onLocationChange = null;
this._initialized = false;
}

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

@ -467,6 +467,14 @@ const PREFS_CONFIG = new Map([
},
],
// See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
[
"discoverystream.campaign.blocks",
{
title: "Track campaign blocks",
skipBroadcast: true,
value: "{}",
},
],
[
"discoverystream.config",
{

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

@ -62,6 +62,7 @@ const PREF_TOPSTORIES = "feeds.section.topstories";
const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear";
const PREF_SHOW_SPONSORED = "showSponsored";
const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
const PREF_CAMPAIGN_BLOCKS = "discoverystream.campaign.blocks";
const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
let defaultLayoutResp;
@ -773,8 +774,11 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
filterBlocked(data) {
const filtered = [];
if (data && data.length) {
let campaigns = this.readDataPref(PREF_CAMPAIGN_BLOCKS);
const filteredItems = data.filter(item => {
const blocked = NewTabUtils.blockedLinks.isBlocked({ url: item.url });
const blocked =
NewTabUtils.blockedLinks.isBlocked({ url: item.url }) ||
campaigns[item.campaign_id];
if (blocked) {
filtered.push(item);
}
@ -838,7 +842,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// `filterItems` as the frequency capped items.
frequencyCapSpocs(spocs) {
if (spocs && spocs.length) {
const impressions = this.readImpressionsPref(PREF_SPOC_IMPRESSIONS);
const impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS);
const caps = [];
const result = spocs.filter(s => {
const isBelow = this.isBelowFrequencyCap(impressions, s);
@ -1009,7 +1013,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
recsExpireTime * 1000 || DEFAULT_RECS_EXPIRE_TIME,
DEFAULT_RECS_EXPIRE_TIME
);
const impressions = this.readImpressionsPref(PREF_REC_IMPRESSIONS);
const impressions = this.readDataPref(PREF_REC_IMPRESSIONS);
const expired = [];
const active = [];
for (const item of recommendations) {
@ -1122,7 +1126,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
async reset() {
this.resetImpressionPrefs();
this.resetDataPrefs();
await this.resetCache();
if (this.loaded) {
Services.obs.removeObserver(this, "idle-daily");
@ -1137,9 +1141,10 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
await this.cache.set("affinities", {});
}
resetImpressionPrefs() {
this.writeImpressionsPref(PREF_SPOC_IMPRESSIONS, {});
this.writeImpressionsPref(PREF_REC_IMPRESSIONS, {});
resetDataPrefs() {
this.writeDataPref(PREF_SPOC_IMPRESSIONS, {});
this.writeDataPref(PREF_REC_IMPRESSIONS, {});
this.writeDataPref(PREF_CAMPAIGN_BLOCKS, {});
}
resetState() {
@ -1164,20 +1169,28 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
recordCampaignImpression(campaignId) {
let impressions = this.readImpressionsPref(PREF_SPOC_IMPRESSIONS);
let impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS);
const timeStamps = impressions[campaignId] || [];
timeStamps.push(Date.now());
impressions = { ...impressions, [campaignId]: timeStamps };
this.writeImpressionsPref(PREF_SPOC_IMPRESSIONS, impressions);
this.writeDataPref(PREF_SPOC_IMPRESSIONS, impressions);
}
recordTopRecImpressions(recId) {
let impressions = this.readImpressionsPref(PREF_REC_IMPRESSIONS);
let impressions = this.readDataPref(PREF_REC_IMPRESSIONS);
if (!impressions[recId]) {
impressions = { ...impressions, [recId]: Date.now() };
this.writeImpressionsPref(PREF_REC_IMPRESSIONS, impressions);
this.writeDataPref(PREF_REC_IMPRESSIONS, impressions);
}
}
recordBlockCampaignId(campaignId) {
const campaigns = this.readDataPref(PREF_CAMPAIGN_BLOCKS);
if (!campaigns[campaignId]) {
campaigns[campaignId] = 1;
this.writeDataPref(PREF_CAMPAIGN_BLOCKS, campaigns);
}
}
@ -1214,17 +1227,17 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
);
}
writeImpressionsPref(pref, impressions) {
writeDataPref(pref, impressions) {
this.store.dispatch(ac.SetPref(pref, JSON.stringify(impressions)));
}
readImpressionsPref(pref) {
readDataPref(pref) {
const prefVal = this.store.getState().Prefs.values[pref];
return prefVal ? JSON.parse(prefVal) : {};
}
cleanUpImpressionPref(isExpired, pref) {
const impressions = this.readImpressionsPref(pref);
const impressions = this.readDataPref(pref);
let changed = false;
Object.keys(impressions).forEach(id => {
@ -1235,7 +1248,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
});
if (changed) {
this.writeImpressionsPref(pref, impressions);
this.writeDataPref(pref, impressions);
}
}
@ -1331,6 +1344,9 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}
}
break;
// This is fired from the browser, it has no concept of spocs, campaign or pocket.
// We match the blocked url with our available spoc urls to see if there is a match.
// I suspect we *could* instead do this in BLOCK_URL but I'm not sure.
case at.PLACES_LINK_BLOCKED:
if (this.showSpocs) {
const spocsState = this.store.getState().DiscoveryStream.spocs;
@ -1346,6 +1362,8 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
this._sendSpocsFill({ blocked_by_user: filtered }, false);
// If we're blocking a spoc, we want a slightly different treatment for open tabs.
// AlsoToPreloaded updates the source data and preloaded tabs with a new spoc.
// BroadcastToContent updates open tabs with a non spoc instead of a new spoc.
this.store.dispatch(
ac.AlsoToPreloaded({
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
@ -1372,6 +1390,16 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// When this feed is shutting down:
this.uninitPrefs();
break;
case at.BLOCK_URL: {
// If we block a story that also has a campaign_id
// we want to record that as blocked too.
// This is because a single campaign might have slightly different urls.
const { campaign_id } = action.data;
if (campaign_id) {
this.recordBlockCampaignId(campaign_id);
}
break;
}
case at.PREF_CHANGED:
switch (action.data.name) {
case PREF_CONFIG:

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

@ -54,6 +54,20 @@ const TRAILHEAD_MODAL_VARIANT_CONTENT = {
skipButton: { string_id: "onboarding-start-browsing-button-label" },
};
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 JOIN_CONTENT = {
className: "joinCohort",
title: { string_id: "onboarding-welcome-body" },
@ -158,6 +172,27 @@ const ONBOARDING_MESSAGES = () => [
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",
@ -233,7 +268,7 @@ const ONBOARDING_MESSAGES = () => [
order: 2,
content: {
title: { string_id: "onboarding-firefox-monitor-title" },
text: { string_id: "onboarding-firefox-monitor-text" },
text: { string_id: "onboarding-firefox-monitor-text2" },
icon: "ffmonitor",
primary_button: {
label: { string_id: "onboarding-firefox-monitor-button" },
@ -461,6 +496,7 @@ const ONBOARDING_MESSAGES = () => [
delay: 5 * ONE_MINUTE,
target: "whats-new-menu-button",
action: { id: "show-whatsnew-button" },
badgeDescription: { string_id: "cfr-badge-reader-label-newfeature" },
},
priority: 1,
trigger: { id: "toolbarBadgeUpdate" },

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

@ -240,6 +240,110 @@ const MESSAGES = () => [
patterns: ["*://*/*.pdf"],
},
},
{
id: "SEND_TAB_CFR",
template: "cfr_doorhanger",
content: {
layout: "icon_and_message",
category: "cfrFeatures",
notification_text: { string_id: "cfr-doorhanger-extension-notification" },
heading_text: { string_id: "cfr-doorhanger-send-tab-header" },
info_icon: {
label: { string_id: "cfr-doorhanger-extension-sumo-link" },
sumo_path: "https://example.com",
},
text: { string_id: "cfr-doorhanger-send-tab-body" },
icon: "chrome://branding/content/icon64.png",
buttons: {
primary: {
label: { string_id: "cfr-doorhanger-send-tab-ok-button" },
action: {
type: "HIGHLIGHT_FEATURE",
data: { args: "pageAction-sendToDevice" },
},
},
secondary: [
{
label: { string_id: "cfr-doorhanger-extension-cancel-button" },
action: { type: "CANCEL" },
},
{
label: {
string_id: "cfr-doorhanger-extension-never-show-recommendation",
},
},
{
label: {
string_id: "cfr-doorhanger-extension-manage-settings-button",
},
action: {
type: "OPEN_PREFERENCES_PAGE",
data: { category: "general-cfrfeatures" },
},
},
],
},
},
targeting: "true",
trigger: {
// Match any URL that has a Reader Mode icon
id: "openArticleURL",
patterns: ["*://*/*"],
},
},
{
id: "SEND_RECIPE_TAB_CFR",
template: "cfr_doorhanger",
// Higher priority because this has the same targeting rules as
// SEND_TAB_CFR but is more specific
priority: 1,
content: {
layout: "icon_and_message",
category: "cfrFeatures",
notification_text: { string_id: "cfr-doorhanger-extension-notification" },
heading_text: { string_id: "cfr-doorhanger-send-tab-recipe-header" },
info_icon: {
label: { string_id: "cfr-doorhanger-extension-sumo-link" },
sumo_path: "https://example.com",
},
text: { string_id: "cfr-doorhanger-send-tab-body" },
icon: "chrome://branding/content/icon64.png",
buttons: {
primary: {
label: { string_id: "cfr-doorhanger-send-tab-ok-button" },
action: {
type: "HIGHLIGHT_FEATURE",
data: { args: "pageAction-sendToDevice" },
},
},
secondary: [
{
label: { string_id: "cfr-doorhanger-extension-cancel-button" },
action: { type: "CANCEL" },
},
{
label: {
string_id: "cfr-doorhanger-extension-never-show-recommendation",
},
},
{
label: {
string_id: "cfr-doorhanger-extension-manage-settings-button",
},
action: {
type: "OPEN_PREFERENCES_PAGE",
data: { category: "general-cfrfeatures" },
},
},
],
},
},
targeting: "true",
trigger: {
id: "openArticleURL",
params: ["www.allrecipes.com", "allrecipes.com"],
},
},
];
const PanelTestProvider = {

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

@ -255,6 +255,9 @@ class PlacesFeed {
/**
* observe - An observer for the LINK_BLOCKED_EVENT.
* Called when a link is blocked.
* Links can be blocked outside of newtab,
* which is why we need to listen to this
* on such a generic level.
*
* @param {null} subject
* @param {str} topic The name of the event

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

@ -3,57 +3,25 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.defineModuleGetter(
this,
"EveryWindow",
"resource:///modules/EveryWindow.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ToolbarPanelHub",
"resource://activity-stream/lib/ToolbarPanelHub.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"setTimeout",
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"clearTimeout",
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"setInterval",
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"clearInterval",
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"requestIdleCallback",
"resource://gre/modules/Timer.jsm"
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
EveryWindow: "resource:///modules/EveryWindow.jsm",
ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
Services: "resource://gre/modules/Services.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
});
const {
setInterval,
clearInterval,
requestIdleCallback,
setTimeout,
clearTimeout,
} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
// Frequency at which to check for new messages
const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
let notificationsByWindow = new WeakMap();
@ -151,6 +119,10 @@ class _ToolbarBadgeHub {
}
}
maybeInsertFTL(win) {
win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
}
executeAction({ id, data, message_id }) {
switch (id) {
case "show-whatsnew-button":
@ -234,6 +206,15 @@ class _ToolbarBadgeHub {
.querySelector(".toolbarbutton-badge")
.classList.remove("feature-callout");
toolbarButton.removeAttribute("badged");
// Remove id used for for aria-label badge description
const notificationDescription = toolbarButton.querySelector(
"#toolbarbutton-notification-description"
);
if (notificationDescription) {
notificationDescription.remove();
toolbarButton.removeAttribute("aria-labelledby");
toolbarButton.removeAttribute("aria-describedby");
}
}
addToolbarNotification(win, message) {
@ -243,11 +224,40 @@ class _ToolbarBadgeHub {
}
let toolbarbutton = document.getElementById(message.content.target);
if (toolbarbutton) {
const badge = toolbarbutton.querySelector(".toolbarbutton-badge");
badge.classList.add("feature-callout");
toolbarbutton.setAttribute("badged", true);
toolbarbutton
.querySelector(".toolbarbutton-badge")
.classList.add("feature-callout");
// If we have additional aria-label information for the notification
// we add this content to the hidden `toolbarbutton-text` node.
// We then use `aria-labelledby` to link this description to the button
// that received the notification badge.
if (message.content.badgeDescription) {
// Insert strings as soon as we know we're showing them
this.maybeInsertFTL(win);
toolbarbutton.setAttribute(
"aria-labelledby",
`toolbarbutton-notification-description ${message.content.target}`
);
// Because tooltiptext is different to the label, it gets duplicated as
// the description. Setting `describedby` to the same value as
// `labelledby` will be detected by the a11y code and the description
// will be removed.
toolbarbutton.setAttribute(
"aria-describedby",
`toolbarbutton-notification-description ${message.content.target}`
);
const descriptionEl = document.createElement("span");
descriptionEl.setAttribute(
"id",
"toolbarbutton-notification-description"
);
descriptionEl.setAttribute("hidden", true);
document.l10n.setAttributes(
descriptionEl,
message.content.badgeDescription.string_id
);
toolbarbutton.appendChild(descriptionEl);
}
// `mousedown` event required because of the `onmousedown` defined on
// the button that prevents `click` events from firing
toolbarbutton.addEventListener("mousedown", this.removeAllNotifications);

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

@ -86,6 +86,10 @@ cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel
# This string is used by screen readers to offer a text based alternative for
# the notification icon
cfr-badge-reader-label-newfeature = New feature:
cfr-whatsnew-button =
.label = Whats New
.tooltiptext = Whats New

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

@ -32,11 +32,22 @@ 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
.aria-label = Dismiss
## 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
@ -98,7 +109,7 @@ onboarding-data-sync-text2 = Sync your bookmarks, passwords, and more everywhere
onboarding-data-sync-button2 = Sign in to { -sync-brand-short-name }
onboarding-firefox-monitor-title = Stay Alert to Data Breaches
onboarding-firefox-monitor-text = { -monitor-brand-name } monitors if your email has appeared in a data breach and alerts you if it appears in a new breach.
onboarding-firefox-monitor-text2 = { -monitor-brand-name } monitors if your email has appeared in a known data breach and alerts you if it appears in a new breach.
onboarding-firefox-monitor-button = Sign up for Alerts
onboarding-browse-privately-title = Browse Privately

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

@ -102,6 +102,31 @@ add_task(async function test_trailhead_branches() {
]
);
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(
"cards-multidevice",
// Expected selectors:

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

@ -37,6 +37,11 @@ ChromeUtils.defineModuleGetter(
"TelemetryEnvironment",
"resource://gre/modules/TelemetryEnvironment.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AppConstants",
"resource://gre/modules/AppConstants.jsm"
);
// ASRouterTargeting.isMatch
add_task(async function should_do_correct_targeting() {
@ -445,6 +450,18 @@ add_task(async function checkdevToolsOpenedCount() {
);
});
add_task(async function check_platformName() {
const message = {
id: "foo",
targeting: `platformName == "${AppConstants.platform}"`,
};
is(
await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
message,
"should select correct item by platformName"
);
});
AddonTestUtils.initMochitest(this);
add_task(async function checkAddonsInfo() {

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

@ -20,6 +20,41 @@ async function openURLInWindow(window, url) {
await BrowserTestUtils.browserLoaded(selectedBrowser, false, url);
}
add_task(async function check_openArticleURL() {
const TEST_URL =
"https://example.com/browser/browser/components/newtab/test/browser/red_page.html";
const articleTrigger = ASRouterTriggerListeners.get("openArticleURL");
// Previously initialized by the Router
articleTrigger.uninit();
// Initialize the trigger with a new triggerHandler that resolves a promise
// with the URL match
const listenerTriggered = new Promise(resolve =>
articleTrigger.init((browser, match) => resolve(match), ["example.com"])
);
const win = await BrowserTestUtils.openNewBrowserWindow();
await openURLInWindow(win, TEST_URL);
// Send a message from the content page (the TEST_URL) to the parent
// This should trigger the `receiveMessage` cb in the articleTrigger
await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
});
await listenerTriggered.then(data =>
is(
data.param.url,
TEST_URL,
"We should match on the TEST_URL as a website article"
)
);
// Cleanup
articleTrigger.uninit();
await BrowserTestUtils.closeWindow(win);
});
add_task(async function check_openURL_listener() {
const TEST_URL =
"https://example.com/browser/browser/components/newtab/test/browser/red_page.html";

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

@ -33,8 +33,23 @@ async function setRTAMOOnboarding() {
ASRouter._updateMessageProviders();
await ASRouter.loadMessagesFromAllProviders();
registerCleanupFunction(() => {
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);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
});
}

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

@ -120,6 +120,9 @@ export const UserEventAction = Joi.object().keys({
"ARCHIVE_FROM_POCKET",
"SKIPPED_SIGNIN",
"SUBMIT_EMAIL",
"SUBMIT_SIGNIN",
"SHOW_PRIVACY_INFO",
"CLICK_PRIVACY_INFO",
]).required(),
source: Joi.valid(["TOP_SITES", "TOP_STORIES", "HIGHLIGHTS"]),
action_position: Joi.number().integer(),

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

@ -2304,9 +2304,17 @@ describe("ASRouter", () => {
describe("#UITour", () => {
let showMenuStub;
const highlightTarget = { target: "target" };
beforeEach(() => {
showMenuStub = sandbox.stub();
globals.set("UITour", { showMenu: showMenuStub });
globals.set("UITour", {
showMenu: showMenuStub,
getTarget: sandbox
.stub()
.withArgs(sinon.match.object, "pageAaction-sendToDevice")
.resolves(highlightTarget),
showHighlight: sandbox.stub(),
});
});
it("should call UITour.showMenu with the correct params on OPEN_APPLICATIONS_MENU", async () => {
const msg = fakeExecuteUserAction({
@ -2322,6 +2330,21 @@ describe("ASRouter", () => {
"appMenu"
);
});
it("should call UITour.showHighlight with the correct params on HIGHLIGHT_FEATURE", async () => {
const msg = fakeExecuteUserAction({
type: "HIGHLIGHT_FEATURE",
data: { args: "pageAction-sendToDevice" },
});
await Router.onMessage(msg);
assert.calledOnce(UITour.getTarget);
assert.calledOnce(UITour.showHighlight);
assert.calledWith(
UITour.showHighlight,
msg.target.browser.ownerGlobal,
highlightTarget
);
});
});
describe("valid preview endpoint", () => {

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

@ -12,6 +12,7 @@ describe("ASRouterTriggerListeners", () => {
const bookmarkedURLListener = ASRouterTriggerListeners.get(
"openBookmarkedURL"
);
const openArticleURLListener = ASRouterTriggerListeners.get("openArticleURL");
const hosts = ["www.mozilla.com", "www.mozilla.org"];
beforeEach(async () => {
@ -91,6 +92,75 @@ describe("ASRouterTriggerListeners", () => {
});
});
describe("openArticleURL", () => {
describe("#init", () => {
beforeEach(() => {
globals.set(
"MatchPatternSet",
sandbox.stub().callsFake(patterns => ({
patterns: new Set(patterns),
matches: url => patterns.includes(url),
}))
);
sandbox.stub(global.Services.mm, "addMessageListener");
sandbox.stub(global.Services.mm, "removeMessageListener");
});
afterEach(() => {
openArticleURLListener.uninit();
});
it("setup an event listener on init", () => {
openArticleURLListener.init(sandbox.stub(), hosts, hosts);
assert.calledOnce(global.Services.mm.addMessageListener);
assert.calledWithExactly(
global.Services.mm.addMessageListener,
openArticleURLListener.readerModeEvent,
sinon.match.object
);
});
it("should call triggerHandler correctly for matches [host match]", () => {
const stub = sandbox.stub();
const target = { currentURI: { host: hosts[0], spec: hosts[1] } };
openArticleURLListener.init(stub, hosts, hosts);
const [
,
{ receiveMessage },
] = global.Services.mm.addMessageListener.firstCall.args;
receiveMessage({ data: { isArticle: true }, target });
assert.calledOnce(stub);
assert.calledWithExactly(stub, target, {
id: openArticleURLListener.id,
param: { host: hosts[0], url: hosts[1] },
});
});
it("should call triggerHandler correctly for matches [pattern match]", () => {
const stub = sandbox.stub();
const target = { currentURI: { host: null, spec: hosts[1] } };
openArticleURLListener.init(stub, hosts, hosts);
const [
,
{ receiveMessage },
] = global.Services.mm.addMessageListener.firstCall.args;
receiveMessage({ data: { isArticle: true }, target });
assert.calledOnce(stub);
assert.calledWithExactly(stub, target, {
id: openArticleURLListener.id,
param: { host: null, url: hosts[1] },
});
});
it("should remove the message listener", () => {
openArticleURLListener.init(sandbox.stub(), hosts, hosts);
openArticleURLListener.uninit();
assert.calledOnce(global.Services.mm.removeMessageListener);
});
});
});
describe("frequentVisits", () => {
let _triggerHandler;
beforeEach(() => {

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

@ -8,7 +8,7 @@ describe("PanelTestProvider", () => {
it("should have a message", () => {
// Careful: when changing this number make sure that new messages also go
// through schema verifications.
assert.lengthOf(messages, 9);
assert.lengthOf(messages, 11);
});
it("should be a valid message", () => {
const fxaMessages = messages.filter(

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

@ -0,0 +1,120 @@
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 sendTelemetryStub;
beforeEach(async () => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
onBlock = sandbox.stub();
onAction = 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}
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",
});
});
});

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

@ -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 },
})
);
});
});

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

@ -1,3 +1,4 @@
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";
@ -20,6 +21,14 @@ describe("<Interrupt>", () => {
);
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(

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

@ -1,4 +1,5 @@
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";
@ -75,6 +76,10 @@ describe("<Trailhead>", () => {
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());

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

@ -2,7 +2,7 @@ import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDi
import React from "react";
import { shallow } from "enzyme";
describe("<DSTextPromo>", () => {
describe("<DSDismiss>", () => {
const fakeSpoc = {
url: "https://foo.com",
guid: "1234",

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

@ -111,4 +111,23 @@ describe("Discovery Stream <DSImage>", () => {
assert.calledOnce(observer.unobserve);
});
describe("DSImage with Idle Callback", () => {
let wrapper;
let windowStub = {
requestIdleCallback: sinon.stub().returns(1),
cancelIdleCallback: sinon.stub(),
};
beforeEach(() => {
wrapper = mount(<DSImage windowObj={windowStub} />);
});
it("should call requestIdleCallback on componentDidMount", () => {
assert.calledOnce(windowStub.requestIdleCallback);
});
it("should call cancelIdleCallback on componentWillUnmount", () => {
wrapper.instance().componentWillUnmount();
assert.calledOnce(windowStub.cancelIdleCallback);
});
});
});

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

@ -1,11 +1,20 @@
import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal";
import { shallow, mount } from "enzyme";
import { actionCreators as ac } from "common/Actions.jsm";
import React from "react";
describe("Discovery Stream <DSPrivacyModal>", () => {
let sandbox;
let dispatch;
let wrapper;
beforeEach(() => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
wrapper = shallow(<DSPrivacyModal dispatch={dispatch} />);
});
afterEach(() => {
sandbox.restore();
});
it("should contain a privacy notice", () => {
@ -16,10 +25,20 @@ describe("Discovery Stream <DSPrivacyModal>", () => {
});
it("should call dispatch when modal is closed", () => {
let dispatch = sandbox.stub();
let wrapper = shallow(<DSPrivacyModal dispatch={dispatch} />);
wrapper.instance().closeModal();
assert.calledOnce(dispatch);
});
it("should call dispatch with the correct events", () => {
wrapper.instance().onLinkClick();
assert.calledOnce(dispatch);
assert.calledWith(
dispatch,
ac.UserEvent({
event: "CLICK_PRIVACY_INFO",
source: "DS_PRIVACY_MODAL",
})
);
});
});

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

@ -892,7 +892,7 @@ describe("DiscoveryStreamFeed", () => {
first: Date.now() - recsExpireTime * 1000,
third: Date.now(),
};
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
const result = feed.rotate(
feedResponse.recommendations,
@ -1107,7 +1107,7 @@ describe("DiscoveryStreamFeed", () => {
const fakeImpressions = {
seen: [Date.now() - 1],
};
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
const { data: result, filtered } = feed.frequencyCapSpocs(fakeSpocs);
@ -1224,16 +1224,29 @@ describe("DiscoveryStreamFeed", () => {
describe("#recordCampaignImpression", () => {
it("should return false if time based cap is hit", () => {
sandbox.stub(feed, "readImpressionsPref").returns({});
sandbox.stub(feed, "writeImpressionsPref").returns();
sandbox.stub(feed, "readDataPref").returns({});
sandbox.stub(feed, "writeDataPref").returns();
feed.recordCampaignImpression("seen");
assert.calledWith(
feed.writeImpressionsPref,
SPOC_IMPRESSION_TRACKING_PREF,
{ seen: [0] }
);
assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
seen: [0],
});
});
});
describe("#recordBlockCampaignId", () => {
it("should call writeDataPref with new campaign id added", () => {
sandbox.stub(feed, "readDataPref").returns({ "1234": 1 });
sandbox.stub(feed, "writeDataPref").returns();
feed.recordBlockCampaignId("5678");
assert.calledOnce(feed.readDataPref);
assert.calledWith(feed.writeDataPref, "discoverystream.campaign.blocks", {
"1234": 1,
"5678": 1,
});
});
});
@ -1267,39 +1280,35 @@ describe("DiscoveryStreamFeed", () => {
"campaign-2": [Date.now() - 1],
"campaign-3": [Date.now() - 1],
};
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "writeImpressionsPref").returns();
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.stub(feed, "writeDataPref").returns();
feed.cleanUpCampaignImpressionPref(fakeSpocs);
assert.calledWith(
feed.writeImpressionsPref,
SPOC_IMPRESSION_TRACKING_PREF,
{ "campaign-2": [-1] }
);
assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
"campaign-2": [-1],
});
});
});
describe("#recordTopRecImpressions", () => {
it("should add a rec id to the rec impression pref", () => {
sandbox.stub(feed, "readImpressionsPref").returns({});
sandbox.stub(feed, "writeImpressionsPref");
sandbox.stub(feed, "readDataPref").returns({});
sandbox.stub(feed, "writeDataPref");
feed.recordTopRecImpressions("rec");
assert.calledWith(
feed.writeImpressionsPref,
REC_IMPRESSION_TRACKING_PREF,
{ rec: 0 }
);
assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {
rec: 0,
});
});
it("should not add an impression if it already exists", () => {
sandbox.stub(feed, "readImpressionsPref").returns({ rec: 4 });
sandbox.stub(feed, "writeImpressionsPref");
sandbox.stub(feed, "readDataPref").returns({ rec: 4 });
sandbox.stub(feed, "writeDataPref");
feed.recordTopRecImpressions("rec");
assert.notCalled(feed.writeImpressionsPref);
assert.notCalled(feed.writeDataPref);
});
});
@ -1336,20 +1345,19 @@ describe("DiscoveryStreamFeed", () => {
rec3: Date.now() - 1,
rec5: Date.now() - 1,
};
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "writeImpressionsPref").returns();
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.stub(feed, "writeDataPref").returns();
feed.cleanUpTopRecImpressionPref(newFeeds);
assert.calledWith(
feed.writeImpressionsPref,
REC_IMPRESSION_TRACKING_PREF,
{ rec2: -1, rec3: -1 }
);
assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {
rec2: -1,
rec3: -1,
});
});
});
describe("#writeImpressionsPref", () => {
describe("#writeDataPref", () => {
it("should call Services.prefs.setStringPref", () => {
sandbox.spy(feed.store, "dispatch");
const fakeImpressions = {
@ -1357,7 +1365,7 @@ describe("DiscoveryStreamFeed", () => {
bar: [Date.now() - 1],
};
feed.writeImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);
feed.writeDataPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);
assert.calledWithMatch(feed.store.dispatch, {
data: {
@ -1369,7 +1377,7 @@ describe("DiscoveryStreamFeed", () => {
});
});
describe("#readImpressionsPref", () => {
describe("#readDataPref", () => {
it("should return what's in Services.prefs.getStringPref", () => {
const fakeImpressions = {
foo: [Date.now() - 1],
@ -1377,7 +1385,7 @@ describe("DiscoveryStreamFeed", () => {
};
setPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);
const result = feed.readImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF);
const result = feed.readDataPref(SPOC_IMPRESSION_TRACKING_PREF);
assert.deepEqual(result, fakeImpressions);
});
@ -1463,7 +1471,7 @@ describe("DiscoveryStreamFeed", () => {
];
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
await feed.onAction({
@ -1484,7 +1492,7 @@ describe("DiscoveryStreamFeed", () => {
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {};
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
await feed.onAction({
@ -1499,7 +1507,7 @@ describe("DiscoveryStreamFeed", () => {
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {};
sandbox.stub(feed, "recordCampaignImpression").returns();
sandbox.stub(feed, "readImpressionsPref").returns(fakeImpressions);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.spy(feed.store, "dispatch");
sandbox.spy(feed, "frequencyCapSpocs");
@ -1622,6 +1630,21 @@ describe("DiscoveryStreamFeed", () => {
});
});
describe("#onAction: BLOCK_URL", () => {
it("should call recordBlockCampaignId whith BLOCK_URL", async () => {
sandbox.stub(feed, "recordBlockCampaignId").returns();
await feed.onAction({
type: at.BLOCK_URL,
data: {
campaign_id: "1234",
},
});
assert.calledWith(feed.recordBlockCampaignId, "1234");
});
});
describe("#onAction: INIT", () => {
it("should be .loaded=false before initialization", () => {
assert.isFalse(feed.loaded);
@ -1725,7 +1748,7 @@ describe("DiscoveryStreamFeed", () => {
assert.calledOnce(feed.resetCache);
});
it("should dispatch DISCOVERY_STREAM_LAYOUT_RESET from DISCOVERY_STREAM_CONFIG_CHANGE", async () => {
sandbox.stub(feed, "resetImpressionPrefs");
sandbox.stub(feed, "resetDataPrefs");
sandbox.stub(feed, "resetCache").resolves();
sandbox.stub(feed, "enable").resolves();
setPref(CONFIG_PREF_NAME, { enabled: true });

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

@ -1,7 +1,7 @@
import { _ToolbarBadgeHub } from "lib/ToolbarBadgeHub.jsm";
import { GlobalOverrider } from "test/unit/utils";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
import { _ToolbarPanelHub, ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
describe("ToolbarBadgeHub", () => {
let sandbox;
@ -23,6 +23,7 @@ describe("ToolbarBadgeHub", () => {
let clearUserPrefStub;
let setStringPrefStub;
let requestIdleCallbackStub;
let fakeWindow;
beforeEach(async () => {
globals = new GlobalOverrider();
sandbox = sinon.createSandbox();
@ -44,6 +45,8 @@ describe("ToolbarBadgeHub", () => {
removeAttribute: sandbox.stub(),
querySelector: sandbox.stub(),
addEventListener: sandbox.stub(),
remove: sandbox.stub(),
appendChild: sandbox.stub(),
};
// Share the same element when selecting child nodes
fakeElement.querySelector.returns(fakeElement);
@ -54,7 +57,8 @@ describe("ToolbarBadgeHub", () => {
clearTimeoutStub = sandbox.stub();
setTimeoutStub = sandbox.stub();
setIntervalStub = sandbox.stub();
const fakeWindow = {
fakeWindow = {
MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
ownerGlobal: {
gBrowser: {
selectedBrowser: "browser",
@ -68,6 +72,7 @@ describe("ToolbarBadgeHub", () => {
setStringPrefStub = sandbox.stub();
requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
globals.set({
ToolbarPanelHub,
requestIdleCallback: requestIdleCallbackStub,
EveryWindow: everyWindowStub,
PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
@ -202,8 +207,12 @@ describe("ToolbarBadgeHub", () => {
let target;
let fakeDocument;
beforeEach(() => {
fakeDocument = { getElementById: sandbox.stub().returns(fakeElement) };
target = { browser: { ownerDocument: fakeDocument } };
fakeDocument = {
getElementById: sandbox.stub().returns(fakeElement),
createElement: sandbox.stub().returns(fakeElement),
l10n: { setAttributes: sandbox.stub() },
};
target = { ...fakeWindow, browser: { ownerDocument: fakeDocument } };
});
it("shouldn't do anything if target element is not found", () => {
fakeDocument.getElementById.returns(null);
@ -252,6 +261,41 @@ describe("ToolbarBadgeHub", () => {
message_id: whatsnewMessage.id,
});
});
it("should create a description element", () => {
sandbox.stub(instance, "executeAction");
instance.addToolbarNotification(target, whatsnewMessage);
assert.calledOnce(fakeDocument.createElement);
assert.calledWithExactly(fakeDocument.createElement, "span");
});
it("should set description id to element and to button", () => {
sandbox.stub(instance, "executeAction");
instance.addToolbarNotification(target, whatsnewMessage);
assert.calledWithExactly(
fakeElement.setAttribute,
"id",
"toolbarbutton-notification-description"
);
assert.calledWithExactly(
fakeElement.setAttribute,
"aria-labelledby",
`toolbarbutton-notification-description ${
whatsnewMessage.content.target
}`
);
});
it("should attach fluent id to description", () => {
sandbox.stub(instance, "executeAction");
instance.addToolbarNotification(target, whatsnewMessage);
assert.calledOnce(fakeDocument.l10n.setAttributes);
assert.calledWithExactly(
fakeDocument.l10n.setAttributes,
fakeElement,
whatsnewMessage.content.badgeDescription.string_id
);
});
});
describe("registerBadgeNotificationListener", () => {
let msg_no_delay;
@ -418,10 +462,13 @@ describe("ToolbarBadgeHub", () => {
it("should remove the notification", () => {
instance.removeToolbarNotification(fakeElement);
assert.calledOnce(fakeElement.removeAttribute);
assert.calledThrice(fakeElement.removeAttribute);
assert.calledWithExactly(fakeElement.removeAttribute, "badged");
assert.calledWithExactly(fakeElement.removeAttribute, "aria-labelledby");
assert.calledWithExactly(fakeElement.removeAttribute, "aria-describedby");
assert.calledOnce(fakeElement.classList.remove);
assert.calledWithExactly(fakeElement.classList.remove, "feature-callout");
assert.calledOnce(fakeElement.remove);
});
});
describe("removeAllNotifications", () => {

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

@ -209,7 +209,7 @@ const TEST_GLOBAL = {
},
urlFormatter: { formatURL: str => str, formatURLPref: str => str },
mm: {
addMessageListener: (msg, cb) => cb(),
addMessageListener: (msg, cb) => this.receiveMessage(),
removeMessageListener() {},
},
appShell: { hiddenDOMWindow: { performance: new FakePerformance() } },

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

@ -86,6 +86,10 @@ cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel
# This string is used by screen readers to offer a text based alternative for
# the notification icon
cfr-badge-reader-label-newfeature = New feature:
cfr-whatsnew-button =
.label = Whats New
.tooltiptext = Whats New

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

@ -32,11 +32,22 @@ 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
.aria-label = Dismiss
## 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
@ -98,7 +109,7 @@ onboarding-data-sync-text2 = Sync your bookmarks, passwords, and more everywhere
onboarding-data-sync-button2 = Sign in to { -sync-brand-short-name }
onboarding-firefox-monitor-title = Stay Alert to Data Breaches
onboarding-firefox-monitor-text = { -monitor-brand-name } monitors if your email has appeared in a data breach and alerts you if it appears in a new breach.
onboarding-firefox-monitor-text2 = { -monitor-brand-name } monitors if your email has appeared in a known data breach and alerts you if it appears in a new breach.
onboarding-firefox-monitor-button = Sign up for Alerts
onboarding-browse-privately-title = Browse Privately