tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 30150bc1117ee8ffd7d3fe77236082322c5b1a85
parent c6c4149ce299c15409d7281c85d6fdaedb9b26c4
Author: Micah Killoran <mtigley@mozilla.com>
Date:   Tue, 18 Nov 2025 16:30:17 +0000

Bug 1996749 - Create a subpage for managing payments. r=dimi,fluent-reviewers,jules,tgiles,bolsson

Differential Revision: https://phabricator.services.mozilla.com/D268930

Diffstat:
Mbrowser/components/preferences/main.js | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/preferences.js | 5+++++
Mbrowser/extensions/formautofill/content/manageDialog.mjs | 72+++++-------------------------------------------------------------------
Mbrowser/locales/en-US/browser/preferences/preferences.ftl | 18++++++++++++++++++
Mtoolkit/components/formautofill/FormAutofillPreferences.sys.mjs | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
5 files changed, 308 insertions(+), 77 deletions(-)

diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js @@ -18,6 +18,8 @@ ChromeUtils.defineESModuleGetters(this, { TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + FormAutofillPreferences: + "resource://autofill/FormAutofillPreferences.sys.mjs", }); // Constants & Enumeration Values @@ -1127,6 +1129,69 @@ Preferences.addSetting({ }, }); +Preferences.addSetting({ + id: "payment-item", + async onUserClick(e) { + const action = e.target.getAttribute("action"); + const guid = e.target.getAttribute("guid"); + if (action === "remove") { + let [title, confirm, cancel] = await document.l10n.formatValues([ + { id: "payments-remove-payment-prompt-title" }, + { id: "payments-remove-payment-prompt-confirm-button" }, + { id: "payments-remove-payment-prompt-cancel-button" }, + ]); + FormAutofillPreferences.prototype.openRemovePaymentDialog( + guid, + window.browsingContext.topChromeWindow.browsingContext, + title, + confirm, + cancel + ); + } else if (action === "edit") { + FormAutofillPreferences.prototype.openEditCreditCardDialog(guid, window); + } + }, +}); + +Preferences.addSetting({ + id: "add-payment-button", + onUserClick: ({ target }) => { + target.ownerGlobal.gSubDialog.open( + "chrome://formautofill/content/editCreditCard.xhtml" + ); + }, +}); + +Preferences.addSetting({ + id: "payments-list-header", +}); + +Preferences.addSetting( + class extends Preferences.AsyncSetting { + static id = "payments-list"; + + async getControlConfig(config) { + return { + ...config, + items: await FormAutofillPreferences.prototype.makePaymentsListItems(), + }; + } + + async setup() { + Services.obs.addObserver( + () => this.emitChange(), + "formautofill-storage-changed" + ); + await FormAutofillPreferences.prototype.initializePaymentsStorage(); + return () => + Services.obs.removeObserver( + this.emitChange, + "formautofill-storage-changed" + ); + } + } +); + SettingGroupManager.registerGroups({ containers: { // This section is marked as in progress for testing purposes @@ -2134,6 +2199,24 @@ SettingGroupManager.registerGroups({ }, ], }, + managePayments: { + items: [ + { + id: "add-payment-button", + control: "moz-button", + l10nId: "autofill-payment-methods-add-button", + }, + { + id: "payments-list", + control: "moz-box-group", + l10nId: "payments-list-header", + controlAttrs: { + hasHeader: true, + type: "list", + }, + }, + ], + }, }); /** diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js @@ -276,6 +276,11 @@ const CONFIG_PANES = Object.freeze({ l10nId: "preferences-doh-header2", groupIds: ["dnsOverHttpsAdvanced"], }, + managePayments: { + parent: "privacy", + l10nId: "autofill-payment-methods-manage-payments-title", + groupIds: ["managePayments"], + }, }); var gLastCategory = { category: undefined, subcategory: undefined }; diff --git a/browser/extensions/formautofill/content/manageDialog.mjs b/browser/extensions/formautofill/content/manageDialog.mjs @@ -3,8 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 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" @@ -20,8 +18,9 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { 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", + FormAutofillPreferences: + "resource://autofill/FormAutofillPreferences.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "log", () => @@ -371,70 +370,9 @@ export class ManageCreditCards extends ManageRecords { * @param {object} creditCard [optional] */ async openEditDialog(creditCard) { - // Ask for reauth if user is trying to edit an existing credit card. - if (creditCard) { - const promptMessage = lazy.FormAutofillUtils.reauthOSPromptMessage( - "autofill-edit-payment-method-os-prompt-macos", - "autofill-edit-payment-method-os-prompt-windows", - "autofill-edit-payment-method-os-prompt-other" - ); - let verified; - let result; - try { - verified = await lazy.FormAutofillUtils.verifyUserOSAuth( - FormAutofill.AUTOFILL_CREDITCARDS_OS_AUTH_LOCKED_PREF, - promptMessage - ); - result = verified ? "success" : "fail_user_canceled"; - } catch (ex) { - result = "fail_error"; - throw ex; - } finally { - Glean.formautofill.promptShownOsReauth.record({ - trigger: "edit", - result, - }); - } - if (!verified) { - return; - } - } - - let decryptedCCNumObj = {}; - let errorResult = 0; - if (creditCard && creditCard["cc-number-encrypted"]) { - try { - decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt( - creditCard["cc-number-encrypted"], - "formautofill_cc" - ); - } catch (ex) { - errorResult = ex.result; - 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); - } finally { - Glean.creditcard.osKeystoreDecrypt.record({ - isDecryptSuccess: errorResult === 0, - errorResult, - trigger: "edit", - }); - } - } - let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj); - this.prefWin.gSubDialog.open( - EDIT_CREDIT_CARD_URL, - { features: "resizable=no" }, - { - record: decryptedCreditCard, - } + return lazy.FormAutofillPreferences.openEditCreditCardDialog( + creditCard, + this.prefWin ); } diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl @@ -1192,6 +1192,8 @@ autofill-payment-methods-header = autofill-payment-methods-checkbox-message-2 = .label = Save and autofill payment info .accesskey = p +autofill-payment-methods-manage-payments-title = + .heading = Manage payment methods autofill-payment-methods-manage-payments-button = .label = Manage payment methods .accesskey = m @@ -1200,6 +1202,13 @@ autofill-reauth-payment-methods-checkbox-2 = .label = Require device sign in to autofill and manage payments methods .accesskey = o +autofill-payment-methods-add-button = Add new payment method +payments-list-header = + .label = Payment methods +payments-list-item-label = <strong>Payment methods</strong> +payments-remove-payment-prompt-title = Remove this payment method? +payments-remove-payment-prompt-confirm-button = Remove +payments-remove-payment-prompt-cancel-button = Cancel autofill-addresses-title = Addresses and more autofill-addresses-header = .aria-label = Addresses and more @@ -1210,6 +1219,15 @@ autofill-addresses-manage-addresses-button = .label = Manage addresses and more .accesskey = M +# These values are displayed for each credit card record listed on the Manage Payment methods +# settings page. +# Variables: +# $cardNumber (string) - The obscured credit card number (for example: 2423 *********) +# $expDate (string) - The obscured expiry date of the credit card (for example: XX/2027) +payment-moz-box-item = + .label = { $cardNumber } + .description = { $expDate } + ## Privacy Section - History history-header = History diff --git a/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs b/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs @@ -10,6 +10,8 @@ const MANAGE_ADDRESSES_URL = "chrome://formautofill/content/manageAddresses.xhtml"; const MANAGE_CREDITCARDS_URL = "chrome://formautofill/content/manageCreditCards.xhtml"; +const EDIT_CREDIT_CARD_URL = + "chrome://formautofill/content/editCreditCard.xhtml"; import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; @@ -17,6 +19,7 @@ import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUti const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", + formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs", }); ChromeUtils.defineLazyGetter( @@ -77,9 +80,7 @@ const FORM_AUTOFILL_CONFIG = { }, }; -export function FormAutofillPreferences() {} - -FormAutofillPreferences.prototype = { +export class FormAutofillPreferences { /** * Create the Form Autofill preference group. * @@ -89,14 +90,14 @@ FormAutofillPreferences.prototype = { init(document) { this.createPreferenceGroup(document); return this.refs.formAutofillFragment; - }, + } /** * Remove event listeners and the preference group. */ uninit() { this.refs.formAutofillGroup.remove(); - }, + } /** * Create Form Autofill preference group @@ -150,8 +151,14 @@ FormAutofillPreferences.prototype = { id: "savedPaymentsButton", pref: null, visible: () => FormAutofill.isAutofillCreditCardsAvailable, - onUserClick: ({ target }) => { - target.ownerGlobal.gSubDialog.open(MANAGE_CREDITCARDS_URL); + onUserClick: e => { + e.preventDefault(); + + if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) { + e.target.ownerGlobal.gotoPref("paneManagePayments"); + } else { + e.target.ownerGlobal.gSubDialog.open(MANAGE_CREDITCARDS_URL); + } }, }); win.Preferences.addSetting({ @@ -184,7 +191,11 @@ FormAutofillPreferences.prototype = { addressesGroup.getSetting = win.Preferences.getSetting.bind( win.Preferences ); - }, + } + + async initializePaymentsStorage() { + await lazy.formAutofillStorage.initialize(); + } async trySetOSAuthEnabled(win, checked) { let messageText = await lazy.l10n.formatValueSync( @@ -218,5 +229,181 @@ FormAutofillPreferences.prototype = { Glean.formautofill.requireOsReauthToggle.record({ toggle_state: checked, }); - }, -}; + } + + async makePaymentsListItems() { + const records = await lazy.formAutofillStorage.creditCards.getAll(); + if (!records.length) { + return []; + } + + const items = records.map(record => { + const config = { + id: "payment-item", + control: "moz-box-item", + l10nId: "payment-moz-box-item", + iconSrc: "chrome://formautofill/content/icon-credit-card-generic.svg", + l10nArgs: { + cardNumber: record["cc-number"].replace(/^(\*+)(\d+)$/, "$2$1"), + expDate: record["cc-exp"].replace(/^(\d{4})-\d{2}$/, "XX/$1"), + }, + options: [ + { + control: "moz-button", + iconSrc: "chrome://global/skin/icons/delete.svg", + type: "icon", + controlAttrs: { + slot: "actions", + action: "remove", + guid: record.guid, + }, + }, + { + control: "moz-button", + iconSrc: "chrome://global/skin/icons/edit.svg", + type: "icon", + controlAttrs: { + slot: "actions", + action: "edit", + guid: record.guid, + }, + }, + ], + }; + + return config; + }); + + return [ + { + id: "payments-list-header", + control: "moz-box-item", + l10nId: "payments-list-item-label", + }, + ...items, + ]; + } + + /** + * Open the browser window modal to prompt the user whether + * or they want to remove their payment. + * + * @param {string} guid + * The guid of the payment item we are prompting to remove. + * @param {object} browsingContext + * Browsing context to open the prompt in + * @param {string} title + * The title text displayed in the modal to prompt the user with + * @param {string} confirmBtn + * The text for confirming removing a payment method + * @param {string} cancelBtn + * The text for cancelling removing a payment method + */ + async openRemovePaymentDialog( + guid, + browsingContext, + title, + confirmBtn, + cancelBtn + ) { + const flags = + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + + Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1; + const result = await Services.prompt.asyncConfirmEx( + browsingContext, + Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + title, + null, + flags, + confirmBtn, + cancelBtn, + null, + null, + false + ); + + const propBag = result.QueryInterface(Ci.nsIPropertyBag2); + // Confirmed + if (propBag.get("buttonNumClicked") === 0) { + lazy.formAutofillStorage.creditCards.remove(guid); + } + } + + async openEditCreditCardDialog(guid, window) { + const creditCard = await lazy.formAutofillStorage.creditCards.get(guid); + return FormAutofillPreferences.openEditCreditCardDialog(creditCard, window); + } + /** + * Open the edit credit card dialog to create/edit a credit card. + * + * @param {object} creditCard + * The credit card we want to edit. + */ + static async openEditCreditCardDialog(creditCard, window) { + // 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" + ); + let verified; + let result; + try { + verified = await FormAutofillUtils.verifyUserOSAuth( + FormAutofill.AUTOFILL_CREDITCARDS_OS_AUTH_LOCKED_PREF, + promptMessage + ); + result = verified ? "success" : "fail_user_canceled"; + } catch (ex) { + result = "fail_error"; + throw ex; + } finally { + Glean.formautofill.promptShownOsReauth.record({ + trigger: "edit", + result, + }); + } + if (!verified) { + return; + } + } + + let decryptedCCNumObj = {}; + let errorResult = 0; + if (creditCard && creditCard["cc-number-encrypted"]) { + try { + decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt( + creditCard["cc-number-encrypted"], + "formautofill_cc" + ); + } catch (ex) { + errorResult = ex.result; + 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); + } finally { + Glean.creditcard.osKeystoreDecrypt.record({ + isDecryptSuccess: errorResult === 0, + errorResult, + trigger: "edit", + }); + } + } + let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj); + window.gSubDialog.open( + EDIT_CREDIT_CARD_URL, + { features: "resizable=no" }, + { + record: decryptedCreditCard, + } + ); + } +}