зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1627337 - Update lockwise card display and copy. r=ewright,fluent-reviewers,flod
Depends on D72693 Differential Revision: https://phabricator.services.mozilla.com/D72694
This commit is contained in:
Родитель
9ed228ef9c
Коммит
a4ed2b48f2
|
@ -139,20 +139,17 @@ class AboutProtectionsParent extends JSWindowActorParent {
|
|||
/**
|
||||
* Retrieves login data for the user.
|
||||
*
|
||||
* @return {{ hasFxa: Boolean,
|
||||
* @return {{
|
||||
* numLogins: Number,
|
||||
* mobileDeviceConnected: Boolean }}
|
||||
* The login data.
|
||||
*/
|
||||
async getLoginData() {
|
||||
if (gTestOverride && "getLoginData" in gTestOverride) {
|
||||
return gTestOverride.getLoginData();
|
||||
}
|
||||
|
||||
let hasFxa = false;
|
||||
|
||||
try {
|
||||
if ((hasFxa = !!(await fxAccounts.getSignedInUser()))) {
|
||||
if (await fxAccounts.getSignedInUser()) {
|
||||
await fxAccounts.device.refreshDeviceList();
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -170,7 +167,6 @@ class AboutProtectionsParent extends JSWindowActorParent {
|
|||
).length;
|
||||
|
||||
return {
|
||||
hasFxa,
|
||||
numLogins: userFacingLogins,
|
||||
mobileDeviceConnected,
|
||||
};
|
||||
|
@ -360,7 +356,9 @@ class AboutProtectionsParent extends JSWindowActorParent {
|
|||
return this.getMonitorData();
|
||||
|
||||
case "FetchUserLoginsData":
|
||||
return this.getLoginData();
|
||||
let { potentiallyBreachedLogins } = await this.getMonitorData();
|
||||
let loginsData = await this.getLoginData();
|
||||
return { ...loginsData, potentiallyBreachedLogins };
|
||||
|
||||
case "ClearMonitorCache":
|
||||
this.monitorResponse = null;
|
||||
|
|
|
@ -23,15 +23,22 @@ export default class LockwiseCard {
|
|||
* Initializes message listeners/senders.
|
||||
*/
|
||||
init() {
|
||||
const openAboutLoginsButton = this.doc.getElementById(
|
||||
"open-about-logins-button"
|
||||
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)
|
||||
);
|
||||
openAboutLoginsButton.addEventListener("click", () => {
|
||||
this.doc.sendTelemetryEvent("click", "lw_open_button");
|
||||
RPMSendAsyncMessage("OpenAboutLogins");
|
||||
});
|
||||
|
||||
// Attach link to Firefox Lockwise ios mobile app.
|
||||
const androidLockwiseAppLink = this.doc.getElementById(
|
||||
"lockwise-android-inline-link"
|
||||
);
|
||||
|
@ -56,15 +63,40 @@ export default class LockwiseCard {
|
|||
});
|
||||
}
|
||||
|
||||
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 { hasFxa, numLogins } = data;
|
||||
const isLoggedIn = numLogins > 0 || hasFxa;
|
||||
const { numLogins, potentiallyBreachedLogins } = data;
|
||||
const hasLogins = numLogins > 0;
|
||||
const title = this.doc.getElementById("lockwise-title");
|
||||
const headerContent = this.doc.getElementById("lockwise-header-content");
|
||||
const headerContent = this.doc.querySelector(
|
||||
"#lockwise-header-content span"
|
||||
);
|
||||
const lockwiseBodyContent = this.doc.getElementById(
|
||||
"lockwise-body-content"
|
||||
);
|
||||
const cardBody = this.doc.querySelector(".lockwise-card .card-body");
|
||||
const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
|
||||
|
||||
const exitIcon = lockwiseBodyContent.querySelector(".exit-icon");
|
||||
// User has closed the lockwise promotion, hide it and don't show again.
|
||||
|
@ -74,16 +106,18 @@ export default class LockwiseCard {
|
|||
cardBody.classList.add("hidden");
|
||||
});
|
||||
|
||||
if (isLoggedIn) {
|
||||
let container = lockwiseBodyContent.querySelector(".has-logins");
|
||||
container.classList.remove("hidden");
|
||||
title.setAttribute("data-l10n-id", "lockwise-title-logged-in");
|
||||
if (hasLogins) {
|
||||
lockwiseCard.classList.remove("no-logins");
|
||||
lockwiseCard.classList.add("has-logins");
|
||||
title.setAttribute("data-l10n-id", "lockwise-title-logged-in2");
|
||||
headerContent.setAttribute(
|
||||
"data-l10n-id",
|
||||
"lockwise-header-content-logged-in"
|
||||
);
|
||||
this.renderContentForLoggedInUser(container, numLogins);
|
||||
this.renderContentForLoggedInUser(numLogins, potentiallyBreachedLogins);
|
||||
} else {
|
||||
lockwiseCard.classList.remove("has-logins");
|
||||
lockwiseCard.classList.add("no-logins");
|
||||
if (
|
||||
!RPMGetBoolPref(
|
||||
"browser.contentblocking.report.hide_lockwise_app",
|
||||
|
@ -104,36 +138,49 @@ export default class LockwiseCard {
|
|||
}
|
||||
|
||||
/**
|
||||
* Displays the number of stored logins for a user.
|
||||
* Displays strings indicating stored logins for a user.
|
||||
*
|
||||
* @param {Element} container
|
||||
* The containing element for the content.
|
||||
* @param {Number} storedLogins
|
||||
* The number of browser-stored logins.
|
||||
* @param {Number} potentiallyBreachedLogins
|
||||
* The number of potentially breached logins.
|
||||
*/
|
||||
renderContentForLoggedInUser(container, storedLogins) {
|
||||
const lockwiseCardBody = this.doc.querySelector(
|
||||
".card.lockwise-card .card-body"
|
||||
renderContentForLoggedInUser(storedLogins, potentiallyBreachedLogins) {
|
||||
const lockwiseScannedText = this.doc.getElementById(
|
||||
"lockwise-scanned-text"
|
||||
);
|
||||
lockwiseCardBody.classList.remove("hidden");
|
||||
const lockwiseScannedIcon = this.doc.getElementById(
|
||||
"lockwise-scanned-icon"
|
||||
);
|
||||
const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
|
||||
|
||||
// Set the text for number of stored logins.
|
||||
const numberOfLoginsBlock = container.querySelector(
|
||||
".number-of-logins.block"
|
||||
);
|
||||
numberOfLoginsBlock.textContent = storedLogins;
|
||||
|
||||
const lockwisePasswordsStored = this.doc.getElementById(
|
||||
"lockwise-passwords-stored"
|
||||
);
|
||||
lockwisePasswordsStored.setAttribute(
|
||||
"data-l10n-args",
|
||||
JSON.stringify({ count: storedLogins })
|
||||
);
|
||||
lockwisePasswordsStored.setAttribute(
|
||||
"data-l10n-id",
|
||||
"lockwise-passwords-stored"
|
||||
);
|
||||
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;
|
||||
|
|
|
@ -81,8 +81,9 @@ h2 {
|
|||
|
||||
#manage-protections,
|
||||
.card-header > button,
|
||||
#save-passwords-button,
|
||||
#get-proxy-extension-link,
|
||||
#open-about-logins-button,
|
||||
#manage-passwords-button,
|
||||
#sign-up-for-monitor-link {
|
||||
grid-area: 1 / 5 / 1 / -1;
|
||||
margin: 0;
|
||||
|
@ -93,6 +94,10 @@ h2 {
|
|||
line-height: initial;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -118,6 +123,10 @@ a.hidden,
|
|||
.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,
|
||||
|
@ -605,11 +614,6 @@ label[for="tab-cryptominer"]:hover ~ #highlight-hover {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.passwords-stored-text {
|
||||
width: max-content;
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
|
||||
.block {
|
||||
background-color: var(--grey-60);
|
||||
border-radius: 4px;
|
||||
|
@ -625,6 +629,27 @@ label[for="tab-cryptominer"]:hover ~ #highlight-hover {
|
|||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
.lockwise-scanned-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 7% 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: 7px;
|
||||
}
|
||||
|
||||
#manage-passwords-button {
|
||||
grid-area: 2 / 5 / 2 / 7;
|
||||
margin-inline-end: 15px;
|
||||
}
|
||||
|
||||
/* Monitor card */
|
||||
|
||||
#monitor-body-content .monitor-breached-passwords {
|
||||
|
@ -653,6 +678,7 @@ label[for="tab-cryptominer"]:hover ~ #highlight-hover {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.lockwise-card #lockwise-header-content > a,
|
||||
.monitor-card #monitor-header-content > a {
|
||||
display: block;
|
||||
margin-block-start: 5px;
|
||||
|
|
|
@ -213,10 +213,20 @@
|
|||
<!-- Insert Lockwise card title here. -->
|
||||
</h2>
|
||||
<p id="lockwise-header-content" class="content">
|
||||
<!-- Insert Lockwise header content here. -->
|
||||
<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="open-about-logins-button" class="primary" data-l10n-id="protection-report-view-logins-button"></button>
|
||||
<button id="save-passwords-button" class="primary" data-l10n-id="protection-report-save-passwords-button"></button>
|
||||
<div class="lockwise-scanned-wrapper">
|
||||
<img class="icon-small" 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-manage-passwords-button"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body hidden">
|
||||
|
@ -235,18 +245,6 @@
|
|||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="has-logins hidden">
|
||||
<span class="number-of-logins block">
|
||||
<!-- Display number of stored logins here. -->
|
||||
</span>
|
||||
<span class="passwords-stored-text">
|
||||
<img class="icon-small" src= "chrome://browser/skin/login.svg"/>
|
||||
<span id="lockwise-passwords-stored">
|
||||
<!-- Display message for stored logins here. -->
|
||||
<a target="_blank" id="lockwise-how-it-works" data-l10n-name="lockwise-how-it-works" href=""></a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -4,114 +4,277 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const nsLoginInfo = new Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1",
|
||||
Ci.nsILoginInfo,
|
||||
"init"
|
||||
const { AboutProtectionsParent } = ChromeUtils.import(
|
||||
"resource:///actors/AboutProtectionsParent.jsm"
|
||||
);
|
||||
const ABOUT_LOGINS_URL = "about:logins";
|
||||
|
||||
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() {
|
||||
add_task(async function testNoLoginsLockwiseCardUI() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab({
|
||||
url: "about:protections",
|
||||
gBrowser,
|
||||
});
|
||||
let aboutLoginsPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
ABOUT_LOGINS_URL
|
||||
);
|
||||
|
||||
info("Check that the correct content is displayed for non-logged in users.");
|
||||
info(
|
||||
"Check that the correct lockwise card content is displayed for non-logged in users."
|
||||
);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
const noLogins = content.document.querySelector(
|
||||
"#lockwise-body-content .no-logins"
|
||||
);
|
||||
return ContentTaskUtils.is_visible(noLogins);
|
||||
}, "Lockwise card for user with no logins is shown.");
|
||||
const lockwiseCard = content.document.querySelector(".lockwise-card");
|
||||
return ContentTaskUtils.is_visible(lockwiseCard);
|
||||
}, "Lockwise card for user with no logins is visible.");
|
||||
|
||||
const noLoginsContent = content.document.querySelector(
|
||||
const lockwiseTitle = content.document.querySelector("#lockwise-title");
|
||||
is(
|
||||
lockwiseTitle.textContent,
|
||||
"Never forget a password again",
|
||||
"Correct lockwise title is shown"
|
||||
);
|
||||
|
||||
const lockwiseHowItWorks = content.document.querySelector(
|
||||
"#lockwise-how-it-works"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_hidden(lockwiseHowItWorks),
|
||||
"How it works link is hidden"
|
||||
);
|
||||
|
||||
const lockwiseHeaderString = content.document.querySelector(
|
||||
"#lockwise-header-content span"
|
||||
).textContent;
|
||||
ok(
|
||||
lockwiseHeaderString.includes(
|
||||
"Firefox Lockwise securely stores your passwords in your browser"
|
||||
),
|
||||
"Correct lockwise header string is shown"
|
||||
);
|
||||
|
||||
const lockwiseScannedWrapper = content.document.querySelector(
|
||||
".lockwise-scanned-wrapper"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_hidden(lockwiseScannedWrapper),
|
||||
"Lockwise scanned wrapper is hidden"
|
||||
);
|
||||
|
||||
const lockwiseBodyContent = content.document.querySelector(
|
||||
"#lockwise-body-content .no-logins"
|
||||
);
|
||||
const hasLoginsContent = content.document.querySelector(
|
||||
"#lockwise-body-content .has-logins"
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(lockwiseBodyContent),
|
||||
"Lockwise app content is visible"
|
||||
);
|
||||
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(noLoginsContent),
|
||||
"Content for user with no logins is shown."
|
||||
const managePasswordsButton = content.document.querySelector(
|
||||
"#manage-passwords-button"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_hidden(hasLoginsContent),
|
||||
"Content for user with logins is hidden."
|
||||
ContentTaskUtils.is_hidden(managePasswordsButton),
|
||||
"Manage passwords button is hidden"
|
||||
);
|
||||
|
||||
const savePasswordsButton = content.document.querySelector(
|
||||
"#save-passwords-button"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(savePasswordsButton),
|
||||
"Save passwords button is visible in the header"
|
||||
);
|
||||
info(
|
||||
"Click on the save passwords button and check that it opens about:logins in a new tab"
|
||||
);
|
||||
savePasswordsButton.click();
|
||||
});
|
||||
let loginsTab = await aboutLoginsPromise;
|
||||
info("about:logins was successfully opened in a new tab");
|
||||
gBrowser.removeTab(loginsTab);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
info("Add a login and check that content for a logged in user is displayed.");
|
||||
add_task(async function testLockwiseCardUIWithLogins() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab({
|
||||
url: "about:protections",
|
||||
gBrowser,
|
||||
});
|
||||
let aboutLoginsPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
ABOUT_LOGINS_URL
|
||||
);
|
||||
|
||||
info(
|
||||
"Add a login and check that lockwise card content for a logged in user is displayed correctly"
|
||||
);
|
||||
Services.logins.addLogin(TEST_LOGIN1);
|
||||
await reloadTab(tab);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
const hasLogins = content.document.querySelector(
|
||||
"#lockwise-body-content .has-logins"
|
||||
);
|
||||
const hasLogins = content.document.querySelector(".lockwise-card");
|
||||
return ContentTaskUtils.is_visible(hasLogins);
|
||||
}, "Lockwise card for user with logins is shown.");
|
||||
}, "Lockwise card for user with logins is visible");
|
||||
|
||||
const noLoginsContent = content.document.querySelector(
|
||||
const lockwiseTitle = content.document.querySelector("#lockwise-title");
|
||||
is(
|
||||
lockwiseTitle.textContent,
|
||||
"Password Management",
|
||||
"Correct lockwise title is shown"
|
||||
);
|
||||
|
||||
const lockwiseHowItWorks = content.document.querySelector(
|
||||
"#lockwise-how-it-works"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(lockwiseHowItWorks),
|
||||
"How it works link is visible"
|
||||
);
|
||||
|
||||
const lockwiseHeaderString = content.document.querySelector(
|
||||
"#lockwise-header-content span"
|
||||
).textContent;
|
||||
ok(
|
||||
lockwiseHeaderString.includes(
|
||||
"Securely store and sync your passwords to all your devices"
|
||||
),
|
||||
"Correct lockwise header string is shown"
|
||||
);
|
||||
|
||||
const lockwiseScannedWrapper = content.document.querySelector(
|
||||
".lockwise-scanned-wrapper"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(lockwiseScannedWrapper),
|
||||
"Lockwise scanned wrapper is visible"
|
||||
);
|
||||
|
||||
const lockwiseScannedText = content.document.querySelector(
|
||||
"#lockwise-scanned-text"
|
||||
).textContent;
|
||||
is(
|
||||
lockwiseScannedText,
|
||||
"1 password stored securely.",
|
||||
"Correct lockwise scanned text is shown"
|
||||
);
|
||||
|
||||
const lockwiseBodyContent = 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(lockwiseBodyContent),
|
||||
"Lockwise app content is hidden"
|
||||
);
|
||||
|
||||
ok(
|
||||
ContentTaskUtils.is_hidden(noLoginsContent),
|
||||
"Content for user with no logins is hidden."
|
||||
const savePasswordsButton = content.document.querySelector(
|
||||
"#save-passwords-button"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(hasLoginsContent),
|
||||
"Content for user with logins is shown."
|
||||
ContentTaskUtils.is_hidden(savePasswordsButton),
|
||||
"Save passwords button is hidden"
|
||||
);
|
||||
is(numberOfLogins.textContent, 1, "One stored login should be displayed");
|
||||
|
||||
const managePasswordsButton = content.document.querySelector(
|
||||
"#manage-passwords-button"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(managePasswordsButton),
|
||||
"Manage passwords button is visible"
|
||||
);
|
||||
info(
|
||||
"Click on the manage passwords button and check that it opens about:logins in a new tab"
|
||||
);
|
||||
managePasswordsButton.click();
|
||||
});
|
||||
let loginsTab = await aboutLoginsPromise;
|
||||
info("about:logins was successfully opened in a new tab");
|
||||
gBrowser.removeTab(loginsTab);
|
||||
|
||||
info(
|
||||
"Add another login and check the number of stored logins is updated after reload."
|
||||
"Add another login and check that the scanned text about stored logins is updated after reload."
|
||||
);
|
||||
Services.logins.addLogin(TEST_LOGIN2);
|
||||
await reloadTab(tab);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
const hasLogins = content.document.querySelector(".has-logins");
|
||||
return ContentTaskUtils.is_visible(hasLogins);
|
||||
}, "Lockwise card for user with logins is shown.");
|
||||
|
||||
const numberOfLogins = content.document.querySelector(
|
||||
"#lockwise-body-content .has-logins .number-of-logins.block"
|
||||
const lockwiseScannedText = content.document.querySelector(
|
||||
"#lockwise-scanned-text"
|
||||
).textContent;
|
||||
ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
lockwiseScannedText.textContent ==
|
||||
"Your passwords are being stored securely.",
|
||||
"Correct lockwise scanned text is shown"
|
||||
);
|
||||
});
|
||||
|
||||
is(numberOfLogins.textContent, 2, "Two stored logins should be displayed");
|
||||
Services.logins.removeLogin(TEST_LOGIN1);
|
||||
Services.logins.removeLogin(TEST_LOGIN2);
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testLockwiseCardUIWithBreachedLogins() {
|
||||
info(
|
||||
"Add a breached login and test that the lockwise scanned text is displayed correctly"
|
||||
);
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab({
|
||||
url: "about:protections",
|
||||
gBrowser,
|
||||
});
|
||||
Services.logins.addLogin(TEST_LOGIN1);
|
||||
|
||||
info("Mock monitor data with a breached login to test the Lockwise UI");
|
||||
AboutProtectionsParent.setTestOverride(mockGetMonitorDataForLockwiseCard(1));
|
||||
await reloadTab(tab);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const lockwiseScannedText = content.document.querySelector(
|
||||
"#lockwise-scanned-text"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(lockwiseScannedText),
|
||||
"Lockwise scanned text is visible"
|
||||
);
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
lockwiseScannedText.textContent ==
|
||||
"1 password may have been exposed in a data breach."
|
||||
);
|
||||
info("Correct lockwise scanned text is shown");
|
||||
});
|
||||
|
||||
info(
|
||||
"Mock monitor data with more than one breached logins to test the Lockwise UI"
|
||||
);
|
||||
AboutProtectionsParent.setTestOverride(mockGetMonitorDataForLockwiseCard(2));
|
||||
await reloadTab(tab);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const lockwiseScannedText = content.document.querySelector(
|
||||
"#lockwise-scanned-text"
|
||||
);
|
||||
ok(
|
||||
ContentTaskUtils.is_visible(lockwiseScannedText),
|
||||
"Lockwise scanned text is visible"
|
||||
);
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
lockwiseScannedText.textContent ==
|
||||
"2 passwords may have been exposed in a data breach."
|
||||
);
|
||||
info("Correct lockwise scanned text is shown");
|
||||
});
|
||||
|
||||
AboutProtectionsParent.setTestOverride(null);
|
||||
Services.logins.removeLogin(TEST_LOGIN1);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testLockwiseCardPref() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab({
|
||||
url: "about:protections",
|
||||
gBrowser,
|
||||
});
|
||||
|
||||
info("Disable showing the Lockwise card.");
|
||||
|
@ -121,24 +284,18 @@ add_task(async function() {
|
|||
);
|
||||
await reloadTab(tab);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const lockwiseCard = content.document.querySelector(".lockwise-card");
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
const lockwiseCard = content.document.querySelector(".lockwise-card");
|
||||
return !lockwiseCard["data-enabled"];
|
||||
}, "Lockwise card is not enabled.");
|
||||
|
||||
const lockwiseCard = content.document.querySelector(".lockwise-card");
|
||||
ok(ContentTaskUtils.is_hidden(lockwiseCard), "Lockwise card is hidden.");
|
||||
});
|
||||
|
||||
// set the pref back to displaying the card.
|
||||
// Set the pref back to displaying the card.
|
||||
Services.prefs.setBoolPref(
|
||||
"browser.contentblocking.report.lockwise.enabled",
|
||||
true
|
||||
);
|
||||
|
||||
// remove logins
|
||||
Services.logins.removeLogin(TEST_LOGIN1);
|
||||
Services.logins.removeLogin(TEST_LOGIN2);
|
||||
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -7,21 +7,6 @@
|
|||
const { AboutProtectionsParent } = ChromeUtils.import(
|
||||
"resource:///actors/AboutProtectionsParent.jsm"
|
||||
);
|
||||
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"
|
||||
);
|
||||
|
||||
let fakeDataWithNoError = {
|
||||
monitoredEmails: 1,
|
||||
|
@ -55,7 +40,6 @@ const mockGetMonitorAndLoginData = data => {
|
|||
getMonitorData: async () => data,
|
||||
getLoginData: () => {
|
||||
return {
|
||||
hasFxa: true,
|
||||
numLogins: Services.logins.countLogins("", "", ""),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -10,6 +10,10 @@ XPCOMUtils.defineLazyServiceGetter(
|
|||
"nsITrackingDBService"
|
||||
);
|
||||
|
||||
const { AboutProtectionsParent } = ChromeUtils.import(
|
||||
"resource:///actors/AboutProtectionsParent.jsm"
|
||||
);
|
||||
|
||||
const LOG = {
|
||||
"https://1.example.com": [
|
||||
[Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
|
||||
|
@ -177,6 +181,73 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
gBrowser,
|
||||
});
|
||||
|
||||
// Add user logins.
|
||||
Services.logins.addLogin(TEST_LOGIN1);
|
||||
await reloadTab(tab);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const managePasswordsButton = await ContentTaskUtils.waitForCondition(
|
||||
() => {
|
||||
return content.document.getElementById("manage-passwords-button");
|
||||
},
|
||||
"Manage passwords button exists"
|
||||
);
|
||||
ContentTaskUtils.waitForCondition(
|
||||
ContentTaskUtils.is_visible(managePasswordsButton),
|
||||
"manage passwords button is visible"
|
||||
);
|
||||
managePasswordsButton.click();
|
||||
});
|
||||
|
||||
let events = await waitForTelemetryEventCount(4);
|
||||
events = events.filter(
|
||||
e =>
|
||||
e[1] == "security.ui.protections" &&
|
||||
e[2] == "click" &&
|
||||
e[3] == "lw_open_button" &&
|
||||
e[4] == "manage_passwords"
|
||||
);
|
||||
is(
|
||||
events.length,
|
||||
1,
|
||||
`recorded telemetry for lw_open_button when there are no breached passwords`
|
||||
);
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
|
||||
// Add breached logins.
|
||||
AboutProtectionsParent.setTestOverride(mockGetMonitorDataForLockwiseCard(4));
|
||||
await reloadTab(tab);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const managePasswordsButton = await ContentTaskUtils.waitForCondition(
|
||||
() => {
|
||||
return content.document.getElementById("manage-passwords-button");
|
||||
},
|
||||
"Manage passwords button exists"
|
||||
);
|
||||
ContentTaskUtils.waitForCondition(
|
||||
ContentTaskUtils.is_visible(managePasswordsButton),
|
||||
"manage passwords button is visible"
|
||||
);
|
||||
managePasswordsButton.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(7);
|
||||
events = events.filter(
|
||||
e =>
|
||||
e[1] == "security.ui.protections" &&
|
||||
e[2] == "click" &&
|
||||
e[3] == "lw_open_button" &&
|
||||
e[4] == "manage_breached_passwords"
|
||||
);
|
||||
is(
|
||||
events.length,
|
||||
1,
|
||||
`recorded telemetry for lw_open_button when there are breached passwords`
|
||||
);
|
||||
AboutProtectionsParent.setTestOverride(null);
|
||||
Services.logins.removeLogin(TEST_LOGIN1);
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
await reloadTab(tab);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
// Show all elements, so we can click on them, even though our user is not logged in.
|
||||
let hidden_elements = content.document.querySelectorAll(".hidden");
|
||||
|
@ -184,23 +255,28 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
el.style.display = "block ";
|
||||
}
|
||||
|
||||
const openAboutLogins = await ContentTaskUtils.waitForCondition(() => {
|
||||
const savePasswordsButton = await ContentTaskUtils.waitForCondition(() => {
|
||||
// Opens an extra tab
|
||||
return content.document.getElementById("open-about-logins-button");
|
||||
}, "openAboutLogins exists");
|
||||
return content.document.getElementById("save-passwords-button");
|
||||
}, "Save Passwords button exists");
|
||||
|
||||
openAboutLogins.click();
|
||||
savePasswordsButton.click();
|
||||
});
|
||||
|
||||
let events = await waitForTelemetryEventCount(2);
|
||||
|
||||
events = await waitForTelemetryEventCount(10);
|
||||
events = events.filter(
|
||||
e =>
|
||||
e[1] == "security.ui.protections" &&
|
||||
e[2] == "click" &&
|
||||
e[3] == "lw_open_button"
|
||||
e[3] == "lw_open_button" &&
|
||||
e[4] == "save_passwords"
|
||||
);
|
||||
is(events.length, 1, `recorded telemetry for lw_open_button`);
|
||||
is(
|
||||
events.length,
|
||||
1,
|
||||
`recorded telemetry for lw_open_button when there are no stored passwords`
|
||||
);
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
|
||||
const lockwiseAndroidAppLink = await ContentTaskUtils.waitForCondition(
|
||||
|
@ -213,7 +289,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
lockwiseAndroidAppLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(3);
|
||||
events = await waitForTelemetryEventCount(11);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -232,7 +308,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
lockwiseReportLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(4);
|
||||
events = await waitForTelemetryEventCount(12);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -251,7 +327,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
openLockwise.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(5);
|
||||
events = await waitForTelemetryEventCount(13);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -269,7 +345,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
monitorReportLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(6);
|
||||
events = await waitForTelemetryEventCount(14);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -287,7 +363,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
monitorAboutLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(7);
|
||||
events = await waitForTelemetryEventCount(15);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -305,7 +381,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
signUpForMonitorLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(8);
|
||||
events = await waitForTelemetryEventCount(16);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -323,7 +399,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
socialLearnMoreLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(9);
|
||||
events = await waitForTelemetryEventCount(17);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -342,7 +418,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
cookieLearnMoreLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(10);
|
||||
events = await waitForTelemetryEventCount(18);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -361,7 +437,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
trackerLearnMoreLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(11);
|
||||
events = await waitForTelemetryEventCount(19);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -387,7 +463,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
fingerprinterLearnMoreLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(12);
|
||||
events = await waitForTelemetryEventCount(20);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -413,7 +489,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
cryptominerLearnMoreLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(13);
|
||||
events = await waitForTelemetryEventCount(21);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -436,7 +512,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
lockwiseIOSAppLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(14);
|
||||
events = await waitForTelemetryEventCount(22);
|
||||
|
||||
events = events.filter(
|
||||
e =>
|
||||
|
@ -461,7 +537,7 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
mobileAppLink.click();
|
||||
});
|
||||
|
||||
events = await waitForTelemetryEventCount(15);
|
||||
events = await waitForTelemetryEventCount(23);
|
||||
events = events.filter(
|
||||
e =>
|
||||
e[1] == "security.ui.protections" &&
|
||||
|
@ -471,10 +547,8 @@ add_task(async function checkTelemetryClickEvents() {
|
|||
is(events.length, 1, `recorded telemetry for mobile_app_link`);
|
||||
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
// We open two extra tabs with the click events.
|
||||
// We open one extra tabs with the click event.
|
||||
// 1. Monitor's "Viewed Saved Logins" link (goes to about:logins)
|
||||
// 2. Lockwise's "Open Nightly" button (goes to about:logins)
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,6 +6,32 @@
|
|||
|
||||
"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"
|
||||
);
|
||||
|
||||
async function reloadTab(tab) {
|
||||
const tabReloaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
gBrowser.reloadTab(tab);
|
||||
|
@ -17,10 +43,31 @@ const mockGetLoginDataWithSyncedDevices = (mobileDeviceConnected = false) => {
|
|||
return {
|
||||
getLoginData: () => {
|
||||
return {
|
||||
hasFxa: true,
|
||||
numLogins: Services.logins.countLogins("", "", ""),
|
||||
mobileDeviceConnected,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Used to replace AboutProtectionsHandler.getMonitorData in front-end tests.
|
||||
const mockGetMonitorDataForLockwiseCard = (
|
||||
potentiallyBreachedLogins = 0,
|
||||
error = false
|
||||
) => {
|
||||
return {
|
||||
getMonitorData: () => {
|
||||
if (error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return {
|
||||
monitoredEmails: 1,
|
||||
numBreaches: 3,
|
||||
passwords: 8,
|
||||
potentiallyBreachedLogins,
|
||||
error,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -73,24 +73,36 @@ mobile-app-card-content = Use the mobile browser with built-in protection agains
|
|||
mobile-app-links = { -brand-product-name } Browser for <a data-l10n-name="android-mobile-inline-link">Android</a> and <a data-l10n-name="ios-mobile-inline-link">iOS</a>
|
||||
|
||||
lockwise-title = Never forget a password again
|
||||
lockwise-title-logged-in = { -lockwise-brand-name }
|
||||
lockwise-title-logged-in2 = Password Management
|
||||
lockwise-header-content = { -lockwise-brand-name } securely stores your passwords in your browser.
|
||||
lockwise-header-content-logged-in = Securely store and sync your passwords to all your devices.
|
||||
protection-report-view-logins-button = View Logins
|
||||
.title = Go to Saved Logins
|
||||
protection-report-save-passwords-button = Save Passwords
|
||||
.title = Save Passwords on { -lockwise-brand-short-name }
|
||||
protection-report-manage-passwords-button = Manage Passwords
|
||||
.title = Manage Passwords on { -lockwise-brand-short-name }
|
||||
lockwise-mobile-app-title = Take your passwords everywhere
|
||||
lockwise-no-logins-card-content = Use passwords saved in { -brand-short-name } on any device.
|
||||
lockwise-app-links = { -lockwise-brand-name } for <a data-l10n-name="lockwise-android-inline-link">Android</a> and <a data-l10n-name="lockwise-ios-inline-link">iOS</a>
|
||||
|
||||
# This string is displayed after a large numeral that indicates the total number
|
||||
# of email addresses being monitored. Don’t add $count to
|
||||
# your localization, because it would result in the number showing twice.
|
||||
lockwise-passwords-stored =
|
||||
# Variables:
|
||||
# $count (Number) - Number of passwords exposed in data breaches.
|
||||
lockwise-scanned-text-breached-logins =
|
||||
{ $count ->
|
||||
[one] Password stored securely <a data-l10n-name="lockwise-how-it-works">How it works</a>
|
||||
*[other] Passwords stored securely <a data-l10n-name="lockwise-how-it-works">How it works</a>
|
||||
[one] 1 password may have been exposed in a data breach.
|
||||
*[other] { $count } passwords may have been exposed in a data breach.
|
||||
}
|
||||
|
||||
# While English doesn't use the number in the plural form, you can add $count to your language
|
||||
# if needed for grammatical reasons.
|
||||
# Variables:
|
||||
# $count (Number) - Number of passwords stored in Lockwise.
|
||||
lockwise-scanned-text-no-breached-logins =
|
||||
{ $count ->
|
||||
[one] 1 password stored securely.
|
||||
*[other] Your passwords are being stored securely.
|
||||
}
|
||||
lockwise-how-it-works-link = How it works
|
||||
|
||||
turn-on-sync = Turn on { -sync-brand-short-name }…
|
||||
.title = Go to sync preferences
|
||||
|
||||
|
|
|
@ -112,6 +112,9 @@
|
|||
skin/classic/browser/panel-icon-retry.svg (../shared/panel-icon-retry.svg)
|
||||
skin/classic/browser/toolbar-drag-indicator.svg (../shared/toolbar-drag-indicator.svg)
|
||||
|
||||
skin/classic/browser/protections/resolved-breach.svg (../shared/protections/resolved-breach.svg)
|
||||
skin/classic/browser/protections/breached-password.svg (../shared/protections/breached-password.svg)
|
||||
|
||||
skin/classic/browser/preferences/bookmark.svg (../shared/preferences/bookmark.svg)
|
||||
skin/classic/browser/preferences/critters-postcard.jpg (../shared/preferences/critters-postcard.jpg)
|
||||
skin/classic/browser/preferences/extensions.svg (../shared/preferences/extensions.svg)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<!-- 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/. -->
|
||||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 1.755A1.755 1.755 0 0112.279.081l6.55 2.046A1.67 1.67 0 0120 3.72v12.546c0 .73-.475 1.375-1.171 1.592l-6.551 2.047A1.754 1.754 0 0110 18.232zM7 2a1 1 0 010 2H2.491A.491.491 0 002 4.491V15.51c0 .271.22.491.491.491h4.51a1 1 0 010 2H2.49A2.494 2.494 0 010 15.51V4.49a2.494 2.494 0 012.491-2.49zm8 11.993c-.552 0-1 .45-1 1.004S14.448 16 15 16s1-.449 1-1.003c0-.555-.448-1.004-1-1.004zM15 4a1 1 0 00-1 1v6a1 1 0 002 0V5a1 1 0 00-1-1z" fill="#A4000F" fill-rule="evenodd"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 765 B |
|
@ -0,0 +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/. -->
|
||||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M10 20a10 10 0 1110-10c-.006 5.52-4.48 9.994-10 10z" fill="#2AC3A2"/>
|
||||
<path d="M8.263 15.296a.881.881 0 01-.613-.252l-2.623-2.622a.875.875 0 011.243-1.233l1.878 1.878 5.492-7.869a.868.868 0 011.422.997l-6.085 8.697a.86.86 0 01-.635.367l-.079.037z" fill="#FFF"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 607 B |
Загрузка…
Ссылка в новой задаче