Bug 1559418 - Create a base card for Firefox Lockwise. r=ewright
☠☠ backed out by e53142c91caf ☠ ☠
authorMicah Tigley <mtigley@mozilla.com>
Tue, 09 Jul 2019 16:55:49 +0000
changeset 481956 f8d8b15ccc84deeed4e98f256ace76ec7ea3733b
parent 481955 e4a2b09bc49c736cdbaaaed9730f20190fb4ce08
child 481957 e97f23bfa99cf7fb97fc2a176be4f2497ec19776
push id113647
push useraciure@mozilla.com
push dateWed, 10 Jul 2019 09:46:39 +0000
treeherdermozilla-inbound@f3a387c13e2c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersewright
bugs1559418
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1559418 - Create a base card for Firefox Lockwise. r=ewright Differential Revision: https://phabricator.services.mozilla.com/D37004
browser/base/content/logos/lockwise.svg
browser/base/jar.mn
browser/components/about/AboutProtectionsHandler.jsm
browser/components/protections/.eslintrc.js
browser/components/protections/content/lockwise-card.js
browser/components/protections/content/protections.css
browser/components/protections/content/protections.html
browser/components/protections/content/protections.js
browser/components/protections/jar.mn
browser/components/protections/moz.build
browser/components/protections/test/browser/browser.ini
browser/components/protections/test/browser/browser_protections_lockwise.js
browser/components/protections/test/browser/head.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/logos/lockwise.svg
@@ -0,0 +1,1 @@
+<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.25" x2="18.88" y1="55.37" y2="11.44"><stop offset="0" stop-color="#ff980e"/><stop offset=".11" stop-color="#ff851b"/><stop offset=".57" stop-color="#ff3750"/><stop offset=".8" stop-color="#f92261"/><stop offset="1" stop-color="#f5156c"/></linearGradient><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="44.12" x2="23.37" y1="62.59" y2="13.68"><stop offset="0" stop-color="#fff261" stop-opacity=".8"/><stop offset=".06" stop-color="#fff261" stop-opacity=".68"/><stop offset=".19" stop-color="#fff261" stop-opacity=".48"/><stop offset=".31" stop-color="#fff261" stop-opacity=".31"/><stop offset=".42" stop-color="#fff261" stop-opacity=".17"/><stop offset=".53" stop-color="#fff261" stop-opacity=".08"/><stop offset=".63" stop-color="#fff261" stop-opacity=".02"/><stop offset=".72" stop-color="#fff261" stop-opacity="0"/></linearGradient><linearGradient id="c" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="54.08" x2="54.08" y1="8.93" y2="42.2"><stop offset="0" stop-color="#0090ed"/><stop offset=".5" stop-color="#9059ff"/><stop offset=".81" stop-color="#b833e1"/></linearGradient><linearGradient id="d" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="16.46" x2="37.88" y1="7.08" y2="43.53"><stop offset=".02" stop-color="#0090ed"/><stop offset=".49" stop-color="#9059ff"/><stop offset="1" stop-color="#b833e1"/></linearGradient><linearGradient id="e" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="19.25" x2="6.77" y1="21.12" y2="33.61"><stop offset=".14" stop-color="#592acb" stop-opacity="0"/><stop offset=".33" stop-color="#542bc8" stop-opacity=".03"/><stop offset=".53" stop-color="#462fbf" stop-opacity=".11"/><stop offset=".74" stop-color="#2f35b1" stop-opacity=".25"/><stop offset=".95" stop-color="#0f3d9c" stop-opacity=".44"/><stop offset="1" stop-color="#054096" stop-opacity=".5"/></linearGradient><linearGradient id="f" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="57" x2="50.71" y1="34.92" y2="24.03"><stop offset="0" stop-color="#722291" stop-opacity=".5"/><stop offset=".5" stop-color="#b833e1" stop-opacity="0"/></linearGradient><linearGradient id="g" gradientTransform="matrix(1 0 0 -1 0 66)" gradientUnits="userSpaceOnUse" x1="43.72" x2="36.42" y1="19.33" y2="11.1"><stop offset="0" stop-color="#054096" stop-opacity=".5"/><stop offset=".03" stop-color="#0f3d9c" stop-opacity=".44"/><stop offset=".17" stop-color="#2f35b1" stop-opacity=".25"/><stop offset=".3" stop-color="#462fbf" stop-opacity=".11"/><stop offset=".43" stop-color="#542bc8" stop-opacity=".03"/><stop offset=".56" stop-color="#592acb" stop-opacity="0"/></linearGradient><path d="M57.45 25.11A218.35 218.35 0 0 0 38.82 6.48a10.81 10.81 0 0 0-13.77 0A219.81 219.81 0 0 0 6.42 25.11a10.83 10.83 0 0 0 0 13.78 218.35 218.35 0 0 0 18.63 18.63 10.84 10.84 0 0 0 13.8 0c3.43-3.1 6.56-6.09 9.57-9.15a3.1 3.1 0 0 0-.24-4.27l-9.25-8.63a10.62 10.62 0 0 0 3.56-8.4 10.78 10.78 0 0 0-10.08-10.26 10.7 10.7 0 0 0-8.37 18c.21.22.42.42.64.62l-3.35 3a2.7 2.7 0 0 0 3.61 4l3.7-3.35.1-.1a5.07 5.07 0 0 0 1.48-3.79 5.2 5.2 0 0 0-1.78-3.71 5.3 5.3 0 1 1 7.47-.63 4.24 4.24 0 0 1-.64.63 5.2 5.2 0 0 0-1.83 3.73A5 5 0 0 0 34.92 39l.06.07 7.77 7.27c-2.38 2.36-4.86 4.7-7.5 7.09a5.51 5.51 0 0 1-6.61 0 214 214 0 0 1-18.2-18.19 5.55 5.55 0 0 1 0-6.62 214 214 0 0 1 18.2-18.19 5.51 5.51 0 0 1 6.61 0 213.86 213.86 0 0 1 18.19 18.23 5.54 5.54 0 0 1 0 6.61c-.93 1-1.86 2.1-2.8 3.06a2.7 2.7 0 1 0 4 3.65c.92-1 1.87-2 2.8-3.12a10.84 10.84 0 0 0 .01-13.75z" fill="url(#a)"/><path d="M57.56 25.1A218.7 218.7 0 0 0 38.91 6.46a10.82 10.82 0 0 0-13.79 0A217.26 217.26 0 0 0 6.48 25.11a10.82 10.82 0 0 0 0 13.79 217.26 217.26 0 0 0 18.65 18.64 10.85 10.85 0 0 0 13.81 0c3.43-3.09 6.56-6.09 9.58-9.15a3.11 3.11 0 0 0-.24-4.28L39 35.45a10.62 10.62 0 0 0 3.56-8.4A10.79 10.79 0 0 0 32.5 16.79a10.71 10.71 0 0 0-8.38 18.05c.2.21.41.42.63.61l-3.35 3a2.71 2.71 0 0 0 3.62 4l3.7-3.36.1-.09a5.08 5.08 0 0 0 1.48-3.8 5.2 5.2 0 0 0-1.78-3.71 5.3 5.3 0 1 1 7.48-.58 5.43 5.43 0 0 1-.64.63 5.2 5.2 0 0 0-1.83 3.73A5.08 5.08 0 0 0 35 39.05l.07.06 7.77 7.29c-2.38 2.36-4.86 4.7-7.5 7.09a5.54 5.54 0 0 1-6.63 0A217.3 217.3 0 0 1 10.5 35.28a5.55 5.55 0 0 1 0-6.62 214.33 214.33 0 0 1 18.21-18.21 5.52 5.52 0 0 1 6.62 0 214.33 214.33 0 0 1 18.21 18.21 5.52 5.52 0 0 1 0 6.62c-.93 1-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.93-1 1.88-2.05 2.8-3.13a10.84 10.84 0 0 0 .02-13.76z" fill="url(#b)"/><path d="M53.41 28.69a5.51 5.51 0 0 1 0 6.61c-.93 1.05-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.92-1 1.87-2 2.8-3.13 3.34-3.73-4-10.19-4-10.19z" fill="url(#c)"/><path d="M42.75 46.38c-2.38 2.36-4.86 4.7-7.5 7.09a5.51 5.51 0 0 1-6.61 0 214 214 0 0 1-18.2-18.19 5.55 5.55 0 0 1 0-6.62l-1.22 1.4a9 9 0 0 0 .15 12.08 216.71 216.71 0 0 0 15.68 15.38 10.84 10.84 0 0 0 13.8 0c1.95-1.77 4.12-3.8 6.07-5.68a2.35 2.35 0 0 0 .08-3.35l-.06-.07z" fill="url(#d)"/><path d="M9.39 42.17c2 2.21 4.08 4.33 6.17 6.4l.83-1.35c.7-1.12 1.4-2.21 2.15-3.3-2.69-2.73-5.36-5.56-8.08-8.6a5.55 5.55 0 0 1 0-6.62l-1.22 1.4a9 9 0 0 0 .13 12z" fill="url(#e)" opacity=".9"/><path d="M53.41 28.69a5.51 5.51 0 0 1 0 6.61c-.93 1.05-1.86 2.1-2.8 3.06a2.7 2.7 0 0 0 4 3.65c.92-1 1.87-2 2.8-3.13 3.34-3.73-4-10.19-4-10.19z" fill="url(#f)"/><path d="M44.89 48.42l-2.14-2c-2.38 2.36-4.86 4.7-7.5 7.09a5.4 5.4 0 0 1-4.25 1v5.43c.31 0 .62.05.93.05a10.45 10.45 0 0 0 6.91-2.49c2-1.77 4.12-3.8 6.07-5.68A2.35 2.35 0 0 0 45 48.5a.6.6 0 0 1-.11-.08z" fill="url(#g)" opacity=".9"/></svg>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -14,16 +14,17 @@ browser.jar:
         content/browser/aboutRobots.js                (content/aboutRobots.js)
         content/browser/aboutRobots.css               (content/aboutRobots.css)
 
         content/browser/illustrations/error-connection-failure.svg (content/illustrations/error-connection-failure.svg)
         content/browser/illustrations/error-server-not-found.svg (content/illustrations/error-server-not-found.svg)
         content/browser/illustrations/error-malformed-url.svg (content/illustrations/error-malformed-url.svg)
         content/browser/illustrations/under-construction.svg (content/illustrations/under-construction.svg)
         content/browser/illustrations/blue-berror.svg (content/illustrations/blue-berror.svg)
+        content/browser/logos/lockwise.svg             (content/logos/lockwise.svg)
         content/browser/aboutNetError.xhtml            (content/aboutNetError.xhtml)
         content/browser/aboutNetError.js               (content/aboutNetError.js)
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutFrameCrashed.html        (content/aboutFrameCrashed.html)
         content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
--- a/browser/components/about/AboutProtectionsHandler.jsm
+++ b/browser/components/about/AboutProtectionsHandler.jsm
@@ -4,22 +4,29 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AboutProtectionsHandler"];
 
 const { RemotePages } = ChromeUtils.import(
   "resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm"
 );
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var AboutProtectionsHandler = {
   _inited: false,
-  _topics: ["openContentBlockingPreferences"],
+  _topics: [
+    "openContentBlockingPreferences",
+    "OpenAboutLogins",
+    "OpenSyncPreferences",
+    "FetchUserLoginsData",
+  ],
 
   init() {
+    this.receiveMessage = this.receiveMessage.bind(this);
     this.pageListener = new RemotePages("about:protections");
     for (let topic of this._topics) {
       this.pageListener.addMessageListener(topic, this.receiveMessage);
     }
     this._inited = true;
   },
 
   uninit() {
@@ -27,19 +34,50 @@ var AboutProtectionsHandler = {
       return;
     }
     for (let topic of this._topics) {
       this.pageListener.removeMessageListener(topic, this.receiveMessage);
     }
     this.pageListener.destroy();
   },
 
+  /**
+   * Retrieves login data for the user.
+   *
+   * @return {{ isLoggedIn: Boolean,
+   *            numberOfLogins: Number,
+   *            numberOfSyncedDevices: Number }}
+   *         The login data.
+   */
+  getLoginData() {
+    const logins = Services.logins.countLogins("", "", "");
+
+    const isLoggedIn = logins > 0;
+    return {
+      isLoggedIn,
+      numberOfLogins: logins,
+      numberOfSyncedDevices: 0,
+    };
+  },
+
   receiveMessage(aMessage) {
     let win = aMessage.target.browser.ownerGlobal;
     switch (aMessage.name) {
       case "openContentBlockingPreferences":
         win.openPreferences("privacy-trackingprotection", {
           origin: "about-protections",
         });
         break;
+      case "OpenAboutLogins":
+        win.openTrustedLinkIn("about:logins", "tab");
+        break;
+      case "OpenSyncPreferences":
+        win.openTrustedLinkIn("about:preferences#sync", "tab");
+        break;
+      case "FetchUserLoginsData":
+        aMessage.target.sendAsyncMessage(
+          "SendUserLoginsData",
+          this.getLoginData()
+        );
+        break;
     }
   },
 };
new file mode 100644
--- /dev/null
+++ b/browser/components/protections/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  parserOptions: {
+    sourceType: "module",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/protections/content/lockwise-card.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/frame-script */
+
+export default class LockwiseCard {
+  constructor(document) {
+    this.doc = document;
+  }
+
+  /**
+   * Initializes message listeners/senders.
+   */
+  init() {
+    const openAboutLoginsButton = this.doc.getElementById(
+      "open-about-logins-button"
+    );
+    openAboutLoginsButton.addEventListener("click", () => {
+      RPMSendAsyncMessage("OpenAboutLogins");
+    });
+
+    const syncLink = this.doc.querySelector(".synced-devices-text a");
+    // Register a click handler for the anchor since it's not possible to navigate to about:preferences via href
+    syncLink.addEventListener("click", () => {
+      RPMSendAsyncMessage("OpenSyncPreferences");
+    });
+
+    RPMAddMessageListener("SendUserLoginsData", ({ data }) => {
+      // Once browser data for the user is retrieved, display it on the card's body
+      // section.
+      this.buildContent(data);
+    });
+
+    // Dispatch messages to retrieve data for the Lockwise  card.
+    RPMSendAsyncMessage("FetchUserLoginsData");
+  }
+
+  buildContent(data) {
+    const { isLoggedIn, numberOfLogins, numberOfSyncedDevices } = data;
+    const title = this.doc.getElementById("lockwise-title");
+    const headerContent = this.doc.getElementById("lockwise-header-content");
+    const lockwiseBodyContent = this.doc.getElementById(
+      "lockwise-body-content"
+    );
+
+    // Get the container for the content to display.
+    const container = isLoggedIn
+      ? lockwiseBodyContent.querySelector(".has-logins")
+      : lockwiseBodyContent.querySelector(".no-logins");
+    // Display the content
+    container.classList.remove("hidden");
+
+    if (isLoggedIn) {
+      title.textContent = "Firefox Lockwise";
+      headerContent.textContent =
+        "Securely store and sync your passwords to all your devices.";
+      this.renderContentForLoggedInUser(
+        container,
+        numberOfLogins,
+        numberOfSyncedDevices
+      );
+    } else {
+      title.textContent = "Never forget a password again";
+      headerContent.textContent =
+        "Firefox Lockwise securely stores your passwords in your browser.";
+    }
+  }
+
+  /**
+   * Displays the number of stored logins and synced devices for a user.
+   *
+   * @param {Element} container
+   *        The containing element for the content.
+   * @param {Number}  storedLogins
+   *        The number of browser-stored logins.
+   * @param {Number}  syncedDevices
+   *        The number of synced devices.
+   */
+  renderContentForLoggedInUser(container, storedLogins, syncedDevices) {
+    // Set the text for number of stored logins.
+    const numberOfLoginsBlock = container.querySelector(
+      ".number-of-logins.block"
+    );
+    numberOfLoginsBlock.textContent = storedLogins;
+
+    // Set the text for the number of synced devices.
+    const syncedDevicesBlock = container.querySelector(
+      ".number-of-synced-devices.block"
+    );
+    syncedDevicesBlock.textContent = syncedDevices;
+
+    const syncedDevicesText = container.querySelector(".synced-devices-text");
+    const textEl = syncedDevicesText.querySelector("span");
+    textEl.textContent =
+      syncedDevices > 0
+        ? `Syncing to ${syncedDevices} other devices.`
+        : "Not syncing to other devices.";
+
+    // Display the link for enabling sync if no synced devices are detected.
+    if (syncedDevices === 0) {
+      const syncLink = syncedDevicesText.querySelector("a");
+      syncLink.classList.remove("hidden");
+    }
+  }
+}
--- a/browser/components/protections/content/protections.css
+++ b/browser/components/protections/content/protections.css
@@ -10,23 +10,39 @@
   --report-background: #FAFAFC;
   --card-padding: 22px;
   --social-color: #AB71FF;
   --social-color-darker: #7F27FF;
   --crossSite-color: #0090F4;
   --crossSite-color-darker: #0073C3;
   --tracker-color: #2AC3A2;
   --tracker-color-darker: #229C82;
-  --fingerprinter-color: #FFBD4F;
-  --fingerprinter-color-darker: #ffA40C;
-  --cryptominer-color: #AFAFBB;
-  --cryptominer-color-darker: #88889A;
+  --orange: #FFBD4F;
+  --dark-orange: #ffA40C;
+  --grey: #AFAFBB;
+  --dark-grey: #88889A;
   --tab-highlight: var(--social-color); /* start with social selected */
+  --blue-60: #0060DF;
+  --blue-70: #003eaa;
+  --blue-80: #002275;
 }
 
+ a {
+  color: var(--blue-60);
+  text-decoration: none;
+ }
+
+ a:active {
+  color: var(--blue-70);
+ }
+
+ a:hover, a:active {
+  text-decoration: underline;
+ }
+
 body {
   background-color: var(--report-background);
   font: message-box;
   margin-top: 82px;
 }
 
 body[focuseddatatype=social] {
   --tab-highlight: var(--social-color);
@@ -36,49 +52,82 @@ body[focuseddatatype=crossSite] {
   --tab-highlight: var(--crossSite-color);
 }
 
 body[focuseddatatype=tracker] {
   --tab-highlight: var(--tracker-color);
 }
 
 body[focuseddatatype=fingerprinter] {
-  --tab-highlight: var(--fingerprinter-color);
+  --tab-highlight: var(--orange);
 }
 
 body[focuseddatatype=cryptominer] {
-  --tab-highlight: var(--cryptominer-color);
+  --tab-highlight: var(--grey);
 }
 
 #report-title {
   font-size: 20px;
   font-weight: 300;
   margin-bottom: 22px;
 }
 
 #report-content {
   width: 763px;
   margin: 0 auto;
 }
 
-.etp-card .icon {
+.card-header button {
+  font-size: 0.95rem;
+  background-color: var(--blue-60);
+  border-radius: 2px;
+  border: none;
+  color: #FFFFFF;
+  cursor: pointer;
+  margin-inline-end: 15px;
+  margin-inline-start: 15px;
+  padding: 10px;
+  align-self: center;
+}
+
+.card-header button:hover {
+  background-color: var(--blue-70);
+}
+
+.card-header button:active {
+  background-color: var(--blue-80);
+}
+
+.report-card.lockwise-card .card-header {
+  grid-template-columns: 2fr 6fr 7fr;
+}
+
+.icon {
   width: 60px;
   height: 60px;
-  background: url("chrome://browser/skin/controlcenter/tracking-protection.svg") no-repeat center/cover;
   grid-column: 1;
   margin: 0 auto;
 }
 
+.etp-card .icon {
+  background: url("chrome://browser/skin/controlcenter/tracking-protection.svg") no-repeat center/cover;
+}
+
+.lockwise-card .icon {
+  background: url("chrome://browser/content/logos/lockwise.svg") no-repeat center/cover;
+}
+
 .report-card {
   display: grid;
   grid-template-columns: 100%;
   grid-template-rows: 20% auto;
   border-radius: 3px;
   background-color: var(--card-background);
   box-shadow: var(--card-box-shadow);
+  margin-bottom: 25px;
 }
 
 .report-card .card-header,
 .report-card .card-body {
   display: grid;
   grid-template-columns: 1fr 7fr;
   padding: var(--card-padding);
   grid-gap: var(--card-padding);
@@ -184,29 +233,29 @@ body[focuseddatatype=cryptominer] {
   background-color: var(--tracker-color);
 }
 
 .hover-tracker .tracker-bar {
   background-color: var(--tracker-color-darker);
 }
 
 .fingerprinter-bar {
-  background-color: var(--fingerprinter-color);
+  background-color: var(--orange);
 }
 
 .hover-fingerprinter .fingerprinter-bar {
-  background-color: var(--fingerprinter-color-darker);
+  background-color: var(--dark-orange);
 }
 
 .cryptominer-bar {
-  background-color: var(--cryptominer-color);
+  background-color: var(--grey);
 }
 
 .hover-cryptominer .cryptominer-bar {
-  background-color: var(--cryptominer-color-darker);
+  background-color: var(--dark-grey);
 }
 
 .column-label {
   justify-self: center;
   margin-top: 5px;
   font-size: 0.9rem;
 }
 
@@ -241,24 +290,24 @@ label[data-type="crossSite"] {
 label[data-type="tracker"] {
   background-image: url(chrome://browser/skin/controlcenter/trackers.svg);
   fill: var(--tracker-color);
   color: var(--tracker-color);
 }
 
 label[data-type="fingerprinter"] {
   background-image: url(chrome://browser/skin/controlcenter/fingerprinters.svg);
-  fill: var(--fingerprinter-color);
-  color: var(--fingerprinter-color);
+  fill: var(--orange);
+  color: var(--orange);
 }
 
 label[data-type="cryptominer"] {
   background-image: url(chrome://browser/skin/controlcenter/cryptominers.svg);
-  fill: var(--cryptominer-color);
-  color: var(--cryptominer-color);
+  fill: var(--grey);
+  color: var(--grey);
 }
 
 .hover-social label[for="tab-social"],
 .hover-crossSite label[for="tab-crossSite"],
 .hover-tracker label[for="tab-tracker"],
 .hover-fingerprinter label[for="tab-fingerprinter"],
 .hover-cryptominer label[for="tab-cryptominer"],
 label:hover {
@@ -291,8 +340,67 @@ label:hover {
 
 #tab-social:checked ~ #social,
 #tab-crossSite:checked ~ #crossSite,
 #tab-tracker:checked ~ #tracker,
 #tab-fingerprinter:checked ~ #fingerprinter,
 #tab-cryptominer:checked ~ #cryptominer {
   display: block;
 }
+
+/* Lockwise Card */
+
+#lockwise-body-content > .no-logins,
+#lockwise-body-content > .has-logins {
+  display: grid;
+  grid: 1fr 1fr / minmax(70px, auto) 1fr;
+  font-size: 0.875rem;
+  grid-gap: 10px;
+  align-items: center;
+}
+
+a.hidden,
+#lockwise-body-content .has-logins.hidden,
+#lockwise-body-content .no-logins.hidden {
+  display: none;
+}
+
+.number-of-logins {
+  background-color: var(--dark-grey);
+}
+
+.number-of-synced-devices {
+  background-color: var(--orange);
+}
+
+.lockwise-text-icon {
+  background-size: 16px 16px;
+  background-repeat: no-repeat;
+  background-position-x: 3px;
+  background-position-y: 5px;
+  padding: 4px 4px 4px 24px;
+  display: inline;
+}
+
+.passwords-stored-text {
+  background-image: url("chrome://browser/skin/login.svg");
+}
+
+.synced-devices-text {
+  background-image: url("chrome://browser/skin/sync.svg");
+}
+
+.non-logged-in-user-content {
+  grid-column: 2;
+}
+
+.block {
+  border-radius: 4px;
+  text-align: center;
+  font-size: 1.125rem;
+  color: var(--card-background);
+  padding: 7px;
+  line-height: 18px;
+}
+
+.has-logins a {
+  margin-inline-start: 10px;
+}
--- a/browser/components/protections/content/protections.html
+++ b/browser/components/protections/content/protections.html
@@ -5,17 +5,17 @@
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src chrome: blob:">
     <link rel="stylesheet" type="text/css"
           href="chrome://browser/content/protections.css">
     <link rel="icon" href="chrome://global/skin/icons/warning.svg">
-    <script type="text/javascript" src="chrome://browser/content/protections.js"></script>
+    <script type="module" src="chrome://browser/content/protections.js"></script>
     <title>Protection Report</title>
   </head>
 
   <body>
     <div id="report-content">
       <h2 id="report-title">Privacy Protections</h2>
       <div class="report-card etp-card">
         <div class="card-header">
@@ -78,11 +78,55 @@
               </div>
             </div>
             <p class="graph-total-summary">
               125,324 trackers blocked since August, 2018
             </p>
           </div>
         </div>
       </div>
+      <!-- Markup for Lockwise card. -->
+      <section class="report-card lockwise-card">
+        <div class="card-header">
+          <div class="icon"></div>
+          <div class="wrapper">
+            <h3 id="lockwise-title" class="card-title">
+              <!-- Insert Lockwise card title here. -->
+            </h3>
+            <p id="lockwise-header-content" class="content">
+              <!-- Insert Lockwise header content here. -->
+            </p>
+          </div>
+          <button id="open-about-logins-button">Open in Firefox</button>
+        </div>
+        <div class="card-body">
+          <div id="lockwise-body-content" class="body-wrapper">
+            <div class="no-logins hidden">
+                <span>
+                  Get the <a target="_blank" href="https://lockwise.firefox.com/">Firefox Lockwise</a>
+                  app to take your passwords everywhere.
+                </span>
+            </div>
+            <div class="has-logins hidden">
+              <span class="number-of-logins block">
+                <!-- Display number of stored logins here. -->
+              </span>
+              <span class="lockwise-text-icon passwords-stored-text">
+                Passwords stored securely.
+                <a href="">How it works</a>
+              </span>
+              <span class="number-of-synced-devices block">
+                <!-- Display number of synced devices here. -->
+              </span>
+              <span class="lockwise-text-icon synced-devices-text">
+                <span>
+                  <!-- Display message for status of synced devices here. -->
+                </span>
+                <a class="hidden" href="" title="Go to sync preferences">Turn on sync…</a>
+              </span>
+            </div>
+           </div>
+          </div>
+        </div>
+      </section>
     </div>
   </body>
 </html>
--- a/browser/components/protections/content/protections.js
+++ b/browser/components/protections/content/protections.js
@@ -1,14 +1,16 @@
 /* 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/frame-script */
 
+import LockwiseCard from "./lockwise-card.js";
+
 document.addEventListener("DOMContentLoaded", e => {
   let dataTypes = [
     "cryptominer",
     "fingerprinter",
     "tracker",
     "crossSite",
     "social",
   ];
@@ -138,11 +140,16 @@ document.addEventListener("DOMContentLoa
     // 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);
       });
     }
   };
+
   createGraph();
   addListeners();
+
+  // Create the Lockwise card.
+  const lockwiseCard = new LockwiseCard(document);
+  lockwiseCard.init();
 });
--- a/browser/components/protections/jar.mn
+++ b/browser/components/protections/jar.mn
@@ -1,8 +1,9 @@
 # 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/.
 
 browser.jar:
+    content/browser/lockwise-card.js                      (content/lockwise-card.js)
     content/browser/protections.css                       (content/protections.css)
     content/browser/protections.html                      (content/protections.html)
     content/browser/protections.js                        (content/protections.js)
--- a/browser/components/protections/moz.build
+++ b/browser/components/protections/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 JAR_MANIFESTS += ['jar.mn']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Tracking Protection')
new file mode 100644
--- /dev/null
+++ b/browser/components/protections/test/browser/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = protections
+support-files =
+  head.js
+
+[browser_protections_lockwise.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_lockwise.js
@@ -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/. */
+
+"use strict";
+
+const nsLoginInfo = new Components.Constructor(
+  "@mozilla.org/login-manager/loginInfo;1",
+  Ci.nsILoginInfo,
+  "init"
+);
+
+const TEST_LOGIN1 = new nsLoginInfo(
+  "https://example.com/",
+  "https://example.com/",
+  null,
+  "user1",
+  "pass1",
+  "username",
+  "password"
+);
+
+const TEST_LOGIN2 = new nsLoginInfo(
+  "https://2.example.com/",
+  "https://2.example.com/",
+  null,
+  "user2",
+  "pass2",
+  "username",
+  "password"
+);
+
+add_task(async function() {
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    url: "about:protections",
+    gBrowser,
+  });
+
+  info("Check that the correct content is displayed for non-logged in users.");
+  await ContentTask.spawn(tab.linkedBrowser, {}, function() {
+    const noLoginsContent = content.document.querySelector(
+      "#lockwise-body-content .no-logins"
+    );
+    const hasLoginsContent = content.document.querySelector(
+      "#lockwise-body-content .has-logins"
+    );
+
+    ok(
+      ContentTaskUtils.is_visible(noLoginsContent),
+      "Content for user with no logins is shown."
+    );
+    ok(
+      ContentTaskUtils.is_hidden(hasLoginsContent),
+      "Content for user with logins is hidden."
+    );
+  });
+
+  info("Add a login and check that content for a logged in user is displayed.");
+  Services.logins.addLogin(TEST_LOGIN1);
+  await reloadTab(tab);
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, function() {
+    const noLoginsContent = content.document.querySelector(
+      "#lockwise-body-content .no-logins"
+    );
+    const hasLoginsContent = content.document.querySelector(
+      "#lockwise-body-content .has-logins"
+    );
+    const numberOfLogins = hasLoginsContent.querySelector(
+      ".number-of-logins.block"
+    );
+
+    ok(
+      ContentTaskUtils.is_hidden(noLoginsContent),
+      "Content for user with no logins is hidden."
+    );
+    ok(
+      ContentTaskUtils.is_visible(hasLoginsContent),
+      "Content for user with logins is shown."
+    );
+    is(numberOfLogins.textContent, 1, "One stored login should be displayed");
+  });
+
+  info(
+    "Add another login and check the number of stored logins is updated after reload."
+  );
+  Services.logins.addLogin(TEST_LOGIN2);
+  await reloadTab(tab);
+
+  await ContentTask.spawn(tab.linkedBrowser, {}, function() {
+    const numberOfLogins = content.document.querySelector(
+      "#lockwise-body-content .has-logins .number-of-logins.block"
+    );
+
+    is(numberOfLogins.textContent, 2, "Two stored logins should be displayed");
+  });
+
+  // remove logins
+  Services.logins.removeLogin(TEST_LOGIN1);
+  Services.logins.removeLogin(TEST_LOGIN2);
+  await BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/protections/test/browser/head.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function reloadTab(tab) {
+  const tabReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  gBrowser.reloadTab(tab);
+  await tabReloaded;
+}