Bug 1867098: FxA PXI Toolbar experiment r=teshaq,desktop-theme-reviewers,flod,dao

Differential Revision: https://phabricator.services.mozilla.com/D194916
This commit is contained in:
Sammy Khamis 2023-12-11 21:53:53 +00:00
parent fbf7297f82
commit dc6400347b
10 changed files with 383 additions and 12 deletions

View File

@ -2438,6 +2438,7 @@ pref("browser.toolbars.bookmarks.showInPrivateBrowsing", false);
pref("identity.fxaccounts.toolbar.enabled", true);
pref("identity.fxaccounts.toolbar.accessed", false);
pref("identity.fxaccounts.toolbar.defaultVisible", false);
pref("identity.fxaccounts.toolbar.pxiToolbarEnabled", false);
// Check bundled omni JARs for corruption.
pref("corroborator.enabled", true);

View File

@ -559,6 +559,7 @@
data-l10n-id="appmenuitem-fxa-sign-in"/>
<label id="fxa-menu-header-description"
crop="end"
hidden="true"
data-l10n-id="fxa-menu-turn-on-sync"/>
</vbox>
</toolbarbutton>
@ -579,6 +580,7 @@
crop="end"/>
</hbox>
</toolbarbutton>
<vbox id="PanelUI-signedin-panel" hidden="true">
<toolbarbutton id="PanelUI-fxa-menu-setup-sync-button"
class="subviewbutton"
data-l10n-id="appmenu-fxa-setup-sync"
@ -600,15 +602,59 @@
data-l10n-id="fxa-menu-sync-settings"
hidden="true"
oncommand="gSync.openPrefsFromFxaMenu('sync_settings', this);"/>
<toolbarseparator id="PanelUI-sign-out-separator"/>
<toolbarseparator id="PanelUI-sign-out-separator" />
<toolbarbutton id="PanelUI-fxa-menu-account-signout-button"
class="subviewbutton"
data-l10n-id="fxa-menu-sign-out"
oncommand="gSync.disconnect();"/>
<toolbarseparator id="PanelUI-remote-tabs-separator"/>
oncommand="gSync.disconnect();"
hidden="true"/>
</vbox>
<!-- updateCTAPanel will control if we show this panel -->
<vbox id="PanelUI-fxa-pxi-cta-menu">
<toolbarbutton id="PanelUI-fxa-menu-sync-button" class="subviewbutton subviewbutton-iconic"
oncommand="gSync.openPrefsFromFxaMenu('sync_cta', this);">
<vbox flex="1">
<label id="fxa-menu-header-title" crop="end" data-l10n-id="sync-menu-title" />
<label id="cta-menu-header-description" crop="end" data-l10n-id="sync-menu-description" />
</vbox>
<toolbarseparator id="PanelUI-remote-tabs-separator" />
</toolbarbutton>
<toolbarseparator id="PanelUI-remote-tabs-separator" />
<toolbarbutton id="PanelUI-fxa-menu-monitor-button" class="subviewbutton subviewbutton-iconic"
oncommand="gSync.openMonitorLink(this)">
<vbox flex="1">
<hbox>
<image class="PanelUI-fxa-menu-monitor-button ctaMenuLogo" role="presentation" />
<label id="fxa-menu-header-title" crop="end" data-l10n-id="pxi-menu-monitor-title" />
</hbox>
<label id="cta-menu-header-description" crop="end" data-l10n-id="pxi-menu-monitor-description" />
</vbox>
</toolbarbutton>
<toolbarbutton id="PanelUI-fxa-menu-relay-button" class="subviewbutton subviewbutton-iconic"
oncommand="gSync.openRelayLink(this)">
<vbox flex="1">
<hbox>
<image class="PanelUI-fxa-menu-relay-button ctaMenuLogo" role="presentation" />
<label id="fxa-menu-header-title" crop="end" data-l10n-id="pxi-menu-relay-title" />
</hbox>
<label id="cta-menu-header-description" crop="end" data-l10n-id="pxi-menu-relay-description" />
</vbox>
</toolbarbutton>
<toolbarbutton id="PanelUI-fxa-menu-vpn-button" class="subviewbutton subviewbutton-iconic"
oncommand="gSync.openVPNLink(this)">
<vbox flex="1">
<hbox>
<image class="PanelUI-fxa-menu-vpn-button ctaMenuLogo" role="presentation" />
<label id="fxa-menu-header-title" crop="end" data-l10n-id="pxi-menu-vpn-title" />
</hbox>
<label id="cta-menu-header-description" crop="end" data-l10n-id="pxi-menu-vpn-description" />
</vbox>
</toolbarbutton>
</vbox>
<deck id="PanelUI-fxa-remotetabs-deck">
<!-- Sync is ready to Sync and the "tabs" engine is enabled -->
<vbox id="PanelUI-fxa-remotetabs-tabspane">
<toolbarseparator id="PanelUI-remote-tabs-separator" />
<vbox id="PanelUI-fxa-remotetabs-tabslist"
notabsforclientlabel="appmenu-remote-tabs-notabs"
/>

View File

@ -5,6 +5,14 @@
// This file is loaded into the browser window scope.
/* eslint-env mozilla/browser-window */
const {
FX_MONITOR_OAUTH_CLIENT_ID,
FX_RELAY_OAUTH_CLIENT_ID,
VPN_OAUTH_CLIENT_ID,
} = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsCommon.sys.mjs"
);
const { UIState } = ChromeUtils.importESModule(
"resource://services-sync/UIState.sys.mjs"
);
@ -348,6 +356,8 @@ var gSync = {
"browser/appmenu.ftl",
"browser/sync.ftl",
"toolkit/branding/accounts.ftl",
// untranslated FTL
"preview/appmenu.ftl",
],
true
));
@ -406,6 +416,11 @@ var gSync = {
"FXA_ENABLED",
"identity.fxaccounts.enabled"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"PXI_TOOLBAR_ENABLED",
"identity.fxaccounts.toolbar.pxiToolbarEnabled"
);
},
maybeUpdateUIState() {
@ -492,6 +507,13 @@ var gSync = {
fxaPanelView.addEventListener("ViewShowing", this);
fxaPanelView.addEventListener("ViewHiding", this);
// If the experiment is enabled, we'll need to update the panels
// to show some different text to the user
if (this.PXI_TOOLBAR_ENABLED) {
this.updateFxAPanel(UIState.get());
this.updateCTAPanel();
}
this._initialized = true;
},
@ -536,6 +558,14 @@ var gSync = {
);
syncPrefsButtonEl.hidden = !UIState.get().syncEnabled;
// We should ensure that we do not show the sign out button
// if the user is not signed in
const signOutButtonEl = PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-account-signout-button"
);
signOutButtonEl.hidden = !this.isSignedIn;
panelview.syncedTabsPanelList = new SyncedTabsPanelList(
panelview,
PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-deck"),
@ -580,6 +610,7 @@ var gSync = {
this.updateSyncButtonsTooltip(state);
this.updateSyncStatus(state);
this.updateFxAPanel(state);
this.updateCTAPanel(state);
// Ensure we have something in the device list in the background.
this.ensureFxaDevices();
},
@ -786,15 +817,29 @@ var gSync = {
entrypoint_variation: fxaButtonVisibilityExperiment.branch.slug,
};
}
let panel =
anchor.id == "appMenu-fxa-label2"
? PanelMultiView.getViewNode(document, "PanelUI-fxa")
: undefined;
this.openFxAEmailFirstPageFromFxaMenu(panel, extraParams);
PanelUI.hide();
// If we're signed out but have the PXI pref enabled
// we should show the PXI panel instead of taking the user
// straight to FxA sign-in
if (this.PXI_TOOLBAR_ENABLED) {
this.updateCTAPanel();
PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
} else {
let panel =
anchor.id == "appMenu-fxa-label2"
? PanelMultiView.getViewNode(document, "PanelUI-fxa")
: undefined;
this.openFxAEmailFirstPageFromFxaMenu(panel, extraParams);
PanelUI.hide();
}
return;
}
// If the user is signed in and we have the PXI pref enabled then add
// the pxi panel to the existing toolbar
if (this.PXI_TOOLBAR_ENABLED) {
this.updateCTAPanel();
} else {
PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
}
if (!gFxaToolbarAccessed) {
Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true);
@ -856,19 +901,33 @@ var gSync = {
"fxa-manage-account-button"
);
const signedInContainer = PanelMultiView.getViewNode(
document,
"PanelUI-signedin-panel"
);
cadButtonEl.setAttribute("disabled", true);
syncNowButtonEl.hidden = true;
signedInContainer.hidden = true;
fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav");
fxaMenuAccountButtonEl.removeAttribute("closemenu");
syncSetupButtonEl.removeAttribute("hidden");
let headerTitleL10nId = "appmenuitem-fxa-sign-in";
let headerTitleL10nId = this.PXI_TOOLBAR_ENABLED
? "appmenuitem-moz-accounts-sign-in"
: "appmenuitem-fxa-sign-in";
let headerDescription;
if (state.status === UIState.STATUS_NOT_CONFIGURED) {
mainWindowEl.style.removeProperty("--avatar-image-url");
headerDescription = this.fluentStrings.formatValueSync(
"appmenu-fxa-signed-in-label"
);
// Signed out, expeirment enabled is the only state we want to hide the
// header description, so we make it empty and check for that when setting
// the value
if (this.PXI_TOOLBAR_ENABLED) {
headerDescription = "";
}
} else if (state.status === UIState.STATUS_LOGIN_FAILED) {
stateValue = "login-failed";
headerTitleL10nId = "account-disconnected2";
@ -895,6 +954,8 @@ var gSync = {
mainWindowEl.style.removeProperty("--avatar-image-url");
};
img.src = state.avatarURL;
signedInContainer.hidden = false;
menuHeaderDescriptionEl.hidden = false;
} else {
mainWindowEl.style.removeProperty("--avatar-image-url");
}
@ -917,6 +978,8 @@ var gSync = {
menuHeaderTitleEl.value =
this.fluentStrings.formatValueSync(headerTitleL10nId);
// If we description is empty, we hide it
menuHeaderDescriptionEl.hidden = !headerDescription;
menuHeaderDescriptionEl.value = headerDescription;
// We remove the data-l10n-id attribute here to prevent the node's value
// attribute from being overwritten by Fluent when the panel is moved
@ -1959,6 +2022,95 @@ var gSync = {
}
},
// This should only be shown if we have enabled the pxiPanel via
// an experiment or explicitly through prefs
updateCTAPanel() {
const mainPanelEl = PanelMultiView.getViewNode(
document,
"PanelUI-fxa-pxi-cta-menu"
);
const syncCtaEl = PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-sync-button"
);
// If we're not in the experiment then we do not enable this at all
if (!this.PXI_TOOLBAR_ENABLED) {
// If we've previously shown this but got disabled
// we should ensure we hide the panel
mainPanelEl.hidden = true;
return;
}
// If we're already signed in an syncing, we shouldn't show the sync CTA
syncCtaEl.hidden = this.isSignedIn;
// ensure the container is visible
mainPanelEl.hidden = false;
},
async openMonitorLink(panel) {
this.emitFxaToolbarTelemetry("monitor_cta", panel);
await this.openPXILink(
FX_MONITOR_OAUTH_CLIENT_ID,
new URL("https://monitor.firefox.com"),
new URL("https://monitor.firefox.com/user/breaches")
);
},
async openRelayLink(panel) {
this.emitFxaToolbarTelemetry("relay_cta", panel);
await this.openPXILink(
FX_RELAY_OAUTH_CLIENT_ID,
new URL("https://relay.firefox.com"),
new URL("https://relay.firefox.com/accounts/profile")
);
},
async openVPNLink(panel) {
this.emitFxaToolbarTelemetry("vpn_cta", panel);
await this.openPXILink(
VPN_OAUTH_CLIENT_ID,
new URL("https://www.mozilla.org/en-US/products/vpn/"),
new URL("https://www.mozilla.org/en-US/products/vpn/")
);
},
// A generic opening based on
async openPXILink(clientId, defaultUrl, signedInUrl) {
const params = {
utm_medium: "firefox-desktop",
utm_source: "toolbar",
utm_campaign: "discovery",
};
const pxiSearchParams = new URLSearchParams(params);
if (!this.isSignedIn) {
// Add the base params + not signed in
defaultUrl.search = pxiSearchParams.toString();
defaultUrl.searchParams.append("utm_content", "notsignedin");
this.openLink(defaultUrl);
PanelUI.hide();
return;
}
// Note: This is a network call
let attachedClients = await fxAccounts.listAttachedOAuthClients();
// If we have at least one client based on clientId passed in
let hasPXIClient = attachedClients.some(c => !!c.id && c.id === clientId);
const url = hasPXIClient ? signedInUrl : defaultUrl;
// Add base params + signed in
url.search = pxiSearchParams.toString();
url.searchParams.append("utm_content", "signedIn");
this.openLink(url);
PanelUI.hide();
},
openLink(url) {
switchToTabHavingURI(url, true, { replaceQueryString: true });
},
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",

View File

@ -97,6 +97,7 @@
<link rel="localization" href="preview/interventions.ftl" />
<link rel="localization" href="browser/shopping.ftl"/>
<link rel="localization" href="preview/shopping.ftl"/>
<link rel="localization" href="preview/appmenu.ftl" />
<title data-l10n-id="browser-main-window-title"></title>

View File

@ -581,6 +581,131 @@ add_task(
}
);
// If the PXI experiment is enabled, we need to ensure we can see the CTAs when signed out
add_task(async function test_experiment_ui_state_unconfigured() {
await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
// The experiment enables this bool, found in FeatureManifest.yaml
Services.prefs.setBoolPref(
"identity.fxaccounts.toolbar.pxiToolbarEnabled",
true
);
let state = {
status: UIState.STATUS_NOT_CONFIGURED,
};
gSync.updateAllUI(state);
checkMenuBarItem("sync-setup");
checkFxAAvatar("not_configured");
let expectedLabel = gSync.fluentStrings.formatValueSync(
"appmenuitem-moz-accounts-sign-in"
);
await openMainPanel();
checkFxaToolbarButtonPanel({
headerTitle: expectedLabel,
headerDescription: "",
enabledItems: [
"PanelUI-fxa-pxi-cta-menu",
"PanelUI-fxa-menu-sync-button",
"PanelUI-fxa-menu-monitor-button",
"PanelUI-fxa-menu-relay-button",
"PanelUI-fxa-menu-vpn-button",
],
disabledItems: [],
hiddenItems: [
"PanelUI-fxa-menu-syncnow-button",
"PanelUI-fxa-menu-sync-prefs-button",
],
});
// Revert the pref at the end of the test
Services.prefs.setBoolPref(
"identity.fxaccounts.toolbar.pxiToolbarEnabled",
false
);
await closeTabAndMainPanel();
});
// Ensure we can see the regular signed in flow + the extra PXI CTAs when
// the experiment is enabled
add_task(async function test_experiment_ui_state_signedin() {
await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/");
// The experiment enables this bool, found in FeatureManifest.yaml
Services.prefs.setBoolPref(
"identity.fxaccounts.toolbar.pxiToolbarEnabled",
true
);
const relativeDateAnchor = new Date();
let state = {
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
email: "foo@bar.com",
displayName: "Foo Bar",
avatarURL: "https://foo.bar",
lastSync: new Date(),
syncing: false,
};
const origRelativeTimeFormat = gSync.relativeTimeFormat;
gSync.relativeTimeFormat = {
formatBestUnit(date) {
return origRelativeTimeFormat.formatBestUnit(date, {
now: relativeDateAnchor,
});
},
};
gSync.updateAllUI(state);
await openFxaPanel();
checkMenuBarItem("sync-syncnowitem");
checkPanelHeader();
checkFxaToolbarButtonPanel({
headerTitle: "Manage account",
headerDescription: state.displayName,
enabledItems: [
"PanelUI-fxa-menu-sendtab-button",
"PanelUI-fxa-menu-connect-device-button",
"PanelUI-fxa-menu-syncnow-button",
"PanelUI-fxa-menu-sync-prefs-button",
"PanelUI-fxa-menu-account-signout-button",
"PanelUI-fxa-pxi-cta-menu",
"PanelUI-fxa-menu-sync-button",
"PanelUI-fxa-menu-monitor-button",
"PanelUI-fxa-menu-relay-button",
"PanelUI-fxa-menu-vpn-button",
],
disabledItems: [],
hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"],
});
checkFxAAvatar("signedin");
gSync.relativeTimeFormat = origRelativeTimeFormat;
await closeFxaPanel();
await openMainPanel();
checkPanelUIStatusBar({
description: "Foo Bar",
titleHidden: true,
hideFxAText: true,
});
// Revert the pref at the end of the test
Services.prefs.setBoolPref(
"identity.fxaccounts.toolbar.pxiToolbarEnabled",
false
);
await closeTabAndMainPanel();
});
function checkPanelUIStatusBar({
description,
title,

View File

@ -14,6 +14,7 @@
preview/enUS-searchFeatures.ftl (../components/urlbar/content/enUS-searchFeatures.ftl)
preview/shopping.ftl (../components/shopping/content/shopping.ftl)
preview/onboarding.ftl (../components/aboutwelcome/content/onboarding.ftl)
preview/appmenu.ftl (../../services/fxaccounts/content/appmenu.ftl)
browser (%browser/**/*.ftl)
@AB_CD@.jar:

View File

@ -752,7 +752,11 @@ toolbarbutton[constrain-size="true"][cui-areatype="panel"] > .toolbarbutton-badg
#fxa-menu-header-description {
color: var(--text-color-deemphasized);
font-weight: 600;
font-weight: var(--font-weight-bold);
}
#cta-menu-header-description {
color: var(--text-color-deemphasized);
}
#PanelUI-appMenu-fxa-label-last-synced {
@ -765,10 +769,24 @@ toolbarbutton[constrain-size="true"][cui-areatype="panel"] > .toolbarbutton-badg
list-style-image: url(chrome://browser/skin/fxa/send.svg);
}
.ctaMenuLogo {
width: 16px;
height: 16px;
margin-inline-start: 5px;
}
#PanelUI-fxa-menu-monitor-button {
list-style-image: url(chrome://browser/skin/fxa/monitor.svg);
}
#PanelUI-fxa-menu-relay-button {
list-style-image: url(chrome://browser/content/logos/relay.svg);
}
#PanelUI-fxa-menu-vpn-button {
list-style-image: url(chrome://browser/content/logos/vpn-dark.svg);
}
:root:not([fxastatus="signedin"]) #PanelUI-fxa-menu-connect-device-button {
color: var(--panel-disabled-color);
}

View File

@ -95,6 +95,8 @@ export let DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY =
// OAuth metadata for other Firefox-related services that we might need to know about
// in order to provide an enhanced user experience.
export let FX_MONITOR_OAUTH_CLIENT_ID = "802d56ef2a9af9fa";
export let FX_RELAY_OAUTH_CLIENT_ID = "9ebfe2c2f9ea3c58";
export let VPN_OAUTH_CLIENT_ID = "e6eb0d1e856335fc";
// UI Requests.
export let UI_REQUEST_SIGN_IN_FLOW = "signInFlow";

View File

@ -0,0 +1,19 @@
# 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/.
### This file is not in a locales directory to prevent it from
### being translated as the feature is still in heavy development
### and strings are likely to change often.
-mozaccount-brand-name = Mozilla account
appmenuitem-moz-accounts-sign-in = Sign in to your { -mozaccount-brand-name }
sync-menu-title = Sync
sync-menu-description = Access your web anywhere
pxi-menu-monitor-title = { -monitor-brand-short-name }
pxi-menu-monitor-description = Get data breach alerts
pxi-menu-relay-title = { -relay-brand-short-name }
pxi-menu-relay-description = Mask your real email and phone
pxi-menu-vpn-title = { -mozilla-vpn-brand-name }
pxi-menu-vpn-description = Protect your online activity

View File

@ -1490,6 +1490,12 @@ fxaButtonVisibility:
description: True if the Firefox Accounts toolbar button should be visible when not signed in.
type: boolean
setPref: identity.fxaccounts.toolbar.defaultVisible
pxiToolbarEnabled:
description: >-
True if we're enabling the PXI dropdown menu for the FxA toolbar button instead of
taking the user straight to login
type: boolean
setPref: identity.fxaccounts.toolbar.pxiToolbarEnabled
legacyHeartbeat:
description: Normandy Heartbeat exposed to Nimbus