зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changeset 2cb4cc20ea9d (bug 1569306) for gecko decision bustage CLOSED TREE
This commit is contained in:
Родитель
97c5d52826
Коммит
dfedc5dd8a
|
@ -44,7 +44,6 @@ module.exports = {
|
||||||
// These files use fluent-dom to insert content
|
// These files use fluent-dom to insert content
|
||||||
"files": [
|
"files": [
|
||||||
"content-src/asrouter/templates/OnboardingMessage/**",
|
"content-src/asrouter/templates/OnboardingMessage/**",
|
||||||
"content-src/asrouter/templates/FirstRun/**",
|
|
||||||
"content-src/asrouter/templates/Trailhead/**",
|
"content-src/asrouter/templates/Trailhead/**",
|
||||||
"content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
|
"content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
|
||||||
"content-src/components/TopSites/**",
|
"content-src/components/TopSites/**",
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
# flod as main contact for string changes
|
# flod as main contact for string changes
|
||||||
locales-src/ @flodolo
|
locales-src/en-US/strings.properties @flodolo
|
||||||
|
|
|
@ -8,19 +8,17 @@ import { generateBundles } from "./rich-text-strings";
|
||||||
import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
|
import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
|
||||||
import { LocalizationProvider } from "fluent-react";
|
import { LocalizationProvider } from "fluent-react";
|
||||||
import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
|
import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
|
||||||
|
import { OnboardingMessage } from "./templates/OnboardingMessage/OnboardingMessage";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { ReturnToAMO } from "./templates/ReturnToAMO/ReturnToAMO";
|
||||||
import { SnippetsTemplates } from "./templates/template-manifest";
|
import { SnippetsTemplates } from "./templates/template-manifest";
|
||||||
import { FirstRun } from "./templates/FirstRun/FirstRun";
|
import { StartupOverlay } from "./templates/StartupOverlay/StartupOverlay";
|
||||||
|
import { Trailhead } from "./templates/Trailhead/Trailhead";
|
||||||
|
|
||||||
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
||||||
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
||||||
const TEMPLATES_ABOVE_PAGE = [
|
const TEMPLATES_ABOVE_PAGE = ["trailhead"];
|
||||||
"trailhead",
|
|
||||||
"fxa_overlay",
|
|
||||||
"return_to_amo_overlay",
|
|
||||||
];
|
|
||||||
const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
|
|
||||||
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
|
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
|
||||||
|
|
||||||
export const ASRouterUtils = {
|
export const ASRouterUtils = {
|
||||||
|
@ -48,6 +46,9 @@ export const ASRouterUtils = {
|
||||||
dismissById(id) {
|
dismissById(id) {
|
||||||
ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
|
ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
|
||||||
},
|
},
|
||||||
|
dismissBundle(bundle) {
|
||||||
|
ASRouterUtils.sendMessage({ type: "DISMISS_BUNDLE", data: { bundle } });
|
||||||
|
},
|
||||||
executeAction(button_action) {
|
executeAction(button_action) {
|
||||||
ASRouterUtils.sendMessage({
|
ASRouterUtils.sendMessage({
|
||||||
type: "USER_ACTION",
|
type: "USER_ACTION",
|
||||||
|
@ -108,7 +109,7 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
this.sendClick = this.sendClick.bind(this);
|
this.sendClick = this.sendClick.bind(this);
|
||||||
this.sendImpression = this.sendImpression.bind(this);
|
this.sendImpression = this.sendImpression.bind(this);
|
||||||
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
|
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
|
||||||
this.state = { message: {} };
|
this.state = { message: {}, bundle: {} };
|
||||||
if (props.document) {
|
if (props.document) {
|
||||||
this.headerPortal = props.document.getElementById(
|
this.headerPortal = props.document.getElementById(
|
||||||
"header-asrouter-container"
|
"header-asrouter-container"
|
||||||
|
@ -120,10 +121,14 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUserActionTelemetry(extraProps = {}) {
|
sendUserActionTelemetry(extraProps = {}) {
|
||||||
const { message } = this.state;
|
const { message, bundle } = this.state;
|
||||||
const eventType = `${message.provider}_user_event`;
|
if (!message && !extraProps.message_id) {
|
||||||
|
throw new Error(`You must provide a message_id for bundled messages`);
|
||||||
|
}
|
||||||
|
// snippets_user_event, onboarding_user_event
|
||||||
|
const eventType = `${message.provider || bundle.provider}_user_event`;
|
||||||
ASRouterUtils.sendTelemetry({
|
ASRouterUtils.sendTelemetry({
|
||||||
message_id: message.id,
|
message_id: message.id || extraProps.message_id,
|
||||||
source: extraProps.id,
|
source: extraProps.id,
|
||||||
action: eventType,
|
action: eventType,
|
||||||
...extraProps,
|
...extraProps,
|
||||||
|
@ -175,6 +180,26 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
return () => ASRouterUtils.dismissById(id);
|
return () => ASRouterUtils.dismissById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissBundle(bundle) {
|
||||||
|
return () => {
|
||||||
|
ASRouterUtils.dismissBundle(bundle);
|
||||||
|
this.sendUserActionTelemetry({
|
||||||
|
event: "DISMISS",
|
||||||
|
id: "onboarding-cards",
|
||||||
|
message_id: bundle.map(m => m.id).join(","),
|
||||||
|
// Passing the action because some bundles (Trailhead) don't have a provider set
|
||||||
|
action: "onboarding_user_event",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerOnboarding() {
|
||||||
|
ASRouterUtils.sendMessage({
|
||||||
|
type: "TRIGGER",
|
||||||
|
data: { trigger: { id: "showOnboarding" } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
clearMessage(id) {
|
clearMessage(id) {
|
||||||
if (id === this.state.message.id) {
|
if (id === this.state.message.id) {
|
||||||
this.setState({ message: {} });
|
this.setState({ message: {} });
|
||||||
|
@ -188,6 +213,9 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
case "SET_MESSAGE":
|
case "SET_MESSAGE":
|
||||||
this.setState({ message: action.data });
|
this.setState({ message: action.data });
|
||||||
break;
|
break;
|
||||||
|
case "SET_BUNDLED_MESSAGES":
|
||||||
|
this.setState({ bundle: action.data });
|
||||||
|
break;
|
||||||
case "CLEAR_MESSAGE":
|
case "CLEAR_MESSAGE":
|
||||||
this.clearMessage(action.data.id);
|
this.clearMessage(action.data.id);
|
||||||
break;
|
break;
|
||||||
|
@ -196,8 +224,13 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
this.setState({ message: {} });
|
this.setState({ message: {} });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "CLEAR_BUNDLE":
|
||||||
|
if (this.state.bundle.bundle) {
|
||||||
|
this.setState({ bundle: {} });
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "CLEAR_ALL":
|
case "CLEAR_ALL":
|
||||||
this.setState({ message: {} });
|
this.setState({ message: {}, bundle: {} });
|
||||||
break;
|
break;
|
||||||
case "AS_ROUTER_TARGETING_UPDATE":
|
case "AS_ROUTER_TARGETING_UPDATE":
|
||||||
action.data.forEach(id => this.clearMessage(id));
|
action.data.forEach(id => this.clearMessage(id));
|
||||||
|
@ -238,11 +271,18 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSnippets() {
|
renderSnippets() {
|
||||||
const { message } = this.state;
|
if (
|
||||||
if (!SnippetsTemplates[message.template]) {
|
this.state.bundle.template === "onboarding" ||
|
||||||
|
[
|
||||||
|
"fxa_overlay",
|
||||||
|
"return_to_amo_overlay",
|
||||||
|
"trailhead",
|
||||||
|
"whatsnew_panel_message",
|
||||||
|
].includes(this.state.message.template)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const SnippetComponent = SnippetsTemplates[message.template];
|
const SnippetComponent = SnippetsTemplates[this.state.message.template];
|
||||||
const { content } = this.state.message;
|
const { content } = this.state.message;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -269,6 +309,70 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderOnboarding() {
|
||||||
|
if (this.state.bundle.template === "onboarding") {
|
||||||
|
return (
|
||||||
|
<OnboardingMessage
|
||||||
|
{...this.state.bundle}
|
||||||
|
UISurface="NEWTAB_OVERLAY"
|
||||||
|
onAction={ASRouterUtils.executeAction}
|
||||||
|
onDismissBundle={this.dismissBundle(this.state.bundle.bundle)}
|
||||||
|
sendUserActionTelemetry={this.sendUserActionTelemetry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFirstRunOverlay() {
|
||||||
|
const { message } = this.state;
|
||||||
|
if (message.template === "fxa_overlay") {
|
||||||
|
global.document.body.classList.add("fxa");
|
||||||
|
return (
|
||||||
|
<StartupOverlay
|
||||||
|
onReady={this.triggerOnboarding}
|
||||||
|
onBlock={this.onDismissById(message.id)}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (message.template === "return_to_amo_overlay") {
|
||||||
|
global.document.body.classList.add("amo");
|
||||||
|
return (
|
||||||
|
<LocalizationProvider
|
||||||
|
bundles={generateBundles({ amo_html: message.content.text })}
|
||||||
|
>
|
||||||
|
<ReturnToAMO
|
||||||
|
{...message}
|
||||||
|
UISurface="NEWTAB_OVERLAY"
|
||||||
|
onReady={this.triggerOnboarding}
|
||||||
|
onBlock={this.onDismissById(message.id)}
|
||||||
|
onAction={ASRouterUtils.executeAction}
|
||||||
|
sendUserActionTelemetry={this.sendUserActionTelemetry}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTrailhead() {
|
||||||
|
const { message } = this.state;
|
||||||
|
if (message.template === "trailhead") {
|
||||||
|
return (
|
||||||
|
<Trailhead
|
||||||
|
document={this.props.document}
|
||||||
|
message={message}
|
||||||
|
onAction={ASRouterUtils.executeAction}
|
||||||
|
onDismissBundle={this.dismissBundle(this.state.message.bundle)}
|
||||||
|
sendUserActionTelemetry={this.sendUserActionTelemetry}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
fxaEndpoint={this.props.fxaEndpoint}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
renderPreviewBanner() {
|
renderPreviewBanner() {
|
||||||
if (this.state.message.provider !== "preview") {
|
if (this.state.message.provider !== "preview") {
|
||||||
return null;
|
return null;
|
||||||
|
@ -282,27 +386,9 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFirstRun() {
|
|
||||||
const { message } = this.state;
|
|
||||||
if (FIRST_RUN_TEMPLATES.includes(message.template)) {
|
|
||||||
return (
|
|
||||||
<FirstRun
|
|
||||||
document={this.props.document}
|
|
||||||
message={message}
|
|
||||||
sendUserActionTelemetry={this.sendUserActionTelemetry}
|
|
||||||
executeAction={ASRouterUtils.executeAction}
|
|
||||||
dispatch={this.props.dispatch}
|
|
||||||
onDismiss={this.onDismissById(this.state.message.id)}
|
|
||||||
fxaEndpoint={this.props.fxaEndpoint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { message } = this.state;
|
const { message, bundle } = this.state;
|
||||||
if (!message.id) {
|
if (!message.id && !bundle.template) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
|
const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
|
||||||
|
@ -321,7 +407,9 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
ReactDOM.createPortal(
|
ReactDOM.createPortal(
|
||||||
<>
|
<>
|
||||||
{this.renderPreviewBanner()}
|
{this.renderPreviewBanner()}
|
||||||
{this.renderFirstRun()}
|
{this.renderTrailhead()}
|
||||||
|
{this.renderFirstRunOverlay()}
|
||||||
|
{this.renderOnboarding()}
|
||||||
{this.renderSnippets()}
|
{this.renderSnippets()}
|
||||||
</>,
|
</>,
|
||||||
shouldRenderInHeader ? this.headerPortal : this.footerPortal
|
shouldRenderInHeader ? this.headerPortal : this.footerPortal
|
||||||
|
|
|
@ -1,227 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Interrupt } from "./Interrupt";
|
|
||||||
import { Triplets } from "./Triplets";
|
|
||||||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
|
||||||
import { addUtmParams } from "./addUtmParams";
|
|
||||||
|
|
||||||
export const FLUENT_FILES = [
|
|
||||||
"branding/brand.ftl",
|
|
||||||
"browser/branding/brandings.ftl",
|
|
||||||
"browser/branding/sync-brand.ftl",
|
|
||||||
"browser/newtab/onboarding.ftl",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const helpers = {
|
|
||||||
selectInterruptAndTriplets(message = {}) {
|
|
||||||
const hasInterrupt = Boolean(message.content);
|
|
||||||
const hasTriplets = Boolean(message.bundle && message.bundle.length);
|
|
||||||
const UTMTerm = message.utm_term || "";
|
|
||||||
return {
|
|
||||||
hasTriplets,
|
|
||||||
hasInterrupt,
|
|
||||||
interrupt: hasInterrupt ? message : null,
|
|
||||||
triplets: hasTriplets ? message.bundle : null,
|
|
||||||
UTMTerm,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addFluent(document) {
|
|
||||||
FLUENT_FILES.forEach(file => {
|
|
||||||
const link = document.head.appendChild(document.createElement("link"));
|
|
||||||
link.href = file;
|
|
||||||
link.rel = "localization";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchFlowParams({ fxaEndpoint, UTMTerm, dispatch, setFlowParams }) {
|
|
||||||
try {
|
|
||||||
const url = new URL(
|
|
||||||
`${fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
|
|
||||||
);
|
|
||||||
addUtmParams(url, UTMTerm);
|
|
||||||
const response = await fetch(url, { credentials: "omit" });
|
|
||||||
if (response.status === 200) {
|
|
||||||
const { deviceId, flowId, flowBeginTime } = await response.json();
|
|
||||||
setFlowParams({ deviceId, flowId, flowBeginTime });
|
|
||||||
} else {
|
|
||||||
dispatch(
|
|
||||||
ac.OnlyToMain({
|
|
||||||
type: at.TELEMETRY_UNDESIRED_EVENT,
|
|
||||||
data: {
|
|
||||||
event: "FXA_METRICS_FETCH_ERROR",
|
|
||||||
value: response.status,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(
|
|
||||||
ac.OnlyToMain({
|
|
||||||
type: at.TELEMETRY_UNDESIRED_EVENT,
|
|
||||||
data: { event: "FXA_METRICS_ERROR" },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FirstRun extends React.PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.didLoadFlowParams = false;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
prevMessage: undefined,
|
|
||||||
|
|
||||||
hasInterrupt: false,
|
|
||||||
hasTriplets: false,
|
|
||||||
|
|
||||||
interrupt: undefined,
|
|
||||||
triplets: undefined,
|
|
||||||
|
|
||||||
isInterruptVisible: false,
|
|
||||||
isTripletsContainerVisible: false,
|
|
||||||
isTripletsContentVisible: false,
|
|
||||||
|
|
||||||
UTMTerm: "",
|
|
||||||
|
|
||||||
flowParams: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.closeInterrupt = this.closeInterrupt.bind(this);
|
|
||||||
this.closeTriplets = this.closeTriplets.bind(this);
|
|
||||||
|
|
||||||
helpers.addFluent(this.props.document);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
const { message } = props;
|
|
||||||
if (message && message.id !== state.prevMessageId) {
|
|
||||||
const {
|
|
||||||
hasTriplets,
|
|
||||||
hasInterrupt,
|
|
||||||
interrupt,
|
|
||||||
triplets,
|
|
||||||
UTMTerm,
|
|
||||||
} = helpers.selectInterruptAndTriplets(message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
prevMessageId: message.id,
|
|
||||||
|
|
||||||
hasInterrupt,
|
|
||||||
hasTriplets,
|
|
||||||
|
|
||||||
interrupt,
|
|
||||||
triplets,
|
|
||||||
|
|
||||||
isInterruptVisible: hasInterrupt,
|
|
||||||
isTripletsContainerVisible: hasTriplets,
|
|
||||||
isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
|
|
||||||
|
|
||||||
UTMTerm,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFlowParams() {
|
|
||||||
const { fxaEndpoint, dispatch } = this.props;
|
|
||||||
const { UTMTerm } = this.state;
|
|
||||||
if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
|
|
||||||
this.didLoadFlowParams = true;
|
|
||||||
helpers.fetchFlowParams({
|
|
||||||
fxaEndpoint,
|
|
||||||
UTMTerm,
|
|
||||||
dispatch,
|
|
||||||
setFlowParams: flowParams => this.setState({ flowParams }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHideMain() {
|
|
||||||
if (!this.state.hasInterrupt) {
|
|
||||||
// We need to remove hide-main since we should show it underneath everything that has rendered
|
|
||||||
this.props.document.body.classList.remove("hide-main", "welcome");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.fetchFlowParams();
|
|
||||||
this.removeHideMain();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
// In case we didn't have FXA info immediately, try again when we receive it.
|
|
||||||
this.fetchFlowParams();
|
|
||||||
this.removeHideMain();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeInterrupt() {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
isInterruptVisible: false,
|
|
||||||
isTripletsContainerVisible: prevState.hasTriplets,
|
|
||||||
isTripletsContentVisible: prevState.hasTriplets,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
closeTriplets() {
|
|
||||||
this.setState({ isTripletsContainerVisible: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props } = this;
|
|
||||||
const {
|
|
||||||
sendUserActionTelemetry,
|
|
||||||
fxaEndpoint,
|
|
||||||
dispatch,
|
|
||||||
executeAction,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
interrupt,
|
|
||||||
triplets,
|
|
||||||
isInterruptVisible,
|
|
||||||
isTripletsContainerVisible,
|
|
||||||
isTripletsContentVisible,
|
|
||||||
hasTriplets,
|
|
||||||
UTMTerm,
|
|
||||||
flowParams,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isInterruptVisible ? (
|
|
||||||
<Interrupt
|
|
||||||
document={props.document}
|
|
||||||
message={interrupt}
|
|
||||||
onNextScene={this.closeInterrupt}
|
|
||||||
UTMTerm={UTMTerm}
|
|
||||||
sendUserActionTelemetry={sendUserActionTelemetry}
|
|
||||||
dispatch={dispatch}
|
|
||||||
flowParams={flowParams}
|
|
||||||
onDismiss={this.closeInterrupt}
|
|
||||||
fxaEndpoint={fxaEndpoint}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{hasTriplets ? (
|
|
||||||
<Triplets
|
|
||||||
document={props.document}
|
|
||||||
cards={triplets}
|
|
||||||
showCardPanel={isTripletsContainerVisible}
|
|
||||||
showContent={isTripletsContentVisible}
|
|
||||||
hideContainer={this.closeTriplets}
|
|
||||||
sendUserActionTelemetry={sendUserActionTelemetry}
|
|
||||||
UTMTerm={`${UTMTerm}-card`}
|
|
||||||
flowParams={flowParams}
|
|
||||||
onAction={executeAction}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Trailhead } from "../Trailhead/Trailhead";
|
|
||||||
import { ReturnToAMO } from "../ReturnToAMO/ReturnToAMO";
|
|
||||||
import { StartupOverlay } from "../StartupOverlay/StartupOverlay";
|
|
||||||
import { LocalizationProvider } from "fluent-react";
|
|
||||||
import { generateBundles } from "../../rich-text-strings";
|
|
||||||
|
|
||||||
export class Interrupt extends React.PureComponent {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
onDismiss,
|
|
||||||
onNextScene,
|
|
||||||
message,
|
|
||||||
sendUserActionTelemetry,
|
|
||||||
executeAction,
|
|
||||||
dispatch,
|
|
||||||
fxaEndpoint,
|
|
||||||
UTMTerm,
|
|
||||||
flowParams,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
switch (message.template) {
|
|
||||||
case "return_to_amo_overlay":
|
|
||||||
return (
|
|
||||||
<LocalizationProvider
|
|
||||||
bundles={generateBundles({ amo_html: message.content.text })}
|
|
||||||
>
|
|
||||||
<ReturnToAMO
|
|
||||||
{...message}
|
|
||||||
UISurface="NEWTAB_OVERLAY"
|
|
||||||
onBlock={onDismiss}
|
|
||||||
onAction={executeAction}
|
|
||||||
sendUserActionTelemetry={sendUserActionTelemetry}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
);
|
|
||||||
case "fxa_overlay":
|
|
||||||
return (
|
|
||||||
<StartupOverlay
|
|
||||||
onBlock={onDismiss}
|
|
||||||
dispatch={dispatch}
|
|
||||||
fxa_endpoint={fxaEndpoint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "trailhead":
|
|
||||||
return (
|
|
||||||
<Trailhead
|
|
||||||
document={this.props.document}
|
|
||||||
message={message}
|
|
||||||
onNextScene={onNextScene}
|
|
||||||
onAction={executeAction}
|
|
||||||
sendUserActionTelemetry={sendUserActionTelemetry}
|
|
||||||
dispatch={dispatch}
|
|
||||||
fxaEndpoint={fxaEndpoint}
|
|
||||||
UTMTerm={UTMTerm}
|
|
||||||
flowParams={flowParams}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
throw new Error(`${message.template} is not a valid FirstRun message`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { OnboardingCard } from "../../templates/OnboardingMessage/OnboardingMessage";
|
|
||||||
import { addUtmParams } from "./addUtmParams";
|
|
||||||
|
|
||||||
export class Triplets extends React.PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.onCardAction = this.onCardAction.bind(this);
|
|
||||||
this.onHideContainer = this.onHideContainer.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
global.document.body.classList.add("inline-onboarding");
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.document.body.classList.remove("inline-onboarding");
|
|
||||||
}
|
|
||||||
|
|
||||||
onCardAction(action) {
|
|
||||||
let actionUpdates = {};
|
|
||||||
const { flowParams, UTMTerm } = this.props;
|
|
||||||
|
|
||||||
if (action.type === "OPEN_URL") {
|
|
||||||
let url = new URL(action.data.args);
|
|
||||||
addUtmParams(url, UTMTerm);
|
|
||||||
|
|
||||||
if (action.addFlowParams) {
|
|
||||||
url.searchParams.append("device_id", flowParams.deviceId);
|
|
||||||
url.searchParams.append("flow_id", flowParams.flowId);
|
|
||||||
url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
actionUpdates = { data: { ...action.data, args: url.toString() } };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onAction({ ...action, ...actionUpdates });
|
|
||||||
}
|
|
||||||
|
|
||||||
onHideContainer() {
|
|
||||||
const { sendUserActionTelemetry, cards, hideContainer } = this.props;
|
|
||||||
hideContainer();
|
|
||||||
sendUserActionTelemetry({
|
|
||||||
event: "DISMISS",
|
|
||||||
id: "onboarding-cards",
|
|
||||||
message_id: cards.map(m => m.id).join(","),
|
|
||||||
action: "onboarding_user_event",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
cards,
|
|
||||||
showCardPanel,
|
|
||||||
showContent,
|
|
||||||
sendUserActionTelemetry,
|
|
||||||
} = this.props;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`trailheadCards ${showCardPanel ? "expanded" : "collapsed"}`}
|
|
||||||
>
|
|
||||||
<div className="trailheadCardsInner" aria-hidden={!showContent}>
|
|
||||||
<h1 data-l10n-id="onboarding-welcome-header" />
|
|
||||||
<div className={`trailheadCardGrid${showContent ? " show" : ""}`}>
|
|
||||||
{cards.map(card => (
|
|
||||||
<OnboardingCard
|
|
||||||
key={card.id}
|
|
||||||
className="trailheadCard"
|
|
||||||
sendUserActionTelemetry={sendUserActionTelemetry}
|
|
||||||
onAction={this.onCardAction}
|
|
||||||
UISurface="TRAILHEAD"
|
|
||||||
{...card}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{showCardPanel && (
|
|
||||||
<button
|
|
||||||
className="icon icon-dismiss"
|
|
||||||
onClick={this.onHideContainer}
|
|
||||||
data-l10n-id="onboarding-cards-dismiss"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
const BASE_PARAMS = {
|
|
||||||
utm_source: "activity-stream",
|
|
||||||
utm_campaign: "firstrun",
|
|
||||||
utm_medium: "referral",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes in a url as a string or URL object and returns a URL object with the
|
|
||||||
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
|
|
||||||
* are added to it (the return value can be ignored in that case as it's the
|
|
||||||
* same object).
|
|
||||||
*/
|
|
||||||
export function addUtmParams(url, utmTerm) {
|
|
||||||
let returnUrl = url;
|
|
||||||
if (typeof returnUrl === "string") {
|
|
||||||
returnUrl = new URL(url);
|
|
||||||
}
|
|
||||||
Object.keys(BASE_PARAMS).forEach(key => {
|
|
||||||
returnUrl.searchParams.append(key, BASE_PARAMS[key]);
|
|
||||||
});
|
|
||||||
returnUrl.searchParams.append("utm_term", utmTerm);
|
|
||||||
return returnUrl;
|
|
||||||
}
|
|
|
@ -2,8 +2,15 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { ModalOverlay } from "../../components/ModalOverlay/ModalOverlay";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
const FLUENT_FILES = [
|
||||||
|
"branding/brand.ftl",
|
||||||
|
"browser/branding/sync-brand.ftl",
|
||||||
|
"browser/newtab/onboarding.ftl",
|
||||||
|
];
|
||||||
|
|
||||||
export class OnboardingCard extends React.PureComponent {
|
export class OnboardingCard extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -50,3 +57,33 @@ export class OnboardingCard extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OnboardingMessage extends React.PureComponent {
|
||||||
|
componentWillMount() {
|
||||||
|
FLUENT_FILES.forEach(file => {
|
||||||
|
const link = document.head.appendChild(document.createElement("link"));
|
||||||
|
link.href = file;
|
||||||
|
link.rel = "localization";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props } = this;
|
||||||
|
const { button_label, header } = props.extraTemplateStrings;
|
||||||
|
return (
|
||||||
|
<ModalOverlay {...props} button_label={button_label} title={header}>
|
||||||
|
<div className="onboardingMessageContainer">
|
||||||
|
{props.bundle.map(message => (
|
||||||
|
<OnboardingCard
|
||||||
|
key={message.id}
|
||||||
|
sendUserActionTelemetry={props.sendUserActionTelemetry}
|
||||||
|
onAction={props.onAction}
|
||||||
|
UISurface={props.UISurface}
|
||||||
|
{...message}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ModalOverlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 21px;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
min-height: 500px;
|
||||||
|
|
||||||
|
// at 850px, the cards go from vertical layout to horizontal layout
|
||||||
|
@media(max-width: 850px) {
|
||||||
|
grid-template-columns: none;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
padding-left: 110px;
|
||||||
|
padding-right: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.onboardingMessage {
|
.onboardingMessage {
|
||||||
height: 340px;
|
height: 340px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -15,11 +15,8 @@ export class ReturnToAMO extends React.PureComponent {
|
||||||
this.onBlockButton = this.onBlockButton.bind(this);
|
this.onBlockButton = this.onBlockButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
global.document.body.classList.add("amo");
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.props.onReady();
|
||||||
this.props.sendUserActionTelemetry({
|
this.props.sendUserActionTelemetry({
|
||||||
event: "IMPRESSION",
|
event: "IMPRESSION",
|
||||||
id: this.props.UISurface,
|
id: this.props.UISurface,
|
||||||
|
|
|
@ -2,15 +2,23 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { actionCreators as ac } from "common/Actions.jsm";
|
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export class StartupOverlay extends React.PureComponent {
|
const FLUENT_FILES = [
|
||||||
|
"branding/brand.ftl",
|
||||||
|
"browser/branding/sync-brand.ftl",
|
||||||
|
"browser/newtab/onboarding.ftl",
|
||||||
|
];
|
||||||
|
|
||||||
|
export class _StartupOverlay extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onInputChange = this.onInputChange.bind(this);
|
this.onInputChange = this.onInputChange.bind(this);
|
||||||
this.onSubmit = this.onSubmit.bind(this);
|
this.onSubmit = this.onSubmit.bind(this);
|
||||||
this.clickSkip = this.clickSkip.bind(this);
|
this.clickSkip = this.clickSkip.bind(this);
|
||||||
|
this.initScene = this.initScene.bind(this);
|
||||||
this.removeOverlay = this.removeOverlay.bind(this);
|
this.removeOverlay = this.removeOverlay.bind(this);
|
||||||
this.onInputInvalid = this.onInputInvalid.bind(this);
|
this.onInputInvalid = this.onInputInvalid.bind(this);
|
||||||
|
|
||||||
|
@ -18,20 +26,71 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
"utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
|
"utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
show: false,
|
|
||||||
emailInput: "",
|
emailInput: "",
|
||||||
|
overlayRemoved: false,
|
||||||
|
deviceId: "",
|
||||||
|
flowId: "",
|
||||||
|
flowBeginTime: 0,
|
||||||
};
|
};
|
||||||
|
this.didFetch = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
async componentWillUpdate() {
|
||||||
global.document.body.classList.add("fxa");
|
if (this.props.fxa_endpoint && !this.didFetch) {
|
||||||
|
try {
|
||||||
|
this.didFetch = true;
|
||||||
|
const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${
|
||||||
|
this.utmParams
|
||||||
|
}`,
|
||||||
|
{ credentials: "omit" }
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { deviceId, flowId, flowBeginTime } = await response.json();
|
||||||
|
this.setState({ deviceId, flowId, flowBeginTime });
|
||||||
|
} else {
|
||||||
|
this.props.dispatch(
|
||||||
|
ac.OnlyToMain({
|
||||||
|
type: at.TELEMETRY_UNDESIRED_EVENT,
|
||||||
|
data: {
|
||||||
|
event: "FXA_METRICS_FETCH_ERROR",
|
||||||
|
value: response.status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.props.dispatch(
|
||||||
|
ac.OnlyToMain({
|
||||||
|
type: at.TELEMETRY_UNDESIRED_EVENT,
|
||||||
|
data: { event: "FXA_METRICS_ERROR" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
FLUENT_FILES.forEach(file => {
|
||||||
|
const link = document.head.appendChild(document.createElement("link"));
|
||||||
|
link.href = file;
|
||||||
|
link.rel = "localization";
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.componentWillUpdate(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.initScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
initScene() {
|
||||||
// Timeout to allow the scene to render once before attaching the attribute
|
// Timeout to allow the scene to render once before attaching the attribute
|
||||||
// to trigger the animation.
|
// to trigger the animation.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({ show: true });
|
this.setState({ show: true });
|
||||||
|
this.props.onReady();
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +98,11 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
window.removeEventListener("visibilitychange", this.removeOverlay);
|
window.removeEventListener("visibilitychange", this.removeOverlay);
|
||||||
document.body.classList.remove("hide-main", "fxa");
|
document.body.classList.remove("hide-main", "fxa");
|
||||||
this.setState({ show: false });
|
this.setState({ show: false });
|
||||||
|
this.props.onBlock();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Allow scrolling and fully remove overlay after animation finishes.
|
// Allow scrolling and fully remove overlay after animation finishes.
|
||||||
this.props.onBlock();
|
|
||||||
document.body.classList.remove("welcome");
|
document.body.classList.remove("welcome");
|
||||||
|
this.setState({ overlayRemoved: true });
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,9 +132,7 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
* Report to telemetry additional information about the form submission.
|
* Report to telemetry additional information about the form submission.
|
||||||
*/
|
*/
|
||||||
_getFormInfo() {
|
_getFormInfo() {
|
||||||
const value = {
|
const value = { has_flow_params: this.state.flowId.length > 0 };
|
||||||
has_flow_params: this.props.flowParams.flowId.length > 0,
|
|
||||||
};
|
|
||||||
return { value };
|
return { value };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +145,12 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// When skipping the onboarding tour we show AS but we are still on
|
||||||
|
// about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
|
||||||
|
if (this.state.overlayRemoved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
|
<div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
|
||||||
<div className="background" />
|
<div className="background" />
|
||||||
|
@ -150,17 +213,13 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
<input
|
<input
|
||||||
name="device_id"
|
name="device_id"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
value={this.props.flowParams.deviceId}
|
value={this.state.deviceId}
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="flow_id"
|
|
||||||
type="hidden"
|
|
||||||
value={this.props.flowParams.flowId}
|
|
||||||
/>
|
/>
|
||||||
|
<input name="flow_id" type="hidden" value={this.state.flowId} />
|
||||||
<input
|
<input
|
||||||
name="flow_begin_time"
|
name="flow_begin_time"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
value={this.props.flowParams.flowBeginTime}
|
value={this.state.flowBeginTime}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="error"
|
className="error"
|
||||||
|
@ -170,7 +229,7 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
className="email-input"
|
className="email-input"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
required={true}
|
required="true"
|
||||||
onInvalid={this.onInputInvalid}
|
onInvalid={this.onInputInvalid}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
data-l10n-id="onboarding-sync-form-input"
|
data-l10n-id="onboarding-sync-form-input"
|
||||||
|
@ -215,6 +274,5 @@ export class StartupOverlay extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StartupOverlay.defaultProps = {
|
const getState = state => ({ fxa_endpoint: state.Prefs.values.fxa_endpoint });
|
||||||
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
|
export const StartupOverlay = connect(getState)(_StartupOverlay);
|
||||||
};
|
|
||||||
|
|
|
@ -2,11 +2,18 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { actionCreators as ac } from "common/Actions.jsm";
|
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||||
import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
|
import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
|
||||||
import { addUtmParams } from "../FirstRun/addUtmParams";
|
import { OnboardingCard } from "../OnboardingMessage/OnboardingMessage";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
const FLUENT_FILES = [
|
||||||
|
"branding/brand.ftl",
|
||||||
|
"browser/branding/brandings.ftl",
|
||||||
|
"browser/branding/sync-brand.ftl",
|
||||||
|
"browser/newtab/onboarding.ftl",
|
||||||
|
];
|
||||||
|
|
||||||
// From resource://devtools/client/shared/focus.js
|
// From resource://devtools/client/shared/focus.js
|
||||||
const FOCUSABLE_SELECTOR = [
|
const FOCUSABLE_SELECTOR = [
|
||||||
"a[href]:not([tabindex='-1'])",
|
"a[href]:not([tabindex='-1'])",
|
||||||
|
@ -22,35 +29,106 @@ export class Trailhead extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.closeModal = this.closeModal.bind(this);
|
this.closeModal = this.closeModal.bind(this);
|
||||||
|
this.hideCardPanel = this.hideCardPanel.bind(this);
|
||||||
this.onInputChange = this.onInputChange.bind(this);
|
this.onInputChange = this.onInputChange.bind(this);
|
||||||
this.onStartBlur = this.onStartBlur.bind(this);
|
this.onStartBlur = this.onStartBlur.bind(this);
|
||||||
this.onSubmit = this.onSubmit.bind(this);
|
this.onSubmit = this.onSubmit.bind(this);
|
||||||
this.onInputInvalid = this.onInputInvalid.bind(this);
|
this.onInputInvalid = this.onInputInvalid.bind(this);
|
||||||
|
this.onCardAction = this.onCardAction.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
emailInput: "",
|
emailInput: "",
|
||||||
|
isModalOpen: true,
|
||||||
|
showCardPanel: true,
|
||||||
|
showCards: false,
|
||||||
|
// The params below are for FxA metrics
|
||||||
|
deviceId: "",
|
||||||
|
flowId: "",
|
||||||
|
flowBeginTime: 0,
|
||||||
};
|
};
|
||||||
|
this.fxaMetricsInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get dialog() {
|
get dialog() {
|
||||||
return this.props.document.getElementById("trailheadDialog");
|
return this.props.document.getElementById("trailheadDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
FLUENT_FILES.forEach(file => {
|
||||||
|
const link = document.head.appendChild(document.createElement("link"));
|
||||||
|
link.href = file;
|
||||||
|
link.rel = "localization";
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.componentWillUpdate(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the fxa data if we don't have it yet from mount or update
|
||||||
|
async componentWillUpdate(props) {
|
||||||
|
if (props.fxaEndpoint && !this.fxaMetricsInitialized) {
|
||||||
|
try {
|
||||||
|
this.fxaMetricsInitialized = true;
|
||||||
|
const url = new URL(
|
||||||
|
`${
|
||||||
|
props.fxaEndpoint
|
||||||
|
}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
|
||||||
|
);
|
||||||
|
this.addUtmParams(url);
|
||||||
|
const response = await fetch(url, { credentials: "omit" });
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { deviceId, flowId, flowBeginTime } = await response.json();
|
||||||
|
this.setState({ deviceId, flowId, flowBeginTime });
|
||||||
|
} else {
|
||||||
|
props.dispatch(
|
||||||
|
ac.OnlyToMain({
|
||||||
|
type: at.TELEMETRY_UNDESIRED_EVENT,
|
||||||
|
data: {
|
||||||
|
event: "FXA_METRICS_FETCH_ERROR",
|
||||||
|
value: response.status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
props.dispatch(
|
||||||
|
ac.OnlyToMain({
|
||||||
|
type: at.TELEMETRY_UNDESIRED_EVENT,
|
||||||
|
data: { event: "FXA_METRICS_ERROR" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// We need to remove hide-main since we should show it underneath everything that has rendered
|
// We need to remove hide-main since we should show it underneath everything that has rendered
|
||||||
this.props.document.body.classList.remove("hide-main");
|
this.props.document.body.classList.remove("hide-main");
|
||||||
|
|
||||||
// The rest of the page is "hidden" to screen readers when the modal is open
|
// Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
|
||||||
this.props.document
|
this.props.document.body.classList.add("inline-onboarding");
|
||||||
.getElementById("root")
|
|
||||||
.setAttribute("aria-hidden", "true");
|
// The rest of the page is "hidden" when the modal is open
|
||||||
// Start with focus in the email input box
|
if (this.props.message.content) {
|
||||||
const input = this.dialog.querySelector("input[name=email]");
|
this.props.document
|
||||||
if (input) {
|
.getElementById("root")
|
||||||
input.focus();
|
.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
// Start with focus in the email input box
|
||||||
|
this.dialog.querySelector("input[name=email]").focus();
|
||||||
|
} else {
|
||||||
|
// No modal overlay, let the user scroll and deal them some cards.
|
||||||
|
this.props.document.body.classList.remove("welcome");
|
||||||
|
|
||||||
|
if (this.props.message.includeBundle || this.props.message.cards) {
|
||||||
|
this.revealCards();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.document.body.classList.remove("inline-onboarding");
|
||||||
|
}
|
||||||
|
|
||||||
onInputChange(e) {
|
onInputChange(e) {
|
||||||
let error = e.target.previousSibling;
|
let error = e.target.previousSibling;
|
||||||
this.setState({ emailInput: e.target.value });
|
this.setState({ emailInput: e.target.value });
|
||||||
|
@ -94,7 +172,8 @@ export class Trailhead extends React.PureComponent {
|
||||||
global.removeEventListener("visibilitychange", this.closeModal);
|
global.removeEventListener("visibilitychange", this.closeModal);
|
||||||
this.props.document.body.classList.remove("welcome");
|
this.props.document.body.classList.remove("welcome");
|
||||||
this.props.document.getElementById("root").removeAttribute("aria-hidden");
|
this.props.document.getElementById("root").removeAttribute("aria-hidden");
|
||||||
this.props.onNextScene();
|
this.setState({ isModalOpen: false });
|
||||||
|
this.revealCards();
|
||||||
|
|
||||||
// If closeModal() was triggered by a visibilitychange event, the user actually
|
// If closeModal() was triggered by a visibilitychange event, the user actually
|
||||||
// submitted the email form so we don't send a SKIPPED_SIGNIN ping.
|
// submitted the email form so we don't send a SKIPPED_SIGNIN ping.
|
||||||
|
@ -112,7 +191,7 @@ export class Trailhead extends React.PureComponent {
|
||||||
* Report to telemetry additional information about the form submission.
|
* Report to telemetry additional information about the form submission.
|
||||||
*/
|
*/
|
||||||
_getFormInfo() {
|
_getFormInfo() {
|
||||||
const value = { has_flow_params: this.props.flowParams.flowId.length > 0 };
|
const value = { has_flow_params: this.state.flowId.length > 0 };
|
||||||
return { value };
|
return { value };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,141 +203,234 @@ export class Trailhead extends React.PureComponent {
|
||||||
e.target.focus();
|
e.target.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideCardPanel() {
|
||||||
|
this.setState({ showCardPanel: false });
|
||||||
|
this.props.onDismissBundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
revealCards() {
|
||||||
|
this.setState({ showCards: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes in a url as a string or URL object and returns a URL object with the
|
||||||
|
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
|
||||||
|
* are added to it (the return value can be ignored in that case as it's the
|
||||||
|
* same object).
|
||||||
|
*/
|
||||||
|
addUtmParams(url, isCard = false) {
|
||||||
|
let returnUrl = url;
|
||||||
|
if (typeof returnUrl === "string") {
|
||||||
|
returnUrl = new URL(url);
|
||||||
|
}
|
||||||
|
returnUrl.searchParams.append("utm_source", "activity-stream");
|
||||||
|
returnUrl.searchParams.append("utm_campaign", "firstrun");
|
||||||
|
returnUrl.searchParams.append("utm_medium", "referral");
|
||||||
|
returnUrl.searchParams.append(
|
||||||
|
"utm_term",
|
||||||
|
`${this.props.message.utm_term}${isCard ? "-card" : ""}`
|
||||||
|
);
|
||||||
|
return returnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCardAction(action) {
|
||||||
|
let actionUpdates = {};
|
||||||
|
|
||||||
|
if (action.type === "OPEN_URL") {
|
||||||
|
let url = new URL(action.data.args);
|
||||||
|
this.addUtmParams(url, true);
|
||||||
|
|
||||||
|
if (action.addFlowParams) {
|
||||||
|
url.searchParams.append("device_id", this.state.deviceId);
|
||||||
|
url.searchParams.append("flow_id", this.state.flowId);
|
||||||
|
url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
actionUpdates = { data: { ...action.data, args: url } };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onAction({ ...action, ...actionUpdates });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
const { UTMTerm } = props;
|
const { bundle: cards, content, utm_term } = props.message;
|
||||||
const { content } = props.message;
|
|
||||||
const innerClassName = ["trailhead", content && content.className]
|
const innerClassName = ["trailhead", content && content.className]
|
||||||
.filter(v => v)
|
.filter(v => v)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalOverlayWrapper
|
<>
|
||||||
innerClassName={innerClassName}
|
{this.state.isModalOpen && content ? (
|
||||||
onClose={this.closeModal}
|
<ModalOverlayWrapper
|
||||||
id="trailheadDialog"
|
innerClassName={innerClassName}
|
||||||
headerId="trailheadHeader"
|
onClose={this.closeModal}
|
||||||
>
|
id="trailheadDialog"
|
||||||
<div className="trailheadInner">
|
headerId="trailheadHeader"
|
||||||
<div className="trailheadContent">
|
|
||||||
<h1 data-l10n-id={content.title.string_id} id="trailheadHeader" />
|
|
||||||
{content.subtitle && (
|
|
||||||
<p data-l10n-id={content.subtitle.string_id} />
|
|
||||||
)}
|
|
||||||
<ul className="trailheadBenefits">
|
|
||||||
{content.benefits.map(item => (
|
|
||||||
<li key={item.id} className={item.id}>
|
|
||||||
<h3 data-l10n-id={item.title.string_id} />
|
|
||||||
<p data-l10n-id={item.text.string_id} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<a
|
|
||||||
className="trailheadLearn"
|
|
||||||
data-l10n-id={content.learn.text.string_id}
|
|
||||||
href={addUtmParams(content.learn.url, UTMTerm)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
aria-labelledby="joinFormHeader"
|
|
||||||
aria-describedby="joinFormBody"
|
|
||||||
className="trailheadForm"
|
|
||||||
>
|
>
|
||||||
<h3
|
<div className="trailheadInner">
|
||||||
id="joinFormHeader"
|
<div className="trailheadContent">
|
||||||
data-l10n-id={content.form.title.string_id}
|
<h1
|
||||||
/>
|
data-l10n-id={content.title.string_id}
|
||||||
<p id="joinFormBody" data-l10n-id={content.form.text.string_id} />
|
id="trailheadHeader"
|
||||||
<form
|
/>
|
||||||
method="get"
|
{content.subtitle && (
|
||||||
action={this.props.fxaEndpoint}
|
<p data-l10n-id={content.subtitle.string_id} />
|
||||||
target="_blank"
|
)}
|
||||||
rel="noopener noreferrer"
|
<ul className="trailheadBenefits">
|
||||||
onSubmit={this.onSubmit}
|
{content.benefits.map(item => (
|
||||||
>
|
<li key={item.id} className={item.id}>
|
||||||
<input name="service" type="hidden" value="sync" />
|
<h3 data-l10n-id={item.title.string_id} />
|
||||||
<input name="action" type="hidden" value="email" />
|
<p data-l10n-id={item.text.string_id} />
|
||||||
<input name="context" type="hidden" value="fx_desktop_v3" />
|
</li>
|
||||||
<input
|
))}
|
||||||
name="entrypoint"
|
</ul>
|
||||||
type="hidden"
|
<a
|
||||||
value="activity-stream-firstrun"
|
className="trailheadLearn"
|
||||||
/>
|
data-l10n-id={content.learn.text.string_id}
|
||||||
<input name="utm_source" type="hidden" value="activity-stream" />
|
href={this.addUtmParams(content.learn.url)}
|
||||||
<input name="utm_campaign" type="hidden" value="firstrun" />
|
target="_blank"
|
||||||
<input name="utm_term" type="hidden" value={UTMTerm} />
|
rel="noopener noreferrer"
|
||||||
<input
|
/>
|
||||||
name="device_id"
|
</div>
|
||||||
type="hidden"
|
<div
|
||||||
value={this.props.flowParams.deviceId}
|
role="group"
|
||||||
/>
|
aria-labelledby="joinFormHeader"
|
||||||
<input
|
aria-describedby="joinFormBody"
|
||||||
name="flow_id"
|
className="trailheadForm"
|
||||||
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
|
<h3
|
||||||
data-l10n-name="terms"
|
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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={addUtmParams(
|
onSubmit={this.onSubmit}
|
||||||
"https://accounts.firefox.com/legal/terms",
|
>
|
||||||
UTMTerm
|
<input name="service" type="hidden" value="sync" />
|
||||||
)}
|
<input name="action" type="hidden" value="email" />
|
||||||
/>
|
<input name="context" type="hidden" value="fx_desktop_v3" />
|
||||||
<a
|
<input
|
||||||
data-l10n-name="privacy"
|
name="entrypoint"
|
||||||
target="_blank"
|
type="hidden"
|
||||||
rel="noopener noreferrer"
|
value="activity-stream-firstrun"
|
||||||
href={addUtmParams(
|
/>
|
||||||
"https://accounts.firefox.com/legal/privacy",
|
<input
|
||||||
UTMTerm
|
name="utm_source"
|
||||||
)}
|
type="hidden"
|
||||||
/>
|
value="activity-stream"
|
||||||
</p>
|
/>
|
||||||
<button
|
<input name="utm_campaign" type="hidden" value="firstrun" />
|
||||||
data-l10n-id={content.form.button.string_id}
|
<input name="utm_term" type="hidden" value={utm_term} />
|
||||||
type="submit"
|
<input
|
||||||
/>
|
name="device_id"
|
||||||
</form>
|
type="hidden"
|
||||||
</div>
|
value={this.state.deviceId}
|
||||||
</div>
|
/>
|
||||||
|
<input
|
||||||
|
name="flow_id"
|
||||||
|
type="hidden"
|
||||||
|
value={this.state.flowId}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="flow_begin_time"
|
||||||
|
type="hidden"
|
||||||
|
value={this.state.flowBeginTime}
|
||||||
|
/>
|
||||||
|
<input name="style" type="hidden" value="trailhead" />
|
||||||
|
<p
|
||||||
|
data-l10n-id="onboarding-join-form-email-error"
|
||||||
|
className="error"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
data-l10n-id={content.form.email.string_id}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
onInvalid={this.onInputInvalid}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="trailheadTerms"
|
||||||
|
data-l10n-id="onboarding-join-form-legal"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
data-l10n-name="terms"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={this.addUtmParams(
|
||||||
|
"https://accounts.firefox.com/legal/terms"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
data-l10n-name="privacy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={this.addUtmParams(
|
||||||
|
"https://accounts.firefox.com/legal/privacy"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
data-l10n-id={content.form.button.string_id}
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="trailheadStart"
|
className="trailheadStart"
|
||||||
data-l10n-id={content.skipButton.string_id}
|
data-l10n-id={content.skipButton.string_id}
|
||||||
onBlur={this.onStartBlur}
|
onBlur={this.onStartBlur}
|
||||||
onClick={this.closeModal}
|
onClick={this.closeModal}
|
||||||
/>
|
/>
|
||||||
</ModalOverlayWrapper>
|
</ModalOverlayWrapper>
|
||||||
|
) : null}
|
||||||
|
{cards && cards.length ? (
|
||||||
|
<div
|
||||||
|
className={`trailheadCards ${
|
||||||
|
this.state.showCardPanel ? "expanded" : "collapsed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="trailheadCardsInner"
|
||||||
|
aria-hidden={!this.state.showCards}
|
||||||
|
>
|
||||||
|
<h1 data-l10n-id="onboarding-welcome-header" />
|
||||||
|
<div
|
||||||
|
className={`trailheadCardGrid${
|
||||||
|
this.state.showCards ? " show" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cards.map(card => (
|
||||||
|
<OnboardingCard
|
||||||
|
key={card.id}
|
||||||
|
className="trailheadCard"
|
||||||
|
sendUserActionTelemetry={props.sendUserActionTelemetry}
|
||||||
|
onAction={this.onCardAction}
|
||||||
|
UISurface="TRAILHEAD"
|
||||||
|
{...card}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{this.state.showCardPanel && (
|
||||||
|
<button
|
||||||
|
className="icon icon-dismiss"
|
||||||
|
onClick={this.hideCardPanel}
|
||||||
|
data-l10n-id="onboarding-cards-dismiss"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Trailhead.defaultProps = {
|
|
||||||
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
|
|
||||||
};
|
|
||||||
|
|
|
@ -119,7 +119,6 @@ export class CollapsibleSection extends React.PureComponent {
|
||||||
|
|
||||||
onKeyPress(event) {
|
onKeyPress(event) {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
|
||||||
this.onHeaderClick();
|
this.onHeaderClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,13 +223,16 @@ export class CollapsibleSection extends React.PureComponent {
|
||||||
>
|
>
|
||||||
{this.renderIcon()}
|
{this.renderIcon()}
|
||||||
<FluentOrText message={title} />
|
<FluentOrText message={title} />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="click-target"
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
onKeyPress={this.onKeyPress}
|
||||||
|
onClick={this.onHeaderClick}
|
||||||
|
>
|
||||||
{isCollapsible && (
|
{isCollapsible && (
|
||||||
<span
|
<span
|
||||||
data-l10n-id={
|
|
||||||
collapsed
|
|
||||||
? "newtab-section-expand-section-label"
|
|
||||||
: "newtab-section-collapse-section-label"
|
|
||||||
}
|
|
||||||
className={`collapsible-arrow icon ${
|
className={`collapsible-arrow icon ${
|
||||||
collapsed
|
collapsed
|
||||||
? "icon-arrowhead-forward-small"
|
? "icon-arrowhead-forward-small"
|
||||||
|
|
|
@ -44,16 +44,16 @@ export class ContextMenu extends React.PureComponent {
|
||||||
// Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
|
// Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/interactive-supports-focus
|
// eslint-disable-next-line jsx-a11y/interactive-supports-focus
|
||||||
<span className="context-menu">
|
<span
|
||||||
<ul
|
role="menu"
|
||||||
role="menu"
|
className="context-menu"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onKeyDown={this.onClick}
|
onKeyDown={this.onClick}
|
||||||
className="context-menu-list"
|
>
|
||||||
>
|
<ul className="context-menu-list">
|
||||||
{this.props.options.map((option, i) =>
|
{this.props.options.map((option, i) =>
|
||||||
option.type === "separator" ? (
|
option.type === "separator" ? (
|
||||||
<li key={i} className="separator" role="separator" />
|
<li key={i} className="separator" />
|
||||||
) : (
|
) : (
|
||||||
option.type !== "empty" && (
|
option.type !== "empty" && (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
|
@ -61,6 +61,7 @@ export class ContextMenu extends React.PureComponent {
|
||||||
option={option}
|
option={option}
|
||||||
hideContext={this.hideContext}
|
hideContext={this.hideContext}
|
||||||
keyboardAccess={this.props.keyboardAccess}
|
keyboardAccess={this.props.keyboardAccess}
|
||||||
|
tabIndex="0"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -76,7 +77,6 @@ export class ContextMenuItem extends React.PureComponent {
|
||||||
super(props);
|
super(props);
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
this.onKeyUp = this.onKeyUp.bind(this);
|
|
||||||
this.focusFirst = this.focusFirst.bind(this);
|
this.focusFirst = this.focusFirst.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,8 +129,6 @@ export class ContextMenuItem extends React.PureComponent {
|
||||||
this.focusSibling(event.target, event.key);
|
this.focusSibling(event.target, event.key);
|
||||||
break;
|
break;
|
||||||
case "Enter":
|
case "Enter":
|
||||||
case " ":
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.hideContext();
|
this.props.hideContext();
|
||||||
option.onClick();
|
option.onClick();
|
||||||
break;
|
break;
|
||||||
|
@ -140,24 +138,15 @@ export class ContextMenuItem extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevents the default behavior of spacebar
|
|
||||||
// scrolling the page & auto-triggering buttons.
|
|
||||||
onKeyUp(event) {
|
|
||||||
if (event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { option } = this.props;
|
const { option } = this.props;
|
||||||
return (
|
return (
|
||||||
<li role="presentation" className="context-menu-item">
|
<li role="menuitem" className="context-menu-item">
|
||||||
<button
|
<button
|
||||||
className={option.disabled ? "disabled" : ""}
|
className={option.disabled ? "disabled" : ""}
|
||||||
role="menuitem"
|
tabIndex="0"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onKeyUp={this.onKeyUp}
|
|
||||||
ref={option.first ? this.focusFirst : null}
|
ref={option.first ? this.focusFirst : null}
|
||||||
>
|
>
|
||||||
{option.icon && (
|
{option.icon && (
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class ContextMenuButton extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(event) {
|
onKeyDown(event) {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.openContextMenu(true, event);
|
this.openContextMenu(true, event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
||||||
.submissionStatus .submitStatusTitle {
|
.submissionStatus .submitStatusTitle {
|
||||||
font-size: 20px; }
|
font-size: 20px; }
|
||||||
|
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 21px;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
min-height: 500px; }
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
grid-template-columns: none;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
padding-left: 110px;
|
||||||
|
padding-right: 110px; } }
|
||||||
|
|
||||||
.onboardingMessage {
|
.onboardingMessage {
|
||||||
height: 340px;
|
height: 340px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -3424,6 +3424,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
||||||
.submissionStatus .submitStatusTitle {
|
.submissionStatus .submitStatusTitle {
|
||||||
font-size: 20px; }
|
font-size: 20px; }
|
||||||
|
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 21px;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
min-height: 500px; }
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
grid-template-columns: none;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
padding-left: 110px;
|
||||||
|
padding-right: 110px; } }
|
||||||
|
|
||||||
.onboardingMessage {
|
.onboardingMessage {
|
||||||
height: 340px;
|
height: 340px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
||||||
.submissionStatus .submitStatusTitle {
|
.submissionStatus .submitStatusTitle {
|
||||||
font-size: 20px; }
|
font-size: 20px; }
|
||||||
|
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 21px;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
min-height: 500px; }
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.onboardingMessageContainer {
|
||||||
|
grid-template-columns: none;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
padding-left: 110px;
|
||||||
|
padding-right: 110px; } }
|
||||||
|
|
||||||
.onboardingMessage {
|
.onboardingMessage {
|
||||||
height: 340px;
|
height: 340px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -1086,23 +1086,3 @@ This reports when a user has seen or clicked a badge/notification in the browser
|
||||||
"event": ["CLICK" | "IMPRESSION"],
|
"event": ["CLICK" | "IMPRESSION"],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Panel interaction pings
|
|
||||||
|
|
||||||
This reports when a user opens the panel, views messages and clicks on a message.
|
|
||||||
For message impressions we concatenate the ids of all messages in the panel.
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"locale": "en-US",
|
|
||||||
"client_id": "9da773d8-4356-f54f-b7cf-6134726bcf3d",
|
|
||||||
"version": "70.0a1",
|
|
||||||
"release_channel": "default",
|
|
||||||
"addon_version": "20190712095934",
|
|
||||||
"action": "cfr_user_event",
|
|
||||||
"source": "CFR",
|
|
||||||
"message_id": "WHATS_NEW_70",
|
|
||||||
"event": ["CLICK" | "IMPRESSION"],
|
|
||||||
"value": { "view": ["application_menu" | "toolbar_dropdown"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
@ -732,7 +732,6 @@ class _ASRouter {
|
||||||
});
|
});
|
||||||
ToolbarPanelHub.init(this.waitForInitialized, {
|
ToolbarPanelHub.init(this.waitForInitialized, {
|
||||||
getMessages: this.handleMessageRequest,
|
getMessages: this.handleMessageRequest,
|
||||||
dispatch: this.dispatch,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loadLocalProviders();
|
this._loadLocalProviders();
|
||||||
|
@ -900,25 +899,18 @@ class _ASRouter {
|
||||||
let interrupt;
|
let interrupt;
|
||||||
let triplet;
|
let triplet;
|
||||||
|
|
||||||
|
// Use control Trailhead Branch (for cards) if we are showing RTAMO.
|
||||||
|
if (await this._hasAddonAttributionData()) {
|
||||||
|
return { experiment, interrupt: "control", triplet: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
|
||||||
const overrideValue = Services.prefs.getStringPref(
|
const overrideValue = Services.prefs.getStringPref(
|
||||||
TRAILHEAD_CONFIG.OVERRIDE_PREF,
|
TRAILHEAD_CONFIG.OVERRIDE_PREF,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
if (overrideValue) {
|
if (overrideValue) {
|
||||||
[interrupt, triplet] = overrideValue.split("-");
|
[interrupt, triplet] = overrideValue.split("-");
|
||||||
}
|
|
||||||
|
|
||||||
// Use control Trailhead Branch (for cards) if we are showing RTAMO.
|
|
||||||
if (await this._hasAddonAttributionData()) {
|
|
||||||
return {
|
|
||||||
experiment,
|
|
||||||
interrupt: "control",
|
|
||||||
triplet: triplet || "privacy",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
|
|
||||||
if (overrideValue) {
|
|
||||||
return { experiment, interrupt, triplet: triplet || "" };
|
return { experiment, interrupt, triplet: triplet || "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -984,7 +976,6 @@ class _ASRouter {
|
||||||
interrupt,
|
interrupt,
|
||||||
triplet,
|
triplet,
|
||||||
} = await this._generateTrailheadBranches();
|
} = await this._generateTrailheadBranches();
|
||||||
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
trailheadInitialized: true,
|
trailheadInitialized: true,
|
||||||
trailheadInterrupt: interrupt,
|
trailheadInterrupt: interrupt,
|
||||||
|
@ -1841,6 +1832,11 @@ class _ASRouter {
|
||||||
data: { id: action.data.id },
|
data: { id: action.data.id },
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "DISMISS_BUNDLE":
|
||||||
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
||||||
|
type: "CLEAR_BUNDLE",
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "BLOCK_BUNDLE":
|
case "BLOCK_BUNDLE":
|
||||||
await this.blockMessageById(action.data.bundle.map(b => b.id));
|
await this.blockMessageById(action.data.bundle.map(b => b.id));
|
||||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
||||||
|
@ -1895,7 +1891,6 @@ class _ASRouter {
|
||||||
break;
|
break;
|
||||||
case "DOORHANGER_TELEMETRY":
|
case "DOORHANGER_TELEMETRY":
|
||||||
case "TOOLBAR_BADGE_TELEMETRY":
|
case "TOOLBAR_BADGE_TELEMETRY":
|
||||||
case "TOOLBAR_PANEL_TELEMETRY":
|
|
||||||
if (this.dispatchToAS) {
|
if (this.dispatchToAS) {
|
||||||
this.dispatchToAS(ac.ASRouterUserEvent(action.data));
|
this.dispatchToAS(ac.ASRouterUserEvent(action.data));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
"use strict";
|
"use strict";
|
||||||
/* globals Localization */
|
/* globals Localization */
|
||||||
|
const { FxAccountsConfig } = ChromeUtils.import(
|
||||||
|
"resource://gre/modules/FxAccountsConfig.jsm"
|
||||||
|
);
|
||||||
const { AttributionCode } = ChromeUtils.import(
|
const { AttributionCode } = ChromeUtils.import(
|
||||||
"resource:///modules/AttributionCode.jsm"
|
"resource:///modules/AttributionCode.jsm"
|
||||||
);
|
);
|
||||||
|
@ -18,7 +21,114 @@ const L10N = new Localization([
|
||||||
"browser/newtab/onboarding.ftl",
|
"browser/newtab/onboarding.ftl",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ONBOARDING_MESSAGES = () => [
|
const ONBOARDING_MESSAGES = async () => [
|
||||||
|
{
|
||||||
|
id: "ONBOARDING_1",
|
||||||
|
template: "onboarding",
|
||||||
|
bundled: 3,
|
||||||
|
order: 2,
|
||||||
|
content: {
|
||||||
|
title: { string_id: "onboarding-private-browsing-title" },
|
||||||
|
text: { string_id: "onboarding-private-browsing-text" },
|
||||||
|
icon: "privatebrowsing",
|
||||||
|
primary_button: {
|
||||||
|
label: { string_id: "onboarding-button-label-try-now" },
|
||||||
|
action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: { id: "showOnboarding" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ONBOARDING_2",
|
||||||
|
template: "onboarding",
|
||||||
|
bundled: 3,
|
||||||
|
order: 3,
|
||||||
|
content: {
|
||||||
|
title: { string_id: "onboarding-screenshots-title" },
|
||||||
|
text: { string_id: "onboarding-screenshots-text" },
|
||||||
|
icon: "screenshots",
|
||||||
|
primary_button: {
|
||||||
|
label: { string_id: "onboarding-button-label-try-now" },
|
||||||
|
action: {
|
||||||
|
type: "OPEN_URL",
|
||||||
|
data: {
|
||||||
|
args: "https://screenshots.firefox.com/#tour",
|
||||||
|
where: "tabshifted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: { id: "showOnboarding" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ONBOARDING_3",
|
||||||
|
template: "onboarding",
|
||||||
|
bundled: 3,
|
||||||
|
order: 1,
|
||||||
|
content: {
|
||||||
|
title: { string_id: "onboarding-addons-title" },
|
||||||
|
text: { string_id: "onboarding-addons-text" },
|
||||||
|
icon: "addons",
|
||||||
|
primary_button: {
|
||||||
|
label: { string_id: "onboarding-button-label-try-now" },
|
||||||
|
action: {
|
||||||
|
type: "OPEN_ABOUT_PAGE",
|
||||||
|
data: { args: "addons" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targeting:
|
||||||
|
"trailheadInterrupt == 'control' && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
||||||
|
trigger: { id: "showOnboarding" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ONBOARDING_4",
|
||||||
|
template: "onboarding",
|
||||||
|
bundled: 3,
|
||||||
|
order: 1,
|
||||||
|
content: {
|
||||||
|
title: { string_id: "onboarding-ghostery-title" },
|
||||||
|
text: { string_id: "onboarding-ghostery-text" },
|
||||||
|
icon: "gift",
|
||||||
|
primary_button: {
|
||||||
|
label: { string_id: "onboarding-button-label-try-now" },
|
||||||
|
action: {
|
||||||
|
type: "OPEN_URL",
|
||||||
|
data: {
|
||||||
|
args: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/",
|
||||||
|
where: "tabshifted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targeting:
|
||||||
|
"trailheadInterrupt == 'control' && providerCohorts.onboarding == 'ghostery'",
|
||||||
|
trigger: { id: "showOnboarding" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ONBOARDING_5",
|
||||||
|
template: "onboarding",
|
||||||
|
bundled: 3,
|
||||||
|
order: 4,
|
||||||
|
content: {
|
||||||
|
title: { string_id: "onboarding-fxa-title" },
|
||||||
|
text: { string_id: "onboarding-fxa-text" },
|
||||||
|
icon: "sync",
|
||||||
|
primary_button: {
|
||||||
|
label: { string_id: "onboarding-button-label-get-started" },
|
||||||
|
action: {
|
||||||
|
type: "OPEN_URL",
|
||||||
|
data: {
|
||||||
|
args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"),
|
||||||
|
where: "tabshifted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targeting:
|
||||||
|
"trailheadInterrupt == 'control' && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||||
|
trigger: { id: "showOnboarding" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "TRAILHEAD_1",
|
id: "TRAILHEAD_1",
|
||||||
template: "trailhead",
|
template: "trailhead",
|
||||||
|
@ -326,13 +436,7 @@ const ONBOARDING_MESSAGES = () => [
|
||||||
{
|
{
|
||||||
id: "FXA_1",
|
id: "FXA_1",
|
||||||
template: "fxa_overlay",
|
template: "fxa_overlay",
|
||||||
content: {},
|
|
||||||
trigger: { id: "firstRun" },
|
trigger: { id: "firstRun" },
|
||||||
includeBundle: {
|
|
||||||
length: 3,
|
|
||||||
template: "onboarding",
|
|
||||||
trigger: { id: "showOnboarding" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "RETURN_TO_AMO_1",
|
id: "RETURN_TO_AMO_1",
|
||||||
|
@ -357,11 +461,6 @@ const ONBOARDING_MESSAGES = () => [
|
||||||
label: { string_id: "return-to-amo-get-started-button" },
|
label: { string_id: "return-to-amo-get-started-button" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
includeBundle: {
|
|
||||||
length: 3,
|
|
||||||
template: "onboarding",
|
|
||||||
trigger: { id: "showOnboarding" },
|
|
||||||
},
|
|
||||||
targeting:
|
targeting:
|
||||||
"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||||
trigger: { id: "firstRun" },
|
trigger: { id: "firstRun" },
|
||||||
|
|
|
@ -79,9 +79,9 @@ const MESSAGES = () => [
|
||||||
// Never saw this message or saw it in the past 4 days or more recent
|
// Never saw this message or saw it in the past 4 days or more recent
|
||||||
targeting: `isWhatsNewPanelEnabled &&
|
targeting: `isWhatsNewPanelEnabled &&
|
||||||
(earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
|
(earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
|
||||||
(!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
|
messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length == 0 ||
|
||||||
(messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
|
(messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
|
||||||
currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
|
currentDate|date - messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000)`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "WHATS_NEW_70_1",
|
id: "WHATS_NEW_70_1",
|
||||||
|
|
|
@ -13,11 +13,6 @@ ChromeUtils.defineModuleGetter(
|
||||||
"EveryWindow",
|
"EveryWindow",
|
||||||
"resource:///modules/EveryWindow.jsm"
|
"resource:///modules/EveryWindow.jsm"
|
||||||
);
|
);
|
||||||
ChromeUtils.defineModuleGetter(
|
|
||||||
this,
|
|
||||||
"PrivateBrowsingUtils",
|
|
||||||
"resource://gre/modules/PrivateBrowsingUtils.jsm"
|
|
||||||
);
|
|
||||||
|
|
||||||
const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
|
const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
|
||||||
|
|
||||||
|
@ -29,16 +24,14 @@ const BUTTON_STRING_ID = "cfr-whatsnew-button";
|
||||||
|
|
||||||
class _ToolbarPanelHub {
|
class _ToolbarPanelHub {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.triggerId = "whatsNewPanelOpened";
|
|
||||||
this._showAppmenuButton = this._showAppmenuButton.bind(this);
|
this._showAppmenuButton = this._showAppmenuButton.bind(this);
|
||||||
this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
|
this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
|
||||||
this._showToolbarButton = this._showToolbarButton.bind(this);
|
this._showToolbarButton = this._showToolbarButton.bind(this);
|
||||||
this._hideToolbarButton = this._hideToolbarButton.bind(this);
|
this._hideToolbarButton = this._hideToolbarButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(waitForInitialized, { getMessages, dispatch }) {
|
async init(waitForInitialized, { getMessages }) {
|
||||||
this._getMessages = getMessages;
|
this._getMessages = getMessages;
|
||||||
this._dispatch = dispatch;
|
|
||||||
// Wait for ASRouter messages to become available in order to know
|
// Wait for ASRouter messages to become available in order to know
|
||||||
// if we can show the What's New panel
|
// if we can show the What's New panel
|
||||||
await waitForInitialized;
|
await waitForInitialized;
|
||||||
|
@ -139,36 +132,19 @@ class _ToolbarPanelHub {
|
||||||
|
|
||||||
if (messages && !container.querySelector(".whatsNew-message")) {
|
if (messages && !container.querySelector(".whatsNew-message")) {
|
||||||
let previousDate = 0;
|
let previousDate = 0;
|
||||||
for (let message of messages) {
|
for (let { content } of messages) {
|
||||||
container.appendChild(
|
container.appendChild(
|
||||||
this._createMessageElements(win, doc, message, previousDate)
|
this._createMessageElements(win, doc, content, previousDate)
|
||||||
);
|
);
|
||||||
previousDate = message.content.published_date;
|
previousDate = content.published_date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: TELEMETRY
|
||||||
this._onPanelHidden(win);
|
this._onPanelHidden(win);
|
||||||
|
|
||||||
// Panel impressions are not associated with one particular message
|
|
||||||
// but with a set of messages. We concatenate message ids and send them
|
|
||||||
// back for every impression.
|
|
||||||
const eventId = {
|
|
||||||
id: messages
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.sort()
|
|
||||||
.join(","),
|
|
||||||
};
|
|
||||||
// Check `mainview` attribute to determine if the panel is shown as a
|
|
||||||
// subview (inside the application menu) or as a toolbar dropdown.
|
|
||||||
// https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268
|
|
||||||
const mainview = win.PanelUI.whatsNewPanel.hasAttribute("mainview");
|
|
||||||
this.sendUserEventTelemetry(win, "IMPRESSION", eventId, {
|
|
||||||
value: { view: mainview ? "toolbar_dropdown" : "application_menu" },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createMessageElements(win, doc, message, previousDate) {
|
_createMessageElements(win, doc, content, previousDate) {
|
||||||
const { content } = message;
|
|
||||||
const messageEl = this._createElement(doc, "div");
|
const messageEl = this._createElement(doc, "div");
|
||||||
messageEl.classList.add("whatsNew-message");
|
messageEl.classList.add("whatsNew-message");
|
||||||
|
|
||||||
|
@ -191,7 +167,7 @@ class _ToolbarPanelHub {
|
||||||
csp: null,
|
csp: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sendUserEventTelemetry(win, "CLICK", message);
|
// TODO: TELEMETRY
|
||||||
});
|
});
|
||||||
|
|
||||||
if (content.icon_url) {
|
if (content.icon_url) {
|
||||||
|
@ -285,30 +261,6 @@ class _ToolbarPanelHub {
|
||||||
_hideElement(document, id) {
|
_hideElement(document, id) {
|
||||||
document.getElementById(id).setAttribute("hidden", true);
|
document.getElementById(id).setAttribute("hidden", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendTelemetry(ping) {
|
|
||||||
this._dispatch({
|
|
||||||
type: "TOOLBAR_PANEL_TELEMETRY",
|
|
||||||
data: { action: "cfr_user_event", source: "CFR", ...ping },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendUserEventTelemetry(win, event, message, options = {}) {
|
|
||||||
// Only send pings for non private browsing windows
|
|
||||||
if (
|
|
||||||
win &&
|
|
||||||
!PrivateBrowsingUtils.isBrowserPrivate(
|
|
||||||
win.ownerGlobal.gBrowser.selectedBrowser
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._sendTelemetry({
|
|
||||||
message_id: message.id,
|
|
||||||
bucket_id: message.id,
|
|
||||||
event,
|
|
||||||
value: options.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ToolbarPanelHub = _ToolbarPanelHub;
|
this._ToolbarPanelHub = _ToolbarPanelHub;
|
||||||
|
|
|
@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
|
||||||
newtab-topsites-preview-button = Preview
|
newtab-topsites-preview-button = Preview
|
||||||
newtab-topsites-add-button = Add
|
newtab-topsites-add-button = Add
|
||||||
|
|
||||||
## Top Sites - Delete history confirmation dialog.
|
## Top Sites - Delete history confirmation dialog.
|
||||||
|
|
||||||
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
|
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
|
||||||
# "This action" refers to deleting a page from history.
|
# "This action" refers to deleting a page from history.
|
||||||
|
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
|
||||||
# Bookmark is a verb here.
|
# Bookmark is a verb here.
|
||||||
newtab-menu-bookmark = Bookmark
|
newtab-menu-bookmark = Bookmark
|
||||||
|
|
||||||
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
|
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
|
||||||
## it is a noun. As in, "Copy the link that belongs to this downloaded item".
|
## it is a noun. As in, "Copy the link that belongs to this downloaded item".
|
||||||
|
|
||||||
newtab-menu-copy-download-link = Copy Download Link
|
newtab-menu-copy-download-link = Copy Download Link
|
||||||
|
@ -117,7 +117,7 @@ newtab-label-recommended = Trending
|
||||||
newtab-label-saved = Saved to { -pocket-brand-name }
|
newtab-label-saved = Saved to { -pocket-brand-name }
|
||||||
newtab-label-download = Downloaded
|
newtab-label-download = Downloaded
|
||||||
|
|
||||||
## Section Menu: These strings are displayed in the section context menu and are
|
## Section Menu: These strings are displayed in the section context menu and are
|
||||||
## meant as a call to action for the given section.
|
## meant as a call to action for the given section.
|
||||||
|
|
||||||
newtab-section-menu-remove-section = Remove Section
|
newtab-section-menu-remove-section = Remove Section
|
||||||
|
@ -131,13 +131,6 @@ newtab-section-menu-move-up = Move Up
|
||||||
newtab-section-menu-move-down = Move Down
|
newtab-section-menu-move-down = Move Down
|
||||||
newtab-section-menu-privacy-notice = Privacy Notice
|
newtab-section-menu-privacy-notice = Privacy Notice
|
||||||
|
|
||||||
## Section aria-labels
|
|
||||||
|
|
||||||
newtab-section-collapse-section-label =
|
|
||||||
.aria-label = Collapse Section
|
|
||||||
newtab-section-expand-section-label =
|
|
||||||
.aria-label = Expand Section
|
|
||||||
|
|
||||||
## Section Headers.
|
## Section Headers.
|
||||||
|
|
||||||
newtab-section-header-topsites = Top Sites
|
newtab-section-header-topsites = Top Sites
|
||||||
|
|
|
@ -27,5 +27,4 @@ skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
|
||||||
[browser_topsites_section.js]
|
[browser_topsites_section.js]
|
||||||
[browser_asrouter_cfr.js]
|
[browser_asrouter_cfr.js]
|
||||||
skip-if = fission
|
skip-if = fission
|
||||||
skip-if = fission
|
|
||||||
[browser_asrouter_bookmarkpanel.js]
|
[browser_asrouter_bookmarkpanel.js]
|
||||||
|
|
|
@ -83,6 +83,9 @@ add_task(async () => {
|
||||||
".ReturnToAMOContainer",
|
".ReturnToAMOContainer",
|
||||||
".ReturnToAMOAddonContents",
|
".ReturnToAMOAddonContents",
|
||||||
".ReturnToAMOIcon",
|
".ReturnToAMOIcon",
|
||||||
|
// Regular onboarding cards
|
||||||
|
".onboardingMessageContainer",
|
||||||
|
".onboardingMessage",
|
||||||
]) {
|
]) {
|
||||||
ok(content.document.querySelector(selector), `Should render ${selector}`);
|
ok(content.document.querySelector(selector), `Should render ${selector}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,6 @@ describe("ASRouter", () => {
|
||||||
Router.waitForInitialized,
|
Router.waitForInitialized,
|
||||||
{
|
{
|
||||||
getMessages: Router.handleMessageRequest,
|
getMessages: Router.handleMessageRequest,
|
||||||
dispatch: Router.dispatch,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1302,6 +1301,23 @@ describe("ASRouter", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#onMessage: DISMISS_BUNDLE", () => {
|
||||||
|
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
|
||||||
|
await Router.setState({ lastMessageId: "foo" });
|
||||||
|
const msg = fakeAsyncMessage({
|
||||||
|
type: "DISMISS_BUNDLE",
|
||||||
|
data: { bundle: FAKE_BUNDLE },
|
||||||
|
});
|
||||||
|
await Router.onMessage(msg);
|
||||||
|
|
||||||
|
assert.calledWith(
|
||||||
|
channel.sendAsyncMessage,
|
||||||
|
PARENT_TO_CHILD_MESSAGE_NAME,
|
||||||
|
{ type: "CLEAR_BUNDLE" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
|
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
|
||||||
it("should remove the id from the messageBlockList", async () => {
|
it("should remove the id from the messageBlockList", async () => {
|
||||||
await Router.onMessage(
|
await Router.onMessage(
|
||||||
|
|
|
@ -20,6 +20,21 @@ const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
|
||||||
);
|
);
|
||||||
|
|
||||||
FAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: "fakeprovider" });
|
FAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: "fakeprovider" });
|
||||||
|
const FAKE_BUNDLED_MESSAGE = {
|
||||||
|
bundle: [
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
template: "onboarding",
|
||||||
|
content: {
|
||||||
|
title: "Foo",
|
||||||
|
primary_button: { label: "Bar" },
|
||||||
|
text: "Foo123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extraTemplateStrings: {},
|
||||||
|
template: "onboarding",
|
||||||
|
};
|
||||||
|
|
||||||
describe("ASRouterUtils", () => {
|
describe("ASRouterUtils", () => {
|
||||||
let global;
|
let global;
|
||||||
|
@ -63,11 +78,6 @@ describe("ASRouterUISurface", () => {
|
||||||
location: { href: "" },
|
location: { href: "" },
|
||||||
_listeners: new Set(),
|
_listeners: new Set(),
|
||||||
_visibilityState: "hidden",
|
_visibilityState: "hidden",
|
||||||
head: {
|
|
||||||
appendChild(el) {
|
|
||||||
return el;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
get visibilityState() {
|
get visibilityState() {
|
||||||
return this._visibilityState;
|
return this._visibilityState;
|
||||||
},
|
},
|
||||||
|
@ -88,15 +98,7 @@ describe("ASRouterUISurface", () => {
|
||||||
return document.createElement("body");
|
return document.createElement("body");
|
||||||
},
|
},
|
||||||
getElementById(id) {
|
getElementById(id) {
|
||||||
switch (id) {
|
return id === "header-asrouter-container" ? headerPortal : footerPortal;
|
||||||
case "header-asrouter-container":
|
|
||||||
return headerPortal;
|
|
||||||
default:
|
|
||||||
return footerPortal;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createElement(tag) {
|
|
||||||
return document.createElement(tag);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
global = new GlobalOverrider();
|
global = new GlobalOverrider();
|
||||||
|
@ -143,6 +145,11 @@ describe("ASRouterUISurface", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render the component if a bundle of messages is defined", () => {
|
||||||
|
wrapper.setState({ bundle: FAKE_BUNDLED_MESSAGE });
|
||||||
|
assert.isTrue(wrapper.exists());
|
||||||
|
});
|
||||||
|
|
||||||
it("should render a preview banner if message provider is preview", () => {
|
it("should render a preview banner if message provider is preview", () => {
|
||||||
wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } });
|
wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } });
|
||||||
assert.isTrue(wrapper.find(".snippets-preview-banner").exists());
|
assert.isTrue(wrapper.find(".snippets-preview-banner").exists());
|
||||||
|
@ -166,13 +173,10 @@ describe("ASRouterUISurface", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render a trailhead message in the header portal", async () => {
|
it("should render a trailhead message in the header portal", async () => {
|
||||||
// wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);
|
|
||||||
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
|
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
|
||||||
msg => msg.template === "trailhead"
|
msg => msg.template === "trailhead"
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({ message });
|
wrapper.setState({ message });
|
||||||
|
|
||||||
assert.isTrue(headerPortal.childElementCount > 0);
|
assert.isTrue(headerPortal.childElementCount > 0);
|
||||||
assert.equal(footerPortal.childElementCount, 0);
|
assert.equal(footerPortal.childElementCount, 0);
|
||||||
});
|
});
|
||||||
|
@ -366,5 +370,18 @@ describe("ASRouterUISurface", () => {
|
||||||
);
|
);
|
||||||
assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR");
|
assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call .sendTelemetry with the right message data when a bundle is dismissed", () => {
|
||||||
|
wrapper.instance().dismissBundle([{ id: 1 }, { id: 2 }, { id: 3 }])();
|
||||||
|
|
||||||
|
assert.calledOnce(ASRouterUtils.sendTelemetry);
|
||||||
|
assert.calledWith(ASRouterUtils.sendTelemetry, {
|
||||||
|
action: "onboarding_user_event",
|
||||||
|
event: "DISMISS",
|
||||||
|
id: "onboarding-cards",
|
||||||
|
message_id: "1,2,3",
|
||||||
|
source: "onboarding-cards",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,14 +4,12 @@ export const PARENT_TO_CHILD_MESSAGE_NAME = "ASRouter:parent-to-child";
|
||||||
export const FAKE_LOCAL_MESSAGES = [
|
export const FAKE_LOCAL_MESSAGES = [
|
||||||
{
|
{
|
||||||
id: "foo",
|
id: "foo",
|
||||||
provider: "snippets",
|
|
||||||
template: "simple_snippet",
|
template: "simple_snippet",
|
||||||
content: { title: "Foo", body: "Foo123" },
|
content: { title: "Foo", body: "Foo123" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "foo1",
|
id: "foo1",
|
||||||
template: "simple_snippet",
|
template: "simple_snippet",
|
||||||
provider: "snippets",
|
|
||||||
bundled: 2,
|
bundled: 2,
|
||||||
order: 1,
|
order: 1,
|
||||||
content: { title: "Foo1", body: "Foo123-1" },
|
content: { title: "Foo1", body: "Foo123-1" },
|
||||||
|
@ -19,7 +17,6 @@ export const FAKE_LOCAL_MESSAGES = [
|
||||||
{
|
{
|
||||||
id: "foo2",
|
id: "foo2",
|
||||||
template: "simple_snippet",
|
template: "simple_snippet",
|
||||||
provider: "snippets",
|
|
||||||
bundled: 2,
|
bundled: 2,
|
||||||
order: 2,
|
order: 2,
|
||||||
content: { title: "Foo2", body: "Foo123-2" },
|
content: { title: "Foo2", body: "Foo123-2" },
|
||||||
|
@ -32,19 +29,16 @@ export const FAKE_LOCAL_MESSAGES = [
|
||||||
{ id: "baz", content: { title: "Foo", body: "Foo123" } },
|
{ id: "baz", content: { title: "Foo", body: "Foo123" } },
|
||||||
{
|
{
|
||||||
id: "newsletter",
|
id: "newsletter",
|
||||||
provider: "snippets",
|
|
||||||
template: "newsletter_snippet",
|
template: "newsletter_snippet",
|
||||||
content: { title: "Foo", body: "Foo123" },
|
content: { title: "Foo", body: "Foo123" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fxa",
|
id: "fxa",
|
||||||
provider: "snippets",
|
|
||||||
template: "fxa_signup_snippet",
|
template: "fxa_signup_snippet",
|
||||||
content: { title: "Foo", body: "Foo123" },
|
content: { title: "Foo", body: "Foo123" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "belowsearch",
|
id: "belowsearch",
|
||||||
provider: "snippets",
|
|
||||||
template: "simple_below_search_snippet",
|
template: "simple_below_search_snippet",
|
||||||
content: { text: "Foo" },
|
content: { text: "Foo" },
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,210 +0,0 @@
|
||||||
import {
|
|
||||||
helpers,
|
|
||||||
FirstRun,
|
|
||||||
FLUENT_FILES,
|
|
||||||
} from "content-src/asrouter/templates/FirstRun/FirstRun";
|
|
||||||
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
|
|
||||||
import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
|
|
||||||
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
|
|
||||||
import { mount } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const FAKE_TRIPLETS = [
|
|
||||||
{
|
|
||||||
id: "CARD_1",
|
|
||||||
content: {
|
|
||||||
title: { string_id: "onboarding-private-browsing-title" },
|
|
||||||
text: { string_id: "onboarding-private-browsing-text" },
|
|
||||||
icon: "icon",
|
|
||||||
primary_button: {
|
|
||||||
label: { string_id: "onboarding-button-label-try-now" },
|
|
||||||
action: {
|
|
||||||
type: "OPEN_URL",
|
|
||||||
data: { args: "https://example.com/" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const FAKE_FLOW_PARAMS = {
|
|
||||||
deviceId: "foo",
|
|
||||||
flowId: "abc1",
|
|
||||||
flowBeginTime: 1234,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getTestMessage(id) {
|
|
||||||
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
|
|
||||||
msg => msg.id === id
|
|
||||||
);
|
|
||||||
return { ...message, bundle: FAKE_TRIPLETS };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("<FirstRun>", () => {
|
|
||||||
let wrapper;
|
|
||||||
let message;
|
|
||||||
let fakeDoc;
|
|
||||||
let sandbox;
|
|
||||||
|
|
||||||
async function setup() {
|
|
||||||
sandbox = sinon.createSandbox();
|
|
||||||
message = await getTestMessage("TRAILHEAD_1");
|
|
||||||
fakeDoc = {
|
|
||||||
body: document.createElement("body"),
|
|
||||||
head: document.createElement("head"),
|
|
||||||
createElement: type => document.createElement(type),
|
|
||||||
getElementById: () => document.createElement("div"),
|
|
||||||
activeElement: document.createElement("div"),
|
|
||||||
};
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(global, "fetch")
|
|
||||||
.withArgs("http://fake.com/endpoint")
|
|
||||||
.resolves({
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: () => Promise.resolve(FAKE_FLOW_PARAMS),
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper = mount(
|
|
||||||
<FirstRun
|
|
||||||
message={message}
|
|
||||||
document={fakeDoc}
|
|
||||||
dispatch={() => {}}
|
|
||||||
sendUserActionTelemetry={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(setup);
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render", () => {
|
|
||||||
assert.ok(wrapper);
|
|
||||||
});
|
|
||||||
describe("with both interrupt and triplets", () => {
|
|
||||||
it("should render interrupt and triplets", () => {
|
|
||||||
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
|
|
||||||
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
|
|
||||||
});
|
|
||||||
it("should show the card panel and hide the content on the Triplets", () => {
|
|
||||||
// This is so the container shows up in the background but we can fade in the content when intterupt is closed.
|
|
||||||
const tripletsProps = wrapper.find(Triplets).props();
|
|
||||||
assert.propertyVal(tripletsProps, "showCardPanel", true);
|
|
||||||
assert.propertyVal(tripletsProps, "showContent", false);
|
|
||||||
});
|
|
||||||
it("should set the UTM term to trailhead-join (for the traihead-join message)", () => {
|
|
||||||
const iProps = wrapper.find(Interrupt).props();
|
|
||||||
const tProps = wrapper.find(Triplets).props();
|
|
||||||
assert.propertyVal(iProps, "UTMTerm", "trailhead-join");
|
|
||||||
assert.propertyVal(tProps, "UTMTerm", "trailhead-join-card");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with an interrupt but no triplets", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
message.bundle = []; // Empty triplets
|
|
||||||
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
|
|
||||||
});
|
|
||||||
it("should render interrupt but no triplets", () => {
|
|
||||||
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
|
|
||||||
assert.lengthOf(wrapper.find(Triplets), 0, "<Triplets>");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with triplets but no interrupt", () => {
|
|
||||||
it("should render interrupt but no triplets", () => {
|
|
||||||
delete message.content; // Empty interrupt
|
|
||||||
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
|
|
||||||
|
|
||||||
assert.lengthOf(wrapper.find(Interrupt), 0, "<Interrupt>");
|
|
||||||
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with no triplets or interrupt", () => {
|
|
||||||
it("should render empty", () => {
|
|
||||||
message = { type: "FOO_123" };
|
|
||||||
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
|
|
||||||
|
|
||||||
assert.isTrue(wrapper.isEmptyRender());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load flow params on mount if fxaEndpoint is defined", () => {
|
|
||||||
const spy = sandbox.spy(helpers, "fetchFlowParams");
|
|
||||||
wrapper = mount(
|
|
||||||
<FirstRun
|
|
||||||
message={message}
|
|
||||||
document={fakeDoc}
|
|
||||||
dispatch={() => {}}
|
|
||||||
fxaEndpoint="https://foo.com"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined", () => {
|
|
||||||
const spy = sandbox.spy(helpers, "fetchFlowParams");
|
|
||||||
wrapper = mount(
|
|
||||||
<FirstRun message={message} document={fakeDoc} dispatch={() => {}} />
|
|
||||||
);
|
|
||||||
assert.notCalled(spy);
|
|
||||||
wrapper.setProps({ fxaEndpoint: "https://foo.com" });
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not load flow params again onUpdate if they were already set", () => {
|
|
||||||
const spy = sandbox.spy(helpers, "fetchFlowParams");
|
|
||||||
wrapper = mount(
|
|
||||||
<FirstRun
|
|
||||||
message={message}
|
|
||||||
document={fakeDoc}
|
|
||||||
dispatch={() => {}}
|
|
||||||
fxaEndpoint="https://foo.com"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
wrapper.setProps({ foo: "bar" });
|
|
||||||
wrapper.setProps({ foo: "baz" });
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load fluent files on mount", () => {
|
|
||||||
assert.lengthOf(fakeDoc.head.querySelectorAll("link"), FLUENT_FILES.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should hide the interrupt and show the triplets when onNextScene is called", () => {
|
|
||||||
// Simulate calling next scene
|
|
||||||
wrapper
|
|
||||||
.find(Interrupt)
|
|
||||||
.find(".trailheadStart")
|
|
||||||
.simulate("click");
|
|
||||||
|
|
||||||
assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
|
|
||||||
assert.isTrue(
|
|
||||||
wrapper
|
|
||||||
.find(Triplets)
|
|
||||||
.find(".trailheadCardGrid")
|
|
||||||
.hasClass("show"),
|
|
||||||
"Show triplet content"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should hide triplets when closeTriplets is called", () => {
|
|
||||||
// Simulate calling next scene
|
|
||||||
wrapper
|
|
||||||
.find(Triplets)
|
|
||||||
.find(".icon-dismiss")
|
|
||||||
.simulate("click");
|
|
||||||
|
|
||||||
assert.isFalse(
|
|
||||||
wrapper
|
|
||||||
.find(Triplets)
|
|
||||||
.find(".trailheadCardGrid")
|
|
||||||
.hasClass("show"),
|
|
||||||
"Show triplet content"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
|
|
||||||
import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
|
|
||||||
import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
|
|
||||||
import { Trailhead } from "content-src/asrouter/templates//Trailhead/Trailhead";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
describe("<Interrupt>", () => {
|
|
||||||
let wrapper;
|
|
||||||
it("should render Return TO AMO when the message has a template of return_to_amo_overlay", () => {
|
|
||||||
wrapper = shallow(
|
|
||||||
<Interrupt
|
|
||||||
message={{ id: "FOO", content: {}, template: "return_to_amo_overlay" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
assert.lengthOf(wrapper.find(ReturnToAMO), 1);
|
|
||||||
});
|
|
||||||
it("should render Trailhead when the message has a template of trailhead", () => {
|
|
||||||
wrapper = shallow(
|
|
||||||
<Interrupt message={{ id: "FOO", content: {}, template: "trailhead" }} />
|
|
||||||
);
|
|
||||||
assert.lengthOf(wrapper.find(Trailhead), 1);
|
|
||||||
});
|
|
||||||
it("should render StartupOverlay when the message has a template of fxa_overlay", () => {
|
|
||||||
wrapper = shallow(
|
|
||||||
<Interrupt message={{ id: "FOO", template: "fxa_overlay" }} />
|
|
||||||
);
|
|
||||||
assert.lengthOf(wrapper.find(StartupOverlay), 1);
|
|
||||||
});
|
|
||||||
it("should throw an error if another type of message is dispatched", () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
wrapper = shallow(
|
|
||||||
<Interrupt message={{ id: "FOO", template: "something" }} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -4,7 +4,7 @@ import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
|
import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
|
||||||
|
|
||||||
export const CARDS = [
|
const CARDS = [
|
||||||
{
|
{
|
||||||
content: {
|
content: {
|
||||||
title: { string_id: "onboarding-private-browsing-title" },
|
title: { string_id: "onboarding-private-browsing-title" },
|
||||||
|
@ -27,13 +27,11 @@ describe("<Trailhead>", () => {
|
||||||
let dispatch;
|
let dispatch;
|
||||||
let onAction;
|
let onAction;
|
||||||
let sandbox;
|
let sandbox;
|
||||||
let onNextScene;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
dispatch = sandbox.stub();
|
dispatch = sandbox.stub();
|
||||||
onAction = sandbox.stub();
|
onAction = sandbox.stub();
|
||||||
onNextScene = sandbox.stub();
|
|
||||||
sandbox.stub(global, "fetch").resolves({
|
sandbox.stub(global, "fetch").resolves({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -61,12 +59,10 @@ describe("<Trailhead>", () => {
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<Trailhead
|
<Trailhead
|
||||||
message={message}
|
message={message}
|
||||||
UTMTerm={message.utm_term}
|
|
||||||
fxaEndpoint="https://accounts.firefox.com/endpoint"
|
fxaEndpoint="https://accounts.firefox.com/endpoint"
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
onAction={onAction}
|
onAction={onAction}
|
||||||
document={fakeDocument}
|
document={fakeDocument}
|
||||||
onNextScene={onNextScene}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -75,13 +71,11 @@ describe("<Trailhead>", () => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit UserEvent SKIPPED_SIGNIN and call nextScene when you click the start browsing button", () => {
|
it("should emit UserEvent SKIPPED_SIGNIN when you click the start browsing button", () => {
|
||||||
let skipButton = wrapper.find(".trailheadStart");
|
let skipButton = wrapper.find(".trailheadStart");
|
||||||
assert.ok(skipButton.exists());
|
assert.ok(skipButton.exists());
|
||||||
skipButton.simulate("click");
|
skipButton.simulate("click");
|
||||||
|
|
||||||
assert.calledOnce(onNextScene);
|
|
||||||
|
|
||||||
assert.calledOnce(dispatch);
|
assert.calledOnce(dispatch);
|
||||||
assert.isUserEventAction(dispatch.firstCall.args[0]);
|
assert.isUserEventAction(dispatch.firstCall.args[0]);
|
||||||
assert.calledWith(
|
assert.calledWith(
|
||||||
|
@ -125,6 +119,37 @@ describe("<Trailhead>", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add utm_* query params to card actions", () => {
|
||||||
|
let { action } = CARDS[0].content.primary_button;
|
||||||
|
wrapper.instance().onCardAction(action);
|
||||||
|
assert.calledOnce(onAction);
|
||||||
|
const url = onAction.firstCall.args[0].data.args;
|
||||||
|
assert.equal(
|
||||||
|
url,
|
||||||
|
"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add flow parameters to card action urls if addFlowParams is true", () => {
|
||||||
|
let action = {
|
||||||
|
type: "OPEN_URL",
|
||||||
|
addFlowParams: true,
|
||||||
|
data: { args: "https://example.com/path?foo=bar" },
|
||||||
|
};
|
||||||
|
wrapper.setState({
|
||||||
|
deviceId: "abc",
|
||||||
|
flowId: "123",
|
||||||
|
flowBeginTime: 456,
|
||||||
|
});
|
||||||
|
wrapper.instance().onCardAction(action);
|
||||||
|
assert.calledOnce(onAction);
|
||||||
|
const url = onAction.firstCall.args[0].data.args;
|
||||||
|
assert.equal(
|
||||||
|
url,
|
||||||
|
"https://example.com/path?foo=bar&utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card&device_id=abc&flow_id=123&flow_begin_time=456"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should keep focus in dialog when blurring start button", () => {
|
it("should keep focus in dialog when blurring start button", () => {
|
||||||
const skipButton = wrapper.find(".trailheadStart");
|
const skipButton = wrapper.find(".trailheadStart");
|
||||||
sandbox.stub(dummyNode, "focus");
|
sandbox.stub(dummyNode, "focus");
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { mount } from "enzyme";
|
|
||||||
import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
|
|
||||||
import { OnboardingCard } from "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const CARDS = [
|
|
||||||
{
|
|
||||||
id: "CARD_1",
|
|
||||||
content: {
|
|
||||||
title: { string_id: "onboarding-private-browsing-title" },
|
|
||||||
text: { string_id: "onboarding-private-browsing-text" },
|
|
||||||
icon: "icon",
|
|
||||||
primary_button: {
|
|
||||||
label: { string_id: "onboarding-button-label-try-now" },
|
|
||||||
action: {
|
|
||||||
type: "OPEN_URL",
|
|
||||||
data: { args: "https://example.com/" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("<Triplets>", () => {
|
|
||||||
let wrapper;
|
|
||||||
let sandbox;
|
|
||||||
let sendTelemetryStub;
|
|
||||||
let onAction;
|
|
||||||
let onHide;
|
|
||||||
|
|
||||||
async function setup() {
|
|
||||||
sandbox = sinon.createSandbox();
|
|
||||||
sendTelemetryStub = sandbox.stub();
|
|
||||||
onAction = sandbox.stub();
|
|
||||||
onHide = sandbox.stub();
|
|
||||||
|
|
||||||
wrapper = mount(
|
|
||||||
<Triplets
|
|
||||||
cards={CARDS}
|
|
||||||
showCardPanel={true}
|
|
||||||
showContent={true}
|
|
||||||
hideContainer={onHide}
|
|
||||||
onAction={onAction}
|
|
||||||
UTMTerm="trailhead-join-card"
|
|
||||||
sendUserActionTelemetry={sendTelemetryStub}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(setup);
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add an expanded class to container if props.showCardPanel is true", () => {
|
|
||||||
wrapper.setProps({ showCardPanel: true });
|
|
||||||
assert.isTrue(
|
|
||||||
wrapper.find(".trailheadCards").hasClass("expanded"),
|
|
||||||
"has .expanded)"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should add a collapsed class to container if props.showCardPanel is true", () => {
|
|
||||||
wrapper.setProps({ showCardPanel: false });
|
|
||||||
assert.isFalse(
|
|
||||||
wrapper.find(".trailheadCards").hasClass("expanded"),
|
|
||||||
"has .expanded)"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should send telemetry and call props.hideContainer when the dismiss button is clicked", () => {
|
|
||||||
wrapper.find("button.icon-dismiss").simulate("click");
|
|
||||||
assert.calledOnce(onHide);
|
|
||||||
assert.calledWith(sendTelemetryStub, {
|
|
||||||
event: "DISMISS",
|
|
||||||
message_id: CARDS[0].id,
|
|
||||||
id: "onboarding-cards",
|
|
||||||
action: "onboarding_user_event",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should add utm_* query params to card actions and send the right ping when a card button is clicked", () => {
|
|
||||||
wrapper
|
|
||||||
.find(OnboardingCard)
|
|
||||||
.find("button.onboardingButton")
|
|
||||||
.simulate("click");
|
|
||||||
assert.calledOnce(onAction);
|
|
||||||
const url = onAction.firstCall.args[0].data.args;
|
|
||||||
assert.equal(
|
|
||||||
url,
|
|
||||||
"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
|
|
||||||
);
|
|
||||||
assert.calledWith(sendTelemetryStub, {
|
|
||||||
event: "CLICK_BUTTON",
|
|
||||||
message_id: CARDS[0].id,
|
|
||||||
id: "TRAILHEAD",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -69,6 +69,21 @@ describe("CollapsibleSection", () => {
|
||||||
.simulate("click");
|
.simulate("click");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should fire a pref change event when section title arrow is clicked", done => {
|
||||||
|
function dispatch(a) {
|
||||||
|
if (a.type === at.UPDATE_SECTION_PREFS) {
|
||||||
|
assert.equal(a.data.id, DEFAULT_PROPS.id);
|
||||||
|
assert.equal(a.data.value.collapsed, true);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setup({ dispatch });
|
||||||
|
wrapper
|
||||||
|
.find(".click-target")
|
||||||
|
.at(1)
|
||||||
|
.simulate("click");
|
||||||
|
});
|
||||||
|
|
||||||
it("should not fire a pref change when section title is clicked if sectionBody is falsy", () => {
|
it("should not fire a pref change when section title is clicked if sectionBody is falsy", () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
setup({ dispatch });
|
setup({ dispatch });
|
||||||
|
|
|
@ -128,10 +128,7 @@ describe("<ContextMenu>", () => {
|
||||||
it("should be tabbable", () => {
|
it("should be tabbable", () => {
|
||||||
const options = [{ label: "item1", icon: "icon1" }, { type: "separator" }];
|
const options = [{ label: "item1", icon: "icon1" }, { type: "separator" }];
|
||||||
const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||||
assert.equal(
|
assert.equal(wrapper.find(".context-menu-item").props().role, "menuitem");
|
||||||
wrapper.find(".context-menu-item").props().role,
|
|
||||||
"presentation"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it("should call onUpdate with false when an option is clicked", () => {
|
it("should call onUpdate with false when an option is clicked", () => {
|
||||||
const onUpdate = sinon.spy();
|
const onUpdate = sinon.spy();
|
||||||
|
|
|
@ -66,6 +66,10 @@ describe("<ReturnToAMO>", () => {
|
||||||
sendUserActionTelemetryStub.reset();
|
sendUserActionTelemetryStub.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call onReady on componentDidMount", () => {
|
||||||
|
assert.calledOnce(onReady);
|
||||||
|
});
|
||||||
|
|
||||||
it("should send telemetry on block", () => {
|
it("should send telemetry on block", () => {
|
||||||
wrapper.instance().onBlockButton();
|
wrapper.instance().onBlockButton();
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
|
import { _StartupOverlay as StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
|
||||||
|
|
||||||
describe("<StartupOverlay>", () => {
|
describe("<StartupOverlay>", () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let dispatch;
|
let dispatch;
|
||||||
|
let onReady;
|
||||||
let onBlock;
|
let onBlock;
|
||||||
let sandbox;
|
let sandbox;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.createSandbox();
|
sandbox = sinon.sandbox.create();
|
||||||
dispatch = sandbox.stub();
|
dispatch = sandbox.stub();
|
||||||
|
onReady = sandbox.stub();
|
||||||
onBlock = sandbox.stub();
|
onBlock = sandbox.stub();
|
||||||
|
|
||||||
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
|
wrapper = mount(
|
||||||
|
<StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add show class after mount and timeout", async () => {
|
it("should not render if state.show is false", () => {
|
||||||
|
wrapper.setState({ overlayRemoved: true });
|
||||||
|
assert.isTrue(wrapper.isEmptyRender());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call prop.onReady after mount + timeout", async () => {
|
||||||
const clock = sandbox.useFakeTimers();
|
const clock = sandbox.useFakeTimers();
|
||||||
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
|
wrapper = mount(
|
||||||
assert.isFalse(
|
<StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
|
||||||
wrapper.find(".overlay-wrapper").hasClass("show"),
|
|
||||||
".overlay-wrapper does not have .show class"
|
|
||||||
);
|
);
|
||||||
|
wrapper.setState({ overlayRemoved: false });
|
||||||
|
|
||||||
clock.tick(10);
|
clock.tick(10);
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
assert.isTrue(
|
assert.calledOnce(onReady);
|
||||||
wrapper.find(".overlay-wrapper").hasClass("show"),
|
|
||||||
".overlay-wrapper has .show class"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit UserEvent SKIPPED_SIGNIN when you click the skip button", () => {
|
it("should emit UserEvent SKIPPED_SIGNIN when you click the skip button", () => {
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { addUtmParams } from "content-src/asrouter/templates/FirstRun/addUtmParams";
|
|
||||||
|
|
||||||
describe("addUtmParams", () => {
|
|
||||||
it("should convert a string URL", () => {
|
|
||||||
const result = addUtmParams("https://foo.com", "foo");
|
|
||||||
assert.equal(result.hostname, "foo.com");
|
|
||||||
});
|
|
||||||
it("should add all base params", () => {
|
|
||||||
assert.match(
|
|
||||||
addUtmParams(new URL("https://foo.com"), "foo").toString(),
|
|
||||||
/utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should add utm_term", () => {
|
|
||||||
const params = addUtmParams(new URL("https://foo.com"), "foo").searchParams;
|
|
||||||
assert.equal(params.get("utm_term"), "foo", "utm_term");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -16,8 +16,6 @@ describe("ToolbarPanelHub", () => {
|
||||||
let removeObserverStub;
|
let removeObserverStub;
|
||||||
let getBoolPrefStub;
|
let getBoolPrefStub;
|
||||||
let waitForInitializedStub;
|
let waitForInitializedStub;
|
||||||
let isBrowserPrivateStub;
|
|
||||||
let fakeDispatch;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sandbox = sinon.createSandbox();
|
sandbox = sinon.createSandbox();
|
||||||
|
@ -30,7 +28,6 @@ describe("ToolbarPanelHub", () => {
|
||||||
querySelector: sandbox.stub().returns(null),
|
querySelector: sandbox.stub().returns(null),
|
||||||
appendChild: sandbox.stub(),
|
appendChild: sandbox.stub(),
|
||||||
addEventListener: sandbox.stub(),
|
addEventListener: sandbox.stub(),
|
||||||
hasAttribute: sandbox.stub(),
|
|
||||||
};
|
};
|
||||||
fakeDocument = {
|
fakeDocument = {
|
||||||
l10n: {
|
l10n: {
|
||||||
|
@ -61,10 +58,6 @@ describe("ToolbarPanelHub", () => {
|
||||||
MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
|
MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
|
||||||
ownerGlobal: {
|
ownerGlobal: {
|
||||||
openLinkIn: sandbox.stub(),
|
openLinkIn: sandbox.stub(),
|
||||||
gBrowser: "gBrowser",
|
|
||||||
},
|
|
||||||
PanelUI: {
|
|
||||||
whatsNewPanel: fakeElementById,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
everyWindowStub = {
|
everyWindowStub = {
|
||||||
|
@ -74,8 +67,6 @@ describe("ToolbarPanelHub", () => {
|
||||||
addObserverStub = sandbox.stub();
|
addObserverStub = sandbox.stub();
|
||||||
removeObserverStub = sandbox.stub();
|
removeObserverStub = sandbox.stub();
|
||||||
getBoolPrefStub = sandbox.stub();
|
getBoolPrefStub = sandbox.stub();
|
||||||
fakeDispatch = sandbox.stub();
|
|
||||||
isBrowserPrivateStub = sandbox.stub();
|
|
||||||
globals.set("EveryWindow", everyWindowStub);
|
globals.set("EveryWindow", everyWindowStub);
|
||||||
globals.set("Services", {
|
globals.set("Services", {
|
||||||
...Services,
|
...Services,
|
||||||
|
@ -85,9 +76,6 @@ describe("ToolbarPanelHub", () => {
|
||||||
getBoolPref: getBoolPrefStub,
|
getBoolPref: getBoolPrefStub,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
globals.set("PrivateBrowsingUtils", {
|
|
||||||
isBrowserPrivate: isBrowserPrivateStub,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
instance.uninit();
|
instance.uninit();
|
||||||
|
@ -97,10 +85,10 @@ describe("ToolbarPanelHub", () => {
|
||||||
it("should create an instance", () => {
|
it("should create an instance", () => {
|
||||||
assert.ok(instance);
|
assert.ok(instance);
|
||||||
});
|
});
|
||||||
it("should not enableAppmenuButton() on init() if pref is not enabled", async () => {
|
it("should not enableAppmenuButton() on init() if pref is not enabled", () => {
|
||||||
getBoolPrefStub.returns(false);
|
getBoolPrefStub.returns(false);
|
||||||
instance.enableAppmenuButton = sandbox.stub();
|
instance.enableAppmenuButton = sandbox.stub();
|
||||||
await instance.init(waitForInitializedStub, { getMessages: () => {} });
|
instance.init(waitForInitializedStub, { getMessages: () => {} });
|
||||||
assert.notCalled(instance.enableAppmenuButton);
|
assert.notCalled(instance.enableAppmenuButton);
|
||||||
});
|
});
|
||||||
it("should enableAppmenuButton() on init() if pref is enabled", async () => {
|
it("should enableAppmenuButton() on init() if pref is enabled", async () => {
|
||||||
|
@ -227,221 +215,85 @@ describe("ToolbarPanelHub", () => {
|
||||||
instance._hideToolbarButton(fakeWindow);
|
instance._hideToolbarButton(fakeWindow);
|
||||||
assert.calledWith(fakeElementById.setAttribute, "hidden", true);
|
assert.calledWith(fakeElementById.setAttribute, "hidden", true);
|
||||||
});
|
});
|
||||||
describe("#renderMessages", () => {
|
it("should render messages to the panel on renderMessages()", async () => {
|
||||||
let getMessagesStub;
|
const messages = (await PanelTestProvider.getMessages()).filter(
|
||||||
beforeEach(() => {
|
m => m.template === "whatsnew_panel_message"
|
||||||
getMessagesStub = sandbox.stub();
|
);
|
||||||
instance.init(waitForInitializedStub, {
|
messages[0].content.link_text = { string_id: "link_text_id" };
|
||||||
getMessages: getMessagesStub,
|
instance.init(waitForInitializedStub, {
|
||||||
dispatch: fakeDispatch,
|
getMessages: sandbox
|
||||||
});
|
.stub()
|
||||||
|
.returns([messages[0], messages[2], messages[1]]),
|
||||||
});
|
});
|
||||||
it("should render messages to the panel on renderMessages()", async () => {
|
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
for (let message of messages) {
|
||||||
m => m.template === "whatsnew_panel_message"
|
assert.ok(
|
||||||
|
createdElements.find(
|
||||||
|
el => el.tagName === "h2" && el.textContent === message.content.title
|
||||||
|
)
|
||||||
);
|
);
|
||||||
messages[0].content.link_text = { string_id: "link_text_id" };
|
assert.ok(
|
||||||
|
createdElements.find(
|
||||||
|
el => el.tagName === "p" && el.textContent === message.content.body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Call the click handler to make coverage happy.
|
||||||
|
eventListeners.click();
|
||||||
|
assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
|
||||||
|
});
|
||||||
|
it("should only render unique dates (no duplicates)", async () => {
|
||||||
|
instance._createDateElement = sandbox.stub();
|
||||||
|
const messages = (await PanelTestProvider.getMessages()).filter(
|
||||||
|
m => m.template === "whatsnew_panel_message"
|
||||||
|
);
|
||||||
|
const uniqueDates = [
|
||||||
|
...new Set(messages.map(m => m.content.published_date)),
|
||||||
|
];
|
||||||
|
instance.init(waitForInitializedStub, {
|
||||||
|
getMessages: sandbox.stub().returns(messages),
|
||||||
|
});
|
||||||
|
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||||
|
assert.callCount(instance._createDateElement, uniqueDates.length);
|
||||||
|
});
|
||||||
|
it("should listen for panelhidden and remove the toolbar button", async () => {
|
||||||
|
instance.init(waitForInitializedStub, {
|
||||||
|
getMessages: sandbox.stub().returns([]),
|
||||||
|
});
|
||||||
|
fakeDocument.getElementById
|
||||||
|
.withArgs("customizationui-widget-panel")
|
||||||
|
.returns(null);
|
||||||
|
|
||||||
getMessagesStub.returns([messages[0], messages[2], messages[1]]);
|
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
assert.notCalled(fakeElementById.addEventListener);
|
||||||
|
});
|
||||||
|
it("should listen for panelhidden and remove the toolbar button", async () => {
|
||||||
|
instance.init(waitForInitializedStub, {
|
||||||
|
getMessages: sandbox.stub().returns([]),
|
||||||
|
});
|
||||||
|
|
||||||
for (let message of messages) {
|
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||||
assert.ok(
|
|
||||||
createdElements.find(
|
assert.calledOnce(fakeElementById.addEventListener);
|
||||||
el =>
|
assert.calledWithExactly(
|
||||||
el.tagName === "h2" && el.textContent === message.content.title
|
fakeElementById.addEventListener,
|
||||||
)
|
"popuphidden",
|
||||||
);
|
sinon.match.func,
|
||||||
assert.ok(
|
{
|
||||||
createdElements.find(
|
once: true,
|
||||||
el => el.tagName === "p" && el.textContent === message.content.body
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Call the click handler to make coverage happy.
|
);
|
||||||
eventListeners.click();
|
const [, cb] = fakeElementById.addEventListener.firstCall.args;
|
||||||
assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
|
|
||||||
});
|
|
||||||
it("should only render unique dates (no duplicates)", async () => {
|
|
||||||
instance._createDateElement = sandbox.stub();
|
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
|
||||||
m => m.template === "whatsnew_panel_message"
|
|
||||||
);
|
|
||||||
const uniqueDates = [
|
|
||||||
...new Set(messages.map(m => m.content.published_date)),
|
|
||||||
];
|
|
||||||
getMessagesStub.returns(messages);
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
assert.notCalled(everyWindowStub.unregisterCallback);
|
||||||
|
|
||||||
assert.callCount(instance._createDateElement, uniqueDates.length);
|
cb();
|
||||||
});
|
|
||||||
it("should listen for panelhidden and remove the toolbar button", async () => {
|
|
||||||
getMessagesStub.returns([]);
|
|
||||||
fakeDocument.getElementById
|
|
||||||
.withArgs("customizationui-widget-panel")
|
|
||||||
.returns(null);
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
assert.calledOnce(everyWindowStub.unregisterCallback);
|
||||||
|
assert.calledWithExactly(
|
||||||
assert.notCalled(fakeElementById.addEventListener);
|
everyWindowStub.unregisterCallback,
|
||||||
});
|
"whats-new-menu-button"
|
||||||
it("should listen for panelhidden and remove the toolbar button", async () => {
|
);
|
||||||
getMessagesStub.returns([]);
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
|
||||||
|
|
||||||
assert.calledOnce(fakeElementById.addEventListener);
|
|
||||||
assert.calledWithExactly(
|
|
||||||
fakeElementById.addEventListener,
|
|
||||||
"popuphidden",
|
|
||||||
sinon.match.func,
|
|
||||||
{
|
|
||||||
once: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [, cb] = fakeElementById.addEventListener.firstCall.args;
|
|
||||||
|
|
||||||
assert.notCalled(everyWindowStub.unregisterCallback);
|
|
||||||
|
|
||||||
cb();
|
|
||||||
|
|
||||||
assert.calledOnce(everyWindowStub.unregisterCallback);
|
|
||||||
assert.calledWithExactly(
|
|
||||||
everyWindowStub.unregisterCallback,
|
|
||||||
"whats-new-menu-button"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe("#IMPRESSION", () => {
|
|
||||||
it("should dispatch a IMPRESSION for messages", async () => {
|
|
||||||
// means panel is triggered from the toolbar button
|
|
||||||
fakeElementById.hasAttribute.returns(true);
|
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
|
||||||
m => m.template === "whatsnew_panel_message"
|
|
||||||
);
|
|
||||||
getMessagesStub.returns(messages);
|
|
||||||
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
|
||||||
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
assert.calledOnce(fakeDispatch);
|
|
||||||
assert.propertyVal(
|
|
||||||
spy.firstCall.args[2],
|
|
||||||
"id",
|
|
||||||
messages
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.sort()
|
|
||||||
.join(",")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should dispatch a CLICK for clicking a message", async () => {
|
|
||||||
// means panel is triggered from the toolbar button
|
|
||||||
fakeElementById.hasAttribute.returns(true);
|
|
||||||
// Force to render the message
|
|
||||||
fakeElementById.querySelector.returns(null);
|
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
|
||||||
m => m.template === "whatsnew_panel_message"
|
|
||||||
);
|
|
||||||
getMessagesStub.returns([messages[0]]);
|
|
||||||
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
|
||||||
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
assert.calledOnce(fakeDispatch);
|
|
||||||
|
|
||||||
spy.resetHistory();
|
|
||||||
|
|
||||||
// Message click event listener cb
|
|
||||||
eventListeners.click();
|
|
||||||
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
assert.calledWithExactly(spy, fakeWindow, "CLICK", messages[0]);
|
|
||||||
});
|
|
||||||
it("should dispatch a IMPRESSION with toolbar_dropdown", async () => {
|
|
||||||
// means panel is triggered from the toolbar button
|
|
||||||
fakeElementById.hasAttribute.returns(true);
|
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
|
||||||
m => m.template === "whatsnew_panel_message"
|
|
||||||
);
|
|
||||||
getMessagesStub.resolves(messages);
|
|
||||||
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
|
|
||||||
const panelPingId = messages
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.sort()
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
|
||||||
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
assert.calledWithExactly(
|
|
||||||
spy,
|
|
||||||
fakeWindow,
|
|
||||||
"IMPRESSION",
|
|
||||||
{
|
|
||||||
id: panelPingId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
view: "toolbar_dropdown",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert.calledOnce(fakeDispatch);
|
|
||||||
const {
|
|
||||||
args: [dispatchPayload],
|
|
||||||
} = fakeDispatch.lastCall;
|
|
||||||
assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
|
|
||||||
assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
|
|
||||||
assert.propertyVal(
|
|
||||||
dispatchPayload.data.value,
|
|
||||||
"view",
|
|
||||||
"toolbar_dropdown"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should dispatch a IMPRESSION with application_menu", async () => {
|
|
||||||
// means panel is triggered as a subview in the application menu
|
|
||||||
fakeElementById.hasAttribute.returns(false);
|
|
||||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
|
||||||
m => m.template === "whatsnew_panel_message"
|
|
||||||
);
|
|
||||||
getMessagesStub.resolves(messages);
|
|
||||||
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
|
|
||||||
const panelPingId = messages
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.sort()
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
|
||||||
|
|
||||||
assert.calledOnce(spy);
|
|
||||||
assert.calledWithExactly(
|
|
||||||
spy,
|
|
||||||
fakeWindow,
|
|
||||||
"IMPRESSION",
|
|
||||||
{
|
|
||||||
id: panelPingId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
view: "application_menu",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert.calledOnce(fakeDispatch);
|
|
||||||
const {
|
|
||||||
args: [dispatchPayload],
|
|
||||||
} = fakeDispatch.lastCall;
|
|
||||||
assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
|
|
||||||
assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
|
|
||||||
assert.propertyVal(
|
|
||||||
dispatchPayload.data.value,
|
|
||||||
"view",
|
|
||||||
"application_menu"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
|
||||||
newtab-topsites-preview-button = Preview
|
newtab-topsites-preview-button = Preview
|
||||||
newtab-topsites-add-button = Add
|
newtab-topsites-add-button = Add
|
||||||
|
|
||||||
## Top Sites - Delete history confirmation dialog.
|
## Top Sites - Delete history confirmation dialog.
|
||||||
|
|
||||||
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
|
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
|
||||||
# "This action" refers to deleting a page from history.
|
# "This action" refers to deleting a page from history.
|
||||||
|
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
|
||||||
# Bookmark is a verb here.
|
# Bookmark is a verb here.
|
||||||
newtab-menu-bookmark = Bookmark
|
newtab-menu-bookmark = Bookmark
|
||||||
|
|
||||||
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
|
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
|
||||||
## it is a noun. As in, "Copy the link that belongs to this downloaded item".
|
## it is a noun. As in, "Copy the link that belongs to this downloaded item".
|
||||||
|
|
||||||
newtab-menu-copy-download-link = Copy Download Link
|
newtab-menu-copy-download-link = Copy Download Link
|
||||||
|
@ -117,7 +117,7 @@ newtab-label-recommended = Trending
|
||||||
newtab-label-saved = Saved to { -pocket-brand-name }
|
newtab-label-saved = Saved to { -pocket-brand-name }
|
||||||
newtab-label-download = Downloaded
|
newtab-label-download = Downloaded
|
||||||
|
|
||||||
## Section Menu: These strings are displayed in the section context menu and are
|
## Section Menu: These strings are displayed in the section context menu and are
|
||||||
## meant as a call to action for the given section.
|
## meant as a call to action for the given section.
|
||||||
|
|
||||||
newtab-section-menu-remove-section = Remove Section
|
newtab-section-menu-remove-section = Remove Section
|
||||||
|
@ -131,13 +131,6 @@ newtab-section-menu-move-up = Move Up
|
||||||
newtab-section-menu-move-down = Move Down
|
newtab-section-menu-move-down = Move Down
|
||||||
newtab-section-menu-privacy-notice = Privacy Notice
|
newtab-section-menu-privacy-notice = Privacy Notice
|
||||||
|
|
||||||
## Section aria-labels
|
|
||||||
|
|
||||||
newtab-section-collapse-section-label =
|
|
||||||
.aria-label = Collapse Section
|
|
||||||
newtab-section-expand-section-label =
|
|
||||||
.aria-label = Expand Section
|
|
||||||
|
|
||||||
## Section Headers.
|
## Section Headers.
|
||||||
|
|
||||||
newtab-section-header-topsites = Top Sites
|
newtab-section-header-topsites = Top Sites
|
||||||
|
|
Загрузка…
Ссылка в новой задаче