summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/content
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js644
-rw-r--r--browser/extensions/formautofill/content/customElements.js410
-rw-r--r--browser/extensions/formautofill/content/editAddress.xhtml134
-rw-r--r--browser/extensions/formautofill/content/editCreditCard.xhtml122
-rw-r--r--browser/extensions/formautofill/content/editDialog.js239
-rw-r--r--browser/extensions/formautofill/content/formautofill.css54
-rw-r--r--browser/extensions/formautofill/content/formfill-anchor.svg8
-rw-r--r--browser/extensions/formautofill/content/icon-address-save.svg6
-rw-r--r--browser/extensions/formautofill/content/icon-address-update.svg6
-rw-r--r--browser/extensions/formautofill/content/icon-credit-card-generic.svg8
-rw-r--r--browser/extensions/formautofill/content/icon-credit-card.svg8
-rw-r--r--browser/extensions/formautofill/content/manageAddresses.xhtml54
-rw-r--r--browser/extensions/formautofill/content/manageCreditCards.xhtml55
-rw-r--r--browser/extensions/formautofill/content/manageDialog.css125
-rw-r--r--browser/extensions/formautofill/content/manageDialog.js464
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-amex.pngbin0 -> 1306 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.pngbin0 -> 2311 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.pngbin0 -> 1240 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.pngbin0 -> 3111 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-diners.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-discover.pngbin0 -> 1117 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.pngbin0 -> 2471 bytes
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-mir.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg1
-rw-r--r--browser/extensions/formautofill/content/third-party/cc-logo-visa.svg1
27 files changed, 2343 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js
new file mode 100644
index 0000000000..3ed64a098a
--- /dev/null
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -0,0 +1,644 @@
+/* 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/. */
+
+/* exported EditAddress, EditCreditCard */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+class EditAutofillForm {
+ constructor(elements) {
+ this._elements = elements;
+ }
+
+ /**
+ * Fill the form with a record object.
+ *
+ * @param {object} [record = {}]
+ */
+ loadRecord(record = {}) {
+ for (let field of this._elements.form.elements) {
+ let value = record[field.id];
+ value = typeof value == "undefined" ? "" : value;
+
+ if (record.guid) {
+ field.value = value;
+ } else if (field.localName == "select") {
+ this.setDefaultSelectedOptionByValue(field, value);
+ } else {
+ // Use .defaultValue instead of .value to avoid setting the `dirty` flag
+ // which triggers form validation UI.
+ field.defaultValue = value;
+ }
+ }
+ if (!record.guid) {
+ // Reset the dirty value flag and validity state.
+ this._elements.form.reset();
+ } else {
+ for (let field of this._elements.form.elements) {
+ this.updatePopulatedState(field);
+ this.updateCustomValidity(field);
+ }
+ }
+ }
+
+ setDefaultSelectedOptionByValue(select, value) {
+ for (let option of select.options) {
+ option.defaultSelected = option.value == value;
+ }
+ }
+
+ /**
+ * Get a record from the form suitable for a save/update in storage.
+ *
+ * @returns {object}
+ */
+ buildFormObject() {
+ let initialObject = {};
+ if (this.hasMailingAddressFields) {
+ // Start with an empty string for each mailing-address field so that any
+ // fields hidden for the current country are blanked in the return value.
+ initialObject = {
+ "street-address": "",
+ "address-level3": "",
+ "address-level2": "",
+ "address-level1": "",
+ "postal-code": "",
+ };
+ }
+
+ return Array.from(this._elements.form.elements).reduce((obj, input) => {
+ if (!input.disabled) {
+ obj[input.id] = input.value;
+ }
+ return obj;
+ }, initialObject);
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "change": {
+ this.handleChange(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle change events
+ *
+ * @param {DOMEvent} event
+ */
+ handleChange(event) {
+ this.updatePopulatedState(event.target);
+ }
+
+ /**
+ * Handle input events
+ *
+ * @param {DOMEvent} event
+ */
+ handleInput(event) {}
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ this._elements.form.addEventListener("input", this);
+ }
+
+ /**
+ * Set the field-populated attribute if the field has a value.
+ *
+ * @param {DOMElement} field The field that will be checked for a value.
+ */
+ updatePopulatedState(field) {
+ let span = field.parentNode.querySelector(".label-text");
+ if (!span) {
+ return;
+ }
+ span.toggleAttribute("field-populated", !!field.value.trim());
+ }
+
+ /**
+ * Run custom validity routines specific to the field and type of form.
+ *
+ * @param {DOMElement} field The field that will be validated.
+ */
+ updateCustomValidity(field) {}
+}
+
+class EditAddress extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record
+ * @param {object} config
+ * @param {boolean} [config.noValidate=undefined] Whether to validate the form
+ */
+ constructor(elements, record, config) {
+ super(elements);
+
+ Object.assign(this, config);
+ let { form } = this._elements;
+ Object.assign(this._elements, {
+ addressLevel3Label: form.querySelector(
+ "#address-level3-container > .label-text"
+ ),
+ addressLevel2Label: form.querySelector(
+ "#address-level2-container > .label-text"
+ ),
+ addressLevel1Label: form.querySelector(
+ "#address-level1-container > .label-text"
+ ),
+ postalCodeLabel: form.querySelector(
+ "#postal-code-container > .label-text"
+ ),
+ country: form.querySelector("#country"),
+ });
+
+ this.populateCountries();
+ // Need to populate the countries before trying to set the initial country.
+ // Also need to use this._record so it has the default country selected.
+ this.loadRecord(record);
+ this.attachEventListeners();
+
+ form.noValidate = !!config.noValidate;
+ }
+
+ loadRecord(record) {
+ this._record = record;
+ if (!record) {
+ record = {
+ country: FormAutofill.DEFAULT_REGION,
+ };
+ }
+
+ let { addressLevel1Options } = FormAutofillUtils.getFormFormat(
+ record.country
+ );
+ this.populateAddressLevel1(addressLevel1Options, record.country);
+
+ super.loadRecord(record);
+ this.loadAddressLevel1(record["address-level1"], record.country);
+ this.formatForm(record.country);
+ }
+
+ get hasMailingAddressFields() {
+ let { addressFields } = this._elements.form.dataset;
+ return (
+ !addressFields ||
+ addressFields.trim().split(/\s+/).includes("mailing-address")
+ );
+ }
+
+ /**
+ * `mailing-address` is a special attribute token to indicate mailing fields + country.
+ *
+ * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
+ * @param {string} addressFields - white-space-separated string of requested address fields to show
+ * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
+ */
+ static computeVisibleFields(mailingFieldsOrder, addressFields) {
+ if (addressFields) {
+ let requestedFieldClasses = addressFields.trim().split(/\s+/);
+ let fieldClasses = [];
+ if (requestedFieldClasses.includes("mailing-address")) {
+ fieldClasses = fieldClasses.concat(mailingFieldsOrder);
+ // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
+ requestedFieldClasses.splice(
+ requestedFieldClasses.indexOf("mailing-address"),
+ 1,
+ "country"
+ );
+ }
+
+ for (let fieldClassName of requestedFieldClasses) {
+ fieldClasses.push({
+ fieldId: fieldClassName,
+ newLine: fieldClassName == "name",
+ });
+ }
+ return fieldClasses;
+ }
+
+ // This is the default which is shown in the management interface and includes all fields.
+ return mailingFieldsOrder.concat([
+ {
+ fieldId: "country",
+ },
+ {
+ fieldId: "tel",
+ },
+ {
+ fieldId: "email",
+ newLine: true,
+ },
+ ]);
+ }
+
+ /**
+ * Format the form based on country. The address-level1 and postal-code labels
+ * should be specific to the given country.
+ *
+ * @param {string} country
+ */
+ formatForm(country) {
+ const {
+ addressLevel3L10nId,
+ addressLevel2L10nId,
+ addressLevel1L10nId,
+ addressLevel1Options,
+ postalCodeL10nId,
+ fieldsOrder: mailingFieldsOrder,
+ postalCodePattern,
+ countryRequiredFields,
+ } = FormAutofillUtils.getFormFormat(country);
+
+ document.l10n.setAttributes(
+ this._elements.addressLevel3Label,
+ addressLevel3L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.addressLevel2Label,
+ addressLevel2L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.addressLevel1Label,
+ addressLevel1L10nId
+ );
+ document.l10n.setAttributes(
+ this._elements.postalCodeLabel,
+ postalCodeL10nId
+ );
+ let addressFields = this._elements.form.dataset.addressFields;
+ let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
+ let fieldClasses = EditAddress.computeVisibleFields(
+ mailingFieldsOrder,
+ addressFields
+ );
+ let requiredFields = new Set(countryRequiredFields);
+ if (extraRequiredFields) {
+ for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
+ requiredFields.add(extraRequiredField);
+ }
+ }
+ this.arrangeFields(fieldClasses, requiredFields);
+ this.updatePostalCodeValidation(postalCodePattern);
+ this.populateAddressLevel1(addressLevel1Options, country);
+ }
+
+ /**
+ * Update address field visibility and order based on libaddressinput data.
+ *
+ * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
+ * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
+ */
+ arrangeFields(fieldsOrder, requiredFields) {
+ /**
+ * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
+ */
+ let fields = [
+ // `name` is a wrapper for the 3 name fields.
+ "name",
+ "organization",
+ "street-address",
+ "address-level3",
+ "address-level2",
+ "address-level1",
+ "postal-code",
+ "country",
+ "tel",
+ "email",
+ ];
+ let inputs = [];
+ for (let i = 0; i < fieldsOrder.length; i++) {
+ let { fieldId, newLine } = fieldsOrder[i];
+
+ let container = this._elements.form.querySelector(
+ `#${fieldId}-container`
+ );
+ let containerInputs = [
+ ...container.querySelectorAll("input, textarea, select"),
+ ];
+ containerInputs.forEach(function (input) {
+ input.disabled = false;
+ // libaddressinput doesn't list 'country' or 'name' as required.
+ // The additional-name field should never get marked as required.
+ input.required =
+ (fieldId == "country" ||
+ fieldId == "name" ||
+ requiredFields.has(fieldId)) &&
+ input.id != "additional-name";
+ });
+ inputs.push(...containerInputs);
+ container.style.display = "flex";
+ container.style.order = i;
+ container.style.pageBreakAfter = newLine ? "always" : "auto";
+ // Remove the field from the list of fields
+ fields.splice(fields.indexOf(fieldId), 1);
+ }
+ for (let i = 0; i < inputs.length; i++) {
+ // Assign tabIndex starting from 1
+ inputs[i].tabIndex = i + 1;
+ }
+ // Hide the remaining fields
+ for (let field of fields) {
+ let container = this._elements.form.querySelector(`#${field}-container`);
+ container.style.display = "none";
+ for (let input of [
+ ...container.querySelectorAll("input, textarea, select"),
+ ]) {
+ input.disabled = true;
+ }
+ }
+ }
+
+ updatePostalCodeValidation(postalCodePattern) {
+ let postalCodeInput = this._elements.form.querySelector("#postal-code");
+ if (postalCodePattern && postalCodeInput.style.display != "none") {
+ postalCodeInput.setAttribute("pattern", postalCodePattern);
+ } else {
+ postalCodeInput.removeAttribute("pattern");
+ }
+ }
+
+ /**
+ * Set the address-level1 value on the form field (input or select, whichever is present).
+ *
+ * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
+ * @param {string} country The corresponding country
+ */
+ loadAddressLevel1(addressLevel1Value, country) {
+ let field = this._elements.form.querySelector("#address-level1");
+
+ if (field.localName == "input") {
+ field.value = addressLevel1Value || "";
+ return;
+ }
+
+ let matchedSelectOption = FormAutofillUtils.findAddressSelectOption(
+ field,
+ {
+ country,
+ "address-level1": addressLevel1Value,
+ },
+ "address-level1"
+ );
+ if (matchedSelectOption && !matchedSelectOption.selected) {
+ field.value = matchedSelectOption.value;
+ field.dispatchEvent(new Event("input", { bubbles: true }));
+ field.dispatchEvent(new Event("change", { bubbles: true }));
+ } else if (addressLevel1Value) {
+ // If the option wasn't found, insert an option at the beginning of
+ // the select that matches the stored value.
+ field.insertBefore(
+ new Option(addressLevel1Value, addressLevel1Value, true, true),
+ field.firstChild
+ );
+ }
+ }
+
+ /**
+ * Replace the text input for address-level1 with a select dropdown if
+ * a fixed set of names exists. Otherwise show a text input.
+ *
+ * @param {Map?} options Map of options with regionCode -> name mappings
+ * @param {string} country The corresponding country
+ */
+ populateAddressLevel1(options, country) {
+ let field = this._elements.form.querySelector("#address-level1");
+
+ if (field.dataset.country == country) {
+ return;
+ }
+
+ if (!options) {
+ if (field.localName == "input") {
+ return;
+ }
+
+ let input = document.createElement("input");
+ input.setAttribute("type", "text");
+ input.id = "address-level1";
+ input.required = field.required;
+ input.disabled = field.disabled;
+ input.tabIndex = field.tabIndex;
+ field.replaceWith(input);
+ return;
+ }
+
+ if (field.localName == "input") {
+ let select = document.createElement("select");
+ select.id = "address-level1";
+ select.required = field.required;
+ select.disabled = field.disabled;
+ select.tabIndex = field.tabIndex;
+ field.replaceWith(select);
+ field = select;
+ }
+
+ field.textContent = "";
+ field.dataset.country = country;
+ let fragment = document.createDocumentFragment();
+ fragment.appendChild(new Option(undefined, undefined, true, true));
+ for (let [regionCode, regionName] of options) {
+ let option = new Option(regionName, regionCode);
+ fragment.appendChild(option);
+ }
+ field.appendChild(fragment);
+ }
+
+ populateCountries() {
+ let fragment = document.createDocumentFragment();
+ // Sort countries by their visible names.
+ let countries = [...FormAutofill.countries.entries()].sort((e1, e2) =>
+ e1[1].localeCompare(e2[1])
+ );
+ for (let [country] of countries) {
+ const countryName = Services.intl.getRegionDisplayNames(undefined, [
+ country.toLowerCase(),
+ ]);
+ const option = new Option(countryName, country);
+ fragment.appendChild(option);
+ }
+ this._elements.country.appendChild(fragment);
+ }
+
+ handleChange(event) {
+ if (event.target == this._elements.country) {
+ this.formatForm(event.target.value);
+ }
+ super.handleChange(event);
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+}
+
+class EditCreditCard extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record with a decrypted cc-number
+ * @param {object} addresses in an object with guid keys for the billing address picker.
+ */
+ constructor(elements, record, addresses) {
+ super(elements);
+
+ this._addresses = addresses;
+ Object.assign(this._elements, {
+ ccNumber: this._elements.form.querySelector("#cc-number"),
+ invalidCardNumberStringElement: this._elements.form.querySelector(
+ "#invalidCardNumberString"
+ ),
+ month: this._elements.form.querySelector("#cc-exp-month"),
+ year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow:
+ this._elements.form.querySelector(".billingAddressRow"),
+ });
+
+ this.attachEventListeners();
+ this.loadRecord(record, addresses);
+ }
+
+ loadRecord(record, addresses, preserveFieldValues) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
+ this._record = record;
+ this._addresses = addresses;
+ this.generateBillingAddressOptions(preserveFieldValues);
+ if (!preserveFieldValues) {
+ // Re-generating the months will reset the selected option.
+ this.generateMonths();
+ // Re-generating the years will reset the selected option.
+ this.generateYears();
+ super.loadRecord(record);
+ }
+ }
+
+ generateMonths() {
+ const count = 12;
+
+ // Clear the list
+ this._elements.month.textContent = "";
+
+ // Empty month option
+ this._elements.month.appendChild(new Option());
+
+ // Populate month list. Format: "month number - month name"
+ let dateFormat = new Intl.DateTimeFormat(navigator.language, {
+ month: "long",
+ }).format;
+ for (let i = 0; i < count; i++) {
+ let monthNumber = (i + 1).toString();
+ let monthName = dateFormat(new Date(1970, i));
+ let option = new Option();
+ option.value = monthNumber;
+ // XXX: Bug 1446164 - Localize this string.
+ option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+ this._elements.month.appendChild(option);
+ }
+ }
+
+ generateYears() {
+ const count = 11;
+ const currentYear = new Date().getFullYear();
+ const ccExpYear = this._record && this._record["cc-exp-year"];
+
+ // Clear the list
+ this._elements.year.textContent = "";
+
+ // Provide an empty year option
+ this._elements.year.appendChild(new Option());
+
+ if (ccExpYear && ccExpYear < currentYear) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+
+ for (let i = 0; i < count; i++) {
+ let year = currentYear + i;
+ let option = new Option(year);
+ this._elements.year.appendChild(option);
+ }
+
+ if (ccExpYear && ccExpYear > currentYear + count) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+ }
+
+ generateBillingAddressOptions(preserveFieldValues) {
+ let billingAddressGUID;
+ if (preserveFieldValues && this._elements.billingAddress.value) {
+ billingAddressGUID = this._elements.billingAddress.value;
+ } else if (this._record) {
+ billingAddressGUID = this._record.billingAddressGUID;
+ }
+
+ this._elements.billingAddress.textContent = "";
+
+ this._elements.billingAddress.appendChild(new Option("", ""));
+
+ let hasAddresses = false;
+ for (let [guid, address] of Object.entries(this._addresses)) {
+ hasAddresses = true;
+ let selected = guid == billingAddressGUID;
+ let option = new Option(
+ FormAutofillUtils.getAddressLabel(address),
+ guid,
+ selected,
+ selected
+ );
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+
+ handleInput(event) {
+ // Clear the error message if cc-number is valid
+ if (
+ event.target == this._elements.ccNumber &&
+ FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
+ ) {
+ this._elements.ccNumber.setCustomValidity("");
+ }
+ super.handleInput(event);
+ }
+
+ updateCustomValidity(field) {
+ super.updateCustomValidity(field);
+
+ // Mark the cc-number field as invalid if the number is empty or invalid.
+ if (
+ field == this._elements.ccNumber &&
+ !FormAutofillUtils.isCCNumber(field.value)
+ ) {
+ let invalidCardNumberString =
+ this._elements.invalidCardNumberStringElement.textContent;
+ field.setCustomValidity(invalidCardNumberString || " ");
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/customElements.js b/browser/extensions/formautofill/content/customElements.js
new file mode 100644
index 0000000000..0b3d761817
--- /dev/null
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -0,0 +1,410 @@
+/* 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 loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+(() => {
+ function sendMessageToBrowser(msgName, data) {
+ let { AutoCompleteParent } = ChromeUtils.importESModule(
+ "resource://gre/actors/AutoCompleteParent.sys.mjs"
+ );
+
+ let actor = AutoCompleteParent.getCurrentActor();
+ if (!actor) {
+ return;
+ }
+
+ actor.manager.getActor("FormAutofill").sendAsyncMessage(msgName, data);
+ }
+
+ class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
+ constructor() {
+ super();
+
+ /**
+ * For form autofill, we want to unify the selection no matter by
+ * keyboard navigation or mouseover in order not to confuse user which
+ * profile preview is being shown. This field is set to true to indicate
+ * that selectedIndex of popup should be changed while mouseover item
+ */
+ this.selectedByMouseOver = true;
+ }
+
+ get _stringBundle() {
+ if (!this.__stringBundle) {
+ this.__stringBundle = Services.strings.createBundle(
+ "chrome://formautofill/locale/formautofill.properties"
+ );
+ }
+ return this.__stringBundle;
+ }
+
+ _cleanup() {
+ this.removeAttribute("formautofillattached");
+ if (this._itemBox) {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+
+ _onOverflow() {}
+
+ _onUnderflow() {}
+
+ handleOverUnderflow() {}
+
+ _adjustAutofillItemLayout() {
+ let outerBoxRect = this.parentNode.getBoundingClientRect();
+
+ // Make item fit in popup as XUL box could not constrain
+ // item's width
+ this._itemBox.style.width = outerBoxRect.width + "px";
+ // Use two-lines layout when width is smaller than 150px or
+ // 185px if an image precedes the label.
+ let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150;
+
+ if (outerBoxRect.width <= oneLineMinRequiredWidth) {
+ this._itemBox.setAttribute("size", "small");
+ } else {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+ }
+
+ MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends (
+ MozAutocompleteProfileListitemBase
+ ) {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box">
+ <div class="profile-label-col profile-item-col">
+ <span class="profile-label-affix"></span>
+ <span class="profile-label"></span>
+ </div>
+ <div class="profile-comment-col profile-item-col">
+ <span class="profile-comment"></span>
+ </div>
+ </div>
+ `;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._labelAffix = this.querySelector(".profile-label-affix");
+ this._label = this.querySelector(".profile-label");
+ this._comment = this.querySelector(".profile-comment");
+
+ this.initializeAttributeInheritance();
+ this._adjustAcItem();
+ }
+
+ static get inheritedAttributes() {
+ return {
+ ".autofill-item-box": "ac-image",
+ };
+ }
+
+ set selected(val) {
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+
+ sendMessageToBrowser("FormAutofill:PreviewProfile");
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+ this._itemBox.style.setProperty(
+ "--primary-icon",
+ `url(${this.getAttribute("ac-image")})`
+ );
+
+ let { primaryAffix, primary, secondary, ariaLabel } = JSON.parse(
+ this.getAttribute("ac-value")
+ );
+
+ this._labelAffix.textContent = primaryAffix;
+ this._label.textContent = primary;
+ this._comment.textContent = secondary;
+ if (ariaLabel) {
+ this.setAttribute("aria-label", ariaLabel);
+ }
+ }
+ };
+
+ customElements.define(
+ "autocomplete-profile-listitem",
+ MozElements.MozAutocompleteProfileListitem,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
+ <div class="autofill-footer-row autofill-warning"></div>
+ <div class="autofill-footer-row autofill-button"></div>
+ </div>
+ `;
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("click", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ if (this._warningTextBox.contains(event.originalTarget)) {
+ return;
+ }
+
+ window.openPreferences("privacy-form-autofill");
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-footer");
+ this._optionButton = this.querySelector(".autofill-button");
+ this._warningTextBox = this.querySelector(".autofill-warning");
+
+ /**
+ * A handler for updating warning message once selectedIndex has been changed.
+ *
+ * There're three different states of warning message:
+ * 1. None of addresses were selected: We show all the categories intersection of fields in the
+ * form and fields in the results.
+ * 2. An address was selested: Show the additional categories that will also be filled.
+ * 3. An address was selected, but the focused category is the same as the only one category: Only show
+ * the exact category that we're going to fill in.
+ *
+ * @private
+ * @param {object} data
+ * Message data
+ * @param {string[]} data.categories
+ * The categories of all the fields contained in the selected address.
+ */
+ this.updateWarningNote = data => {
+ let categories =
+ data && data.categories ? data.categories : this._allFieldCategories;
+ // If the length of categories is 1, that means all the fillable fields are in the same
+ // category. We will change the way to inform user according to this flag. When the value
+ // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only.
+ let hasExtraCategories = categories.length > 1;
+ // Show the categories in certain order to conform with the spec.
+ let orderedCategoryList = [
+ { id: "address", l10nId: "category.address" },
+ { id: "name", l10nId: "category.name" },
+ { id: "organization", l10nId: "category.organization2" },
+ { id: "tel", l10nId: "category.tel" },
+ { id: "email", l10nId: "category.email" },
+ ];
+ let showCategories = hasExtraCategories
+ ? orderedCategoryList.filter(
+ category =>
+ categories.includes(category.id) &&
+ category.id != this._focusedCategory
+ )
+ : [
+ orderedCategoryList.find(
+ category => category.id == this._focusedCategory
+ ),
+ ];
+
+ let separator =
+ this._stringBundle.GetStringFromName("fieldNameSeparator");
+ let warningTextTmplKey = hasExtraCategories
+ ? "phishingWarningMessage"
+ : "phishingWarningMessage2";
+ let categoriesText = showCategories
+ .map(category =>
+ this._stringBundle.GetStringFromName(category.l10nId)
+ )
+ .join(separator);
+
+ this._warningTextBox.textContent =
+ this._stringBundle.formatStringFromName(warningTextTmplKey, [
+ categoriesText,
+ ]);
+ this.parentNode.parentNode.adjustHeight();
+ };
+
+ this._adjustAcItem();
+ }
+
+ _onCollapse() {
+ if (this.showWarningText) {
+ let { FormAutofillParent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillParent.sys.mjs"
+ );
+ FormAutofillParent.removeMessageObserver(this);
+ }
+ this._itemBox.removeAttribute("no-warning");
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let buttonTextBundleKey;
+ if (this._itemBox.getAttribute("size") == "small") {
+ buttonTextBundleKey =
+ AppConstants.platform == "macosx"
+ ? "autocompleteFooterOptionOSXShort2"
+ : "autocompleteFooterOptionShort2";
+ } else {
+ buttonTextBundleKey =
+ AppConstants.platform == "macosx"
+ ? "autocompleteFooterOptionOSX2"
+ : "autocompleteFooterOption2";
+ }
+
+ let buttonText =
+ this._stringBundle.GetStringFromName(buttonTextBundleKey);
+ this._optionButton.textContent = buttonText;
+
+ let value = JSON.parse(this.getAttribute("ac-value"));
+
+ this._allFieldCategories = value.categories;
+ this._focusedCategory = value.focusedCategory;
+ this.showWarningText = this._allFieldCategories && this._focusedCategory;
+
+ if (this.showWarningText) {
+ let { FormAutofillParent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillParent.sys.mjs"
+ );
+ FormAutofillParent.addMessageObserver(this);
+ this.updateWarningNote();
+ } else {
+ this._itemBox.setAttribute("no-warning", "true");
+ }
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-footer",
+ MozAutocompleteProfileListitemFooter,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-insecure-item"></div>
+ `;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-insecure-item");
+
+ this._adjustAcItem();
+ }
+
+ set selected(val) {
+ // This item is unselectable since we see this item as a pure message.
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let value = this.getAttribute("ac-value");
+ this._itemBox.textContent = value;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-creditcard-insecure-field",
+ MozAutocompleteCreditcardInsecureField,
+ { extends: "richlistitem" }
+ );
+
+ class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase {
+ static get markup() {
+ return `
+ <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
+ <div class="autofill-footer-row autofill-button"></div>
+ </div>
+ `;
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("click", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ sendMessageToBrowser("FormAutofill:ClearForm");
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._clearBtn = this.querySelector(".autofill-button");
+
+ this._adjustAcItem();
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let clearFormBtnLabel =
+ this._stringBundle.GetStringFromName("clearFormBtnLabel2");
+ this._clearBtn.textContent = clearFormBtnLabel;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-clear-button",
+ MozAutocompleteProfileListitemClearButton,
+ { extends: "richlistitem" }
+ );
+})();
diff --git a/browser/extensions/formautofill/content/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml
new file mode 100644
index 0000000000..8972e75c47
--- /dev/null
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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 xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title data-l10n-id="autofill-add-new-address-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog-shared.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editAddress.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog.css"
+ />
+ <script src="chrome://formautofill/content/editDialog.js"></script>
+ <script src="chrome://formautofill/content/autofillEditForms.js"></script>
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-button-group.mjs"
+ ></script>
+ </head>
+ <body>
+ <form id="form" class="editAddressForm" autocomplete="off">
+ <!--
+ The <span class="label-text" …/> needs to be after the form field in the same element in
+ order to get proper label styling with :focus and :moz-ui-invalid.
+ -->
+ <div id="name-container" class="container">
+ <label id="given-name-container">
+ <input id="given-name" type="text" required="required" />
+ <span data-l10n-id="autofill-address-given-name" class="label-text" />
+ </label>
+ <label id="additional-name-container">
+ <input id="additional-name" type="text" />
+ <span
+ data-l10n-id="autofill-address-additional-name"
+ class="label-text"
+ />
+ </label>
+ <label id="family-name-container">
+ <input id="family-name" type="text" required="required" />
+ <span
+ data-l10n-id="autofill-address-family-name"
+ class="label-text"
+ />
+ </label>
+ </div>
+ <label id="organization-container" class="container">
+ <input id="organization" type="text" />
+ <span data-l10n-id="autofill-address-organization" class="label-text" />
+ </label>
+ <label id="street-address-container" class="container">
+ <textarea id="street-address" rows="3" />
+ <span data-l10n-id="autofill-address-street" class="label-text" />
+ </label>
+ <label id="address-level3-container" class="container">
+ <input id="address-level3" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="address-level2-container" class="container">
+ <input id="address-level2" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="address-level1-container" class="container">
+ <!-- The address-level1 input will get replaced by a select dropdown
+ by autofillEditForms.js when the selected country has provided
+ specific options. -->
+ <input id="address-level1" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="postal-code-container" class="container">
+ <input id="postal-code" type="text" />
+ <span class="label-text" />
+ </label>
+ <label id="country-container" class="container">
+ <select id="country" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-address-country" class="label-text" />
+ </label>
+ <label id="tel-container" class="container">
+ <input id="tel" type="tel" dir="auto" />
+ <span data-l10n-id="autofill-address-tel" class="label-text" />
+ </label>
+ <label id="email-container" class="container">
+ <input id="email" type="email" required="required" />
+ <span data-l10n-id="autofill-address-email" class="label-text" />
+ </label>
+ </form>
+ <div id="controls-container">
+ <span
+ id="country-warning-message"
+ data-l10n-id="autofill-country-warning-message"
+ />
+ <moz-button-group>
+ <button id="cancel" data-l10n-id="autofill-cancel-button" />
+ <button id="save" class="primary" data-l10n-id="autofill-save-button" />
+ </moz-button-group>
+ </div>
+ <script>
+ <![CDATA[
+ "use strict";
+
+ const {
+ record,
+ noValidate,
+ } = window.arguments?.[0] ?? {};
+
+ /* import-globals-from autofillEditForms.js */
+ const fieldContainer = new EditAddress({
+ form: document.getElementById("form"),
+ }, record, {
+ noValidate,
+ });
+
+ /* import-globals-from editDialog.js */
+ new EditAddressDialog({
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ }, record);
+ ]]>
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/editCreditCard.xhtml b/browser/extensions/formautofill/content/editCreditCard.xhtml
new file mode 100644
index 0000000000..c8315540c6
--- /dev/null
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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 xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title data-l10n-id="autofill-add-new-card-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog-shared.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editCreditCard.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/skin/editDialog.css"
+ />
+ <script src="chrome://formautofill/content/editDialog.js"></script>
+ <script src="chrome://formautofill/content/autofillEditForms.js"></script>
+ </head>
+ <body>
+ <form id="form" class="editCreditCardForm contentPane" autocomplete="off">
+ <!--
+ The <span class="label-text" …/> needs to be after the form field in the same element in
+ order to get proper label styling with :focus and :moz-ui-invalid.
+ -->
+ <label id="cc-number-container" class="container" role="none">
+ <span
+ id="invalidCardNumberString"
+ hidden="hidden"
+ data-l10n-id="autofill-card-invalid-number"
+ ></span>
+ <!-- Because there is text both before and after the input, a11y will
+ include the value of the input in the label. Therefore, we override
+ with aria-labelledby.
+ -->
+ <input
+ id="cc-number"
+ type="text"
+ required="required"
+ minlength="14"
+ pattern="[- 0-9]+"
+ aria-labelledby="cc-number-label"
+ />
+ <span
+ id="cc-number-label"
+ data-l10n-id="autofill-card-number"
+ class="label-text"
+ />
+ </label>
+ <label id="cc-exp-month-container" class="container">
+ <select id="cc-exp-month" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-card-expires-month" class="label-text" />
+ </label>
+ <label id="cc-exp-year-container" class="container">
+ <select id="cc-exp-year" required="required">
+ <option />
+ </select>
+ <span data-l10n-id="autofill-card-expires-year" class="label-text" />
+ </label>
+ <label id="cc-name-container" class="container">
+ <input id="cc-name" type="text" required="required" />
+ <span data-l10n-id="autofill-card-name-on-card" class="label-text" />
+ </label>
+ <label id="cc-csc-container" class="container" hidden="hidden">
+ <!-- The CSC container will get filled in by forms that need a CSC (using csc-input.js) -->
+ </label>
+ <div
+ id="billingAddressGUID-container"
+ class="billingAddressRow container rich-picker"
+ >
+ <select id="billingAddressGUID" required="required"></select>
+ <label
+ for="billingAddressGUID"
+ data-l10n-id="autofill-card-billing-address"
+ class="label-text"
+ />
+ </div>
+ </form>
+ <div id="controls-container">
+ <button id="cancel" data-l10n-id="autofill-cancel-button" />
+ <button id="save" class="primary" data-l10n-id="autofill-save-button" />
+ </div>
+ <script>
+ <![CDATA[
+ "use strict";
+
+ /* import-globals-from editDialog.js */
+
+ (async () => {
+ const {
+ record,
+ } = window.arguments?.[0] ?? {};
+
+ const addresses = {};
+ for (let address of await formAutofillStorage.addresses.getAll()) {
+ addresses[address.guid] = address;
+ }
+
+ /* import-globals-from autofillEditForms.js */
+ const fieldContainer = new EditCreditCard({
+ form: document.getElementById("form"),
+ }, record, addresses);
+
+ new EditCreditCardDialog({
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ }, record);
+ })();
+ ]]>
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/editDialog.js b/browser/extensions/formautofill/content/editDialog.js
new file mode 100644
index 0000000000..77dcbb2ae0
--- /dev/null
+++ b/browser/extensions/formautofill/content/editDialog.js
@@ -0,0 +1,239 @@
+/* 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/. */
+
+/* exported EditAddressDialog, EditCreditCardDialog */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "AutofillTelemetry",
+ "resource://autofill/AutofillTelemetry.jsm"
+);
+
+class AutofillEditDialog {
+ constructor(subStorageName, elements, record) {
+ this._storageInitPromise = formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._record = record;
+ this.localizeDocument();
+ window.addEventListener("DOMContentLoaded", this, { once: true });
+ }
+
+ async init() {
+ this.updateSaveButtonState();
+ this.attachEventListeners();
+ // For testing only: signal to tests that the dialog is ready for testing.
+ // This is likely no longer needed since retrieving from storage is fully
+ // handled in manageDialog.js now.
+ window.dispatchEvent(new CustomEvent("FormReady"));
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Asks FormAutofillParent to save or update an record.
+ *
+ * @param {object} record
+ * @param {string} guid [optional]
+ */
+ async saveRecord(record, guid) {
+ let storage = await this.getStorage();
+ if (guid) {
+ await storage.update(guid, record);
+ } else {
+ await storage.add(record);
+ }
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ if (
+ !HTMLInputElement.isInstance(event.target) &&
+ !HTMLTextAreaElement.isInstance(event.target)
+ ) {
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.cancel) {
+ window.close();
+ }
+ if (event.target == this._elements.save) {
+ this.handleSubmit();
+ }
+ }
+
+ /**
+ * Handle input events
+ *
+ * @param {DOMEvent} event
+ */
+ handleInput(event) {
+ this.updateSaveButtonState();
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ }
+
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!Object.keys(this._elements.fieldContainer.buildFormObject()).length) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ document.addEventListener("input", this);
+ }
+
+ // An interface to be inherited.
+ localizeDocument() {}
+
+ recordFormSubmit() {
+ let method = this._record?.guid ? "edit" : "add";
+ AutofillTelemetry.recordManageEvent(this.telemetryType, method);
+ }
+}
+
+class EditAddressDialog extends AutofillEditDialog {
+ telemetryType = AutofillTelemetry.ADDRESS;
+
+ constructor(elements, record) {
+ super("addresses", elements, record);
+ if (record) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ }
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-address-title"
+ );
+ }
+ }
+
+ async handleSubmit() {
+ await this.saveRecord(
+ this._elements.fieldContainer.buildFormObject(),
+ this._record ? this._record.guid : null
+ );
+ this.recordFormSubmit();
+
+ window.close();
+ }
+}
+
+class EditCreditCardDialog extends AutofillEditDialog {
+ telemetryType = AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements, record) {
+ elements.fieldContainer._elements.billingAddress.disabled = true;
+ super("creditCards", elements, record);
+ elements.fieldContainer._elements.ccNumber.addEventListener(
+ "blur",
+ this._onCCNumberFieldBlur.bind(this)
+ );
+ if (record) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ }
+ }
+
+ _onCCNumberFieldBlur() {
+ let elem = this._elements.fieldContainer._elements.ccNumber;
+ this._elements.fieldContainer.updateCustomValidity(elem);
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-card-title"
+ );
+ }
+ }
+
+ async handleSubmit() {
+ let creditCard = this._elements.fieldContainer.buildFormObject();
+ if (!this._elements.fieldContainer._elements.form.reportValidity()) {
+ return;
+ }
+
+ try {
+ await this.saveRecord(
+ creditCard,
+ this._record ? this._record.guid : null
+ );
+
+ this.recordFormSubmit();
+
+ window.close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/formautofill.css b/browser/extensions/formautofill/content/formautofill.css
new file mode 100644
index 0000000000..fad9ee410a
--- /dev/null
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -0,0 +1,54 @@
+/* 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/. */
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"] {
+ display: block;
+ margin: 0;
+ padding: 0;
+ height: auto;
+ min-height: auto;
+}
+
+/* Treat @collpased="true" as display: none similar to how it is for XUL elements.
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"][collapsed="true"] {
+ display: none;
+}
+
+#PopupAutoComplete[resultstyles~="autofill-profile"] {
+ min-width: 150px !important;
+}
+
+#PopupAutoComplete[resultstyles~="autofill-insecureWarning"] {
+ min-width: 200px !important;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[disabled="true"] {
+ opacity: 0.5;
+}
+
+/* Form Autofill Doorhanger */
+#autofill-address-notification popupnotificationcontent > .desc-message-box,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box {
+ margin-block-end: 12px;
+}
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > image {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: auto;
+ height: auto;
+ list-style-image: url(chrome://formautofill/content/icon-credit-card-generic.svg);
+}
+#autofill-address-notification popupnotificationcontent > .desc-message-box > description,
+#autofill-address-notification popupnotificationcontent > .desc-message-box > additional-description,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > description {
+ font-style: italic;
+ margin-inline-start: 4px;
+}
diff --git a/browser/extensions/formautofill/content/formfill-anchor.svg b/browser/extensions/formautofill/content/formfill-anchor.svg
new file mode 100644
index 0000000000..0a9ef19add
--- /dev/null
+++ b/browser/extensions/formautofill/content/formfill-anchor.svg
@@ -0,0 +1,8 @@
+<!-- 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M7.3 6h1.5c.1 0 .2-.1.2-.3V2c0-.5-.4-1-1-1s-1 .4-1 1v3.8c0 .1.1.2.3.2z"/>
+ <path d="M13.5 3H11c-.6 0-1 .4-1 1s.4 1 1 1h2.5c.3 0 .5.2.5.5v7c0 .3-.2.5-.5.5h-11c-.3 0-.5-.3-.5-.5v-7c0-.3.2-.5.5-.5H5c.6 0 1-.4 1-1s-.4-1-1-1H2.5C1.1 3 0 4.1 0 5.5v7C0 13.8 1.1 15 2.5 15h11c1.4 0 2.5-1.1 2.5-2.5v-7C16 4.1 14.9 3 13.5 3z"/>
+ <path d="M3.6 7h2.8c.3 0 .6.2.6.5v2.8c0 .4-.3.7-.6.7H3.6c-.3 0-.6-.3-.6-.6V7.5c0-.3.3-.5.6-.5zM9.5 8h3c.3 0 .5-.3.5-.5s-.2-.5-.5-.5h-3c-.3 0-.5.2-.5.5s.2.5.5.5zM9.5 9c-.3 0-.5.2-.5.5s.2.5.5.5h2c.3 0 .5-.2.5-.5s-.2-.5-.5-.5h-2z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-address-save.svg b/browser/extensions/formautofill/content/icon-address-save.svg
new file mode 100644
index 0000000000..8fdcf1cd5f
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-address-save.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 xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm21.1-1.9h-2.5V20c0-.4-.3-.8-.8-.8h-3.1c-.4 0-.8.3-.8.8v4.6h-2.5c-.6 0-.8.4-.3.8l4.3 4.2c.2.2.5.3.8.3s.6-.1.8-.3l4.3-4.2c.6-.4.4-.7-.2-.7zm-11.3-5.6H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-address-update.svg b/browser/extensions/formautofill/content/icon-address-update.svg
new file mode 100644
index 0000000000..1455423fed
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-address-update.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 xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm9.8-7.5H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zm-5.7 16l4.4-4.3c.2-.2.5-.3.8-.3s.6.1.8.3l4.4 4.3c.5.5.3.8-.3.8h-2.6v4.7c0 .4-.4.8-.8.8h-3c-.4 0-.8-.4-.8-.8v-4.7h-2.5c-.7 0-.8-.4-.4-.8z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-credit-card-generic.svg b/browser/extensions/formautofill/content/icon-credit-card-generic.svg
new file mode 100644
index 0000000000..5d554fe7ce
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-credit-card-generic.svg
@@ -0,0 +1,8 @@
+<!-- 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 height="16" width="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill" viewBox="0 0 16 16">
+ <path d="M4.5,9.4H3.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h1.3c0.3,0,0.5-0.2,0.5-0.5S4.8,9.4,4.5,9.4z"/>
+ <path d="M9.3,9.4H6.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h3.2c0.3,0,0.5-0.2,0.5-0.5S9.6,9.4,9.3,9.4z"/>
+ <path d="M14,2H2C0.9,2,0,2.9,0,4v8c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V4C16,2.9,15.1,2,14,2z M14,12H2V7.7h12V12z M14,6H2V4h12V6z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/icon-credit-card.svg b/browser/extensions/formautofill/content/icon-credit-card.svg
new file mode 100644
index 0000000000..7ec782f880
--- /dev/null
+++ b/browser/extensions/formautofill/content/icon-credit-card.svg
@@ -0,0 +1,8 @@
+<!-- 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 xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 32 32">
+ <path d="M9 22.2H6.4c-.6 0-1 .4-1 1s.4 1 1 1H9c.6 0 1-.4 1-1s-.4-1-1-1z"/>
+ <path d="M28 7.6v8H4v-4h10v-4H4c-2.2 0-4 1.8-4 4v16c0 2.2 1.8 4 4 4h24c2.2 0 4-1.8 4-4v-16c0-2.2-1.8-4-4-4zm-24 20V19h24v8.6H4z"/>
+ <path d="M19.2 22.2h-6.3c-.6 0-1 .4-1 1s.4 1 1 1h6.3c.6 0 1-.4 1-1s-.5-1-1-1zM16.3 7.9c-.4.4-.4 1 0 1.4l4 4c.4.4 1 .4 1.4 0l4-4c.4-.4.4-1 0-1.4s-1-.4-1.4 0L22 10.2v-9c0-.5-.4-1-1-1-.5 0-1 .4-1 1v9l-2.3-2.3c-.4-.4-1-.4-1.4 0z"/>
+</svg>
diff --git a/browser/extensions/formautofill/content/manageAddresses.xhtml b/browser/extensions/formautofill/content/manageAddresses.xhtml
new file mode 100644
index 0000000000..68e810179e
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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
+ xmlns="http://www.w3.org/1999/xhtml"
+ data-l10n-id="autofill-manage-dialog"
+ data-l10n-attrs="style"
+>
+ <head>
+ <title data-l10n-id="autofill-manage-addresses-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/manageDialog.css"
+ />
+ <script src="chrome://formautofill/content/manageDialog.js"></script>
+ </head>
+ <body>
+ <fieldset>
+ <legend data-l10n-id="autofill-manage-addresses-list-header" />
+ <select id="addresses" size="9" multiple="multiple" />
+ </fieldset>
+ <div id="controls-container">
+ <button
+ id="remove"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-remove-button"
+ />
+ <!-- Wrapper is used to properly compute the search tooltip position -->
+ <div>
+ <button id="add" data-l10n-id="autofill-manage-add-button" />
+ </div>
+ <button
+ id="edit"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-edit-button"
+ />
+ </div>
+ <script>
+ "use strict";
+ /* global ManageAddresses */
+ new ManageAddresses({
+ records: document.getElementById("addresses"),
+ controlsContainer: document.getElementById("controls-container"),
+ remove: document.getElementById("remove"),
+ add: document.getElementById("add"),
+ edit: document.getElementById("edit"),
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/manageCreditCards.xhtml b/browser/extensions/formautofill/content/manageCreditCards.xhtml
new file mode 100644
index 0000000000..3e5bdcfbf5
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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
+ xmlns="http://www.w3.org/1999/xhtml"
+ data-l10n-id="autofill-manage-dialog"
+ data-l10n-attrs="style"
+>
+ <head>
+ <title data-l10n-id="autofill-manage-credit-cards-title"></title>
+ <link rel="localization" href="browser/preferences/formAutofill.ftl" />
+ <link rel="localization" href="toolkit/payments/payments.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://formautofill/content/manageDialog.css"
+ />
+ <script src="chrome://formautofill/content/manageDialog.js"></script>
+ </head>
+ <body>
+ <fieldset>
+ <legend data-l10n-id="autofill-manage-credit-cards-list-header" />
+ <select id="credit-cards" size="9" multiple="multiple" />
+ </fieldset>
+ <div id="controls-container">
+ <button
+ id="remove"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-remove-button"
+ />
+ <!-- Wrapper is used to properly compute the search tooltip position -->
+ <div>
+ <button id="add" data-l10n-id="autofill-manage-add-button" />
+ </div>
+ <button
+ id="edit"
+ disabled="disabled"
+ data-l10n-id="autofill-manage-edit-button"
+ />
+ </div>
+ <script>
+ "use strict";
+ /* global ManageCreditCards */
+ new ManageCreditCards({
+ records: document.getElementById("credit-cards"),
+ controlsContainer: document.getElementById("controls-container"),
+ remove: document.getElementById("remove"),
+ add: document.getElementById("add"),
+ edit: document.getElementById("edit"),
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/content/manageDialog.css b/browser/extensions/formautofill/content/manageDialog.css
new file mode 100644
index 0000000000..f347c79118
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageDialog.css
@@ -0,0 +1,125 @@
+/* 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/. */
+
+html {
+ /* Prevent unnecessary horizontal scroll bar from showing */
+ overflow-x: hidden;
+}
+
+div {
+ display: flex;
+}
+
+button {
+ padding-inline: 10px;
+}
+
+fieldset {
+ margin: 0 4px;
+ padding: 0;
+ border: none;
+}
+
+fieldset > legend {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0.4em 0.7em;
+ background-color: var(--in-content-box-background);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 2px 2px 0 0;
+ user-select: none;
+}
+
+option:nth-child(even) {
+ background-color: var(--in-content-box-background-odd);
+}
+
+#addresses,
+#credit-cards {
+ width: 100%;
+ height: 16.6em;
+ margin: 0;
+ padding-inline: 0;
+ border-top: none;
+ border-radius: 0 0 2px 2px;
+}
+
+#addresses > option,
+#credit-cards > option {
+ display: flex;
+ align-items: center;
+ height: 1.6em;
+ padding-inline-start: 0.6em;
+}
+
+#controls-container {
+ margin-top: 1em;
+}
+
+#remove {
+ margin-inline-end: auto;
+}
+
+#credit-cards > option::before {
+ content: "";
+ background: url("icon-credit-card-generic.svg") no-repeat;
+ background-size: contain;
+ float: inline-start;
+ width: 16px;
+ height: 16px;
+ padding-inline-end: 10px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+/*
+ We use .png / @2x.png images where we don't yet have a vector version of a logo
+*/
+#credit-cards.branded > option[cc-type="amex"]::before {
+ background-image: url("third-party/cc-logo-amex.png");
+}
+
+#credit-cards.branded > option[cc-type="cartebancaire"]::before {
+ background-image: url("third-party/cc-logo-cartebancaire.png");
+}
+
+#credit-cards.branded > option[cc-type="diners"]::before {
+ background-image: url("third-party/cc-logo-diners.svg");
+}
+
+#credit-cards.branded > option[cc-type="discover"]::before {
+ background-image: url("third-party/cc-logo-discover.png");
+}
+
+#credit-cards.branded > option[cc-type="jcb"]::before {
+ background-image: url("third-party/cc-logo-jcb.svg");
+}
+
+#credit-cards.branded > option[cc-type="mastercard"]::before {
+ background-image: url("third-party/cc-logo-mastercard.svg");
+}
+
+#credit-cards.branded > option[cc-type="mir"]::before {
+ background-image: url("third-party/cc-logo-mir.svg");
+}
+
+#credit-cards.branded > option[cc-type="unionpay"]::before {
+ background-image: url("third-party/cc-logo-unionpay.svg");
+}
+
+#credit-cards.branded > option[cc-type="visa"]::before {
+ background-image: url("third-party/cc-logo-visa.svg");
+}
+
+@media (min-resolution: 1.1dppx) {
+ #credit-cards.branded > option[cc-type="amex"]::before {
+ background-image: url("third-party/cc-logo-amex@2x.png");
+ }
+ #credit-cards.branded > option[cc-type="cartebancaire"]::before {
+ background-image: url("third-party/cc-logo-cartebancaire@2x.png");
+ }
+ #credit-cards.branded > option[cc-type="discover"]::before {
+ background-image: url("third-party/cc-logo-discover@2x.png");
+ }
+}
diff --git a/browser/extensions/formautofill/content/manageDialog.js b/browser/extensions/formautofill/content/manageDialog.js
new file mode 100644
index 0000000000..25498fbcaf
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -0,0 +1,464 @@
+/* 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/. */
+
+/* exported ManageAddresses, ManageCreditCards */
+
+"use strict";
+
+const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
+const EDIT_CREDIT_CARD_URL =
+ "chrome://formautofill/content/editCreditCard.xhtml";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { AutofillTelemetry } = ChromeUtils.import(
+ "resource://autofill/AutofillTelemetry.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+
+const lazy = {};
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () =>
+ new Localization([
+ "browser/preferences/formAutofill.ftl",
+ "branding/brand.ftl",
+ ])
+);
+
+this.log = null;
+XPCOMUtils.defineLazyGetter(this, "log", () =>
+ FormAutofill.defineLogGetter(this, "manageAddresses")
+);
+
+class ManageRecords {
+ constructor(subStorageName, elements) {
+ this._storageInitPromise = formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._newRequest = false;
+ this._isLoadingRecords = false;
+ this.prefWin = window.opener;
+ window.addEventListener("DOMContentLoaded", this, { once: true });
+ }
+
+ async init() {
+ await this.loadRecords();
+ this.attachEventListeners();
+ // For testing only: Notify when the dialog is ready for interaction
+ window.dispatchEvent(new CustomEvent("FormReady"));
+ }
+
+ uninit() {
+ log.debug("uninit");
+ this.detachEventListeners();
+ this._elements = null;
+ }
+
+ /**
+ * Get the selected options on the addresses element.
+ *
+ * @returns {Array<DOMElement>}
+ */
+ get _selectedOptions() {
+ return Array.from(this._elements.records.selectedOptions);
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Load records and render them. This function is a wrapper for _loadRecords
+ * to ensure any reentrant will be handled well.
+ */
+ async loadRecords() {
+ // This function can be early returned when there is any reentrant happends.
+ // "_newRequest" needs to be set to ensure all changes will be applied.
+ if (this._isLoadingRecords) {
+ this._newRequest = true;
+ return;
+ }
+ this._isLoadingRecords = true;
+
+ await this._loadRecords();
+
+ // _loadRecords should be invoked again if there is any multiple entrant
+ // during running _loadRecords(). This step ensures that the latest request
+ // still is applied.
+ while (this._newRequest) {
+ this._newRequest = false;
+ await this._loadRecords();
+ }
+ this._isLoadingRecords = false;
+
+ // For testing only: Notify when records are loaded
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
+ }
+
+ async _loadRecords() {
+ let storage = await this.getStorage();
+ let records = await storage.getAll();
+ // Sort by last used time starting with most recent
+ records.sort((a, b) => {
+ let aLastUsed = a.timeLastUsed || a.timeLastModified;
+ let bLastUsed = b.timeLastUsed || b.timeLastModified;
+ return bLastUsed - aLastUsed;
+ });
+ await this.renderRecordElements(records);
+ this.updateButtonsStates(this._selectedOptions.length);
+ }
+
+ /**
+ * Render the records onto the page while maintaining selected options if
+ * they still exist.
+ *
+ * @param {Array<object>} records
+ */
+ async renderRecordElements(records) {
+ let selectedGuids = this._selectedOptions.map(option => option.value);
+ this.clearRecordElements();
+ for (let record of records) {
+ let { id, args, raw } = await this.getLabelInfo(record);
+ let option = new Option(
+ raw ?? "",
+ record.guid,
+ false,
+ selectedGuids.includes(record.guid)
+ );
+ if (id) {
+ document.l10n.setAttributes(option, id, args);
+ }
+
+ option.record = record;
+ this._elements.records.appendChild(option);
+ }
+ }
+
+ /**
+ * Remove all existing record elements.
+ */
+ clearRecordElements() {
+ let parent = this._elements.records;
+ while (parent.lastChild) {
+ parent.removeChild(parent.lastChild);
+ }
+ }
+
+ /**
+ * Remove records by selected options.
+ *
+ * @param {Array<DOMElement>} options
+ */
+ async removeRecords(options) {
+ let storage = await this.getStorage();
+ // Pause listening to storage change event to avoid triggering `loadRecords`
+ // when removing records
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+
+ for (let option of options) {
+ storage.remove(option.value);
+ option.remove();
+ }
+ this.updateButtonsStates(this._selectedOptions);
+
+ // Resume listening to storage change event
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ // For testing only: notify record(s) has been removed
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
+
+ for (let i = 0; i < options.length; i++) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
+ }
+ }
+
+ /**
+ * Enable/disable the Edit and Remove buttons based on number of selected
+ * options.
+ *
+ * @param {number} selectedCount
+ */
+ updateButtonsStates(selectedCount) {
+ log.debug("updateButtonsStates:", selectedCount);
+ if (selectedCount == 0) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.setAttribute("disabled", "disabled");
+ } else if (selectedCount == 1) {
+ this._elements.edit.removeAttribute("disabled");
+ this._elements.remove.removeAttribute("disabled");
+ } else if (selectedCount > 1) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "change": {
+ this.updateButtonsStates(this._selectedOptions.length);
+ break;
+ }
+ case "unload": {
+ this.uninit();
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.remove) {
+ this.removeRecords(this._selectedOptions);
+ } else if (event.target == this._elements.add) {
+ this.openEditDialog();
+ } else if (
+ event.target == this._elements.edit ||
+ (event.target.parentNode == this._elements.records && event.detail > 1)
+ ) {
+ this.openEditDialog(this._selectedOptions[0].record);
+ }
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.removeRecords(this._selectedOptions);
+ }
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "formautofill-storage-changed": {
+ this.loadRecords();
+ }
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("unload", this, { once: true });
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.records.addEventListener("change", this);
+ this._elements.records.addEventListener("click", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ }
+
+ /**
+ * Remove event listener
+ */
+ detachEventListeners() {
+ window.removeEventListener("keypress", this);
+ window.removeEventListener("contextmenu", this);
+ this._elements.records.removeEventListener("change", this);
+ this._elements.records.removeEventListener("click", this);
+ this._elements.controlsContainer.removeEventListener("click", this);
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+ }
+}
+
+class ManageAddresses extends ManageRecords {
+ telemetryType = AutofillTelemetry.ADDRESS;
+
+ constructor(elements) {
+ super("addresses", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
+ );
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ /**
+ * Open the edit address dialog to create/edit an address.
+ *
+ * @param {object} address [optional]
+ */
+ openEditDialog(address) {
+ this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, undefined, {
+ record: address,
+ // Don't validate in preferences since it's fine for fields to be missing
+ // for autofill purposes. For PaymentRequest addresses get more validation.
+ noValidate: true,
+ });
+ }
+
+ getLabelInfo(address) {
+ return { raw: FormAutofillUtils.getAddressLabel(address) };
+ }
+}
+
+class ManageCreditCards extends ManageRecords {
+ telemetryType = AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements) {
+ super("creditCards", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
+ );
+
+ this._isDecrypted = false;
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ /**
+ * Open the edit address dialog to create/edit a credit card.
+ *
+ * @param {object} creditCard [optional]
+ */
+ async openEditDialog(creditCard) {
+ // Ask for reauth if user is trying to edit an existing credit card.
+ if (creditCard) {
+ const reauthPasswordPromptMessage = await lazy.l10n.formatValue(
+ "autofill-edit-card-password-prompt"
+ );
+ const loggedIn = await FormAutofillUtils.ensureLoggedIn(
+ reauthPasswordPromptMessage
+ );
+ if (!loggedIn.authenticated) {
+ return;
+ }
+ }
+
+ let decryptedCCNumObj = {};
+ if (creditCard && creditCard["cc-number-encrypted"]) {
+ try {
+ decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(
+ creditCard["cc-number-encrypted"]
+ );
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ABORT) {
+ // User shouldn't be ask to reauth here, but it could happen.
+ // Return here and skip opening the dialog.
+ return;
+ }
+ // We've got ourselves a real error.
+ // Recover from encryption error so the user gets a chance to re-enter
+ // unencrypted credit card number.
+ decryptedCCNumObj["cc-number"] = "";
+ console.error(ex);
+ }
+ }
+ let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
+ this.prefWin.gSubDialog.open(
+ EDIT_CREDIT_CARD_URL,
+ { features: "resizable=no" },
+ {
+ record: decryptedCreditCard,
+ }
+ );
+ }
+
+ /**
+ * Get credit card display label. It should display masked numbers and the
+ * cardholder's name, separated by a comma.
+ *
+ * @param {object} creditCard
+ * @returns {Promise<string>}
+ */
+ async getLabelInfo(creditCard) {
+ // The card type is displayed visually using an image. For a11y, we need
+ // to expose it as text. We do this using aria-label. However,
+ // aria-label overrides the text content, so we must include that also.
+ // Since the text content is generated by Fluent, aria-label must be
+ // generated by Fluent also.
+ const type = creditCard["cc-type"];
+ const typeL10nId = CreditCard.getNetworkL10nId(type);
+ const typeName = typeL10nId
+ ? await document.l10n.formatValue(typeL10nId)
+ : type ?? ""; // Unknown card type
+ return CreditCard.getLabelInfo({
+ name: creditCard["cc-name"],
+ number: creditCard["cc-number"],
+ month: creditCard["cc-exp-month"],
+ year: creditCard["cc-exp-year"],
+ type: typeName,
+ });
+ }
+
+ async renderRecordElements(records) {
+ // Revert back to encrypted form when re-rendering happens
+ this._isDecrypted = false;
+ // Display third-party card icons when possible
+ this._elements.records.classList.toggle(
+ "branded",
+ AppConstants.MOZILLA_OFFICIAL
+ );
+ await super.renderRecordElements(records);
+
+ let options = this._elements.records.options;
+ for (let option of options) {
+ let record = option.record;
+ if (record && record["cc-type"]) {
+ option.setAttribute("cc-type", record["cc-type"]);
+ } else {
+ option.removeAttribute("cc-type");
+ }
+ }
+ }
+
+ updateButtonsStates(selectedCount) {
+ super.updateButtonsStates(selectedCount);
+ }
+
+ handleClick(event) {
+ super.handleClick(event);
+ }
+}
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-amex.png b/browser/extensions/formautofill/content/third-party/cc-logo-amex.png
new file mode 100644
index 0000000000..c51a5be4a0
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png
new file mode 100644
index 0000000000..f794641f3e
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png
new file mode 100644
index 0000000000..781c6e4958
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png
new file mode 100644
index 0000000000..38158846dd
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg b/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg
new file mode 100644
index 0000000000..9cc4d8b9ff
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-diners.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M19.863 20.068c4.698.022 8.987-3.839 8.987-8.536 0-5.137-4.289-8.688-8.987-8.686h-4.044c-4.755-.002-8.669 3.55-8.669 8.686 0 4.698 3.914 8.559 8.669 8.536h4.044z" fill="#4186CD"/><path d="M15.76 3.535a7.923 7.923 0 0 0 0 15.844 7.923 7.923 0 0 0 0-15.844zm-4.821 7.75c.004-2.122 1.288-3.931 3.1-4.65v9.3c-1.812-.719-3.096-2.527-3.1-4.65zm6.544 4.65v-9.3c1.811.717 3.097 2.527 3.1 4.65-.003 2.123-1.289 3.931-3.1 4.65z" fill="#FFF"/><g fill="#211E1F"><path d="M.65 22.925c0-.71-.375-.663-.733-.671v-.205c.31.015.63.015.94.015.336 0 .79-.015 1.381-.015 2.065 0 3.19 1.365 3.19 2.763 0 .782-.462 2.748-3.286 2.748-.407 0-.782-.016-1.157-.016-.358 0-.71.008-1.068.016v-.205c.478-.048.71-.064.733-.6v-3.83zm.644 3.636c0 .586.437.654.825.654 1.713 0 2.275-1.24 2.275-2.373 0-1.422-.951-2.449-2.48-2.449-.326 0-.476.022-.62.03v4.138zM5.428 27.364h.152c.225 0 .387 0 .387-.25v-2.041c0-.332-.121-.378-.419-.528v-.12c.378-.107.83-.249.861-.272a.301.301 0 0 1 .145-.038c.04 0 .057.046.057.106v2.893c0 .25.177.25.402.25h.137v.196c-.274 0-.556-.015-.845-.015-.29 0-.58.007-.877.015v-.196zm.689-4.627a.36.36 0 0 1-.345-.35c0-.177.169-.338.345-.338.182 0 .344.148.344.337 0 .19-.155.351-.344.351zM7.993 25.117c0-.278-.084-.353-.438-.496v-.143c.325-.106.634-.204.996-.363.022 0 .045.016.045.076v.49c.43-.309.8-.566 1.307-.566.64 0 .867.468.867 1.055v1.944c0 .25.166.25.377.25h.136v.196c-.265 0-.528-.015-.8-.015s-.544.007-.815.015v-.196h.136c.211 0 .362 0 .362-.25v-1.95c0-.43-.263-.642-.694-.642-.241 0-.626.196-.876.362v2.23c0 .25.166.25.378.25h.136v.196c-.264 0-.529-.015-.8-.015-.272 0-.544.007-.816.015v-.196h.137c.21 0 .362 0 .362-.25v-1.997zM11.943 25.569c-.017.072-.017.192 0 .465.049.762.553 1.388 1.212 1.388.453 0 .809-.24 1.113-.537l.115.113c-.38.489-.849.906-1.525.906-1.31 0-1.575-1.236-1.575-1.75 0-1.573 1.089-2.039 1.665-2.039.668 0 1.386.41 1.394 1.26 0 .05 0 .097-.008.145l-.074.049h-2.317zm1.514-.42c.212 0 .237-.077.237-.147 0-.3-.264-.542-.742-.542-.52 0-.877.264-.98.689h1.485zM14.383 27.364h.191c.198 0 .34 0 .34-.25v-2.117c0-.233-.262-.279-.368-.339v-.113c.516-.234.799-.43.863-.43.042 0 .063.023.063.099v.678h.015c.176-.294.474-.777.905-.777.176 0 .402.128.402.4 0 .203-.133.385-.331.385-.22 0-.22-.182-.468-.182-.12 0-.516.174-.516.626v1.77c0 .25.142.25.34.25h.395v.196c-.389-.008-.684-.015-.99-.015-.289 0-.586.007-.84.015v-.196zM17.282 26.668c.102.53.418.98.996.98.465 0 .64-.29.64-.57 0-.948-1.724-.643-1.724-1.935 0-.45.357-1.028 1.226-1.028.252 0 .592.073.9.234l.056.818h-.182c-.079-.505-.355-.795-.862-.795-.316 0-.616.185-.616.53 0 .94 1.834.65 1.834 1.91 0 .53-.42 1.092-1.36 1.092a2.06 2.06 0 0 1-.964-.272l-.087-.924.143-.04zM26.431 23.625h-.192c-.147-.94-.786-1.318-1.649-1.318-.886 0-2.173.618-2.173 2.548 0 1.626 1.11 2.792 2.296 2.792.763 0 1.395-.547 1.55-1.392l.176.048-.177 1.175c-.323.21-1.194.426-1.703.426-1.802 0-2.942-1.214-2.942-3.024 0-1.649 1.41-2.831 2.92-2.831.623 0 1.224.21 1.817.427l.077 1.15zM26.783 27.36h.153c.226 0 .387 0 .387-.253v-4.268c0-.498-.12-.514-.427-.598v-.123c.322-.099.66-.237.83-.33.087-.045.152-.084.176-.084.05 0 .065.046.065.108v5.295c0 .254.177.254.403.254h.136v.199c-.273 0-.555-.016-.845-.016-.29 0-.58.008-.878.016v-.2zM31.775 27.032c0 .136.084.143.214.143.092 0 .206-.007.305-.007v.159c-.328.03-.955.188-1.1.233l-.038-.023v-.61c-.458.37-.81.633-1.353.633-.412 0-.84-.264-.84-.897v-1.93c0-.196-.03-.384-.457-.422v-.143c.275-.007.885-.053.984-.053.085 0 .085.053.085.219v1.944c0 .226 0 .874.664.874.26 0 .604-.195.924-.458v-2.029c0-.15-.366-.233-.64-.309v-.135c.686-.046 1.115-.106 1.19-.106.062 0 .062.053.062.136v2.78zM33.372 24.72c.323-.27.76-.572 1.206-.572.94 0 1.505.804 1.505 1.671 0 1.042-.776 2.085-1.935 2.085-.599 0-.914-.191-1.125-.278l-.243.182-.169-.087a9.26 9.26 0 0 0 .113-1.416v-3.422c0-.518-.122-.534-.43-.621v-.128c.325-.103.664-.246.834-.342.09-.048.154-.088.18-.088.047 0 .064.048.064.112v2.905zm-.044 2.032c0 .301.291.808.834.808.868 0 1.232-.831 1.232-1.535 0-.854-.664-1.565-1.296-1.565-.3 0-.552.19-.77.372v1.92z"/></g></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-discover.png b/browser/extensions/formautofill/content/third-party/cc-logo-discover.png
new file mode 100644
index 0000000000..104f9ee2d6
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png b/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png
new file mode 100644
index 0000000000..1caaa01995
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png
Binary files differ
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg b/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg
new file mode 100644
index 0000000000..5cdbb027f8
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg
@@ -0,0 +1 @@
+<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" id="a"/><linearGradient x1="-.003%" y1="49.999%" x2="100.002%" y2="49.999%" id="b"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M0 1.564l.007.001V.007L0 .002v1.562z" id="d"/><linearGradient x1="0%" y1="50.019%" x2="1.21%" y2="50.019%" id="e"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" id="g"/><linearGradient x1=".004%" y1="49.999%" x2="99.996%" y2="49.999%" id="h"><stop stop-color="#753136" offset="0%"/><stop stop-color="#ED1746" offset="100%"/></linearGradient><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" id="j"/><linearGradient x1="0%" y1="50.008%" x2="99.996%" y2="50.008%" id="k"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" id="m"/><linearGradient x1=".022%" y1="49.994%" x2="100.012%" y2="49.994%" id="n"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" id="p"/><linearGradient x1="-.007%" y1="49.999%" x2="100.004%" y2="49.999%" id="q"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.013)"><mask id="c" fill="#fff"><use href="#a"/></mask><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" fill="url(#b)" mask="url(#c)"/></g><g transform="translate(0 16.543)"><mask id="f" fill="#fff"><use href="#d"/></mask><path d="M0 1.564l.007.001V.007L0 .002v1.562z" fill="url(#e)" mask="url(#f)"/></g><g transform="translate(10 3.013)"><mask id="i" fill="#fff"><use href="#g"/></mask><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" fill="url(#h)" mask="url(#i)"/></g><g transform="translate(22.353 14.778)"><mask id="l" fill="#fff"><use href="#j"/></mask><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" fill="url(#k)" mask="url(#l)"/></g><g transform="translate(22.353 11.837)"><mask id="o" fill="#fff"><use href="#m"/></mask><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" fill="url(#n)" mask="url(#o)"/></g><g transform="translate(20.588 3.013)"><mask id="r" fill="#fff"><use href="#p"/></mask><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" fill="url(#q)" mask="url(#r)"/></g></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg b/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg
new file mode 100644
index 0000000000..3e0f21f9e3
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg
@@ -0,0 +1 @@
+<svg width="38" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M7.485 29.258v-1.896a1.125 1.125 0 0 0-1.188-1.2 1.17 1.17 0 0 0-1.061.537 1.109 1.109 0 0 0-.999-.537.998.998 0 0 0-.885.448v-.373h-.657v3.021h.664v-1.662a.708.708 0 0 1 .74-.802c.435 0 .656.284.656.796v1.68h.664v-1.674a.71.71 0 0 1 .74-.802c.448 0 .663.284.663.796v1.68l.663-.012zm9.817-3.02h-1.08v-.917h-.664v.916h-.6v.6h.613v1.391c0 .701.271 1.119 1.049 1.119.29 0 .575-.08.821-.234l-.19-.563a1.213 1.213 0 0 1-.58.17c-.317 0-.437-.201-.437-.505v-1.377h1.074l-.006-.6zm5.605-.076a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.474.316a2.26 2.26 0 0 0-1.232-.316c-.765 0-1.264.366-1.264.966 0 .493.367.797 1.043.891l.316.045c.36.05.53.145.53.316 0 .234-.24.366-.688.366-.361.01-.715-.1-1.005-.316l-.316.512a2.18 2.18 0 0 0 1.308.392c.872 0 1.378-.41 1.378-.986 0-.575-.398-.809-1.056-.904l-.316-.044c-.284-.038-.511-.095-.511-.297 0-.202.214-.354.575-.354.333.004.659.093.947.26l.291-.531zm17.602-.316a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.467 1.58a1.526 1.526 0 0 0 1.611 1.58 1.58 1.58 0 0 0 1.087-.36l-.316-.532a1.327 1.327 0 0 1-.79.272.97.97 0 0 1 0-1.934c.286.003.563.099.79.272l.316-.53a1.58 1.58 0 0 0-1.087-.361 1.526 1.526 0 0 0-1.611 1.58v.012zm6.155 0v-1.505h-.658v.367a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.37.013.722-.152.948-.443v.366h.658v-1.504zm-2.446 0a.913.913 0 1 1 .916.966.907.907 0 0 1-.916-.967zm-7.93-1.58a1.58 1.58 0 1 0 .044 3.16c.454.023.901-.124 1.254-.411l-.316-.487c-.25.2-.559.311-.878.316a.837.837 0 0 1-.904-.74h2.243v-.252c0-.948-.587-1.58-1.434-1.58l-.01-.006zm0 .587a.749.749 0 0 1 .764.733h-1.58a.777.777 0 0 1 .803-.733h.012zm16.464.999v-2.724h-.632v1.58a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.369.013.722-.152.948-.443v.366h.632v-1.497zm1.096 1.07a.316.316 0 0 1 .218.086.294.294 0 0 1-.098.487.297.297 0 0 1-.12.025.316.316 0 0 1-.284-.183.297.297 0 0 1 .066-.329.316.316 0 0 1 .228-.085h-.01zm0 .535a.224.224 0 0 0 .165-.07.234.234 0 0 0 0-.316.234.234 0 0 0-.165-.07.237.237 0 0 0-.167.07.234.234 0 0 0 0 .316.234.234 0 0 0 .076.05c.032.015.066.021.101.02h-.01zm.02-.376a.126.126 0 0 1 .082.025c.02.016.03.041.028.066a.076.076 0 0 1-.022.057.11.11 0 0 1-.066.029l.091.104h-.072l-.086-.104h-.028v.104h-.06v-.278l.132-.003zm-.07.054v.075h.07a.066.066 0 0 0 .037 0 .032.032 0 0 0 0-.028.032.032 0 0 0 0-.028.066.066 0 0 0-.038 0l-.07-.02zm-3.476-1.283a.913.913 0 1 1 .917.967.907.907 0 0 1-.917-.967zm-22.19 0v-1.51h-.657v.366a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 1 0 0 3.16c.369.013.722-.152.948-.443v.366h.657v-1.497zm-2.445 0a.913.913 0 1 1 .916.967.907.907 0 0 1-.922-.967h.006z" fill="#231F20"/><path fill="#FF5F00" d="M14.215 3.22h9.953v17.886h-9.953z"/><path d="M14.847 12.165a11.356 11.356 0 0 1 4.345-8.945 11.375 11.375 0 1 0 0 17.886 11.356 11.356 0 0 1-4.345-8.941z" fill="#EB001B"/><path d="M37.596 12.165a11.375 11.375 0 0 1-18.404 8.941 11.375 11.375 0 0 0 0-17.886 11.375 11.375 0 0 1 18.404 8.941v.004zM36.51 19.265v-.412h.148v-.085h-.376v.085h.161v.412h.066zm.73 0v-.497h-.115l-.132.355-.133-.355h-.101v.497h.082v-.373l.123.323h.086l.123-.323v.376l.066-.003z" fill="#F79E1B"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg b/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg
new file mode 100644
index 0000000000..26a24f985d
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-mir.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="100%" y1="312.751%" x2=".612%" y2="312.751%" id="a"><stop stop-color="#1E5CD8" offset="0%"/><stop stop-color="#02AFFF" offset="100%"/></linearGradient></defs><g fill-rule="nonzero" fill="none"><path d="M7.812 11.313l-1.326 4.593h-.227l-1.326-4.594A1.823 1.823 0 0 0 3.18 10H0v10h3.184v-5.91h.227L5.234 20H7.51l1.819-5.91h.226V20h3.185V10H9.56c-.81 0-1.522.535-1.75 1.313zM25.442 20h3.204v-2.957h3.223c1.686 0 3.122-.953 3.677-2.293H25.442V20zm-5.676-8.945l-2.241 4.855h-.227V10h-3.184v10h2.703c.712 0 1.357-.414 1.654-1.055l2.242-4.851h.227V20h3.184V10H21.42c-.712 0-1.358.414-1.655 1.055z" fill="#006848"/><path d="M32.186 0c.92 0 1.752.352 2.382.93a3.49 3.49 0 0 1 1.146 2.59c0 .21-.023.417-.058.62H29.74a4.478 4.478 0 0 1-4.272-3.124c-.007-.02-.011-.043-.02-.067-.015-.054-.027-.113-.042-.168A4.642 4.642 0 0 1 25.293 0h6.893z" fill="url(#a)" transform="translate(0 10)"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg b/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg
new file mode 100644
index 0000000000..99ef7e86b4
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg
@@ -0,0 +1 @@
+<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path id="a" d="M0 .04h17.771v22.433H0z"/><path id="c" d="M.134.04h18.093v22.433H.134z"/><path id="e" d="M.202.04h17.77v22.433H.202z"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.179)"><mask id="b" fill="#fff"><use href="#a"/></mask><path d="M7.023.04h8.952C17.225.04 18 1.057 17.71 2.31l-4.168 17.893c-.294 1.25-1.545 2.269-2.795 2.269h-8.95c-1.248 0-2.027-1.02-1.736-2.269l4.17-17.893C4.52 1.058 5.771.04 7.022.04" fill="#E21837" mask="url(#b)"/></g><g transform="translate(8.073 3.179)"><mask id="d" fill="#fff"><use href="#c"/></mask><path d="M7.157.04h10.294c1.25 0 .686 1.018.392 2.271l-4.167 17.893c-.292 1.25-.201 2.269-1.453 2.269H1.93c-1.252 0-2.026-1.02-1.732-2.269L4.363 2.311C4.66 1.058 5.907.04 7.157.04" fill="#00457C" mask="url(#d)"/></g><g transform="translate(17.89 3.179)"><mask id="f" fill="#fff"><use href="#e"/></mask><path d="M7.224.04h8.952c1.251 0 2.028 1.018 1.734 2.271l-4.166 17.893c-.295 1.25-1.547 2.269-2.798 2.269H2c-1.252 0-2.028-1.02-1.735-2.269L4.432 2.311C4.723 1.058 5.972.04 7.224.04" fill="#007B84" mask="url(#f)"/></g><path d="M26.582 16.428L25.49 20.04h.295l-.228.746h-.292l-.069.23h-1.038l.07-.23H22.12l.21-.69h.215l1.106-3.667.22-.739h1.06l-.111.373s.282-.203.55-.272c.266-.07 1.801-.096 1.801-.096l-.227.734h-.362zm-1.866 0l-.28.923s.315-.142.484-.189c.174-.046.434-.061.434-.061l.203-.673h-.841zm-.42 1.38l-.29.96s.321-.163.492-.215c.174-.039.438-.072.438-.072l.205-.673h-.845zm-.675 2.24h.844l.242-.81h-.841l-.245.81z" fill="#FEFEFE"/><path d="M27.05 15.694h1.13l.012.42c-.008.072.054.106.186.106h.23l-.21.695h-.612c-.528.038-.73-.19-.715-.445l-.022-.776zM27.2 18.993H26.12l.185-.619h1.232l.175-.566h-1.216l.207-.698h3.384l-.21.698h-1.135l-.178.566h1.139l-.19.619h-1.229l-.219.26h.5l.121.78c.014.078.014.13.04.162.025.028.175.042.262.042h.152l-.231.759h-.385c-.058 0-.147-.005-.27-.01-.114-.01-.195-.077-.273-.116a.367.367 0 0 1-.202-.265l-.12-.778-.56.766c-.177.243-.417.428-.824.428h-.782l.205-.677h.3a.484.484 0 0 0 .218-.063.336.336 0 0 0 .166-.138l.816-1.15zM15.397 17.298h2.855l-.211.68H16.9l-.179.581h1.168l-.213.702h-1.167l-.284.945c-.034.104.278.117.39.117l.584-.08-.235.778H15.65c-.106 0-.185-.015-.299-.04a.312.312 0 0 1-.209-.153c-.048-.077-.122-.14-.071-.305l.378-1.25H14.8l.215-.714h.65l.173-.581h-.648l.207-.68zM17.317 16.074h1.171l-.212.712h-1.6l-.173.15c-.075.072-.1.042-.198.094-.09.045-.28.136-.525.136h-.513l.207-.684h.154c.13 0 .219-.012.264-.04a.617.617 0 0 0 .171-.222l.296-.535h1.163l-.205.389zM18.991 15.694h.997l-.146.502s.316-.252.536-.343c.22-.081.716-.154.716-.154l1.615-.01-.55 1.832a2.139 2.139 0 0 1-.269.608.7.7 0 0 1-.271.251 1.02 1.02 0 0 1-.375.126c-.106.008-.27.01-.496.014h-1.556l-.437 1.447c-.042.144-.061.213-.034.252a.18.18 0 0 0 .148.073l.686-.065-.235.794h-.766c-.245 0-.422-.006-.547-.015-.118-.01-.242 0-.325-.063-.07-.063-.18-.147-.177-.231.007-.078.04-.209.09-.389l1.396-4.63zm2.117 1.848h-1.634l-.1.33h1.414c.167-.02.202.004.216-.004l.104-.326zm-1.545-.297s.32-.292.867-.387c.124-.023.9-.015.9-.015l.119-.392h-1.647l-.24.794z" fill="#FEFEFE"/><path d="M21.899 18.648l-.093.44c-.04.137-.073.24-.177.328-.11.093-.237.19-.536.19l-.554.023-.005.497c-.005.14.032.126.054.149.026.025.049.035.073.045l.175-.01.529-.03-.22.726h-.606c-.425 0-.74-.01-.842-.091-.103-.065-.116-.146-.115-.286l.04-1.938h.968l-.014.397h.233c.08 0 .134-.008.167-.03a.175.175 0 0 0 .065-.1l.097-.31h.76zM8.082 8.932c-.033.158-.655 3.024-.656 3.026-.134.58-.231.993-.562 1.26a1 1 0 0 1-.66.23c-.409 0-.646-.203-.687-.587l-.007-.132.124-.781s.652-2.611.769-2.957l.01-.039c-1.27.011-1.495 0-1.51-.02-.009.028-.04.19-.04.19l-.666 2.943-.057.25-.11.816c0 .242.047.44.142.607.303.53 1.168.609 1.657.609.63 0 1.222-.134 1.622-.378.694-.41.875-1.051 1.037-1.62l.075-.293s.672-2.712.786-3.065c.004-.02.006-.03.012-.039-.92.01-1.192 0-1.28-.02M11.798 14.319c-.45-.008-.61-.008-1.135.02l-.02-.04c.045-.2.095-.398.14-.6l.065-.275c.097-.425.191-.92.202-1.072.01-.09.042-.317-.218-.317-.109 0-.223.053-.339.107-.063.226-.19.863-.252 1.153-.13.61-.138.681-.197.983l-.038.041a12.946 12.946 0 0 0-1.159.02l-.024-.046c.089-.362.178-.728.263-1.091.224-.986.278-1.362.338-1.863l.044-.03c.52-.073.647-.088 1.21-.202l.048.053-.087.313c.096-.057.187-.114.283-.163.266-.13.562-.17.724-.17.248 0 .518.069.63.355.107.254.036.567-.104 1.184l-.072.316c-.144.686-.168.812-.25 1.283l-.052.041zM13.627 14.319c-.272-.002-.448-.008-.617-.002-.17.002-.335.01-.588.022l-.013-.022-.016-.024c.069-.26.106-.35.14-.443a3.13 3.13 0 0 0 .128-.449c.08-.345.128-.586.16-.797.037-.204.057-.378.085-.58l.02-.015.02-.02c.27-.037.442-.062.618-.09.177-.023.355-.06.635-.113l.01.024.008.025c-.052.214-.105.427-.156.643-.05.217-.103.43-.15.643-.101.453-.142.623-.166.745-.024.115-.03.178-.069.412l-.025.021-.024.02zM17.67 12.768c.159-.692.036-1.015-.119-1.212-.234-.3-.648-.396-1.078-.396-.258 0-.873.025-1.354.468-.345.32-.505.754-.6 1.17-.098.423-.21 1.186.492 1.47.216.093.528.118.73.118.513 0 1.04-.141 1.436-.561.305-.341.445-.848.494-1.057m-1.18-.05c-.022.117-.124.551-.262.736-.097.136-.21.219-.337.219-.037 0-.26 0-.264-.332-.002-.163.031-.33.072-.512.119-.524.258-.964.616-.964.28 0 .3.328.175.853M28.677 14.365c-.544-.004-.7-.004-1.202.017l-.031-.04c.135-.517.272-1.032.393-1.554.158-.678.194-.966.245-1.363l.041-.033c.54-.077.69-.099 1.252-.203l.016.047c-.103.426-.203.85-.304 1.278-.206.893-.281 1.346-.36 1.813l-.05.038z" fill="#FEFEFE"/><path d="M28.935 12.83c.158-.688-.479-.062-.58-.289-.154-.354-.058-1.072-.683-1.312-.24-.095-.804.027-1.29.469-.34.315-.504.747-.597 1.161-.098.418-.21 1.18.488 1.452.222.095.422.123.624.113.702-.038 1.236-1.098 1.633-1.516.305-.333.358.124.405-.079m-1.074-.05c-.027.112-.13.549-.268.732-.092.13-.311.211-.437.211-.036 0-.257 0-.264-.325a2.225 2.225 0 0 1 .073-.512c.12-.515.258-.95.616-.95.28 0 .4.316.28.843M20.746 14.319a12.427 12.427 0 0 0-1.134.02l-.02-.04c.046-.2.097-.398.144-.6l.061-.275c.099-.425.194-.92.203-1.072.01-.09.042-.317-.216-.317-.113 0-.225.053-.341.107-.062.226-.192.863-.255 1.153-.126.61-.136.681-.193.983l-.04.041a12.904 12.904 0 0 0-1.156.02l-.024-.046c.088-.362.177-.728.262-1.091.224-.986.276-1.362.339-1.863l.04-.03c.52-.073.648-.088 1.212-.202l.043.053-.08.313a4.81 4.81 0 0 1 .281-.163c.264-.13.562-.17.724-.17.244 0 .516.069.632.355.105.254.033.567-.108 1.184l-.07.316c-.15.686-.17.812-.25 1.283l-.054.041zM25.133 10.61c-.079.359-.312.66-.61.806-.247.124-.549.134-.86.134h-.201l.015-.08.37-1.608.011-.082.005-.063.149.015.782.067c.302.117.426.418.34.81m-.487-1.68l-.375.003c-.974.012-1.364.008-1.524-.011l-.04.197-.348 1.618-.874 3.597c.85-.01 1.199-.01 1.345.006.034-.161.23-1.121.232-1.121 0 0 .168-.704.178-.73 0 0 .053-.073.106-.102h.078c.732 0 1.56 0 2.209-.477.441-.328.743-.81.877-1.398.035-.144.061-.315.061-.487 0-.225-.045-.447-.176-.62-.33-.464-.99-.472-1.75-.476M33.124 11.185l-.043-.05c-.556.113-.656.131-1.167.2l-.038.038-.005.024-.002-.009c-.38.877-.37.688-.679 1.378l-.003-.084-.077-1.497-.05-.05c-.581.113-.595.131-1.133.2l-.041.038c-.006.017-.006.037-.01.059l.004.007c.067.344.05.267.118.809.032.266.073.533.105.796.053.44.083.656.147 1.327-.363.6-.449.826-.798 1.352l.022.049c.524-.02.646-.02 1.035-.02l.084-.096c.294-.633 2.531-4.47 2.531-4.47M14.12 11.556c.298-.207.335-.493.085-.641-.254-.15-.7-.102-1 .105-.3.203-.333.49-.08.642.25.146.697.103.994-.106" fill="#FEFEFE"/><path d="M30.554 15.709l-.437.75c-.139.256-.395.447-.803.448l-.696-.012.203-.674h.137c.07 0 .121-.003.16-.023.036-.012.062-.04.09-.08l.258-.409h1.088z" fill="#FEFEFE"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg b/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg
new file mode 100644
index 0000000000..57bcc144d1
--- /dev/null
+++ b/browser/extensions/formautofill/content/third-party/cc-logo-visa.svg
@@ -0,0 +1 @@
+<svg width="44" height="30" xmlns="http://www.w3.org/2000/svg"><defs><path d="M22.8 9.786c-.025-1.96 1.765-3.053 3.113-3.703 1.385-.667 1.85-1.095 1.845-1.691-.01-.913-1.105-1.316-2.13-1.332-1.787-.027-2.826.478-3.652.86L21.332.938c.83-.378 2.364-.708 3.956-.722 3.735 0 6.18 1.824 6.193 4.653.014 3.59-5.02 3.79-4.985 5.395.012.486.481 1.005 1.51 1.138.508.066 1.914.117 3.506-.609l.626 2.884a9.623 9.623 0 0 1-3.329.605c-3.516 0-5.99-1.85-6.01-4.497m15.347 4.248a1.621 1.621 0 0 1-1.514-.998L31.296.428h3.733l.743 2.032h4.561l.431-2.032h3.29l-2.87 13.606h-3.038m.522-3.675l1.077-5.11h-2.95l1.873 5.11m-20.394 3.675L15.33.428h3.557l2.942 13.606h-3.556m-8.965-9.26L7.81 12.648c-.176.879-.87 1.386-1.64 1.386H.116l-.084-.395c1.242-.267 2.654-.697 3.51-1.157.523-.282.672-.527.844-1.196L7.224.428h3.76l5.763 13.606H13.01L9.31 4.774z" id="a"/><linearGradient x1="16.148%" y1="34.401%" x2="85.832%" y2="66.349%" id="b"><stop stop-color="#222357" offset="0%"/><stop stop-color="#254AA5" offset="100%"/></linearGradient></defs><g transform="matrix(1 0 0 -1 0 22.674)" fill="none" fill-rule="evenodd"><mask id="c" fill="#fff"><use href="#a"/></mask><path fill="url(#b)" fill-rule="nonzero" mask="url(#c)" d="M-4.669 12.849l44.237 16.12L49.63 1.929 5.395-14.19"/></g></svg> \ No newline at end of file