summaryrefslogtreecommitdiffstats
path: root/browser/components/protections/content
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/protections/content/lockwise-card.mjs142
-rw-r--r--browser/components/protections/content/monitor-card.mjs457
-rw-r--r--browser/components/protections/content/protections.css1130
-rw-r--r--browser/components/protections/content/protections.ftl26
-rw-r--r--browser/components/protections/content/protections.html313
-rw-r--r--browser/components/protections/content/protections.mjs493
-rw-r--r--browser/components/protections/content/proxy-card.mjs29
-rw-r--r--browser/components/protections/content/vpn-card.mjs102
8 files changed, 2692 insertions, 0 deletions
diff --git a/browser/components/protections/content/lockwise-card.mjs b/browser/components/protections/content/lockwise-card.mjs
new file mode 100644
index 0000000000..8d660430ed
--- /dev/null
+++ b/browser/components/protections/content/lockwise-card.mjs
@@ -0,0 +1,142 @@
+/* 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-env mozilla/remote-page */
+
+const HOW_IT_WORKS_URL_PREF = RPMGetFormatURLPref(
+ "browser.contentblocking.report.lockwise.how_it_works.url"
+);
+
+export default class LockwiseCard {
+ constructor(document) {
+ this.doc = document;
+ }
+
+ /**
+ * Initializes message listeners/senders.
+ */
+ init() {
+ const savePasswordsButton = this.doc.getElementById(
+ "save-passwords-button"
+ );
+ savePasswordsButton.addEventListener(
+ "click",
+ this.openAboutLogins.bind(this)
+ );
+
+ const managePasswordsButton = this.doc.getElementById(
+ "manage-passwords-button"
+ );
+ managePasswordsButton.addEventListener(
+ "click",
+ this.openAboutLogins.bind(this)
+ );
+
+ // Attack link to Firefox Lockwise "How it works" page.
+ const lockwiseReportLink = this.doc.getElementById("lockwise-how-it-works");
+ lockwiseReportLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "lw_about_link");
+ });
+ }
+
+ openAboutLogins() {
+ const lockwiseCard = this.doc.querySelector(".lockwise-card");
+ if (lockwiseCard.classList.contains("has-logins")) {
+ if (lockwiseCard.classList.contains("breached-logins")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "lw_open_button",
+ "manage_breached_passwords"
+ );
+ } else if (lockwiseCard.classList.contains("no-breached-logins")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "lw_open_button",
+ "manage_passwords"
+ );
+ }
+ } else if (lockwiseCard.classList.contains("no-logins")) {
+ this.doc.sendTelemetryEvent("click", "lw_open_button", "save_passwords");
+ }
+ RPMSendAsyncMessage("OpenAboutLogins");
+ }
+
+ buildContent(data) {
+ const { numLogins, potentiallyBreachedLogins } = data;
+ const hasLogins = numLogins > 0;
+ const title = this.doc.getElementById("lockwise-title");
+ const headerContent = this.doc.querySelector(
+ "#lockwise-header-content span"
+ );
+ const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
+
+ if (hasLogins) {
+ lockwiseCard.classList.remove("no-logins");
+ lockwiseCard.classList.add("has-logins");
+ title.setAttribute("data-l10n-id", "passwords-title-logged-in");
+ headerContent.setAttribute(
+ "data-l10n-id",
+ "lockwise-header-content-logged-in"
+ );
+ this.renderContentForLoggedInUser(numLogins, potentiallyBreachedLogins);
+ } else {
+ lockwiseCard.classList.remove("has-logins");
+ lockwiseCard.classList.add("no-logins");
+ title.setAttribute("data-l10n-id", "lockwise-title");
+ headerContent.setAttribute("data-l10n-id", "passwords-header-content");
+ }
+
+ const lockwiseUI = document.querySelector(".card.lockwise-card.loading");
+ lockwiseUI.classList.remove("loading");
+ }
+
+ /**
+ * Displays strings indicating stored logins for a user.
+ *
+ * @param {Number} storedLogins
+ * The number of browser-stored logins.
+ * @param {Number} potentiallyBreachedLogins
+ * The number of potentially breached logins.
+ */
+ renderContentForLoggedInUser(storedLogins, potentiallyBreachedLogins) {
+ const lockwiseScannedText = this.doc.getElementById(
+ "lockwise-scanned-text"
+ );
+ const lockwiseScannedIcon = this.doc.getElementById(
+ "lockwise-scanned-icon"
+ );
+ const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
+
+ if (potentiallyBreachedLogins) {
+ document.l10n.setAttributes(
+ lockwiseScannedText,
+ "lockwise-scanned-text-breached-logins",
+ {
+ count: potentiallyBreachedLogins,
+ }
+ );
+ lockwiseScannedIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/breached-password.svg"
+ );
+ lockwiseCard.classList.add("breached-logins");
+ } else {
+ document.l10n.setAttributes(
+ lockwiseScannedText,
+ "lockwise-scanned-text-no-breached-logins",
+ {
+ count: storedLogins,
+ }
+ );
+ lockwiseScannedIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ lockwiseCard.classList.add("no-breached-logins");
+ }
+
+ const howItWorksLink = this.doc.getElementById("lockwise-how-it-works");
+ howItWorksLink.href = HOW_IT_WORKS_URL_PREF;
+ }
+}
diff --git a/browser/components/protections/content/monitor-card.mjs b/browser/components/protections/content/monitor-card.mjs
new file mode 100644
index 0000000000..018aa0c7f4
--- /dev/null
+++ b/browser/components/protections/content/monitor-card.mjs
@@ -0,0 +1,457 @@
+/* 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-env mozilla/remote-page */
+
+const MONITOR_URL = RPMGetStringPref(
+ "browser.contentblocking.report.monitor.url",
+ ""
+);
+const MONITOR_SIGN_IN_URL = RPMGetStringPref(
+ "browser.contentblocking.report.monitor.sign_in_url",
+ ""
+);
+const HOW_IT_WORKS_URL_PREF = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.how_it_works.url"
+);
+const MONITOR_PREFERENCES_URL = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.preferences_url"
+);
+const MONITOR_HOME_PAGE_URL = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.home_page_url"
+);
+
+export default class MonitorClass {
+ constructor(document) {
+ this.doc = document;
+ }
+
+ init() {
+ // Wait for monitor data and display the card.
+ this.getMonitorData();
+
+ let monitorAboutLink = this.doc.getElementById("monitor-link");
+ monitorAboutLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "mtr_about_link");
+ });
+
+ const storedEmailLink = this.doc.getElementById(
+ "monitor-stored-emails-link"
+ );
+ storedEmailLink.href = MONITOR_PREFERENCES_URL;
+ storedEmailLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+
+ const knownBreachesLink = this.doc.getElementById(
+ "monitor-known-breaches-link"
+ );
+ knownBreachesLink.href = MONITOR_HOME_PAGE_URL;
+ knownBreachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+
+ const exposedPasswordsLink = this.doc.getElementById(
+ "monitor-exposed-passwords-link"
+ );
+ exposedPasswordsLink.href = MONITOR_HOME_PAGE_URL;
+ exposedPasswordsLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+
+ onClickMonitorButton(evt) {
+ RPMSendAsyncMessage("ClearMonitorCache");
+ switch (evt.currentTarget.id) {
+ case "monitor-partial-breaches-link":
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "resolve_breaches"
+ );
+ break;
+ case "monitor-breaches-link":
+ if (evt.currentTarget.classList.contains("no-breaches-resolved")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "manage_breaches"
+ );
+ } else {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "view_report"
+ );
+ }
+ break;
+ case "monitor-stored-emails-link":
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "stored_emails"
+ );
+ break;
+ case "monitor-known-breaches-link":
+ const knownBreaches = this.doc.querySelector(
+ "span[data-type='known-breaches']"
+ );
+ if (knownBreaches.classList.contains("known-resolved-breaches")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "known_resolved_breaches"
+ );
+ } else if (
+ knownBreaches.classList.contains("known-unresolved-breaches")
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "known_unresolved_breaches"
+ );
+ }
+ break;
+ case "monitor-exposed-passwords-link":
+ const exposedPasswords = this.doc.querySelector(
+ "span[data-type='exposed-passwords']"
+ );
+ if (
+ exposedPasswords.classList.contains("passwords-exposed-all-breaches")
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "exposed_passwords_all_breaches"
+ );
+ } else if (
+ exposedPasswords.classList.contains(
+ "passwords-exposed-unresolved-breaches"
+ )
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "exposed_passwords_unresolved_breaches"
+ );
+ }
+ break;
+ }
+ }
+
+ /**
+ * Retrieves the monitor data and displays this data in the card.
+ **/
+ getMonitorData() {
+ RPMSendQuery("FetchMonitorData", {}).then(monitorData => {
+ // Once data for the user is retrieved, display the monitor card.
+ this.buildContent(monitorData);
+
+ // Show the Monitor card.
+ const monitorUI = this.doc.querySelector(".card.monitor-card.loading");
+ monitorUI.classList.remove("loading");
+ });
+ }
+
+ buildContent(monitorData) {
+ const headerContent = this.doc.querySelector(
+ "#monitor-header-content span"
+ );
+ const monitorCard = this.doc.querySelector(".card.monitor-card");
+ if (!monitorData.error) {
+ monitorCard.classList.add("has-logins");
+ headerContent.setAttribute(
+ "data-l10n-id",
+ "monitor-header-content-signed-in"
+ );
+ this.renderContentForUserWithAccount(monitorData);
+ } else {
+ monitorCard.classList.add("no-logins");
+ const signUpForMonitorLink = this.doc.getElementById(
+ "sign-up-for-monitor-link"
+ );
+ signUpForMonitorLink.href = this.buildMonitorUrl(monitorData.userEmail);
+ signUpForMonitorLink.setAttribute("data-l10n-id", "monitor-sign-up-link");
+ headerContent.setAttribute(
+ "data-l10n-id",
+ "monitor-header-content-no-account"
+ );
+ signUpForMonitorLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "mtr_signup_button");
+ });
+ }
+ }
+
+ /**
+ * Builds the appropriate URL that takes the user to the Monitor website's
+ * sign-up/sign-in page.
+ *
+ * @param {String|null} email
+ * Optional. The email used to direct the user to the Monitor website's OAuth
+ * sign-in flow. If null, then direct user to just the Monitor website.
+ *
+ * @return URL to Monitor website.
+ */
+ buildMonitorUrl(email = null) {
+ return email
+ ? `${MONITOR_SIGN_IN_URL}${encodeURIComponent(email)}`
+ : MONITOR_URL;
+ }
+
+ renderContentForUserWithAccount(monitorData) {
+ const {
+ numBreaches,
+ numBreachesResolved,
+ passwords,
+ passwordsResolved,
+ monitoredEmails,
+ } = monitorData;
+ const monitorCardBody = this.doc.querySelector(
+ ".card.monitor-card .card-body"
+ );
+ monitorCardBody.classList.remove("hidden");
+
+ const howItWorksLink = this.doc.getElementById("monitor-link");
+ howItWorksLink.href = HOW_IT_WORKS_URL_PREF;
+
+ const storedEmail = this.doc.querySelector(
+ "span[data-type='stored-emails']"
+ );
+ storedEmail.textContent = monitoredEmails;
+ const infoMonitoredAddresses = this.doc.getElementById(
+ "info-monitored-addresses"
+ );
+ this.doc.l10n.setAttributes(
+ infoMonitoredAddresses,
+ "info-monitored-emails",
+ { count: monitoredEmails }
+ );
+
+ const knownBreaches = this.doc.querySelector(
+ "span[data-type='known-breaches']"
+ );
+ const exposedPasswords = this.doc.querySelector(
+ "span[data-type='exposed-passwords']"
+ );
+
+ const infoKnownBreaches = this.doc.getElementById("info-known-breaches");
+ const infoExposedPasswords = this.doc.getElementById(
+ "info-exposed-passwords"
+ );
+
+ const breachesWrapper = this.doc.querySelector(".monitor-breaches-wrapper");
+ const partialBreachesWrapper = this.doc.querySelector(
+ ".monitor-partial-breaches-wrapper"
+ );
+ const breachesTitle = this.doc.getElementById("monitor-breaches-title");
+ const breachesIcon = this.doc.getElementById("monitor-breaches-icon");
+ const breachesDesc = this.doc.getElementById(
+ "monitor-breaches-description"
+ );
+ const breachesLink = this.doc.getElementById("monitor-breaches-link");
+ if (numBreaches) {
+ if (!numBreachesResolved) {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreaches;
+ knownBreaches.classList.add("known-unresolved-breaches");
+ knownBreaches.classList.remove("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-found",
+ { count: numBreaches }
+ );
+ exposedPasswords.textContent = passwords;
+ exposedPasswords.classList.add("passwords-exposed-all-breaches");
+ exposedPasswords.classList.remove(
+ "passwords-exposed-unresolved-breaches"
+ );
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-found",
+ { count: passwords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/new-feature.svg"
+ );
+ breachesTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-breaches-unresolved-title"
+ );
+ breachesDesc.setAttribute(
+ "data-l10n-id",
+ "monitor-breaches-unresolved-description"
+ );
+ breachesLink.setAttribute(
+ "data-l10n-id",
+ "monitor-manage-breaches-link"
+ );
+ breachesLink.classList.add("no-breaches-resolved");
+ } else if (numBreaches == numBreachesResolved) {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreachesResolved;
+ knownBreaches.classList.remove("known-unresolved-breaches");
+ knownBreaches.classList.add("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-resolved",
+ { count: numBreachesResolved }
+ );
+ let unresolvedPasswords = passwords - passwordsResolved;
+ exposedPasswords.textContent = unresolvedPasswords;
+ exposedPasswords.classList.remove("passwords-exposed-all-breaches");
+ exposedPasswords.classList.add("passwords-exposed-unresolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-resolved",
+ { count: unresolvedPasswords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ breachesTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-breaches-resolved-title"
+ );
+ breachesDesc.setAttribute(
+ "data-l10n-id",
+ "monitor-breaches-resolved-description"
+ );
+ breachesLink.setAttribute("data-l10n-id", "monitor-view-report-link");
+ } else {
+ breachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreachesResolved;
+ knownBreaches.classList.remove("known-unresolved-breaches");
+ knownBreaches.classList.add("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-resolved",
+ { count: numBreachesResolved }
+ );
+ let unresolvedPasswords = passwords - passwordsResolved;
+ exposedPasswords.textContent = unresolvedPasswords;
+ exposedPasswords.classList.remove("passwords-exposed-all-breaches");
+ exposedPasswords.classList.add("passwords-exposed-unresolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-resolved",
+ { count: unresolvedPasswords }
+ );
+
+ const partialBreachesTitle = document.getElementById(
+ "monitor-partial-breaches-title"
+ );
+ partialBreachesTitle.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({
+ numBreaches,
+ numBreachesResolved,
+ })
+ );
+ partialBreachesTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-partial-breaches-title"
+ );
+
+ const progressBar = this.doc.querySelector(".progress-bar");
+ const partialBreachesMotivationTitle = document.getElementById(
+ "monitor-partial-breaches-motivation-title"
+ );
+
+ let percentageResolved = Math.floor(
+ (numBreachesResolved / numBreaches) * 100
+ );
+ progressBar.setAttribute("value", 100 - percentageResolved);
+ switch (true) {
+ case percentageResolved > 0 && percentageResolved < 25:
+ partialBreachesMotivationTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-partial-breaches-motivation-title-start"
+ );
+ break;
+
+ case percentageResolved >= 25 && percentageResolved < 75:
+ partialBreachesMotivationTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-partial-breaches-motivation-title-middle"
+ );
+ break;
+
+ case percentageResolved >= 75 && percentageResolved < 100:
+ partialBreachesMotivationTitle.setAttribute(
+ "data-l10n-id",
+ "monitor-partial-breaches-motivation-title-end"
+ );
+ break;
+ }
+
+ const partialBreachesPercentage = document.getElementById(
+ "monitor-partial-breaches-percentage"
+ );
+ partialBreachesPercentage.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({
+ percentageResolved,
+ })
+ );
+ partialBreachesPercentage.setAttribute(
+ "data-l10n-id",
+ "monitor-partial-breaches-percentage"
+ );
+
+ const partialBreachesLink = document.getElementById(
+ "monitor-partial-breaches-link"
+ );
+ partialBreachesLink.setAttribute("href", MONITOR_HOME_PAGE_URL);
+ partialBreachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+ } else {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreaches;
+ knownBreaches.classList.add("known-unresolved-breaches");
+ knownBreaches.classList.remove("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-found",
+ { count: numBreaches }
+ );
+ exposedPasswords.textContent = passwords;
+ exposedPasswords.classList.add("passwords-exposed-all-breaches");
+ exposedPasswords.classList.remove(
+ "passwords-exposed-unresolved-breaches"
+ );
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-found",
+ { count: passwords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ breachesTitle.setAttribute("data-l10n-id", "monitor-no-breaches-title");
+ breachesDesc.setAttribute(
+ "data-l10n-id",
+ "monitor-no-breaches-description"
+ );
+ breachesLink.setAttribute("data-l10n-id", "monitor-view-report-link");
+ }
+
+ breachesLink.setAttribute("href", MONITOR_HOME_PAGE_URL);
+ breachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+}
diff --git a/browser/components/protections/content/protections.css b/browser/components/protections/content/protections.css
new file mode 100644
index 0000000000..202e229465
--- /dev/null
+++ b/browser/components/protections/content/protections.css
@@ -0,0 +1,1130 @@
+/* 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/. */
+
+:root {
+ --card-padding: 24px;
+ --exit-icon-size: 12px;
+ --exit-icon-position: calc((var(--card-padding) - var(--exit-icon-size)) / 2);
+ --social-color: #9059FF;
+ --cookie-color: #0090F4;
+ --tracker-color: #2AC3A2;
+ --fingerprinter-color: #FFA436;
+ --cryptominer-color: #ADADBC;
+
+ /* Highlight colors for trackers */
+ --social-highlight-color: #7B4CDB;
+ --cookie-highlight-color: #0081DB;
+ --tracker-highlight-color: #23A488;
+ --fingerprinter-highlight-color: #D37F17;
+ --cryptominer-highlight-color: #9292A0;
+
+ --tab-highlight: var(--social-color); /* start with social selected */
+ --column-width: 16px;
+ --graph-empty: #CECECF;
+ --graph-curve: cubic-bezier(.66,.75,.59,.91);
+
+ /* Colors for the loading indicator */
+ --protection-report-loader-color-stop: #AEAEAE3D;
+ --protection-report-loader-gradient-opacity: 0.95;
+
+ --grey-70: #38383D;
+ --grey-90-a60: rgba(12, 12, 13, 0.6);
+
+ --gear-icon-fill: var(--grey-90-a60);
+ --hover-grey-link: var(--grey-70);
+ --feature-banner-color: rgba(0, 0, 0, 0.05);
+}
+
+body {
+ box-sizing: border-box;
+}
+
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body[focuseddatatype=social] {
+ --tab-highlight: var(--social-color);
+}
+
+body[focuseddatatype=cookie] {
+ --tab-highlight: var(--cookie-color);
+}
+
+body[focuseddatatype=tracker] {
+ --tab-highlight: var(--tracker-color);
+}
+
+body[focuseddatatype=fingerprinter] {
+ --tab-highlight: var(--fingerprinter-color);
+}
+
+body[focuseddatatype=cryptominer] {
+ --tab-highlight: var(--cryptominer-color);
+}
+
+h2 {
+ font-weight: 700;
+}
+
+#report-title {
+ font-size: 20px;
+ font-weight: 300;
+ margin-block-end: 20px;
+}
+
+#report-summary {
+ color: var(--in-content-deemphasized-text);
+ line-height: 26px;
+ font-size: 16px;
+ margin: 0;
+ margin-bottom: 15px;
+}
+
+#report-content {
+ width: 763px;
+ margin: 0 auto;
+ margin-block: 40px 80px;
+}
+
+.card-header .wrapper,
+.new-banner .wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ align-items: center;
+}
+
+#manage-protections,
+.card-header > button,
+#save-passwords-button,
+#get-proxy-extension-link,
+#get-vpn-link,
+#vpn-banner-link,
+#manage-passwords-button,
+#sign-up-for-monitor-link {
+ grid-area: 1 / 5 / 1 / -1;
+ margin: 0;
+ font-size: 0.95em;
+ cursor: pointer;
+ padding: 10px;
+ text-align: center;
+ line-height: initial;
+}
+
+#vpn-banner-link {
+ grid-area: 1 / 6 / 1 / -1;
+}
+
+.new-banner .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 6;
+ padding-inline-end: 15px;
+}
+
+.lockwise-card.has-logins .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 6;
+}
+
+.card:not(.has-logins) .wrapper div:nth-child(1),
+.etp-card.custom-not-blocking .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 5;
+ padding-inline-end: 15px;
+}
+
+.etp-card:not(.custom-not-blocking) .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 8;
+}
+
+.card.has-logins .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / -1;
+}
+
+.vpn-card.subscribed .wrapper div:nth-child(1) {
+ padding-inline-end: 29px;
+ grid-area: 1 / 1 / 1 / 7;
+}
+
+/* We want to hide certain components depending on its state. */
+.no-logins .monitor-scanned-wrapper,
+.etp-card.custom-not-blocking .card-body,
+.etp-card.custom-not-blocking #protection-settings,
+#manage-protections,
+.etp-card .icon.dark,
+.proxy-card .icon.dark,
+.vpn-card .icon.dark,
+.vpn-banner .icon.dark,
+a.hidden,
+.loading .card-body,
+.lockwise-card.hidden,
+#lockwise-body-content .has-logins.hidden,
+#lockwise-body-content .no-logins.hidden,
+.lockwise-card.no-logins #lockwise-how-it-works,
+.lockwise-card.no-logins .lockwise-scanned-wrapper,
+.lockwise-card.no-logins #manage-passwords-button,
+.lockwise-card.has-logins #save-passwords-button,
+.monitor-card.hidden,
+.monitor-card.no-logins .card-body,
+.monitor-card.no-logins #monitor-header-content a,
+.monitor-card.no-logins .monitor-scanned-text,
+.monitor-card.no-logins .icon-small,
+.monitor-card.no-logins .monitor-breaches-wrapper,
+.monitor-card.no-logins .monitor-partial-breaches-wrapper,
+.monitor-card .monitor-breaches-wrapper.hidden,
+.monitor-card .monitor-partial-breaches-wrapper.hidden,
+.monitor-card.has-logins #sign-up-for-monitor-link,
+.loading a,
+.loading button,
+.loading .wrapper,
+.proxy-card.hidden,
+.vpn-card.hidden,
+.card-body.hidden,
+.hidden {
+ display: none;
+}
+
+.icon {
+ width: 64px;
+ height: 64px;
+ grid-column: 1;
+ margin: 0 auto;
+ z-index: 1;
+}
+
+.vpn-card .icon {
+ width: 56px;
+ height: 56px;
+}
+
+.new-banner .icon {
+ width: 50px;
+ height: 50px;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --social-highlight-color: #9661FF;
+ --cookie-highlight-color: #2BA8FF;
+ --tracker-highlight-color: #39E1BC;
+ --fingerprinter-highlight-color: #FFB65E;
+ --cryptominer-highlight-color: #BEBECA;
+
+ --gear-icon-fill: rgba(249, 249, 250, 0.60);
+ --hover-grey-link: var(--grey-30);
+ --feature-banner-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .etp-card .icon.dark,
+ .proxy-card .icon.dark,
+ .vpn-card .icon.dark,
+ .vpn-banner .icon.dark {
+ display: block;
+ }
+
+ .etp-card .icon.light,
+ .proxy-card .icon.light,
+ .vpn-card .icon.light,
+ .vpn-banner .icon.light {
+ display: none;
+ }
+}
+
+.card {
+ display: grid;
+ grid-template-columns: 100%;
+ grid-template-rows: 20% auto;
+ padding: 0;
+ margin-block-end: 25px;
+ line-height: 1.3em;
+}
+
+.card-header,
+.card-body {
+ display: grid;
+ grid-template-columns: 1fr 7fr;
+ padding: var(--card-padding);
+ grid-gap: var(--card-padding);
+}
+
+.card .card-title {
+ font-size: inherit;
+ line-height: 1.25;
+ margin: 0;
+ cursor: default;
+}
+
+.card .content {
+ margin-block: 5px 0;
+ font-size: .93em;
+ cursor: default;
+ color: var(--in-content-deemphasized-text);
+}
+
+.exit-icon {
+ position: absolute;
+ width: var(--exit-icon-size);
+ height: var(--exit-icon-size);
+ top: var(--exit-icon-position);
+ inset-inline-end: var(--exit-icon-position);
+ background-image: url(chrome://global/skin/icons/close.svg);
+ background-size: calc(var(--exit-icon-size) - 2px);
+ background-color: transparent;
+ background-position: center;
+ background-repeat: no-repeat;
+ -moz-context-properties: fill;
+ fill: currentColor;
+
+ /* Override margin, padding, min-height and min-width set in common-shared.css */
+ padding: 0;
+ margin: 0;
+ min-width: 1px;
+ min-height: 1px;
+}
+
+.custom-not-blocking .content {
+ margin-block-end: 5px;
+}
+
+.custom-not-blocking #manage-protections {
+ display: initial;
+}
+
+#protection-settings {
+ -moz-context-properties: fill;
+ fill: var(--gear-icon-fill);
+ background: url("chrome://global/skin/icons/settings.svg") no-repeat 0;
+ font-size: 0.75em;
+ cursor: pointer;
+ width: max-content;
+ color: var(--in-content-deemphasized-text);
+ margin-block: 6px 0;
+ font-size: 12px;
+ padding-block: 4px;
+ padding-inline: 24px 4px;
+}
+
+#protection-settings:dir(rtl) {
+ background-position-x: right;
+}
+
+#protection-settings:hover,
+#protection-settings:focus {
+ text-decoration: underline;
+ color: var(--hover-grey-link);
+ fill: var(--hover-grey-link);
+}
+
+#protection-settings:hover:active {
+ text-decoration: none;
+ color: var(--in-content-text-color);
+ fill: var(--in-content-text-color);
+}
+
+#protection-settings:-moz-focusring {
+ outline: none;
+}
+
+.card .card-body {
+ border-block-start: 1px solid var(--in-content-border-color);
+ grid-column: span 2;
+ grid-row: 2;
+ position: relative;
+}
+
+.body-wrapper {
+ grid-column: 2;
+}
+
+#graph-week-summary,
+#graph-total-summary {
+ font-size: 0.8em;
+}
+
+#graph-week-summary {
+ font-weight: bold;
+}
+
+#graph-wrapper {
+ width: 100%;
+ margin-block: 33px 25px;
+}
+
+#graph {
+ display: grid;
+ grid: repeat(10, 1fr) max-content / repeat(7, 1fr);
+ height: 130px;
+ margin-block-end: 10px;
+}
+
+#private-window-message {
+ border: 1px solid var(--in-content-border-color);
+ grid-area: 1 / 2 / 1 / 7;
+ background-color: var(--in-content-box-background-odd);
+ padding: 13px 45px;
+ font-size: 13px;
+ margin-bottom: 25px;
+ text-align: center;
+}
+
+#graph:not(.private-window) #private-window-message {
+ display: none;
+}
+
+/* Graph Bars */
+.graph-bar {
+ grid-row: 2 / -2;
+ align-self: flex-end;
+ width: var(--column-width);
+ position: relative;
+ height: 0;
+ transition: height 500ms var(--graph-curve);
+}
+
+.graph-wrapper-bar {
+ height: 100%;
+ width: 100%;
+ border-radius: 2px;
+ overflow: hidden;
+ outline: 1px solid transparent;
+}
+
+.graph-bar:hover {
+ cursor: pointer;
+}
+
+.graph-bar.empty {
+ height: 0;
+ border: 1px var(--graph-empty) solid;
+ border-radius: 4px;
+ cursor: default;
+}
+
+.social-bar {
+ background-color: var(--social-color);
+}
+
+.hover-social .social-bar {
+ background-color: var(--social-highlight-color);
+}
+
+.cookie-bar {
+ background-color: var(--cookie-color);
+}
+
+.hover-cookie .cookie-bar {
+ background-color: var(--cookie-highlight-color);
+}
+
+.tracker-bar {
+ background-color: var(--tracker-color);
+}
+
+.hover-tracker .tracker-bar {
+ background-color: var(--tracker-highlight-color);
+}
+
+.fingerprinter-bar {
+ background-color: var(--fingerprinter-color);
+}
+
+.hover-fingerprinter .fingerprinter-bar {
+ background-color: var(--fingerprinter-highlight-color);
+}
+
+.cryptominer-bar {
+ background-color: var(--cryptominer-color);
+}
+
+.hover-cryptominer .cryptominer-bar {
+ background-color: var(--cryptominer-highlight-color);
+}
+
+.column-label {
+ margin-block-start: 5px;
+ font-size: 0.9em;
+ width: var(--column-width);
+ grid-row: -1;
+}
+
+.bar-count {
+ position: absolute;
+ top: -21px;
+ font-size: 0.8em;
+ opacity: 0;
+ transition: opacity 700ms;
+ pointer-events: none;
+}
+
+.bar-count.animate {
+ opacity: 1;
+}
+
+/* Legend */
+#graphLegendDescription {
+ position: absolute;
+ opacity: 0;
+ z-index: -1;
+}
+
+input[type="radio"] {
+ position: absolute;
+ inset-inline-start: -100px;
+}
+
+#legend input:focus + label {
+ outline: solid 1px;
+ outline-offset: -1px;
+ outline-color: var(--tab-highlight);
+}
+
+#legend {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-template-rows: auto auto;
+ grid-gap: 0;
+ position: relative;
+ overflow: hidden;
+}
+
+#highlight {
+ background: var(--tab-highlight);
+ position: absolute;
+ height: 3px;
+ width: 100%;
+ align-self: end;
+ grid-column: 1 / span 1;
+ grid-row: 1 / 1;
+}
+
+#highlight-hover {
+ position: absolute;
+ height: 4px;
+ width: 100%;
+ bottom: -1px;
+ align-self: end;
+}
+
+#legend label {
+ margin-block-end: -1px;
+ padding: 15px 0;
+ padding-inline-end: 5px;
+ border: 3px solid transparent;
+ -moz-context-properties: fill;
+ display: inline-block;
+}
+
+#legend label:-moz-focusring {
+ outline: none;
+}
+
+.icon-small {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ margin-inline-end: 2px;
+}
+
+label span {
+ margin-inline-start: 1px;
+ display: inline-block;
+}
+
+label[data-type="social"] .icon-small {
+ fill: var(--social-color);
+}
+
+label[data-type="cookie"] .icon-small {
+ fill: var(--cookie-color);
+}
+
+label[data-type="tracker"] .icon-small {
+ fill: var(--tracker-color);
+}
+
+label[data-type="fingerprinter"] .icon-small {
+ fill: var(--fingerprinter-color);
+}
+
+label[data-type="cryptominer"] .icon-small {
+ fill: var(--cryptominer-color);
+}
+
+.hover-social label[for="tab-social"],
+.hover-cookie label[for="tab-cookie"],
+.hover-tracker label[for="tab-tracker"],
+.hover-fingerprinter label[for="tab-fingerprinter"],
+.hover-cryptominer label[for="tab-cryptominer"],
+label:hover {
+ cursor: pointer;
+}
+
+.tab-content {
+ display: none;
+ padding: 22px 20px 20px;
+ border-block-start: 1px solid var(--tab-highlight);
+ background-color: var(--in-content-box-background-odd);
+ font-size: 0.8em;
+ line-height: 1.75;
+}
+
+.tab-content .content-title {
+ font-weight: bold;
+}
+
+.tab-content p {
+ margin: 0;
+}
+
+.tab-content p:nth-of-type(2) {
+ color: var(--in-content-deemphasized-text);
+}
+
+#tab-social:checked ~ #social,
+#tab-cookie:checked ~ #cookie,
+#tab-tracker:checked ~ #tracker,
+#tab-fingerprinter:checked ~ #fingerprinter,
+#tab-cryptominer:checked ~ #cryptominer {
+ display: block;
+ grid-column: 1/ -1;
+ grid-row: 2;
+}
+
+input[data-type="social"]:checked ~ #highlight,
+.hover-social label[for="tab-social"] ~ #highlight-hover,
+label[for="tab-social"]:hover ~ #highlight-hover {
+ background-color: var(--social-color);
+ grid-area: social;
+}
+
+input[data-type="cookie"]:checked ~ #highlight,
+.hover-cookie label[for="tab-cookie"] ~ #highlight-hover,
+label[for="tab-cookie"]:hover ~ #highlight-hover {
+ background-color: var(--cookie-color);
+ grid-area: cookie;
+}
+
+input[data-type="tracker"]:checked ~ #highlight,
+.hover-tracker label[for="tab-tracker"] ~ #highlight-hover,
+label[for="tab-tracker"]:hover ~ #highlight-hover {
+ background-color: var(--tracker-color);
+ grid-area: tracker;
+}
+
+input[data-type="fingerprinter"]:checked ~ #highlight,
+.hover-fingerprinter label[for="tab-fingerprinter"] ~ #highlight-hover,
+label[for="tab-fingerprinter"]:hover ~ #highlight-hover {
+ background-color: var(--fingerprinter-color);
+ grid-area: fingerprinter;
+}
+
+input[data-type="cryptominer"]:checked ~ #highlight,
+.hover-cryptominer label[for="tab-cryptominer"] ~ #highlight-hover,
+label[for="tab-cryptominer"]:hover ~ #highlight-hover {
+ background-color: var(--cryptominer-color);
+ grid-area: cryptominer;
+}
+
+#mobile-hanger {
+ grid-column: 1;
+ grid-row: 3;
+}
+
+.etp-card {
+ margin-top: 31px;
+ grid-template-rows: 20% auto auto;
+}
+
+/* Lockwise Card */
+
+#lockwise-body-content > .no-logins,
+#lockwise-body-content > .has-logins,
+#etp-mobile-content {
+ display: grid;
+ font-size: 0.875em;
+ align-items: center;
+}
+
+#lockwise-body-content > .no-logins,
+#etp-mobile-content {
+ grid: 1fr / 1fr 6fr;
+}
+
+#lockwise-body-content > .has-logins {
+ grid: 1fr 1fr / minmax(70px, auto) 1fr;
+ grid-gap: 10px;
+}
+
+.mobile-app-icon {
+ height: 56px;
+ width: auto;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+#lockwise-app-links,
+#mobile-app-links {
+ display: block;
+}
+
+.block {
+ background-color: var(--grey-60);
+ border-radius: 4px;
+ text-align: center;
+ font-size: 1.125em;
+ font-weight: bold;
+ color: #fff;
+ padding: 7px;
+ line-height: 18px;
+}
+
+#lockwise-body-content .has-logins a {
+ margin-inline-start: 10px;
+}
+
+.lockwise-scanned-wrapper {
+ display: grid;
+ grid-template-columns: 24px auto;
+ margin-block-start: 24px;
+ grid-area: 2 / 1 / 2 / 5;
+ padding-bottom: 1.7em;
+}
+
+#lockwise-scanned-text {
+ margin-inline-end: 15px;
+}
+
+#lockwise-scanned-icon {
+ margin-top: 5px;
+}
+
+#manage-passwords-button {
+ grid-area: 2 / 5 / 2 / 7;
+ margin-inline-end: 15px;
+}
+
+.vpn-card.subscribed #get-vpn-link {
+ display: none;
+}
+
+.vpn-card:not(.subscribed) .content.subscribed {
+ display: none;
+}
+
+.vpn-card.subscribed .content:not(.subscribed) {
+ display: none;
+}
+
+/* Monitor card */
+.monitor-info-wrapper {
+ display: grid;
+ grid: 1fr / 1fr 1fr 1fr;
+ grid-column-start: 1;
+ grid-column-end: 7;
+}
+
+.monitor-scanned-wrapper {
+ margin-block-start: 24px;
+ font-size: 0.85em;
+ display: block;
+}
+
+.monitor-breaches-wrapper {
+ display: grid;
+ grid-area: 2 / 1 / 2 / 8;
+ grid: 1fr auto / repeat(7, 1fr);
+ margin-bottom: 24px;
+}
+
+.monitor-partial-breaches-wrapper {
+ display: grid;
+ grid-area: 2 / 1 / 2 / 8;
+ grid-template-columns: repeat(7, 1fr);
+ margin-block: 24px;
+}
+
+.monitor-partial-breaches-header {
+ grid-area: 1 / 1 / 1 / 7;
+ margin-inline-end: 15px;
+ margin-block: 6px;
+}
+
+#monitor-partial-breaches-percentage {
+ font-size: .93em;
+ cursor: default;
+ color: var(--in-content-deemphasized-text);
+ float: inline-end;
+}
+
+.progress-bar {
+ grid-area: 2 / 1 / 2 / 7;
+ margin-inline-start: 15px;
+ border-radius: 4px;
+ height: 25px;
+ box-shadow: 0 0 0 1px rgba(202, 201, 213, 0.5);
+ border: none;
+ background: linear-gradient(-45deg, #0250BB 0%, #9059FF 100%);
+ direction: rtl;
+}
+
+.progress-bar:dir(rtl) {
+ direction: ltr;
+ background: linear-gradient(-45deg, #0250BB 0%, #9059FF 100%);
+}
+
+.progress-bar::-moz-progress-bar {
+ background: #FFFFFF;
+ border-radius: 0 4px 4px 0;
+}
+
+.monitor-partial-breaches-motivation-text {
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 3 / 1 / 3 / 8;
+ margin-top: 25px;
+ display: grid;
+}
+
+.monitor-partial-breaches-motivation-wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 2 / 1 / 2 / 8;
+}
+
+#monitor-partial-breaches-motivation-title {
+ font-weight: 700;
+ grid-area: 1 / 1 / 1 / 7;
+ margin-inline-end: 15px;
+}
+
+#monitor-breaches-description,
+#monitor-partial-breaches-motivation-desc {
+ grid-area: 1 / 1 / 1 / 5;
+ margin-block: auto;
+ margin-inline-end: 15px;
+}
+
+.monitor-breaches-header {
+ margin-top: 30px;
+ grid-area: 1 / 1 / 1 / 8;
+}
+
+.monitor-breaches-description-wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 2 / 1 / 2 / 8;
+}
+
+#monitor-partial-breaches-icon,
+#monitor-breaches-icon {
+ vertical-align: middle;
+ margin-inline-end: 2px;
+}
+
+#monitor-partial-breaches-title {
+ font-size: 0.93em;
+}
+
+#monitor-breaches-title {
+ font-weight: 700;
+}
+
+#monitor-breaches-title,
+#monitor-partial-breaches-title,
+#monitor-partial-breaches-motivation-title {
+ cursor: default;
+}
+
+.monitor-partial-breaches-link-wrapper,
+.monitor-breaches-link-wrapper {
+ margin-block: auto;
+ grid-area: 1 / 5 / 1 / 7;
+ margin-inline: 0 15px;
+ font-size: 0.95em;
+ text-align: center;
+ display: flex;
+}
+
+#monitor-breaches-link,
+#monitor-partial-breaches-link {
+ color: inherit;
+ outline: none;
+ text-decoration: none;
+ width: 157.267px;
+ padding: 9px;
+}
+
+.lockwise-card #lockwise-header-content > a,
+.monitor-card #monitor-header-content > a {
+ display: block;
+ margin-block-start: 5px;
+ width: max-content;
+}
+
+.monitor-card.has-logins #monitor-body-content {
+ display: grid;
+ grid: 1fr auto / repeat(7, 1fr);
+ align-items: center;
+}
+
+.monitor-card .card-body {
+ padding-top: 0;
+ border-block-start: none;
+}
+
+.monitor-block {
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ text-align: center;
+ margin-inline-end: 15px;
+}
+
+.monitor-block a {
+ outline: none;
+ color: #FFFFFF;
+ padding: 19px 0;
+}
+
+.monitor-block a:hover {
+ text-decoration: none;
+ color: #FFFFFF;
+}
+
+.email {
+ background: linear-gradient(162.33deg, #AB71FF 0%, #9059FF 100%);
+ grid-column: 1;
+}
+
+.email:hover {
+ background: linear-gradient(162.33deg, #7D43D1 0%, #7740E6 100%);
+}
+
+.email:dir(rtl) {
+ background: linear-gradient(197.67deg, #AB71FF 0%, #9059FF 100%);
+}
+
+.email:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #7D43D1 0%, #7740E6 100%);
+}
+
+.breaches {
+ background: linear-gradient(162.33deg, #9059FF 0%, #7542E5 100%);
+ grid-column: 2;
+}
+
+.breaches:hover {
+ background: linear-gradient(162.33deg, #7740E6 0%, #4714B7 100%);
+}
+
+.breaches:dir(rtl) {
+ background: linear-gradient(197.67deg, #9059FF 0%, #7542E5 100%)
+}
+
+.breaches:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #7740E6 0%, #4714B7 100%)
+}
+
+.passwords {
+ background: linear-gradient(162.33deg, #7542E5 0%, #592ACB 100%);
+ grid-column: 3;
+}
+
+.passwords:hover {
+ background: linear-gradient(162.33deg, #4714B7 0%, #2B009D 100%);
+}
+
+.passwords:dir(rtl) {
+ background: linear-gradient(197.67deg, #7542E5 0%, #592ACB 100%)
+}
+
+.passwords:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #4714B7 0%, #2B009D 100%)
+}
+
+.monitor-stat {
+ display: flex;
+ font-size: 1.75em;
+ font-weight: bold;
+ margin-block-end: 5px;
+ word-break: break-all;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.monitor-icon {
+ margin-inline-end: 3px;
+}
+
+.icon-med {
+ width: 24px;
+ height: 24px;
+ -moz-context-properties: fill,fill-opacity;
+ fill: white;
+ fill-opacity: 0.65;
+ padding: 5px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.info-text {
+ font-size: 0.75em;
+ line-height: 13px;
+ margin: 0 10px;
+ display: inline-block;
+}
+
+.number-of-breaches.block {
+ font-size: 1.45em;
+ background-color: var(--orange-50);
+ padding: 15px;
+ grid-column: 1 / 2;
+ width: 70px;
+ height: 48px;
+}
+
+#manage-protections,
+#sign-up-for-monitor-link,
+#get-proxy-extension-link,
+#get-vpn-link,
+#vpn-banner-link,
+.monitor-partial-breaches-link-wrapper,
+.monitor-breaches-link-wrapper {
+ background-color: var(--in-content-primary-button-background);
+ border: 1px solid var(--in-content-primary-button-border-color);
+ border-radius: 4px;
+ text-decoration: none;
+ color: var(--in-content-primary-button-text-color);
+ font-weight: 600;
+}
+
+#manage-protections:hover,
+#sign-up-for-monitor-link:hover,
+#get-proxy-extension-link:hover,
+#get-vpn-link:hover,
+#vpn-banner-link:hover,
+#monitor-partial-breaches-link:hover,
+#monitor-breaches-link:hover {
+ background-color: var(--in-content-primary-button-background-hover);
+ color: var(--in-content-primary-button-text-color-hover);
+ border-color: var(--in-content-button-border-color-hover);
+}
+
+#manage-protections:hover:active,
+#sign-up-for-monitor-link:hover:active,
+#get-proxy-extension-link:hover:active,
+#get-vpn-link:hover:active,
+#vpn-banner-link:hover:active,
+#monitor-partial-breaches-link:hover:active,
+#monitor-breaches-link:hover:active {
+ background-color: var(--in-content-primary-button-background-active);
+ color: var(--in-content-primary-button-text-color-active);
+ border-color: var(--in-content-button-border-color-active);
+}
+
+#manage-protections:focus-visible,
+#sign-up-for-monitor-link:focus-visible,
+#get-proxy-extension-link:focus-visible,
+#get-vpn-link:focus-visible,
+#vpn-banner-link:focus-visible,
+#monitor-partial-breaches-link:focus-visible,
+.monitor-block > a:focus-visible,
+#monitor-breaches-link:focus-visible {
+ outline: var(--in-content-focus-outline);
+ outline-offset: var(--in-content-focus-outline-offset);
+}
+
+.monitor-card.loading::after,
+.lockwise-card.loading::after {
+ position: absolute;
+ height: 110px;
+ width: 765px;
+ content: "";
+ background-image: linear-gradient(to right, var(--in-content-box-background) 0%, var(--protection-report-loader-color-stop) 30%, var(--in-content-box-background) 40%, var(--in-content-box-background) 100%);
+ background-repeat: no-repeat;
+ animation-duration: 2s;
+ animation-iteration-count: infinite;
+ animation-name: loading;
+ animation-timing-function: cubic-bezier(.07,.95,1,1);
+ background-size: 700px 100%;
+ opacity: var(--protection-report-loader-gradient-opacity);
+}
+
+.monitor-card.loading:dir(rtl)::after,
+.lockwise-card.loading:dir(rtl)::after {
+ background-image: linear-gradient(to left, var(--in-content-box-background) 0%, var(--protection-report-loader-color-stop) 30%, var(--in-content-box-background) 40%, var(--in-content-box-background) 100%);
+ animation-name: loading-rtl;
+}
+
+@keyframes loading {
+ 0% {
+ background-position-x: -300px;
+ }
+
+ 100% {
+ background-position-x: 700px;
+ opacity: 0.02;
+ }
+}
+
+@keyframes loading-rtl {
+ 0% {
+ background-position-x: right -300px;
+ }
+
+ 100% {
+ background-position-x: right 700px;
+ opacity: 0.02;
+ }
+}
+
+.new-banner {
+ width: 100%;
+ background: var(--feature-banner-color);
+}
+
+.banner-wrapper {
+ width: 763px;
+ display: grid;
+ grid-template-columns: 1fr 7fr;
+ grid-gap: var(--card-padding);
+ line-height: 1.3em;
+ margin: 0 auto;
+ padding: 12px var(--card-padding);
+}
+
+.new-banner .banner-title {
+ margin: 0;
+ line-height: 1.25;
+ cursor: default;
+ font-size: inherit;
+}
+
+.new-banner .content {
+ margin-block: 5px 0;
+ font-size: 0.88em;
+ cursor: default;
+ color: var(--in-content-deemphasized-text);
+}
+
+.new-banner .exit-icon {
+ top: auto;
+ inset-inline-end: 30px;
+}
+
+.vpn-card .title-wrapper {
+ display: grid;
+ grid-template-columns: 24px auto;
+}
+
+.vpn-card:not(.subscribed) .card-title {
+ grid-area: 1 / 1 / 1 / -1;
+}
+
+.vpn-card.subscribed .card-title {
+ margin-inline-start: 3px;
+}
+
+.vpn-card:not(.subscribed) #check-icon {
+ display: none;
+}
diff --git a/browser/components/protections/content/protections.ftl b/browser/components/protections/content/protections.ftl
new file mode 100644
index 0000000000..b51ad81fe7
--- /dev/null
+++ b/browser/components/protections/content/protections.ftl
@@ -0,0 +1,26 @@
+# 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.
+
+-secure-proxy-brand-name = Firefox Private Network
+
+proxy-title = Stay safe on public Wi-Fi
+proxy-header-content = { -secure-proxy-brand-name } makes wireless hotspots more secure to protect you from hackers.
+get-proxy-extension-link = Get the Extension
+
+vpn-title = Take privacy protections beyond the browser
+vpn-header-content = Protect your entire device with { -mozilla-vpn-brand-name }. One tap encrypts all traffic and hides your location.
+get-vpn-link = Get { -mozilla-vpn-brand-name }
+
+vpn-title-subscribed = VPN: Subscribed
+# Note This text is not being translated, and the <br> will need to be removed if or when it does get translated
+vpn-header-content-subscribed = Using the { -mozilla-vpn-brand-name } encrypts all your traffic and hides your location — on up to 5 devices. Get the most from your subscription — add it from <br> the <a data-l10n-name="vpn-google-playstore-link">Google Play Store</a> or <a data-l10n-name="vpn-app-store-link">Apple App Store</a>.
+
+vpn-banner-header = Protection that extends beyond the browser
+# Note This text is not being translated, and the <br> will need to be removed if or when it does get translated
+vpn-banner-content = Try { -mozilla-vpn-brand-name } risk-free and see why TechRadar says, <br> “its speed, simplicity and low monthly price make it worth a look.”
+vpn-banner-link = Get { -mozilla-vpn-brand-name }
diff --git a/browser/components/protections/content/protections.html b/browser/components/protections/content/protections.html
new file mode 100644
index 0000000000..fd6dfbade2
--- /dev/null
+++ b/browser/components/protections/content/protections.html
@@ -0,0 +1,313 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome: blob:; object-src 'none'">
+ <meta name="color-scheme" content="light dark">
+ <link rel="localization" href="browser/branding/brandings.ftl"/>
+ <link rel="localization" href="branding/brand.ftl"/>
+ <link rel="localization" href="browser/branding/sync-brand.ftl">
+ <link rel="localization" href="browser/protections.ftl">
+ <!-- Temporary "en-US"-only l10n strings -->
+ <link rel="localization" href="preview/protections.ftl">
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
+ <link rel="stylesheet" href="chrome://browser/content/protections.css">
+ <link rel="icon" href="chrome://browser/skin/controlcenter/dashboard.svg">
+ <script type="module" src="chrome://browser/content/protections.mjs"></script>
+ <title data-l10n-id="protection-report-webpage-title"></title>
+ </head>
+
+ <body>
+ <div class="new-banner vpn-banner hidden">
+ <div class=banner-wrapper>
+ <img class="icon light" src="chrome://browser/content/logos/vpn-light.svg"/>
+ <img class="icon dark" src="chrome://browser/content/logos/vpn-dark.svg"/>
+ <div class="wrapper">
+ <div>
+ <h3 class="banner-title" data-l10n-id="vpn-banner-header"></h3>
+ <span class="content" data-l10n-id="vpn-banner-content"></span>
+ </div>
+ <a target="_blank" id="vpn-banner-link" data-l10n-id="get-vpn-link"></a>
+ <button class="exit-icon" data-l10n-id="protections-close-button2"></button>
+ </div>
+ </div>
+ </div>
+ <div id="report-content">
+ <h1 id="report-title" data-l10n-id="protection-report-page-content-title"></h1>
+ <p id="report-summary" data-l10n-id="protection-report-page-summary-default"></p>
+ <p id="protection-settings" data-l10n-id="protection-report-settings-link" role="link" tabindex="0"></p>
+ <div class="card card-no-hover etp-card">
+ <div class="card-header">
+ <img class="icon light" src="chrome://browser/content/logos/tracking-protection.svg"/>
+ <img class="icon dark" src="chrome://browser/content/logos/tracking-protection-dark-theme.svg"/>
+ <div class="wrapper">
+ <div>
+ <h2 class="card-title" data-l10n-id="etp-card-title-always"></h2>
+ <p id="etp-card-content" class="content" data-l10n-id="etp-card-content-description"></p>
+ </div>
+ <a target="_blank" id="manage-protections" data-l10n-id="protection-report-manage-protections"></a>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="body-wrapper">
+ <p id="graph-week-summary"></p>
+ <div id="graph-wrapper">
+ <div id="graph" role="table" aria-labelledby="graphLegendDescription">
+ <div id="private-window-message" data-l10n-id="graph-private-window"></div>
+ </div>
+ <div id="legend">
+ <label id="graphLegendDescription" data-l10n-id="graph-legend-description"></label>
+ <input id="tab-social" data-type="social" type="radio" name="tabs" aria-labelledby="socialLabel socialTitle" aria-describedby="socialContent" checked>
+ <label id="socialLabel" for="tab-social" data-type="social">
+ <img class="icon-small" src="chrome://browser/skin/thumb-down.svg" data-type="social"/>
+ <span data-type="social"></span>
+ </label>
+
+ <input id="tab-cookie" data-type="cookie" type="radio" name="tabs" aria-labelledby="cookieLabel cookieTitle" aria-describedby="cookieContent">
+ <label id="cookieLabel" for="tab-cookie" data-type="cookie">
+ <img class="icon-small" src="chrome://browser/skin/controlcenter/3rdpartycookies.svg" data-type="cookie"/>
+ <span data-type="cookie"></span>
+ </label>
+
+ <input id="tab-tracker" data-type="tracker" type="radio" name="tabs" aria-labelledby="trackerLabel trackerTitle" aria-describedby="trackerContent">
+ <label id="trackerLabel" for="tab-tracker" data-type="tracker">
+ <img class="icon-small" src="chrome://browser/skin/canvas.svg" data-type="tracker"/>
+ <span data-type="tracker"></span>
+ </label>
+
+ <input id="tab-fingerprinter" data-type="fingerprinter" type="radio" name="tabs" aria-labelledby="fingerprinterLabel fingerprinterTitle" aria-describedby="fingerprinterContent">
+ <label id="fingerprinterLabel" for="tab-fingerprinter" data-type="fingerprinter">
+ <img class="icon-small" src="chrome://browser/skin/fingerprint.svg" data-type="fingerprinter"/>
+ <span data-type="fingerprinter"></span>
+ </label>
+
+ <input id="tab-cryptominer" data-type="cryptominer" type="radio" name="tabs" aria-labelledby="cryptominerLabel cryptominerTitle" aria-describedby="cryptominerContent">
+ <label id="cryptominerLabel" for="tab-cryptominer" data-type="cryptominer">
+ <img class="icon-small" src="chrome://browser/skin/controlcenter/cryptominers.svg" data-type="cryptominer"/>
+ <span data-type="cryptominer"></span>
+ </label>
+ <div id=highlight></div>
+ <div id=highlight-hover></div>
+ <div id="social" class="tab-content">
+ <p id="socialTitle" class="content-title" data-l10n-id="social-tab-title"></p>
+ <p id="socialContent" data-l10n-id="social-tab-contant">
+ <a target="_blank" id="social-link" data-l10n-name="learn-more-link"></a>
+ </p>
+ </div>
+ <div id="cookie" class="tab-content">
+ <p id="cookieTitle" class="content-title" data-l10n-id="cookie-tab-title"></p>
+ <p id="cookieContent" data-l10n-id="cookie-tab-content">
+ <a target="_blank" id="cookie-link" data-l10n-name="learn-more-link"></a>
+ </p>
+ </div>
+ <div id="tracker" class="tab-content">
+ <p id="trackerTitle" class="content-title" data-l10n-id="tracker-tab-title"></p>
+ <p id="trackerContent" data-l10n-id="tracker-tab-description">
+ <a target="_blank" id="tracker-link" data-l10n-name="learn-more-link"></a>
+ </p>
+ </div>
+ <div id="fingerprinter" class="tab-content">
+ <p id="fingerprinterTitle" class="content-title" data-l10n-id="fingerprinter-tab-title"></p>
+ <p id="fingerprinterContent" data-l10n-id="fingerprinter-tab-content">
+ <a target="_blank" id="fingerprinter-link" data-l10n-name="learn-more-link"></a>
+ </p>
+ </div>
+ <div id="cryptominer" class="tab-content">
+ <p id="cryptominerTitle" class="content-title" data-l10n-id="cryptominer-tab-title"></p>
+ <p id="cryptominerContent" data-l10n-id="cryptominer-tab-content">
+ <a target="_blank" id="cryptominer-link" data-l10n-name="learn-more-link"></a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div id="graph-total-summary"></div>
+ </div>
+ </div>
+ <div id="mobile-hanger" class="card-body hidden">
+ <div class="body-wrapper">
+ <button class="exit-icon" data-l10n-id="protections-close-button2"></button>
+ <div id="etp-mobile-content">
+ <img class="mobile-app-icon" src="chrome://browser/content/logos/etp-mobile.svg"/>
+ <span>
+ <h2 class="card-title" data-l10n-id="mobile-app-title"></h2>
+ <p class="content">
+ <span data-l10n-id="mobile-app-card-content"></span>
+ <span target="_blank" id="mobile-app-links" data-l10n-id="mobile-app-links">
+ <a target="_blank" id="android-mobile-inline-link" data-l10n-name="android-mobile-inline-link" href=""></a>
+ <a target="_blank" id="ios-mobile-inline-link" data-l10n-name="ios-mobile-inline-link" href=""></a>
+ </span>
+ </p>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- Markup for Monitor card. -->
+ <section class="card card-no-hover monitor-card hidden">
+ <div class="card-header">
+ <img class="icon" src="chrome://browser/content/logos/monitor.svg"/>
+ <div class="wrapper">
+ <div>
+ <h2 id="monitor-title" class="card-title" data-l10n-id="monitor-title"></h2>
+ <p id="monitor-header-content" class="content">
+ <span>
+ <!-- Insert Monitor header content here. -->
+ </span>
+ <a target="_blank" href="" id="monitor-link" data-l10n-id="monitor-link"></a>
+ </p>
+ <div class="monitor-scanned-wrapper">
+ <img class="icon-small" src="chrome://global/skin/icons/reload.svg"/>
+ <span class="monitor-scanned-text" data-l10n-id="auto-scan"></span>
+ </div>
+ </div>
+ <a target="_blank" id="sign-up-for-monitor-link">
+ <!-- Insert Monitor link content here. -->
+ </a>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="body-wrapper">
+ <div id="monitor-body-content">
+ <div class="monitor-info-wrapper">
+ <div class="monitor-block email">
+ <a target="_blank" id="monitor-stored-emails-link" data-l10n-id="monitor-emails-tooltip">
+ <span class="monitor-stat">
+ <img class="icon-med" src="chrome://browser/skin/mail.svg"/>
+ <span data-type="stored-emails">
+ <!-- Display number of stored emails here. -->
+ </span>
+ </span>
+ <span id="info-monitored-addresses" class="info-text"></span>
+ </a>
+ </div>
+ <div class="monitor-block breaches">
+ <a target="_blank" id="monitor-known-breaches-link" data-l10n-id="monitor-breaches-tooltip">
+ <span class="monitor-stat">
+ <img class="icon-med" src="chrome://browser/skin/fxa/avatar.svg"/>
+ <span data-type="known-breaches">
+ <!-- Display number of known breaches here. -->
+ </span>
+ </span>
+ <span id="info-known-breaches" class="info-text"></span>
+ </a>
+ </div>
+ <div class="monitor-block passwords">
+ <a target="_blank" id="monitor-exposed-passwords-link" data-l10n-id="monitor-passwords-tooltip">
+ <span class="monitor-stat">
+ <img class="icon-med" src="chrome://browser/skin/login.svg"/>
+ <span data-type="exposed-passwords">
+ <!-- Display number of exposed passwords here. -->
+ </span>
+ </span>
+ <span id="info-exposed-passwords" class="info-text"></span>
+ </a>
+ </div>
+ </div>
+ <div class="monitor-breaches-wrapper">
+ <div class="monitor-breaches-header">
+ <img id="monitor-breaches-icon"/>
+ <span id="monitor-breaches-title"/>
+ </div>
+ <div class="monitor-breaches-description-wrapper">
+ <span id="monitor-breaches-description" class="content"></span>
+ <div class="monitor-breaches-link-wrapper">
+ <a id="monitor-breaches-link" target="_blank"></a>
+ </div>
+ </div>
+ </div>
+ <div class="monitor-partial-breaches-wrapper">
+ <div class="monitor-partial-breaches-header">
+ <img id="monitor-partial-breaches-icon" src="chrome://browser/skin/protections/resolved-breach-gray.svg"/>
+ <span id="monitor-partial-breaches-title"></span>
+ <span id="monitor-partial-breaches-percentage"></span>
+ </div>
+ <progress class="progress-bar" max="100"></progress>
+ <div class="monitor-partial-breaches-motivation-text">
+ <span id="monitor-partial-breaches-motivation-title"></span>
+ <div class="monitor-partial-breaches-motivation-wrapper">
+ <span id="monitor-partial-breaches-motivation-desc" class="content" data-l10n-id="monitor-partial-breaches-motivation-description"></span>
+ <div class="monitor-partial-breaches-link-wrapper">
+ <a id="monitor-partial-breaches-link" target="_blank" data-l10n-id="monitor-resolve-breaches-link"></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <!-- Markup for passwords card. -->
+ <section class="card card-no-hover lockwise-card hidden">
+ <div class="card-header">
+ <img class="icon" src="chrome://browser/content/logos/lockwise.svg"/>
+ <div class="wrapper">
+ <div>
+ <h2 id="lockwise-title" class="card-title">
+ <!-- Insert Lockwise card title here. -->
+ </h2>
+ <p id="lockwise-header-content" class="content">
+ <span>
+ <!-- Insert Lockwise header content here. -->
+ </span>
+ <a target="_blank" id="lockwise-how-it-works" data-l10n-id="lockwise-how-it-works-link" href=""></a>
+ </p>
+ </div>
+ <button id="save-passwords-button" class="primary" data-l10n-id="protection-report-passwords-save-passwords-button"></button>
+ <div class="lockwise-scanned-wrapper">
+ <img id="lockwise-scanned-icon" />
+ <span id="lockwise-scanned-text" class="content">
+ <!-- Display message for stored logins here. -->
+ </span>
+ </div>
+ <button id="manage-passwords-button" class="primary" data-l10n-id="protection-report-passwords-manage-passwords-button"></button>
+ </div>
+ </div>
+ <!-- <div class="card-body hidden">
+ <div id="lockwise-body-content" class="body-wrapper">
+ <div class="no-logins hidden">
+ </div>
+ </div>
+ </div> -->
+ </section>
+ <!-- Markup for Proxy card -->
+ <section class="card card-no-hover proxy-card hidden">
+ <div class="card-header">
+ <img class="icon light" src="chrome://browser/content/logos/proxy-light.svg"/>
+ <img class="icon dark" src="chrome://browser/content/logos/proxy-dark.svg"/>
+ <div class="wrapper">
+ <div>
+ <h3 class="card-title" data-l10n-id="proxy-title"></h3>
+ <p class="content" data-l10n-id="proxy-header-content"></p>
+ </div>
+ <a target="_blank" id="get-proxy-extension-link" data-l10n-id="get-proxy-extension-link"></a>
+ </div>
+ </div>
+ </section>
+
+ <section class="card card-no-hover vpn-card hidden">
+ <div class="card-header">
+ <img class="icon light" src="chrome://browser/content/logos/vpn-light.svg"/>
+ <img class="icon dark" src="chrome://browser/content/logos/vpn-dark.svg"/>
+ <div class="wrapper">
+ <div>
+ <div class="title-wrapper">
+ <img id="check-icon" src="chrome://browser/skin/protections/resolved-breach.svg">
+ <h3 class="card-title" data-l10n-id="vpn-title"></h3>
+ </div>
+ <p class="content" data-l10n-id="vpn-header-content"></p>
+ <p class="content subscribed" data-l10n-id="vpn-header-content-subscribed">
+ <a target="_blank" id="vpn-google-playstore-link" data-l10n-name="vpn-google-playstore-link"></a>
+ <a target="_blank" id="vpn-app-store-link" data-l10n-name="vpn-app-store-link"></a>
+ </p>
+ </div>
+ <a target="_blank" id="get-vpn-link" data-l10n-id="get-vpn-link"></a>
+ </div>
+ </div>
+ </section>
+ </div>
+ </body>
+</html>
diff --git a/browser/components/protections/content/protections.mjs b/browser/components/protections/content/protections.mjs
new file mode 100644
index 0000000000..5f4b8dc8e9
--- /dev/null
+++ b/browser/components/protections/content/protections.mjs
@@ -0,0 +1,493 @@
+/* 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-env mozilla/remote-page */
+
+import LockwiseCard from "./lockwise-card.mjs";
+import MonitorCard from "./monitor-card.mjs";
+import ProxyCard from "./proxy-card.mjs";
+import VPNCard from "./vpn-card.mjs";
+
+let cbCategory = RPMGetStringPref("browser.contentblocking.category");
+document.sendTelemetryEvent = (action, object, value = "") => {
+ // eslint-disable-next-line no-undef
+ RPMRecordTelemetryEvent("security.ui.protections", action, object, value, {
+ category: cbCategory,
+ });
+};
+
+let { protocol, pathname, searchParams } = new URL(document.location);
+
+let searchParamsChanged = false;
+if (searchParams.has("entrypoint")) {
+ RPMSendAsyncMessage("RecordEntryPoint", {
+ entrypoint: searchParams.get("entrypoint"),
+ });
+ // Remove this parameter from the URL (after recording above) to make it
+ // cleaner for bookmarking and switch-to-tab and so that bookmarked values
+ // don't skew telemetry.
+ searchParams.delete("entrypoint");
+ searchParamsChanged = true;
+}
+
+document.addEventListener("DOMContentLoaded", e => {
+ if (searchParamsChanged) {
+ let newURL = protocol + pathname;
+ let params = searchParams.toString();
+ if (params) {
+ newURL += "?" + params;
+ }
+ window.location.replace(newURL);
+ return;
+ }
+
+ RPMSendQuery("FetchEntryPoint", {}).then(entrypoint => {
+ // Send telemetry on arriving on this page
+ document.sendTelemetryEvent("show", "protection_report", entrypoint);
+ });
+
+ // We need to send the close telemetry before unload while we still have a connection to RPM.
+ window.addEventListener("beforeunload", () => {
+ document.sendTelemetryEvent("close", "protection_report");
+ });
+
+ let todayInMs = Date.now();
+ let weekAgoInMs = todayInMs - 6 * 24 * 60 * 60 * 1000;
+
+ let dataTypes = [
+ "cryptominer",
+ "fingerprinter",
+ "tracker",
+ "cookie",
+ "social",
+ ];
+
+ let manageProtectionsLink = document.getElementById("protection-settings");
+ let manageProtections = document.getElementById("manage-protections");
+ let protectionSettingsEvtHandler = evt => {
+ if (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click") {
+ RPMSendAsyncMessage("OpenContentBlockingPreferences");
+ if (evt.target.id == "protection-settings") {
+ document.sendTelemetryEvent(
+ "click",
+ "settings_link",
+ "header-settings"
+ );
+ } else if (evt.target.id == "manage-protections") {
+ document.sendTelemetryEvent(
+ "click",
+ "settings_link",
+ "custom-card-settings"
+ );
+ }
+ }
+ };
+ manageProtectionsLink.addEventListener("click", protectionSettingsEvtHandler);
+ manageProtectionsLink.addEventListener(
+ "keypress",
+ protectionSettingsEvtHandler
+ );
+ manageProtections.addEventListener("click", protectionSettingsEvtHandler);
+ manageProtections.addEventListener("keypress", protectionSettingsEvtHandler);
+
+ let legend = document.getElementById("legend");
+ legend.style.gridTemplateAreas =
+ "'social cookie tracker fingerprinter cryptominer'";
+
+ let createGraph = data => {
+ let graph = document.getElementById("graph");
+ let summary = document.getElementById("graph-total-summary");
+ let weekSummary = document.getElementById("graph-week-summary");
+
+ // User is in private mode, show no data on the graph
+ if (data.isPrivate) {
+ graph.classList.add("private-window");
+ } else {
+ let earliestDate = data.earliestDate || Date.now();
+ summary.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ count: data.sumEvents, earliestDate })
+ );
+ summary.setAttribute("data-l10n-id", "graph-total-tracker-summary");
+ }
+
+ // Set a default top size for the height of the graph bars so that small
+ // numbers don't fill the whole graph.
+ let largest = 100;
+ if (largest < data.largest) {
+ largest = data.largest;
+ }
+ let weekCount = 0;
+ let weekTypeCounts = {
+ social: 0,
+ cookie: 0,
+ tracker: 0,
+ fingerprinter: 0,
+ cryptominer: 0,
+ };
+
+ // For accessibility clients, we turn the graph into a fake table with annotated text.
+ // We use WAI-ARIA roles, properties, and states to mark up the table, rows and cells.
+ // Each day becomes one row in the table.
+ // Each row contains the day, total, and then one cell for each bar that we display.
+ // At most, a row can contain seven cells.
+ // But we need to caclulate the actual number of the most cells in a row to give accurate information.
+ let maxColumnCount = 0;
+ let date = new Date();
+ for (let i = 0; i <= 6; i++) {
+ let dateString = date.toISOString().split("T")[0];
+ let ariaOwnsString = ""; // Get the row's colummns in order
+ let currentColumnCount = 0;
+ let bar = document.createElement("div");
+ bar.className = "graph-bar";
+ bar.setAttribute("role", "row");
+ let innerBar = document.createElement("div");
+ innerBar.className = "graph-wrapper-bar";
+ if (data[dateString]) {
+ let content = data[dateString];
+ let count = document.createElement("div");
+ count.className = "bar-count";
+ count.id = "count" + i;
+ count.setAttribute("role", "cell");
+ count.textContent = content.total;
+ setTimeout(() => {
+ count.classList.add("animate");
+ }, 400);
+ bar.appendChild(count);
+ ariaOwnsString = count.id;
+ currentColumnCount += 1;
+ let barHeight = (content.total / largest) * 100;
+ weekCount += content.total;
+ // Add a short timeout to allow the elements to be added to the dom before triggering an animation.
+ setTimeout(() => {
+ bar.style.height = `${barHeight}%`;
+ }, 20);
+ for (let type of dataTypes) {
+ if (content[type]) {
+ let dataHeight = (content[type] / content.total) * 100;
+ // Since we are dealing with non-visual content, screen readers need a parent container to get the text
+ let cellSpan = document.createElement("span");
+ cellSpan.id = type + i;
+ cellSpan.setAttribute("role", "cell");
+ let div = document.createElement("div");
+ div.className = `${type}-bar inner-bar`;
+ div.setAttribute("role", "img");
+ div.setAttribute("data-type", type);
+ div.style.height = `${dataHeight}%`;
+ div.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ count: content[type], percentage: dataHeight })
+ );
+ div.setAttribute("data-l10n-id", `bar-tooltip-${type}`);
+ weekTypeCounts[type] += content[type];
+ cellSpan.appendChild(div);
+ innerBar.appendChild(cellSpan);
+ ariaOwnsString = ariaOwnsString + " " + cellSpan.id;
+ currentColumnCount += 1;
+ }
+ }
+ if (currentColumnCount > maxColumnCount) {
+ // The current row has more than any previous rows
+ maxColumnCount = currentColumnCount;
+ }
+ } else {
+ // There were no content blocking events on this day.
+ bar.classList.add("empty");
+ }
+ bar.appendChild(innerBar);
+ graph.prepend(bar);
+
+ if (data.isPrivate) {
+ weekSummary.setAttribute(
+ "data-l10n-id",
+ "graph-week-summary-private-window"
+ );
+ } else {
+ weekSummary.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ count: weekCount })
+ );
+ weekSummary.setAttribute("data-l10n-id", "graph-week-summary");
+ }
+
+ let label = document.createElement("span");
+ label.className = "column-label";
+ // While the graphs fill up from the right, the days fill up from the left, so match the IDs
+ label.id = "day" + (6 - i);
+ label.setAttribute("role", "rowheader");
+ if (i == 6) {
+ label.setAttribute("data-l10n-id", "graph-today");
+ } else {
+ label.textContent = data.weekdays[(i + 1 + new Date().getDay()) % 7];
+ }
+ graph.append(label);
+ // Make the day the first column in a row, making it the row header.
+ bar.setAttribute("aria-owns", "day" + i + " " + ariaOwnsString);
+ date.setDate(date.getDate() - 1);
+ }
+ maxColumnCount += 1; // Add the day column in the fake table
+ graph.setAttribute("aria-colCount", maxColumnCount);
+ // Set the total number of each type of tracker on the tabs as well as their
+ // "Learn More" links
+ for (let type of dataTypes) {
+ document.querySelector(`label[data-type=${type}] span`).textContent =
+ weekTypeCounts[type];
+ const learnMoreLink = document.getElementById(`${type}-link`);
+ learnMoreLink.href = RPMGetFormatURLPref(
+ `browser.contentblocking.report.${type}.url`
+ );
+ learnMoreLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "trackers_about_link", type);
+ });
+ }
+
+ let blockingCookies =
+ RPMGetIntPref("network.cookie.cookieBehavior", 0) != 0;
+ let cryptominingEnabled = RPMGetBoolPref(
+ "privacy.trackingprotection.cryptomining.enabled",
+ false
+ );
+ let fingerprintingEnabled = RPMGetBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ false
+ );
+ let tpEnabled = RPMGetBoolPref("privacy.trackingprotection.enabled", false);
+ let socialTracking = RPMGetBoolPref(
+ "privacy.trackingprotection.socialtracking.enabled",
+ false
+ );
+ let socialCookies = RPMGetBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ false
+ );
+ let socialEnabled =
+ socialCookies && (blockingCookies || (tpEnabled && socialTracking));
+ let notBlocking =
+ !blockingCookies &&
+ !cryptominingEnabled &&
+ !fingerprintingEnabled &&
+ !tpEnabled &&
+ !socialEnabled;
+
+ // User has turned off all blocking, show a different card.
+ if (notBlocking) {
+ document
+ .getElementById("etp-card-content")
+ .setAttribute(
+ "data-l10n-id",
+ "protection-report-etp-card-content-custom-not-blocking"
+ );
+ document
+ .querySelector(".etp-card .card-title")
+ .setAttribute("data-l10n-id", "etp-card-title-custom-not-blocking");
+ document
+ .getElementById("report-summary")
+ .setAttribute("data-l10n-id", "protection-report-page-summary");
+ document.querySelector(".etp-card").classList.add("custom-not-blocking");
+
+ // Hide the link to settings from the header, so we are not showing two links.
+ manageProtectionsLink.style.display = "none";
+ } else {
+ // Hide each type of tab if blocking of that type is off.
+ if (!tpEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "tracker",
+ ""
+ );
+ let radio = document.getElementById("tab-tracker");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-tracker ~ label").style.display = "none";
+ }
+ if (!socialEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "social",
+ ""
+ );
+ let radio = document.getElementById("tab-social");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-social ~ label").style.display = "none";
+ }
+ if (!blockingCookies) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "cookie",
+ ""
+ );
+ let radio = document.getElementById("tab-cookie");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-cookie ~ label").style.display = "none";
+ }
+ if (!cryptominingEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "cryptominer",
+ ""
+ );
+ let radio = document.getElementById("tab-cryptominer");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-cryptominer ~ label").style.display =
+ "none";
+ }
+ if (!fingerprintingEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "fingerprinter",
+ ""
+ );
+ let radio = document.getElementById("tab-fingerprinter");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-fingerprinter ~ label").style.display =
+ "none";
+ }
+
+ let firstRadio = document.querySelector("input:enabled");
+ // There will be no radio options if we are showing the
+ firstRadio.checked = true;
+ document.body.setAttribute("focuseddatatype", firstRadio.dataset.type);
+
+ addListeners();
+ }
+ };
+
+ let addListeners = () => {
+ let wrapper = document.querySelector(".body-wrapper");
+ let triggerTabClick = ev => {
+ if (ev.originalTarget.dataset.type) {
+ document.getElementById(`tab-${ev.target.dataset.type}`).click();
+ }
+ };
+
+ let triggerTabFocus = ev => {
+ if (ev.originalTarget.dataset) {
+ wrapper.classList.add("hover-" + ev.originalTarget.dataset.type);
+ }
+ };
+
+ let triggerTabBlur = ev => {
+ if (ev.originalTarget.dataset) {
+ wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type);
+ }
+ };
+ wrapper.addEventListener("mouseout", triggerTabBlur);
+ wrapper.addEventListener("mouseover", triggerTabFocus);
+ wrapper.addEventListener("click", triggerTabClick);
+
+ // Change the class on the body to change the color variable.
+ let radios = document.querySelectorAll("#legend input");
+ for (let radio of radios) {
+ radio.addEventListener("change", ev => {
+ document.body.setAttribute("focuseddatatype", ev.target.dataset.type);
+ });
+ radio.addEventListener("focus", ev => {
+ wrapper.classList.add("hover-" + ev.originalTarget.dataset.type);
+ document.body.setAttribute("focuseddatatype", ev.target.dataset.type);
+ });
+ radio.addEventListener("blur", ev => {
+ wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type);
+ });
+ }
+ };
+
+ RPMSendQuery("FetchContentBlockingEvents", {
+ from: weekAgoInMs,
+ to: todayInMs,
+ }).then(createGraph);
+
+ let exitIcon = document.querySelector("#mobile-hanger .exit-icon");
+ // hide the mobile promotion and keep hidden with a pref.
+ exitIcon.addEventListener("click", () => {
+ RPMSetBoolPref("browser.contentblocking.report.show_mobile_app", false);
+ document.getElementById("mobile-hanger").classList.add("hidden");
+ });
+
+ let androidMobileAppLink = document.getElementById(
+ "android-mobile-inline-link"
+ );
+ androidMobileAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.mobile-android.url"
+ );
+ androidMobileAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "mobile_app_link", "android");
+ });
+ let iosMobileAppLink = document.getElementById("ios-mobile-inline-link");
+ iosMobileAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.mobile-ios.url"
+ );
+ iosMobileAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "mobile_app_link", "ios");
+ });
+
+ let lockwiseEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.lockwise.enabled",
+ true
+ );
+
+ let lockwiseCard;
+ if (lockwiseEnabled) {
+ const lockwiseUI = document.querySelector(".lockwise-card");
+ lockwiseUI.classList.remove("hidden");
+ lockwiseUI.classList.add("loading");
+
+ lockwiseCard = new LockwiseCard(document);
+ lockwiseCard.init();
+ }
+
+ RPMSendQuery("FetchUserLoginsData", {}).then(data => {
+ if (lockwiseCard) {
+ // Once data for the user is retrieved, display the lockwise card.
+ lockwiseCard.buildContent(data);
+ }
+
+ if (
+ RPMGetBoolPref("browser.contentblocking.report.show_mobile_app") &&
+ !data.mobileDeviceConnected
+ ) {
+ document
+ .getElementById("mobile-hanger")
+ .classList.toggle("hidden", false);
+ }
+ });
+
+ // For tests
+ const lockwiseUI = document.querySelector(".lockwise-card");
+ lockwiseUI.dataset.enabled = lockwiseEnabled;
+
+ let monitorEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.monitor.enabled",
+ true
+ );
+ if (monitorEnabled) {
+ // Show the Monitor card.
+ const monitorUI = document.querySelector(".card.monitor-card.hidden");
+ monitorUI.classList.remove("hidden");
+ monitorUI.classList.add("loading");
+
+ const monitorCard = new MonitorCard(document);
+ monitorCard.init();
+ }
+
+ // For tests
+ const monitorUI = document.querySelector(".monitor-card");
+ monitorUI.dataset.enabled = monitorEnabled;
+
+ const proxyEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.proxy.enabled",
+ true
+ );
+
+ if (proxyEnabled) {
+ const proxyCard = new ProxyCard(document);
+ proxyCard.init();
+ }
+
+ // For tests
+ const proxyUI = document.querySelector(".proxy-card");
+ proxyUI.dataset.enabled = proxyEnabled;
+
+ const VPNEnabled = RPMGetBoolPref("browser.vpn_promo.enabled", true);
+ if (VPNEnabled) {
+ const vpnCard = new VPNCard(document);
+ vpnCard.init();
+ }
+ // For tests
+ const vpnUI = document.querySelector(".vpn-card");
+ vpnUI.dataset.enabled = VPNEnabled;
+});
diff --git a/browser/components/protections/content/proxy-card.mjs b/browser/components/protections/content/proxy-card.mjs
new file mode 100644
index 0000000000..7f08e1f95c
--- /dev/null
+++ b/browser/components/protections/content/proxy-card.mjs
@@ -0,0 +1,29 @@
+/* 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-env mozilla/remote-page */
+
+const PROXY_EXTENSION_URL = RPMGetStringPref(
+ "browser.contentblocking.report.proxy_extension.url",
+ ""
+);
+
+export default class ProxyCard {
+ constructor(document) {
+ this.doc = document;
+ }
+
+ init() {
+ const proxyExtensionLink = this.doc.getElementById(
+ "get-proxy-extension-link"
+ );
+ proxyExtensionLink.href = PROXY_EXTENSION_URL;
+
+ // Show the Proxy card
+ RPMSendQuery("GetShowProxyCard", {}).then(shouldShow => {
+ const proxyCard = this.doc.querySelector(".proxy-card");
+ proxyCard.classList.toggle("hidden", !shouldShow);
+ });
+ }
+}
diff --git a/browser/components/protections/content/vpn-card.mjs b/browser/components/protections/content/vpn-card.mjs
new file mode 100644
index 0000000000..9b7602f75c
--- /dev/null
+++ b/browser/components/protections/content/vpn-card.mjs
@@ -0,0 +1,102 @@
+/* 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-env mozilla/remote-page */
+
+export default class VPNCard {
+ constructor(document) {
+ this.doc = document;
+ }
+
+ init() {
+ const vpnLink = this.doc.getElementById("get-vpn-link");
+ const vpnBannerLink = this.doc.getElementById("vpn-banner-link");
+ vpnLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn.url",
+ ""
+ );
+ vpnBannerLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-promo.url",
+ ""
+ );
+
+ vpnLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "vpn_card_link");
+ });
+ let androidVPNAppLink = document.getElementById(
+ "vpn-google-playstore-link"
+ );
+ androidVPNAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-android.url"
+ );
+ androidVPNAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "vpn_app_link_android");
+ });
+ let iosVPNAppLink = document.getElementById("vpn-app-store-link");
+ iosVPNAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-ios.url"
+ );
+ iosVPNAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "vpn_app_link_ios");
+ });
+
+ const vpnBanner = this.doc.querySelector(".vpn-banner");
+ const exitIcon = vpnBanner.querySelector(".exit-icon");
+ vpnBannerLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "vpn_banner_link");
+ });
+ // User has closed the vpn banner, hide it.
+ exitIcon.addEventListener("click", () => {
+ vpnBanner.classList.add("hidden");
+ this.doc.sendTelemetryEvent("click", "vpn_banner_close");
+ });
+
+ this.showVPNCard();
+ }
+
+ // Show the VPN card if user is located in areas, and on platforms, it serves
+ async showVPNCard() {
+ const showVPNBanner = this.showVPNBanner.bind(this);
+ RPMSendQuery("FetchShowVPNCard", {}).then(shouldShow => {
+ if (!shouldShow) {
+ return;
+ }
+ const vpnCard = this.doc.querySelector(".vpn-card");
+
+ // add 'subscribed' class if user is subscribed to vpn
+ RPMSendQuery("FetchVPNSubStatus", {}).then(async hasVPN => {
+ if (hasVPN) {
+ vpnCard.classList.add("subscribed");
+ vpnCard
+ .querySelector(".card-title")
+ .setAttribute("data-l10n-id", "vpn-title-subscribed");
+
+ // hide the promo banner if the user is already subscribed to vpn
+ await RPMSetBoolPref(
+ "browser.contentblocking.report.hide_vpn_banner",
+ true
+ );
+ }
+
+ vpnCard.classList.remove("hidden");
+ showVPNBanner();
+ });
+ });
+ }
+
+ showVPNBanner() {
+ if (
+ RPMGetBoolPref("browser.contentblocking.report.hide_vpn_banner", false) ||
+ !RPMGetBoolPref("browser.vpn_promo.enabled", false)
+ ) {
+ return;
+ }
+
+ const vpnBanner = this.doc.querySelector(".vpn-banner");
+ vpnBanner.classList.remove("hidden");
+ this.doc.sendTelemetryEvent("show", "vpn_banner");
+ // VPN banner only shows on the first visit, flip a pref so it does not show again.
+ RPMSetBoolPref("browser.contentblocking.report.hide_vpn_banner", true);
+ }
+}