Bug 1920799 - Add a new messaging surface to the AppMenu and PXI menus for describing the value of signing into an FxA. r=pdahiya,Gijs,desktop-theme-reviewers,omc-reviewers,home-newtab-reviewers,fluent-reviewers,hjones,skhamis,nbarrett

Developing tests in a later patch in this series.

Differential Revision: https://phabricator.services.mozilla.com/D223409
This commit is contained in:
Mike Conley
2024-10-15 16:15:09 +00:00
parent 23219d1d28
commit dd4373aecb
26 changed files with 804 additions and 1 deletions

View File

@@ -10,6 +10,10 @@
<toolbarbutton id="appMenu-update-banner" class="panel-banner-item subviewbutton" <toolbarbutton id="appMenu-update-banner" class="panel-banner-item subviewbutton"
wrap="true" wrap="true"
hidden="true"/> hidden="true"/>
<toolbaritem id="appMenu-fxa-menu-message"
closemenu="none">
</toolbaritem>
<toolbaritem id="appMenu-fxa-status2" <toolbaritem id="appMenu-fxa-status2"
closemenu="none" closemenu="none"
class="subviewbutton toolbaritem-combined-buttons"> class="subviewbutton toolbaritem-combined-buttons">
@@ -568,6 +572,9 @@
<panelview id="PanelUI-fxa" class="PanelUI-subView"> <panelview id="PanelUI-fxa" class="PanelUI-subView">
<vbox id="PanelUI-fxa-menu" class="panel-subview-body"> <vbox id="PanelUI-fxa-menu" class="panel-subview-body">
<toolbaritem id="PanelUI-fxa-menu-message"
closemenu="none">
</toolbaritem>
<toolbarbutton id="fxa-manage-account-button" <toolbarbutton id="fxa-manage-account-button"
align="center" align="center"
class="subviewbutton" class="subviewbutton"

View File

@@ -18,11 +18,13 @@ const { UIState } = ChromeUtils.importESModule(
); );
ChromeUtils.defineESModuleGetters(this, { ChromeUtils.defineESModuleGetters(this, {
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
EnsureFxAccountsWebChannel: EnsureFxAccountsWebChannel:
"resource://gre/modules/FxAccountsWebChannel.sys.mjs", "resource://gre/modules/FxAccountsWebChannel.sys.mjs",
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs", FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
SyncedTabsManagement: "resource://services-sync/SyncedTabs.sys.mjs", SyncedTabsManagement: "resource://services-sync/SyncedTabs.sys.mjs",
Weave: "resource://services-sync/main.sys.mjs", Weave: "resource://services-sync/main.sys.mjs",
@@ -697,6 +699,19 @@ var gSync = {
}, },
onFxAPanelViewShowing(panelview) { onFxAPanelViewShowing(panelview) {
let messageId = panelview.getAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
);
if (messageId) {
MenuMessage.recordMenuMessageTelemetry(
"IMPRESSION",
MenuMessage.SOURCES.PXI_MENU,
messageId
);
let message = ASRouter.getMessageById(messageId);
ASRouter.addImpression(message);
}
let syncNowBtn = panelview.querySelector(".syncnow-label"); let syncNowBtn = panelview.querySelector(".syncnow-label");
let l10nId = syncNowBtn.getAttribute( let l10nId = syncNowBtn.getAttribute(
this._isCurrentlySyncing this._isCurrentlySyncing
@@ -730,6 +745,7 @@ var gSync = {
}, },
onFxAPanelViewHiding(panelview) { onFxAPanelViewHiding(panelview) {
MenuMessage.hidePxiMenuMessage(gBrowser.selectedBrowser);
panelview.syncedTabsPanelList.destroy(); panelview.syncedTabsPanelList.destroy();
panelview.syncedTabsPanelList = null; panelview.syncedTabsPanelList = null;
}, },
@@ -954,7 +970,7 @@ var gSync = {
} }
}, },
toggleAccountPanel( async toggleAccountPanel(
anchor = document.getElementById("fxa-toolbar-menu-button"), anchor = document.getElementById("fxa-toolbar-menu-button"),
aEvent aEvent
) { ) {
@@ -972,6 +988,19 @@ var gSync = {
return; return;
} }
if (
anchor == document.getElementById("fxa-toolbar-menu-button") &&
anchor.getAttribute("open") != "true"
) {
if (ASRouter.initialized) {
await ASRouter.sendTriggerMessage({
browser: gBrowser.selectedBrowser,
id: "menuOpened",
context: { source: MenuMessage.SOURCES.PXI_MENU },
});
}
}
// We read the state that's been set on the root node, since that makes // We read the state that's been set on the root node, since that makes
// it easier to test the various front-end states without having to actually // it easier to test the various front-end states without having to actually
// have UIState know about it. // have UIState know about it.

View File

@@ -637,6 +637,13 @@ customElements.setElementCreationCallback("screenshots-buttons", () => {
); );
}); });
customElements.setElementCreationCallback("fxa-menu-message", () => {
ChromeUtils.importESModule(
"chrome://browser/content/asrouter/components/fxa-menu-message.mjs",
{ global: "current" }
);
});
var gBrowser; var gBrowser;
var gContextMenu = null; // nsContextMenu instance var gContextMenu = null; // nsContextMenu instance
var gMultiProcessBrowser = window.docShell.QueryInterface( var gMultiProcessBrowser = window.docShell.QueryInterface(

View File

@@ -135,6 +135,10 @@ async function getMessageValidators(skipValidation) {
"./content-src/templates/OnboardingMessage/Spotlight.schema.json", "./content-src/templates/OnboardingMessage/Spotlight.schema.json",
{ common: true } { common: true }
), ),
menu_message: await getValidator(
"./content-src/templates/OnboardingMessage/MenuMessage.schema.json",
{ common: true }
),
}; };
messageValidators.milestone_message = messageValidators.cfr_doorhanger; messageValidators.milestone_message = messageValidators.cfr_doorhanger;

View File

@@ -638,6 +638,98 @@
} }
} }
}, },
"MenuMessage": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "file:///MenuMessage.schema.json",
"title": "MenuMessage",
"description": "A template for messages that appear within our menus.",
"allOf": [
{
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/Message"
}
],
"type": "object",
"properties": {
"content": {
"type": "object",
"properties": {
"messageType": {
"type": "string",
"description": "The subtype of the message.",
"enum": [
"fxa_cta"
]
},
"primaryText": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
"description": "The primary text for the message, which offers the value proposition to the user."
},
"secondaryText": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
"description": "The second text for the message, which offers more detail on the value proposition to the user."
},
"closeAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
},
"data": {
"type": "object"
}
},
"required": [
"type"
],
"additionalProperties": true,
"description": "The action to take upon clicking the close button."
},
"primaryAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
},
"data": {
"type": "object"
}
},
"required": [
"type"
],
"additionalProperties": true,
"description": "The action to take upon clicking the primary action button."
},
"primaryActionText": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
"description": "The label for the primary action."
},
"imageURL": {
"type": "string",
"description": "URL for image to use with the content."
},
"imageVerticalOffset": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels."
}
}
},
"template": {
"type": "string",
"const": "menu_message"
},
"testingTriggerContext": {
"type": "string",
"enum": [
"app_menu",
"pxi_menu"
]
}
},
"additionalProperties": true
},
"NewtabPromoMessage": { "NewtabPromoMessage": {
"$schema": "https://json-schema.org/draft/2019-09/schema", "$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "file:///NewtabPromoMessage.schema.json", "$id": "file:///NewtabPromoMessage.schema.json",
@@ -1180,6 +1272,7 @@
"cfr_doorhanger", "cfr_doorhanger",
"milestone_message", "milestone_message",
"infobar", "infobar",
"menu_message",
"pb_newtab", "pb_newtab",
"spotlight", "spotlight",
"feature_callout", "feature_callout",
@@ -1388,6 +1481,25 @@
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/InfoBar" "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/InfoBar"
} }
}, },
{
"if": {
"type": "object",
"properties": {
"template": {
"type": "string",
"enum": [
"menu_message"
]
}
},
"required": [
"template"
]
},
"then": {
"$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/MenuMessage"
}
},
{ {
"if": { "if": {
"type": "object", "type": "object",

View File

@@ -73,6 +73,9 @@ SCHEMAS = [
SCHEMA_DIR / "CFR" / "templates" / "ExtensionDoorhanger.schema.json" SCHEMA_DIR / "CFR" / "templates" / "ExtensionDoorhanger.schema.json"
), ),
"InfoBar": SCHEMA_DIR / "CFR" / "templates" / "InfoBar.schema.json", "InfoBar": SCHEMA_DIR / "CFR" / "templates" / "InfoBar.schema.json",
"MenuMessage": (
SCHEMA_DIR / "OnboardingMessage" / "MenuMessage.schema.json"
),
"NewtabPromoMessage": ( "NewtabPromoMessage": (
SCHEMA_DIR / "PBNewtab" / "NewtabPromoMessage.schema.json" SCHEMA_DIR / "PBNewtab" / "NewtabPromoMessage.schema.json"
), ),

View File

@@ -0,0 +1,83 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "file:///MenuMessage.schema.json",
"title": "MenuMessage",
"description": "A template for messages that appear within our menus.",
"allOf": [
{
"$ref": "file:///FxMSCommon.schema.json#/$defs/Message"
}
],
"type": "object",
"properties": {
"content": {
"type": "object",
"properties": {
"messageType": {
"type": "string",
"description": "The subtype of the message.",
"enum": ["fxa_cta"]
},
"primaryText": {
"$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
"description": "The primary text for the message, which offers the value proposition to the user."
},
"secondaryText": {
"$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
"description": "The second text for the message, which offers more detail on the value proposition to the user."
},
"closeAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
},
"data": {
"type": "object"
}
},
"required": ["type"],
"additionalProperties": true,
"description": "The action to take upon clicking the close button."
},
"primaryAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
},
"data": {
"type": "object"
}
},
"required": ["type"],
"additionalProperties": true,
"description": "The action to take upon clicking the primary action button."
},
"primaryActionText": {
"$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
"description": "The label for the primary action."
},
"imageURL": {
"type": "string",
"description": "URL for image to use with the content."
},
"imageVerticalOffset": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels."
}
}
},
"template": {
"type": "string",
"const": "menu_message"
},
"testingTriggerContext": {
"type": "string",
"enum": ["app_menu", "pxi_menu"]
}
},
"additionalProperties": true
}

View File

@@ -153,6 +153,7 @@ const MESSAGE_TYPE_LIST = [
"INFOBAR_TELEMETRY", "INFOBAR_TELEMETRY",
"SPOTLIGHT_TELEMETRY", "SPOTLIGHT_TELEMETRY",
"TOAST_NOTIFICATION_TELEMETRY", "TOAST_NOTIFICATION_TELEMETRY",
"MENU_MESSAGE_TELEMETRY",
"AS_ROUTER_TELEMETRY_USER_EVENT", "AS_ROUTER_TELEMETRY_USER_EVENT",
// Admin types // Admin types

View File

@@ -0,0 +1,50 @@
/* 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 https://mozilla.org/MPL/2.0/. */
:host {
margin-inline: var(--arrowpanel-menuitem-margin-inline, var(--space-xsmall));
margin-block: var(--space-xsmall);
--illustration-margin-block-offset: 0px;
}
#container {
display: flex;
position: relative;
flex-direction: column;
background-color: var(--background-color-information);
border-radius: var(--border-radius-small);
padding-block: var(--arrowpanel-menuitem-padding-block, var(--space-small));
padding-inline: var(--arrowpanel-menuitem-padding-inline, var(--space-small));
margin-block-start: var(--space-small);
color: var(--text-color);
}
#close-button {
position: absolute;
top: 0;
inset-inline-end: 0;
padding-block: var(--arrowpanel-menuitem-padding-block, var(--space-small));
padding-inline: var(--arrowpanel-menuitem-padding-inline, var(--space-small));
}
#container:not([has-image]) > #illustration-container {
display: none;
}
#container[has-image] > #illustration-container {
display: flex;
justify-content: center;
margin-block-start: var(--illustration-margin-block-offset);
pointer-events: none;
}
#primary {
font-size: 1.154em;
font-weight: var(--font-weight-bold);
margin-block-end: var(--space-xsmall);
}
#sign-up-button {
margin-block-start: var(--space-medium);
}

View File

@@ -0,0 +1,106 @@
/* 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
/**
* This widget is for a message that can be displayed in panelview menus when
* the user is signed out to encourage them to sign in.
*/
export default class FxAMenuMessage extends MozLitElement {
static shadowRootOptions = {
...MozLitElement.shadowRootOptions,
delegatesFocus: true,
};
static properties = {
imageURL: { type: String },
buttonText: { type: String },
primaryText: { type: String },
secondaryText: { type: String },
};
static queries = {
signUpButton: "#sign-up-button",
closeButton: "#close-button",
};
constructor() {
super();
this.addEventListener(
"keydown",
event => {
let keyCode = event.code;
switch (keyCode) {
case "ArrowLeft":
// Intentional fall-through
case "ArrowRight":
// Intentional fall-through
case "ArrowUp":
// Intentional fall-through
case "ArrowDown": {
if (this.shadowRoot.activeElement === this.signUpButton) {
this.closeButton.focus();
} else {
this.signUpButton.focus();
}
break;
}
}
},
{ capture: true }
);
}
handleClose(event) {
// Keep the menu open by stopping the click event from
// propagating up.
event.stopPropagation();
this.dispatchEvent(new CustomEvent("FxAMenuMessage:Close"), {
bubbles: true,
});
}
handleSignUp() {
this.dispatchEvent(new CustomEvent("FxAMenuMessage:SignUp"), {
bubbles: true,
});
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://browser/content/asrouter/components/fxa-menu-message.css"
/>
<div id="container" ?has-image="${this.imageURL}">
<moz-button
id="close-button"
@click=${this.handleClose}
type="ghost"
iconsrc="chrome://global/skin/icons/close-12.svg"
tabindex="2"
data-l10n-id="fxa-menu-message-close-button"
>
</moz-button>
<div id="illustration-container">
<img id="illustration" role="presentation" src="${this.imageURL}" />
</div>
<div id="primary">${this.primaryText}</div>
<div id="secondary">${this.secondaryText}</div>
<moz-button
id="sign-up-button"
@click=${this.handleSignUp}
type="primary"
tabindex="1"
autofocus
>${this.buttonText}</moz-button
>
</div>
`;
}
}
customElements.define("fxa-menu-message", FxAMenuMessage);

View File

@@ -0,0 +1,44 @@
/* 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/. */
// eslint-disable-next-line import/no-unresolved
import { html } from "lit.all.mjs";
import "chrome://global/content/elements/moz-card.mjs";
import "./fxa-menu-message.mjs";
export default {
title: "Domain-specific UI Widgets/ASRouter/FxA Menu Message",
component: "fxa-menu-message",
argTypes: {},
};
const Template = ({
buttonText,
imageURL,
primaryText,
secondaryText,
imageVerticalOffset,
}) => html`
<moz-card style="width: 22.5rem;">
<fxa-menu-message
buttonText="${buttonText}"
primaryText="${primaryText}"
secondaryText="${secondaryText}"
imageURL="${imageURL}"
style="--illustration-margin-block-offset: ${imageVerticalOffset}px"
>
</fxa-menu-message>
</moz-card>
`;
export const Default = Template.bind({});
Default.args = {
buttonText: "Sign up",
imageURL:
"chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png",
primaryText: "Bounce between devices",
secondaryText:
"Sync and encrypt your bookmarks, passwords, and more on all your devices.",
imageVerticalOffset: -24,
};

View File

@@ -6,6 +6,8 @@ browser.jar:
content/browser/asrouter/asrouter-admin.html (content/asrouter-admin.html) content/browser/asrouter/asrouter-admin.html (content/asrouter-admin.html)
content/browser/asrouter/asrouter-admin.bundle.js (content/asrouter-admin.bundle.js) content/browser/asrouter/asrouter-admin.bundle.js (content/asrouter-admin.bundle.js)
content/browser/asrouter/components/ASRouterAdmin/ASRouterAdmin.css (content/components/ASRouterAdmin/ASRouterAdmin.css) content/browser/asrouter/components/ASRouterAdmin/ASRouterAdmin.css (content/components/ASRouterAdmin/ASRouterAdmin.css)
content/browser/asrouter/components/fxa-menu-message.mjs (content/components/fxa-menu-message/fxa-menu-message.mjs)
content/browser/asrouter/components/fxa-menu-message.css (content/components/fxa-menu-message/fxa-menu-message.css)
content/browser/asrouter/components/remote-text.js (content/components/remote-text.js) content/browser/asrouter/components/remote-text.js (content/components/remote-text.js)
content/browser/asrouter/render.js (content/render.js) content/browser/asrouter/render.js (content/render.js)
content/browser/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json (content-src/schemas/BackgroundTaskMessagingExperiment.schema.json) content/browser/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json (content-src/schemas/BackgroundTaskMessagingExperiment.schema.json)

View File

@@ -43,6 +43,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
InfoBar: "resource:///modules/asrouter/InfoBar.sys.mjs", InfoBar: "resource:///modules/asrouter/InfoBar.sys.mjs",
KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs", KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs",
MacAttribution: "resource:///modules/MacAttribution.sys.mjs", MacAttribution: "resource:///modules/MacAttribution.sys.mjs",
MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
MomentsPageHub: "resource:///modules/asrouter/MomentsPageHub.sys.mjs", MomentsPageHub: "resource:///modules/asrouter/MomentsPageHub.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PanelTestProvider: "resource:///modules/asrouter/PanelTestProvider.sys.mjs", PanelTestProvider: "resource:///modules/asrouter/PanelTestProvider.sys.mjs",
@@ -1478,6 +1479,9 @@ export class _ASRouter {
case "bookmarks_bar_button": case "bookmarks_bar_button":
lazy.BookmarksBarButton.showBookmarksBarButton(browser, message); lazy.BookmarksBarButton.showBookmarksBarButton(browser, message);
break; break;
case "menu_message":
lazy.MenuMessage.showMenuMessage(browser, message, trigger, force);
break;
} }
return { message }; return { message };

View File

@@ -30,6 +30,7 @@ export class ASRouterParentProcessMessageHandler {
case msg.MOMENTS_PAGE_TELEMETRY: case msg.MOMENTS_PAGE_TELEMETRY:
case msg.DOORHANGER_TELEMETRY: case msg.DOORHANGER_TELEMETRY:
case msg.SPOTLIGHT_TELEMETRY: case msg.SPOTLIGHT_TELEMETRY:
case msg.MENU_MESSAGE_TELEMETRY:
case msg.TOAST_NOTIFICATION_TELEMETRY: { case msg.TOAST_NOTIFICATION_TELEMETRY: {
return this.handleTelemetry({ type, data }); return this.handleTelemetry({ type, data });
} }

View File

@@ -16,6 +16,7 @@ export const MESSAGE_TYPE_LIST = [
"INFOBAR_TELEMETRY", "INFOBAR_TELEMETRY",
"SPOTLIGHT_TELEMETRY", "SPOTLIGHT_TELEMETRY",
"TOAST_NOTIFICATION_TELEMETRY", "TOAST_NOTIFICATION_TELEMETRY",
"MENU_MESSAGE_TELEMETRY",
"AS_ROUTER_TELEMETRY_USER_EVENT", "AS_ROUTER_TELEMETRY_USER_EVENT",
// Admin types // Admin types

View File

@@ -0,0 +1,232 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
RemoteL10n: "resource:///modules/asrouter/RemoteL10n.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
UIState: "resource://services-sync/UIState.sys.mjs",
});
export const MenuMessage = {
SOURCES: Object.freeze({
APP_MENU: "app_menu",
PXI_MENU: "pxi_menu",
}),
SHOWING_FXA_MENU_MESSAGE_ATTR: "showing-fxa-menu-message",
async showMenuMessage(browser, message, trigger, force) {
if (!browser) {
return;
}
let win = browser.ownerGlobal;
if (!win || lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
return;
}
let source = trigger?.context?.source || message.testingTriggerContext;
switch (source) {
case MenuMessage.SOURCES.APP_MENU: {
this.showAppMenuMessage(browser, message, force);
break;
}
case MenuMessage.SOURCES.PXI_MENU: {
this.showPxiMenuMessage(browser, message, force);
break;
}
}
},
async showAppMenuMessage(browser, message, force) {
const win = browser.ownerGlobal;
const msgContainer = this.hideAppMenuMessage(browser);
// This version of the browser only supports the fxa_cta version
// of this message in the AppMenu. We also don't draw focus away from any
// existing AppMenuNotifications.
if (
!message ||
message.content.messageType !== "fxa_cta" ||
lazy.AppMenuNotifications.activeNotification
) {
return;
}
// Since we know this is an fxa_cta message, we know that if we're already
// signed in, we don't want to show it in the AppMenu.
if (lazy.UIState.get().status === lazy.UIState.STATUS_SIGNED_IN) {
return;
}
let msgElement = await this.constructFxAMessage(
win,
message,
MenuMessage.SOURCES.APP_MENU
);
msgElement.addEventListener("FxAMenuMessage:Close", () => {
win.PanelUI.mainView.removeAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
);
});
msgContainer.appendChild(msgElement);
win.PanelUI.mainView.setAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR,
message.id
);
if (force) {
win.PanelUI.show();
}
},
hideAppMenuMessage(browser) {
const win = browser.ownerGlobal;
const document = browser.ownerDocument;
const msgContainer = lazy.PanelMultiView.getViewNode(
document,
"appMenu-fxa-menu-message"
);
msgContainer.innerHTML = "";
win.PanelUI.mainView.removeAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
);
return msgContainer;
},
async showPxiMenuMessage(browser, message, force) {
const win = browser.ownerGlobal;
const { document } = win;
const msgContainer = this.hidePxiMenuMessage(browser);
// This version of the browser only supports the fxa_cta version
// of this message in the PXI menu.
if (!message || message.content.messageType !== "fxa_cta") {
return;
}
// Since we know this is an fxa_cta message, we know that if we're already
// signed in, we don't want to show it in the AppMenu.
if (lazy.UIState.get().status === lazy.UIState.STATUS_SIGNED_IN) {
return;
}
let msgElement = await this.constructFxAMessage(
win,
message,
MenuMessage.SOURCES.PXI_MENU
);
let fxaPanelView = lazy.PanelMultiView.getViewNode(document, "PanelUI-fxa");
msgElement.addEventListener("FxAMenuMessage:Close", () => {
fxaPanelView.removeAttribute(MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR);
});
msgContainer.appendChild(msgElement);
fxaPanelView.setAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR,
message.id
);
if (force) {
await win.gSync.toggleAccountPanel(
document.getElementById("fxa-toolbar-menu-button"),
new MouseEvent("mousedown")
);
}
},
hidePxiMenuMessage(browser) {
const document = browser.ownerDocument;
const msgContainer = lazy.PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-message"
);
msgContainer.innerHTML = "";
let fxaPanelView = lazy.PanelMultiView.getViewNode(document, "PanelUI-fxa");
fxaPanelView.removeAttribute(MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR);
return msgContainer;
},
async constructFxAMessage(win, message, source) {
let { document, gBrowser } = win;
win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
const msgElement = document.createElement("fxa-menu-message");
msgElement.imageURL = message.content.imageURL;
msgElement.buttonText = await lazy.RemoteL10n.formatLocalizableText(
message.content.primaryActionText
);
msgElement.primaryText = await lazy.RemoteL10n.formatLocalizableText(
message.content.primaryText
);
msgElement.secondaryText = await lazy.RemoteL10n.formatLocalizableText(
message.content.secondaryText
);
msgElement.dataset.navigableWithTabOnly = "true";
msgElement.style.setProperty(
"--illustration-margin-block-offset",
`${message.content.imageVerticalOffset}px`
);
msgElement.addEventListener("FxAMenuMessage:Close", () => {
msgElement.remove();
this.recordMenuMessageTelemetry("DISMISS", source, message.id);
lazy.SpecialMessageActions.handleAction(
message.content.closeAction,
gBrowser.selectedBrowser
);
});
msgElement.addEventListener("FxAMenuMessage:SignUp", () => {
this.recordMenuMessageTelemetry("CLICK", source, message.id);
// Depending on the source that showed the message, we'll want to set
// a particular entrypoint in the data payload in the event that we're
// opening up the FxA sign-up page.
let clonedPrimaryAction = structuredClone(message.content.primaryAction);
if (source === MenuMessage.SOURCES.APP_MENU) {
clonedPrimaryAction.data.entrypoint = "fxa_app_menu";
} else if (source === MenuMessage.SOURCES.PXI_MENU) {
clonedPrimaryAction.data.entrypoint = "fxa_avatar_menu";
}
lazy.SpecialMessageActions.handleAction(
clonedPrimaryAction,
gBrowser.selectedBrowser
);
});
return msgElement;
},
recordMenuMessageTelemetry(event, source, messageId) {
let ping = {
message_id: messageId,
event,
source,
};
lazy.ASRouter.dispatchCFRAction({
type: "MENU_MESSAGE_TELEMETRY",
data: { action: "menu_message_user_event", ...ping },
});
},
};

View File

@@ -809,6 +809,44 @@ const MESSAGES = () => [
], ],
}, },
}, },
{
id: "FXA_ACCOUNTS_APPMENU_PROTECT_BROWSING_DATA",
template: "menu_message",
content: {
messageType: "fxa_cta",
primaryText: "Bounce between devices",
secondaryText:
"Sync and encrypt your bookmarks, passwords, and more on all your devices.",
primaryActionText: "Sign up",
primaryAction: {
type: "FXA_SIGNIN_FLOW",
data: {
where: "tab",
extraParams: {
utm_source: "firefox-desktop",
utm_medium: "product",
utm_campaign: "some-campaign",
utm_content: "some-content",
},
autoClose: false,
},
},
closeAction: {
type: "BLOCK_MESSAGE",
data: {
id: "FXA_ACCOUNTS_APPMENU_PROTECT_BROWSING_DATA",
},
},
imageURL:
"chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png",
imageVerticalOffset: -24,
},
skip_in_tests: "TODO",
trigger: {
id: "menuOpened",
},
testingTriggerContext: "app_menu",
},
]; ];
export const PanelTestProvider = { export const PanelTestProvider = {

View File

@@ -30,6 +30,7 @@ EXTRA_JS_MODULES.asrouter += [
"modules/FeatureCalloutBroker.sys.mjs", "modules/FeatureCalloutBroker.sys.mjs",
"modules/FeatureCalloutMessages.sys.mjs", "modules/FeatureCalloutMessages.sys.mjs",
"modules/InfoBar.sys.mjs", "modules/InfoBar.sys.mjs",
"modules/MenuMessage.sys.mjs",
"modules/MessagingExperimentConstants.sys.mjs", "modules/MessagingExperimentConstants.sys.mjs",
"modules/MomentsPageHub.sys.mjs", "modules/MomentsPageHub.sys.mjs",
"modules/OnboardingMessageProvider.sys.mjs", "modules/OnboardingMessageProvider.sys.mjs",
@@ -55,6 +56,7 @@ TESTING_JS_MODULES += [
"content-src/templates/CFR/templates/ExtensionDoorhanger.schema.json", "content-src/templates/CFR/templates/ExtensionDoorhanger.schema.json",
"content-src/templates/CFR/templates/InfoBar.schema.json", "content-src/templates/CFR/templates/InfoBar.schema.json",
"content-src/templates/OnboardingMessage/BookmarksBarButton.schema.json", "content-src/templates/OnboardingMessage/BookmarksBarButton.schema.json",
"content-src/templates/OnboardingMessage/MenuMessage.schema.json",
"content-src/templates/OnboardingMessage/Spotlight.schema.json", "content-src/templates/OnboardingMessage/Spotlight.schema.json",
"content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json", "content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json",
"content-src/templates/OnboardingMessage/UpdateAction.schema.json", "content-src/templates/OnboardingMessage/UpdateAction.schema.json",

View File

@@ -65,6 +65,10 @@ async function makeValidators() {
"resource://testing-common/InfoBar.schema.json", "resource://testing-common/InfoBar.schema.json",
{ common: true } { common: true }
), ),
menu_message: await schemaValidatorFor(
"resource://testing-common/MenuMessage.schema.json",
{ common: true }
),
pb_newtab: await schemaValidatorFor( pb_newtab: await schemaValidatorFor(
"resource://testing-common/NewtabPromoMessage.schema.json", "resource://testing-common/NewtabPromoMessage.schema.json",
{ common: true } { common: true }

View File

@@ -27,6 +27,7 @@ add_task(async function test_PanelTestProvider() {
pb_newtab: 2, pb_newtab: 2,
toast_notification: 3, toast_notification: 3,
bookmarks_bar_button: 1, bookmarks_bar_button: 1,
menu_message: 1,
}; };
const EXPECTED_TOTAL_MESSAGE_COUNT = Object.values( const EXPECTED_TOTAL_MESSAGE_COUNT = Object.values(

View File

@@ -2409,6 +2409,14 @@ var CustomizableUIInternal = {
continue; continue;
} }
// Skip out of shadow roots
if (
target.nodeType == target.DOCUMENT_FRAGMENT_NODE &&
target.containingShadowRoot == target
) {
continue;
}
// Break out of the loop immediately for disabled items, as we need to // Break out of the loop immediately for disabled items, as we need to
// keep the menu open in that case. // keep the menu open in that case.
if (target.getAttribute("disabled") == "true") { if (target.getAttribute("disabled") == "true") {

View File

@@ -4,6 +4,8 @@
ChromeUtils.defineESModuleGetters(this, { ChromeUtils.defineESModuleGetters(this, {
AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
}); });
@@ -137,6 +139,7 @@ const PanelUI = {
"appMenu-libraryView" "appMenu-libraryView"
).addEventListener("command", this._onLibraryCommand); ).addEventListener("command", this._onLibraryCommand);
this.mainView.addEventListener("command", this); this.mainView.addEventListener("command", this);
this.mainView.addEventListener("ViewShowing", this._onMainViewShow);
this._eventListenersAdded = true; this._eventListenersAdded = true;
}, },
@@ -216,6 +219,14 @@ const PanelUI = {
return; return;
} }
if (ASRouter.initialized) {
await ASRouter.sendTriggerMessage({
browser: gBrowser.selectedBrowser,
id: "menuOpened",
context: { source: MenuMessage.SOURCES.APP_MENU },
});
}
let domEvent = null; let domEvent = null;
if (aEvent && aEvent.type != "command") { if (aEvent && aEvent.type != "command") {
domEvent = aEvent; domEvent = aEvent;
@@ -284,6 +295,7 @@ const PanelUI = {
this._updatePanelButton(aEvent.target); this._updatePanelButton(aEvent.target);
if (aEvent.type == "popuphidden") { if (aEvent.type == "popuphidden") {
CustomizableUI.removePanelCloseListeners(this.panel); CustomizableUI.removePanelCloseListeners(this.panel);
MenuMessage.hideAppMenuMessage(gBrowser.selectedBrowser);
} }
break; break;
case "mousedown": case "mousedown":
@@ -619,6 +631,22 @@ const PanelUI = {
} }
}, },
_onMainViewShow(event) {
let panelview = event.target;
let messageId = panelview.getAttribute(
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
);
if (messageId) {
MenuMessage.recordMenuMessageTelemetry(
"IMPRESSION",
MenuMessage.SOURCES.APP_MENU,
messageId
);
let message = ASRouter.getMessageById(messageId);
ASRouter.addImpression(message);
}
},
_onHelpViewShow() { _onHelpViewShow() {
// Call global menu setup function // Call global menu setup function
buildHelpMenu(); buildHelpMenu();

View File

@@ -504,6 +504,9 @@ export class TelemetryFeed {
case "onboarding_user_event": case "onboarding_user_event":
event = await this.applyOnboardingPolicy(event, session); event = await this.applyOnboardingPolicy(event, session);
break; break;
case "menu_message_user_event":
event = await this.applyMenuMessagePolicy(event);
break;
case "asrouter_undesired_event": case "asrouter_undesired_event":
event = this.applyUndesiredEventPolicy(event); event = this.applyUndesiredEventPolicy(event);
break; break;
@@ -570,6 +573,13 @@ export class TelemetryFeed {
return { ping, pingType: "toast_notification" }; return { ping, pingType: "toast_notification" };
} }
async applyMenuMessagePolicy(ping) {
ping.client_id = await this.telemetryClientId;
ping.browser_session_id = lazy.browserSessionId;
delete ping.action;
return { ping, pingType: "menu" };
}
/** /**
* Per Bug 1484035, Moments metrics comply with following policies: * Per Bug 1484035, Moments metrics comply with following policies:
* 1). In release, it collects impression_id, and treats bucket_id as message_id * 1). In release, it collects impression_id, and treats bucket_id as message_id
@@ -1104,6 +1114,8 @@ export class TelemetryFeed {
// Intentional fall-through // Intentional fall-through
case msg.TOAST_NOTIFICATION_TELEMETRY: case msg.TOAST_NOTIFICATION_TELEMETRY:
// Intentional fall-through // Intentional fall-through
case msg.MENU_MESSAGE_TELEMETRY:
// Intentional fall-through
case msg.AS_ROUTER_TELEMETRY_USER_EVENT: case msg.AS_ROUTER_TELEMETRY_USER_EVENT:
this.handleASRouterUserEvent(action); this.handleASRouterUserEvent(action);
break; break;

View File

@@ -23,6 +23,8 @@ module.exports = {
`${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`, `${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`,
// about:logins components stories // about:logins components stories
`${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`, `${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`,
// ASRouter components stories
`${projectRoot}/browser/components/asrouter/content/**/*.stories.mjs`,
// Backup components stories // Backup components stories
`${projectRoot}/browser/components/backup/content/**/*.stories.mjs`, `${projectRoot}/browser/components/backup/content/**/*.stories.mjs`,
// Reader View components stories // Reader View components stories

View File

@@ -265,3 +265,9 @@ root-certificate-succession-infobar-primary-button =
root-certificate-succession-infobar-secondary-button = root-certificate-succession-infobar-secondary-button =
.label = Later .label = Later
.accesskey = L .accesskey = L
## FxA Menu Message variants
fxa-menu-message-close-button =
.title = Close
.aria-label = Close

View File

@@ -648,6 +648,22 @@ toolbarbutton[constrain-size="true"][cui-areatype="panel"] > .toolbarbutton-badg
/* Handle different UI states. */ /* Handle different UI states. */
#appMenu-mainView:not([showing-fxa-menu-message]) #appMenu-fxa-menu-message,
#PanelUI-fxa:not([showing-fxa-menu-message]) #PanelUI-fxa-menu-message {
display: none;
}
#appMenu-mainView[showing-fxa-menu-message] {
& #appMenu-fxa-status2,
& #appMenu-fxa-separator {
display: none;
}
}
#PanelUI-fxa[showing-fxa-menu-message] #fxa-manage-account-button {
display: none
}
:root:not([fxastatus="signedin"]) #PanelUI-fxa-menu-syncnow-button { :root:not([fxastatus="signedin"]) #PanelUI-fxa-menu-syncnow-button {
display: none; display: none;
} }