Bug 1429180 - Option to use a new billing address when adding a new payment method. r?mattn draft
authorJared Wein <jwein@mozilla.com>
Thu, 10 May 2018 15:12:01 -0400
changeset 798378 a892847d1db0f9db95abf93375df16472a12957b
parent 798084 b75acf9652937ce79a9bf02de843c100db0e5ec7
child 798379 1b0dfaf79ba8f497b20158a16156fae3be7ef94c
push id110742
push userbmo:jaws@mozilla.com
push dateTue, 22 May 2018 19:21:05 +0000
reviewersmattn
bugs1429180
milestone62.0a1
Bug 1429180 - Option to use a new billing address when adding a new payment method. r?mattn MozReview-Commit-ID: 5LrpUw1LdT3
browser/components/payments/content/paymentDialogWrapper.js
browser/components/payments/res/containers/address-form.js
browser/components/payments/res/containers/address-picker.js
browser/components/payments/res/containers/basic-card-form.js
browser/components/payments/res/containers/payment-method-picker.js
browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
browser/components/payments/res/paymentRequest.js
browser/components/payments/res/paymentRequest.xhtml
browser/components/payments/test/browser/browser_address_edit.js
browser/components/payments/test/browser/browser_card_edit.js
browser/components/payments/test/mochitest/test_address_form.html
browser/components/payments/test/mochitest/test_basic_card_form.html
browser/extensions/formautofill/content/autofillEditForms.js
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -577,16 +577,21 @@ var paymentDialogWrapper = {
 
       // Select the new record
       if (selectedStateKey) {
         Object.assign(successStateChange, {
           [selectedStateKey]: guid,
         });
       }
 
+      const pageId = collectionName == "creditCards" ?
+                                   "basic-card-page" :
+                                   "address-page";
+      successStateChange[pageId].guid = guid;
+
       this.sendMessageToContent("updateState", successStateChange);
     } catch (ex) {
       this.sendMessageToContent("updateState", errorStateChange);
     }
   },
 
   /**
    * @implements {nsIObserver}
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -81,48 +81,49 @@ export default class AddressForm extends
       super.connectedCallback();
     });
   }
 
   render(state) {
     let record = {};
     let {
       page,
+      "address-page": addressPage,
     } = state;
 
     if (this.id && page && page.id !== this.id) {
       log.debug(`AddressForm: no need to further render inactive page: ${page.id}`);
       return;
     }
 
     this.cancelButton.textContent = this.dataset.cancelButtonLabel;
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = this.dataset.saveButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
 
     this.backButton.hidden = page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
-    if (page.addressFields) {
-      this.setAttribute("address-fields", page.addressFields);
+    if (addressPage.addressFields) {
+      this.setAttribute("address-fields", addressPage.addressFields);
     } else {
       this.removeAttribute("address-fields");
     }
 
-    this.pageTitle.textContent = page.title;
+    this.pageTitle.textContent = addressPage.title;
     this.genericErrorText.textContent = page.error;
 
-    let editing = !!page.guid;
+    let editing = !!addressPage.guid;
     let addresses = paymentRequest.getAddresses(state);
 
     // If an address is selected we want to edit it.
     if (editing) {
-      record = addresses[page.guid];
+      record = addresses[addressPage.guid];
       if (!record) {
-        throw new Error("Trying to edit a non-existing address: " + page.guid);
+        throw new Error("Trying to edit a non-existing address: " + addressPage.guid);
       }
       // When editing an existing record, prevent changes to persistence
       this.persistCheckbox.hidden = true;
     } else {
       // Adding a new record: default persistence to checked when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = !state.isPrivate;
     }
@@ -141,21 +142,29 @@ export default class AddressForm extends
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
       }
       case this.backButton: {
-        this.requestStore.setState({
+        let currentState = this.requestStore.getState();
+        const previousId = currentState.page.previousId;
+        let state = {
           page: {
-            id: "payment-summary",
+            id: previousId || "payment-summary",
           },
-        });
+        };
+        if (previousId) {
+          state[previousId] = Object.assign({}, currentState[previousId], {
+            preserveFieldValues: true,
+          });
+        }
+        this.requestStore.setState(state);
         break;
       }
       case this.saveButton: {
         this.saveRecord();
         break;
       }
       default: {
         throw new Error("Unexpected click target");
@@ -164,48 +173,61 @@ export default class AddressForm extends
   }
 
   saveRecord() {
     let record = this.formHandler.buildFormObject();
     let {
       page,
       tempAddresses,
       savedBasicCards,
+      "address-page": addressPage,
+      "basic-card-page": basicCardPage,
     } = this.requestStore.getState();
-    let editing = !!page.guid;
+    let editing = !!addressPage.guid;
 
-    if (editing ? (page.guid in tempAddresses) : !this.persistCheckbox.checked) {
+    if (editing ? (addressPage.guid in tempAddresses) : !this.persistCheckbox.checked) {
       record.isTemporary = true;
     }
 
     let state = {
       errorStateChange: {
         page: {
           id: "address-page",
           onboardingWizard: page.onboardingWizard,
           error: this.dataset.errorGenericSave,
         },
+        "address-page": addressPage,
       },
       preserveOldProperties: true,
       selectedStateKey: page.selectedStateKey,
     };
 
     if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
       state.successStateChange = {
         page: {
           id: "basic-card-page",
-          onboardingWizard: true,
-          guid: null,
+          previousId: "address-page",
+          onboardingWizard: page.onboardingWizard,
         },
       };
     } else {
       state.successStateChange = {
         page: {
-          id: "payment-summary",
+          id: page.previousId || "payment-summary",
+          onboardingWizard: page.onboardingWizard,
         },
       };
     }
 
-    paymentRequest.updateAutofillRecord("addresses", record, page.guid, state);
+    state.successStateChange["address-page"] = addressPage;
+    state.successStateChange["basic-card-page"] = basicCardPage;
+
+    const previousId = page.previousId;
+    if (previousId) {
+      state.successStateChange[previousId].preserveFieldValues = true;
+      state.successStateChange[previousId].addressesModified = true;
+    }
+
+    paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid, state);
   }
 }
 
 customElements.define("address-form", AddressForm);
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -163,32 +163,34 @@ export default class AddressPicker exten
       });
     }
   }
 
   onClick({target}) {
     let nextState = {
       page: {
         id: "address-page",
+      },
+      "address-page": {
+        addressFields: this.getAttribute("address-fields"),
         selectedStateKey: this.selectedStateKey,
-        addressFields: this.getAttribute("address-fields"),
       },
     };
 
     switch (target) {
       case this.addLink: {
-        nextState.page.guid = null;
-        nextState.page.title = this.dataset.addAddressTitle;
+        nextState["address-page"].guid = null;
+        nextState["address-page"].title = this.dataset.addAddressTitle;
         break;
       }
       case this.editLink: {
         let state = this.requestStore.getState();
         let selectedAddressGUID = state[this.selectedStateKey];
-        nextState.page.guid = selectedAddressGUID;
-        nextState.page.title = this.dataset.editAddressTitle;
+        nextState["address-page"].guid = selectedAddressGUID;
+        nextState["address-page"].title = this.dataset.editAddressTitle;
         break;
       }
       default: {
         throw new Error("Unexpected onClick");
       }
     }
 
     this.requestStore.setState(nextState);
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -22,16 +22,25 @@ export default class BasicCardForm exten
 
     this.pageTitle = document.createElement("h1");
     this.genericErrorText = document.createElement("div");
 
     this.cancelButton = document.createElement("button");
     this.cancelButton.className = "cancel-button";
     this.cancelButton.addEventListener("click", this);
 
+    this.addressAddLink = document.createElement("a");
+    this.addressAddLink.className = "add-link";
+    this.addressAddLink.href = "javascript:void(0)";
+    this.addressAddLink.addEventListener("click", this);
+    this.addressEditLink = document.createElement("a");
+    this.addressEditLink.className = "edit-link";
+    this.addressEditLink.href = "javascript:void(0)";
+    this.addressEditLink.addEventListener("click", this);
+
     this.backButton = document.createElement("button");
     this.backButton.className = "back-button";
     this.backButton.addEventListener("click", this);
 
     this.saveButton = document.createElement("button");
     this.saveButton.className = "save-button";
     this.saveButton.addEventListener("click", this);
 
@@ -67,77 +76,95 @@ export default class BasicCardForm exten
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
         getAddressLabel: PaymentDialogUtils.getAddressLabel,
       });
 
+      let fragment = document.createDocumentFragment();
+      fragment.append(this.addressAddLink);
+      fragment.append(" ");
+      fragment.append(this.addressEditLink);
+      let billingAddressRow = this.form.querySelector(".billingAddressRow");
+      billingAddressRow.appendChild(fragment);
+
       this.appendChild(this.persistCheckbox);
       this.appendChild(this.genericErrorText);
       this.appendChild(this.cancelButton);
       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();
     });
   }
 
   render(state) {
     let {
       page,
       selectedShippingAddress,
+      "basic-card-page": basicCardPage,
     } = state;
 
     if (this.id && page && page.id !== this.id) {
       log.debug(`BasicCardForm: no need to further render inactive page: ${page.id}`);
       return;
     }
 
     this.cancelButton.textContent = this.dataset.cancelButtonLabel;
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = this.dataset.saveButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
+    this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
+    this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
 
     // The back button is temporarily hidden(See Bug 1462461).
     this.backButton.hidden = !!page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
     let record = {};
     let basicCards = paymentRequest.getBasicCards(state);
     let addresses = paymentRequest.getAddresses(state);
 
     this.genericErrorText.textContent = page.error;
 
-    let editing = !!page.guid;
+    let editing = !!basicCardPage.guid;
     this.form.querySelector("#cc-number").disabled = editing;
 
     // If a card is selected we want to edit it.
     if (editing) {
       this.pageTitle.textContent = this.dataset.editBasicCardTitle;
-      record = basicCards[page.guid];
+      record = basicCards[basicCardPage.guid];
       if (!record) {
-        throw new Error("Trying to edit a non-existing card: " + page.guid);
+        throw new Error("Trying to edit a non-existing card: " + basicCardPage.guid);
       }
       // When editing an existing record, prevent changes to persistence
       this.persistCheckbox.hidden = true;
     } else {
       this.pageTitle.textContent = this.dataset.addBasicCardTitle;
       // Use a currently selected shipping address as the default billing address
-      if (selectedShippingAddress) {
+      if (!record.billingAddressGUID && selectedShippingAddress) {
         record.billingAddressGUID = selectedShippingAddress;
       }
       // Adding a new record: default persistence to checked when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = !state.isPrivate;
     }
 
-    this.formHandler.loadRecord(record, addresses);
+    this.formHandler.loadRecord(record, addresses, basicCardPage.preserveFieldValues);
+
+    this.form.querySelector(".billingAddressRow").hidden = false;
+
+    if (basicCardPage.addressesModified) {
+      let addressGuid = state["address-page"].guid;
+      let billingAddressSelect = this.form.querySelector("#billingAddressGUID");
+      billingAddressSelect.value = addressGuid;
+    }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
@@ -145,16 +172,45 @@ export default class BasicCardForm exten
   }
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
       }
+      case this.addressAddLink:
+      case this.addressEditLink: {
+        let {
+          "basic-card-page": basicCardPage,
+        } = this.requestStore.getState();
+        let nextState = {
+          page: {
+            id: "address-page",
+            previousId: "basic-card-page",
+          },
+          "address-page": {
+            guid: null,
+            title: this.dataset.billingAddressTitleAdd,
+          },
+          "basic-card-page": {
+            preserveFieldValues: true,
+            guid: basicCardPage.guid,
+          },
+        };
+        let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
+        let selectedOption = billingAddressGUID.selectedOptions.length &&
+                             billingAddressGUID.selectedOptions[0];
+        if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
+          nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
+          nextState["address-page"].guid = selectedOption.value;
+        }
+        this.requestStore.setState(nextState);
+        break;
+      }
       case this.backButton: {
         this.requestStore.setState({
           page: {
             id: "payment-summary",
           },
         });
         break;
       }
@@ -165,47 +221,56 @@ export default class BasicCardForm exten
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
   saveRecord() {
     let record = this.formHandler.buildFormObject();
+    let currentState = this.requestStore.getState();
     let {
       page,
       tempBasicCards,
-    } = this.requestStore.getState();
-    let editing = !!page.guid;
+      "basic-card-page": basicCardPage,
+    } = currentState;
+    let editing = !!basicCardPage.guid;
 
-    if (editing ? (page.guid in tempBasicCards) : !this.persistCheckbox.checked) {
+    if (editing ? (basicCardPage.guid in tempBasicCards) : !this.persistCheckbox.checked) {
       record.isTemporary = true;
     }
 
     for (let editableFieldName of ["cc-name", "cc-exp-month", "cc-exp-year"]) {
       record[editableFieldName] = record[editableFieldName] || "";
     }
 
     // Only save the card number if we're saving a new record, otherwise we'd
     // overwrite the unmasked card number with the masked one.
     if (!editing) {
       record["cc-number"] = record["cc-number"] || "";
     }
 
-    paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
+    let state = {
       errorStateChange: {
         page: {
           id: "basic-card-page",
           error: this.dataset.errorGenericSave,
         },
       },
       preserveOldProperties: true,
       selectedStateKey: "selectedPaymentCard",
       successStateChange: {
         page: {
           id: "payment-summary",
         },
       },
-    });
+    };
+
+    const previousId = page.previousId;
+    if (previousId) {
+      state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
+    }
+
+    paymentRequest.updateAutofillRecord("creditCards", record, basicCardPage.guid, state);
   }
 }
 
 customElements.define("basic-card-form", BasicCardForm);
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -128,27 +128,28 @@ export default class PaymentMethodPicker
     this.requestStore.setState(stateChange);
   }
 
   onClick({target}) {
     let nextState = {
       page: {
         id: "basic-card-page",
       },
+      "basic-card-page": {},
     };
 
     switch (target) {
       case this.addLink: {
-        nextState.page.guid = null;
+        nextState["basic-card-page"].guid = null;
         break;
       }
       case this.editLink: {
         let state = this.requestStore.getState();
         let selectedPaymentCardGUID = state[this.selectedStateKey];
-        nextState.page.guid = selectedPaymentCardGUID;
+        nextState["basic-card-page"].guid = selectedPaymentCardGUID;
         break;
       }
       default: {
         throw new Error("Unexpected onClick");
       }
     }
 
     this.requestStore.setState(nextState);
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -10,20 +10,31 @@ import PaymentsStore from "../PaymentsSt
 
 /**
  * State of the payment request dialog.
  */
 export let requestStore = new PaymentsStore({
   changesPrevented: false,
   completionState: "initial",
   orderDetailsShowing: false,
+  "basic-card-page": {
+    guid: null,
+    // preserveFieldValues: true,
+  },
+  "address-page": {
+    guid: null,
+    title: "",
+  },
+  "payment-summary": {
+  },
   page: {
     id: "payment-summary",
+    previousId: null,
     // onboardingWizard: true,
-    // error: "My error",
+    // error: "",
   },
   request: {
     tabId: null,
     topLevelPrincipal: {URI: {displayHost: null}},
     requestId: null,
     paymentMethods: [],
     paymentDetails: {
       id: null,
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -129,17 +129,16 @@ var paymentRequest = {
       state.page = {
         id: "address-page",
         onboardingWizard: true,
       };
     } else if (Object.keys(detail.savedBasicCards).length == 0) {
       state.page = {
         id: "basic-card-page",
         onboardingWizard: true,
-        guid: null,
       };
     }
 
     document.querySelector("payment-dialog").setStateFromParent(state);
   },
 
   cancel() {
     this.sendMessageToChrome("paymentCancel");
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -20,30 +20,34 @@
   <!ENTITY payer.addLink.label        "Add">
   <!ENTITY payer.editLink.label       "Edit">
   <!ENTITY shippingAddress.addPage.title  "Add Shipping Address">
   <!ENTITY shippingAddress.editPage.title "Edit Shipping Address">
   <!ENTITY deliveryAddress.addPage.title  "Add Delivery Address">
   <!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
   <!ENTITY pickupAddress.addPage.title    "Add Pickup Address">
   <!ENTITY pickupAddress.editPage.title   "Edit Pickup Address">
+  <!ENTITY billingAddress.addPage.title   "Add Billing Address">
+  <!ENTITY billingAddress.editPage.title  "Edit Billing Address">
   <!ENTITY basicCard.addPage.title    "Add Credit Card">
   <!ENTITY basicCard.editPage.title   "Edit Credit Card">
   <!ENTITY payer.addPage.title        "Add Payer Contact">
   <!ENTITY payer.editPage.title       "Edit Payer Contact">
   <!ENTITY payerLabel                 "Contact Information">
   <!ENTITY cancelPaymentButton.label   "Cancel">
   <!ENTITY approvePaymentButton.label  "Pay">
   <!ENTITY processingPaymentButton.label "Processing">
   <!ENTITY successPaymentButton.label    "Done">
   <!ENTITY failPaymentButton.label       "Fail">
   <!ENTITY unknownPaymentButton.label    "Unknown">
   <!ENTITY orderDetailsLabel          "Order Details">
   <!ENTITY orderTotalLabel            "Total">
   <!ENTITY basicCardPage.error.genericSave    "There was an error saving the payment card.">
+  <!ENTITY basicCardPage.addressAddLink.label "Add">
+  <!ENTITY basicCardPage.addressEditLink.label "Edit">
   <!ENTITY basicCardPage.backButton.label     "Back">
   <!ENTITY basicCardPage.saveButton.label     "Save">
   <!ENTITY basicCardPage.persistCheckbox.label     "Save credit card to Firefox (Security code will not be saved)">
   <!ENTITY addressPage.error.genericSave      "There was an error saving the address.">
   <!ENTITY addressPage.cancelButton.label     "Cancel">
   <!ENTITY addressPage.backButton.label       "Back">
   <!ENTITY addressPage.saveButton.label       "Save">
   <!ENTITY addressPage.persistCheckbox.label  "Save address to Firefox">
@@ -133,16 +137,20 @@
         <order-details></order-details>
       </section>
 
       <basic-card-form id="basic-card-page"
                        class="page"
                        data-add-basic-card-title="&basicCard.addPage.title;"
                        data-edit-basic-card-title="&basicCard.editPage.title;"
                        data-error-generic-save="&basicCardPage.error.genericSave;"
+                       data-address-add-link-label="&basicCardPage.addressAddLink.label;"
+                       data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
+                       data-billing-address-title-add="&billingAddress.addPage.title;"
+                       data-billing-address-title-edit="&billingAddress.editPage.title;"
                        data-back-button-label="&basicCardPage.backButton.label;"
                        data-save-button-label="&basicCardPage.saveButton.label;"
                        data-cancel-button-label="&cancelPaymentButton.label;"
                        data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
                        hidden="hidden"></basic-card-form>
 
       <address-form id="address-page"
                     class="page"
--- a/browser/components/payments/test/browser/browser_address_edit.js
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -49,25 +49,25 @@ add_task(async function test_add_link() 
       }, "No saved addresses when starting test");
 
       let addLink = content.document.querySelector("address-picker .add-link");
       is(addLink.textContent, "Add", "Add link text");
 
       addLink.click();
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-        return state.page.id == "address-page" && !state.page.guid;
+        return state.page.id == "address-page" && !state["address-page"].guid;
       }, "Check add page state");
 
       let title = content.document.querySelector("address-form h1");
       is(title.textContent, "Add Shipping Address", "Page title should be set");
 
-      let persistInput = content.document.querySelector("address-form labelled-checkbox");
-      ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
-      ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
+      let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
+      ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
+      ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
 
       info("filling fields");
       for (let [key, val] of Object.entries(address)) {
         let field = content.document.getElementById(key);
         if (!field) {
           ok(false, `${key} field not found`);
         }
         field.value = val;
@@ -140,24 +140,24 @@ add_task(async function test_edit_link()
       Cu.waiveXrays(picker).dropdown.popupBox.children[0].click();
 
       let editLink = content.document.querySelector("address-picker .edit-link");
       is(editLink.textContent, "Edit", "Edit link text");
 
       editLink.click();
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-        return state.page.id == "address-page" && !!state.page.guid;
+        return state.page.id == "address-page" && !!state["address-page"].guid;
       }, "Check edit page state");
 
       let title = content.document.querySelector("address-form h1");
       is(title.textContent, "Edit Shipping Address", "Page title should be set");
 
-      let persistInput = content.document.querySelector("address-form labelled-checkbox");
-      ok(persistInput.hidden, "checkbox should be hidden when editing an address");
+      let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
+      ok(persistCheckbox.hidden, "checkbox should be hidden when editing an address");
 
       info("overwriting field values");
       for (let [key, val] of Object.entries(address)) {
         let field = content.document.getElementById(key);
         field.value = val;
         ok(!field.disabled, `Field #${key} shouldn't be disabled`);
       }
 
@@ -225,25 +225,25 @@ add_task(async function test_add_payer_c
       }, "No saved addresses when starting test");
 
       let addLink = content.document.querySelector("address-picker.payer-related .add-link");
       is(addLink.textContent, "Add", "Add link text");
 
       addLink.click();
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-        return state.page.id == "address-page" && !state.page.guid;
+        return state.page.id == "address-page" && !state["address-page"].guid;
       }, "Check add page state");
 
       let title = content.document.querySelector("address-form h1");
       is(title.textContent, "Add Payer Contact", "Page title should be set");
 
-      let persistInput = content.document.querySelector("address-form labelled-checkbox");
-      ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
-      ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
+      let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
+      ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
+      ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
 
       info("filling fields");
       for (let [key, val] of Object.entries(address)) {
         let field = content.document.getElementById(key);
         if (!field) {
           ok(false, `${key} field not found`);
         }
         field.value = val;
@@ -312,25 +312,24 @@ add_task(async function test_edit_payer_
 
       let editLink =
         content.document.querySelector("address-picker.payer-related .edit-link");
       is(editLink.textContent, "Edit", "Edit link text");
 
       editLink.click();
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-        info("state.page.id: " + state.page.id + "; state.page.guid: " + state.page.guid);
-        return state.page.id == "address-page" && !!state.page.guid;
+        return state.page.id == "address-page" && !!state["address-page"].guid;
       }, "Check edit page state");
 
       let title = content.document.querySelector("address-form h1");
       is(title.textContent, "Edit Payer Contact", "Page title should be set");
 
-      let persistInput = content.document.querySelector("address-form labelled-checkbox");
-      ok(persistInput.hidden, "checkbox should be hidden when editing an address");
+      let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
+      ok(persistCheckbox.hidden, "checkbox should be hidden when editing an address");
 
       info("overwriting field values");
       for (let [key, val] of Object.entries(address)) {
         let field = content.document.getElementById(key);
         field.value = val + "1";
         ok(!field.disabled, `Field #${key} shouldn't be disabled`);
       }
 
@@ -432,19 +431,20 @@ add_task(async function test_private_per
     // add an address
     // (return to summary view)
     info("add an address");
     await spawnPaymentDialogTask(frame, async () => {
       let {
         PaymentTestUtils: PTU,
       } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
-      let persistInput = content.document.querySelector("address-form labelled-checkbox");
-      ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
-      ok(!Cu.waiveXrays(persistInput).checked, "persist checkbox should be unchecked by default");
+      let persistCheckbox = content.document.querySelector("address-form labelled-checkbox");
+      ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new address");
+      ok(!Cu.waiveXrays(persistCheckbox).checked,
+         "persist checkbox should be unchecked by default");
 
       info("add the temp address");
       let addressToAdd = PTU.Addresses.Temp;
       for (let [key, val] of Object.entries(addressToAdd)) {
         let field = content.document.getElementById(key);
         field.value = val;
       }
       content.document.querySelector("address-form button:last-of-type").click();
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -13,55 +13,128 @@ add_task(async function test_add_link() 
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let addLink = content.document.querySelector("payment-method-picker .add-link");
     is(addLink.textContent, "Add", "Add link text");
 
     addLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !state.page.guid;
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
     }, "Check add page state");
 
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return Object.keys(state.savedBasicCards).length == 0 &&
+             Object.keys(state.savedAddresses).length == 0;
+    }, "Check no cards or addresses present at beginning of test");
+
     let title = content.document.querySelector("basic-card-form h1");
     is(title.textContent, "Add Credit Card", "Add title should be set");
 
     ok(!state.isPrivate,
        "isPrivate flag is not set when paymentrequest is shown from a non-private session");
-    let persistInput = content.document.querySelector("basic-card-form labelled-checkbox");
-    ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
+    let persistCheckbox = content.document.querySelector("basic-card-form labelled-checkbox");
+    ok(Cu.waiveXrays(persistCheckbox).checked, "persist checkbox should be checked by default");
 
     let year = (new Date()).getFullYear();
     let card = {
       "cc-number": "4111111111111111",
       "cc-name": "J. Smith",
       "cc-exp-month": 11,
       "cc-exp-year": year,
     };
 
     info("filling fields");
     for (let [key, val] of Object.entries(card)) {
       let field = content.document.getElementById(key);
       field.value = val;
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
 
+    let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
+    isnot(billingAddressSelect.getBoundingClientRect().height, 0,
+          "The billing address selector should always be visible");
+    is(billingAddressSelect.childElementCount, 1,
+       "Only one child option should exist by default");
+    is(billingAddressSelect.children[0].value, "",
+       "The only option should be the blank/empty option");
+
+    let addressAddLink = content.document.querySelector(".billingAddressRow .add-link");
+    addressAddLink.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "address-page" && !state["address-page"].guid;
+    }, "Check address page state");
+
+    let addressTitle = content.document.querySelector("address-form h1");
+    is(addressTitle.textContent, "Add Billing Address",
+       "Address on add address page should be correct");
+
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return Object.keys(state.savedBasicCards).length == 0;
+    }, "Check card was not added when clicking the 'add' address button");
+
+    let addressBackButton = content.document.querySelector("address-form .back-button");
+    addressBackButton.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid &&
+             Object.keys(state.savedAddresses).length == 0;
+    }, "Check basic-card page, but card should not be saved and no addresses present");
+
+    is(title.textContent, "Add Credit Card", "Add title should be still be on credit card page");
+
+    for (let [key, val] of Object.entries(card)) {
+      let field = content.document.getElementById(key);
+      is(field.value, val, "Field should still have previous value entered");
+      ok(!field.disabled, "Fields should still be enabled for editing");
+    }
+
+    addressAddLink.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "address-page" && !state["address-page"].guid;
+    }, "Check address page state");
+
+    info("filling address fields");
+    for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
+      let field = content.document.getElementById(key);
+      if (!field) {
+        ok(false, `${key} field not found`);
+      }
+      field.value = val;
+      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+    }
+
+    content.document.querySelector("address-form button:last-of-type").click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check address was added and we're back on basic-card page (add)");
+
+    is(billingAddressSelect.childElementCount, 2,
+       "Two options should exist in the billingAddressSelect");
+    let selectedOption =
+      billingAddressSelect.children[billingAddressSelect.selectedIndex];
+    let selectedAddressGuid = selectedOption.value;
+    is(selectedAddressGuid, Object.values(state.savedAddresses)[0].guid,
+       "The select should have the new address selected");
+
     content.document.querySelector("basic-card-form button:last-of-type").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return Object.keys(state.savedBasicCards).length == 1;
-    }, "Check card was added");
+    }, "Check card was not added again");
 
     let cardGUIDs = Object.keys(state.savedBasicCards);
     is(cardGUIDs.length, 1, "Check there is one card");
     let savedCard = state.savedBasicCards[cardGUIDs[0]];
     card["cc-number"] = "************1111"; // Card should be masked
     for (let [key, val] of Object.entries(card)) {
       is(savedCard[key], val, "Check " + key);
     }
+    is(savedCard.billingAddressGUID, selectedAddressGuid,
+       "The saved card should be associated with the billing address");
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
     }, "Switched back to payment-summary");
   }, args);
 });
 
 add_task(async function test_edit_link() {
@@ -75,19 +148,24 @@ add_task(async function test_edit_link()
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let editLink = content.document.querySelector("payment-method-picker .edit-link");
     is(editLink.textContent, "Edit", "Edit link text");
 
     editLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !!state.page.guid;
+      return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
     }, "Check edit page state");
 
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return Object.keys(state.savedBasicCards).length == 1 &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check card and address present at beginning of test");
+
     let title = content.document.querySelector("basic-card-form h1");
     is(title.textContent, "Edit Credit Card", "Edit title should be set");
 
     let nextYear = (new Date()).getFullYear() + 1;
     let card = {
       // cc-number cannot be modified
       "cc-name": "A. Nonymous",
       "cc-exp-month": 3,
@@ -97,16 +175,100 @@ add_task(async function test_edit_link()
     info("overwriting field values");
     for (let [key, val] of Object.entries(card)) {
       let field = content.document.getElementById(key);
       field.value = val;
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
     ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
 
+    let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
+    is(billingAddressSelect.childElementCount, 2,
+       "Two options should exist in the billingAddressSelect");
+    is(billingAddressSelect.selectedIndex, 1,
+       "The billing address set by the previous test should be selected by default");
+
+    info("Test clicking 'edit' on the empty option first");
+    billingAddressSelect.selectedIndex = 0;
+
+    let addressEditLink = content.document.querySelector(".billingAddressRow .edit-link");
+    addressEditLink.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "address-page" && !state["address-page"].guid;
+    }, "Clicking edit button when the empty option is selected will go to 'add' page (no guid)");
+
+    let addressTitle = content.document.querySelector("address-form h1");
+    is(addressTitle.textContent, "Add Billing Address",
+       "Address on add address page should be correct");
+
+    let addressBackButton = content.document.querySelector("address-form .back-button");
+    addressBackButton.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check we're back at basic-card page with no state changed after adding");
+
+    info("Go back to previously selected option before clicking 'edit' now");
+    billingAddressSelect.selectedIndex = 1;
+
+    let selectedOption = billingAddressSelect.selectedOptions.length &&
+                         billingAddressSelect.selectedOptions[0];
+    ok(selectedOption && selectedOption.value, "select should have a selected option value");
+
+    addressEditLink.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "address-page" && state["address-page"].guid;
+    }, "Check address page state (editing)");
+
+    is(addressTitle.textContent, "Edit Billing Address",
+       "Address on edit address page should be correct");
+
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return Object.keys(state.savedBasicCards).length == 1;
+    }, "Check card was not added again when clicking the 'edit' address button");
+
+    addressBackButton.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check we're back at basic-card page with no state changed after editing");
+
+    for (let [key, val] of Object.entries(card)) {
+      let field = content.document.getElementById(key);
+      is(field.value, val, "Field should still have previous value entered");
+    }
+
+    selectedOption = billingAddressSelect.selectedOptions.length &&
+                     billingAddressSelect.selectedOptions[0];
+    ok(selectedOption && selectedOption.value, "select should have a selected option value");
+
+    addressEditLink.click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "address-page" && state["address-page"].guid;
+    }, "Check address page state (editing)");
+
+    info("filling address fields");
+    for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
+      let field = content.document.getElementById(key);
+      if (!field) {
+        ok(false, `${key} field not found`);
+      }
+      field.value = val + "1";
+      ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+    }
+
+    content.document.querySelector("address-form button:last-of-type").click();
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check still only one address and we're back on basic-card page");
+
+    is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel + "1",
+       "Check that address was edited and saved");
+
     content.document.querySelector("basic-card-form button:last-of-type").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       let cards = Object.entries(state.savedBasicCards);
       return cards.length == 1 &&
              cards[0][1]["cc-name"] == card["cc-name"];
     }, "Check card was edited");
 
@@ -135,47 +297,47 @@ add_task(async function test_private_per
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let addLink = content.document.querySelector("payment-method-picker .add-link");
     is(addLink.textContent, "Add", "Add link text");
 
     addLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !state.page.guid;
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
     },
                                                           "Check add page state");
 
     ok(!state.isPrivate,
        "isPrivate flag is not set when paymentrequest is shown from a non-private session");
-    let persistInput = content.document.querySelector("basic-card-form labelled-checkbox");
-    ok(Cu.waiveXrays(persistInput).checked,
+    let persistCheckbox = content.document.querySelector("basic-card-form labelled-checkbox");
+    ok(Cu.waiveXrays(persistCheckbox).checked,
        "checkbox is checked by default from a non-private session");
   }, args);
 
   let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
   await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() {
     let {
       PaymentTestUtils: PTU,
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let addLink = content.document.querySelector("payment-method-picker .add-link");
     is(addLink.textContent, "Add", "Add link text");
 
     addLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !state.page.guid;
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
     },
                                                           "Check add page state");
 
     ok(state.isPrivate,
        "isPrivate flag is set when paymentrequest is shown from a private session");
-    let persistInput = content.document.querySelector("labelled-checkbox");
-    ok(!Cu.waiveXrays(persistInput).checked,
+    let persistCheckbox = content.document.querySelector("labelled-checkbox");
+    ok(!Cu.waiveXrays(persistCheckbox).checked,
        "checkbox is not checked by default from a private session");
   }, args, {
     browser: privateWin.gBrowser,
   });
   await BrowserTestUtils.closeWindow(privateWin);
 });
 
 add_task(async function test_private_card_adding() {
@@ -190,17 +352,17 @@ add_task(async function test_private_car
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let addLink = content.document.querySelector("payment-method-picker .add-link");
     is(addLink.textContent, "Add", "Add link text");
 
     addLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
-      return state.page.id == "basic-card-page" && !state.page.guid;
+      return state.page.id == "basic-card-page" && !state["basic-card-page"].guid;
     },
                                                           "Check add page state");
 
     let savedCardCount = Object.keys(state.savedBasicCards).length;
     let tempCardCount = Object.keys(state.tempBasicCards).length;
 
     let year = (new Date()).getFullYear();
     let card = {
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -69,17 +69,19 @@ add_task(async function test_initialStat
   form.remove();
 });
 
 add_task(async function test_backButton() {
   let form = new AddressForm();
   form.dataset.backButtonLabel = "Back";
   await form.requestStore.setState({
     page: {
-      id: "test-page",
+      id: "address-page",
+    },
+    "address-page": {
       title: "Sample page title",
     },
   });
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
@@ -133,16 +135,19 @@ add_task(async function test_saveButton(
   isDeeply(details, {
     collectionName: "addresses",
     errorStateChange: {
       page: {
         id: "address-page",
         error: "Generic error",
         onboardingWizard: undefined,
       },
+      "address-page": {
+        title: "Sample page title",
+      },
     },
     guid: undefined,
     messageType: "updateAutofillRecord",
     preserveOldProperties: true,
     record: {
       "given-name": "Jaws",
       "family-name": "Swaj",
       "organization": "Allizom",
@@ -153,16 +158,23 @@ add_task(async function test_saveButton(
       "country": "US",
       "email": "test@example.com",
       "tel": "+15555551212",
     },
     selectedStateKey: undefined,
     successStateChange: {
       page: {
         id: "payment-summary",
+        onboardingWizard: undefined,
+      },
+      "address-page": {
+        title: "Sample page title",
+      },
+      "basic-card-page": {
+        guid: null,
       },
     },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
 add_task(async function test_genericError() {
   let form = new AddressForm();
@@ -188,16 +200,18 @@ add_task(async function test_edit() {
   await asyncElementRendered();
 
   let address1 = deepClone(PTU.Addresses.TimBL);
   address1.guid = "9864798564";
 
   await form.requestStore.setState({
     page: {
       id: "address-page",
+    },
+    "address-page": {
       guid: address1.guid,
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
     },
   });
   await asyncElementRendered();
   checkAddressForm(form, address1);
@@ -205,30 +219,33 @@ add_task(async function test_edit() {
   info("test change to minimal record");
   let minimalAddress = {
     "given-name": address1["given-name"],
     guid: "9gnjdhen46",
   };
   await form.requestStore.setState({
     page: {
       id: "address-page",
+    },
+    "address-page": {
       guid: minimalAddress.guid,
     },
     savedAddresses: {
       [minimalAddress.guid]: deepClone(minimalAddress),
     },
   });
   await asyncElementRendered();
   checkAddressForm(form, minimalAddress);
 
   info("change to no selected address");
   await form.requestStore.setState({
     page: {
       id: "address-page",
     },
+    "address-page": {},
   });
   await asyncElementRendered();
   checkAddressForm(form, {});
 
   form.remove();
 });
 
 add_task(async function test_restricted_address_fields() {
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -64,17 +64,19 @@ add_task(async function test_initialStat
 });
 
 add_task(async function test_backButton() {
   let form = new BasicCardForm();
   form.dataset.backButtonLabel = "Back";
   form.dataset.addBasicCardTitle = "Sample page title 2";
   await form.requestStore.setState({
     page: {
-      id: "test-page",
+      id: "basic-card-page",
+    },
+    "basic-card-page": {
     },
   });
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
   is(form.pageTitle.textContent, "Sample page title 2", "Check title");
@@ -239,16 +241,18 @@ add_task(async function test_edit() {
   info("test year before current");
   let card1 = deepClone(PTU.BasicCards.JohnDoe);
   card1.guid = "9864798564";
   card1["cc-exp-year"] = 2011;
 
   await form.requestStore.setState({
     page: {
       id: "basic-card-page",
+    },
+    "basic-card-page": {
       guid: card1.guid,
     },
     savedBasicCards: {
       [card1.guid]: deepClone(card1),
     },
   });
   await asyncElementRendered();
   checkCCForm(form, card1);
@@ -268,30 +272,35 @@ add_task(async function test_edit() {
   let minimalCard = {
     // no expiration date or name
     "cc-number": "1234567690123",
     guid: "9gnjdhen46",
   };
   await form.requestStore.setState({
     page: {
       id: "basic-card-page",
+    },
+    "basic-card-page": {
       guid: minimalCard.guid,
     },
     savedBasicCards: {
       [minimalCard.guid]: deepClone(minimalCard),
     },
   });
   await asyncElementRendered();
   checkCCForm(form, minimalCard);
 
   info("change to no selected card");
   await form.requestStore.setState({
     page: {
       id: "basic-card-page",
     },
+    "basic-card-page": {
+      guid: null,
+    },
   });
   await asyncElementRendered();
   checkCCForm(form, {});
 
   form.remove();
 });
 </script>
 
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -192,23 +192,26 @@ class EditCreditCard extends EditAutofil
       billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
       billingAddressRow: this._elements.form.querySelector(".billingAddressRow"),
     });
 
     this.loadRecord(record, addresses);
     this.attachEventListeners();
   }
 
-  loadRecord(record, addresses) {
+  loadRecord(record, addresses, preserveFieldValues) {
     // _record must be updated before generateYears and generateBillingAddressOptions are called.
     this._record = record;
     this._addresses = addresses;
-    this.generateYears();
     this.generateBillingAddressOptions();
-    super.loadRecord(record);
+    if (!preserveFieldValues) {
+      // Re-generating the years will reset the selected option.
+      this.generateYears();
+      super.loadRecord(record);
+    }
   }
 
   generateYears() {
     const count = 11;
     const currentYear = new Date().getFullYear();
     const ccExpYear = this._record && this._record["cc-exp-year"];
 
     // Clear the list