Bug 1387221 - Connect the shipping address picker with autofill address storage. r=jaws draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 23 Jan 2018 16:44:56 -0800
changeset 723879 8c17a01a005afd117d112acc7a224eaa7da32d0c
parent 721208 b2cb61e83ac50115a28f04aaa8a32d4db90aad23
child 746983 7734ab46a32e2e5f5035fa5d7f8522fefe7aea52
push id96566
push usermozilla@noorenberghe.ca
push dateWed, 24 Jan 2018 00:46:18 +0000
reviewersjaws
bugs1387221
milestone59.0a1
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
@@ -35,16 +35,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 dispatchKeyPress(key, keyCode) {
   select1.dispatchEvent(new KeyboardEvent("keypress", {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");