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:
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,
+ }
+ );
+ }
+}