Bug 1427960 - Add temporary addresses to state, add toggle to enable saving/not new addresses. r=MattN draft
authorSam Foster <sfoster@mozilla.com>
Thu, 26 Apr 2018 15:05:10 -0700
changeset 797310 28e17a2c018a684fab42b1321b739047b1522884
parent 797309 65e06278238452941cc10d9d0969cc2a982321ad
push id110454
push userbmo:sfoster@mozilla.com
push dateFri, 18 May 2018 21:24:23 +0000
reviewersMattN
bugs1427960
milestone62.0a1
Bug 1427960 - Add temporary addresses to state, add toggle to enable saving/not new addresses. r=MattN * Implement store in paymentDialogWrapper for temporary addresses & creditCards * Add the persist checkbox to the add/edit address form. Defaults to unchecked when adding an address in a private session. * The union of saved and temporary addresses can be retrieved from paymentRequest.getAddresses(). References to state.savedAddresses updated to use this when appropriate * New tests for adding and editing addresses from a private window MozReview-Commit-ID: KyD2BPNFYtZ
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-dialog.js
browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
browser/components/payments/res/paymentRequest.js
browser/components/payments/res/paymentRequest.xhtml
browser/components/payments/test/PaymentTestUtils.jsm
browser/components/payments/test/browser/browser_address_edit.js
browser/components/payments/test/browser/head.js
browser/components/payments/test/mochitest/test_address_picker.html
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -30,35 +30,62 @@ XPCOMUtils.defineLazyGetter(this, "formA
   } catch (ex) {
     storage = null;
     Cu.reportError(ex);
   }
 
   return storage;
 });
 
+class TempCollection {
+  constructor(data = {}) {
+    this._data = data;
+  }
+  get(guid) {
+    return this._data[guid];
+  }
+  update(guid, record, preserveOldProperties) {
+    if (preserveOldProperties) {
+      Object.assign(this._data[guid], record);
+    } else {
+      this._data[guid] = record;
+    }
+    return this._data[guid];
+  }
+  add(record) {
+    let guid = "temp-" + Math.abs(Math.random() * 0xffffffff|0);
+    this._data[guid] = record;
+    return guid;
+  }
+  getAll() {
+    return this._data;
+  }
+}
+
 var paymentDialogWrapper = {
   componentsLoaded: new Map(),
   frame: null,
   mm: null,
   request: null,
+  temporaryStore: null,
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
   ]),
 
   /**
    * Note: This method is async because formAutofillStorage plans to become async.
    *
    * @param {string} guid
    * @returns {object} containing only the requested payer values.
    */
   async _convertProfileAddressToPayerData(guid) {
-    let addressData = formAutofillStorage.addresses.get(guid);
+    let addressData = this.temporaryStore.addresses.get(guid) ||
+                      formAutofillStorage.addresses.get(guid);
     if (!addressData) {
       throw new Error(`Payer address not found: ${guid}`);
     }
 
     let {
       requestPayerName,
       requestPayerEmail,
       requestPayerPhone,
@@ -75,17 +102,18 @@ var paymentDialogWrapper = {
 
   /**
    * Note: This method is async because formAutofillStorage plans to become async.
    *
    * @param {string} guid
    * @returns {nsIPaymentAddress}
    */
   async _convertProfileAddressToPaymentAddress(guid) {
-    let addressData = formAutofillStorage.addresses.get(guid);
+    let addressData = this.temporaryStore.addresses.get(guid) ||
+                      formAutofillStorage.addresses.get(guid);
     if (!addressData) {
       throw new Error(`Shipping address not found: ${guid}`);
     }
 
     let address = this.createPaymentAddress({
       country: addressData.country,
       addressLines: addressData["street-address"].split("\n"),
       region: addressData["address-level1"],
@@ -102,30 +130,35 @@ var paymentDialogWrapper = {
   /**
    * @param {string} guid The GUID of the basic card record from storage.
    * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
    * @throws if the user cancels entering their master password or an error decrypting
    * @returns {nsIBasicCardResponseData?} returns response data or null (if the
    *                                      master password dialog was cancelled);
    */
   async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
-    let cardData = formAutofillStorage.creditCards.get(guid);
+    let cardData = this.temporaryStore.creditCards.get(guid) ||
+                   formAutofillStorage.creditCards.get(guid);
     if (!cardData) {
       throw new Error(`Basic card not found in storage: ${guid}`);
     }
 
     let cardNumber;
-    try {
-      cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
-    } catch (ex) {
-      if (ex.result != Cr.NS_ERROR_ABORT) {
-        throw ex;
+    if (cardData.isTemporary) {
+      cardNumber = cardData["cc-number"];
+    } else {
+      try {
+        cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+      } catch (ex) {
+        if (ex.result != Cr.NS_ERROR_ABORT) {
+          throw ex;
+        }
+        // User canceled master password entry
+        return null;
       }
-      // User canceled master password entry
-      return null;
     }
 
     let billingAddressGUID = cardData.billingAddressGUID;
     let billingAddress;
     try {
       billingAddress = await this._convertProfileAddressToPaymentAddress(billingAddressGUID);
     } catch (ex) {
       // The referenced address may not exist if it was deleted or hasn't yet synced to this profile
@@ -159,16 +192,21 @@ var paymentDialogWrapper = {
     this.frame = frame;
     this.mm = frame.frameLoader.messageManager;
     this.mm.addMessageListener("paymentContentToChrome", this);
     this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
     if (AppConstants.platform == "win") {
       this.frame.setAttribute("selectmenulist", "ContentSelectDropdown-windows");
     }
     this.frame.loadURI("resource://payments/paymentRequest.xhtml");
+
+    this.temporaryStore = {
+      addresses: new TempCollection(),
+      creditCards: new TempCollection(),
+    };
   },
 
   createShowResponse({
     acceptStatus,
     methodName = "",
     methodData = null,
     payerName = "",
     payerEmail = "",
@@ -390,17 +428,19 @@ var paymentDialogWrapper = {
   initializeFrame() {
     let requestSerialized = this._serializeRequest(this.request);
     let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
     let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
 
     this.sendMessageToContent("showPaymentRequest", {
       request: requestSerialized,
       savedAddresses: this.fetchSavedAddresses(),
+      tempAddresses: this.temporaryStore.addresses.getAll(),
       savedBasicCards: this.fetchSavedPaymentCards(),
+      tempBasicCards: this.temporaryStore.creditCards.getAll(),
       isPrivate,
     });
 
     Services.obs.addObserver(this, "formautofill-storage-changed", true);
   },
 
   debugFrame() {
     // To avoid self-XSS-type attacks, ensure that Browser Chrome debugging is enabled.
@@ -493,32 +533,51 @@ var paymentDialogWrapper = {
   },
 
   async onUpdateAutofillRecord(collectionName, record, guid, {
     errorStateChange,
     preserveOldProperties,
     selectedStateKey,
     successStateChange,
   }) {
-    if (collectionName == "creditCards" && !guid) {
+    if (collectionName == "creditCards" && !guid && !record.isTemporary) {
       // We need to be logged in so we can encrypt the credit card number and
       // that's only supported when we're adding a new record.
       // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
       // APIs are refactored to be async functions (bug 1399367).
       if (!await MasterPassword.ensureLoggedIn()) {
         Cu.reportError("User canceled master password entry");
         return;
       }
     }
 
+    let isTemporary = record.isTemporary;
+    let collection = isTemporary ? this.temporaryStore[collectionName] :
+                                   formAutofillStorage[collectionName];
+
     try {
       if (guid) {
-        await formAutofillStorage[collectionName].update(guid, record, preserveOldProperties);
+        await collection.update(guid, record, preserveOldProperties);
       } else {
-        guid = await formAutofillStorage[collectionName].add(record);
+        guid = await collection.add(record);
+      }
+
+      if (isTemporary && collectionName == "addresses") {
+        // there will be no formautofill-storage-changed event to update state
+        // so add updated collection here
+        Object.assign(successStateChange, {
+          tempAddresses: this.temporaryStore.addresses.getAll(),
+        });
+      }
+      if (isTemporary && collectionName == "creditCards") {
+        // there will be no formautofill-storage-changed event to update state
+        // so add updated collection here
+        Object.assign(successStateChange, {
+          tempBasicCards: this.temporaryStore.creditCards.getAll(),
+        });
       }
 
       // Select the new record
       if (selectedStateKey) {
         Object.assign(successStateChange, {
           [selectedStateKey]: guid,
         });
       }
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -1,13 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import LabelledCheckbox from "../components/labelled-checkbox.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <address-form></address-form>
  *
  * XXX: Bug 1446164 - This form isn't localized when used via this custom element
@@ -28,16 +29,18 @@ export default class AddressForm extends
     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);
 
+    this.persistCheckbox = new LabelledCheckbox();
+
     // The markup is shared with form autofill preferences.
     let url = "formautofill/editAddress.xhtml";
     this.promiseReady = this._fetchMarkup(url).then(doc => {
       this.form = doc.getElementById("form");
       return this.form;
     });
   }
 
@@ -63,57 +66,70 @@ export default class AddressForm extends
       this.formHandler = new EditAddress({
         form,
       }, record, {
         DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
         getFormFormat: PaymentDialogUtils.getFormFormat,
         supportedCountries: PaymentDialogUtils.supportedCountries,
       });
 
+      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 record = {};
+    let {
+      page,
+    } = 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;
-
-    let record = {};
-    let {
-      page,
-      savedAddresses,
-    } = state;
+    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);
     } else {
       this.removeAttribute("address-fields");
     }
 
     this.pageTitle.textContent = page.title;
     this.genericErrorText.textContent = page.error;
 
     let editing = !!page.guid;
+    let addresses = paymentRequest.getAddresses(state);
 
     // If an address is selected we want to edit it.
     if (editing) {
-      record = savedAddresses[page.guid];
+      record = addresses[page.guid];
       if (!record) {
         throw new Error("Trying to edit a non-existing address: " + page.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;
     }
 
     this.formHandler.loadRecord(record);
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
@@ -146,18 +162,24 @@ export default class AddressForm extends
       }
     }
   }
 
   saveRecord() {
     let record = this.formHandler.buildFormObject();
     let {
       page,
+      tempAddresses,
       savedBasicCards,
     } = this.requestStore.getState();
+    let editing = !!page.guid;
+
+    if (editing ? (page.guid in tempAddresses) : !this.persistCheckbox.checked) {
+      record.isTemporary = true;
+    }
 
     let state = {
       errorStateChange: {
         page: {
           id: "address-page",
           onboardingWizard: page.onboardingWizard,
           error: this.dataset.errorGenericSave,
         },
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -1,20 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import AddressOption from "../components/address-option.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import RichSelect from "../components/rich-select.js";
+import paymentRequest from "../paymentRequest.js";
 
 /**
  * <address-picker></address-picker>
  * Container around add/edit links and <rich-select> with
- * <address-option> listening to savedAddresses.
+ * <address-option> listening to savedAddresses & tempAddresses.
  */
 
 export default class AddressPicker extends PaymentStateSubscriberMixin(HTMLElement) {
   static get observedAttributes() {
     return ["address-fields"];
   }
 
   constructor() {
@@ -83,26 +84,26 @@ export default class AddressPicker exten
           result[guid] = address;
         }
       }
     }
     return result;
   }
 
   render(state) {
-    let {savedAddresses} = state;
+    let addresses = paymentRequest.getAddresses(state);
     let desiredOptions = [];
     let fieldNames;
     if (this.hasAttribute("address-fields")) {
       let names = this.getAttribute("address-fields").split(/\s+/);
       if (names.length) {
         fieldNames = names;
       }
     }
-    let filteredAddresses = this.filterAddresses(savedAddresses, fieldNames);
+    let filteredAddresses = this.filterAddresses(addresses, fieldNames);
 
     for (let [guid, address] of Object.entries(filteredAddresses)) {
       let optionEl = this.dropdown.getOptionByValue(guid);
       if (!optionEl) {
         optionEl = new AddressOption();
         optionEl.value = guid;
       }
 
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -81,31 +81,36 @@ export default class BasicCardForm exten
       // connected, including the shared form fetched asynchronously.
       super.connectedCallback();
     });
   }
 
   render(state) {
     let {
       page,
-      savedAddresses,
       selectedShippingAddress,
     } = 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;
 
     // 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;
     this.form.querySelector("#cc-number").disabled = editing;
 
     // If a card is selected we want to edit it.
     if (editing) {
@@ -113,25 +118,26 @@ export default class BasicCardForm exten
       record = basicCards[page.guid];
       if (!record) {
         throw new Error("Trying to edit a non-existing card: " + page.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) {
         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, savedAddresses);
+    this.formHandler.loadRecord(record, addresses);
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
@@ -164,58 +170,42 @@ export default class BasicCardForm exten
 
   saveRecord() {
     let record = this.formHandler.buildFormObject();
     let {
       page,
       tempBasicCards,
     } = this.requestStore.getState();
     let editing = !!page.guid;
-    let tempRecord = editing && tempBasicCards[page.guid];
+
+    if (editing ? (page.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"] || "";
     }
 
-    if (!tempRecord && this.persistCheckbox.checked) {
-      log.debug(`BasicCardForm: persisting creditCard record: ${page.guid || "(new)"}`);
-      paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
-        errorStateChange: {
-          page: {
-            id: "basic-card-page",
-            error: this.dataset.errorGenericSave,
-          },
+    paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
+      errorStateChange: {
+        page: {
+          id: "basic-card-page",
+          error: this.dataset.errorGenericSave,
         },
-        preserveOldProperties: true,
-        selectedStateKey: "selectedPaymentCard",
-        successStateChange: {
-          page: {
-            id: "payment-summary",
-          },
-        },
-      });
-    } else {
-      // This record will never get inserted into the store
-      // so we generate a faux-guid for a new record
-      record.guid = page.guid || "temp-" + Math.abs(Math.random() * 0xffffffff|0);
-
-      log.debug(`BasicCardForm: saving temporary record: ${record.guid}`);
-      this.requestStore.setState({
+      },
+      preserveOldProperties: true,
+      selectedStateKey: "selectedPaymentCard",
+      successStateChange: {
         page: {
           id: "payment-summary",
         },
-        selectedPaymentCard: record.guid,
-        tempBasicCards: Object.assign({}, tempBasicCards, {
-        // Mix-in any previous values - equivalent to the store's preserveOldProperties: true,
-          [record.guid]: Object.assign({}, tempRecord, record),
-        }),
-      });
-    }
+      },
+    });
   }
 }
 
 customElements.define("basic-card-form", BasicCardForm);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -124,32 +124,32 @@ export default class PaymentDialog exten
   /**
    * Set some state from the privileged parent process.
    * Other elements that need to set state should use their own `this.requestStore.setState`
    * method provided by the `PaymentStateSubscriberMixin`.
    *
    * @param {object} state - See `PaymentsStore.setState`
    */
   setStateFromParent(state) {
-    let oldSavedAddresses = this.requestStore.getState().savedAddresses;
+    let oldAddresses = paymentRequest.getAddresses(this.requestStore.getState());
     this.requestStore.setState(state);
 
     // Check if any foreign-key constraints were invalidated.
     state = this.requestStore.getState();
     let {
-      savedAddresses,
       selectedPayerAddress,
       selectedPaymentCard,
       selectedShippingAddress,
       selectedShippingOption,
     } = state;
+    let addresses = paymentRequest.getAddresses(state);
     let shippingOptions = state.request.paymentDetails.shippingOptions;
-    let shippingAddress = selectedShippingAddress && savedAddresses[selectedShippingAddress];
+    let shippingAddress = selectedShippingAddress && addresses[selectedShippingAddress];
     let oldShippingAddress = selectedShippingAddress &&
-                             oldSavedAddresses[selectedShippingAddress];
+                             oldAddresses[selectedShippingAddress];
 
     // Ensure `selectedShippingAddress` never refers to a deleted address.
     // We also compare address timestamps to notify about changes
     // made outside the payments UI.
     if (shippingAddress) {
       // invalidate the cached value if the address was modified
       if (oldShippingAddress &&
           shippingAddress.guid == oldShippingAddress.guid &&
@@ -190,19 +190,19 @@ export default class PaymentDialog exten
       this._cachedState.selectedShippingOption = selectedShippingOption;
       this.requestStore.setState({
         selectedShippingOption,
       });
     }
 
     // Ensure `selectedPayerAddress` never refers to a deleted address and refers
     // to an address if one exists.
-    if (!savedAddresses[selectedPayerAddress]) {
+    if (!addresses[selectedPayerAddress]) {
       this.requestStore.setState({
-        selectedPayerAddress: Object.keys(savedAddresses)[0] || null,
+        selectedPayerAddress: Object.keys(addresses)[0] || null,
       });
     }
   }
 
   _renderPayButton(state) {
     this._payButton.disabled = state.changesPrevented;
     switch (state.completionState) {
       case "initial":
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -44,16 +44,17 @@ export let requestStore = new PaymentsSt
   },
   selectedPayerAddress: null,
   selectedPaymentCard: null,
   selectedPaymentCardSecurityCode: null,
   selectedShippingAddress: null,
   selectedShippingOption: null,
   savedAddresses: {},
   savedBasicCards: {},
+  tempAddresses: {},
   tempBasicCards: {},
 });
 
 
 /**
  * A mixin to render UI based upon the requestStore and get updated when that store changes.
  *
  * Attaches `requestStore` to the element to give access to the store.
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -220,16 +220,21 @@ var paymentRequest = {
     return state.request.paymentDetails.totalItem;
   },
 
   onPaymentRequestUnload() {
     // remove listeners that may be used multiple times here
     window.removeEventListener("paymentChromeToContent", this);
   },
 
+  getAddresses(state) {
+    let addresses = Object.assign({}, state.savedAddresses, state.tempAddresses);
+    return addresses;
+  },
+
   getBasicCards(state) {
     let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
     return cards;
   },
 };
 
 paymentRequest.init();
 
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -41,16 +41,17 @@
   <!ENTITY basicCardPage.error.genericSave    "There was an error saving the payment card.">
   <!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">
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>&paymentSummaryTitle;</title>
 
   <!-- chrome: is needed for global.dtd -->
   <meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
 
@@ -88,16 +89,17 @@
 
         <section>
           <div id="error-text"></div>
 
           <div class="shipping-related"
                id="shipping-type-label"
                data-shipping-address-label="&shippingAddressLabel;"
                data-delivery-address-label="&deliveryAddressLabel;"
+               data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
                data-pickup-address-label="&pickupAddressLabel;"><label></label></div>
           <address-picker class="shipping-related"
                           data-add-link-label="&address.addLink.label;"
                           data-edit-link-label="&address.editLink.label;"
                           selected-state-key="selectedShippingAddress"></address-picker>
 
           <div class="shipping-related"><label>&shippingOptionsLabel;</label></div>
           <shipping-option-picker class="shipping-related"></shipping-option-picker>
@@ -143,16 +145,17 @@
                        hidden="hidden"></basic-card-form>
 
       <address-form id="address-page"
                     class="page"
                     data-error-generic-save="&addressPage.error.genericSave;"
                     data-cancel-button-label="&addressPage.cancelButton.label;"
                     data-back-button-label="&addressPage.backButton.label;"
                     data-save-button-label="&addressPage.saveButton.label;"
+                    data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
                     hidden="hidden"></address-form>
     </div>
 
     <div id="disabled-overlay" hidden="hidden">
       <!-- overlay to prevent changes while waiting for a response from the merchant -->
     </div>
   </template>
 
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -359,16 +359,29 @@ var PaymentTestUtils = {
       "street-address": "1 Pommes Frittes Place",
       "address-level2": "Berlin",
       "address-level1": "BE",
       "postal-code": "02138",
       country: "DE",
       tel: "+16172535702",
       email: "timbl@example.org",
     },
+    /* Used as a temporary (not persisted in autofill storage) address in tests */
+    Temp: {
+      "given-name": "Temp",
+      "family-name": "McTempFace",
+      "organization": "Temps Inc.",
+      "street-address": "1a Temporary Ave.",
+      "address-level2": "Temp Town",
+      "address-level1": "CA",
+      "postal-code": "31337",
+      "country": "US",
+      "tel": "+15032541000",
+      "email": "tempie@example.com",
+    },
   },
 
   BasicCards: {
     JohnDoe: {
       "cc-exp-month": 1,
       "cc-exp-year": 9999,
       "cc-name": "John Doe",
       "cc-number": "999999999999",
--- a/browser/components/payments/test/browser/browser_address_edit.js
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -1,12 +1,18 @@
 /* eslint-disable no-shadow */
 
 "use strict";
 
+async function setup() {
+  await setupFormAutofillStorage();
+  // adds 2 addresses and one card
+  await addSampleAddressesAndBasicCard();
+}
+
 add_task(async function test_add_link() {
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let {win, frame} =
       await setupPaymentDialog(browser, {
         methodData: [PTU.MethodData.basicCard],
@@ -49,16 +55,20 @@ add_task(async function test_add_link() 
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && !state.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");
+
       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;
         ok(!field.disabled, `Field #${key} shouldn't be disabled`);
@@ -136,31 +146,37 @@ add_task(async function test_edit_link()
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && !!state.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");
+
       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`);
       }
 
       content.document.querySelector("address-form button:last-of-type").click();
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         let addresses = Object.entries(state.savedAddresses);
         return addresses.length == 1 &&
                addresses[0][1]["given-name"] == address["given-name"];
       }, "Check address was edited");
 
+      // check nothing went into tempAddresses
+      is(Object.keys(state.tempAddresses).length, 0, "tempAddresses collection was untouched");
+
       let addressGUIDs = Object.keys(state.savedAddresses);
       is(addressGUIDs.length, 1, "Check there is still one address");
       let savedAddress = state.savedAddresses[addressGUIDs[0]];
       for (let [key, val] of Object.entries(address)) {
         is(savedAddress[key], val, "Check updated " + key);
       }
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
@@ -215,16 +231,20 @@ add_task(async function test_add_payer_c
 
       state = await PTU.DialogContentUtils.waitForState(content, (state) => {
         return state.page.id == "address-page" && !state.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");
+
       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;
         ok(!field.disabled, `Field #${key} shouldn't be disabled`);
@@ -299,16 +319,19 @@ add_task(async function test_edit_payer_
       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;
       }, "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");
+
       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`);
       }
 
       info("check that non-payer requested fields are hidden");
@@ -345,8 +368,115 @@ add_task(async function test_edit_payer_
     }, EXPECTED_ADDRESS);
 
     info("clicking cancel");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
+
+add_task(async function test_private_persist_addresses() {
+  await formAutofillStorage.addresses._nukeAllRecords();
+  await setup();
+
+  is((await formAutofillStorage.addresses.getAll()).length, 2,
+     "Setup results in 2 stored addresses at start of test");
+
+  await withNewTabInPrivateWindow({
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    info("in new tab w. private window");
+    let {frame} =
+      // setupPaymentDialog from a private window.
+      await setupPaymentDialog(browser, {
+        methodData: [PTU.MethodData.basicCard],
+        details: PTU.Details.twoShippingOptions,
+        options: PTU.Options.requestShippingOption,
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+    info("/setupPaymentDialog");
+
+    await spawnPaymentDialogTask(frame, async () => {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      info("adding link");
+      // click through to add/edit address page
+      let addLink = content.document.querySelector("address-picker a.add-link");
+      addLink.click();
+      info("link clicked");
+
+      let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && !state.page.guid;
+      }, "Check add page state");
+
+      info("on address-page and state.isPrivate: " + state.isPrivate);
+      ok(state.isPrivate,
+         "isPrivate flag is set when paymentrequest is shown in a private session");
+      ok(typeof state.isPrivate,
+         "isPrivate is a flag");
+    });
+
+    info("wait for initialAddresses");
+    let initialAddresses =
+      await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
+    is(initialAddresses.options.length, 2, "Got expected number of pre-filled shipping addresses");
+
+    // // listen for shippingaddresschange event in merchant (private) window
+    info("listen for shippingaddresschange");
+    let shippingAddressChangePromise = ContentTask.spawn(browser, {
+      eventName: "shippingaddresschange",
+    }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+
+    // 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");
+
+      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();
+
+      info("wait until we return to the summary page");
+      let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Return to summary page");
+
+      is(Object.keys(state.tempAddresses).length, 1, "Address added to temporary collection");
+    });
+
+    await shippingAddressChangePromise;
+    info("got shippingaddresschange event");
+
+    // verify address picker has more addresses and new selected
+    info("check shipping addresses");
+    let addresses =
+      await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
+
+    is(addresses.options.length, 3, "Got expected number of shipping addresses");
+
+    info("clicking pay");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+
+    // Add a handler to complete the payment above.
+    info("acknowledging the completion from the merchant page");
+    let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
+    is(result.response.methodName, "basic-card", "Check methodName");
+  });
+  // verify formautofill store doesnt have the new temp addresses
+  is((await formAutofillStorage.addresses.getAll()).length, 2,
+     "Original 2 stored addresses at end of test");
+});
+
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -112,16 +112,25 @@ function withNewDialogFrame(requestId, t
 
   let args = {
     gBrowser,
     url: `chrome://payments/content/paymentDialogWrapper.xul?requestId=${requestId}`,
   };
   return BrowserTestUtils.withNewTab(args, dialogTabTask);
 }
 
+async function withNewTabInPrivateWindow(args = {}, taskFn) {
+  let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
+  let tabArgs = Object.assign(args, {
+    browser: privateWin.gBrowser,
+  });
+  await withMerchantTab(tabArgs, taskFn);
+  await BrowserTestUtils.closeWindow(privateWin);
+}
+
 /**
  * Spawn a content task inside the inner unprivileged frame of a privileged Payment Request dialog.
  *
  * @param {string} requestId
  * @param {Function} contentTaskFn
  * @param {object?} [args = null] for the content task
  * @returns {Promise}
  */
--- a/browser/components/payments/test/mochitest/test_address_picker.html
+++ b/browser/components/payments/test/mochitest/test_address_picker.html
@@ -6,16 +6,17 @@ Test the address-picker component
 <head>
   <meta charset="utf-8">
   <title>Test the address-picker component</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script src="payments_common.js"></script>
   <script src="../../res/vendor/custom-elements.min.js"></script>
+  <script src="../../res/unprivileged-fallbacks.js"></script>
 
   <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
     <address-picker id="picker1"