Bug 1659169 - RTAMO custom onboarding r=fluent-reviewers,Mardak,flod

Differential Revision: https://phabricator.services.mozilla.com/D91667
This commit is contained in:
Punam Dahiya 2020-10-09 02:57:40 +00:00
Родитель af00a62acd
Коммит c02f6a2ed9
15 изменённых файлов: 1044 добавлений и 63 удалений

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

@ -41,6 +41,7 @@ module.exports = {
"content-src/aboutwelcome/components/HeroText.jsx",
"content-src/aboutwelcome/components/Zap.jsx",
"content-src/aboutwelcome/components/MultiStageAboutWelcome.jsx",
"content-src/aboutwelcome/components/ReturnToAMO.jsx",
"content-src/asrouter/templates/OnboardingMessage/**",
"content-src/asrouter/templates/FirstRun/**",
"content-src/components/TopSites/**",

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

@ -160,14 +160,18 @@ class AboutWelcomeChild extends JSWindowActorChild {
exportFunctions() {
let window = this.contentWindow;
Cu.exportFunction(this.AWGetStartupData.bind(this), window, {
defineAs: "AWGetStartupData",
Cu.exportFunction(this.AWGetExperimentData.bind(this), window, {
defineAs: "AWGetExperimentData",
});
Cu.exportFunction(this.AWGetAttributionData.bind(this), window, {
defineAs: "AWGetAttributionData",
});
// For local dev, checks for JSON content inside pref browser.aboutwelcome.overrideContent
// that is used to override default 3 cards welcome UI with multistage welcome
Cu.exportFunction(this.AWGetMultiStageScreens.bind(this), window, {
defineAs: "AWGetMultiStageScreens",
// that is used to override default welcome UI
Cu.exportFunction(this.AWGetWelcomeOverrideContent.bind(this), window, {
defineAs: "AWGetWelcomeOverrideContent",
});
Cu.exportFunction(this.AWGetFxAMetricsFlowURI.bind(this), window, {
@ -219,7 +223,7 @@ class AboutWelcomeChild extends JSWindowActorChild {
/**
* Send multistage welcome JSON data read from aboutwelcome.overrideConetent pref to page
*/
AWGetMultiStageScreens() {
AWGetWelcomeOverrideContent() {
return Cu.cloneInto(
multiStageAboutWelcomeContent || {},
this.contentWindow
@ -232,10 +236,70 @@ class AboutWelcomeChild extends JSWindowActorChild {
);
}
async getAddonInfo(attrbObj) {
let { content, source } = attrbObj;
try {
if (!content || source !== "addons.mozilla.org") {
return null;
}
// Attribution data can be double encoded
while (content.includes("%")) {
try {
const result = decodeURIComponent(content);
if (result === content) {
break;
}
content = result;
} catch (e) {
break;
}
}
return await this.sendQuery("AWPage:GET_ADDON_FROM_REPOSITORY", content);
} catch (e) {
Cu.reportError(
"Failed to get the latest add-on version for Return to AMO"
);
return null;
}
}
isRTAMO(attributionData) {
// RTAMO Attribution
return (
attributionData &&
attributionData.campaign === "non-fx-button" &&
attributionData.source === "addons.mozilla.org"
);
}
async formatAttributionData(attribution) {
let result = {};
if (this.isRTAMO(attribution)) {
result = {
template: "return_to_amo",
extraProps: await this.getAddonInfo(attribution),
};
}
return result;
}
async getAttributionData() {
return Cu.cloneInto(
await this.formatAttributionData(
await this.sendQuery("AWPage:GET_ATTRIBUTION_DATA")
),
this.contentWindow
);
}
AWGetAttributionData() {
return this.wrapPromise(this.getAttributionData());
}
/**
* Send initial data to page including experiment information
*/
AWGetStartupData() {
AWGetExperimentData() {
let experimentData;
try {
// Note that we specifically don't wait for experiments to be loaded from disk so if

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

@ -13,6 +13,7 @@ const { XPCOMUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
FxAccounts: "resource://gre/modules/FxAccounts.jsm",
MigrationUtils: "resource:///modules/MigrationUtils.jsm",
OS: "resource://gre/modules/osfile.jsm",
@ -20,6 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
"resource://messaging-system/lib/SpecialMessageActions.jsm",
AboutWelcomeTelemetry:
"resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm",
AttributionCode: "resource:///modules/AttributionCode.jsm",
});
XPCOMUtils.defineLazyGetter(this, "log", () => {
@ -185,6 +187,8 @@ class AboutWelcomeParent extends JSWindowActorParent {
break;
case "AWPage:FXA_METRICS_FLOW_URI":
return FxAccounts.config.promiseMetricsFlowURI("aboutwelcome");
case "AWPage:GET_ATTRIBUTION_DATA":
return AttributionCode.getAttrDataAsync();
case "AWPage:IMPORTABLE_SITES":
return getImportableSites();
case "AWPage:TELEMETRY_EVENT":
@ -194,6 +198,16 @@ class AboutWelcomeParent extends JSWindowActorParent {
this.AboutWelcomeObserver.terminateReason =
AWTerminate.ADDRESS_BAR_NAVIGATED;
break;
case "AWPage:GET_ADDON_FROM_REPOSITORY":
const [addonInfo] = await AddonRepository.getAddonsByIDs([data]);
if (addonInfo.sourceURI.scheme !== "https") {
return null;
}
return {
name: addonInfo.name,
url: addonInfo.sourceURI.spec,
iconURL: addonInfo.icons["64"] || addonInfo.icons["32"],
};
case "AWPage:SELECT_THEME":
return AddonManager.getAddonByID(
LIGHT_WEIGHT_THEMES[data]

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

@ -0,0 +1,86 @@
/* 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/. */
.ReturnToAMOOverlay {
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center; }
.ReturnToAMOOverlay .ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%; }
.ReturnToAMOOverlay .ReturnToAMOText img {
margin-inline: 2px;
width: 20px;
height: 20px; }
.ReturnToAMOOverlay h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px; }
.ReturnToAMOOverlay p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px; }
.ReturnToAMOOverlay button {
font-family: inherit;
cursor: pointer;
border: 0; }
.ReturnToAMOOverlay .puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px; }
.ReturnToAMOOverlay .blue {
color: #FFF;
background-color: #0060DF; }
.ReturnToAMOOverlay .blue:hover {
box-shadow: none;
background-color: #003EAA; }
.ReturnToAMOOverlay .blue:active {
background-color: #002275; }
.ReturnToAMOOverlay .default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px; }
.ReturnToAMOOverlay .grey {
color: #000;
background-color: rgba(12, 12, 13, 0.1); }
.ReturnToAMOOverlay .grey:hover {
box-shadow: none;
background-color: rgba(12, 12, 13, 0.2); }
.ReturnToAMOOverlay .grey:active {
background-color: rgba(12, 12, 13, 0.3); }
.ReturnToAMOOverlay .ReturnToAMOGetStarted {
margin-top: 40px;
float: inline-end; }
.ReturnToAMOOverlay .ReturnToAMOAddExtension {
margin-top: 20px; }
.ReturnToAMOOverlay .ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px; }
.ReturnToAMOOverlay .ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px; }
.ReturnToAMOOverlay .ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url("chrome://activity-stream/content/data/content/assets/gift-extension.svg"); }

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

@ -102,7 +102,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
/* harmony import */ var _components_SimpleAboutWelcome__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(6);
/* harmony import */ var _components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(12);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(6);
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* This Source Code Form is subject to the terms of the Mozilla Public
@ -114,6 +115,7 @@ function _extends() { _extends = Object.assign || function (target) { for (var i
class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
constructor(props) {
super(props);
@ -133,7 +135,7 @@ class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
componentDidMount() {
this.fetchFxAFlowUri(); // Record impression with performance data after allowing the page to load
window.addEventListener("load", () => {
const recordImpression = domState => {
const {
domComplete,
domInteractive
@ -144,21 +146,32 @@ class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
domComplete,
domInteractive,
mountStart: performance.getEntriesByName("mount").pop().startTime,
domState,
source: this.props.UTMTerm,
page: "about:welcome"
},
message_id: this.props.messageId
});
}, {
once: true
}); // Captures user has seen about:welcome by setting
};
if (document.readyState === "complete") {
// Page might have already triggered a load event because it waited for async data,
// e.g., attribution, so the dom load timing could be of a empty content
// with domState in telemetry captured as 'complete'
recordImpression(document.readyState);
} else {
window.addEventListener("load", () => recordImpression("load"), {
once: true
});
} // Captures user has seen about:welcome by setting
// firstrun.didSeeAboutWelcome pref to true and capturing welcome UI unique messageId
window.AWSendToParent("SET_WELCOME_MESSAGE_SEEN", this.props.messageId);
}
handleStartBtnClick() {
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_4__["AboutWelcomeUtils"].handleUserAction(this.props.startButton.action);
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_5__["AboutWelcomeUtils"].handleUserAction(this.props.startButton.action);
const ping = {
event: "CLICK_BUTTON",
event_context: {
@ -187,6 +200,13 @@ class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
startButton: props.startButton,
handleStartBtnClick: this.handleStartBtnClick
});
} else if (props.template === "return_to_amo") {
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_4__["ReturnToAMO"], {
message_id: props.messageId,
name: props.name,
url: props.url,
iconURL: props.iconURL
});
}
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_2__["MultiStageAboutWelcome"], {
@ -199,14 +219,14 @@ class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
}
AboutWelcome.defaultProps = _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_4__["DEFAULT_WELCOME_CONTENT"];
AboutWelcome.defaultProps = _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_5__["DEFAULT_WELCOME_CONTENT"]; // Computes messageId and UTMTerm info used in telemetry
function ComputeMessageId(experimentId, branchId, settings) {
let messageId = "DEFAULT_ABOUTWELCOME";
function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) {
let messageId = welcomeContent.template === "return_to_amo" ? "RTAMO_DEFAULT_WELCOME" : "DEFAULT_ABOUTWELCOME";
let UTMTerm = "default";
if (settings.id && settings.screens) {
messageId = settings.id.toUpperCase();
if (welcomeContent.id) {
messageId = welcomeContent.id.toUpperCase();
}
if (experimentId && branchId) {
@ -219,26 +239,67 @@ function ComputeMessageId(experimentId, branchId, settings) {
};
}
async function mount() {
async function retrieveRenderContent() {
var _aboutWelcomeProps;
// Check for override content in pref browser.aboutwelcome.overrideContent
let aboutWelcomeProps = await window.AWGetWelcomeOverrideContent();
if ((_aboutWelcomeProps = aboutWelcomeProps) === null || _aboutWelcomeProps === void 0 ? void 0 : _aboutWelcomeProps.template) {
let {
messageId,
UTMTerm
} = ComputeTelemetryInfo(aboutWelcomeProps);
return {
aboutWelcomeProps,
messageId,
UTMTerm
};
} // Check for experiment and retrieve content
const {
slug,
branch
} = await window.AWGetStartupData();
let settings = (branch === null || branch === void 0 ? void 0 : branch.feature) ? branch.feature.value : {};
} = await window.AWGetExperimentData();
aboutWelcomeProps = (branch === null || branch === void 0 ? void 0 : branch.feature) ? branch.feature.value : {}; // Check if there is any attribution data, this could take a while to await in series
// especially when there is an add-on that requires remote lookup
// Moving RTAMO as part of another screen of multistage is one option to fix the delay
// as it will allow the initial page to be fast while we fetch attribution data in parallel for a later screen.
if (!(branch === null || branch === void 0 ? void 0 : branch.feature)) {
// Check for override content in pref browser.aboutwelcome.overrideContent
settings = await window.AWGetMultiStageScreens();
const attribution = await window.AWGetAttributionData();
if (attribution === null || attribution === void 0 ? void 0 : attribution.template) {
var _aboutWelcomeProps2;
aboutWelcomeProps = { ...aboutWelcomeProps,
// If part of an experiment, render experiment template
template: ((_aboutWelcomeProps2 = aboutWelcomeProps) === null || _aboutWelcomeProps2 === void 0 ? void 0 : _aboutWelcomeProps2.template) ? aboutWelcomeProps.template : attribution.template,
...attribution.extraProps
};
}
let {
messageId,
UTMTerm
} = ComputeMessageId(slug, branch && branch.slug, settings);
} = ComputeTelemetryInfo(aboutWelcomeProps, slug, branch && branch.slug);
return {
aboutWelcomeProps,
messageId,
UTMTerm
};
}
async function mount() {
let {
aboutWelcomeProps,
messageId,
UTMTerm
} = await retrieveRenderContent();
react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(AboutWelcome, _extends({
messageId: messageId,
UTMTerm: UTMTerm
}, settings)), document.getElementById("root"));
}, aboutWelcomeProps)), document.getElementById("root"));
}
performance.mark("mount");
@ -774,6 +835,7 @@ const Zap = props => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AboutWelcomeUtils", function() { return AboutWelcomeUtils; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DEFAULT_RTAMO_CONTENT", function() { return DEFAULT_RTAMO_CONTENT; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DEFAULT_WELCOME_CONTENT", function() { return DEFAULT_WELCOME_CONTENT; });
/* 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,
@ -844,6 +906,41 @@ const AboutWelcomeUtils = {
}
};
const DEFAULT_RTAMO_CONTENT = {
template: "return_to_amo",
content: {
header: {
string_id: "onboarding-welcome-header"
},
subtitle: {
string_id: "return-to-amo-subtitle"
},
text: {
string_id: "return-to-amo-addon-title"
},
primary_button: {
label: {
string_id: "return-to-amo-add-extension-label"
},
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {
url: null,
telemetrySource: "rtamo"
}
}
},
startButton: {
label: {
string_id: "onboarding-start-browsing-button-label"
},
message_id: "RTAMO_START_BROWSING_BUTTON",
action: {
type: "OPEN_AWESOME_BAR"
}
}
}
};
const DEFAULT_WELCOME_CONTENT = {
template: "multistage",
screens: [{
@ -1276,5 +1373,123 @@ class OnboardingCard extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureCo
}
/***/ }),
/* 12 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReturnToAMO", function() { return ReturnToAMO; });
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6);
/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
/* 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/. */
class ReturnToAMO extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
constructor(props) {
super(props);
this.onClickAddExtension = this.onClickAddExtension.bind(this);
this.handleStartBtnClick = this.handleStartBtnClick.bind(this);
}
onClickAddExtension() {
var _content$primary_butt, _content$primary_butt2;
const {
content,
message_id,
url
} = this.props;
if (!(content === null || content === void 0 ? void 0 : (_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : (_content$primary_butt2 = _content$primary_butt.action) === null || _content$primary_butt2 === void 0 ? void 0 : _content$primary_butt2.data)) {
return;
} // Set add-on url in action.data.url property from JSON
content.primary_button.action.data.url = url;
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__["AboutWelcomeUtils"].handleUserAction(content.primary_button.action);
const ping = {
event: "INSTALL",
event_context: {
source: "ADD_EXTENSION_BUTTON",
page: "about:welcome"
},
message_id
};
window.AWSendEventTelemetry(ping);
}
handleStartBtnClick() {
const {
content,
message_id
} = this.props;
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__["AboutWelcomeUtils"].handleUserAction(content.startButton.action);
const ping = {
event: "CLICK_BUTTON",
event_context: {
source: content.startButton.message_id,
page: "about:welcome"
},
message_id
};
window.AWSendEventTelemetry(ping);
}
render() {
const {
content
} = this.props;
if (!content) {
return null;
} // For experiments, when needed below rendered UI allows settings hard coded strings
// directly inside JSON except for ReturnToAMOText which picks add-on name and icon from fluent string
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "ReturnToAMOOverlay"
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_2__["Localized"], {
text: content.header
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", null)), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "ReturnToAMOContainer"
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "ReturnToAMOAddonContents"
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_2__["Localized"], {
text: content.subtitle
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("p", null)), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_2__["Localized"], {
text: content.text
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "ReturnToAMOText",
"data-l10n-args": this.props.name ? JSON.stringify({
"addon-name": this.props.name
}) : null
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
"data-l10n-name": "icon",
src: this.props.iconURL,
alt: ""
}))), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_2__["Localized"], {
text: content.primary_button.label
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
onClick: this.onClickAddExtension,
className: "puffy blue ReturnToAMOAddExtension"
}))), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "ReturnToAMOIcon"
})), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_2__["Localized"], {
text: content.startButton.label
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
onClick: this.handleStartBtnClick,
className: "default grey ReturnToAMOGetStarted"
}))));
}
}
ReturnToAMO.defaultProps = _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__["DEFAULT_RTAMO_CONTENT"];
/***/ })
/******/ ]);

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

@ -531,3 +531,90 @@ body {
opacity: 0.25; }
.multistageContainer .steps .indicator.current {
opacity: 1; }
/* 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/. */
.ReturnToAMOOverlay {
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center; }
.ReturnToAMOOverlay .ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%; }
.ReturnToAMOOverlay .ReturnToAMOText img {
margin-inline: 2px;
width: 20px;
height: 20px; }
.ReturnToAMOOverlay h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px; }
.ReturnToAMOOverlay p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px; }
.ReturnToAMOOverlay button {
font-family: inherit;
cursor: pointer;
border: 0; }
.ReturnToAMOOverlay .puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px; }
.ReturnToAMOOverlay .blue {
color: #FFF;
background-color: #0060DF; }
.ReturnToAMOOverlay .blue:hover {
box-shadow: none;
background-color: #003EAA; }
.ReturnToAMOOverlay .blue:active {
background-color: #002275; }
.ReturnToAMOOverlay .default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px; }
.ReturnToAMOOverlay .grey {
color: #000;
background-color: rgba(12, 12, 13, 0.1); }
.ReturnToAMOOverlay .grey:hover {
box-shadow: none;
background-color: rgba(12, 12, 13, 0.2); }
.ReturnToAMOOverlay .grey:active {
background-color: rgba(12, 12, 13, 0.3); }
.ReturnToAMOOverlay .ReturnToAMOGetStarted {
margin-top: 40px;
float: inline-end; }
.ReturnToAMOOverlay .ReturnToAMOAddExtension {
margin-top: 20px; }
.ReturnToAMOOverlay .ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px; }
.ReturnToAMOOverlay .ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px; }
.ReturnToAMOOverlay .ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url("chrome://activity-stream/content/data/content/assets/gift-extension.svg"); }

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

@ -0,0 +1,125 @@
// sass-lint:disable no-css-comments
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.ReturnToAMOOverlay {
// sass-lint:disable no-color-literals
background: #F9F9FA;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.ReturnToAMOText {
color: #0C0C0D;
line-height: 32px;
font-size: 23px;
width: 100%;
img {
margin-inline: 2px;
width: 20px;
height: 20px;
}
}
h2 {
color: #4A4A4F;
font-weight: 100;
margin: 0 0 36px;
font-size: 36px;
line-height: 48px;
letter-spacing: 1.2px;
}
p {
color: #4A4A4F;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
}
button {
font-family: inherit;
cursor: pointer;
border: 0;
}
.puffy {
border-radius: 4px;
height: 48px;
padding: 0 16px;
font-size: 15px;
}
.blue {
color: #FFF;
background-color: #0060DF;
&:hover {
box-shadow: none;
background-color: #003EAA;
}
&:active {
background-color: #002275;
}
}
.default {
border-radius: 2px;
height: 40px;
padding: 0 12px;
font-size: 15px;
}
.grey {
color: #000;
background-color: rgba(#0C0C0D, 0.1);
&:hover {
box-shadow: none;
background-color: rgba(#0C0C0D, 0.2);
}
&:active {
background-color: rgba(#0C0C0D, 0.3);
}
}
.ReturnToAMOGetStarted {
margin-top: 40px;
float: inline-end;
}
.ReturnToAMOAddExtension {
margin-top: 20px;
}
.ReturnToAMOContainer {
width: 960px;
background: #FFF;
box-shadow: 0 1px 15px 0 rgba(#000, 0.3);
border-radius: 4px;
display: flex;
padding: 64px 64px 72px;
}
.ReturnToAMOAddonContents {
width: 560px;
margin-top: 32px;
margin-inline-end: 24px;
}
.ReturnToAMOIcon {
width: 292px;
height: 254px;
background-size: 292px 254px;
background-position: center center;
background-repeat: no-repeat;
background-image: url('chrome://activity-stream/content/data/content/assets/gift-extension.svg');
}
}

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

@ -6,6 +6,7 @@ import React from "react";
import ReactDOM from "react-dom";
import { MultiStageAboutWelcome } from "./components/MultiStageAboutWelcome";
import { SimpleAboutWelcome } from "./components/SimpleAboutWelcome";
import { ReturnToAMO } from "./components/ReturnToAMO";
import {
AboutWelcomeUtils,
@ -28,26 +29,33 @@ class AboutWelcome extends React.PureComponent {
this.fetchFxAFlowUri();
// Record impression with performance data after allowing the page to load
window.addEventListener(
"load",
() => {
const { domComplete, domInteractive } = performance
.getEntriesByType("navigation")
.pop();
window.AWSendEventTelemetry({
event: "IMPRESSION",
event_context: {
domComplete,
domInteractive,
mountStart: performance.getEntriesByName("mount").pop().startTime,
source: this.props.UTMTerm,
page: "about:welcome",
},
message_id: this.props.messageId,
});
},
{ once: true }
);
const recordImpression = domState => {
const { domComplete, domInteractive } = performance
.getEntriesByType("navigation")
.pop();
window.AWSendEventTelemetry({
event: "IMPRESSION",
event_context: {
domComplete,
domInteractive,
mountStart: performance.getEntriesByName("mount").pop().startTime,
domState,
source: this.props.UTMTerm,
page: "about:welcome",
},
message_id: this.props.messageId,
});
};
if (document.readyState === "complete") {
// Page might have already triggered a load event because it waited for async data,
// e.g., attribution, so the dom load timing could be of a empty content
// with domState in telemetry captured as 'complete'
recordImpression(document.readyState);
} else {
window.addEventListener("load", () => recordImpression("load"), {
once: true,
});
}
// Captures user has seen about:welcome by setting
// firstrun.didSeeAboutWelcome pref to true and capturing welcome UI unique messageId
@ -83,6 +91,15 @@ class AboutWelcome extends React.PureComponent {
handleStartBtnClick={this.handleStartBtnClick}
/>
);
} else if (props.template === "return_to_amo") {
return (
<ReturnToAMO
message_id={props.messageId}
name={props.name}
url={props.url}
iconURL={props.iconURL}
/>
);
}
return (
@ -98,12 +115,16 @@ class AboutWelcome extends React.PureComponent {
AboutWelcome.defaultProps = DEFAULT_WELCOME_CONTENT;
function ComputeMessageId(experimentId, branchId, settings) {
let messageId = "DEFAULT_ABOUTWELCOME";
// Computes messageId and UTMTerm info used in telemetry
function ComputeTelemetryInfo(welcomeContent, experimentId, branchId) {
let messageId =
welcomeContent.template === "return_to_amo"
? "RTAMO_DEFAULT_WELCOME"
: "DEFAULT_ABOUTWELCOME";
let UTMTerm = "default";
if (settings.id && settings.screens) {
messageId = settings.id.toUpperCase();
if (welcomeContent.id) {
messageId = welcomeContent.id.toUpperCase();
}
if (experimentId && branchId) {
@ -115,23 +136,50 @@ function ComputeMessageId(experimentId, branchId, settings) {
};
}
async function mount() {
const { slug, branch } = await window.AWGetStartupData();
let settings = branch?.feature ? branch.feature.value : {};
if (!branch?.feature) {
// Check for override content in pref browser.aboutwelcome.overrideContent
settings = await window.AWGetMultiStageScreens();
async function retrieveRenderContent() {
// Check for override content in pref browser.aboutwelcome.overrideContent
let aboutWelcomeProps = await window.AWGetWelcomeOverrideContent();
if (aboutWelcomeProps?.template) {
let { messageId, UTMTerm } = ComputeTelemetryInfo(aboutWelcomeProps);
return { aboutWelcomeProps, messageId, UTMTerm };
}
let { messageId, UTMTerm } = ComputeMessageId(
slug,
branch && branch.slug,
settings
);
// Check for experiment and retrieve content
const { slug, branch } = await window.AWGetExperimentData();
aboutWelcomeProps = branch?.feature ? branch.feature.value : {};
// Check if there is any attribution data, this could take a while to await in series
// especially when there is an add-on that requires remote lookup
// Moving RTAMO as part of another screen of multistage is one option to fix the delay
// as it will allow the initial page to be fast while we fetch attribution data in parallel for a later screen.
const attribution = await window.AWGetAttributionData();
if (attribution?.template) {
aboutWelcomeProps = {
...aboutWelcomeProps,
// If part of an experiment, render experiment template
template: aboutWelcomeProps?.template
? aboutWelcomeProps.template
: attribution.template,
...attribution.extraProps,
};
}
let { messageId, UTMTerm } = ComputeTelemetryInfo(
aboutWelcomeProps,
slug,
branch && branch.slug
);
return { aboutWelcomeProps, messageId, UTMTerm };
}
async function mount() {
let { aboutWelcomeProps, messageId, UTMTerm } = await retrieveRenderContent();
ReactDOM.render(
<AboutWelcome messageId={messageId} UTMTerm={UTMTerm} {...settings} />,
<AboutWelcome
messageId={messageId}
UTMTerm={UTMTerm}
{...aboutWelcomeProps}
/>,
document.getElementById("root")
);
}

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

@ -650,3 +650,5 @@ body {
}
}
}
@import './ReturnToAMO';

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

@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import {
AboutWelcomeUtils,
DEFAULT_RTAMO_CONTENT,
} from "../../lib/aboutwelcome-utils";
import { Localized } from "./MSLocalized";
export class ReturnToAMO extends React.PureComponent {
constructor(props) {
super(props);
this.onClickAddExtension = this.onClickAddExtension.bind(this);
this.handleStartBtnClick = this.handleStartBtnClick.bind(this);
}
onClickAddExtension() {
const { content, message_id, url } = this.props;
if (!content?.primary_button?.action?.data) {
return;
}
// Set add-on url in action.data.url property from JSON
content.primary_button.action.data.url = url;
AboutWelcomeUtils.handleUserAction(content.primary_button.action);
const ping = {
event: "INSTALL",
event_context: {
source: "ADD_EXTENSION_BUTTON",
page: "about:welcome",
},
message_id,
};
window.AWSendEventTelemetry(ping);
}
handleStartBtnClick() {
const { content, message_id } = this.props;
AboutWelcomeUtils.handleUserAction(content.startButton.action);
const ping = {
event: "CLICK_BUTTON",
event_context: {
source: content.startButton.message_id,
page: "about:welcome",
},
message_id,
};
window.AWSendEventTelemetry(ping);
}
render() {
const { content } = this.props;
if (!content) {
return null;
}
// For experiments, when needed below rendered UI allows settings hard coded strings
// directly inside JSON except for ReturnToAMOText which picks add-on name and icon from fluent string
return (
<div className="ReturnToAMOOverlay">
<div>
<Localized text={content.header}>
<h2 />
</Localized>
<div className="ReturnToAMOContainer">
<div className="ReturnToAMOAddonContents">
<Localized text={content.subtitle}>
<p />
</Localized>
<Localized text={content.text}>
<div
className="ReturnToAMOText"
data-l10n-args={
this.props.name
? JSON.stringify({ "addon-name": this.props.name })
: null
}
>
<img data-l10n-name="icon" src={this.props.iconURL} alt="" />
</div>
</Localized>
<Localized text={content.primary_button.label}>
<button
onClick={this.onClickAddExtension}
className="puffy blue ReturnToAMOAddExtension"
/>
</Localized>
</div>
<div className="ReturnToAMOIcon" />
</div>
<Localized text={content.startButton.label}>
<button
onClick={this.handleStartBtnClick}
className="default grey ReturnToAMOGetStarted"
/>
</Localized>
</div>
</div>
);
}
}
ReturnToAMO.defaultProps = DEFAULT_RTAMO_CONTENT;

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

@ -54,6 +54,33 @@ export const AboutWelcomeUtils = {
},
};
export const DEFAULT_RTAMO_CONTENT = {
template: "return_to_amo",
content: {
header: { string_id: "onboarding-welcome-header" },
subtitle: { string_id: "return-to-amo-subtitle" },
text: {
string_id: "return-to-amo-addon-title",
},
primary_button: {
label: { string_id: "return-to-amo-add-extension-label" },
action: {
type: "INSTALL_ADDON_FROM_URL",
data: { url: null, telemetrySource: "rtamo" },
},
},
startButton: {
label: {
string_id: "onboarding-start-browsing-button-label",
},
message_id: "RTAMO_START_BROWSING_BUTTON",
action: {
type: "OPEN_AWESOME_BAR",
},
},
},
};
export const DEFAULT_WELCOME_CONTENT = {
template: "multistage",
screens: [

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

После

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

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

@ -17,6 +17,7 @@ prefs =
[browser_aboutwelcome_actors.js]
[browser_aboutwelcome_simplified.js]
[browser_aboutwelcome_multistage.js]
[browser_aboutwelcome_rtamo.js]
skip-if = fission
[browser_aboutwelcome_observer.js]
[browser_as_load_location.js]

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

@ -0,0 +1,193 @@
"use strict";
const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF =
"browser.aboutwelcome.overrideContent";
const TEST_RTAMO_WELCOME_CONTENT = {
template: "return_to_amo",
name: "Test add on",
url: "https://test.xpi",
iconURL: "https://test.svg",
content: {
header: { string_id: "onboarding-welcome-header" },
subtitle: { string_id: "return-to-amo-subtitle" },
text: {
string_id: "return-to-amo-addon-title",
},
primary_button: {
label: { string_id: "return-to-amo-add-extension-label" },
action: {
type: "INSTALL_ADDON_FROM_URL",
data: { url: null, telemetrySource: "rtamo" },
},
},
startButton: {
label: {
string_id: "onboarding-start-browsing-button-label",
},
message_id: "RTAMO_START_BROWSING_BUTTON",
action: {
type: "OPEN_AWESOME_BAR",
},
},
},
};
const TEST_RTAMO_WELCOME_JSON = JSON.stringify(TEST_RTAMO_WELCOME_CONTENT);
async function setAboutWelcomeOverride(value) {
return pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, value]);
}
async function openRTAMOWelcomePage() {
await setAboutWelcomeOverride(TEST_RTAMO_WELCOME_JSON);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:welcome",
true
);
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(tab);
});
return tab.linkedBrowser;
}
/**
* Setup and test RTAMO welcome UI
*/
async function test_screen_content(
browser,
experiment,
expectedSelectors = [],
unexpectedSelectors = []
) {
await ContentTask.spawn(
browser,
{ expectedSelectors, experiment, unexpectedSelectors },
async ({
expectedSelectors: expected,
experiment: experimentName,
unexpectedSelectors: unexpected,
}) => {
for (let selector of expected) {
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector(selector),
`Should render ${selector} in ${experimentName}`
);
}
for (let selector of unexpected) {
ok(
!content.document.querySelector(selector),
`Should not render ${selector} in ${experimentName}`
);
}
}
);
}
async function onButtonClick(browser, elementId) {
await ContentTask.spawn(
browser,
{ elementId },
async ({ elementId: buttonId }) => {
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector(buttonId),
buttonId
);
let button = content.document.querySelector(buttonId);
button.click();
}
);
}
/**
* Test the RTAMO welcome UI
*/
add_task(async function test_rtamo_aboutwelcome() {
let browser = await openRTAMOWelcomePage();
await test_screen_content(
browser,
"RTAMO UI",
// Expected selectors:
[
"div.ReturnToAMOContainer",
"div.ReturnToAMOText",
"div[data-l10n-id='return-to-amo-addon-title']",
"img[data-l10n-name='icon']",
"button.puffy.blue.ReturnToAMOAddExtension",
"button.default.grey.ReturnToAMOGetStarted",
],
// Unexpected selectors:
[
"div.multistageContainer",
"main.AW_STEP1",
"main.AW_STEP2",
"main.AW_STEP3",
"div.tiles-container.info",
]
);
await onButtonClick(browser, "button.default.grey.ReturnToAMOGetStarted");
Assert.ok(gURLBar.focused, "Focus should be on awesome bar");
let windowGlobalParent = browser.browsingContext.currentWindowGlobal;
let aboutWelcomeActor = windowGlobalParent.getActor("AboutWelcome");
const sandbox = sinon.createSandbox();
// Stub AboutWelcomeParent Content Message Handler
sandbox.stub(aboutWelcomeActor, "onContentMessage");
registerCleanupFunction(() => {
sandbox.restore();
});
await onButtonClick(browser, "button.puffy.blue.ReturnToAMOAddExtension");
const { callCount } = aboutWelcomeActor.onContentMessage;
ok(
callCount === 2,
`${callCount} Stub called twice to install extension and send telemetry`
);
const installExtensionCall = aboutWelcomeActor.onContentMessage.getCall(0);
Assert.equal(
installExtensionCall.args[0],
"AWPage:SPECIAL_ACTION",
"send special action to install add on"
);
Assert.equal(
installExtensionCall.args[1].type,
"INSTALL_ADDON_FROM_URL",
"Special action type is INSTALL_ADDON_FROM_URL"
);
Assert.equal(
installExtensionCall.args[1].data.url,
"https://test.xpi",
"Install add on url"
);
Assert.equal(
installExtensionCall.args[1].data.telemetrySource,
"rtamo",
"Install add on telemetry source"
);
const telemetryCall = aboutWelcomeActor.onContentMessage.getCall(1);
Assert.equal(
telemetryCall.args[0],
"AWPage:TELEMETRY_EVENT",
"send add extension telemetry"
);
Assert.equal(
telemetryCall.args[1].event,
"INSTALL",
"Telemetry event sent as INSTALL"
);
Assert.equal(
telemetryCall.args[1].event_context.source,
"ADD_EXTENSION_BUTTON",
"Source of the event is Add Extension Button"
);
Assert.equal(
telemetryCall.args[1].message_id,
"RTAMO_DEFAULT_WELCOME",
"Message Id sent in telemetry for default RTAMO"
);
});

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

@ -14,6 +14,16 @@ onboarding-cards-dismiss =
.title = Dismiss
.aria-label = Dismiss
## Custom Return To AMO onboarding strings
return-to-amo-subtitle = Great, youve got { -brand-short-name }
# <img data-l10n-name="icon"/> will be replaced with the icon belonging to the extension
#
# Variables:
# $addon-name (String) - Name of the add-on
return-to-amo-addon-title = Now lets get you <img data-l10n-name="icon"/> <b>{ $addon-name }</b>.
return-to-amo-add-extension-label = Add the Extension
## Multistage 3-screen onboarding flow strings (about:welcome pages)
# The <span data-l10n-name="zap"></span> in this string allows a "zap" underline style to be