Fix Bug 1475354 - Create first-run template and content for Return To AMO experience (#4578)
This commit is contained in:
Родитель
678c6cc941
Коммит
f40d201cfe
|
@ -25,6 +25,7 @@ enableASRouterContent(store, asrouterContent);
|
||||||
|
|
||||||
ReactDOM.hydrate(<Provider store={store}>
|
ReactDOM.hydrate(<Provider store={store}>
|
||||||
<Base
|
<Base
|
||||||
|
isFirstrun={global.document.location.href === "about:welcome"}
|
||||||
isPrerendered={!!global.gActivityStreamPrerenderedState}
|
isPrerendered={!!global.gActivityStreamPrerenderedState}
|
||||||
locale={global.document.documentElement.lang}
|
locale={global.document.documentElement.lang}
|
||||||
strings={global.gActivityStreamStrings} />
|
strings={global.gActivityStreamStrings} />
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {LocalizationProvider} from "fluent-react";
|
||||||
import {OnboardingMessage} from "./templates/OnboardingMessage/OnboardingMessage";
|
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 {StartupOverlay} from "./templates/StartupOverlay/StartupOverlay";
|
import {StartupOverlay} from "./templates/StartupOverlay/StartupOverlay";
|
||||||
|
|
||||||
|
@ -30,8 +31,8 @@ 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}});
|
||||||
},
|
},
|
||||||
blockBundle(bundle) {
|
dismissBundle(bundle) {
|
||||||
ASRouterUtils.sendMessage({type: "BLOCK_BUNDLE", data: {bundle}});
|
ASRouterUtils.sendMessage({type: "DISMISS_BUNDLE", data: {bundle}});
|
||||||
},
|
},
|
||||||
executeAction(button_action) {
|
executeAction(button_action) {
|
||||||
ASRouterUtils.sendMessage({
|
ASRouterUtils.sendMessage({
|
||||||
|
@ -139,8 +140,12 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
return () => ASRouterUtils.dismissById(id);
|
return () => ASRouterUtils.dismissById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBundle(bundle) {
|
dismissBundle(bundle) {
|
||||||
return () => ASRouterUtils.blockBundle(bundle);
|
return () => ASRouterUtils.dismissBundle(bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerOnboarding() {
|
||||||
|
ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "showOnboarding"}}});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageFromParent({data: action}) {
|
onMessageFromParent({data: action}) {
|
||||||
|
@ -186,21 +191,14 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.document.location.href === "about:welcome") {
|
|
||||||
// Trigger the onboarding overlay slightly after the startup overlay is mounted,
|
|
||||||
// to ensure we don't flash the onboarding content before hand
|
|
||||||
ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "showOnboarding"}}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ASRouterUtils.removeListener(this.onMessageFromParent);
|
ASRouterUtils.removeListener(this.onMessageFromParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSnippets() {
|
renderSnippets() {
|
||||||
if (this.state.bundle.template === "onboarding" ||
|
if (this.state.bundle.template === "onboarding" ||
|
||||||
this.state.message.template === "fxa_overlay") {
|
this.state.message.template === "fxa_overlay" ||
|
||||||
|
this.state.message.template === "return_to_amo_overlay") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const SnippetComponent = SnippetsTemplates[this.state.message.template];
|
const SnippetComponent = SnippetsTemplates[this.state.message.template];
|
||||||
|
@ -234,23 +232,36 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||||
{...this.state.bundle}
|
{...this.state.bundle}
|
||||||
UISurface="NEWTAB_OVERLAY"
|
UISurface="NEWTAB_OVERLAY"
|
||||||
onAction={ASRouterUtils.executeAction}
|
onAction={ASRouterUtils.executeAction}
|
||||||
onDoneButton={this.clearBundle(this.state.bundle.bundle)}
|
onDoneButton={this.dismissBundle(this.state.bundle.bundle)}
|
||||||
sendUserActionTelemetry={this.sendUserActionTelemetry} />);
|
sendUserActionTelemetry={this.sendUserActionTelemetry} />);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFirstRunOverlay() {
|
renderFirstRunOverlay() {
|
||||||
if (this.state.message.template === "fxa_overlay") {
|
const {message} = this.state;
|
||||||
global.document.body.classList.add("welcome", "hide-main");
|
if (message.template === "fxa_overlay") {
|
||||||
|
global.document.body.classList.add("fxa");
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={global.document.documentElement.lang} messages={global.gActivityStreamStrings}>
|
<IntlProvider locale={global.document.documentElement.lang} messages={global.gActivityStreamStrings}>
|
||||||
<StartupOverlay
|
<StartupOverlay
|
||||||
onBlock={this.onBlockById(this.state.message.id)}
|
onReady={this.triggerOnboarding}
|
||||||
|
onBlock={this.onDismissById(message.id)}
|
||||||
dispatch={this.props.activityStreamStore.dispatch}
|
dispatch={this.props.activityStreamStore.dispatch}
|
||||||
store={this.props.activityStreamStore} />
|
store={this.props.activityStreamStore} />
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
} else if (message.template === "return_to_amo_overlay") {
|
||||||
|
global.document.body.classList.add("amo");
|
||||||
|
return (
|
||||||
|
<LocalizationProvider messages={generateMessages({"amo_html": message.content.text})}>
|
||||||
|
<ReturnToAMO
|
||||||
|
{...message}
|
||||||
|
onReady={this.triggerOnboarding}
|
||||||
|
onBlock={this.onDismissById(message.id)}
|
||||||
|
onAction={ASRouterUtils.executeAction} />
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ const RICH_TEXT_CONFIG = {
|
||||||
"success_text": "success_text",
|
"success_text": "success_text",
|
||||||
"error_text": "error_text",
|
"error_text": "error_text",
|
||||||
"scene2_text": "scene2_text",
|
"scene2_text": "scene2_text",
|
||||||
|
"amo_html": "amo_html",
|
||||||
"privacy_html": "scene2_privacy_html",
|
"privacy_html": "scene2_privacy_html",
|
||||||
"disclaimer_html": "scene2_disclaimer_html",
|
"disclaimer_html": "scene2_disclaimer_html",
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ class OnboardingCard extends React.PureComponent {
|
||||||
id: props.UISurface,
|
id: props.UISurface,
|
||||||
};
|
};
|
||||||
props.sendUserActionTelemetry(ping);
|
props.sendUserActionTelemetry(ping);
|
||||||
props.onAction(props.content.button_action);
|
props.onAction(props.content.primary_button.action);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -29,7 +29,7 @@ class OnboardingCard extends React.PureComponent {
|
||||||
<p> {content.text} </p>
|
<p> {content.text} </p>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<button tabIndex="1" className="button onboardingButton" onClick={this.onClick}> {content.button_label} </button>
|
<button tabIndex="1" className="button onboardingButton" onClick={this.onClick}> {content.primary_button.label} </button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,6 +34,10 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"string_id": {
|
"string_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "An optional argument to pass to the localization module"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["string_id"],
|
"required": ["string_id"],
|
||||||
|
@ -50,11 +54,14 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"button_label": {
|
"primary_button": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The label of the onboarding card's action button"
|
"description": "The label of the onboarding messages' action button"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -64,12 +71,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["string_id"],
|
"required": ["string_id"],
|
||||||
"description": "Id of localized string for onboarding card button"
|
"description": "Id of localized string for onboarding messages' button"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Id of localized string or message override."
|
"description": "Id of localized string or message override."
|
||||||
},
|
},
|
||||||
"button_action": {
|
"action": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -86,8 +93,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["title", "text", "icon", "button_label", "button_action"]
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"secondary_buttons": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The label of the onboarding messages' (optional) secondary action button"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"string_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["string_id"],
|
||||||
|
"description": "Id of localized string for onboarding messages' button"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Id of localized string or message override."
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Action dispatched by the button."
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Additional parameters for button action, for example which link the button should open."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["title", "text", "icon", "primary_button"]
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React from "react";
|
||||||
|
import {RichText} from "../../components/RichText/RichText";
|
||||||
|
|
||||||
|
export class ReturnToAMO extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onClickAddExtension = this.onClickAddExtension.bind(this);
|
||||||
|
this.onBlockButton = this.onBlockButton.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.onReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickAddExtension() {
|
||||||
|
this.props.onAction(this.props.content.primary_button.action);
|
||||||
|
this.props.onBlock();
|
||||||
|
document.body.classList.remove("welcome", "hide-main", "amo");
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlockButton() {
|
||||||
|
this.props.onBlock();
|
||||||
|
document.body.classList.remove("welcome", "hide-main", "amo");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderText() {
|
||||||
|
const customElement = <img src={this.props.content.addon_icon} width="20px" height="20px" />;
|
||||||
|
return (<RichText
|
||||||
|
customElements={{icon: customElement}}
|
||||||
|
amo_html={this.props.content.text}
|
||||||
|
localization_id="amo_html" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {content} = this.props;
|
||||||
|
return (
|
||||||
|
<div className="ReturnToAMOOverlay" >
|
||||||
|
<div>
|
||||||
|
<h2> {content.header} </h2>
|
||||||
|
<div className="ReturnToAMOContainer" >
|
||||||
|
<div className="ReturnToAMOAddonContents">
|
||||||
|
<p> {content.title} </p>
|
||||||
|
<div className="ReturnToAMOText">
|
||||||
|
<span> {this.renderText()} </span>
|
||||||
|
</div>
|
||||||
|
<button onClick={this.onClickAddExtension} className="puffy blue ReturnToAMOAddExtension"> <span className="icon icon-add" /> {content.primary_button.label} </button>
|
||||||
|
</div>
|
||||||
|
<div className="ReturnToAMOIcon" />
|
||||||
|
</div>
|
||||||
|
<button onClick={this.onBlockButton} className="default grey ReturnToAMOGetStarted"> {content.secondary_button.label} </button>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
.ReturnToAMOOverlay,
|
||||||
|
.amo + body.hide-main { // sass-lint:disable-line no-qualifying-elements
|
||||||
|
background: $grey-10;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 2100;
|
||||||
|
|
||||||
|
.ReturnToAMOText {
|
||||||
|
color: $grey-90-80;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 23px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-inline-start: 6px;
|
||||||
|
margin-inline-end: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: $grey-60-60;
|
||||||
|
font-weight: 100;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 48px;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $grey-90-50;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.puffy {
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
border: 0;
|
||||||
|
color: $white;
|
||||||
|
background-color: $blue-60;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: $blue-70;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $blue-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey {
|
||||||
|
border: 0;
|
||||||
|
background-color: $grey-90-10;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: $grey-90-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $grey-90-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReturnToAMOGetStarted {
|
||||||
|
margin-top: 40px;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
&:dir(rtl) {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReturnToAMOAddExtension {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReturnToAMOContainer {
|
||||||
|
width: 835px;
|
||||||
|
background: $white;
|
||||||
|
box-shadow: 0 1px 15px 0 $black-30;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
padding: 38px 96px 28px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReturnToAMOAddonContents {
|
||||||
|
width: 400px;
|
||||||
|
margin-top: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReturnToAMOIcon {
|
||||||
|
width: 292px;
|
||||||
|
height: 254px;
|
||||||
|
background-size: 292px 254px;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url('resource://activity-stream/data/content/assets/gift-extension.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-add {
|
||||||
|
fill: $white;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,12 +49,13 @@ export class _StartupOverlay extends React.PureComponent {
|
||||||
// to trigger the animation.
|
// to trigger the animation.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({show: true});
|
this.setState({show: true});
|
||||||
|
this.props.onReady();
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOverlay() {
|
removeOverlay() {
|
||||||
window.removeEventListener("visibilitychange", this.removeOverlay);
|
window.removeEventListener("visibilitychange", this.removeOverlay);
|
||||||
document.body.classList.remove("hide-main");
|
document.body.classList.remove("hide-main", "fxa");
|
||||||
this.setState({show: false});
|
this.setState({show: false});
|
||||||
this.props.onBlock();
|
this.props.onBlock();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.background,
|
.background,
|
||||||
body.hide-main { // sass-lint:disable-line no-qualifying-elements
|
.fxa + body.hide-main { // sass-lint:disable-line no-qualifying-elements
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -12,6 +12,8 @@ export class ASRouterAdmin extends React.PureComponent {
|
||||||
this.findOtherBundledMessagesOfSameTemplate = this.findOtherBundledMessagesOfSameTemplate.bind(this);
|
this.findOtherBundledMessagesOfSameTemplate = this.findOtherBundledMessagesOfSameTemplate.bind(this);
|
||||||
this.handleExpressionEval = this.handleExpressionEval.bind(this);
|
this.handleExpressionEval = this.handleExpressionEval.bind(this);
|
||||||
this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(this);
|
this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(this);
|
||||||
|
this.onChangeAttributionParameters = this.onChangeAttributionParameters.bind(this);
|
||||||
|
this.setAttribution = this.setAttribution.bind(this);
|
||||||
this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this);
|
this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this);
|
||||||
this.onPasteTargetingParams = this.onPasteTargetingParams.bind(this);
|
this.onPasteTargetingParams = this.onPasteTargetingParams.bind(this);
|
||||||
this.onNewTargetingParams = this.onNewTargetingParams.bind(this);
|
this.onNewTargetingParams = this.onNewTargetingParams.bind(this);
|
||||||
|
@ -22,6 +24,11 @@ export class ASRouterAdmin extends React.PureComponent {
|
||||||
newStringTargetingParameters: null,
|
newStringTargetingParameters: null,
|
||||||
copiedToClipboard: false,
|
copiedToClipboard: false,
|
||||||
pasteFromClipboard: false,
|
pasteFromClipboard: false,
|
||||||
|
attributionParameters: {
|
||||||
|
source: "addons.mozilla.org",
|
||||||
|
campaign: "non-fx-button",
|
||||||
|
content: "iridium@particlecore.github.io",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,6 +359,46 @@ export class ASRouterAdmin extends React.PureComponent {
|
||||||
</tbody></table>);
|
</tbody></table>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeAttributionParameters(event) {
|
||||||
|
const {name, value} = event.target;
|
||||||
|
|
||||||
|
this.setState(({attributionParameters}) => {
|
||||||
|
const updatedParameters = {...attributionParameters};
|
||||||
|
updatedParameters[name] = value;
|
||||||
|
|
||||||
|
return {attributionParameters: updatedParameters};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribution(e) {
|
||||||
|
ASRouterUtils.sendMessage({type: "FORCE_ATTRIBUTION", data: this.state.attributionParameters});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAttributionParamers() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2> Attribution Parameters </h2>
|
||||||
|
<p> This forces the browser to set some attribution parameters, useful for testing the Return To AMO feature. Clicking on 'Force Attribution', with the default values in each field, will demo the Return To AMO flow with the addon called 'Iridium for Youtube'. If you wish to try different attribution parameters, enter them in the text boxes. If you wish to try a different addon with the Return To AMO flow, make sure the 'content' text box has the addon GUID, then click 'Force Attribution'.</p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><b> Source </b></td>
|
||||||
|
<td> <input type="text" name="source" placeholder="addons.mozilla.org" value={this.state.attributionParameters.source} onChange={this.onChangeAttributionParameters} /> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b> Campaign </b></td>
|
||||||
|
<td> <input type="text" name="campaign" placeholder="non-fx-button" value={this.state.attributionParameters.campaign} onChange={this.onChangeAttributionParameters} /> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b> Content </b></td>
|
||||||
|
<td> <input type="text" name="content" placeholder="iridium@particlecore.github.io" value={this.state.attributionParameters.content} onChange={this.onChangeAttributionParameters} /> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> <button className="ASRouterButton primary button" onClick={this.setAttribution} > Force Attribution </button> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (<div className="asrouter-admin outer-wrapper">
|
return (<div className="asrouter-admin outer-wrapper">
|
||||||
<h1>AS Router Admin</h1>
|
<h1>AS Router Admin</h1>
|
||||||
|
@ -372,6 +419,7 @@ export class ASRouterAdmin extends React.PureComponent {
|
||||||
{this.renderMessages()}
|
{this.renderMessages()}
|
||||||
{this.renderPasteModal()}
|
{this.renderPasteModal()}
|
||||||
{this.renderTargetingParameters()}
|
{this.renderTargetingParameters()}
|
||||||
|
{this.renderAttributionParamers()}
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ export class _Base extends React.PureComponent {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const {locale} = this.props;
|
const {locale} = this.props;
|
||||||
addLocaleDataForReactIntl(locale);
|
addLocaleDataForReactIntl(locale);
|
||||||
|
if (this.props.isFirstrun) {
|
||||||
|
global.document.body.classList.add("welcome", "hide-main");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
|
@ -148,6 +148,7 @@ input {
|
||||||
@import '../asrouter/components/Button/Button';
|
@import '../asrouter/components/Button/Button';
|
||||||
@import '../asrouter/components/SnippetBase/SnippetBase';
|
@import '../asrouter/components/SnippetBase/SnippetBase';
|
||||||
@import '../asrouter/components/ModalOverlay/ModalOverlay';
|
@import '../asrouter/components/ModalOverlay/ModalOverlay';
|
||||||
|
@import '../asrouter/templates/ReturnToAMO/ReturnToAMO';
|
||||||
@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
|
@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
|
||||||
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
|
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
|
||||||
@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';
|
@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';
|
||||||
|
|
|
@ -29,6 +29,7 @@ $grey-10-95: rgba($grey-10, 0.95);
|
||||||
$grey-20-60: rgba($grey-20, 0.6);
|
$grey-20-60: rgba($grey-20, 0.6);
|
||||||
$grey-20-80: rgba($grey-20, 0.8);
|
$grey-20-80: rgba($grey-20, 0.8);
|
||||||
$grey-30-60: rgba($grey-30, 0.6);
|
$grey-30-60: rgba($grey-30, 0.6);
|
||||||
|
$grey-60-60: rgba($grey-60, 0.6);
|
||||||
$grey-60-70: rgba($grey-60, 0.7);
|
$grey-60-70: rgba($grey-60, 0.7);
|
||||||
$grey-80-95: rgba($grey-80, 0.95);
|
$grey-80-95: rgba($grey-80, 0.95);
|
||||||
$grey-90-10: rgba($grey-90, 0.1);
|
$grey-90-10: rgba($grey-90, 0.1);
|
||||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
После Ширина: | Высота: | Размер: 53 KiB |
|
@ -26,6 +26,7 @@ ChromeUtils.defineModuleGetter(this, "QueryCache",
|
||||||
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
||||||
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
|
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
|
||||||
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
|
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
|
||||||
|
ChromeUtils.import("resource:///modules/AttributionCode.jsm");
|
||||||
|
|
||||||
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
||||||
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
||||||
|
@ -43,6 +44,8 @@ const MAX_MESSAGE_LIFETIME_CAP = 100;
|
||||||
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider, SnippetsTestMessageProvider};
|
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider, SnippetsTestMessageProvider};
|
||||||
const STARTPAGE_VERSION = "6";
|
const STARTPAGE_VERSION = "6";
|
||||||
|
|
||||||
|
const ADDONS_API_URL = "https://services.addons.mozilla.org/api/v3/addons/addon";
|
||||||
|
|
||||||
const MessageLoaderUtils = {
|
const MessageLoaderUtils = {
|
||||||
STARTPAGE_VERSION,
|
STARTPAGE_VERSION,
|
||||||
REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
|
REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
|
||||||
|
@ -796,6 +799,25 @@ class _ASRouter {
|
||||||
return impressions;
|
return impressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fetchAddonInfo() {
|
||||||
|
let data = {};
|
||||||
|
const {content} = await AttributionCode.getAttrDataAsync();
|
||||||
|
if (!content) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ADDONS_API_URL}/${content}`);
|
||||||
|
if (response.status !== 204 && response.ok) {
|
||||||
|
const json = await response.json();
|
||||||
|
data.url = json.current_version.files[0].url;
|
||||||
|
data.iconURL = json.icon_url;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Cu.reportError("Failed to get the latest add-on version for Return to AMO");
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async sendNextMessage(target, trigger) {
|
async sendNextMessage(target, trigger) {
|
||||||
const msgs = this._getUnblockedMessages();
|
const msgs = this._getUnblockedMessages();
|
||||||
let message = null;
|
let message = null;
|
||||||
|
@ -807,6 +829,19 @@ class _ASRouter {
|
||||||
message = await this._findMessage(msgs, trigger);
|
message = await this._findMessage(msgs, trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need some addon info if we are showing return to amo overlay, so fetch
|
||||||
|
// that, and update the message accordingly
|
||||||
|
if (message && message.template === "return_to_amo_overlay") {
|
||||||
|
const {url, iconURL} = await this._fetchAddonInfo();
|
||||||
|
|
||||||
|
// If we failed to get this info, we do not want to show this message
|
||||||
|
if (!url || !iconURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.content.addon_icon = iconURL;
|
||||||
|
message.content.primary_button.action.data.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
if (previewMsgs.length) {
|
if (previewMsgs.length) {
|
||||||
// We don't want to cache preview messages, remove them after we selected the message to show
|
// We don't want to cache preview messages, remove them after we selected the message to show
|
||||||
await this.setState(state => ({
|
await this.setState(state => ({
|
||||||
|
@ -921,6 +956,34 @@ class _ASRouter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* forceAttribution - this function should only be called from within about:newtab#asrouter.
|
||||||
|
* It forces the browser attribution to be set to something specified in asrouter admin
|
||||||
|
* tools, and reloads the providers in order to get messages that are dependant on this
|
||||||
|
* attribution data (see Return to AMO flow in bug 1475354 for example). Note - only works with OSX
|
||||||
|
* @param {data} Object an object containing the attribtion data that came from asrouter admin page
|
||||||
|
*/
|
||||||
|
async forceAttribution(data) {
|
||||||
|
// Extract the parameters from data that will make up the referrer url
|
||||||
|
const {source, campaign, content} = data;
|
||||||
|
let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path;
|
||||||
|
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"]
|
||||||
|
.getService(Ci.nsIMacAttributionService);
|
||||||
|
|
||||||
|
let referrer = `https://www.mozilla.org/anything/?utm_campaign=${campaign}&utm_source=${source}&utm_content=${encodeURIComponent(content)}`;
|
||||||
|
|
||||||
|
// This sets the Attribution to be the referrer
|
||||||
|
attributionSvc.setReferrerUrl(appPath, referrer, true);
|
||||||
|
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
|
||||||
|
|
||||||
|
// Clear and refresh Attribution, and then fetch the messages again to update
|
||||||
|
AttributionCode._clearCache();
|
||||||
|
AttributionCode.getAttrDataAsync();
|
||||||
|
this._updateMessageProviders();
|
||||||
|
await this.loadMessagesFromAllProviders();
|
||||||
|
}
|
||||||
|
|
||||||
async handleUserAction({data: action, target}) {
|
async handleUserAction({data: action, target}) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ra.OPEN_PRIVATE_BROWSER_WINDOW:
|
case ra.OPEN_PRIVATE_BROWSER_WINDOW:
|
||||||
|
@ -994,6 +1057,9 @@ class _ASRouter {
|
||||||
await this.blockProviderById(action.data.id);
|
await this.blockProviderById(action.data.id);
|
||||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: action.data.id}});
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_PROVIDER", 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, {type: "CLEAR_BUNDLE"});
|
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
||||||
|
@ -1062,6 +1128,9 @@ class _ASRouter {
|
||||||
break;
|
break;
|
||||||
case "EVALUATE_JEXL_EXPRESSION":
|
case "EVALUATE_JEXL_EXPRESSION":
|
||||||
this.evaluateExpression(target, action.data);
|
this.evaluateExpression(target, action.data);
|
||||||
|
break;
|
||||||
|
case "FORCE_ATTRIBUTION":
|
||||||
|
this.forceAttribution(action.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,21 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
ChromeUtils.import("resource://gre/modules/Localization.jsm");
|
ChromeUtils.import("resource://gre/modules/Localization.jsm");
|
||||||
ChromeUtils.import("resource://gre/modules/FxAccountsConfig.jsm");
|
ChromeUtils.import("resource://gre/modules/FxAccountsConfig.jsm");
|
||||||
|
ChromeUtils.import("resource:///modules/AttributionCode.jsm");
|
||||||
|
ChromeUtils.import("resource://gre/modules/addons/AddonRepository.jsm");
|
||||||
|
|
||||||
|
async function getAddonName() {
|
||||||
|
try {
|
||||||
|
const {content} = await AttributionCode.getAttrDataAsync();
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const addons = await AddonRepository.getAddonsByIDs([content]);
|
||||||
|
return addons[0].name;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const L10N = new Localization([
|
const L10N = new Localization([
|
||||||
"branding/brand.ftl",
|
"branding/brand.ftl",
|
||||||
|
@ -21,8 +36,10 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
title: {string_id: "onboarding-private-browsing-title"},
|
title: {string_id: "onboarding-private-browsing-title"},
|
||||||
text: {string_id: "onboarding-private-browsing-text"},
|
text: {string_id: "onboarding-private-browsing-text"},
|
||||||
icon: "privatebrowsing",
|
icon: "privatebrowsing",
|
||||||
button_label: {string_id: "onboarding-button-label-try-now"},
|
primary_button: {
|
||||||
button_action: {type: "OPEN_PRIVATE_BROWSER_WINDOW"},
|
label: {string_id: "onboarding-button-label-try-now"},
|
||||||
|
action: {type: "OPEN_PRIVATE_BROWSER_WINDOW"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
trigger: {id: "showOnboarding"},
|
trigger: {id: "showOnboarding"},
|
||||||
},
|
},
|
||||||
|
@ -35,12 +52,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
title: {string_id: "onboarding-screenshots-title"},
|
title: {string_id: "onboarding-screenshots-title"},
|
||||||
text: {string_id: "onboarding-screenshots-text"},
|
text: {string_id: "onboarding-screenshots-text"},
|
||||||
icon: "screenshots",
|
icon: "screenshots",
|
||||||
button_label: {string_id: "onboarding-button-label-try-now"},
|
primary_button: {
|
||||||
button_action: {
|
label: {string_id: "onboarding-button-label-try-now"},
|
||||||
|
action: {
|
||||||
type: "OPEN_URL",
|
type: "OPEN_URL",
|
||||||
data: {args: "https://screenshots.firefox.com/#tour", where: "tabshifted"},
|
data: {args: "https://screenshots.firefox.com/#tour", where: "tabshifted"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
trigger: {id: "showOnboarding"},
|
trigger: {id: "showOnboarding"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -52,12 +71,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
title: {string_id: "onboarding-addons-title"},
|
title: {string_id: "onboarding-addons-title"},
|
||||||
text: {string_id: "onboarding-addons-text"},
|
text: {string_id: "onboarding-addons-text"},
|
||||||
icon: "addons",
|
icon: "addons",
|
||||||
button_label: {string_id: "onboarding-button-label-try-now"},
|
primary_button: {
|
||||||
button_action: {
|
label: {string_id: "onboarding-button-label-try-now"},
|
||||||
|
action: {
|
||||||
type: "OPEN_ABOUT_PAGE",
|
type: "OPEN_ABOUT_PAGE",
|
||||||
data: {args: "addons"},
|
data: {args: "addons"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
targeting: "attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
targeting: "attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
||||||
trigger: {id: "showOnboarding"},
|
trigger: {id: "showOnboarding"},
|
||||||
},
|
},
|
||||||
|
@ -70,12 +91,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
title: {string_id: "onboarding-ghostery-title"},
|
title: {string_id: "onboarding-ghostery-title"},
|
||||||
text: {string_id: "onboarding-ghostery-text"},
|
text: {string_id: "onboarding-ghostery-text"},
|
||||||
icon: "gift",
|
icon: "gift",
|
||||||
button_label: {string_id: "onboarding-button-label-try-now"},
|
primary_button: {
|
||||||
button_action: {
|
label: {string_id: "onboarding-button-label-try-now"},
|
||||||
|
action: {
|
||||||
type: "OPEN_URL",
|
type: "OPEN_URL",
|
||||||
data: {args: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/", where: "tabshifted"},
|
data: {args: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/", where: "tabshifted"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
targeting: "providerCohorts.onboarding == 'ghostery'",
|
targeting: "providerCohorts.onboarding == 'ghostery'",
|
||||||
trigger: {id: "showOnboarding"},
|
trigger: {id: "showOnboarding"},
|
||||||
},
|
},
|
||||||
|
@ -88,12 +111,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
title: {string_id: "onboarding-fxa-title"},
|
title: {string_id: "onboarding-fxa-title"},
|
||||||
text: {string_id: "onboarding-fxa-text"},
|
text: {string_id: "onboarding-fxa-text"},
|
||||||
icon: "sync",
|
icon: "sync",
|
||||||
button_label: {string_id: "onboarding-button-label-get-started"},
|
primary_button: {
|
||||||
button_action: {
|
label: {string_id: "onboarding-button-label-get-started"},
|
||||||
|
action: {
|
||||||
type: "OPEN_URL",
|
type: "OPEN_URL",
|
||||||
data: {args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"), where: "tabshifted"},
|
data: {args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"), where: "tabshifted"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
targeting: "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
targeting: "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||||
trigger: {id: "showOnboarding"},
|
trigger: {id: "showOnboarding"},
|
||||||
},
|
},
|
||||||
|
@ -103,6 +128,29 @@ const ONBOARDING_MESSAGES = async () => ([
|
||||||
targeting: "attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
targeting: "attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
||||||
trigger: {id: "firstRun"},
|
trigger: {id: "firstRun"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "RETURN_TO_AMO_1",
|
||||||
|
template: "return_to_amo_overlay",
|
||||||
|
content: {
|
||||||
|
header: {string_id: "onboarding-welcome-header"},
|
||||||
|
title: {string_id: "return-to-amo-sub-header"},
|
||||||
|
addon_icon: null, // to be dynamically filled in, in ASRouter.jsm
|
||||||
|
icon: "gift-extension",
|
||||||
|
text: {string_id: "return-to-amo-addon-header", args: {"addon-name": await getAddonName()}},
|
||||||
|
primary_button: {
|
||||||
|
label: {string_id: "return-to-amo-extension-button"},
|
||||||
|
action: {
|
||||||
|
type: "INSTALL_ADDON_FROM_URL",
|
||||||
|
data: {url: null}, // to be dynamically filled in, in ASRouter.jsm
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondary_button: {
|
||||||
|
label: {string_id: "return-to-amo-get-started-button"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targeting: "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||||
|
trigger: {id: "firstRun"},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const OnboardingMessageProvider = {
|
const OnboardingMessageProvider = {
|
||||||
|
@ -125,20 +173,32 @@ const OnboardingMessageProvider = {
|
||||||
async translateMessages(messages) {
|
async translateMessages(messages) {
|
||||||
let translatedMessages = [];
|
let translatedMessages = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
let translatedMessage = msg;
|
let translatedMessage = {...msg};
|
||||||
|
|
||||||
// If the message has no content, do not attempt to translate it
|
// If the message has no content, do not attempt to translate it
|
||||||
if (!msg.content) {
|
if (!translatedMessage.content) {
|
||||||
translatedMessages.push(msg);
|
translatedMessages.push(translatedMessage);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const [button_string, title_string, text_string] = await L10N.formatMessages([
|
|
||||||
{id: msg.content.button_label.string_id},
|
const [primary_button_string, title_string, text_string] = await L10N.formatMessages([
|
||||||
|
{id: msg.content.primary_button.label.string_id},
|
||||||
{id: msg.content.title.string_id},
|
{id: msg.content.title.string_id},
|
||||||
{id: msg.content.text.string_id},
|
{id: msg.content.text.string_id, args: msg.content.text.args},
|
||||||
]);
|
]);
|
||||||
translatedMessage.content.button_label = button_string.value;
|
translatedMessage.content.primary_button.label = primary_button_string.value;
|
||||||
translatedMessage.content.title = title_string.value;
|
translatedMessage.content.title = title_string.value;
|
||||||
translatedMessage.content.text = text_string.value;
|
translatedMessage.content.text = text_string.value;
|
||||||
|
|
||||||
|
// Translate any secondary buttons separately
|
||||||
|
if (msg.content.secondary_button) {
|
||||||
|
const [secondary_button_string] = await L10N.formatMessages([{id: msg.content.secondary_button.label.string_id}]);
|
||||||
|
translatedMessage.content.secondary_button.label = secondary_button_string.value;
|
||||||
|
}
|
||||||
|
if (msg.content.header) {
|
||||||
|
const [header_string] = await L10N.formatMessages([{id: msg.content.header.string_id}]);
|
||||||
|
translatedMessage.content.header = header_string.value;
|
||||||
|
}
|
||||||
translatedMessages.push(translatedMessage);
|
translatedMessages.push(translatedMessage);
|
||||||
}
|
}
|
||||||
return translatedMessages;
|
return translatedMessages;
|
||||||
|
|
|
@ -36,6 +36,7 @@ function fakeExecuteUserAction(action) {
|
||||||
|
|
||||||
describe("ASRouter", () => {
|
describe("ASRouter", () => {
|
||||||
let Router;
|
let Router;
|
||||||
|
let globals;
|
||||||
let channel;
|
let channel;
|
||||||
let sandbox;
|
let sandbox;
|
||||||
let messageBlockList;
|
let messageBlockList;
|
||||||
|
@ -47,6 +48,7 @@ describe("ASRouter", () => {
|
||||||
let clock;
|
let clock;
|
||||||
let getStringPrefStub;
|
let getStringPrefStub;
|
||||||
let dispatchStub;
|
let dispatchStub;
|
||||||
|
let fakeAttributionCode;
|
||||||
|
|
||||||
function createFakeStorage() {
|
function createFakeStorage() {
|
||||||
const getStub = sandbox.stub();
|
const getStub = sandbox.stub();
|
||||||
|
@ -75,6 +77,7 @@ describe("ASRouter", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
globals = new GlobalOverrider();
|
||||||
messageBlockList = [];
|
messageBlockList = [];
|
||||||
providerBlockList = [];
|
providerBlockList = [];
|
||||||
messageImpressions = {};
|
messageImpressions = {};
|
||||||
|
@ -92,11 +95,18 @@ describe("ASRouter", () => {
|
||||||
.withArgs("http://fake.com/endpoint")
|
.withArgs("http://fake.com/endpoint")
|
||||||
.resolves({ok: true, status: 200, json: () => Promise.resolve({messages: FAKE_REMOTE_MESSAGES})});
|
.resolves({ok: true, status: 200, json: () => Promise.resolve({messages: FAKE_REMOTE_MESSAGES})});
|
||||||
getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
|
getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
|
||||||
|
|
||||||
|
fakeAttributionCode = {
|
||||||
|
_clearCache: () => sinon.stub(),
|
||||||
|
getAttrDataAsync: () => (Promise.resolve({content: "addonID"})),
|
||||||
|
};
|
||||||
|
globals.set("AttributionCode", fakeAttributionCode);
|
||||||
await createRouterAndInit();
|
await createRouterAndInit();
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
ASRouterPreferences.uninit();
|
ASRouterPreferences.uninit();
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
|
globals.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(".state", () => {
|
describe(".state", () => {
|
||||||
|
@ -606,17 +616,13 @@ describe("ASRouter", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#onMessage: BLOCK_BUNDLE", () => {
|
describe("#onMessage: DISMISS_BUNDLE", () => {
|
||||||
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
|
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
|
||||||
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
|
|
||||||
await Router.setState({lastMessageId: "foo"});
|
await Router.setState({lastMessageId: "foo"});
|
||||||
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
|
const msg = fakeAsyncMessage({type: "DISMISS_BUNDLE", data: {bundle: FAKE_BUNDLE}});
|
||||||
await Router.onMessage(msg);
|
await Router.onMessage(msg);
|
||||||
|
|
||||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
|
||||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
|
||||||
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
||||||
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -791,6 +797,58 @@ describe("ASRouter", () => {
|
||||||
it("should have previousSessionEnd in the message context", () => {
|
it("should have previousSessionEnd in the message context", () => {
|
||||||
assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
|
assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
|
||||||
});
|
});
|
||||||
|
it("should update parameters of the message if the template is return to amo", async () => {
|
||||||
|
let message = [
|
||||||
|
{id: "foo1", template: "return_to_amo_overlay", trigger: {id: "foo"}, content: {addon_icon: null, primary_button: {action: {data: {url: null}}}, title: "Foo1", body: "Foo123-1"}},
|
||||||
|
];
|
||||||
|
await Router.setState({messages: message});
|
||||||
|
sandbox.stub(Router, "_fetchAddonInfo").returns({url: "foo.com", iconURL: "url/foo.ico"});
|
||||||
|
await Router.sendNextMessage({sendAsyncMessage: sandbox.stub()}, {id: "foo"});
|
||||||
|
const msg = await Router._findMessage(message, {id: "foo"});
|
||||||
|
assert.calledOnce(Router._fetchAddonInfo);
|
||||||
|
assert.equal(msg.content.addon_icon, message[0].content.addon_icon);
|
||||||
|
assert.equal(msg.content.primary_button.action.data.url, message[0].content.primary_button.action.data.url);
|
||||||
|
});
|
||||||
|
it("should return early and not send a message if we failed to get addon info for return to amo template", async () => {
|
||||||
|
let message = [
|
||||||
|
{id: "foo1", template: "return_to_amo_overlay", trigger: {id: "foo"}, content: {addon_icon: null, primary_button: {action: {data: {url: null}}}, title: "Foo1", body: "Foo123-1"}},
|
||||||
|
];
|
||||||
|
await Router.setState({messages: message});
|
||||||
|
sandbox.stub(Router, "_fetchAddonInfo").returns({});
|
||||||
|
sandbox.spy(Router, "_sendMessageToTarget");
|
||||||
|
await Router.sendNextMessage({sendAsyncMessage: sandbox.stub()}, {id: "foo"});
|
||||||
|
assert.calledOnce(Router._fetchAddonInfo);
|
||||||
|
assert.notCalled(Router._sendMessageToTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#_fetchAddonInfo", () => {
|
||||||
|
it("should fetch the addon url and the icon for the addon", async () => {
|
||||||
|
fetchStub
|
||||||
|
.withArgs("https://services.addons.mozilla.org/api/v3/addons/addon/addonID")
|
||||||
|
.resolves({ok: true, status: 200, json: () => Promise.resolve({icon_url: "url/foo.ico", current_version: {files: [{url: "foo.com"}]}})});
|
||||||
|
const {url, iconURL} = await Router._fetchAddonInfo();
|
||||||
|
assert.equal(url, "foo.com");
|
||||||
|
assert.equal(iconURL, "url/foo.ico");
|
||||||
|
});
|
||||||
|
it("should return empty object if AttributionCode doesn't return anything", async () => {
|
||||||
|
fakeAttributionCode.getAttrDataAsync = () => (Promise.resolve({content: null}));
|
||||||
|
fetchStub
|
||||||
|
.withArgs("https://services.addons.mozilla.org/api/v3/addons/addon/addonID")
|
||||||
|
.resolves({ok: true, status: 200, json: () => Promise.resolve({icon_url: "url/foo.ico", current_version: {files: [{url: "foo.com"}]}})});
|
||||||
|
const data = await Router._fetchAddonInfo();
|
||||||
|
assert.deepEqual(data, {});
|
||||||
|
});
|
||||||
|
it("should throw if we failed to get the addon version", async () => {
|
||||||
|
fetchStub
|
||||||
|
.withArgs("https://services.addons.mozilla.org/api/v3/addons/addon/addonID")
|
||||||
|
.rejects();
|
||||||
|
sandbox.stub(Cu, "reportError");
|
||||||
|
const data = await Router._fetchAddonInfo();
|
||||||
|
|
||||||
|
assert.calledOnce(Cu.reportError);
|
||||||
|
assert.deepEqual(data, {});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#onMessage: OVERRIDE_MESSAGE", () => {
|
describe("#onMessage: OVERRIDE_MESSAGE", () => {
|
||||||
|
@ -860,9 +918,7 @@ describe("ASRouter", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
|
describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
|
||||||
let globals;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
globals = new GlobalOverrider();
|
|
||||||
globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
|
globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
|
||||||
});
|
});
|
||||||
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
|
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
|
||||||
|
@ -994,6 +1050,41 @@ describe("ASRouter", () => {
|
||||||
assert.calledWithExactly(Router.evaluateExpression, msg.target, msg.data.data);
|
assert.calledWithExactly(Router.evaluateExpression, msg.target, msg.data.data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("#onMessage: FORCE_ATTRIBUTION", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.Cc["@mozilla.org/mac-attribution;1"] = {
|
||||||
|
getService: () => ({setReferrerUrl: sinon.spy()}),
|
||||||
|
};
|
||||||
|
global.Cc["@mozilla.org/process/environment;1"] = {
|
||||||
|
getService: () => ({set: sandbox.stub()}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
globals.restore();
|
||||||
|
});
|
||||||
|
it("should call forceAttribution", async () => {
|
||||||
|
const msg = fakeAsyncMessage({type: "FORCE_ATTRIBUTION", data: {foo: true}});
|
||||||
|
sandbox.stub(Router, "forceAttribution");
|
||||||
|
|
||||||
|
await Router.onMessage(msg);
|
||||||
|
|
||||||
|
assert.calledOnce(Router.forceAttribution);
|
||||||
|
assert.calledWithExactly(Router.forceAttribution, msg.data.data);
|
||||||
|
});
|
||||||
|
it("should force attribution and update providers", async () => {
|
||||||
|
sandbox.stub(Router, "_updateMessageProviders");
|
||||||
|
sandbox.stub(Router, "loadMessagesFromAllProviders");
|
||||||
|
sandbox.stub(fakeAttributionCode, "_clearCache");
|
||||||
|
sandbox.stub(fakeAttributionCode, "getAttrDataAsync");
|
||||||
|
const msg = fakeAsyncMessage({type: "FORCE_ATTRIBUTION", data: {foo: true}});
|
||||||
|
await Router.onMessage(msg);
|
||||||
|
|
||||||
|
assert.calledOnce(fakeAttributionCode._clearCache);
|
||||||
|
assert.calledOnce(fakeAttributionCode.getAttrDataAsync);
|
||||||
|
assert.calledOnce(Router._updateMessageProviders);
|
||||||
|
assert.calledOnce(Router.loadMessagesFromAllProviders);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("_triggerHandler", () => {
|
describe("_triggerHandler", () => {
|
||||||
|
@ -1008,10 +1099,8 @@ describe("ASRouter", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#UITour", () => {
|
describe("#UITour", () => {
|
||||||
let globals;
|
|
||||||
let showMenuStub;
|
let showMenuStub;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
globals = new GlobalOverrider();
|
|
||||||
showMenuStub = sandbox.stub();
|
showMenuStub = sandbox.stub();
|
||||||
globals.set("UITour", {showMenu: showMenuStub});
|
globals.set("UITour", {showMenu: showMenuStub});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ const FAKE_NEWSLETTER_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "news
|
||||||
const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
|
const FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === "fxa");
|
||||||
|
|
||||||
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", body: "Foo123"}}], extraTemplateStrings: {}, template: "onboarding"};
|
const FAKE_BUNDLED_MESSAGE = {bundle: [{id: "foo", template: "onboarding", content: {title: "Foo", primary_button: {}, body: "Foo123"}}], extraTemplateStrings: {}, template: "onboarding"};
|
||||||
|
|
||||||
describe("ASRouterUtils", () => {
|
describe("ASRouterUtils", () => {
|
||||||
let global;
|
let global;
|
||||||
|
|
|
@ -6,19 +6,23 @@ const DEFAULT_CONTENT = {
|
||||||
"title": "A title",
|
"title": "A title",
|
||||||
"text": "A description",
|
"text": "A description",
|
||||||
"icon": "icon",
|
"icon": "icon",
|
||||||
"button_label": "some_button_label",
|
"primary_button": {
|
||||||
"button_action": {
|
"label": "some_button_label",
|
||||||
|
"action": {
|
||||||
"type": "SOME_TYPE",
|
"type": "SOME_TYPE",
|
||||||
"data": {"args": "example.com"},
|
"data": {"args": "example.com"},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const L10N_CONTENT = {
|
const L10N_CONTENT = {
|
||||||
"title": {string_id: "onboarding-private-browsing-title"},
|
"title": {string_id: "onboarding-private-browsing-title"},
|
||||||
"text": {string_id: "onboarding-private-browsing-text"},
|
"text": {string_id: "onboarding-private-browsing-text"},
|
||||||
"icon": "icon",
|
"icon": "icon",
|
||||||
"button_label": {string_id: "onboarding-button-label-try-now"},
|
"primary_button": {
|
||||||
"button_action": {type: "SOME_TYPE"},
|
"label": {string_id: "onboarding-button-label-try-now"},
|
||||||
|
"action": {type: "SOME_TYPE"},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("OnboardingMessage", () => {
|
describe("OnboardingMessage", () => {
|
||||||
|
|
|
@ -152,6 +152,9 @@ const TEST_GLOBAL = {
|
||||||
File: function() {}, // NB: This is a function/constructor
|
File: function() {}, // NB: This is a function/constructor
|
||||||
},
|
},
|
||||||
Services: {
|
Services: {
|
||||||
|
dirsvc: {
|
||||||
|
get: () => ({parent: {parent: {path: "appPath"}}}),
|
||||||
|
},
|
||||||
locale: {
|
locale: {
|
||||||
get appLocaleAsLangTag() { return "en-US"; },
|
get appLocaleAsLangTag() { return "en-US"; },
|
||||||
negotiateLanguages() {},
|
negotiateLanguages() {},
|
||||||
|
|
Загрузка…
Ссылка в новой задаче