Bug 1786647 - Split up Feature Callout messages and fetch from ASRouter on page load, focus, and screen advance r=Mardak

Differential Revision: https://phabricator.services.mozilla.com/D155524
This commit is contained in:
Meg Viar 2022-08-31 16:09:26 +00:00
parent 3577ed2643
commit 9dac4ed419
9 changed files with 465 additions and 199 deletions

View File

@ -12,8 +12,13 @@ const lazy = {};
XPCOMUtils.defineLazyModuleGetters(lazy, {
AboutWelcomeParent: "resource:///actors/AboutWelcomeParent.jsm",
ASRouter: "resource://activity-stream/lib/ASRouter.jsm",
});
// When expanding the use of Feature Callout
// to new about: pages, make `progressPref` a
// configurable field on callout messages and
// use it to determine which pref to observe
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"featureTourProgress",
@ -36,7 +41,7 @@ async function _handlePrefChange() {
container?.classList.add("hidden");
// wait for fade out transition
setTimeout(async () => {
_loadConfig(lazy.featureTourProgress.message);
await _loadConfig();
container?.remove();
await _renderCallout();
}, TRANSITION_MS);
@ -78,170 +83,6 @@ let READY = false;
const TRANSITION_MS = 500;
const CONTAINER_ID = "root";
const MESSAGES = [
{
id: "FIREFOX_VIEW_FEATURE_TOUR",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "FEATURE_CALLOUT_1",
parent_selector: "#tab-pickup-container",
content: {
position: "callout",
arrow_position: "top",
title: {
string_id: "callout-firefox-view-tab-pickup-title",
},
subtitle: {
string_id: "callout-firefox-view-tab-pickup-subtitle",
},
logo: {
imageURL: "chrome://browser/content/callout-tab-pickup.svg",
darkModeImageURL:
"chrome://browser/content/callout-tab-pickup-dark.svg",
height: "128px",
},
primary_button: {
label: {
string_id: "callout-primary-advance-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_2",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_1",
complete: true,
}),
},
},
},
},
},
},
{
id: "FEATURE_CALLOUT_2",
parent_selector: "#recently-closed-tabs-container",
content: {
position: "callout",
arrow_position: "bottom",
title: {
string_id: "callout-firefox-view-recently-closed-title",
},
subtitle: {
string_id: "callout-firefox-view-recently-closed-subtitle",
},
primary_button: {
label: {
string_id: "callout-primary-advance-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_3",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_2",
complete: true,
}),
},
},
},
},
},
},
{
id: "FEATURE_CALLOUT_3",
parent_selector: "#colorways.content-container",
content: {
position: "callout",
arrow_position: "end",
title: {
string_id: "callout-firefox-view-colorways-title",
},
subtitle: {
string_id: "callout-firefox-view-colorways-subtitle",
},
logo: {
imageURL: "chrome://browser/content/callout-colorways.svg",
darkModeImageURL:
"chrome://browser/content/callout-colorways-dark.svg",
height: "128px",
},
primary_button: {
label: {
string_id: "callout-primary-complete-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: "browser.firefox-view.feature-tour",
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_3",
complete: true,
}),
},
},
},
},
},
},
],
},
];
function _createContainer() {
let parent = document.querySelector(CURRENT_SCREEN?.parent_selector);
@ -511,31 +352,15 @@ function _observeRender(container) {
RENDER_OBSERVER?.observe(container, { childList: true });
}
function _loadConfig(messageId) {
// If the parent element a screen describes doesn't exist, remove screen
// and ensure last screen displays the final primary CTA
// (for example, when there are no active colorways in about:firefoxview)
function _getRelevantScreens(screens) {
const finalCTA = screens[screens.length - 1].content.primary_button;
screens = screens.filter((s, i) => {
return document.querySelector(s.parent_selector);
});
if (screens.length) {
screens[screens.length - 1].content.primary_button = finalCTA;
}
return screens;
}
let content = MESSAGES.find(m => m.id === messageId);
const screenId = lazy.featureTourProgress.screen;
let screenIndex;
if (content?.screens?.length && screenId) {
content.screens = _getRelevantScreens(content.screens);
screenIndex = content.screens.findIndex(s => s.id === screenId);
content.startScreen = screenIndex;
}
CURRENT_SCREEN = content?.screens?.[screenIndex || 0];
CONFIG = content;
async function _loadConfig() {
await lazy.ASRouter.waitForInitialized;
let result = await lazy.ASRouter.sendTriggerMessage({
// triggerId and triggerContext
id: "featureCalloutCheck",
context: { source: document.location.pathname.toLowerCase() },
});
CONFIG = result.message.content;
CURRENT_SCREEN = CONFIG?.screens?.[CONFIG?.startScreen || 0];
}
async function _renderCallout() {
@ -555,9 +380,9 @@ async function showFeatureCallout(messageId) {
return;
}
_loadConfig(messageId);
await _loadConfig();
if (!CONFIG) {
if (!CONFIG?.screens?.length) {
return;
}

View File

@ -1,5 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { BuiltInThemes } = ChromeUtils.import(
"resource:///modules/BuiltInThemes.jsm"
);
const calloutId = "root";
const calloutSelector = `#${calloutId}.featureCallout`;
const primaryButtonSelector = `#${calloutId} .primary`;
@ -327,6 +334,10 @@ add_task(async function feature_callout_only_highlights_existing_elements() {
],
});
const sandbox = sinon.createSandbox();
// Return no active colorways
sandbox.stub(BuiltInThemes, "findActiveColorwayCollection").returns(false);
await BrowserTestUtils.withNewTab(
{
gBrowser,
@ -335,9 +346,6 @@ add_task(async function feature_callout_only_highlights_existing_elements() {
async browser => {
const { document } = browser.contentWindow;
await waitForCalloutScreen(document, ".FEATURE_CALLOUT_1");
// Remove parent element for third screen in tour
document.querySelector("#colorways.content-container").remove();
// Advance to second screen
await clickPrimaryButton(document);
await waitForCalloutScreen(document, ".FEATURE_CALLOUT_2");
@ -357,6 +365,8 @@ add_task(async function feature_callout_only_highlights_existing_elements() {
!document.querySelector(`${calloutSelector}:not(.hidden)`),
"Feature Callout screen does not render if its parent element does not exist"
);
sandbox.restore();
}
);
});

View File

@ -117,7 +117,8 @@
"template": {
"type": "string",
"enum": [
"spotlight"
"spotlight",
"feature_callout"
]
}
},
@ -1258,7 +1259,8 @@
},
"template": {
"type": "string",
"const": "spotlight"
"description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog",
"enum": ["spotlight", "feature_callout"]
}
},
"additionalProperties": true,
@ -1576,6 +1578,7 @@
"pb_newtab",
"protections_panel",
"spotlight",
"feature_callout",
"toast_notification",
"toolbar_badge",
"update_action",

View File

@ -161,7 +161,8 @@
},
"template": {
"type": "string",
"const": "spotlight"
"description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog",
"enum": ["spotlight", "feature_callout"]
}
},
"additionalProperties": true,

View File

@ -0,0 +1,388 @@
/* 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/. */
"use strict";
// Eventually, make this a messaging system
// provider instead of adding these message
// into OnboardingMessageProvider.jsm
const FIREFOX_VIEW_PREF = "browser.firefox-view.feature-tour";
// Empty screens are included as placeholders to ensure step
// indicator shows the correct number of total steps in the tour
const EMPTY_SCREEN = { content: {} };
// Generate a JEXL targeting string based on the current screen
// id found in a given Feature Callout tour progress preference
const matchCurrentScreenTargeting = (prefName, screenId) => {
return `'${prefName}' | preferenceValue | regExpMatch('(?<=screen\"\:)"(.*)(?=",)')[1] == '${screenId}'`;
};
const MESSAGES = () => [
// about:firefoxview messages
{
id: "FIREFOX_VIEW_FEATURE_TOUR_1",
template: "feature_callout",
content: {
id: "FIREFOX_VIEW_FEATURE_TOUR",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "FEATURE_CALLOUT_1",
parent_selector: "#tabpickup-steps",
content: {
position: "callout",
arrow_position: "top",
title: {
string_id: "callout-firefox-view-tab-pickup-title",
},
subtitle: {
string_id: "callout-firefox-view-tab-pickup-subtitle",
},
logo: {
imageURL: "chrome://browser/content/callout-tab-pickup.svg",
darkModeImageURL:
"chrome://browser/content/callout-tab-pickup-dark.svg",
height: "128px",
},
primary_button: {
label: {
string_id: "callout-primary-advance-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_2",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
},
},
EMPTY_SCREEN,
EMPTY_SCREEN,
],
},
priority: 1,
targeting: `source == "firefoxview" && colorwaysActive && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_1"
)}`,
trigger: { id: "featureCalloutCheck" },
},
{
id: "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS",
template: "feature_callout",
content: {
id: "FIREFOX_VIEW_FEATURE_TOUR",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "FEATURE_CALLOUT_1",
parent_selector: "#tabpickup-steps",
content: {
position: "callout",
arrow_position: "top",
title: {
string_id: "callout-firefox-view-tab-pickup-title",
},
subtitle: {
string_id: "callout-firefox-view-tab-pickup-subtitle",
},
logo: {
imageURL: "chrome://browser/content/callout-tab-pickup.svg",
darkModeImageURL:
"chrome://browser/content/callout-tab-pickup-dark.svg",
height: "128px",
},
primary_button: {
label: {
string_id: "callout-primary-advance-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_2",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
},
},
EMPTY_SCREEN,
],
},
priority: 1,
targeting: `source == "firefoxview" && !colorwaysActive && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_1"
)}`,
trigger: { id: "featureCalloutCheck" },
},
{
id: "FIREFOX_VIEW_FEATURE_TOUR_2",
template: "feature_callout",
content: {
id: "FIREFOX_VIEW_FEATURE_TOUR",
startScreen: 1,
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
EMPTY_SCREEN,
{
id: "FEATURE_CALLOUT_2",
parent_selector: "#recently-closed-tabs-container",
content: {
position: "callout",
arrow_position: "bottom",
title: {
string_id: "callout-firefox-view-recently-closed-title",
},
subtitle: {
string_id: "callout-firefox-view-recently-closed-subtitle",
},
primary_button: {
label: {
string_id: "callout-primary-advance-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_3",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
},
},
EMPTY_SCREEN,
],
},
priority: 1,
targeting: `source == "firefoxview" && colorwaysActive && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_2"
)}`,
trigger: { id: "featureCalloutCheck" },
},
{
id: "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS",
template: "feature_callout",
content: {
id: "FIREFOX_VIEW_FEATURE_TOUR",
startScreen: 1,
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
EMPTY_SCREEN,
{
id: "FEATURE_CALLOUT_2",
parent_selector: "#recently-closed-tabs-container",
content: {
position: "callout",
arrow_position: "bottom",
title: {
string_id: "callout-firefox-view-recently-closed-title",
},
subtitle: {
string_id: "callout-firefox-view-recently-closed-subtitle",
},
primary_button: {
label: {
string_id: "callout-primary-complete-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "FEATURE_CALLOUT_3",
complete: false,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
},
},
],
},
priority: 1,
targeting: `source == "firefoxview" && !colorwaysActive && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_2"
)}`,
trigger: { id: "featureCalloutCheck" },
},
{
id: "FIREFOX_VIEW_FEATURE_TOUR_3",
template: "feature_callout",
content: {
id: "FIREFOX_VIEW_FEATURE_TOUR",
startScreen: 2,
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
EMPTY_SCREEN,
EMPTY_SCREEN,
{
id: "FEATURE_CALLOUT_3",
parent_selector: "#colorways.content-container",
content: {
position: "callout",
arrow_position: "end",
title: {
string_id: "callout-firefox-view-colorways-title",
},
subtitle: {
string_id: "callout-firefox-view-colorways-subtitle",
},
logo: {
imageURL: "chrome://browser/content/callout-colorways.svg",
darkModeImageURL:
"chrome://browser/content/callout-colorways-dark.svg",
height: "128px",
},
primary_button: {
label: {
string_id: "callout-primary-complete-button-label",
},
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
dismiss_button: {
action: {
type: "SET_PREF",
data: {
pref: {
name: FIREFOX_VIEW_PREF,
value: JSON.stringify({
message: "FIREFOX_VIEW_FEATURE_TOUR",
screen: "",
complete: true,
}),
},
},
},
},
},
},
],
},
priority: 1,
targeting: `source == "firefoxview" && colorwaysActive && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_3"
)}`,
trigger: { id: "featureCalloutCheck" },
},
];
const FeatureCalloutMessages = {
getMessages() {
return MESSAGES();
},
};
const EXPORTED_SYMBOLS = ["FeatureCalloutMessages"];

View File

@ -7,6 +7,9 @@
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const { FeatureCalloutMessages } = ChromeUtils.import(
"resource://activity-stream/lib/FeatureCalloutMessages.jsm"
);
const lazy = {};
@ -39,7 +42,7 @@ const L10N = new Localization([
const HOMEPAGE_PREF = "browser.startup.homepage";
const NEWTAB_PREF = "browser.newtabpage.enabled";
const ONBOARDING_MESSAGES = () => [
const BASE_MESSAGES = () => [
{
id: "FXA_ACCOUNTS_BADGE",
template: "toolbar_badge",
@ -881,6 +884,10 @@ const ONBOARDING_MESSAGES = () => [
},
];
// Eventually, move Feature Callout messages to their own provider
const ONBOARDING_MESSAGES = () =>
BASE_MESSAGES().concat(FeatureCalloutMessages.getMessages());
const OnboardingMessageProvider = {
async getExtraAttributes() {
const [header, button_label] = await L10N.formatMessages([

View File

@ -94,6 +94,11 @@ async function makeValidators() {
"resource://testing-common/WhatsNewMessage.schema.json",
{ common: true }
),
feature_callout: await schemaValidatorFor(
// For now, Feature Callout and Spotlight share a common schema
"resource://testing-common/Spotlight.schema.json",
{ common: true }
),
};
messageValidators.milestone_message = messageValidators.cfr_doorhanger;

View File

@ -216,6 +216,29 @@
},
"additionalProperties": false,
"required": ["id", "params"]
},
{
"type": "object",
"properties": {
"id": {
"type": "string",
"enum": ["featureCalloutCheck"]
},
"context": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": ["firefoxview"],
"description": "Which about page is the source of the trigger"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": ["id"],
"description": "Happens when navigating to about:firefoxview or other about pages with Feature Callout tours enabled"
}
]
}

View File

@ -130,3 +130,7 @@ Watch for changes on any number of preferences. Runs when a pref is added, remov
params: ["pref name"]
}
```
### `featureCalloutCheck`
Happens when navigating to about:firefoxview or other about pages with Feature Callout tours enabled