Bug 1410321 - Prototype: Custom Elements with Redux-like global state for the payment request dialog draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 07 Nov 2017 13:23:56 -0800
changeset 694521 56a3f14f76dd1e14faafe61d3785ddc2e29482d4
parent 694520 ad406f41e8e46fbbb20c80f786b39ae3d11db41a
child 695315 78f155675df959f370dbe6d2d4cd0143331ecb5d
push id88149
push usermozilla@noorenberghe.ca
push dateTue, 07 Nov 2017 21:27:58 +0000
bugs1410321
milestone58.0a1
Bug 1410321 - Prototype: Custom Elements with Redux-like global state for the payment request dialog You can simply load toolkit/components/payments/res/paymentRequest.html in the browser and set state using the debugging buttons to play around. MozReview-Commit-ID: LWFXlJOyUQw
.eslintignore
toolkit/components/payments/content/paymentDialog.js
toolkit/components/payments/content/paymentDialog.xhtml
toolkit/components/payments/jar.mn
toolkit/components/payments/paymentUIService.js
toolkit/components/payments/res/components.js
toolkit/components/payments/res/containers.js
toolkit/components/payments/res/debugging.html
toolkit/components/payments/res/debugging.js
toolkit/components/payments/res/paymentRequest.css
toolkit/components/payments/res/paymentRequest.html
toolkit/components/payments/res/paymentRequest.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/res/paymentsStore.js
toolkit/components/payments/res/vendor/custom-elements.min.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -347,16 +347,17 @@ toolkit/components/workerloader/tests/mo
 # Tests old non-star function generators
 toolkit/modules/tests/xpcshell/test_task.js
 
 # External code:
 toolkit/components/microformats/test/**
 toolkit/components/microformats/microformat-shiv.js
 toolkit/components/reader/Readability.js
 toolkit/components/reader/JSDOMParser.js
+toolkit/components/payments/res/vendor/*
 
 # Uses preprocessing
 toolkit/content/widgets/wizard.xml
 toolkit/components/osfile/osfile.jsm
 toolkit/components/urlformatter/nsURLFormatter.js
 toolkit/modules/AppConstants.jsm
 toolkit/mozapps/downloads/nsHelperAppDlg.js
 toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
--- a/toolkit/components/payments/content/paymentDialog.js
+++ b/toolkit/components/payments/content/paymentDialog.js
@@ -40,34 +40,71 @@ let PaymentDialog = {
         maxLogLevelPref: "dom.payments.loglevel",
         prefix: `paymentDialog (${requestId})`,
       });
     });
 
     this.log.debug("init:", this.request);
     this.mm.addMessageListener("paymentContentToChrome", this);
     this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
-    this.frame.src = "resource://payments/paymentRequest.xhtml";
+    this.frame.src = "resource://payments/paymentRequest.html";
   },
 
-  createShowResponse({acceptStatus, methodName = "", data = null,
+  createShowResponse({acceptStatus, paymentMethodName = "", paymentMethodData = null,
                       payerName = "", payerEmail = "", payerPhone = ""}) {
     let showResponse = this.createComponentInstance(Ci.nsIPaymentShowActionResponse);
-    let methodData = this.createComponentInstance(Ci.nsIGeneralResponseData);
 
     showResponse.init(this.request.requestId,
                       acceptStatus,
-                      methodName,
-                      methodData,
+                      paymentMethodName,
+                      paymentMethodData,
                       payerName,
                       payerEmail,
                       payerPhone);
     return showResponse;
   },
 
+  createBasicCardResponseData({cardNumber,
+                               cardholderName,
+                               cardSecurityCode,
+                               expiryMonth,
+                               expiryYear,
+                               billingAddress = null}) {
+    const basicCardResponseData = Cc["@mozilla.org/dom/payments/basiccard-response-data;1"]
+                                  .createInstance(Ci.nsIBasicCardResponseData);
+    basicCardResponseData.initData(cardholderName,
+                                   cardNumber,
+                                   expiryMonth,
+                                   expiryYear,
+                                   cardSecurityCode,
+                                   billingAddress);
+    return basicCardResponseData;
+  },
+
+  createPaymentAddress() {
+    const billingAddress = Cc["@mozilla.org/dom/payments/payment-address;1"]
+          .createInstance(Ci.nsIPaymentAddress);
+    const addressLine = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    const address = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+    address.data = "Easton Ave";
+    addressLine.appendElement(address);
+    billingAddress.init("USA",              // country
+                        addressLine,        // address line
+                        "CA",               // region
+                        "San Bruno",        // city
+                        "",                 // dependent locality
+                        "94066",            // postal code
+                        "123456",           // sorting code
+                        "en",               // language code
+                        "",                 // organization
+                        "Bill A. Pacheco",  // recipient
+                        "+14344413879"); // phone
+    return billingAddress;
+  },
+
   createComponentInstance(componentInterface) {
     let componentName;
     switch (componentInterface) {
       case Ci.nsIPaymentShowActionResponse: {
         componentName = "@mozilla.org/dom/payments/payment-show-action-response;1";
         break;
       }
       case Ci.nsIGeneralResponseData: {
@@ -82,22 +119,36 @@ let PaymentDialog = {
       this.componentsLoaded.set(componentName, component);
     }
 
     return component.createInstance(componentInterface);
   },
 
   onPaymentCancel() {
     const showResponse = this.createShowResponse({
-      acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
+      acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, // abort?
     });
     paymentSrv.respondPayment(showResponse);
     window.close();
   },
 
+  pay(details) {
+    let {payerName, payerEmail, payerPhone, paymentMethodName, paymentMethodData} = details;
+    let basicCardData = this.createBasicCardResponseData(paymentMethodData);
+    const showResponse = this.createShowResponse({
+      acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED,
+      payerName,
+      payerEmail,
+      payerPhone,
+      paymentMethodName,
+      paymentMethodData: basicCardData,
+    });
+    paymentSrv.respondPayment(showResponse);
+  },
+
   receiveMessage({data}) {
     let {messageType} = data;
 
     switch (messageType) {
       case "initializeRequest": {
         let requestSerialized = JSON.parse(JSON.stringify(this.request));
 
         // Manually serialize the nsIPrincipal.
@@ -107,23 +158,56 @@ let PaymentDialog = {
             displayHost,
           },
         };
 
         this.mm.sendAsyncMessage("paymentChromeToContent", {
           messageType: "showPaymentRequest",
           data: {
             request: requestSerialized,
+            savedAddresses: [ // TODO: Object with guid key for lookup?
+              {
+                guid: "89twgn34y4g",
+                name: "Mr. Baz",
+              },
+              {
+                guid: "3feg745sed",
+                name: "Mrs. Smith",
+              },
+            ],
+            savedBasicCards: [ // TODO
+              {
+                "cc-exp-month": 3,
+                "cc-number-encrypted": "_decrypt_me_",
+                "cc-number": "************5678",
+                "guid": "587gdh45",
+                "version": 1,
+                "timeCreated": 1506397876390,
+                "timeLastModified": 1506397909614,
+                "timeLastUsed": 0,
+                "timesUsed": 0,
+                "cc-name": "Jane Doe",
+                "cc-exp-year": 2021,
+                "cc-given-name": "Jane",
+                "cc-additional-name": "",
+                "cc-family-name": "Doe",
+                "cc-exp": "2021-03",
+              },
+            ],
           },
         });
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
+      case "pay": {
+        this.pay(data);
+        break;
+      }
     }
   },
 };
 
 let frame = document.getElementById("paymentRequestFrame");
 let requestId = (new URLSearchParams(window.location.search)).get("requestId");
 PaymentDialog.init(requestId, frame);
--- a/toolkit/components/payments/content/paymentDialog.xhtml
+++ b/toolkit/components/payments/content/paymentDialog.xhtml
@@ -7,14 +7,16 @@
 <head>
   <title></title>
   <link rel="stylesheet" href="chrome://payments/content/paymentDialog.css"/>
 </head>
 
 <body>
   <iframe type="content"
           id="paymentRequestFrame"
+          width="600"
+          height="400"
           mozbrowser="true"
           remote="true"
           name="paymentRequestFrame"></iframe>
   <script src="chrome://payments/content/paymentDialog.js"></script>
 </body>
 </html>
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -4,9 +4,15 @@
 
 toolkit.jar:
 %   content payments %content/payments/
     content/payments/paymentDialog.css                (content/paymentDialog.css)
     content/payments/paymentDialog.js                 (content/paymentDialog.js)
     content/payments/paymentDialogFrameScript.js      (content/paymentDialogFrameScript.js)
     content/payments/paymentDialog.xhtml              (content/paymentDialog.xhtml)
 %   resource payments %res/payments/
-    res/payments (res/paymentRequest.*)
+    res/payments                                      (res/paymentRequest.*)
+    res/payments/components.js                        (res/components.js)
+    res/payments/containers.js                        (res/containers.js)
+    res/payments/debugging.html                       (res/debugging.html)
+    res/payments/debugging.js                         (res/debugging.js)
+    res/payments/paymentsStore.js                     (res/paymentsStore.js)
+    res/payments/vendor                               (res/vendor/*.js)
--- a/toolkit/components/payments/paymentUIService.js
+++ b/toolkit/components/payments/paymentUIService.js
@@ -20,16 +20,52 @@ const { classes: Cc, interfaces: Ci, res
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "paymentSrv",
                                    "@mozilla.org/dom/payments/payment-request-service;1",
                                    "nsIPaymentRequestService");
 
+XPCOMUtils.defineLazyGetter(this, "profileStorage", () => {
+  let _profileStorage;
+
+  try {
+    _profileStorage = Cu.import("resource://formautofill/ProfileStorage.jsm", {}).profileStorage;
+    _profileStorage.initialize();
+  } catch (e) {
+    _profileStorage = null;
+  }
+
+  return _profileStorage;
+});
+
+/*
+  // get storage info, and keep only the fields we need
+  let profiles = profileStorage.addresses.getAll().map(profile => {
+    return {
+      address: profile["street-address"],
+      addressLines: [
+        profile["address-line1"],
+        profile["address-line2"],
+        profile["address-line3"],
+      ],
+      city: profile["address-level2"],
+      country: profile["country-name"],
+      email: profile.email,
+      name: profile.name,
+      organization: profile.organization,
+      phone: profile.tel,
+      postalCode: profile["postal-code"],
+      region: profile["address-level1"],
+      // dependentLocality && sortingCode && languageCode ??
+    };
+  });
+*/
+
 function PaymentUIService() {
   this.wrappedJSObject = this;
   XPCOMUtils.defineLazyGetter(this, "log", () => {
     let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
     return new ConsoleAPI({
       maxLogLevelPref: "dom.payments.loglevel",
       prefix: "Payment UI Service",
     });
@@ -53,50 +89,62 @@ PaymentUIService.prototype = {
                             "modal,dialog,centerscreen,resizable=no");
   },
 
   abortPayment(requestId) {
     this.log.debug("abortPayment:", requestId);
     let abortResponse = Cc["@mozilla.org/dom/payments/payment-abort-action-response;1"]
                           .createInstance(Ci.nsIPaymentAbortActionResponse);
 
-    let enu = Services.wm.getEnumerator(null);
-    let win;
-    while ((win = enu.getNext())) {
-      if (win.name == `${this.REQUEST_ID_PREFIX}${requestId}`) {
-        this.log.debug(`closing: ${win.name}`);
-        win.close();
-        break;
-      }
-    }
+    let found = this.closeDialog(requestId);
 
     // if `win` is falsy, then we haven't found the dialog, so the abort fails
     // otherwise, the abort is successful
-    let response = win ?
+    let response = found ?
       Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED :
       Ci.nsIPaymentActionResponse.ABORT_FAILED;
 
     abortResponse.init(requestId, response);
     paymentSrv.respondPayment(abortResponse);
   },
 
   completePayment(requestId) {
     this.log.debug("completePayment:", requestId);
     let completeResponse = Cc["@mozilla.org/dom/payments/payment-complete-action-response;1"]
                              .createInstance(Ci.nsIPaymentCompleteActionResponse);
-    completeResponse.init(requestId, Ci.nsIPaymentActionResponse.COMPLTETE_SUCCEEDED);
+    completeResponse.init(requestId, Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED);
     paymentSrv.respondPayment(completeResponse.QueryInterface(Ci.nsIPaymentActionResponse));
+
+    this.closeDialog(requestId); // TODO
   },
 
   updatePayment(requestId) {
     this.log.debug("updatePayment:", requestId);
   },
 
   // other helper methods
 
+  /**
+   * @param {string} requestId - Payment Request ID of the dialog to close.
+   * @returns {boolean} whether the specified dialog was closed.
+   */
+  closeDialog(requestId) {
+    let enu = Services.wm.getEnumerator(null);
+    let win;
+    while ((win = enu.getNext())) {
+      if (win.name == `${this.REQUEST_ID_PREFIX}${requestId}`) {
+        this.log.debug(`closing: ${win.name}`);
+        win.close();
+        return true;
+      }
+    }
+
+    return false;
+  },
+
   requestIdForWindow(window) {
     let windowName = window.name;
 
     return windowName.startsWith(this.REQUEST_ID_PREFIX) ?
       windowName.replace(this.REQUEST_ID_PREFIX, "") : // returns suffix, which is the requestId
       null;
   },
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+/**
+ * State-less presentational components.
+ *
+ * Do not connect to state.
+ */
+
+class PaymentElement extends HTMLElement {
+  constructor() {
+    super();
+
+    console.log("constructing:", this.constructor.name);
+
+    // Reflect property changes for `observedAttributes` to attributes.
+    for (let name of (this.constructor.observedAttributes || [])) {
+      if (name in this) {
+        // Don't overwrite existing properties.
+        continue;
+      }
+      Object.defineProperty(this, name.replace(/-([a-z])/g, ($0, $1) => $1.toUpperCase()), {
+        configurable: true,
+        get() {
+          return this.getAttribute(name);
+        },
+        set(value) {
+          if (value === null || value === undefined) {
+            this.removeAttribute(name);
+          } else {
+            this.setAttribute(name, value);
+          }
+        },
+      });
+    }
+  }
+
+  attributeChangedCallback(attr, oldValue, newValue) {
+    this.render();
+  }
+}
+
+class PaymentItem extends PaymentElement {
+  static get observedAttributes() {
+    return ["label", "currency", "value"];
+  }
+
+  render() {
+    if (!this._labelEl && !this._priceEl) {
+      this._labelEl = document.createElement("dt");
+      this._priceEl = document.createElement("dd");
+      this._currencyEl = document.createElement("currency-amount");
+      this._priceEl.appendChild(this._currencyEl);
+      this.appendChild(this._labelEl);
+      this.appendChild(this._priceEl);
+    }
+
+    this._labelEl.textContent = this.getAttribute("label");
+    this._currencyEl.currency = this.getAttribute("currency");
+    this._currencyEl.value = this.getAttribute("value");
+  }
+}
+
+customElements.define("payment-item", PaymentItem);
+
+class CurrencyAmount extends PaymentElement {
+  static get observedAttributes() {
+    return ["currency", "value"];
+  }
+
+  render() {
+    if (this.value && this.currency) {
+      const formatter = new Intl.NumberFormat(navigator.languages, {
+        style: "currency",
+        currency: this.currency,
+        currencyDisplay: "symbol",
+      });
+      this.textContent = formatter.format(this.value);
+    } else {
+      this.textContent = "";
+    }
+  }
+}
+customElements.define("currency-amount", CurrencyAmount);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/containers.js
@@ -0,0 +1,298 @@
+/* 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/. */
+
+let requestStore = new PaymentsStore({
+  savedAddresses: [],
+  savedBasicCards: [],
+  request: {
+    tabId: null,
+    topLevelPrincipal: {URI: {displayHost: null}},
+    requestId: null,
+    paymentMethods: [],
+    paymentDetails: {
+      id: null,
+      totalItem: {label: null, amount: {currency: null, value: null}},
+      displayItems: [],
+      shippingOptions: [],
+      modifiers: null,
+      error: "",
+    },
+    paymentOptions: {
+      requestPayerName: false,
+      requestPayerEmail: false,
+      requestPayerPhone: false,
+      requestShipping: false,
+      shippingType: "shipping",
+    },
+  },
+});
+
+/**
+ * Container components.
+ *
+ * Containers connected to state and pass state to inner components.
+ *
+ * They generally don't use attributes as they get all their info from global state.
+ */
+
+// TODO: use mixins instead of inheritance
+
+class PaymentStateSubscriber extends HTMLElement {
+  connectedCallback() {
+    requestStore.subscribe(this);
+    console.log("subscribed from", this);
+  }
+
+  disconnectedCallback() {
+    requestStore.unsubscribe(this);
+  }
+
+  shouldComponentUpdate(nextState) { // TODO
+    // Do a shallow prop comparison to track whether there were any changes.
+    for (let key in nextState) {
+      if (nextState[key] !== this.state[key]) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  stateChangeCallback(state) {
+    this.render(state);
+  }
+
+  /**
+   * Forward events dispatched on the element to on* methods.
+   */
+  handleEvent(event) {
+    let handlerName = "on" + event.type;
+    if (typeof(this[handlerName]) != "function") {
+      return;
+    }
+
+    this[handlerName](event);
+  }
+}
+
+class PaymentItems extends PaymentStateSubscriber {
+  constructor() {
+    super();
+    this._details = document.createElement("details");
+    this._summary = document.createElement("summary");
+    this._summary.textContent = "All Items"; // TODO: l10n
+    this._list = document.createElement("dl");
+  }
+
+  render(state) {
+    this._list.innerHTML = "";
+    console.log("rendering", this);
+    let displayItems = state.request.paymentDetails.displayItems;
+    for (let item of displayItems) {
+      let pi = new PaymentItem();
+      pi.label = item.label;
+      pi.value = item.amount.value;
+      pi.currency = item.amount.currency;
+      this._list.appendChild(pi);
+    }
+
+    if (displayItems.length == 0) {
+      let pi = new PaymentItem();
+      pi.label = "Subtotal"; // l10n
+      pi.value = ""; // TODO: what goes here?
+      this._list.appendChild(pi);
+    }
+
+    this._summary.hidden = displayItems.length <= 1;
+    // TODO: could handle already toggle opened or closed by user
+    // Hopefully we'll move this to its own page anyways
+    this._details.open = displayItems.length <= 1;
+
+    if (!this._details.parentNode) {
+      this._details.appendChild(this._summary);
+      this._details.appendChild(this._list);
+      this.appendChild(this._details);
+    }
+  }
+}
+
+customElements.define("payment-items", PaymentItems);
+
+class PaymentDropdown extends PaymentStateSubscriber {
+  constructor() {
+    super();
+    this._selector = document.createElement("select");
+  }
+
+  get selectedStateKey() {
+    return this.getAttribute("selected-state-key");
+  }
+
+  connectedCallback() {
+    this._selector.name = this.selectedStateKey;
+    this._selector.addEventListener("change", this);
+    this.appendChild(this._selector);
+    super.connectedCallback();
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    this._selector.removeEventListener("change", this);
+  }
+
+  onchange(evt) {
+    requestStore.setState({
+      [this.selectedStateKey]: evt.target.value,
+    });
+  }
+}
+
+class AddressPicker extends PaymentDropdown {
+  render(state) {
+    console.log("rendering", this.id);
+    this._selector.innerHTML = "";
+    let frag = new DocumentFragment();
+    for (let address of state.savedAddresses) {
+      let isSelected = state[this.selectedStateKey] == address.guid;
+      frag.appendChild(new Option(address.name,
+                                  address.guid,
+                                  isSelected,
+                                  isSelected));
+    }
+    this._selector.appendChild(frag);
+  }
+}
+
+customElements.define("address-picker", AddressPicker);
+
+class BasicCardPicker extends PaymentDropdown {
+  constructor() {
+    super();
+    this._securityCodeField = document.createElement("input");
+    this._securityCodeField.placeholder = "CVV"; // TODO: l10n
+    this._securityCodeField.size = 3;
+    this._securityCodeField.autocomplete = "off";
+    // this._securityCodeField.type = "password";
+    this._securityCodeField.name = "cardSecurityCode";
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    this.appendChild(this._securityCodeField);
+  }
+
+  render(state) {
+    this._selector.innerHTML = "";
+    let frag = new DocumentFragment();
+    for (let card of state.savedBasicCards) {
+      let isSelected = state[this.selectedStateKey] == card.guid;
+      frag.appendChild(new Option(`${card["cc-number"]}, ${card["cc-name"]}`,
+                                  card.guid,
+                                  isSelected,
+                                  isSelected));
+    }
+    this._selector.appendChild(frag);
+  }
+}
+
+customElements.define("basiccard-picker", BasicCardPicker);
+
+class ShippingOptionPicker extends PaymentDropdown {
+  render(state) {
+    this._selector.innerHTML = "";
+    let shippingOptions = state.request.paymentDetails.shippingOptions;
+    let merchantSelected = (shippingOptions.find(opt => opt.selected) || {}).id;
+    let selected = state[this.selectedStateKey] || merchantSelected;
+
+    let frag = new DocumentFragment();
+    for (let opt of shippingOptions) {
+      let isSelected = selected === opt.id;
+      frag.appendChild(new Option(opt.label,
+                                  opt.id,
+                                  isSelected,
+                                  isSelected));
+    }
+    this._selector.appendChild(frag);
+  }
+}
+
+customElements.define("shippingoption-picker", ShippingOptionPicker);
+
+class PaymentDialog extends PaymentStateSubscriber {
+  constructor() {
+    super();
+    this._template = document.getElementById("payment-dialog-template");
+    this._requestStore = requestStore;
+  }
+
+  connectedCallback() {
+    let contents = document.importNode(this._template.content, true);
+    this._hostNameEl = contents.querySelector("#host-name");
+
+    this._cancelButton = contents.querySelector("#cancel");
+    this._cancelButton.addEventListener("click", this);
+
+    this._form = contents.querySelector("form");
+    this._form.addEventListener("submit", this);
+
+    this.appendChild(contents);
+
+    super.connectedCallback();
+  }
+
+  disconnectedCallback() {
+    this._cancelButtonEl.removeEventListener("click", this);
+    this._form.removeEventListener("submit", this);
+    super.disconnectedCallback();
+  }
+
+  setLoadingState(state) {
+    requestStore.setState(state);
+  }
+
+  onclick(event) {
+    PaymentRequest.cancel();
+  }
+
+  // https://w3c.github.io/payment-method-basic-card/#basiccardresponse-dictionary
+  convertSavedCardToBasicCardResponse(savedCard, cardSecurityCode, billingAddress = null) {
+    return {
+      cardNumber: savedCard["cc-number-encrypted"], // TODO: decrypt
+      cardholderName: savedCard["cc-name"],
+      cardSecurityCode,
+      expiryMonth: String(savedCard["cc-exp-month"]).padStart(2, "0"),
+      expiryYear: String(savedCard["cc-exp-year"]),
+      billingAddress,
+    };
+  }
+
+  onsubmit(event) {
+    event.preventDefault();
+    let formData = new FormData(event.target);
+    let state = requestStore.getState();
+    let selectedCardGuid = formData.get("selectedBasicCard");
+    let selectedCard = state.savedBasicCards.find(card => selectedCardGuid == card.guid);
+    let cardSecurityCode = formData.get("cardSecurityCode");
+
+    // TODO: only send info that was requested
+    PaymentRequest.pay({
+      payerName: "Foo",
+      payerEmail: "foo@bar.com",
+      payerPhone: "1234567890",
+      paymentMethodName: "basic-card",
+      paymentMethodData: this.convertSavedCardToBasicCardResponse(selectedCard, cardSecurityCode),
+    });
+  }
+
+  render() {
+    let request = requestStore.getState().request;
+    this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
+
+    let totalItem = request.paymentDetails.totalItem;
+    let totalAmountEl = this.querySelector("#total > currency-amount");
+    totalAmountEl.value = totalItem.amount.value;
+    totalAmountEl.currency = totalItem.amount.currency;
+  }
+}
+
+customElements.define("payment-dialog", PaymentDialog);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/debugging.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
+    <script src="debugging.js"></script>
+  </head>
+  <body>
+    <div>
+      <button id="logState">Log state</button>
+      <button id="refresh">Refresh</button>
+    </div>
+    <div>
+      <button id="setRequest1">Set Request 1</button>
+      <button id="setRequest2">Set Request 2</button>
+    </div>
+    <div>
+      <button id="setAddresses1">Set Addreses 1</button>
+      <button id="delete1Address">Delete 1 Address</button>
+    </div>
+    <div>
+      <button id="setBasicCards1">Set Basic Cards 1</button>
+    </div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/debugging.js
@@ -0,0 +1,196 @@
+const requestStore = window.parent.document.querySelector("payment-dialog")._requestStore;
+let REQUEST_1 = {
+  tabId: 9,
+  topLevelPrincipal: {URI: {displayHost: "tschaeff.github.io"}},
+  requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
+  paymentMethods: [],
+  paymentDetails: {
+    id: "",
+    totalItem: {label: "Demo total", amount: {currency: "EUR", value: "1.00"}, pending: false},
+    displayItems: [
+      {
+        label: "Square",
+        amount: {
+          currency: "USD",
+          value: "5",
+        },
+      },
+    ],
+    shippingOptions: [
+      {
+        id: "123",
+        label: "Fast",
+        amount: {
+          currency: "USD",
+          value: 10,
+        },
+        selected: false,
+      },
+      {
+        id: "456",
+        label: "Faster (default)",
+        amount: {
+          currency: "USD",
+          value: 20,
+        },
+        selected: true,
+      },
+    ],
+    modifiers: null,
+    error: "",
+  },
+  paymentOptions: {
+    requestPayerName: false,
+    requestPayerEmail: false,
+    requestPayerPhone: false,
+    requestShipping: false,
+    shippingType: "shipping",
+  },
+};
+
+let REQUEST_2 = {
+  tabId: 9,
+  topLevelPrincipal: {URI: {displayHost: "example.com"}},
+  requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
+  paymentMethods: [],
+  paymentDetails: {
+    id: "",
+    totalItem: {label: "Demo total", amount: {currency: "CAD", value: "25.75"}, pending: false},
+    displayItems: [
+      {
+        label: "Triangle",
+        amount: {
+          currency: "CAD",
+          value: "3",
+        },
+      },
+      {
+        label: "Circle",
+        amount: {
+          currency: "EUR",
+          value: "10.50",
+        },
+      },
+    ],
+    shippingOptions: [
+      {
+        id: "123",
+        label: "Fast (default)",
+        amount: {
+          currency: "USD",
+          value: 10,
+        },
+        selected: true,
+      },
+      {
+        id: "947",
+        label: "Slow",
+        amount: {
+          currency: "USD",
+          value: 10,
+        },
+        selected: false,
+      },
+    ],
+    modifiers: null,
+    error: "",
+  },
+  paymentOptions: {
+    requestPayerName: false,
+    requestPayerEmail: false,
+    requestPayerPhone: false,
+    requestShipping: false,
+    shippingType: "shipping",
+  },
+};
+
+let ADDRESSES_1 = [
+  {
+    guid: "48bnds6854t",
+    name: "Mr. Foo",
+  },
+  {
+    guid: "65tfdgg34",
+    name: "Mrs. Bar",
+  },
+];
+
+let BASIC_CARDS_1 = [
+  {
+    "cc-exp-month": 5,
+    "cc-number-encrypted": "_decrypt_me_",
+    "cc-number": "************1234",
+    "guid": "5bd7e7337f18",
+    "version": 1,
+    "timeCreated": 1506397876390,
+    "timeLastModified": 1506397909614,
+    "timeLastUsed": 0,
+    "timesUsed": 0,
+    "cc-name": "John Smith",
+    "cc-exp-year": 2020,
+    "cc-given-name": "John",
+    "cc-additional-name": "",
+    "cc-family-name": "Smith",
+    "cc-exp": "2020-05",
+  },
+  {
+    "cc-exp-month": 6,
+    "cc-number-encrypted": "_decrypt_me_",
+    "cc-number": "************8473",
+    "guid": "87gh3hf6g4",
+    "version": 1,
+    "timeCreated": 1506397876390,
+    "timeLastModified": 1506397909614,
+    "timeLastUsed": 0,
+    "timesUsed": 0,
+    "cc-name": "Mr. J. Magoo",
+    "cc-exp-year": 2019,
+    "cc-given-name": "J.",
+    "cc-additional-name": "",
+    "cc-family-name": "Magoo",
+    "cc-exp": "2019-06",
+  },
+];
+
+let buttonActions = {
+  delete1Address() {
+    requestStore.setState({
+      savedAddresses: requestStore.getState().savedAddresses.slice(1),
+    });
+  },
+
+  logState() {
+    let state = requestStore.getState();
+    console.log(state);
+    dump(`${JSON.stringify(state, null, 2)}\n`);
+  },
+
+  refresh() {
+    window.parent.location.reload(true);
+  },
+
+  setAddresses1() {
+    requestStore.setState({savedAddresses: ADDRESSES_1});
+  },
+
+  setBasicCards1() {
+    requestStore.setState({savedBasicCards: BASIC_CARDS_1});
+  },
+
+  setRequest1() {
+    requestStore.setState({request: REQUEST_1});
+  },
+
+  setRequest2() {
+    requestStore.setState({request: REQUEST_2});
+  },
+};
+
+window.addEventListener("click", function onButtonClick(evt) {
+  let id = evt.target.id;
+  if (!id || typeof(buttonActions[id]) != "function") {
+    return;
+  }
+
+  buttonActions[id]();
+});
--- a/toolkit/components/payments/res/paymentRequest.css
+++ b/toolkit/components/payments/res/paymentRequest.css
@@ -1,24 +1,32 @@
 /* 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/. */
 
-html {
-  background: -moz-dialog;
+body {
+  background: white;
+}
+
+dl {
+  max-height: 8em;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+summary {
+  cursor: pointer;
+}
+
+#debugging {
+  float: right;
 }
 
 #total {
   border: 1px solid black;
   margin: 5px;
   text-align: center;
 }
 
 #total .label {
   font-size: 15px;
   font-weight: bold;
 }
-
-#cancel {
-  position: absolute;
-  bottom: 10px;
-  left: 10px;
-}
rename from toolkit/components/payments/res/paymentRequest.xhtml
rename to toolkit/components/payments/res/paymentRequest.html
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.html
@@ -1,24 +1,55 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!-- 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/. -->
 <!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
+<html>
 <head>
+  <meta charset="utf-8">
+  <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
   <title></title>
-  <link rel="stylesheet" href="resource://payments/paymentRequest.css" />
-  <script src="resource://payments/paymentRequest.js"></script>
+  <link rel="stylesheet" href="paymentRequest.css"/>
+  <script src="vendor/custom-elements.min.js"></script>
+  <script src="paymentsStore.js"></script>
+  <script src="components.js"></script>
+  <script src="containers.js"></script>
+  <script src="paymentRequest.js"></script>
+
+  <template id="payment-dialog-template">
+    <form>
+      <h1>Your Order</h1>
+      <div id="host-name">placeholder.example.com</div>
+
+      <payment-items></payment-items>
+
+      <div id="total">
+        <h2 class="label">Total</h2>
+        <currency-amount currency="USD" value="100.00"></currency-amount>
+      </div>
+
+      <fieldset>
+        <legend>Ship to</legend>
+        <shippingoption-picker selected-state-key="selectedShippingOption"></shippingoption-picker>
+        <address-picker id="shipping-address" selected-state-key="selectedShippingAddress"></address-picker>
+        <add-address></add-address>
+      </fieldset>
+
+      <fieldset>
+        <legend>Pay by</legend>
+        <basiccard-picker selected-state-key="selectedBasicCard"></basiccard-picker>
+        <add-address></add-address>
+        <address-picker id="billing-address" selected-state-key="selectedBillingAddress"></address-picker>
+      </fieldset>
+
+      <div id="controls-container">
+        <button id="cancel">Cancel payment</button>
+        <button id="pay">Pay</button>
+      </div>
+    </form>
+  </template>
 </head>
 <body>
-  <div id="host-name"></div>
+  <iframe id="debugging" src="debugging.html"></iframe>
 
-  <div id="total">
-    <h2 class="label"></h2>
-    <span class="value"></span>
-    <span class="currency"></span>
-  </div>
-  <div id="controls-container">
-    <button id="cancel">Cancel payment</button>
-  </div>
+  <payment-dialog></payment-dialog>
 </body>
 </html>
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -5,19 +5,17 @@
 /**
  * Loaded in the unprivileged frame of each payment dialog.
  *
  * Communicates with privileged code via DOM Events.
  */
 
 "use strict";
 
-let PaymentRequest = {
-  request: null,
-
+var PaymentRequest = {
   init() {
     // listen to content
     window.addEventListener("paymentChromeToContent", this);
 
     // listen to user events
     window.addEventListener("DOMContentLoaded", this, {once: true});
 
     // This scope is now ready to listen to the initialization data
@@ -26,22 +24,16 @@ let PaymentRequest = {
 
   handleEvent(event) {
     switch (event.type) {
       case "DOMContentLoaded": {
         this.onPaymentRequestLoad();
         break;
       }
       case "click": {
-        switch (event.target.id) {
-          case "cancel": {
-            this.onCancel();
-            break;
-          }
-        }
         break;
       }
       case "unload": {
         this.onPaymentRequestUnload();
         break;
       }
       case "paymentChromeToContent": {
         this.onChromeToContent(event);
@@ -63,42 +55,38 @@ let PaymentRequest = {
     document.dispatchEvent(event);
   },
 
   onChromeToContent({detail}) {
     let {messageType} = detail;
 
     switch (messageType) {
       case "showPaymentRequest": {
-        this.request = detail.request;
-
-        let hostNameEl = document.getElementById("host-name");
-        hostNameEl.textContent = this.request.topLevelPrincipal.URI.displayHost;
-
-        let totalItem = this.request.paymentDetails.totalItem;
-        let totalEl = document.getElementById("total");
-        totalEl.querySelector(".value").textContent = totalItem.amount.value;
-        totalEl.querySelector(".currency").textContent = totalItem.amount.currency;
-        totalEl.querySelector(".label").textContent = totalItem.label;
+        document.querySelector("payment-dialog").setLoadingState({
+          request: detail.request,
+          savedAddresses: detail.savedAddresses,
+          savedBasicCards: detail.savedBasicCards,
+        });
         break;
       }
     }
   },
 
   onPaymentRequestLoad(requestId) {
-    let cancelBtn = document.getElementById("cancel");
-    cancelBtn.addEventListener("click", this, {once: true});
-
     window.addEventListener("unload", this, {once: true});
     this.sendMessageToChrome("paymentDialogReady");
   },
 
-  onCancel() {
+  cancel() {
     this.sendMessageToChrome("paymentCancel");
   },
 
+  pay(data) {
+    this.sendMessageToChrome("pay", data);
+  },
+
   onPaymentRequestUnload() {
     // remove listeners that may be used multiple times here
     window.removeEventListener("paymentChromeToContent", this);
   },
 };
 
 PaymentRequest.init();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/paymentsStore.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+/* exported PaymentsStore */
+
+class PaymentsStore {
+  constructor(defaultState = {}) {
+    console.debug("constructing store");
+    this._state = defaultState;
+    this._nextNotifification = 0;
+    this.subscribers = new Set();
+  }
+
+  getState() {
+    return Object.assign({}, this._state);
+  }
+
+  async setState(obj) {
+    console.debug("Setting state:", obj);
+    Object.assign(this._state, obj);
+    let thisChangeNum = ++this._nextNotifification;
+
+    // This lets any *synchronous* setState
+    // calls that happen after the current setState call complete first.
+    // Their effects on the state will be batched up before the render
+    // call below actually happens.
+    await Promise.resolve();
+
+    // https://github.com/elix/elix/blob/frp/mixins/ReactiveMixin.js
+
+    if (thisChangeNum !== this._nextNotifification) {
+      console.log("not notifying for stale state change", obj);
+      return;
+    }
+
+    console.log("notifying subscribers of state change", this._state);
+    for (let subscriber of this.subscribers) {
+      try {
+        subscriber.stateChangeCallback(this.getState());
+      } catch (ex) {
+        console.error(ex);
+      }
+    }
+  }
+
+  subscribe(component) {
+    if (this.subscribers.has(component)) {
+      return;
+    }
+
+    this.subscribers.add(component);
+    component.stateChangeCallback(this.getState());
+  }
+
+  unsubscribe(component) {
+    this.subscribers.delete(component);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/vendor/custom-elements.min.js
@@ -0,0 +1,37 @@
+(function(){
+'use strict';var h=new function(){};var aa=new Set("annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" "));function m(b){var a=aa.has(b);b=/^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/.test(b);return!a&&b}function n(b){var a=b.isConnected;if(void 0!==a)return a;for(;b&&!(b.__CE_isImportDocument||b instanceof Document);)b=b.parentNode||(window.ShadowRoot&&b instanceof ShadowRoot?b.host:void 0);return!(!b||!(b.__CE_isImportDocument||b instanceof Document))}
+function p(b,a){for(;a&&a!==b&&!a.nextSibling;)a=a.parentNode;return a&&a!==b?a.nextSibling:null}
+function t(b,a,d){d=d?d:new Set;for(var c=b;c;){if(c.nodeType===Node.ELEMENT_NODE){var e=c;a(e);var f=e.localName;if("link"===f&&"import"===e.getAttribute("rel")){c=e.import;if(c instanceof Node&&!d.has(c))for(d.add(c),c=c.firstChild;c;c=c.nextSibling)t(c,a,d);c=p(b,e);continue}else if("template"===f){c=p(b,e);continue}if(e=e.__CE_shadowRoot)for(e=e.firstChild;e;e=e.nextSibling)t(e,a,d)}c=c.firstChild?c.firstChild:p(b,c)}}function u(b,a,d){b[a]=d};function v(){this.a=new Map;this.o=new Map;this.f=[];this.b=!1}function ba(b,a,d){b.a.set(a,d);b.o.set(d.constructor,d)}function w(b,a){b.b=!0;b.f.push(a)}function x(b,a){b.b&&t(a,function(a){return y(b,a)})}function y(b,a){if(b.b&&!a.__CE_patched){a.__CE_patched=!0;for(var d=0;d<b.f.length;d++)b.f[d](a)}}function z(b,a){var d=[];t(a,function(b){return d.push(b)});for(a=0;a<d.length;a++){var c=d[a];1===c.__CE_state?b.connectedCallback(c):A(b,c)}}
+function B(b,a){var d=[];t(a,function(b){return d.push(b)});for(a=0;a<d.length;a++){var c=d[a];1===c.__CE_state&&b.disconnectedCallback(c)}}
+function C(b,a,d){d=d?d:{};var c=d.w||new Set,e=d.s||function(a){return A(b,a)},f=[];t(a,function(a){if("link"===a.localName&&"import"===a.getAttribute("rel")){var d=a.import;d instanceof Node&&"complete"===d.readyState?(d.__CE_isImportDocument=!0,d.__CE_hasRegistry=!0):a.addEventListener("load",function(){var d=a.import;if(!d.__CE_documentLoadHandled){d.__CE_documentLoadHandled=!0;d.__CE_isImportDocument=!0;d.__CE_hasRegistry=!0;var f=new Set(c);f.delete(d);C(b,d,{w:f,s:e})}})}else f.push(a)},c);
+if(b.b)for(a=0;a<f.length;a++)y(b,f[a]);for(a=0;a<f.length;a++)e(f[a])}
+function A(b,a){if(void 0===a.__CE_state){var d=b.a.get(a.localName);if(d){d.constructionStack.push(a);var c=d.constructor;try{try{if(new c!==a)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{d.constructionStack.pop()}}catch(r){throw a.__CE_state=2,r;}a.__CE_state=1;a.__CE_definition=d;if(d.attributeChangedCallback)for(d=d.observedAttributes,c=0;c<d.length;c++){var e=d[c],f=a.getAttribute(e);null!==f&&b.attributeChangedCallback(a,e,null,f,null)}n(a)&&
+b.connectedCallback(a)}}}v.prototype.connectedCallback=function(b){var a=b.__CE_definition;a.connectedCallback&&a.connectedCallback.call(b)};v.prototype.disconnectedCallback=function(b){var a=b.__CE_definition;a.disconnectedCallback&&a.disconnectedCallback.call(b)};v.prototype.attributeChangedCallback=function(b,a,d,c,e){var f=b.__CE_definition;f.attributeChangedCallback&&-1<f.observedAttributes.indexOf(a)&&f.attributeChangedCallback.call(b,a,d,c,e)};function D(b,a){this.c=b;this.a=a;this.b=void 0;C(this.c,this.a);"loading"===this.a.readyState&&(this.b=new MutationObserver(this.f.bind(this)),this.b.observe(this.a,{childList:!0,subtree:!0}))}function E(b){b.b&&b.b.disconnect()}D.prototype.f=function(b){var a=this.a.readyState;"interactive"!==a&&"complete"!==a||E(this);for(a=0;a<b.length;a++)for(var d=b[a].addedNodes,c=0;c<d.length;c++)C(this.c,d[c])};function ca(){var b=this;this.b=this.a=void 0;this.f=new Promise(function(a){b.b=a;b.a&&a(b.a)})}function F(b){if(b.a)throw Error("Already resolved.");b.a=void 0;b.b&&b.b(void 0)};function G(b){this.i=!1;this.c=b;this.m=new Map;this.j=function(b){return b()};this.g=!1;this.l=[];this.u=new D(b,document)}
+G.prototype.define=function(b,a){var d=this;if(!(a instanceof Function))throw new TypeError("Custom element constructors must be functions.");if(!m(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(this.c.a.get(b))throw Error("A custom element with name '"+b+"' has already been defined.");if(this.i)throw Error("A custom element is already being defined.");this.i=!0;var c,e,f,r,k;try{var g=function(b){var a=l[b];if(void 0!==a&&!(a instanceof Function))throw Error("The '"+b+"' callback must be a function.");
+return a},l=a.prototype;if(!(l instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");c=g("connectedCallback");e=g("disconnectedCallback");f=g("adoptedCallback");r=g("attributeChangedCallback");k=a.observedAttributes||[]}catch(q){return}finally{this.i=!1}a={localName:b,constructor:a,connectedCallback:c,disconnectedCallback:e,adoptedCallback:f,attributeChangedCallback:r,observedAttributes:k,constructionStack:[]};ba(this.c,b,a);this.l.push(a);this.g||
+(this.g=!0,this.j(function(){return da(d)}))};function da(b){if(!1!==b.g){b.g=!1;for(var a=b.l,d=[],c=new Map,e=0;e<a.length;e++)c.set(a[e].localName,[]);C(b.c,document,{s:function(a){if(void 0===a.__CE_state){var e=a.localName,f=c.get(e);f?f.push(a):b.c.a.get(e)&&d.push(a)}}});for(e=0;e<d.length;e++)A(b.c,d[e]);for(;0<a.length;){for(var f=a.shift(),e=f.localName,f=c.get(f.localName),r=0;r<f.length;r++)A(b.c,f[r]);(e=b.m.get(e))&&F(e)}}}G.prototype.get=function(b){if(b=this.c.a.get(b))return b.constructor};
+G.prototype.whenDefined=function(b){if(!m(b))return Promise.reject(new SyntaxError("'"+b+"' is not a valid custom element name."));var a=this.m.get(b);if(a)return a.f;a=new ca;this.m.set(b,a);this.c.a.get(b)&&!this.l.some(function(a){return a.localName===b})&&F(a);return a.f};G.prototype.v=function(b){E(this.u);var a=this.j;this.j=function(d){return b(function(){return a(d)})}};window.CustomElementRegistry=G;G.prototype.define=G.prototype.define;G.prototype.get=G.prototype.get;
+G.prototype.whenDefined=G.prototype.whenDefined;G.prototype.polyfillWrapFlushCallback=G.prototype.v;var H=window.Document.prototype.createElement,ea=window.Document.prototype.createElementNS,fa=window.Document.prototype.importNode,ga=window.Document.prototype.prepend,ha=window.Document.prototype.append,ia=window.DocumentFragment.prototype.prepend,ja=window.DocumentFragment.prototype.append,I=window.Node.prototype.cloneNode,J=window.Node.prototype.appendChild,K=window.Node.prototype.insertBefore,L=window.Node.prototype.removeChild,M=window.Node.prototype.replaceChild,N=Object.getOwnPropertyDescriptor(window.Node.prototype,
+"textContent"),O=window.Element.prototype.attachShadow,P=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),Q=window.Element.prototype.getAttribute,R=window.Element.prototype.setAttribute,S=window.Element.prototype.removeAttribute,T=window.Element.prototype.getAttributeNS,U=window.Element.prototype.setAttributeNS,ka=window.Element.prototype.removeAttributeNS,la=window.Element.prototype.insertAdjacentElement,ma=window.Element.prototype.prepend,na=window.Element.prototype.append,
+V=window.Element.prototype.before,oa=window.Element.prototype.after,pa=window.Element.prototype.replaceWith,qa=window.Element.prototype.remove,ra=window.HTMLElement,W=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),sa=window.HTMLElement.prototype.insertAdjacentElement;function ta(){var b=X;window.HTMLElement=function(){function a(){var a=this.constructor,c=b.o.get(a);if(!c)throw Error("The custom element being constructed was not registered with `customElements`.");var e=c.constructionStack;if(!e.length)return e=H.call(document,c.localName),Object.setPrototypeOf(e,a.prototype),e.__CE_state=1,e.__CE_definition=c,y(b,e),e;var c=e.length-1,f=e[c];if(f===h)throw Error("The HTMLElement constructor was either called reentrantly for this constructor or called multiple times.");
+e[c]=h;Object.setPrototypeOf(f,a.prototype);y(b,f);return f}a.prototype=ra.prototype;return a}()};function Y(b,a,d){function c(a){return function(d){for(var e=[],c=0;c<arguments.length;++c)e[c-0]=arguments[c];for(var c=[],f=[],l=0;l<e.length;l++){var q=e[l];q instanceof Element&&n(q)&&f.push(q);if(q instanceof DocumentFragment)for(q=q.firstChild;q;q=q.nextSibling)c.push(q);else c.push(q)}a.apply(this,e);for(e=0;e<f.length;e++)B(b,f[e]);if(n(this))for(e=0;e<c.length;e++)f=c[e],f instanceof Element&&z(b,f)}}d.h&&(a.prepend=c(d.h));d.append&&(a.append=c(d.append))};function ua(){var b=X;u(Document.prototype,"createElement",function(a){if(this.__CE_hasRegistry){var d=b.a.get(a);if(d)return new d.constructor}a=H.call(this,a);y(b,a);return a});u(Document.prototype,"importNode",function(a,d){a=fa.call(this,a,d);this.__CE_hasRegistry?C(b,a):x(b,a);return a});u(Document.prototype,"createElementNS",function(a,d){if(this.__CE_hasRegistry&&(null===a||"http://www.w3.org/1999/xhtml"===a)){var c=b.a.get(d);if(c)return new c.constructor}a=ea.call(this,a,d);y(b,a);return a});
+Y(b,Document.prototype,{h:ga,append:ha})};function va(){var b=X;function a(a,c){Object.defineProperty(a,"textContent",{enumerable:c.enumerable,configurable:!0,get:c.get,set:function(a){if(this.nodeType===Node.TEXT_NODE)c.set.call(this,a);else{var e=void 0;if(this.firstChild){var d=this.childNodes,k=d.length;if(0<k&&n(this))for(var e=Array(k),g=0;g<k;g++)e[g]=d[g]}c.set.call(this,a);if(e)for(a=0;a<e.length;a++)B(b,e[a])}}})}u(Node.prototype,"insertBefore",function(a,c){if(a instanceof DocumentFragment){var e=Array.prototype.slice.apply(a.childNodes);
+a=K.call(this,a,c);if(n(this))for(c=0;c<e.length;c++)z(b,e[c]);return a}e=n(a);c=K.call(this,a,c);e&&B(b,a);n(this)&&z(b,a);return c});u(Node.prototype,"appendChild",function(a){if(a instanceof DocumentFragment){var c=Array.prototype.slice.apply(a.childNodes);a=J.call(this,a);if(n(this))for(var e=0;e<c.length;e++)z(b,c[e]);return a}c=n(a);e=J.call(this,a);c&&B(b,a);n(this)&&z(b,a);return e});u(Node.prototype,"cloneNode",function(a){a=I.call(this,a);this.ownerDocument.__CE_hasRegistry?C(b,a):x(b,a);
+return a});u(Node.prototype,"removeChild",function(a){var c=n(a),e=L.call(this,a);c&&B(b,a);return e});u(Node.prototype,"replaceChild",function(a,c){if(a instanceof DocumentFragment){var e=Array.prototype.slice.apply(a.childNodes);a=M.call(this,a,c);if(n(this))for(B(b,c),c=0;c<e.length;c++)z(b,e[c]);return a}var e=n(a),f=M.call(this,a,c),d=n(this);d&&B(b,c);e&&B(b,a);d&&z(b,a);return f});N&&N.get?a(Node.prototype,N):w(b,function(b){a(b,{enumerable:!0,configurable:!0,get:function(){for(var a=[],b=
+0;b<this.childNodes.length;b++)a.push(this.childNodes[b].textContent);return a.join("")},set:function(a){for(;this.firstChild;)L.call(this,this.firstChild);J.call(this,document.createTextNode(a))}})})};function wa(b){var a=Element.prototype;function d(a){return function(e){for(var c=[],d=0;d<arguments.length;++d)c[d-0]=arguments[d];for(var d=[],k=[],g=0;g<c.length;g++){var l=c[g];l instanceof Element&&n(l)&&k.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)d.push(l);else d.push(l)}a.apply(this,c);for(c=0;c<k.length;c++)B(b,k[c]);if(n(this))for(c=0;c<d.length;c++)k=d[c],k instanceof Element&&z(b,k)}}V&&(a.before=d(V));V&&(a.after=d(oa));pa&&u(a,"replaceWith",function(a){for(var e=
+[],c=0;c<arguments.length;++c)e[c-0]=arguments[c];for(var c=[],d=[],k=0;k<e.length;k++){var g=e[k];g instanceof Element&&n(g)&&d.push(g);if(g instanceof DocumentFragment)for(g=g.firstChild;g;g=g.nextSibling)c.push(g);else c.push(g)}k=n(this);pa.apply(this,e);for(e=0;e<d.length;e++)B(b,d[e]);if(k)for(B(b,this),e=0;e<c.length;e++)d=c[e],d instanceof Element&&z(b,d)});qa&&u(a,"remove",function(){var a=n(this);qa.call(this);a&&B(b,this)})};function xa(){var b=X;function a(a,c){Object.defineProperty(a,"innerHTML",{enumerable:c.enumerable,configurable:!0,get:c.get,set:function(a){var e=this,d=void 0;n(this)&&(d=[],t(this,function(a){a!==e&&d.push(a)}));c.set.call(this,a);if(d)for(var f=0;f<d.length;f++){var r=d[f];1===r.__CE_state&&b.disconnectedCallback(r)}this.ownerDocument.__CE_hasRegistry?C(b,this):x(b,this);return a}})}function d(a,c){u(a,"insertAdjacentElement",function(a,e){var d=n(e);a=c.call(this,a,e);d&&B(b,e);n(a)&&z(b,e);
+return a})}O&&u(Element.prototype,"attachShadow",function(a){return this.__CE_shadowRoot=a=O.call(this,a)});if(P&&P.get)a(Element.prototype,P);else if(W&&W.get)a(HTMLElement.prototype,W);else{var c=H.call(document,"div");w(b,function(b){a(b,{enumerable:!0,configurable:!0,get:function(){return I.call(this,!0).innerHTML},set:function(a){var b="template"===this.localName?this.content:this;for(c.innerHTML=a;0<b.childNodes.length;)L.call(b,b.childNodes[0]);for(;0<c.childNodes.length;)J.call(b,c.childNodes[0])}})})}u(Element.prototype,
+"setAttribute",function(a,c){if(1!==this.__CE_state)return R.call(this,a,c);var e=Q.call(this,a);R.call(this,a,c);c=Q.call(this,a);b.attributeChangedCallback(this,a,e,c,null)});u(Element.prototype,"setAttributeNS",function(a,c,d){if(1!==this.__CE_state)return U.call(this,a,c,d);var e=T.call(this,a,c);U.call(this,a,c,d);d=T.call(this,a,c);b.attributeChangedCallback(this,c,e,d,a)});u(Element.prototype,"removeAttribute",function(a){if(1!==this.__CE_state)return S.call(this,a);var c=Q.call(this,a);S.call(this,
+a);null!==c&&b.attributeChangedCallback(this,a,c,null,null)});u(Element.prototype,"removeAttributeNS",function(a,c){if(1!==this.__CE_state)return ka.call(this,a,c);var d=T.call(this,a,c);ka.call(this,a,c);var e=T.call(this,a,c);d!==e&&b.attributeChangedCallback(this,c,d,e,a)});sa?d(HTMLElement.prototype,sa):la?d(Element.prototype,la):console.warn("Custom Elements: `Element#insertAdjacentElement` was not patched.");Y(b,Element.prototype,{h:ma,append:na});wa(b)};/*
+
+ Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+ This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+ The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+ The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+ Code distributed by Google as part of the polymer project is also
+ subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+*/
+var Z=window.customElements;if(!Z||Z.forcePolyfill||"function"!=typeof Z.define||"function"!=typeof Z.get){var X=new v;ta();ua();Y(X,DocumentFragment.prototype,{h:ia,append:ja});va();xa();document.__CE_hasRegistry=!0;var customElements=new G(X);Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:customElements})};
+}).call(self);
+
+//# sourceMappingURL=custom-elements.min.js.map