Bug 1387221 - Connect the shipping address picker with autofill address storage. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 23 Jan 2018 16:44:56 -0800
changeset 455478 3139e03e1335220be27edf94f2c49bc29179e69a
parent 455477 609c91107fc3c71c652c79da0287d85dac8c2548
child 455479 3a416a44273e75c8d57490e8b5e3ee46b7bbfac7
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1387221
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1387221 - Connect the shipping address picker with autofill address storage. r=jaws MozReview-Commit-ID: DVujZR0ksV6
toolkit/components/payments/content/paymentDialog.js
toolkit/components/payments/content/paymentDialog.xhtml
toolkit/components/payments/res/components/address-option.css
toolkit/components/payments/res/components/address-option.js
toolkit/components/payments/res/components/basic-card-option.js
toolkit/components/payments/res/components/rich-select.js
toolkit/components/payments/res/containers/address-picker.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/debugging.html
toolkit/components/payments/res/debugging.js
toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
toolkit/components/payments/res/paymentRequest.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/browser/browser.ini
toolkit/components/payments/test/browser/browser_profile_storage.js
toolkit/components/payments/test/browser/browser_show_dialog.js
toolkit/components/payments/test/browser/head.js
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/test_address_picker.html
toolkit/components/payments/test/mochitest/test_rich_select.html
--- a/toolkit/components/payments/content/paymentDialog.js
+++ b/toolkit/components/payments/content/paymentDialog.js
@@ -8,24 +8,43 @@
  */
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "profileStorage", () => {
+  let profileStorage;
+  try {
+    profileStorage = Cu.import("resource://formautofill/ProfileStorage.jsm", {}).profileStorage;
+    profileStorage.initialize();
+  } catch (ex) {
+    profileStorage = null;
+    Cu.reportError(ex);
+  }
+
+  return profileStorage;
+});
+
 var PaymentDialog = {
   componentsLoaded: new Map(),
   frame: null,
   mm: null,
   request: null,
 
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference,
+  ]),
+
   init(requestId, frame) {
     if (!requestId || typeof(requestId) != "string") {
       throw new Error("Invalid PaymentRequest ID");
     }
     this.request = paymentSrv.getPaymentRequestById(requestId);
 
     if (!this.request) {
       throw new Error(`PaymentRequest not found: ${requestId}`);
@@ -129,16 +148,68 @@ var PaymentDialog = {
     if (!component) {
       component = Cc[componentName];
       this.componentsLoaded.set(componentName, component);
     }
 
     return component.createInstance(componentInterface);
   },
 
+  fetchSavedAddresses() {
+    let savedAddresses = {};
+    for (let address of profileStorage.addresses.getAll()) {
+      savedAddresses[address.guid] = address;
+    }
+    return savedAddresses;
+  },
+
+  fetchSavedPaymentCards() {
+    let savedBasicCards = {};
+    for (let card of profileStorage.creditCards.getAll()) {
+      savedBasicCards[card.guid] = card;
+      // Filter out the encrypted card number since the dialog content is
+      // considered untrusted and runs in a content process.
+      delete card["cc-number-encrypted"];
+    }
+    return savedBasicCards;
+  },
+
+  onAutofillStorageChange() {
+    this.mm.sendAsyncMessage("paymentChromeToContent", {
+      messageType: "updateState",
+      data: {
+        savedAddresses: this.fetchSavedAddresses(),
+        savedBasicCards: this.fetchSavedPaymentCards(),
+      },
+    });
+  },
+
+  initializeFrame() {
+    let requestSerialized = JSON.parse(JSON.stringify(this.request));
+
+    // Manually serialize the nsIPrincipal.
+    let displayHost = this.request.topLevelPrincipal.URI.displayHost;
+    requestSerialized.topLevelPrincipal = {
+      URI: {
+        displayHost,
+      },
+    };
+
+    this.mm.sendAsyncMessage("paymentChromeToContent", {
+      messageType: "showPaymentRequest",
+      data: {
+        request: requestSerialized,
+        savedAddresses: this.fetchSavedAddresses(),
+        savedBasicCards: this.fetchSavedPaymentCards(),
+      },
+    });
+
+    Services.obs.addObserver(this, "formautofill-storage-changed", true);
+  },
+
   onPaymentCancel() {
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
     });
     paymentSrv.respondPayment(showResponse);
     window.close();
   },
 
@@ -156,37 +227,40 @@ var PaymentDialog = {
       payerEmail,
       payerPhone,
       methodName,
       methodData: basicCardData,
     });
     paymentSrv.respondPayment(showResponse);
   },
 
+  /**
+   * @implements {nsIObserver}
+   * @param {nsISupports} subject
+   * @param {string} topic
+   * @param {string} data
+   */
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "formautofill-storage-changed": {
+        if (data == "notifyUsed") {
+          break;
+        }
+        this.onAutofillStorageChange();
+        break;
+      }
+    }
+  },
+
   receiveMessage({data}) {
     let {messageType} = data;
 
     switch (messageType) {
       case "initializeRequest": {
-        let requestSerialized = JSON.parse(JSON.stringify(this.request));
-
-        // Manually serialize the nsIPrincipal.
-        let displayHost = this.request.topLevelPrincipal.URI.displayHost;
-        requestSerialized.topLevelPrincipal = {
-          URI: {
-            displayHost,
-          },
-        };
-
-        this.mm.sendAsyncMessage("paymentChromeToContent", {
-          messageType: "showPaymentRequest",
-          data: {
-            request: requestSerialized,
-          },
-        });
+        this.initializeFrame();
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
       case "pay": {
         this.pay(data);
--- a/toolkit/components/payments/content/paymentDialog.xhtml
+++ b/toolkit/components/payments/content/paymentDialog.xhtml
@@ -9,12 +9,13 @@
   <link rel="stylesheet" href="chrome://payments/content/paymentDialog.css"/>
 </head>
 
 <body>
   <iframe type="content"
           id="paymentRequestFrame"
           mozbrowser="true"
           remote="true"
-          name="paymentRequestFrame"></iframe>
+          height="400"
+          width="700"></iframe>
   <script src="chrome://payments/content/paymentDialog.js"></script>
 </body>
 </html>
--- a/toolkit/components/payments/res/components/address-option.css
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -2,57 +2,57 @@
  * 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/. */
 
 address-option {
   display: grid;
   grid-row-gap: 5px;
   grid-column-gap: 10px;
   grid-template-areas:
-    "recipient  "
-    "addressLine";
+    "name          "
+    "street-address";
 
   border-bottom: 1px solid #ddd;
   background: #fff;
   padding: 5px;
   padding-inline-start: 20px;
   width: 400px;
   font-size: .8em;
 }
 
 rich-select[open] > .rich-select-popup-box > address-option {
   grid-template-areas:
-    "recipient   recipient"
-    "addressLine addressLine"
-    "email       phone      ";
+    "name           name          "
+    "street-address street-address"
+    "email          tel           ";
 }
 
-address-option > .recipient {
-  grid-area: recipient;
+address-option > .name {
+  grid-area: name;
 }
 
-address-option > .addressLine {
-  grid-area: addressLine;
+address-option > .street-address {
+  grid-area: street-address;
 }
 
 address-option > .email {
   grid-area: email;
 }
 
-address-option > .phone {
-  grid-area: phone;
+address-option > .tel {
+  grid-area: tel;
 }
 
-address-option > .recipient,
-address-option > .addressLine,
+address-option > .name,
+address-option > .street-address,
 address-option > .email,
-address-option > .phone {
+address-option > .tel {
   white-space: nowrap;
 }
 
 .rich-select-popup-box > address-option[selected] {
   background-color: #ffa;
 }
 
 rich-select > .rich-select-selected-clone > .email,
-rich-select > .rich-select-selected-clone > .phone {
+rich-select > .rich-select-selected-clone > .tel {
   display: none;
 }
--- a/toolkit/components/payments/res/components/address-option.js
+++ b/toolkit/components/payments/res/components/address-option.js
@@ -1,68 +1,66 @@
 /* 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/. */
 
 /**
  * <rich-select>
- *  <address-option addressLine="1234 Anywhere St"
- *                  city="Some City"
+ *  <address-option guid="98hgvnbmytfc"
+ *                  address-level1="MI"
+ *                  address-level2="Some City"
+ *                  email="foo@example.com"
  *                  country="USA"
- *                  dependentLocality=""
- *                  languageCode="en-US"
- *                  phone=""
- *                  postalCode="90210"
- *                  recipient="Jared Wein"
- *                  region="MI"></address-option>
+ *                  name="Jared Wein"
+ *                  postal-code="90210"
+ *                  street-address="1234 Anywhere St"
+ *                  tel="+1 650 555-5555"></address-option>
  * </rich-select>
+ *
+ * Attribute names follow ProfileStorage.jsm.
  */
 
 /* global ObservedPropertiesMixin, RichOption */
 
 class AddressOption extends ObservedPropertiesMixin(RichOption) {
   static get observedAttributes() {
     return RichOption.observedAttributes.concat([
-      "addressLine",
-      "city",
+      "address-level1",
+      "address-level2",
       "country",
-      "dependentLocality",
       "email",
-      "languageCode",
-      "organization",
-      "phone",
-      "postalCode",
-      "recipient",
-      "region",
-      "sortingCode",
+      "guid",
+      "name",
+      "postal-code",
+      "street-address",
+      "tel",
     ]);
   }
 
   connectedCallback() {
     for (let child of this.children) {
       child.remove();
     }
 
     let fragment = document.createDocumentFragment();
-    RichOption._createElement(fragment, "recipient");
-    RichOption._createElement(fragment, "addressLine");
+    RichOption._createElement(fragment, "name");
+    RichOption._createElement(fragment, "street-address");
     RichOption._createElement(fragment, "email");
-    RichOption._createElement(fragment, "phone");
+    RichOption._createElement(fragment, "tel");
     this.appendChild(fragment);
 
     super.connectedCallback();
   }
 
   render() {
     if (!this.parentNode) {
       return;
     }
 
-    this.querySelector(".recipient").textContent = this.recipient;
-    this.querySelector(".addressLine").textContent =
-      `${this.addressLine} ${this.city} ${this.region} ${this.postalCode} ${this.country}`;
+    this.querySelector(".name").textContent = this.name;
+    this.querySelector(".street-address").textContent = `${this.streetAddress} ` +
+      `${this.addressLevel2} ${this.addressLevel1} ${this.postalCode} ${this.country}`;
     this.querySelector(".email").textContent = this.email;
-    this.querySelector(".phone").textContent = this.phone;
+    this.querySelector(".tel").textContent = this.tel;
   }
 }
 
 customElements.define("address-option", AddressOption);
-
--- a/toolkit/components/payments/res/components/basic-card-option.js
+++ b/toolkit/components/payments/res/components/basic-card-option.js
@@ -9,16 +9,17 @@
  */
 
 /* global ObservedPropertiesMixin, RichOption */
 
 class BasicCardOption extends ObservedPropertiesMixin(RichOption) {
   static get observedAttributes() {
     return RichOption.observedAttributes.concat([
       "expiration",
+      "guid",
       "number",
       "owner",
       "type",
     ]);
   }
 
   connectedCallback() {
     for (let child of this.children) {
--- a/toolkit/components/payments/res/components/rich-select.js
+++ b/toolkit/components/payments/res/components/rich-select.js
@@ -43,16 +43,20 @@ class RichSelect extends ObservedPropert
   get popupBox() {
     return this.querySelector(":scope > .rich-select-popup-box");
   }
 
   get selectedOption() {
     return this.popupBox.querySelector(":scope > [selected]");
   }
 
+  namedItem(name) {
+    return this.popupBox.querySelector(`:scope > [name="${CSS.escape(name)}"]`);
+  }
+
   handleEvent(event) {
     switch (event.type) {
       case "blur": {
         this.onBlur(event);
         break;
       }
       case "click": {
         this.onClick(event);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/containers/address-picker.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+/* global PaymentStateSubscriberMixin, PaymentRequest */
+
+"use strict";
+
+/**
+ * <address-picker></address-picker>
+ * Container around <rich-select> (eventually providing add/edit links) with
+ * <address-option> listening to savedAddresses.
+ */
+
+class AddressPicker extends PaymentStateSubscriberMixin(HTMLElement) {
+  constructor() {
+    super();
+    this.dropdown = document.createElement("rich-select");
+  }
+
+  connectedCallback() {
+    this.appendChild(this.dropdown);
+    super.connectedCallback();
+  }
+
+  render(state) {
+    let {savedAddresses} = state;
+    let desiredOptions = [];
+    for (let [guid, address] of Object.entries(savedAddresses)) {
+      let optionEl = this.dropdown.namedItem(guid);
+      if (!optionEl) {
+        optionEl = document.createElement("address-option");
+        optionEl.name = guid;
+        optionEl.guid = guid;
+      }
+      for (let [key, val] of Object.entries(address)) {
+        optionEl.setAttribute(key, val);
+      }
+      desiredOptions.push(optionEl);
+    }
+    let el = null;
+    while ((el = this.dropdown.popupBox.querySelector(":scope > address-option"))) {
+      el.remove();
+    }
+    this.dropdown.popupBox.append(...desiredOptions);
+  }
+}
+
+customElements.define("address-picker", AddressPicker);
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -49,17 +49,24 @@ class PaymentDialog extends PaymentState
         cardNumber: "9999999999",
         expiryMonth: "01",
         expiryYear: "9999",
         cardSecurityCode: "999",
       },
     });
   }
 
-  setLoadingState(state) {
+  /**
+   * 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) {
     this.requestStore.setState(state);
   }
 
   render(state) {
     let request = state.request;
     this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
 
     let totalItem = request.paymentDetails.totalItem;
--- a/toolkit/components/payments/res/debugging.html
+++ b/toolkit/components/payments/res/debugging.html
@@ -9,11 +9,13 @@
     <script src="debugging.js"></script>
   </head>
   <body>
     <div>
       <button id="refresh">Refresh</button>
       <button id="logState">Log state</button>
       <button id="setRequest1">Set Request 1</button>
       <button id="setRequest2">Set Request 2</button>
+      <button id="setAddresses1">Set Addreses 1</button>
+      <button id="delete1Address">Delete 1 Address</button>
     </div>
   </body>
 </html>
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -104,29 +104,63 @@ let REQUEST_2 = {
     requestPayerName: false,
     requestPayerEmail: false,
     requestPayerPhone: false,
     requestShipping: false,
     shippingType: "shipping",
   },
 };
 
+let ADDRESSES_1 = {
+  "48bnds6854t": {
+    "address-level1": "MI",
+    "address-level2": "Some City",
+    "country": "US",
+    "guid": "48bnds6854t",
+    "name": "Mr. Foo",
+    "postal-code": "90210",
+    "street-address": "123 Sesame Street,\nApt 40",
+    "tel": "+1 519 555-5555",
+  },
+  "68gjdh354j": {
+    "address-level1": "CA",
+    "address-level2": "Mountain View",
+    "country": "US",
+    "guid": "68gjdh354j",
+    "name": "Mrs. Bar",
+    "postal-code": "94041",
+    "street-address": "P.O. Box 123",
+    "tel": "+1 650 555-5555",
+  },
+};
 
 let buttonActions = {
+  delete1Address() {
+    let savedAddresses = Object.assign({}, requestStore.getState().savedAddresses);
+    delete savedAddresses[Object.keys(savedAddresses)[0]];
+    requestStore.setState({
+      savedAddresses,
+    });
+  },
+
   logState() {
     let state = requestStore.getState();
     // eslint-disable-next-line no-console
     console.log(state);
     dump(`${JSON.stringify(state, null, 2)}\n`);
   },
 
   refresh() {
     window.parent.location.reload(true);
   },
 
+  setAddresses1() {
+    requestStore.setState({savedAddresses: ADDRESSES_1});
+  },
+
   setRequest1() {
     requestStore.setState({request: REQUEST_1});
   },
 
   setRequest2() {
     requestStore.setState({request: REQUEST_2});
   },
 };
--- a/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -30,18 +30,18 @@ let requestStore = new PaymentsStore({
     paymentOptions: {
       requestPayerName: false,
       requestPayerEmail: false,
       requestPayerPhone: false,
       requestShipping: false,
       shippingType: "shipping",
     },
   },
-  savedAddresses: [],
-  savedBasicCards: [],
+  savedAddresses: {},
+  savedBasicCards: {},
 });
 
 
 /* exported PaymentStateSubscriberMixin */
 
 /**
  * A mixin to render UI based upon the requestStore and get updated when that store changes.
  *
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -68,29 +68,33 @@ let PaymentRequest = {
   onChromeToContent({detail}) {
     let {messageType} = detail;
 
     switch (messageType) {
       case "showPaymentRequest": {
         this.onShowPaymentRequest(detail);
         break;
       }
+      case "updateState": {
+        document.querySelector("payment-dialog").setStateFromParent(detail);
+        break;
+      }
     }
   },
 
   onPaymentRequestLoad(requestId) {
     window.addEventListener("unload", this, {once: true});
     this.sendMessageToChrome("paymentDialogReady");
   },
 
   async onShowPaymentRequest(detail) {
     // Handle getting called before the DOM is ready.
     await this.domReadyPromise;
 
-    document.querySelector("payment-dialog").setLoadingState({
+    document.querySelector("payment-dialog").setStateFromParent({
       request: detail.request,
       savedAddresses: detail.savedAddresses,
       savedBasicCards: detail.savedBasicCards,
     });
   },
 
   cancel() {
     this.sendMessageToChrome("paymentCancel");
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -17,75 +17,36 @@
   <script src="mixins/ObservedPropertiesMixin.js"></script>
   <script src="mixins/PaymentStateSubscriberMixin.js"></script>
 
   <script src="components/currency-amount.js"></script>
 
   <script src="components/rich-select.js"></script>
   <script src="components/rich-option.js"></script>
   <script src="components/address-option.js"></script>
+  <script src="containers/address-picker.js"></script>
   <script src="containers/payment-dialog.js"></script>
 
   <script src="paymentRequest.js"></script>
 
   <template id="payment-dialog-template">
     <div id="host-name"></div>
 
     <div id="total">
       <h2 class="label"></h2>
       <currency-amount></currency-amount>
     </div>
+
+    <div><label>Shipping Address</label></div>
+    <address-picker>
+    </address-picker>
+
     <div id="controls-container">
       <button id="cancel">Cancel</button>
       <button id="pay">Pay</button>
     </div>
   </template>
 </head>
 <body>
   <iframe id="debugging-console" hidden="hidden" src="debugging.html"></iframe>
-
-  <rich-select>
-    <address-option email="emzembrano92@example.com"
-                    recipient="Emily Zembrano"
-                    addressLine="717 Hyde Street #6"
-                    city="San Francisco"
-                    region="CA"
-                    phone="415 203 0845"
-                    postalCode="94109"
-                    country="USA"></address-option>
-    <address-option email="jenz9382@example.com"
-                    recipient="Jennifer Zembrano"
-                    addressLine="42 Fairydust Lane"
-                    city="Lala Land"
-                    region="HI"
-                    phone="415 439 2827"
-                    postalCode="98765"
-                    country="USA"></address-option>
-    <address-option email="johnz9382@example.com"
-                    recipient="John Zembrano"
-                    addressLine="42 Fairydust Lane"
-                    city="Lala Land"
-                    missinginformation="true"
-                    region="HI"
-                    phone="415 439 2827"
-                    postalCode="98765"
-                    country="USA"></address-option>
-    <address-option email="adbrwodne@example.com"
-                    recipient="Andrew Browne"
-                    addressLine="42 Fairydust Lane"
-                    city="Lala Land"
-                    region="HI"
-                    phone="517 410 0845"
-                    postalCode="98765"
-                    country="USA"></address-option>
-    <address-option email="johnz9382@example.com"
-                    recipient="Jacob Humphrey"
-                    addressLine="1855 Pinecrest Rd"
-                    city="East Lansing"
-                    region="MI"
-                    phone="517 439 2827"
-                    postalCode="48823"
-                    country="USA"></address-option>
-  </rich-select>
-
   <payment-dialog></payment-dialog>
 </body>
 </html>
--- a/toolkit/components/payments/test/browser/browser.ini
+++ b/toolkit/components/payments/test/browser/browser.ini
@@ -2,12 +2,13 @@
 head = head.js
 prefs =
   dom.payments.request.enabled=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
 [browser_host_name.js]
+[browser_profile_storage.js]
 [browser_request_summary.js]
 [browser_show_dialog.js]
 skip-if = os == 'win' && debug # bug 1418385
 [browser_total.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/browser/browser_profile_storage.js
@@ -0,0 +1,192 @@
+"use strict";
+
+// Disable CPOW checks because they have false-positives from use of ContentTask in a helper.
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+const methodData = [PTU.MethodData.basicCard];
+const details = PTU.Details.total60USD;
+
+add_task(async function test_initial_state() {
+  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                          (subject, data) => data == "add");
+  let address1GUID = profileStorage.addresses.add({
+    "given-name": "Timothy",
+    "additional-name": "John",
+    "family-name": "Berners-Lee",
+    organization: "World Wide Web Consortium",
+    "street-address": "32 Vassar Street\nMIT Room 32-G524",
+    "address-level2": "Cambridge",
+    "address-level1": "MA",
+    "postal-code": "02139",
+    country: "US",
+    tel: "+16172535702",
+    email: "timbl@w3.org",
+  });
+  await onChanged;
+
+  onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                      (subject, data) => data == "add");
+  let card1GUID = profileStorage.creditCards.add({
+    "cc-name": "John Doe",
+    "cc-number": "1234567812345678",
+    "cc-exp-month": 4,
+    "cc-exp-year": 2028,
+  });
+  await onChanged;
+
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let dialogReadyPromise = waitForWidgetReady();
+    // start by creating a PaymentRequest, and show it
+    await ContentTask.spawn(browser, {methodData, details}, PTU.ContentTasks.createAndShowRequest);
+
+    // get a reference to the UI dialog and the requestId
+    let win = await getPaymentWidget();
+    let requestId = paymentUISrv.requestIdForWindow(win);
+    ok(requestId, "requestId should be defined");
+    is(win.closed, false, "dialog should not be closed");
+
+    let frame = await getPaymentFrame(win);
+    ok(frame, "Got payment frame");
+    await dialogReadyPromise;
+    info("dialog ready");
+
+    await spawnPaymentDialogTask(frame, async function checkInitialStore({
+      address1GUID,
+      card1GUID,
+    }) {
+      info("checkInitialStore");
+      let contentWin = Components.utils.waiveXrays(content);
+      let {
+        savedAddresses,
+        savedBasicCards,
+      } = contentWin.document.querySelector("payment-dialog").requestStore.getState();
+
+      is(Object.keys(savedAddresses).length, 1, "Initially one savedAddresses");
+      is(savedAddresses[address1GUID].name, "Timothy John Berners-Lee", "Check full name");
+      is(savedAddresses[address1GUID].guid, address1GUID, "Check address guid matches key");
+
+      is(Object.keys(savedBasicCards).length, 1, "Initially one savedBasicCards");
+      is(savedBasicCards[card1GUID]["cc-number"], "************5678", "Check cc-number");
+      is(savedBasicCards[card1GUID].guid, card1GUID, "Check card guid matches key");
+    }, {
+      address1GUID,
+      card1GUID,
+    });
+
+    let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                            (subject, data) => data == "add");
+    info("adding an address");
+    let address2GUID = profileStorage.addresses.add({
+      "given-name": "John",
+      "additional-name": "",
+      "family-name": "Smith",
+      "street-address": "331 E. Evelyn Ave.",
+      "address-level2": "Mountain View",
+      "address-level1": "CA",
+      "postal-code": "94041",
+      country: "US",
+    });
+    await onChanged;
+
+    await spawnPaymentDialogTask(frame, async function checkAdd({
+      address1GUID,
+      address2GUID,
+      card1GUID,
+    }) {
+      info("checkAdd");
+      let contentWin = Components.utils.waiveXrays(content);
+      let {
+        savedAddresses,
+        savedBasicCards,
+      } = contentWin.document.querySelector("payment-dialog").requestStore.getState();
+
+      let addressGUIDs = Object.keys(savedAddresses);
+      is(addressGUIDs.length, 2, "Now two savedAddresses");
+      is(addressGUIDs[0], address1GUID, "Check first address GUID");
+      is(savedAddresses[address1GUID].guid, address1GUID, "Check address 1 guid matches key");
+      is(addressGUIDs[1], address2GUID, "Check second address GUID");
+      is(savedAddresses[address2GUID].guid, address2GUID, "Check address 2 guid matches key");
+
+      is(Object.keys(savedBasicCards).length, 1, "Still one savedBasicCards");
+      is(savedBasicCards[card1GUID].guid, card1GUID, "Check card guid matches key");
+    }, {
+      address1GUID,
+      address2GUID,
+      card1GUID,
+    });
+
+    onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                        (subject, data) => data == "update");
+    info("updating the credit expiration");
+    profileStorage.creditCards.update(card1GUID, {
+      "cc-exp-month": 6,
+      "cc-exp-year": 2029,
+    }, true);
+    await onChanged;
+
+    await spawnPaymentDialogTask(frame, async function checkUpdate({
+      address1GUID,
+      address2GUID,
+      card1GUID,
+    }) {
+      info("checkUpdate");
+      let contentWin = Components.utils.waiveXrays(content);
+      let {
+        savedAddresses,
+        savedBasicCards,
+      } = contentWin.document.querySelector("payment-dialog").requestStore.getState();
+
+      let addressGUIDs = Object.keys(savedAddresses);
+      is(addressGUIDs.length, 2, "Still two savedAddresses");
+      is(addressGUIDs[0], address1GUID, "Check first address GUID");
+      is(savedAddresses[address1GUID].guid, address1GUID, "Check address 1 guid matches key");
+      is(addressGUIDs[1], address2GUID, "Check second address GUID");
+      is(savedAddresses[address2GUID].guid, address2GUID, "Check address 2 guid matches key");
+
+      is(Object.keys(savedBasicCards).length, 1, "Still one savedBasicCards");
+      is(savedBasicCards[card1GUID].guid, card1GUID, "Check card guid matches key");
+      is(savedBasicCards[card1GUID]["cc-exp-month"], 6, "Check expiry month");
+      is(savedBasicCards[card1GUID]["cc-exp-year"], 2029, "Check expiry year");
+    }, {
+      address1GUID,
+      address2GUID,
+      card1GUID,
+    });
+
+    onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                        (subject, data) => data == "remove");
+    info("removing the first address");
+    profileStorage.addresses.remove(address1GUID);
+    await onChanged;
+
+    await spawnPaymentDialogTask(frame, async function checkRemove({
+      address2GUID,
+      card1GUID,
+    }) {
+      info("checkRemove");
+      let contentWin = Components.utils.waiveXrays(content);
+      let {
+        savedAddresses,
+        savedBasicCards,
+      } = contentWin.document.querySelector("payment-dialog").requestStore.getState();
+
+      is(Object.keys(savedAddresses).length, 1, "Now one savedAddresses");
+      is(savedAddresses[address2GUID].name, "John Smith", "Check full name");
+      is(savedAddresses[address2GUID].guid, address2GUID, "Check address guid matches key");
+
+      is(Object.keys(savedBasicCards).length, 1, "Still one savedBasicCards");
+      is(savedBasicCards[card1GUID]["cc-number"], "************5678", "Check cc-number");
+      is(savedBasicCards[card1GUID].guid, card1GUID, "Check card guid matches key");
+    }, {
+      address2GUID,
+      card1GUID,
+    });
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+});
--- a/toolkit/components/payments/test/browser/browser_show_dialog.js
+++ b/toolkit/components/payments/test/browser/browser_show_dialog.js
@@ -37,18 +37,16 @@ add_task(async function test_show_manual
     ok(win, "Got payment widget");
     let requestId = paymentUISrv.requestIdForWindow(win);
     ok(requestId, "requestId should be defined");
     is(win.closed, false, "dialog should not be closed");
 
     // abort the payment request manually
     let frame = await getPaymentFrame(win);
     ok(frame, "Got payment frame");
-    await dialogReadyPromise;
-    info("dialog ready");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
 
 add_task(async function test_show_completePayment() {
   await BrowserTestUtils.withNewTab({
     gBrowser,
@@ -62,18 +60,17 @@ add_task(async function test_show_comple
     let [win] = await Promise.all([getPaymentWidget(), dialogReadyPromise]);
     ok(win, "Got payment widget");
     let requestId = paymentUISrv.requestIdForWindow(win);
     ok(requestId, "requestId should be defined");
     is(win.closed, false, "dialog should not be closed");
 
     let frame = await getPaymentFrame(win);
     ok(frame, "Got payment frame");
-    await dialogReadyPromise;
-    info("dialog ready, clicking pay");
+    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");
 
     let methodDetails = result.methodDetails;
--- a/toolkit/components/payments/test/browser/head.js
+++ b/toolkit/components/payments/test/browser/head.js
@@ -11,16 +11,17 @@
 
 const BLANK_PAGE_PATH = "/browser/toolkit/components/payments/test/browser/blank_page.html";
 const BLANK_PAGE_URL = "https://example.com" + BLANK_PAGE_PATH;
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
                      .getService().wrappedJSObject;
+const {profileStorage} = Cu.import("resource://formautofill/ProfileStorage.jsm", {});
 const {PaymentTestUtils: PTU} = Cu.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
 function getPaymentRequests() {
   let requestsEnum = paymentSrv.enumerate();
   let requests = [];
   while (requestsEnum.hasMoreElements()) {
     requests.push(requestsEnum.getNext().QueryInterface(Ci.nsIPaymentRequest));
   }
@@ -155,12 +156,16 @@ async function spawnInDialogForMerchantT
     let request = requests[0];
     ok(!!request.requestId, "Got a payment request with an ID");
 
     await spawnTaskInNewDialog(request.requestId, dialogTaskFn, taskArgs);
   });
 }
 
 add_task(async function setup_head() {
+  await profileStorage.initialize();
+
   SimpleTest.registerCleanupFunction(function cleanup() {
     paymentSrv.cleanup();
+    profileStorage.addresses._nukeAllRecords();
+    profileStorage.creditCards._nukeAllRecords();
   });
 });
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,22 +1,26 @@
 [DEFAULT]
+prefs =
+   dom.webcomponents.customelements.enabled=false
 support-files =
    ../../../../../testing/modules/sinon-2.3.2.js
    ../../res/PaymentsStore.js
    ../../res/components/currency-amount.js
    ../../res/components/address-option.js
    ../../res/components/address-option.css
    ../../res/components/basic-card-option.js
    ../../res/components/basic-card-option.css
    ../../res/components/rich-option.js
    ../../res/components/rich-select.css
    ../../res/components/rich-select.js
+   ../../res/containers/address-picker.js
    ../../res/mixins/ObservedPropertiesMixin.js
    ../../res/mixins/PaymentStateSubscriberMixin.js
    ../../res/vendor/custom-elements.min.js
    ../../res/vendor/custom-elements.min.js.map
    payments_common.js
 
+[test_address_picker.html]
 [test_currency_amount.html]
 [test_rich_select.html]
 [test_ObservedPropertiesMixin.html]
 [test_PaymentStateSubscriberMixin.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_address_picker.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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/SpawnTask.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="payments_common.js"></script>
+  <script src="custom-elements.min.js"></script>
+  <script src="PaymentsStore.js"></script>
+  <script src="ObservedPropertiesMixin.js"></script>
+  <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="rich-select.js"></script>
+  <script src="address-picker.js"></script>
+  <script src="rich-option.js"></script>
+  <script src="address-option.js"></script>
+  <link rel="stylesheet" type="text/css" href="rich-select.css"/>
+  <link rel="stylesheet" type="text/css" href="address-option.css"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+    <address-picker id="picker1"></address-picker>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the address-picker component **/
+
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/components/address-option.js */
+
+let picker1 = document.getElementById("picker1");
+
+add_task(async function test_empty() {
+  ok(picker1, "Check picker1 exists");
+  let {savedAddresses} = picker1.requestStore.getState();
+  is(Object.keys(savedAddresses).length, 0, "Check empty initial state");
+  is(picker1.dropdown.popupBox.children.length, 0, "Check dropdown is empty");
+});
+
+add_task(async function test_initialSet() {
+  picker1.requestStore.setState({
+    savedAddresses: {
+      "48bnds6854t": {
+        "address-level1": "MI",
+        "address-level2": "Some City",
+        "country": "US",
+        "guid": "48bnds6854t",
+        "name": "Mr. Foo",
+        "postal-code": "90210",
+        "street-address": "123 Sesame Street,\nApt 40",
+        "tel": "+1 519 555-5555",
+      },
+      "68gjdh354j": {
+        "address-level1": "CA",
+        "address-level2": "Mountain View",
+        "country": "US",
+        "guid": "68gjdh354j",
+        "name": "Mrs. Bar",
+        "postal-code": "94041",
+        "street-address": "P.O. Box 123",
+        "tel": "+1 650 555-5555",
+      },
+    },
+  });
+  await asyncElementRendered();
+  let options = picker1.dropdown.popupBox.children;
+  is(options.length, 2, "Check dropdown has both addresses");
+  ok(options[0].textContent.includes("123 Sesame Street"), "Check first address");
+  ok(options[1].textContent.includes("P.O. Box 123"), "Check second address");
+});
+
+add_task(async function test_update() {
+  picker1.requestStore.setState({
+    savedAddresses: {
+      "48bnds6854t": {
+        // Same GUID, different values to trigger an update
+        "address-level1": "MI-edit",
+        "address-level2": "Some City-edit",
+        "country": "CA",
+        "guid": "48bnds6854t",
+        "name": "Mr. Foo-edit",
+        "postal-code": "90210-1234",
+        "street-address": "new-edit",
+        "tel": "+1 650 555-5555",
+      },
+      "68gjdh354j": {
+        "address-level1": "CA",
+        "address-level2": "Mountain View",
+        "country": "US",
+        "guid": "68gjdh354j",
+        "name": "Mrs. Bar",
+        "postal-code": "94041",
+        "street-address": "P.O. Box 123",
+        "tel": "+1 650 555-5555",
+      },
+    },
+  });
+  await asyncElementRendered();
+  let options = picker1.dropdown.popupBox.children;
+  is(options.length, 2, "Check dropdown still has both addresses");
+  ok(options[0].textContent.includes("MI-edit"), "Check updated first address-level1");
+  ok(options[0].textContent.includes("Some City-edit"), "Check updated first address-level2");
+  ok(options[0].textContent.includes("new-edit"), "Check updated first address");
+
+  ok(options[1].textContent.includes("P.O. Box 123"), "Check second address is the same");
+});
+
+add_task(async function test_delete() {
+  picker1.requestStore.setState({
+    savedAddresses: {
+      // 48bnds6854t was deleted
+      "68gjdh354j": {
+        "address-level1": "CA",
+        "address-level2": "Mountain View",
+        "country": "US",
+        "guid": "68gjdh354j",
+        "name": "Mrs. Bar",
+        "postal-code": "94041",
+        "street-address": "P.O. Box 123",
+        "tel": "+1 650 555-5555",
+      },
+    },
+  });
+  await asyncElementRendered();
+  let options = picker1.dropdown.popupBox.children;
+  is(options.length, 1, "Check dropdown has one remaining address");
+  ok(options[0].textContent.includes("P.O. Box 123"), "Check remaining address");
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/payments/test/mochitest/test_rich_select.html
+++ b/toolkit/components/payments/test/mochitest/test_rich_select.html
@@ -21,41 +21,41 @@ Test the rich-select component
   <link rel="stylesheet" type="text/css" href="basic-card-option.css"/>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
     <rich-select id="select1">
       <address-option id="option1"
                       email="emzembrano92@email.com"
-                      recipient="Emily Zembrano"
-                      addressLine="717 Hyde Street #6"
-                      city="San Francisco"
-                      region="CA"
-                      phone="415 203 0845"
-                      postalCode="94109"
+                      name="Emily Zembrano"
+                      street-address="717 Hyde Street #6"
+                      address-level2="San Francisco"
+                      address-level1="CA"
+                      tel="415 203 0845"
+                      postal-code="94109"
                       country="USA"></address-option>
       <address-option id="option2"
                       email="jenz9382@email.com"
-                      recipient="Jennifer Zembrano"
-                      addressLine="42 Fairydust Lane"
-                      city="Lala Land"
-                      region="HI"
-                      phone="415 439 2827"
-                      postalCode="98765"
+                      name="Jennifer Zembrano"
+                      street-address="42 Fairydust Lane"
+                      address-level2="Lala Land"
+                      address-level1="HI"
+                      tel="415 439 2827"
+                      postal-code="98765"
                       country="USA"></address-option>
       <address-option id="option3"
                       email="johnz9382@email.com"
-                      recipient="John Zembrano"
-                      addressLine="42 Fairydust Lane"
-                      city="Lala Land"
+                      name="John Zembrano"
+                      street-address="42 Fairydust Lane"
+                      address-level2="Lala Land"
                       missinginformation="true"
-                      region="HI"
-                      phone="415 439 2827"
-                      postalCode="98765"
+                      address-level1="HI"
+                      tel="415 439 2827"
+                      postal-code="98765"
                       country="USA"></address-option>
     </rich-select>
 
     <rich-select id="select2">
       <basic-card-option owner="Jared Wein"
                          expiration="01/1970"
                          number="4024007197293599"
                          type="Visa"></basic-card-option>
@@ -97,22 +97,22 @@ function is_visible(element, message) {
 function is_hidden(element, message) {
   ok(isHidden(element), message);
 }
 
 function dispatchKeyDown(key, keyCode) {
   select1.dispatchEvent(new KeyboardEvent("keydown", {key, keyCode}));
 }
 
-add_task(async function test_addressLine_combines_address_city_region_postalCode_country() {
+add_task(async function test_streetAddress_combines_street_level2_level1_postalCode_country() {
   ok(option1, "option1 exists");
-  let addressLine = option1.querySelector(".addressLine");
+  let streetAddress = option1.querySelector(".street-address");
   /* eslint-disable max-len */
-  is(addressLine.textContent,
-     `${option1.addressLine} ${option1.city} ${option1.region} ${option1.postalCode} ${option1.country}`);
+  is(streetAddress.textContent,
+     `${option1.streetAddress} ${option1.addressLevel2} ${option1.addressLevel1} ${option1.postalCode} ${option1.country}`);
   /* eslint-enable max-len */
 });
 
 add_task(async function test_no_option_selected_first_displayed() {
   ok(select1, "select1 exists");
 
   await asyncElementRendered();
 
@@ -120,18 +120,18 @@ add_task(async function test_no_option_s
   is_hidden(option2, "option 2 should be hidden when popup is not open");
   is_hidden(option3, "option 3 should be hidden when popup is not open");
   ok(option1.selected, "option 1 should be selected");
   ok(option1.hasAttribute("selected"), "option 1 should have selected attribute");
   let selectedClone = get_selected_clone();
   is_visible(selectedClone, "The selected clone should be visible at all times");
   is(selectedClone.getAttribute("email"), option1.getAttribute("email"),
      "The selected clone email should be equivalent to the selected option 1");
-  is(selectedClone.getAttribute("recipient"), option1.getAttribute("recipient"),
-     "The selected clone recipient should be equivalent to the selected option 1");
+  is(selectedClone.getAttribute("name"), option1.getAttribute("name"),
+     "The selected clone name should be equivalent to the selected option 1");
 });
 
 add_task(async function test_clicking_on_select_shows_all_options() {
   ok(select1, "select1 exists");
   ok(!select1.open, "select is not open by default");
   ok(option1.selected, "option 1 should be selected by default");
 
   select1.click();
@@ -152,18 +152,18 @@ add_task(async function test_clicking_on
   is_hidden(option3, "option 3 is hidden when select is closed");
 
   await asyncElementRendered();
 
   let selectedClone = get_selected_clone();
   is_visible(selectedClone, "The selected clone should be visible at all times");
   is(selectedClone.getAttribute("email"), option2.getAttribute("email"),
      "The selected clone email should be equivalent to the selected option 2");
-  is(selectedClone.getAttribute("recipient"), option2.getAttribute("recipient"),
-     "The selected clone recipient should be equivalent to the selected option 2");
+  is(selectedClone.getAttribute("name"), option2.getAttribute("name"),
+     "The selected clone name should be equivalent to the selected option 2");
 });
 
 add_task(async function test_changing_option_selected_affects_other_options() {
   ok(option2.selected, "Option 2 should be selected from prior test");
 
   option1.selected = true;
   ok(!option2.selected, "Option 2 should no longer be selected after making option 1 selected");
   ok(option1.hasAttribute("selected"), "Option 1 should now have selected attribute");