diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/extensions/formautofill/content | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/extensions/formautofill/content')
25 files changed, 2296 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..fea7a90a0a --- /dev/null +++ b/browser/extensions/formautofill/content/autofillEditForms.js @@ -0,0 +1,642 @@ +/* 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. + input.required = + fieldId == "country" || + fieldId == "name" || + requiredFields.has(fieldId); + }); + 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..0f1260634d --- /dev/null +++ b/browser/extensions/formautofill/content/customElements.js @@ -0,0 +1,401 @@ +/* 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"></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._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 { primary, secondary, ariaLabel } = JSON.parse( + this.getAttribute("ac-value") + ); + + this._label.textContent = primary.toString().replaceAll("*", "•"); + this._comment.textContent = secondary.toString().replaceAll("*", "•"); + 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 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"); + } + + // After focusing a field that was previously filled with cc information, + // the "ac-image" is falsely set for the listitem-footer. For now it helps us + // to distinguish between address and cc footer. In the future this false attribute + // setting should be fixed and the "ac-image" check replaced by a different method. + const buttonTextBundleKey = !this.getAttribute("ac-image") + ? "autocompleteManageAddresses" + : "autocompleteManageCreditCards"; + const buttonText = + this._stringBundle.GetStringFromName(buttonTextBundleKey); + this._optionButton.textContent = buttonText; + } + } + + 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..47ae4a2a3b --- /dev/null +++ b/browser/extensions/formautofill/content/editAddress.xhtml @@ -0,0 +1,118 @@ +<?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-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 :user-invalid + --> + <label id="name-container" class="container"> + <input id="name" type="text" required="required" /> + <span data-l10n-id="autofill-address-name" class="label-text" /> + </label> + <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..920be841c5 --- /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-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 :user-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..8aad87ceb3 --- /dev/null +++ b/browser/extensions/formautofill/content/editDialog.js @@ -0,0 +1,235 @@ +/* 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, { + AutofillTelemetry: "resource://autofill/AutofillTelemetry.sys.mjs", + formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs", +}); + +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-title2" + ); + } + } + + 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..911b152f8d --- /dev/null +++ b/browser/extensions/formautofill/content/formautofill.css @@ -0,0 +1,58 @@ +/* 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 { + &[resultstyles~="autofill-profile"] { + min-width: 150px !important; + } + + &[resultstyles~="autofill-insecureWarning"] { + min-width: 200px !important; + } + + > richlistbox > richlistitem { + &[originaltype="autofill-profile"], + &[originaltype="autofill-footer"], + &[originaltype="autofill-insecureWarning"], + &[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 */ + &[collapsed="true"] { + display: none; + } + } + + &[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-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..2b33f7750f --- /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-payment-methods-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-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..d7d90bf784 --- /dev/null +++ b/browser/extensions/formautofill/content/manageDialog.css @@ -0,0 +1,130 @@ +/* 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; + + > 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; + + > 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 + */ + &.branded > option { + &[cc-type="amex"]::before { + background-image: url("third-party/cc-logo-amex.png"); + } + + &[cc-type="cartebancaire"]::before { + background-image: url("third-party/cc-logo-cartebancaire.png"); + } + + &[cc-type="diners"]::before { + background-image: url("third-party/cc-logo-diners.svg"); + } + + &[cc-type="discover"]::before { + background-image: url("third-party/cc-logo-discover.png"); + } + + &[cc-type="jcb"]::before { + background-image: url("third-party/cc-logo-jcb.svg"); + } + + &[cc-type="mastercard"]::before { + background-image: url("third-party/cc-logo-mastercard.svg"); + } + + &[cc-type="mir"]::before { + background-image: url("third-party/cc-logo-mir.svg"); + } + + &[cc-type="unionpay"]::before { + background-image: url("third-party/cc-logo-unionpay.svg"); + } + + &[cc-type="visa"]::before { + background-image: url("third-party/cc-logo-visa.svg"); + } + + @media (min-resolution: 1.1dppx) { + &[cc-type="amex"]::before { + background-image: url("third-party/cc-logo-amex@2x.png"); + } + + &[cc-type="cartebancaire"]::before { + background-image: url("third-party/cc-logo-cartebancaire@2x.png"); + } + + &[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..b6bcb3a77b --- /dev/null +++ b/browser/extensions/formautofill/content/manageDialog.js @@ -0,0 +1,451 @@ +/* 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 { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); +const { AutofillTelemetry } = ChromeUtils.importESModule( + "resource://autofill/AutofillTelemetry.sys.mjs" +); + +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", +}); + +this.log = null; +ChromeUtils.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 promptMessage = FormAutofillUtils.reauthOSPromptMessage( + "autofill-edit-payment-method-os-prompt-macos", + "autofill-edit-payment-method-os-prompt-windows", + "autofill-edit-payment-method-os-prompt-other" + ); + + const loggedIn = await FormAutofillUtils.ensureLoggedIn(promptMessage); + 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 Binary files differnew file mode 100644 index 0000000000..c51a5be4a0 --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex.png 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 Binary files differnew file mode 100644 index 0000000000..f794641f3e --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png diff --git a/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png Binary files differnew file mode 100644 index 0000000000..781c6e4958 --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png 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 Binary files differnew file mode 100644 index 0000000000..38158846dd --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png 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 Binary files differnew file mode 100644 index 0000000000..104f9ee2d6 --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover.png 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 Binary files differnew file mode 100644 index 0000000000..1caaa01995 --- /dev/null +++ b/browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png 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 |