Bug 1432952 - Add a billing address picker to the credit card add/edit form. r=jaws draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 10 Apr 2018 18:31:05 -0700
changeset 780843 97ae3457c11e3a593bb1abe163a32e7008a086b1
parent 780012 e28eacb8c2c5d1e58c7e93ef71bd50226dfa08fd
child 780844 e821039b7e60c2c1fe016519dccad827dc5240a5
push id106139
push usermozilla@noorenberghe.ca
push dateThu, 12 Apr 2018 03:11:23 +0000
reviewersjaws
bugs1432952
milestone61.0a1
Bug 1432952 - Add a billing address picker to the credit card add/edit form. r=jaws MozReview-Commit-ID: 9tquQ0C7D96
browser/extensions/formautofill/content/autofillEditForms.js
browser/extensions/formautofill/content/editCreditCard.xhtml
browser/extensions/formautofill/locales/en-US/formautofill.properties
browser/extensions/formautofill/skin/shared/editCreditCard.css
browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
toolkit/components/payments/content/paymentDialogFrameScript.js
toolkit/components/payments/res/containers/basic-card-form.js
toolkit/components/payments/res/debugging.js
toolkit/components/payments/res/unprivileged-fallbacks.js
--- 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;
   },
 };