diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js index 83de8791a68d..516c7cc0e444 100644 --- a/browser/base/content/browser-siteProtections.js +++ b/browser/base/content/browser-siteProtections.js @@ -7,10 +7,8 @@ ChromeUtils.defineESModuleGetters(this, { ContentBlockingAllowList: "resource://gre/modules/ContentBlockingAllowList.sys.mjs", -}); - -XPCOMUtils.defineLazyModuleGetters(this, { - ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm", + SpecialMessageActions: + "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", }); XPCOMUtils.defineLazyServiceGetter( @@ -1657,6 +1655,13 @@ var gProtectionsHandler = { () => this.maybeSetMilestoneCounterText() ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "protectionsPanelMessageSeen", + "browser.protections_panel.infoMessage.seen", + false + ); + for (let blocker of Object.values(this.blockers)) { if (blocker.init) { blocker.init(); @@ -1813,7 +1818,7 @@ var gProtectionsHandler = { // Insert the info message if needed. This will be shown once and then // remain collapsed. - ToolbarPanelHub.insertProtectionPanelMessage(event); + this._insertProtectionsPanelInfoMessage(event); if (!event.target.hasAttribute("toast")) { Services.telemetry.recordEvent( @@ -2693,4 +2698,188 @@ var gProtectionsHandler = { this._earliestRecordedDate = date; } }, + + _sendUserEventTelemetry(event, value = null, options = {}) { + // Only send telemetry for non private browsing windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + Services.telemetry.recordEvent( + "security.ui.protectionspopup", + event, + "protectionspopup_cfr", + value, + options + ); + } + }, + + /** + * Dispatch the action defined in the message and user telemetry event. + */ + _dispatchUserAction(message) { + let url; + try { + // Set platform specific path variables for SUMO articles + url = Services.urlFormatter.formatURL(message.content.cta_url); + } catch (e) { + console.error(e); + url = message.content.cta_url; + } + SpecialMessageActions.handleAction( + { + type: message.content.cta_type, + data: { + args: url, + where: message.content.cta_where || "tabshifted", + }, + }, + window.browser + ); + + this._sendUserEventTelemetry("click", "learn_more_link", { + message: message.id, + }); + }, + + /** + * Attach event listener to dispatch message defined action. + */ + _attachCommandListener(element, message) { + // Add event listener for `mouseup` not to overlap with the + // `mousedown` & `click` events dispatched from PanelMultiView.sys.mjs + // https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837 + element.addEventListener("mouseup", () => { + this._dispatchUserAction(message); + }); + element.addEventListener("keyup", e => { + if (e.key === "Enter" || e.key === " ") { + this._dispatchUserAction(message); + } + }); + }, + + /** + * Inserts a message into the Protections Panel. The message is visible once + * and afterwards set in a collapsed state. It can be shown again using the + * info button in the panel header. + */ + _insertProtectionsPanelInfoMessage(event) { + // const PROTECTIONS_PANEL_INFOMSG_PREF = + // "browser.protections_panel.infoMessage.seen"; + const message = { + id: "PROTECTIONS_PANEL_1", + content: { + title: { string_id: "cfr-protections-panel-header" }, + body: { string_id: "cfr-protections-panel-body" }, + link_text: { string_id: "cfr-protections-panel-link-text" }, + cta_url: `${Services.urlFormatter.formatURLPref( + "app.support.baseURL" + )}etp-promotions?as=u&utm_source=inproduct`, + cta_type: "OPEN_URL", + }, + }; + + const doc = event.target.ownerDocument; + const container = doc.getElementById("messaging-system-message-container"); + const infoButton = doc.getElementById("protections-popup-info-button"); + const panelContainer = doc.getElementById("protections-popup"); + const toggleMessage = () => { + const learnMoreLink = doc.querySelector( + "#messaging-system-message-container .text-link" + ); + if (learnMoreLink) { + container.toggleAttribute("disabled"); + infoButton.toggleAttribute("checked"); + panelContainer.toggleAttribute("infoMessageShowing"); + learnMoreLink.disabled = !learnMoreLink.disabled; + } + // If the message panel is opened, send impression telemetry + if (panelContainer.hasAttribute("infoMessageShowing")) { + this._sendUserEventTelemetry("open", "impression", { + message: message.id, + }); + } + }; + if (!container.childElementCount) { + const messageEl = this._createHeroElement(doc, message); + container.appendChild(messageEl); + infoButton.addEventListener("click", toggleMessage); + } + // Message is collapsed by default. If it was never shown before we want + // to expand it + if ( + !this.protectionsPanelMessageSeen && + container.hasAttribute("disabled") + ) { + toggleMessage(message); + } + // Save state that we displayed the message + if (!this.protectionsPanelMessageSeen) { + Services.prefs.setBoolPref( + "browser.protections_panel.infoMessage.seen", + true + ); + } + // Collapse the message after the panel is hidden so we don't get the + // animation when opening the panel + panelContainer.addEventListener( + "popuphidden", + () => { + if ( + this.protectionsPanelMessageSeen && + !container.hasAttribute("disabled") + ) { + toggleMessage(message); + } + }, + { + once: true, + } + ); + }, + + _createElement(doc, elem, options = {}) { + const node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem); + if (options.classList) { + node.classList.add(options.classList); + } + if (options.content) { + doc.l10n.setAttributes(node, options.content.string_id); + } + return node; + }, + + _createHeroElement(doc, message) { + const messageEl = this._createElement(doc, "div"); + messageEl.setAttribute("id", "protections-popup-message"); + messageEl.classList.add("whatsNew-hero-message"); + const wrapperEl = this._createElement(doc, "div"); + wrapperEl.classList.add("whatsNew-message-body"); + messageEl.appendChild(wrapperEl); + + wrapperEl.appendChild( + this._createElement(doc, "h2", { + classList: "whatsNew-message-title", + content: message.content.title, + }) + ); + + wrapperEl.appendChild( + this._createElement(doc, "p", { content: message.content.body }) + ); + + if (message.content.link_text) { + let linkEl = this._createElement(doc, "a", { + classList: "text-link", + content: message.content.link_text, + }); + + linkEl.disabled = true; + wrapperEl.appendChild(linkEl); + this._attachCommandListener(linkEl, message); + } else { + this._attachCommandListener(wrapperEl, message); + } + + return messageEl; + }, }; diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI.js b/browser/base/content/test/protectionsUI/browser_protectionsUI.js index 10ef13210220..c95bcd6db5c0 100644 --- a/browser/base/content/test/protectionsUI/browser_protectionsUI.js +++ b/browser/base/content/test/protectionsUI/browser_protectionsUI.js @@ -47,6 +47,65 @@ async function clickToggle(toggle) { await changed; } +add_task(async function testPanelInfoMessage() { + const PROTECTIONS_PANEL_INFOMSG_PREF = + "browser.protections_panel.infoMessage.seen"; + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TRACKING_PAGE + ); + // Set the infomessage pref to ensure the message is displayed every time + Services.prefs.setBoolPref(PROTECTIONS_PANEL_INFOMSG_PREF, false); + + await openProtectionsPanel(); + + await BrowserTestUtils.waitForMutationCondition( + gProtectionsHandler._protectionsPopup, + { attributes: true, attributeFilter: ["infoMessageShowing"] }, + () => + !gProtectionsHandler._protectionsPopup.hasAttribute("infoMessageShowing") + ); + + // Test that the info message is displayed when the panel opens + let container = document.getElementById("messaging-system-message-container"); + let message = document.getElementById("protections-popup-message"); + let learnMoreLink = document.querySelector( + "#messaging-system-message-container .text-link" + ); + + // Check the visibility of the info message. + ok( + BrowserTestUtils.is_visible(container), + "The message container should exist." + ); + + ok(BrowserTestUtils.is_visible(message), "The message should be visible."); + + ok(BrowserTestUtils.is_visible(learnMoreLink), "The link should be visible."); + + // Check telemetry for the info message + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ).parent; + let messageEvents = events.filter( + e => + e[1] == "security.ui.protectionspopup" && + e[2] == "open" && + e[3] == "protectionspopup_cfr" && + e[4] == "impression" + ); + is( + messageEvents.length, + 1, + "recorded telemetry for showing the info message" + ); + + Services.telemetry.clearEvents(); + BrowserTestUtils.removeTab(tab); +}); + add_task(async function testToggleSwitch() { let tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js index 6bbda62fdea3..4dcdaaa68f02 100644 --- a/browser/components/newtab/karma.mc.config.js +++ b/browser/components/newtab/karma.mc.config.js @@ -170,6 +170,12 @@ module.exports = function (config) { functions: 0, branches: 0, }, + "lib/ToolbarPanelHub.jsm": { + statements: 88, + lines: 88, + functions: 94, + branches: 84, + }, "lib/*.jsm": { statements: 100, lines: 100, diff --git a/browser/components/newtab/lib/ToolbarPanelHub.jsm b/browser/components/newtab/lib/ToolbarPanelHub.jsm index 7a6638f9f399..e86139268744 100644 --- a/browser/components/newtab/lib/ToolbarPanelHub.jsm +++ b/browser/components/newtab/lib/ToolbarPanelHub.jsm @@ -50,8 +50,6 @@ class _ToolbarPanelHub { this._hideAppmenuButton = this._hideAppmenuButton.bind(this); this._showToolbarButton = this._showToolbarButton.bind(this); this._hideToolbarButton = this._hideToolbarButton.bind(this); - this.insertProtectionPanelMessage = - this.insertProtectionPanelMessage.bind(this); this.state = {}; this._initialized = false; @@ -511,73 +509,6 @@ class _ToolbarPanelHub { } } - /** - * Inserts a message into the Protections Panel. The message is visible once - * and afterwards set in a collapsed state. It can be shown again using the - * info button in the panel header. - */ - async insertProtectionPanelMessage(event) { - const win = event.target.ownerGlobal; - this.maybeInsertFTL(win); - - const doc = event.target.ownerDocument; - const container = doc.getElementById("messaging-system-message-container"); - const infoButton = doc.getElementById("protections-popup-info-button"); - const panelContainer = doc.getElementById("protections-popup"); - const toggleMessage = () => { - const learnMoreLink = doc.querySelector( - "#messaging-system-message-container .text-link" - ); - if (learnMoreLink) { - container.toggleAttribute("disabled"); - infoButton.toggleAttribute("checked"); - panelContainer.toggleAttribute("infoMessageShowing"); - learnMoreLink.disabled = !learnMoreLink.disabled; - } - }; - if (!container.childElementCount) { - const message = await this._getMessages({ - template: "protections_panel", - triggerId: "protectionsPanelOpen", - }); - if (message) { - const messageEl = this._createHeroElement(win, doc, message); - container.appendChild(messageEl); - infoButton.addEventListener("click", toggleMessage); - this.sendUserEventTelemetry(win, "IMPRESSION", message); - } - } - // Message is collapsed by default. If it was never shown before we want - // to expand it - if ( - !this.state.protectionPanelMessageSeen && - container.hasAttribute("disabled") - ) { - toggleMessage(); - } - // Save state that we displayed the message - if (!this.state.protectionPanelMessageSeen) { - Services.prefs.setBoolPref(PROTECTIONS_PANEL_INFOMSG_PREF, true); - this.state.protectionPanelMessageSeen = true; - } - // Collapse the message after the panel is hidden so we don't get the - // animation when opening the panel - panelContainer.addEventListener( - "popuphidden", - () => { - if ( - this.state.protectionPanelMessageSeen && - !container.hasAttribute("disabled") - ) { - toggleMessage(); - } - }, - { - once: true, - } - ); - } - /** * @param {object} [browser] MessageChannel target argument as a response to a * user action. No message is shown if undefined. diff --git a/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js b/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js index 2f752f4634be..c3a0df569a0f 100644 --- a/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js +++ b/browser/components/newtab/test/unit/lib/ToolbarPanelHub.test.js @@ -1,6 +1,5 @@ import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm"; import { GlobalOverrider } from "test/unit/utils"; -import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm"; import { PanelTestProvider } from "lib/PanelTestProvider.sys.mjs"; describe("ToolbarPanelHub", () => { @@ -760,169 +759,4 @@ describe("ToolbarPanelHub", () => { }); }); }); - describe("#insertProtectionPanelMessage", () => { - const fakeInsert = () => - instance.insertProtectionPanelMessage({ - target: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument }, - }); - let getMessagesStub; - beforeEach(async () => { - const onboardingMsgs = - await OnboardingMessageProvider.getUntranslatedMessages(); - getMessagesStub = sandbox - .stub() - .resolves( - onboardingMsgs.find(msg => msg.template === "protections_panel") - ); - await instance.init(waitForInitializedStub, { - sendTelemetry: fakeSendTelemetry, - getMessages: getMessagesStub, - }); - }); - it("should remember it showed", async () => { - await fakeInsert(); - - assert.calledWithExactly( - setBoolPrefStub, - "browser.protections_panel.infoMessage.seen", - true - ); - }); - it("should toggle/expand when default collapsed/disabled", async () => { - fakeElementById.hasAttribute.returns(true); - - await fakeInsert(); - - assert.calledThrice(fakeElementById.toggleAttribute); - }); - it("should toggle again when popup hides", async () => { - fakeElementById.addEventListener.callsArg(1); - - await fakeInsert(); - - assert.callCount(fakeElementById.toggleAttribute, 6); - }); - it("should open link on click (separate link element)", async () => { - const sendTelemetryStub = sandbox.stub( - instance, - "sendUserEventTelemetry" - ); - const onboardingMsgs = - await OnboardingMessageProvider.getUntranslatedMessages(); - const msg = onboardingMsgs.find(m => m.template === "protections_panel"); - - await fakeInsert(); - - assert.calledOnce(sendTelemetryStub); - assert.calledWithExactly( - sendTelemetryStub, - fakeWindow, - "IMPRESSION", - msg - ); - - eventListeners.mouseup(); - - assert.calledOnce(global.SpecialMessageActions.handleAction); - assert.calledWithExactly( - global.SpecialMessageActions.handleAction, - { - type: "OPEN_URL", - data: { - args: sinon.match.string, - where: "tabshifted", - }, - }, - fakeWindow.browser - ); - }); - it("should format the url", async () => { - const stub = sandbox - .stub(global.Services.urlFormatter, "formatURL") - .returns("formattedURL"); - const onboardingMsgs = - await OnboardingMessageProvider.getUntranslatedMessages(); - const msg = onboardingMsgs.find(m => m.template === "protections_panel"); - - await fakeInsert(); - - eventListeners.mouseup(); - - assert.calledOnce(stub); - assert.calledWithExactly(stub, msg.content.cta_url); - assert.calledOnce(global.SpecialMessageActions.handleAction); - assert.calledWithExactly( - global.SpecialMessageActions.handleAction, - { - type: "OPEN_URL", - data: { - args: "formattedURL", - where: "tabshifted", - }, - }, - fakeWindow.browser - ); - }); - it("should report format url errors", async () => { - const stub = sandbox - .stub(global.Services.urlFormatter, "formatURL") - .throws(); - const onboardingMsgs = - await OnboardingMessageProvider.getUntranslatedMessages(); - const msg = onboardingMsgs.find(m => m.template === "protections_panel"); - sandbox.spy(global.console, "error"); - - await fakeInsert(); - - eventListeners.mouseup(); - - assert.calledOnce(stub); - assert.calledOnce(global.console.error); - assert.calledOnce(global.SpecialMessageActions.handleAction); - assert.calledWithExactly( - global.SpecialMessageActions.handleAction, - { - type: "OPEN_URL", - data: { - args: msg.content.cta_url, - where: "tabshifted", - }, - }, - fakeWindow.browser - ); - }); - it("should open link on click (directly attached to the message)", async () => { - const onboardingMsgs = - await OnboardingMessageProvider.getUntranslatedMessages(); - const msg = onboardingMsgs.find(m => m.template === "protections_panel"); - getMessagesStub.resolves({ - ...msg, - content: { ...msg.content, link_text: null }, - }); - await fakeInsert(); - - eventListeners.mouseup(); - - assert.calledOnce(global.SpecialMessageActions.handleAction); - assert.calledWithExactly( - global.SpecialMessageActions.handleAction, - { - type: "OPEN_URL", - data: { - args: sinon.match.string, - where: "tabshifted", - }, - }, - fakeWindow.browser - ); - }); - it("should handle user actions from mouseup and keyup", async () => { - await fakeInsert(); - - eventListeners.mouseup(); - eventListeners.keyup({ key: "Enter" }); - eventListeners.keyup({ key: " " }); - assert.calledThrice(global.SpecialMessageActions.handleAction); - }); - }); }); diff --git a/browser/locales/en-US/browser/newtab/asrouter.ftl b/browser/locales/en-US/browser/newtab/asrouter.ftl index 8a4272409521..d898911456d0 100644 --- a/browser/locales/en-US/browser/newtab/asrouter.ftl +++ b/browser/locales/en-US/browser/newtab/asrouter.ftl @@ -75,12 +75,6 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip = .aria-label = Close button .title = Close -## Protections panel - -cfr-protections-panel-header = Browse without being followed -cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online. -cfr-protections-panel-link-text = Learn more - ## What's New toolbar button and panel # This string is used by screen readers to offer a text based alternative for diff --git a/browser/locales/en-US/browser/protectionsPanel.ftl b/browser/locales/en-US/browser/protectionsPanel.ftl index a6865fd286c6..94b461d49e28 100644 --- a/browser/locales/en-US/browser/protectionsPanel.ftl +++ b/browser/locales/en-US/browser/protectionsPanel.ftl @@ -153,3 +153,9 @@ protections-panel-cookie-banner-view-turn-on-label = protections-panel-report-broken-site = .label = Report broken site .title = Report broken site + +## Protections panel info message + +cfr-protections-panel-header = Browse without being followed +cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online. +cfr-protections-panel-link-text = Learn more diff --git a/python/l10n/fluent_migrations/bug_1863022_protectionsPanel_infomessage.py b/python/l10n/fluent_migrations/bug_1863022_protectionsPanel_infomessage.py new file mode 100644 index 000000000000..b09d36247598 --- /dev/null +++ b/python/l10n/fluent_migrations/bug_1863022_protectionsPanel_infomessage.py @@ -0,0 +1,24 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from fluent.migrate.helpers import transforms_from + + +def migrate(ctx): + """Bug 1863022 - Move Protection Panel Message to calling code, part {index}""" + + source = "browser/browser/newtab/asrouter.ftl" + target = "browser/browser/protectionsPanel.ftl" + + ctx.add_transforms( + target, + target, + transforms_from( + """ +cfr-protections-panel-header = {COPY_PATTERN(from_path, "cfr-protections-panel-header")} +cfr-protections-panel-body = {COPY_PATTERN(from_path, "cfr-protections-panel-body")} +cfr-protections-panel-link-text = {COPY_PATTERN(from_path, "cfr-protections-panel-link-text")} +""", + from_path=source, + ), + ) diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml index 6182a8db1a32..02fd26e61464 100644 --- a/toolkit/components/telemetry/Events.yaml +++ b/toolkit/components/telemetry/Events.yaml @@ -2497,7 +2497,10 @@ security.ui.app_menu: security.ui.protectionspopup: open: - objects: ["protections_popup"] + objects: ["protections_popup", "protectionspopup_cfr",] + extra_keys: + message: > + For protectionspopup_cfr, the message ID. bug_numbers: - 1560327 - 1607488 @@ -2534,7 +2537,11 @@ security.ui.protectionspopup: "milestone_message", "cookieb_toggle_on", "cookieb_toggle_off", + "protectionspopup_cfr", ] + extra_keys: + message: > + For protectionspopup_cfr, the message ID. bug_numbers: - 1560327 - 1602015