Bug 1432952 - Add a billing address picker to the credit card add/edit form. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 10 Apr 2018 18:31:05 -0700
changeset 412904 a6871a7a0aaa3116c33761328b634e4259fe4b50
parent 412903 0980a649d1d00aed704e9e2d887c61e99d2be05e
child 412905 83c1d17f2d85b89bc3429a92d1aa88006b093292
push id33823
push userebalazs@mozilla.com
push dateThu, 12 Apr 2018 09:38:35 +0000
treeherdermozilla-central@abd91e812e7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1432952
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;
   },
 };