--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -172,36 +172,42 @@ class EditAddress extends EditAutofillFo
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.
* @param {object} config
- * @param {function} config.isCCNumber Function to determine is a string is a valid CC number.
+ * @param {function} config.isCCNumber Function to determine if a string is a valid CC number.
*/
- constructor(elements, record, config) {
+ constructor(elements, record, addresses, config) {
super(elements);
+ this._addresses = addresses;
Object.assign(this, config);
Object.assign(this._elements, {
ccNumber: this._elements.form.querySelector("#cc-number"),
year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow: this._elements.form.querySelector(".billingAddressRow"),
});
- this.loadRecord(record);
+ this.loadRecord(record, addresses);
this.attachEventListeners();
}
- loadRecord(record) {
- // _record must be updated before generateYears is called.
+ loadRecord(record, addresses) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
this._record = record;
+ this._addresses = addresses;
this.generateYears();
+ this.generateBillingAddressOptions();
super.loadRecord(record);
}
generateYears() {
const count = 11;
const currentYear = new Date().getFullYear();
const ccExpYear = this._record && this._record["cc-exp-year"];
@@ -218,16 +224,34 @@ class EditCreditCard extends EditAutofil
this._elements.year.appendChild(option);
}
if (ccExpYear && ccExpYear > currentYear + count) {
this._elements.year.appendChild(new Option(ccExpYear));
}
}
+ generateBillingAddressOptions() {
+ let billingAddressGUID = this._record && 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(this.getAddressLabel(address), guid, selected, selected);
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
attachEventListeners() {
this._elements.ccNumber.addEventListener("change", this);
super.attachEventListeners();
}
handleChange(event) {
super.handleChange(event);
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -42,34 +42,45 @@
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
<select id="cc-exp-year">
<option/>
</select>
</div>
+ <label class="billingAddressRow">
+ <span data-localization="billingAddress"/>
+ <select id="billingAddressGUID">
+ </select>
+ </label>
</form>
<div id="controls-container">
<button id="cancel" data-localization="cancelBtnLabel"/>
<button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
</div>
<script type="application/javascript"><![CDATA[
"use strict";
let {
+ getAddressLabel,
isCCNumber,
} = FormAutofillUtils;
let record = window.arguments && window.arguments[0];
+ let addresses = {};
+ for (let address of formAutofillStorage.addresses.getAll()) {
+ addresses[address.guid] = address;
+ }
/* import-globals-from autofillEditForms.js */
let fieldContainer = new EditCreditCard({
form: document.getElementById("form"),
- }, record,
+ }, record, addresses,
{
+ getAddressLabel: getAddressLabel.bind(FormAutofillUtils),
isCCNumber: isCCNumber.bind(FormAutofillUtils),
});
/* import-globals-from editDialog.js */
new EditCreditCardDialog({
title: document.querySelector("title"),
fieldContainer,
controlsContainer: document.getElementById("controls-container"),
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -133,8 +133,9 @@ countryWarningMessage2 = Form Autofill i
# LOCALIZATION NOTE (addNewCreditCardTitle, editCreditCardTitle): The dialog title for creating or editing
# credit cards in browser preferences.
addNewCreditCardTitle = Add New Credit Card
editCreditCardTitle = Edit Credit Card
cardNumber = Card Number
nameOnCard = Name on Card
cardExpires = Expires
+billingAddress = Billing Address
--- a/browser/extensions/formautofill/skin/shared/editCreditCard.css
+++ b/browser/extensions/formautofill/skin/shared/editCreditCard.css
@@ -8,16 +8,17 @@ form {
form > label,
form > div {
flex: 1 0 100%;
align-self: center;
margin: 0 0 0.5em !important;
}
+#billingAddressGUID,
input {
flex: 1 0 auto;
}
select {
margin: 0;
margin-inline-end: 0.7em;
}
--- a/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
@@ -1,12 +1,17 @@
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
+add_task(async function setup() {
+ let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
+ await formAutofillStorage.initialize();
+});
+
add_task(async function test_cancelEditCreditCardDialog() {
await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
win.document.querySelector("#cancel").click();
});
});
add_task(async function test_cancelEditCreditCardDialogWithESC() {
await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
@@ -21,59 +26,106 @@ add_task(async function test_saveCreditC
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(), {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-exp-year"].toString(), {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
info("saving credit card");
EventUtils.synthesizeKey("VK_RETURN", {}, win);
});
let creditCards = await getCreditCards();
is(creditCards.length, 1, "only one credit card is in storage");
for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_1)) {
if (fieldName === "cc-number") {
fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
}
is(creditCards[0][fieldName], fieldValue, "check " + fieldName);
}
+ is(creditCards[0].billingAddressGUID, undefined, "check billingAddressGUID");
ok(creditCards[0]["cc-number-encrypted"], "cc-number-encrypted exists");
});
add_task(async function test_saveCreditCardWithMaxYear() {
await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-number"], {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-name"], {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-exp-month"].toString(), {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-exp-year"].toString(), {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
info("saving credit card");
EventUtils.synthesizeKey("VK_RETURN", {}, win);
});
let creditCards = await getCreditCards();
- is(creditCards.length, 2, "Two credit card is in storage");
+ is(creditCards.length, 2, "Two credit cards are in storage");
for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_2)) {
if (fieldName === "cc-number") {
fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
}
is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
}
ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
await removeCreditCards([creditCards[1].guid]);
});
+add_task(async function test_saveCreditCardWithBillingAddress() {
+ await saveAddress(TEST_ADDRESS_4);
+ await saveAddress(TEST_ADDRESS_1);
+ let addresses = await getAddresses();
+ let billingAddress = addresses[0];
+
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: billingAddress.guid,
+ });
+
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-exp-month"].toString(), {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-exp-year"].toString(), {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(billingAddress["given-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 2, "Two credit cards are in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
+ }
+ ok(creditCards[1].billingAddressGUID, "billingAddressGUID is truthy");
+ ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
+ await removeCreditCards([creditCards[1].guid]);
+ await removeAddresses([
+ addresses[0].guid,
+ addresses[1].guid,
+ ]);
+});
+
add_task(async function test_editCreditCard() {
let creditCards = await getCreditCards();
is(creditCards.length, 1, "only one credit card is in storage");
await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("VK_RIGHT", {}, win);
EventUtils.synthesizeKey("test", {}, win);
@@ -85,16 +137,46 @@ add_task(async function test_editCreditC
is(creditCards.length, 1, "only one credit card is in storage");
is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"] + "test", "cc name changed");
await removeCreditCards([creditCards[0].guid]);
creditCards = await getCreditCards();
is(creditCards.length, 0, "Credit card storage is empty");
});
+add_task(async function test_editCreditCardWithMissingBillingAddress() {
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: "unknown-guid",
+ });
+ await saveCreditCard(TEST_CREDIT_CARD);
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "one credit card in storage");
+ is(creditCards[0].billingAddressGUID, TEST_CREDIT_CARD.billingAddressGUID,
+ "Check saved billingAddressGUID");
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ }, creditCards[0]);
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(creditCards[0]["cc-name"], TEST_CREDIT_CARD["cc-name"] + "test", "cc name changed");
+ is(creditCards[0].billingAddressGUID, undefined,
+ "unknown GUID removed upon manual save");
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
add_task(async function test_addInvalidCreditCard() {
await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
const unloadHandler = () => ok(false, "Edit credit card dialog shouldn't be closed");
win.addEventListener("unload", unloadHandler);
EventUtils.synthesizeKey("VK_TAB", {}, win);
EventUtils.synthesizeKey("test", {}, win);
EventUtils.synthesizeMouseAtCenter(win.document.querySelector("#save"), {}, win);
--- a/toolkit/components/payments/content/paymentDialogFrameScript.js
+++ b/toolkit/components/payments/content/paymentDialogFrameScript.js
@@ -61,16 +61,20 @@ let PaymentFrameScript = {
}
},
/**
* Expose privileged utility functions to the unprivileged page.
*/
exposeUtilityFunctions() {
let PaymentDialogUtils = {
+ getAddressLabel(address) {
+ return FormAutofillUtils.getAddressLabel(address);
+ },
+
isCCNumber(value) {
return FormAutofillUtils.isCCNumber(value);
},
};
let waivedContent = Cu.waiveXrays(content);
waivedContent.PaymentDialogUtils = Cu.cloneInto(PaymentDialogUtils, waivedContent, {
cloneFunctions: true,
});
--- a/toolkit/components/payments/res/containers/basic-card-form.js
+++ b/toolkit/components/payments/res/containers/basic-card-form.js
@@ -49,20 +49,22 @@ class BasicCardForm extends PaymentState
});
}
connectedCallback() {
this.promiseReady.then(form => {
this.appendChild(form);
let record = {};
+ let addresses = [];
this.formHandler = new EditCreditCard({
form,
- }, record, {
+ }, record, addresses, {
isCCNumber: PaymentDialogUtils.isCCNumber,
+ getAddressLabel: PaymentDialogUtils.getAddressLabel,
});
this.appendChild(this.genericErrorText);
this.appendChild(this.backButton);
this.appendChild(this.saveButton);
// Only call the connected super callback(s) once our markup is fully
// connected, including the shared form fetched asynchronously.
super.connectedCallback();
@@ -71,33 +73,34 @@ class BasicCardForm extends PaymentState
render(state) {
this.backButton.textContent = this.dataset.backButtonLabel;
this.saveButton.textContent = this.dataset.saveButtonLabel;
let record = {};
let {
page,
+ savedAddresses,
savedBasicCards,
} = state;
this.genericErrorText.textContent = page.error;
let editing = !!page.guid;
this.form.querySelector("#cc-number").disabled = editing;
// If a card is selected we want to edit it.
if (editing) {
record = savedBasicCards[page.guid];
if (!record) {
throw new Error("Trying to edit a non-existing card: " + page.guid);
}
}
- this.formHandler.loadRecord(record);
+ this.formHandler.loadRecord(record, savedAddresses);
}
handleEvent(event) {
switch (event.type) {
case "click": {
this.onClick(event);
break;
}
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -206,16 +206,17 @@ let DUPED_ADDRESSES = {
"guid": "46b2635a5b26",
"name": "Rita Foo",
"address-line1": "432 Another St",
},
};
let BASIC_CARDS_1 = {
"53f9d009aed2": {
+ billingAddressGUID: "68gjdh354j",
"cc-number": "************5461",
"guid": "53f9d009aed2",
"version": 1,
"timeCreated": 1505240896213,
"timeLastModified": 1515609524588,
"timeLastUsed": 0,
"timesUsed": 0,
"cc-name": "John Smith",
--- a/toolkit/components/payments/res/unprivileged-fallbacks.js
+++ b/toolkit/components/payments/res/unprivileged-fallbacks.js
@@ -24,12 +24,15 @@ var log = {
console.info("log.js", ...args);
},
debug(...args) {
console.debug("log.js", ...args);
},
};
var PaymentDialogUtils = {
+ getAddressLabel(address) {
+ return `${address.name} (${address.guid})`;
+ },
isCCNumber(str) {
return str.length > 0;
},
};