summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/res
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/payments/res/PaymentsStore.js97
-rw-r--r--browser/components/payments/res/components/accepted-cards.css108
-rw-r--r--browser/components/payments/res/components/accepted-cards.js75
-rw-r--r--browser/components/payments/res/components/address-option.css29
-rw-r--r--browser/components/payments/res/components/address-option.js159
-rw-r--r--browser/components/payments/res/components/basic-card-option.css40
-rw-r--r--browser/components/payments/res/components/basic-card-option.js89
-rw-r--r--browser/components/payments/res/components/card-icon.svg9
-rw-r--r--browser/components/payments/res/components/csc-input.js112
-rw-r--r--browser/components/payments/res/components/currency-amount.js63
-rw-r--r--browser/components/payments/res/components/labelled-checkbox.js59
-rw-r--r--browser/components/payments/res/components/payment-details-item.css12
-rw-r--r--browser/components/payments/res/components/payment-details-item.js47
-rw-r--r--browser/components/payments/res/components/payment-request-page.js36
-rw-r--r--browser/components/payments/res/components/rich-option.js26
-rw-r--r--browser/components/payments/res/components/rich-select.css58
-rw-r--r--browser/components/payments/res/components/rich-select.js104
-rw-r--r--browser/components/payments/res/components/shipping-option.css16
-rw-r--r--browser/components/payments/res/components/shipping-option.js65
-rw-r--r--browser/components/payments/res/containers/address-form.css55
-rw-r--r--browser/components/payments/res/containers/address-form.js447
-rw-r--r--browser/components/payments/res/containers/address-picker.js282
-rw-r--r--browser/components/payments/res/containers/basic-card-form.css43
-rw-r--r--browser/components/payments/res/containers/basic-card-form.js507
-rw-r--r--browser/components/payments/res/containers/billing-address-picker.js33
-rw-r--r--browser/components/payments/res/containers/completion-error-page.js112
-rw-r--r--browser/components/payments/res/containers/cvv-hint-image-back.svg27
-rw-r--r--browser/components/payments/res/containers/cvv-hint-image-front.svg25
-rw-r--r--browser/components/payments/res/containers/error-page.css42
-rw-r--r--browser/components/payments/res/containers/order-details.css55
-rw-r--r--browser/components/payments/res/containers/order-details.js143
-rw-r--r--browser/components/payments/res/containers/payment-dialog.js593
-rw-r--r--browser/components/payments/res/containers/payment-method-picker.js199
-rw-r--r--browser/components/payments/res/containers/rich-picker.css83
-rw-r--r--browser/components/payments/res/containers/rich-picker.js114
-rw-r--r--browser/components/payments/res/containers/shipping-option-picker.js72
-rw-r--r--browser/components/payments/res/containers/timeout.svg84
-rw-r--r--browser/components/payments/res/containers/warning.svg32
-rw-r--r--browser/components/payments/res/debugging.css35
-rw-r--r--browser/components/payments/res/debugging.html75
-rw-r--r--browser/components/payments/res/debugging.js664
-rw-r--r--browser/components/payments/res/mixins/HandleEventMixin.js28
-rw-r--r--browser/components/payments/res/mixins/ObservedPropertiesMixin.js71
-rw-r--r--browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js112
-rw-r--r--browser/components/payments/res/paymentRequest.css265
-rw-r--r--browser/components/payments/res/paymentRequest.js356
-rw-r--r--browser/components/payments/res/paymentRequest.xhtml303
-rw-r--r--browser/components/payments/res/unprivileged-fallbacks.js159
48 files changed, 6220 insertions, 0 deletions
diff --git a/browser/components/payments/res/PaymentsStore.js b/browser/components/payments/res/PaymentsStore.js
new file mode 100644
index 0000000000..7e439076d8
--- /dev/null
+++ b/browser/components/payments/res/PaymentsStore.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+/**
+ * The PaymentsStore class provides lightweight storage with an async publish/subscribe mechanism.
+ * Synchronous state changes are batched to improve application performance and to reduce partial
+ * state propagation.
+ */
+
+export default class PaymentsStore {
+ /**
+ * @param {object} [defaultState = {}] The initial state of the store.
+ */
+ constructor(defaultState = {}) {
+ this._defaultState = Object.assign({}, defaultState);
+ this._state = defaultState;
+ this._nextNotifification = 0;
+ this._subscribers = new Set();
+ }
+
+ /**
+ * Get the current state as a shallow clone with a shallow freeze.
+ * You shouldn't modify any part of the returned state object as that would bypass notifying
+ * subscribers and could lead to subscribers assuming old state.
+ *
+ * @returns {Object} containing the current state
+ */
+ getState() {
+ return Object.freeze(Object.assign({}, this._state));
+ }
+
+ /**
+ * Used for testing to reset to the default state from the constructor.
+ * @returns {Promise} returned by setState.
+ */
+ async reset() {
+ return this.setState(this._defaultState);
+ }
+
+ /**
+ * Augment the current state with the keys of `obj` and asynchronously notify
+ * state subscribers. As a result, multiple synchronous state changes will lead
+ * to a single subscriber notification which leads to better performance and
+ * reduces partial state changes.
+ *
+ * @param {Object} obj The object to augment the state with. Keys in the object
+ * will be shallow copied with Object.assign.
+ *
+ * @example If the state is currently {a:3} then setState({b:"abc"}) will result in a state of
+ * {a:3, b:"abc"}.
+ */
+ async setState(obj) {
+ Object.assign(this._state, obj);
+ let thisChangeNum = ++this._nextNotifification;
+
+ // Let any synchronous setState calls that happen after the current setState call
+ // complete first.
+ // Their effects on the state will be batched up before the callback is actually called below.
+ await Promise.resolve();
+
+ // Don't notify for state changes that are no longer the most recent. We only want to call the
+ // callback once with the latest state.
+ if (thisChangeNum !== this._nextNotifification) {
+ return;
+ }
+
+ for (let subscriber of this._subscribers) {
+ try {
+ subscriber.stateChangeCallback(this.getState());
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ }
+
+ /**
+ * Subscribe the object to state changes notifications via a `stateChangeCallback` method.
+ *
+ * @param {Object} component to receive state change callbacks via a `stateChangeCallback` method.
+ * If the component is already subscribed, do nothing.
+ */
+ subscribe(component) {
+ if (this._subscribers.has(component)) {
+ return;
+ }
+
+ this._subscribers.add(component);
+ }
+
+ /**
+ * @param {Object} component to stop receiving state change callbacks.
+ */
+ unsubscribe(component) {
+ this._subscribers.delete(component);
+ }
+}
diff --git a/browser/components/payments/res/components/accepted-cards.css b/browser/components/payments/res/components/accepted-cards.css
new file mode 100644
index 0000000000..575e17b4f1
--- /dev/null
+++ b/browser/components/payments/res/components/accepted-cards.css
@@ -0,0 +1,108 @@
+/* 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/. */
+
+accepted-cards {
+ margin: 1em 0;
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: first baseline;
+}
+
+.accepted-cards-label {
+ display: inline-block;
+ font-size: smaller;
+ flex: 0 2 content;
+ white-space: nowrap;
+}
+
+.accepted-cards-list {
+ display: inline-block;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ flex: 2 1 auto;
+}
+
+.accepted-cards-list > .accepted-cards-item {
+ display: inline-block;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ margin: 5px 0;
+ margin-inline-start: 10px;
+ vertical-align: middle;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+}
+
+/* placeholders for specific card icons we don't yet have assets for */
+accepted-cards:not(.branded) .accepted-cards-item[data-network-id] {
+ width: 48px;
+ text-align: center;
+ background-image: url("./card-icon.svg");
+ -moz-context-properties: fill-opacity;
+ fill-opacity: 0.5;
+}
+accepted-cards:not(.branded) .accepted-cards-item[data-network-id]::after {
+ box-sizing: border-box;
+ content: attr(data-network-id);
+ padding: 8px 4px 0 4px;
+ text-align: center;
+ font-size: 0.7rem;
+ display: inline-block;
+ overflow: hidden;
+ width: 100%;
+}
+
+/*
+ We use .png / @2x.png images where we don't yet have a vector version of a logo
+*/
+.accepted-cards-item[data-network-id="amex"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-amex.png");
+}
+
+.accepted-cards-item[data-network-id="cartebancaire"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-cartebancaire.png");
+}
+
+.accepted-cards-item[data-network-id="diners"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-diners.svg");
+}
+
+.accepted-cards-item[data-network-id="discover"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-discover.png");
+}
+
+.accepted-cards-item[data-network-id="jcb"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-jcb.svg");
+}
+
+.accepted-cards-item[data-network-id="mastercard"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-mastercard.svg");
+}
+
+.accepted-cards-item[data-network-id="mir"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-mir.svg");
+}
+
+.accepted-cards-item[data-network-id="unionpay"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-unionpay.svg");
+}
+
+.accepted-cards-item[data-network-id="visa"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-visa.svg");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .accepted-cards-item[data-network-id="amex"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-amex@2x.png");
+ }
+ .accepted-cards-item[data-network-id="cartebancaire"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-cartebancaire@2x.png");
+ }
+ .accepted-cards-item[data-network-id="discover"] {
+ background-image: url("chrome://formautofill/content/third-party/cc-logo-discover@2x.png");
+ }
+}
diff --git a/browser/components/payments/res/components/accepted-cards.js b/browser/components/payments/res/components/accepted-cards.js
new file mode 100644
index 0000000000..c66e040576
--- /dev/null
+++ b/browser/components/payments/res/components/accepted-cards.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <accepted-cards></accepted-cards>
+ */
+
+export default class AcceptedCards extends PaymentStateSubscriberMixin(
+ HTMLElement
+) {
+ constructor() {
+ super();
+
+ this._listEl = document.createElement("ul");
+ this._listEl.classList.add("accepted-cards-list");
+ this._labelEl = document.createElement("span");
+ this._labelEl.classList.add("accepted-cards-label");
+ }
+
+ connectedCallback() {
+ this.label = this.getAttribute("label");
+ this.appendChild(this._labelEl);
+
+ this._listEl.textContent = "";
+ let allNetworks = PaymentDialogUtils.getCreditCardNetworks();
+ for (let network of allNetworks) {
+ let item = document.createElement("li");
+ item.classList.add("accepted-cards-item");
+ item.dataset.networkId = network;
+ item.setAttribute("aria-role", "image");
+ item.setAttribute("aria-label", network);
+ this._listEl.appendChild(item);
+ }
+ let isBranded = PaymentDialogUtils.isOfficialBranding();
+ this.classList.toggle("branded", isBranded);
+ this.appendChild(this._listEl);
+ // Only call the connected super callback(s) once our markup is fully
+ // connected
+ super.connectedCallback();
+ }
+
+ render(state) {
+ let basicCardMethod = state.request.paymentMethods.find(
+ method => method.supportedMethods == "basic-card"
+ );
+ let merchantNetworks =
+ basicCardMethod &&
+ basicCardMethod.data &&
+ basicCardMethod.data.supportedNetworks;
+ if (merchantNetworks && merchantNetworks.length) {
+ for (let item of this._listEl.children) {
+ let network = item.dataset.networkId;
+ item.hidden = !(network && merchantNetworks.includes(network));
+ }
+ this.hidden = false;
+ } else {
+ // hide the whole list if the merchant didn't specify a preference
+ this.hidden = true;
+ }
+ }
+
+ set label(value) {
+ this._labelEl.textContent = value;
+ }
+
+ get acceptedItems() {
+ return Array.from(this._listEl.children).filter(item => !item.hidden);
+ }
+}
+
+customElements.define("accepted-cards", AcceptedCards);
diff --git a/browser/components/payments/res/components/address-option.css b/browser/components/payments/res/components/address-option.css
new file mode 100644
index 0000000000..9619048ae6
--- /dev/null
+++ b/browser/components/payments/res/components/address-option.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+address-option.rich-option {
+ grid-row-gap: 5px;
+}
+
+address-option > .line {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+address-option > .line:empty {
+ /* Hide the 2nd line in cases where it's empty
+ (e.g. payer field with one or two fields requested) */
+ display: none;
+}
+
+address-option > .line > span {
+ white-space: nowrap;
+}
+
+address-option > .line > span:empty::before {
+ /* Show the string for missing fields in grey when the field is empty */
+ color: GrayText;
+ content: attr(data-missing-string);
+}
diff --git a/browser/components/payments/res/components/address-option.js b/browser/components/payments/res/components/address-option.js
new file mode 100644
index 0000000000..2661f3e231
--- /dev/null
+++ b/browser/components/payments/res/components/address-option.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+import RichOption from "./rich-option.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * Up to two-line address display. After bug 1475684 this will also be used for
+ * the single-line <option> substitute too.
+ *
+ * <rich-select>
+ * <address-option guid="98hgvnbmytfc"
+ * address-level1="MI"
+ * address-level2="Some City"
+ * email="foo@example.com"
+ * country="USA"
+ * name="Jared Wein"
+ * postal-code="90210"
+ * street-address="1234 Anywhere St"
+ * tel="+1 650 555-5555"></address-option>
+ * </rich-select>
+ *
+ * Attribute names follow FormAutofillStorage.jsm.
+ */
+
+export default class AddressOption extends ObservedPropertiesMixin(RichOption) {
+ static get recordAttributes() {
+ return [
+ "address-level1",
+ "address-level2",
+ "address-level3",
+ "country",
+ "email",
+ "guid",
+ "name",
+ "organization",
+ "postal-code",
+ "street-address",
+ "tel",
+ ];
+ }
+
+ static get observedAttributes() {
+ return RichOption.observedAttributes.concat(
+ AddressOption.recordAttributes,
+ "address-fields",
+ "break-after-nth-field",
+ "data-field-separator"
+ );
+ }
+
+ constructor() {
+ super();
+
+ this._line1 = document.createElement("div");
+ this._line1.classList.add("line");
+ this._line2 = document.createElement("div");
+ this._line2.classList.add("line");
+
+ for (let name of AddressOption.recordAttributes) {
+ this[`_${name}`] = document.createElement("span");
+ this[`_${name}`].classList.add(name);
+ // XXX Bug 1490816: Use appropriate strings
+ let missingValueString =
+ name.replace(/(-|^)([a-z])/g, ($0, $1, $2) => {
+ return $1.replace("-", " ") + $2.toUpperCase();
+ }) + " Missing";
+ this[`_${name}`].dataset.missingString = missingValueString;
+ }
+ }
+
+ connectedCallback() {
+ this.appendChild(this._line1);
+ this.appendChild(this._line2);
+ super.connectedCallback();
+ }
+
+ static formatSingleLineLabel(address, addressFields) {
+ return PaymentDialogUtils.getAddressLabel(address, addressFields);
+ }
+
+ get requiredFields() {
+ if (this.hasAttribute("address-fields")) {
+ let names = this.getAttribute("address-fields")
+ .trim()
+ .split(/\s+/);
+ if (names.length) {
+ return names;
+ }
+ }
+
+ return [
+ // "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE
+ "address-level2",
+ "country",
+ "name",
+ "postal-code",
+ "street-address",
+ ];
+ }
+
+ render() {
+ // Clear the lines of the fields so we can append only the ones still
+ // visible in the correct order below.
+ this._line1.textContent = "";
+ this._line2.textContent = "";
+
+ // Fill the fields with their text/strings.
+ // Fall back to empty strings to prevent 'null' from appearing.
+ for (let name of AddressOption.recordAttributes) {
+ let camelCaseName = super.constructor.kebabToCamelCase(name);
+ let fieldEl = this[`_${name}`];
+ fieldEl.textContent = this[camelCaseName] || "";
+ }
+
+ let { fieldsOrder } = PaymentDialogUtils.getFormFormat(this.country);
+ // A subset of the requested fields may be returned if the fields don't apply to the country.
+ let requestedVisibleFields = this.addressFields || "mailing-address";
+ let visibleFields = EditAddress.computeVisibleFields(
+ fieldsOrder,
+ requestedVisibleFields
+ );
+ let visibleFieldCount = 0;
+ let requiredFields = this.requiredFields;
+ // Start by populating line 1
+ let lineEl = this._line1;
+ // Which field number to start line 2 after.
+ let breakAfterNthField = this.breakAfterNthField || 2;
+
+ // Now actually place the fields in the proper place on the lines.
+ for (let field of visibleFields) {
+ let fieldEl = this[`_${field.fieldId}`];
+ if (!fieldEl) {
+ log.warn(`address-option render: '${field.fieldId}' doesn't exist`);
+ continue;
+ }
+
+ if (!fieldEl.textContent && !requiredFields.includes(field.fieldId)) {
+ // The field is empty and we don't need to show "Missing …" so don't append.
+ continue;
+ }
+
+ if (lineEl.children.length) {
+ lineEl.append(this.dataset.fieldSeparator);
+ }
+ lineEl.appendChild(fieldEl);
+
+ // Add a break after this field, if requested.
+ if (++visibleFieldCount == breakAfterNthField) {
+ lineEl = this._line2;
+ }
+ }
+ }
+}
+
+customElements.define("address-option", AddressOption);
diff --git a/browser/components/payments/res/components/basic-card-option.css b/browser/components/payments/res/components/basic-card-option.css
new file mode 100644
index 0000000000..29776b0722
--- /dev/null
+++ b/browser/components/payments/res/components/basic-card-option.css
@@ -0,0 +1,40 @@
+/* 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/. */
+
+basic-card-option {
+ grid-column-gap: 1em;
+ grid-template-areas: "cc-type cc-number cc-exp cc-name";
+ /* Need to set a minimum width for the cc-type svg in the <img> to fill */
+ grid-template-columns: minmax(1em, auto);
+ justify-content: start;
+}
+
+basic-card-option > .cc-number,
+basic-card-option > .cc-name,
+basic-card-option > .cc-exp,
+basic-card-option > .cc-type {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+basic-card-option > .cc-number {
+ grid-area: cc-number;
+ /* Don't truncate the card number */
+ overflow: visible;
+}
+
+basic-card-option > .cc-name {
+ grid-area: cc-name;
+}
+
+basic-card-option > .cc-exp {
+ grid-area: cc-exp;
+}
+
+basic-card-option > .cc-type {
+ grid-area: cc-type;
+ height: 100%;
+ text-transform: capitalize;
+}
diff --git a/browser/components/payments/res/components/basic-card-option.js b/browser/components/payments/res/components/basic-card-option.js
new file mode 100644
index 0000000000..cda4a5d892
--- /dev/null
+++ b/browser/components/payments/res/components/basic-card-option.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+import RichOption from "./rich-option.js";
+
+/**
+ * <rich-select>
+ * <basic-card-option></basic-card-option>
+ * </rich-select>
+ */
+
+export default class BasicCardOption extends ObservedPropertiesMixin(
+ RichOption
+) {
+ static get recordAttributes() {
+ return ["cc-exp", "cc-name", "cc-number", "cc-type", "guid"];
+ }
+
+ static get observedAttributes() {
+ return RichOption.observedAttributes.concat(
+ BasicCardOption.recordAttributes
+ );
+ }
+
+ constructor() {
+ super();
+
+ for (let name of ["cc-name", "cc-number", "cc-exp", "cc-type"]) {
+ this[`_${name}`] = document.createElement(
+ name == "cc-type" ? "img" : "span"
+ );
+ this[`_${name}`].classList.add(name);
+ }
+ }
+
+ connectedCallback() {
+ for (let name of ["cc-name", "cc-number", "cc-exp", "cc-type"]) {
+ this.appendChild(this[`_${name}`]);
+ }
+ super.connectedCallback();
+ }
+
+ static formatCCNumber(ccNumber) {
+ // XXX: Bug 1470175 - This should probably be unified with CreditCard.jsm logic.
+ return ccNumber ? ccNumber.replace(/[*]{4,}/, "****") : "";
+ }
+
+ static formatSingleLineLabel(basicCard) {
+ let ccNumber = BasicCardOption.formatCCNumber(basicCard["cc-number"]);
+
+ // XXX Bug 1473772 - Hard-coded string
+ let ccExp = basicCard["cc-exp"] ? "Exp. " + basicCard["cc-exp"] : "";
+ let ccName = basicCard["cc-name"];
+ // XXX: Bug 1491040, displaying cc-type in this context may need its own localized string
+ let ccType = basicCard["cc-type"] || "";
+ // Filter out empty/undefined tokens before joining by three spaces
+ // (&nbsp; in the middle of two normal spaces to avoid them visually collapsing in HTML)
+ return [
+ ccType.replace(/^[a-z]/, $0 => $0.toUpperCase()),
+ ccNumber,
+ ccExp,
+ ccName,
+ // XXX Bug 1473772 - Hard-coded string:
+ ]
+ .filter(str => !!str)
+ .join(" \xa0 ");
+ }
+
+ get requiredFields() {
+ return BasicCardOption.recordAttributes;
+ }
+
+ render() {
+ this["_cc-name"].textContent = this.ccName || "";
+ this["_cc-number"].textContent = BasicCardOption.formatCCNumber(
+ this.ccNumber
+ );
+ // XXX Bug 1473772 - Hard-coded string:
+ this["_cc-exp"].textContent = this.ccExp ? "Exp. " + this.ccExp : "";
+ // XXX: Bug 1491040, displaying cc-type in this context may need its own localized string
+ this["_cc-type"].alt = this.ccType || "";
+ this["_cc-type"].src =
+ "chrome://formautofill/content/icon-credit-card-generic.svg";
+ }
+}
+
+customElements.define("basic-card-option", BasicCardOption);
diff --git a/browser/components/payments/res/components/card-icon.svg b/browser/components/payments/res/components/card-icon.svg
new file mode 100644
index 0000000000..1ea36d7fae
--- /dev/null
+++ b/browser/components/payments/res/components/card-icon.svg
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32">
+ <rect x="0" y="0" width="48" height="32" rx="4" ry="4" fill="#000" fill-opacity="context-fill-opacity">
+ </rect>
+ <rect x="0" y="6" width="48" height="20" fill="#fff" fill-opacity="1">
+ </rect>
+</svg>
diff --git a/browser/components/payments/res/components/csc-input.js b/browser/components/payments/res/components/csc-input.js
new file mode 100644
index 0000000000..4497f8a789
--- /dev/null
+++ b/browser/components/payments/res/components/csc-input.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+
+/**
+ * <csc-input placeholder="CVV*"
+ default-value="123"
+ front-tooltip="Look on front of card for CSC"
+ back-tooltip="Look on back of card for CSC"></csc-input>
+ */
+
+export default class CscInput extends ObservedPropertiesMixin(HTMLElement) {
+ static get observedAttributes() {
+ return [
+ "back-tooltip",
+ "card-type",
+ "default-value",
+ "disabled",
+ "front-tooltip",
+ "placeholder",
+ "value",
+ ];
+ }
+ constructor({ useAlwaysVisiblePlaceholder, inputId } = {}) {
+ super();
+
+ this.useAlwaysVisiblePlaceholder = useAlwaysVisiblePlaceholder;
+
+ this._input = document.createElement("input");
+ this._input.id = inputId || "";
+ this._input.setAttribute("type", "text");
+ this._input.autocomplete = "off";
+ this._input.size = 3;
+ this._input.required = true;
+ // 3 or more digits
+ this._input.pattern = "[0-9]{3,}";
+ this._input.classList.add("security-code");
+ if (useAlwaysVisiblePlaceholder) {
+ this._label = document.createElement("span");
+ this._label.dataset.localization = "cardCVV";
+ this._label.className = "label-text";
+ }
+ this._tooltip = document.createElement("span");
+ this._tooltip.className = "info-tooltip csc";
+ this._tooltip.setAttribute("tabindex", "0");
+ this._tooltip.setAttribute("role", "tooltip");
+
+ // The parent connectedCallback calls its render method before
+ // our connectedCallback can run. This causes issues for parent
+ // code that is looking for all the form elements. Thus, we
+ // append the children during the constructor to make sure they
+ // be part of the DOM sooner.
+ this.appendChild(this._input);
+ if (this.useAlwaysVisiblePlaceholder) {
+ this.appendChild(this._label);
+ }
+ this.appendChild(this._tooltip);
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ render() {
+ if (this.defaultValue) {
+ let oldDefaultValue = this._input.defaultValue;
+ this._input.defaultValue = this.defaultValue;
+ if (this._input.defaultValue != oldDefaultValue) {
+ // Setting defaultValue will place a value in the field
+ // but doesn't trigger a 'change' event, which is needed
+ // to update the Pay button state on the summary page.
+ this._input.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+ } else {
+ this._input.defaultValue = "";
+ }
+ if (this.value) {
+ // Setting the value will trigger form validation
+ // so only set the value if one has been provided.
+ this._input.value = this.value;
+ }
+ if (this.useAlwaysVisiblePlaceholder) {
+ this._label.textContent = this.placeholder || "";
+ } else {
+ this._input.placeholder = this.placeholder || "";
+ }
+ if (this.cardType == "amex") {
+ this._tooltip.setAttribute("aria-label", this.frontTooltip || "");
+ } else {
+ this._tooltip.setAttribute("aria-label", this.backTooltip || "");
+ }
+ }
+
+ get value() {
+ return this._input.value;
+ }
+
+ get isValid() {
+ return this._input.validity.valid;
+ }
+
+ set disabled(value) {
+ // This is kept out of render() since callers
+ // are expecting it to apply immediately.
+ this._input.disabled = value;
+ return !!value;
+ }
+}
+
+customElements.define("csc-input", CscInput);
diff --git a/browser/components/payments/res/components/currency-amount.js b/browser/components/payments/res/components/currency-amount.js
new file mode 100644
index 0000000000..5c8d07cb2d
--- /dev/null
+++ b/browser/components/payments/res/components/currency-amount.js
@@ -0,0 +1,63 @@
+/* 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/. */
+
+/**
+ * <currency-amount value="7.5" currency="USD" display-code></currency-amount>
+ */
+
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+
+export default class CurrencyAmount extends ObservedPropertiesMixin(
+ HTMLElement
+) {
+ static get observedAttributes() {
+ return ["currency", "display-code", "value"];
+ }
+
+ constructor() {
+ super();
+ this._currencyAmountTextNode = document.createTextNode("");
+ this._currencyCodeElement = document.createElement("span");
+ this._currencyCodeElement.classList.add("currency-code");
+ }
+
+ render() {
+ this.append(this._currencyAmountTextNode, this._currencyCodeElement);
+ let currencyAmount = "";
+ let currencyCode = "";
+ try {
+ if (this.value && this.currency) {
+ let number = Number.parseFloat(this.value);
+ if (Number.isNaN(number) || !Number.isFinite(number)) {
+ throw new RangeError("currency-amount value must be a finite number");
+ }
+ const symbolFormatter = new Intl.NumberFormat(navigator.languages, {
+ style: "currency",
+ currency: this.currency,
+ currencyDisplay: "symbol",
+ });
+ currencyAmount = symbolFormatter.format(this.value);
+
+ if (this.displayCode !== null) {
+ // XXX: Bug 1473772 will move the separator to a Fluent string.
+ currencyAmount += " ";
+
+ const codeFormatter = new Intl.NumberFormat(navigator.languages, {
+ style: "currency",
+ currency: this.currency,
+ currencyDisplay: "code",
+ });
+ let parts = codeFormatter.formatToParts(this.value);
+ let currencyPart = parts.find(part => part.type == "currency");
+ currencyCode = currencyPart.value;
+ }
+ }
+ } finally {
+ this._currencyAmountTextNode.textContent = currencyAmount;
+ this._currencyCodeElement.textContent = currencyCode;
+ }
+ }
+}
+
+customElements.define("currency-amount", CurrencyAmount);
diff --git a/browser/components/payments/res/components/labelled-checkbox.js b/browser/components/payments/res/components/labelled-checkbox.js
new file mode 100644
index 0000000000..f26d982f4f
--- /dev/null
+++ b/browser/components/payments/res/components/labelled-checkbox.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/. */
+
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+
+/**
+ * <labelled-checkbox label="Some label" value="The value"></labelled-checkbox>
+ */
+
+export default class LabelledCheckbox extends ObservedPropertiesMixin(
+ HTMLElement
+) {
+ static get observedAttributes() {
+ return ["infoTooltip", "form", "label", "value"];
+ }
+ constructor() {
+ super();
+
+ this._label = document.createElement("label");
+ this._labelSpan = document.createElement("span");
+ this._infoTooltip = document.createElement("span");
+ this._infoTooltip.className = "info-tooltip";
+ this._infoTooltip.setAttribute("tabindex", "0");
+ this._infoTooltip.setAttribute("role", "tooltip");
+ this._checkbox = document.createElement("input");
+ this._checkbox.type = "checkbox";
+ }
+
+ connectedCallback() {
+ this.appendChild(this._label);
+ this._label.appendChild(this._checkbox);
+ this._label.appendChild(this._labelSpan);
+ this._label.appendChild(this._infoTooltip);
+ this.render();
+ }
+
+ render() {
+ this._labelSpan.textContent = this.label;
+ this._infoTooltip.setAttribute("aria-label", this.infoTooltip);
+ // We don't use the ObservedPropertiesMixin behaviour because we want to be able to mirror
+ // form="" but ObservedPropertiesMixin removes attributes when "".
+ if (this.hasAttribute("form")) {
+ this._checkbox.setAttribute("form", this.getAttribute("form"));
+ } else {
+ this._checkbox.removeAttribute("form");
+ }
+ }
+
+ get checked() {
+ return this._checkbox.checked;
+ }
+
+ set checked(value) {
+ return (this._checkbox.checked = value);
+ }
+}
+
+customElements.define("labelled-checkbox", LabelledCheckbox);
diff --git a/browser/components/payments/res/components/payment-details-item.css b/browser/components/payments/res/components/payment-details-item.css
new file mode 100644
index 0000000000..3332698014
--- /dev/null
+++ b/browser/components/payments/res/components/payment-details-item.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+payment-details-item {
+ margin: 1px 0;
+ min-height: 2em;
+}
+
+payment-details-item > currency-amount {
+ text-align: end;
+}
diff --git a/browser/components/payments/res/components/payment-details-item.js b/browser/components/payments/res/components/payment-details-item.js
new file mode 100644
index 0000000000..c2fe31ccfd
--- /dev/null
+++ b/browser/components/payments/res/components/payment-details-item.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+/**
+ * <ul>
+ * <payment-details-item
+ label="Some item"
+ amount-value="1.00"
+ amount-currency="USD"></payment-details-item>
+ * </ul>
+ */
+
+import CurrencyAmount from "./currency-amount.js";
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+
+export default class PaymentDetailsItem extends ObservedPropertiesMixin(
+ HTMLElement
+) {
+ static get observedAttributes() {
+ return ["label", "amount-currency", "amount-value"];
+ }
+
+ constructor() {
+ super();
+ this._label = document.createElement("span");
+ this._label.classList.add("label");
+ this._currencyAmount = new CurrencyAmount();
+ }
+
+ connectedCallback() {
+ this.appendChild(this._label);
+ this.appendChild(this._currencyAmount);
+
+ if (super.connectedCallback) {
+ super.connectedCallback();
+ }
+ }
+
+ render() {
+ this._currencyAmount.value = this.amountValue;
+ this._currencyAmount.currency = this.amountCurrency;
+ this._label.textContent = this.label;
+ }
+}
+
+customElements.define("payment-details-item", PaymentDetailsItem);
diff --git a/browser/components/payments/res/components/payment-request-page.js b/browser/components/payments/res/components/payment-request-page.js
new file mode 100644
index 0000000000..76b41f178e
--- /dev/null
+++ b/browser/components/payments/res/components/payment-request-page.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/**
+ * <payment-request-page></payment-request-page>
+ */
+
+export default class PaymentRequestPage extends HTMLElement {
+ constructor() {
+ super();
+
+ this.classList.add("page");
+
+ this.pageTitleHeading = document.createElement("h2");
+
+ // The body and footer may be pre-defined in the template so re-use them if they exist.
+ this.body =
+ this.querySelector(":scope > .page-body") ||
+ document.createElement("div");
+ this.body.classList.add("page-body");
+
+ this.footer =
+ this.querySelector(":scope > footer") || document.createElement("footer");
+ }
+
+ connectedCallback() {
+ // The heading goes inside the body so it scrolls.
+ this.body.prepend(this.pageTitleHeading);
+ this.appendChild(this.body);
+
+ this.appendChild(this.footer);
+ }
+}
+
+customElements.define("payment-request-page", PaymentRequestPage);
diff --git a/browser/components/payments/res/components/rich-option.js b/browser/components/payments/res/components/rich-option.js
new file mode 100644
index 0000000000..d82697de20
--- /dev/null
+++ b/browser/components/payments/res/components/rich-option.js
@@ -0,0 +1,26 @@
+/* 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>
+ * <rich-option></rich-option>
+ * </rich-select>
+ */
+
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+
+export default class RichOption extends ObservedPropertiesMixin(HTMLElement) {
+ static get observedAttributes() {
+ return ["selected", "value"];
+ }
+
+ connectedCallback() {
+ this.classList.add("rich-option");
+ this.render();
+ }
+
+ render() {}
+}
+
+customElements.define("rich-option", RichOption);
diff --git a/browser/components/payments/res/components/rich-select.css b/browser/components/payments/res/components/rich-select.css
new file mode 100644
index 0000000000..8ec2fead3d
--- /dev/null
+++ b/browser/components/payments/res/components/rich-select.css
@@ -0,0 +1,58 @@
+/* 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 {
+ /* Include the padding in the max-width calculation so that we truncate rather
+ than grow wider than 100% of the parent. */
+ box-sizing: border-box;
+ display: block;
+ /* Has to be the same as `payment-method-picker > input`: */
+ margin: 10px 0;
+ /* Padding for the dropmarker (copied from common.css) */
+ padding-inline-end: 24px;
+ position: relative;
+ /* Don't allow the <rich-select> to grow wider than the container so that we
+ truncate with text-overflow for long options instead. */
+ max-width: 100%;
+}
+
+/* Focusing on the underlying select element outlines the outer
+ rich-select wrapper making it appear like rich-select is focused. */
+rich-select:focus-within > select {
+ outline: 1px dotted var(--in-content-text-color);
+}
+
+/*
+ * The HTML select element is hidden and placed on the rich-option
+ * element to make it look like clicking on the rich-option element
+ * in the closed state opens the HTML select dropdown. */
+rich-select > select {
+ /* Hide the text from the closed state so that the text/layout from
+ <rich-option> won't overlap it. The !important matches common.css. */
+ color: transparent !important;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+rich-select > select > option {
+ /* Reset the text color in the popup/open state */
+ color: var(--in-content-text-color);
+}
+
+.rich-option {
+ display: grid;
+ padding: 8px;
+}
+
+.rich-select-selected-option {
+ /* Clicks on the selected rich option should go to the <select> below to open the popup */
+ pointer-events: none;
+ /* Use position:relative so this is positioned on top of the <select> which
+ also has position:relative. */
+ position: relative;
+}
diff --git a/browser/components/payments/res/components/rich-select.js b/browser/components/payments/res/components/rich-select.js
new file mode 100644
index 0000000000..0226b32a79
--- /dev/null
+++ b/browser/components/payments/res/components/rich-select.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+import RichOption from "./rich-option.js";
+
+/**
+ * <rich-select>
+ * <rich-option></rich-option>
+ * </rich-select>
+ *
+ * Note: The only supported way to change the selected option is via the
+ * `value` setter.
+ */
+export default class RichSelect extends HandleEventMixin(
+ ObservedPropertiesMixin(HTMLElement)
+) {
+ static get observedAttributes() {
+ return ["disabled", "hidden"];
+ }
+
+ constructor() {
+ super();
+ this.popupBox = document.createElement("select");
+ }
+
+ connectedCallback() {
+ // the popupBox element may change in between constructor and being connected
+ // so wait until connected before listening to events on it
+ this.popupBox.addEventListener("change", this);
+ this.appendChild(this.popupBox);
+ this.render();
+ }
+
+ get selectedOption() {
+ return this.getOptionByValue(this.value);
+ }
+
+ get selectedRichOption() {
+ // XXX: Bug 1475684 - This can be removed once `selectedOption` returns a
+ // RichOption which extends HTMLOptionElement.
+ return this.querySelector(":scope > .rich-select-selected-option");
+ }
+
+ get value() {
+ return this.popupBox.value;
+ }
+
+ set value(guid) {
+ this.popupBox.value = guid;
+ this.render();
+ }
+
+ getOptionByValue(value) {
+ return this.popupBox.querySelector(
+ `:scope > [value="${CSS.escape(value)}"]`
+ );
+ }
+
+ onChange(event) {
+ // Since the render function depends on the popupBox's value, we need to
+ // re-render if the value changes.
+ this.render();
+ }
+
+ render() {
+ let selectedRichOption = this.querySelector(
+ ":scope > .rich-select-selected-option"
+ );
+ if (selectedRichOption) {
+ selectedRichOption.remove();
+ }
+
+ if (this.value) {
+ let optionType = this.getAttribute("option-type");
+ if (!selectedRichOption || selectedRichOption.localName != optionType) {
+ selectedRichOption = document.createElement(optionType);
+ }
+
+ let option = this.getOptionByValue(this.value);
+ let attributeNames = selectedRichOption.constructor.observedAttributes;
+ for (let attributeName of attributeNames) {
+ let attributeValue = option.getAttribute(attributeName);
+ if (attributeValue) {
+ selectedRichOption.setAttribute(attributeName, attributeValue);
+ } else {
+ selectedRichOption.removeAttribute(attributeName);
+ }
+ }
+ } else {
+ selectedRichOption = new RichOption();
+ selectedRichOption.textContent = "(None selected)"; // XXX: bug 1473772
+ }
+ selectedRichOption.classList.add("rich-select-selected-option");
+ // Hide the rich-option from a11y tools since the native <select> will
+ // already provide the selected option label.
+ selectedRichOption.setAttribute("aria-hidden", "true");
+ selectedRichOption = this.appendChild(selectedRichOption);
+ }
+}
+
+customElements.define("rich-select", RichSelect);
diff --git a/browser/components/payments/res/components/shipping-option.css b/browser/components/payments/res/components/shipping-option.css
new file mode 100644
index 0000000000..6c84c5eb84
--- /dev/null
+++ b/browser/components/payments/res/components/shipping-option.css
@@ -0,0 +1,16 @@
+/* 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/. */
+
+shipping-option.rich-option {
+ display: block;
+ /* Below properties are to support truncating with an ellipsis for long options */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+shipping-option > .label,
+shipping-option > .amount {
+ white-space: nowrap;
+}
diff --git a/browser/components/payments/res/components/shipping-option.js b/browser/components/payments/res/components/shipping-option.js
new file mode 100644
index 0000000000..1832546463
--- /dev/null
+++ b/browser/components/payments/res/components/shipping-option.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import CurrencyAmount from "./currency-amount.js";
+import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
+import RichOption from "./rich-option.js";
+
+/**
+ * <rich-select>
+ * <shipping-option></shipping-option>
+ * </rich-select>
+ */
+
+export default class ShippingOption extends ObservedPropertiesMixin(
+ RichOption
+) {
+ static get recordAttributes() {
+ return ["label", "amount-currency", "amount-value"];
+ }
+
+ static get observedAttributes() {
+ return RichOption.observedAttributes.concat(
+ ShippingOption.recordAttributes
+ );
+ }
+
+ constructor() {
+ super();
+
+ this.amount = null;
+ this._currencyAmount = new CurrencyAmount();
+ this._currencyAmount.classList.add("amount");
+ this._label = document.createElement("span");
+ this._label.classList.add("label");
+ }
+
+ connectedCallback() {
+ this.appendChild(this._currencyAmount);
+ this.append(" ");
+ this.appendChild(this._label);
+ super.connectedCallback();
+ }
+
+ static formatSingleLineLabel(option) {
+ let amount = new CurrencyAmount();
+ amount.value = option.amount.value;
+ amount.currency = option.amount.currency;
+ amount.render();
+
+ return amount.textContent + " " + option.label;
+ }
+
+ render() {
+ this._label.textContent = this.label;
+ this._currencyAmount.currency = this.amountCurrency;
+ this._currencyAmount.value = this.amountValue;
+ // Need to call render after setting these properties
+ // if we want the amount to get displayed in the same
+ // render pass as the label.
+ this._currencyAmount.render();
+ }
+}
+
+customElements.define("shipping-option", ShippingOption);
diff --git a/browser/components/payments/res/containers/address-form.css b/browser/components/payments/res/containers/address-form.css
new file mode 100644
index 0000000000..484610414c
--- /dev/null
+++ b/browser/components/payments/res/containers/address-form.css
@@ -0,0 +1,55 @@
+/* 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/. */
+
+.error-text {
+ color: #fff;
+ background-color: #d70022;
+ border-radius: 2px;
+ margin: 5px 3px 0 3px;
+ /* The padding-top and padding-bottom are referenced by address-form.js */ /* TODO */
+ padding: 5px 12px;
+ position: absolute;
+ z-index: 1;
+ pointer-events: none;
+ top: 100%;
+ visibility: hidden;
+}
+
+/* ::before is the error on the error text panel */
+:is(input, textarea, select) ~ .error-text::before {
+ background-color: #d70022;
+ top: -7px;
+ content: '.';
+ height: 16px;
+ position: absolute;
+ text-indent: -999px;
+ transform: rotate(45deg);
+ white-space: nowrap;
+ width: 16px;
+ z-index: -1
+}
+
+/* Position the arrow */
+.error-text:dir(ltr)::before {
+ left: 12px
+}
+
+.error-text:dir(rtl)::before {
+ right: 12px
+}
+
+:is(input, textarea, select):-moz-ui-invalid:focus ~ .error-text {
+ visibility: visible;
+}
+
+address-form > footer > .cancel-button {
+ /* When cancel is shown (during onboarding), it should always be on the left with a space after it */
+ margin-right: auto;
+}
+
+address-form > footer > .back-button {
+ /* When back is shown (outside onboarding) we want "Back <space> Add/Save" */
+ /* Bug 1468153 may change the button ordering to match platform conventions */
+ margin-right: auto;
+}
diff --git a/browser/components/payments/res/containers/address-form.js b/browser/components/payments/res/containers/address-form.js
new file mode 100644
index 0000000000..287251ea7d
--- /dev/null
+++ b/browser/components/payments/res/containers/address-form.js
@@ -0,0 +1,447 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import LabelledCheckbox from "../components/labelled-checkbox.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <address-form></address-form>
+ *
+ * Don't use document.getElementById or document.querySelector* to access form
+ * elements, use querySelector on `this` or `this.form` instead so that elements
+ * can be found before the element is connected.
+ *
+ * XXX: Bug 1446164 - This form isn't localized when used via this custom element
+ * as it will be much easier to share the logic once we switch to Fluent.
+ */
+
+export default class AddressForm extends HandleEventMixin(
+ PaymentStateSubscriberMixin(PaymentRequestPage)
+) {
+ constructor() {
+ super();
+
+ this.genericErrorText = document.createElement("div");
+ this.genericErrorText.setAttribute("aria-live", "polite");
+ this.genericErrorText.classList.add("page-error");
+
+ this.cancelButton = document.createElement("button");
+ this.cancelButton.className = "cancel-button";
+ this.cancelButton.addEventListener("click", this);
+
+ this.backButton = document.createElement("button");
+ this.backButton.className = "back-button";
+ this.backButton.addEventListener("click", this);
+
+ this.saveButton = document.createElement("button");
+ this.saveButton.className = "save-button primary";
+ this.saveButton.addEventListener("click", this);
+
+ this.persistCheckbox = new LabelledCheckbox();
+ this.persistCheckbox.className = "persist-checkbox";
+
+ // Combination of AddressErrors and PayerErrors as keys
+ this._errorFieldMap = {
+ addressLine: "#street-address",
+ city: "#address-level2",
+ country: "#country",
+ dependentLocality: "#address-level3",
+ email: "#email",
+ // Bug 1472283 is on file to support
+ // additional-name and family-name.
+ // XXX: For now payer name errors go on the family-name and address-errors
+ // go on the given-name so they don't overwrite each other.
+ name: "#family-name",
+ organization: "#organization",
+ phone: "#tel",
+ postalCode: "#postal-code",
+ // Bug 1472283 is on file to support
+ // additional-name and family-name.
+ recipient: "#given-name",
+ region: "#address-level1",
+ // Bug 1474905 is on file to properly support regionCode. See
+ // full note in paymentDialogWrapper.js
+ regionCode: "#address-level1",
+ };
+
+ // The markup is shared with form autofill preferences.
+ let url = "formautofill/editAddress.xhtml";
+ this.promiseReady = this._fetchMarkup(url).then(doc => {
+ this.form = doc.getElementById("form");
+ return this.form;
+ });
+ }
+
+ _fetchMarkup(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.addEventListener("error", reject);
+ xhr.addEventListener("load", evt => {
+ resolve(xhr.response);
+ });
+ xhr.open("GET", url);
+ xhr.send();
+ });
+ }
+
+ connectedCallback() {
+ this.promiseReady.then(form => {
+ this.body.appendChild(form);
+
+ let record = undefined;
+ this.formHandler = new EditAddress(
+ {
+ form,
+ },
+ record,
+ {
+ DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
+ getFormFormat: PaymentDialogUtils.getFormFormat,
+ findAddressSelectOption: PaymentDialogUtils.findAddressSelectOption,
+ countries: PaymentDialogUtils.countries,
+ }
+ );
+
+ // The EditAddress constructor adds `input` event listeners on the same element,
+ // which update field validity. By adding our event listeners after this constructor,
+ // validity will be updated before our handlers get the event
+ this.form.addEventListener("input", this);
+ this.form.addEventListener("invalid", this);
+ this.form.addEventListener("change", this);
+
+ // The "invalid" event does not bubble and needs to be listened for on each
+ // form element.
+ for (let field of this.form.elements) {
+ field.addEventListener("invalid", this);
+ }
+
+ this.body.appendChild(this.persistCheckbox);
+ this.body.appendChild(this.genericErrorText);
+
+ this.footer.appendChild(this.cancelButton);
+ this.footer.appendChild(this.backButton);
+ this.footer.appendChild(this.saveButton);
+ // Only call the connected super callback(s) once our markup is fully
+ // connected, including the shared form fetched asynchronously.
+ super.connectedCallback();
+ });
+ }
+
+ render(state) {
+ if (!this.id) {
+ throw new Error("AddressForm without an id");
+ }
+ let record;
+ let { page, [this.id]: addressPage } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`${this.id}: no need to further render inactive page`);
+ return;
+ }
+
+ let editing = !!addressPage.guid;
+ this.cancelButton.textContent = this.dataset.cancelButtonLabel;
+ this.backButton.textContent = this.dataset.backButtonLabel;
+ if (editing) {
+ this.saveButton.textContent = this.dataset.updateButtonLabel;
+ } else {
+ this.saveButton.textContent = this.dataset.nextButtonLabel;
+ }
+
+ this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
+ this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
+
+ this.backButton.hidden = page.onboardingWizard;
+ this.cancelButton.hidden = !page.onboardingWizard;
+
+ this.pageTitleHeading.textContent = editing
+ ? this.dataset.titleEdit
+ : this.dataset.titleAdd;
+ this.genericErrorText.textContent = page.error;
+
+ let addresses = paymentRequest.getAddresses(state);
+
+ // If an address is selected we want to edit it.
+ if (editing) {
+ record = addresses[addressPage.guid];
+ if (!record) {
+ throw new Error(
+ "Trying to edit a non-existing address: " + addressPage.guid
+ );
+ }
+ // When editing an existing record, prevent changes to persistence
+ this.persistCheckbox.hidden = true;
+ } else {
+ let {
+ saveAddressDefaultChecked,
+ } = PaymentDialogUtils.getDefaultPreferences();
+ if (typeof saveAddressDefaultChecked != "boolean") {
+ throw new Error(`Unexpected non-boolean value for saveAddressDefaultChecked from
+ PaymentDialogUtils.getDefaultPreferences(): ${typeof saveAddressDefaultChecked}`);
+ }
+ // Adding a new record: default persistence to the pref value when in a not-private session
+ this.persistCheckbox.hidden = false;
+ this.persistCheckbox.checked = state.isPrivate
+ ? false
+ : saveAddressDefaultChecked;
+ }
+
+ let selectedStateKey = this.getAttribute("selected-state-key").split("|");
+ log.debug(`${this.id}#render got selectedStateKey: ${selectedStateKey}`);
+
+ if (addressPage.addressFields) {
+ this.form.dataset.addressFields = addressPage.addressFields;
+ } else {
+ this.form.dataset.addressFields = "mailing-address tel";
+ }
+ this.formHandler.loadRecord(record);
+
+ // Add validation to some address fields
+ this.updateRequiredState();
+
+ // Show merchant errors for the appropriate address form.
+ let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
+ state,
+ selectedStateKey
+ );
+ for (let [errorName, errorSelector] of Object.entries(
+ this._errorFieldMap
+ )) {
+ let errorText = "";
+ // Never show errors on an 'add' screen as they would be for a different address.
+ if (editing && merchantFieldErrors) {
+ if (errorName == "region" || errorName == "regionCode") {
+ errorText =
+ merchantFieldErrors.regionCode || merchantFieldErrors.region || "";
+ } else {
+ errorText = merchantFieldErrors[errorName] || "";
+ }
+ }
+ let container = this.form.querySelector(errorSelector + "-container");
+ let field = this.form.querySelector(errorSelector);
+ field.setCustomValidity(errorText);
+ let span = paymentRequest.maybeCreateFieldErrorElement(container);
+ span.textContent = errorText;
+ }
+
+ this.updateSaveButtonState();
+ }
+
+ onChange(event) {
+ if (event.target.id == "country") {
+ this.updateRequiredState();
+ }
+ this.updateSaveButtonState();
+ }
+
+ onInvalid(event) {
+ if (event.target instanceof HTMLFormElement) {
+ this.onInvalidForm(event);
+ } else {
+ this.onInvalidField(event);
+ }
+ }
+
+ onClick(evt) {
+ switch (evt.target) {
+ case this.cancelButton: {
+ paymentRequest.cancel();
+ break;
+ }
+ case this.backButton: {
+ let currentState = this.requestStore.getState();
+ const previousId = currentState.page.previousId;
+ let state = {
+ page: {
+ id: previousId || "payment-summary",
+ },
+ };
+ if (previousId) {
+ state[previousId] = Object.assign({}, currentState[previousId], {
+ preserveFieldValues: true,
+ });
+ }
+ this.requestStore.setState(state);
+ break;
+ }
+ case this.saveButton: {
+ if (this.form.checkValidity()) {
+ this.saveRecord();
+ }
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+
+ onInput(event) {
+ event.target.setCustomValidity("");
+ this.updateSaveButtonState();
+ }
+
+ /**
+ * @param {Event} event - "invalid" event
+ * Note: Keep this in-sync with the equivalent version in basic-card-form.js
+ */
+ onInvalidField(event) {
+ let field = event.target;
+ let container = field.closest(`#${field.id}-container`);
+ let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
+ errorTextSpan.textContent = field.validationMessage;
+ }
+
+ onInvalidForm() {
+ this.saveButton.disabled = true;
+ }
+
+ updateRequiredState() {
+ for (let field of this.form.elements) {
+ let container = field.closest(`#${field.id}-container`);
+ if (field.localName == "button" || !container) {
+ continue;
+ }
+ let span = container.querySelector(".label-text");
+ span.setAttribute(
+ "fieldRequiredSymbol",
+ this.dataset.fieldRequiredSymbol
+ );
+ container.toggleAttribute("required", field.required && !field.disabled);
+ }
+ }
+
+ updateSaveButtonState() {
+ this.saveButton.disabled = !this.form.checkValidity();
+ }
+
+ async saveRecord() {
+ let record = this.formHandler.buildFormObject();
+ let currentState = this.requestStore.getState();
+ let {
+ page,
+ tempAddresses,
+ savedBasicCards,
+ [this.id]: addressPage,
+ } = currentState;
+ let editing = !!addressPage.guid;
+
+ if (
+ editing
+ ? addressPage.guid in tempAddresses
+ : !this.persistCheckbox.checked
+ ) {
+ record.isTemporary = true;
+ }
+
+ let successStateChange;
+ const previousId = page.previousId;
+ if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
+ successStateChange = {
+ "basic-card-page": {
+ selectedStateKey: "selectedPaymentCard",
+ // Preserve field values as the user may have already edited the card
+ // page and went back to the address page to make a correction.
+ preserveFieldValues: true,
+ },
+ page: {
+ id: "basic-card-page",
+ previousId: this.id,
+ onboardingWizard: page.onboardingWizard,
+ },
+ };
+ } else {
+ successStateChange = {
+ page: {
+ id: previousId || "payment-summary",
+ onboardingWizard: page.onboardingWizard,
+ },
+ };
+ }
+
+ if (previousId) {
+ successStateChange[previousId] = Object.assign(
+ {},
+ currentState[previousId]
+ );
+ successStateChange[previousId].preserveFieldValues = true;
+ }
+
+ try {
+ let { guid } = await paymentRequest.updateAutofillRecord(
+ "addresses",
+ record,
+ addressPage.guid
+ );
+ let selectedStateKey = this.getAttribute("selected-state-key").split("|");
+
+ if (selectedStateKey.length == 1) {
+ Object.assign(successStateChange, {
+ [selectedStateKey[0]]: guid,
+ });
+ } else if (selectedStateKey.length == 2) {
+ // Need to keep properties like preserveFieldValues from getting removed.
+ let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
+ subObj[selectedStateKey[1]] = guid;
+ Object.assign(successStateChange, {
+ [selectedStateKey[0]]: subObj,
+ });
+ } else {
+ throw new Error(
+ `selectedStateKey not supported: '${selectedStateKey}'`
+ );
+ }
+
+ this.requestStore.setState(successStateChange);
+ } catch (ex) {
+ log.warn("saveRecord: error:", ex);
+ this.requestStore.setState({
+ page: {
+ id: this.id,
+ onboardingWizard: page.onboardingWizard,
+ error: this.dataset.errorGenericSave,
+ },
+ });
+ }
+ }
+
+ /**
+ * Get the dictionary of field-specific merchant errors relevant to the
+ * specific form identified by the state key.
+ * @param {object} state The application state
+ * @param {string[]} stateKey The key in state to return address errors for.
+ * @returns {object} with keys as PaymentRequest field names and values of
+ * merchant-provided error strings.
+ */
+ static merchantFieldErrorsForForm(state, stateKey) {
+ let { paymentDetails } = state.request;
+ switch (stateKey.join("|")) {
+ case "selectedShippingAddress": {
+ return paymentDetails.shippingAddressErrors;
+ }
+ case "selectedPayerAddress": {
+ return paymentDetails.payerErrors;
+ }
+ case "basic-card-page|billingAddressGUID": {
+ // `paymentMethod` can be null.
+ return (
+ (paymentDetails.paymentMethodErrors &&
+ paymentDetails.paymentMethodErrors.billingAddress) ||
+ {}
+ );
+ }
+ default: {
+ throw new Error("Unknown selectedStateKey");
+ }
+ }
+ }
+}
+
+customElements.define("address-form", AddressForm);
diff --git a/browser/components/payments/res/containers/address-picker.js b/browser/components/payments/res/containers/address-picker.js
new file mode 100644
index 0000000000..b76a0f5d02
--- /dev/null
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import AddressForm from "./address-form.js";
+import AddressOption from "../components/address-option.js";
+import RichPicker from "./rich-picker.js";
+import paymentRequest from "../paymentRequest.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+
+/**
+ * <address-picker></address-picker>
+ * Container around add/edit links and <rich-select> with
+ * <address-option> listening to savedAddresses & tempAddresses.
+ */
+
+export default class AddressPicker extends HandleEventMixin(RichPicker) {
+ static get pickerAttributes() {
+ return ["address-fields", "break-after-nth-field", "data-field-separator"];
+ }
+
+ static get observedAttributes() {
+ return RichPicker.observedAttributes.concat(AddressPicker.pickerAttributes);
+ }
+
+ constructor() {
+ super();
+ this.dropdown.setAttribute("option-type", "address-option");
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ super.attributeChangedCallback(name, oldValue, newValue);
+ // connectedCallback may add and adjust elements & values
+ // so avoid calling render before the element is connected
+ if (
+ this.isConnected &&
+ AddressPicker.pickerAttributes.includes(name) &&
+ oldValue !== newValue
+ ) {
+ this.render(this.requestStore.getState());
+ }
+ }
+
+ get fieldNames() {
+ if (this.hasAttribute("address-fields")) {
+ let names = this.getAttribute("address-fields")
+ .trim()
+ .split(/\s+/);
+ if (names.length) {
+ return names;
+ }
+ }
+
+ return [
+ // "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE
+ "address-level2",
+ "country",
+ "name",
+ "postal-code",
+ "street-address",
+ ];
+ }
+
+ /**
+ * De-dupe and filter addresses for the given set of fields that will be visible
+ *
+ * @param {object} addresses
+ * @param {array?} fieldNames - optional list of field names that be used when
+ * de-duping and excluding entries
+ * @returns {object} filtered copy of given addresses
+ */
+ filterAddresses(addresses, fieldNames = this.fieldNames) {
+ let uniques = new Set();
+ let result = {};
+ for (let [guid, address] of Object.entries(addresses)) {
+ let addressCopy = {};
+ let isMatch = false;
+ // exclude addresses that are missing all of the requested fields
+ for (let name of fieldNames) {
+ if (address[name]) {
+ isMatch = true;
+ addressCopy[name] = address[name];
+ }
+ }
+ if (isMatch) {
+ let key = JSON.stringify(addressCopy);
+ // exclude duplicated addresses
+ if (!uniques.has(key)) {
+ uniques.add(key);
+ result[guid] = address;
+ }
+ }
+ }
+ return result;
+ }
+
+ get options() {
+ return this.dropdown.popupBox.options;
+ }
+
+ /**
+ * @param {object} state - See `PaymentsStore.setState`
+ * The value of the picker is retrieved from state store rather than the DOM
+ * @returns {string} guid
+ */
+ getCurrentValue(state) {
+ let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
+ let guid = state[selectedKey];
+ if (selectedLeaf) {
+ guid = guid[selectedLeaf];
+ }
+ return guid;
+ }
+
+ render(state) {
+ let selectedAddressGUID = this.getCurrentValue(state) || "";
+ let addresses = paymentRequest.getAddresses(state);
+ let desiredOptions = [];
+ let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
+ for (let [guid, address] of Object.entries(filteredAddresses)) {
+ let optionEl = this.dropdown.getOptionByValue(guid);
+ if (!optionEl) {
+ optionEl = document.createElement("option");
+ optionEl.value = guid;
+ }
+
+ for (let key of AddressOption.recordAttributes) {
+ let val = address[key];
+ if (val) {
+ optionEl.setAttribute(key, val);
+ } else {
+ optionEl.removeAttribute(key);
+ }
+ }
+
+ optionEl.dataset.fieldSeparator = this.dataset.fieldSeparator;
+
+ if (this.hasAttribute("address-fields")) {
+ optionEl.setAttribute(
+ "address-fields",
+ this.getAttribute("address-fields")
+ );
+ } else {
+ optionEl.removeAttribute("address-fields");
+ }
+
+ if (this.hasAttribute("break-after-nth-field")) {
+ optionEl.setAttribute(
+ "break-after-nth-field",
+ this.getAttribute("break-after-nth-field")
+ );
+ } else {
+ optionEl.removeAttribute("break-after-nth-field");
+ }
+
+ // fieldNames getter is not used here because it returns a default array with
+ // attributes even when "address-fields" observed attribute is null.
+ let addressFields = this.getAttribute("address-fields");
+ optionEl.textContent = AddressOption.formatSingleLineLabel(
+ address,
+ addressFields
+ );
+ desiredOptions.push(optionEl);
+ }
+
+ this.dropdown.popupBox.textContent = "";
+
+ if (this._allowEmptyOption) {
+ let optionEl = document.createElement("option");
+ optionEl.value = "";
+ desiredOptions.unshift(optionEl);
+ }
+
+ for (let option of desiredOptions) {
+ this.dropdown.popupBox.appendChild(option);
+ }
+
+ // Update selectedness after the options are updated
+ this.dropdown.value = selectedAddressGUID;
+
+ if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
+ throw new Error(
+ `${this.selectedStateKey} option ${selectedAddressGUID} ` +
+ `does not exist in the address picker`
+ );
+ }
+
+ super.render(state);
+ }
+
+ get selectedStateKey() {
+ return this.getAttribute("selected-state-key");
+ }
+
+ errorForSelectedOption(state) {
+ let superError = super.errorForSelectedOption(state);
+ if (superError) {
+ return superError;
+ }
+
+ if (!this.selectedOption) {
+ return "";
+ }
+
+ let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
+ state,
+ this.selectedStateKey.split("|")
+ );
+ // TODO: errors in priority order.
+ return (
+ Object.values(merchantFieldErrors).find(msg => {
+ return typeof msg == "string" && msg.length;
+ }) || ""
+ );
+ }
+
+ onChange(event) {
+ let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
+ if (!selectedKey) {
+ return;
+ }
+ // selectedStateKey can be a '|' delimited string indicating a path into the state object
+ // to update with the new value
+ let newState = {};
+
+ if (selectedLeaf) {
+ let currentState = this.requestStore.getState();
+ newState[selectedKey] = Object.assign({}, currentState[selectedKey], {
+ [selectedLeaf]: this.dropdown.value,
+ });
+ } else {
+ newState[selectedKey] = this.dropdown.value;
+ }
+ this.requestStore.setState(newState);
+ }
+
+ onClick({ target }) {
+ let pageId;
+ let currentState = this.requestStore.getState();
+ let nextState = {
+ page: {},
+ };
+
+ switch (this.selectedStateKey) {
+ case "selectedShippingAddress":
+ pageId = "shipping-address-page";
+ break;
+ case "selectedPayerAddress":
+ pageId = "payer-address-page";
+ break;
+ case "basic-card-page|billingAddressGUID":
+ pageId = "billing-address-page";
+ break;
+ default: {
+ throw new Error(
+ "onClick, un-matched selectedStateKey: " + this.selectedStateKey
+ );
+ }
+ }
+ nextState.page.id = pageId;
+ let addressFields = this.getAttribute("address-fields");
+ nextState[pageId] = { addressFields };
+
+ switch (target) {
+ case this.addLink: {
+ nextState[pageId].guid = null;
+ break;
+ }
+ case this.editLink: {
+ nextState[pageId].guid = this.getCurrentValue(currentState);
+ break;
+ }
+ default: {
+ throw new Error("Unexpected onClick");
+ }
+ }
+
+ this.requestStore.setState(nextState);
+ }
+}
+
+customElements.define("address-picker", AddressPicker);
diff --git a/browser/components/payments/res/containers/basic-card-form.css b/browser/components/payments/res/containers/basic-card-form.css
new file mode 100644
index 0000000000..f4a8721e03
--- /dev/null
+++ b/browser/components/payments/res/containers/basic-card-form.css
@@ -0,0 +1,43 @@
+/* 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/. */
+
+basic-card-form .editCreditCardForm {
+ /* Add the persist-checkbox row to the grid */
+ grid-template-areas:
+ "cc-number cc-exp-month cc-exp-year"
+ "cc-name cc-type cc-csc"
+ "accepted accepted accepted"
+ "persist-checkbox persist-checkbox persist-checkbox"
+ "billingAddressGUID billingAddressGUID billingAddressGUID";
+}
+
+basic-card-form csc-input {
+ display: flex;
+ flex-grow: 1;
+}
+
+basic-card-form .editCreditCardForm > accepted-cards {
+ grid-area: accepted;
+ margin: 0;
+}
+
+basic-card-form .editCreditCardForm .persist-checkbox {
+ display: flex;
+ grid-area: persist-checkbox;
+}
+
+#billingAddressGUID-container {
+ display: grid;
+}
+
+basic-card-form > footer > .cancel-button {
+ /* When cancel is shown (during onboarding), it should always be on the left with a space after it */
+ margin-right: auto;
+}
+
+basic-card-form > footer > .cancel-button[hidden] ~ .back-button {
+ /* When back is shown (outside onboarding) we want "Back <space> Add/Save" */
+ /* Bug 1468153 may change the button ordering to match platform conventions */
+ margin-right: auto;
+}
diff --git a/browser/components/payments/res/containers/basic-card-form.js b/browser/components/payments/res/containers/basic-card-form.js
new file mode 100644
index 0000000000..f71b7fc74c
--- /dev/null
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -0,0 +1,507 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import AcceptedCards from "../components/accepted-cards.js";
+import BillingAddressPicker from "./billing-address-picker.js";
+import CscInput from "../components/csc-input.js";
+import LabelledCheckbox from "../components/labelled-checkbox.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <basic-card-form></basic-card-form>
+ *
+ * XXX: Bug 1446164 - This form isn't localized when used via this custom element
+ * as it will be much easier to share the logic once we switch to Fluent.
+ */
+
+export default class BasicCardForm extends HandleEventMixin(
+ PaymentStateSubscriberMixin(PaymentRequestPage)
+) {
+ constructor() {
+ super();
+
+ this.genericErrorText = document.createElement("div");
+ this.genericErrorText.setAttribute("aria-live", "polite");
+ this.genericErrorText.classList.add("page-error");
+
+ this.cscInput = new CscInput({
+ useAlwaysVisiblePlaceholder: true,
+ inputId: "cc-csc",
+ });
+
+ this.persistCheckbox = new LabelledCheckbox();
+ // The persist checkbox shouldn't be part of the record which gets saved so
+ // exclude it from the form.
+ this.persistCheckbox.form = "";
+ this.persistCheckbox.className = "persist-checkbox";
+
+ this.acceptedCardsList = new AcceptedCards();
+
+ // page footer
+ this.cancelButton = document.createElement("button");
+ this.cancelButton.className = "cancel-button";
+ this.cancelButton.addEventListener("click", this);
+
+ this.backButton = document.createElement("button");
+ this.backButton.className = "back-button";
+ this.backButton.addEventListener("click", this);
+
+ this.saveButton = document.createElement("button");
+ this.saveButton.className = "save-button primary";
+ this.saveButton.addEventListener("click", this);
+
+ this.footer.append(this.cancelButton, this.backButton, this.saveButton);
+
+ // The markup is shared with form autofill preferences.
+ let url = "formautofill/editCreditCard.xhtml";
+ this.promiseReady = this._fetchMarkup(url).then(doc => {
+ this.form = doc.getElementById("form");
+ return this.form;
+ });
+ }
+
+ _fetchMarkup(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.addEventListener("error", reject);
+ xhr.addEventListener("load", evt => {
+ resolve(xhr.response);
+ });
+ xhr.open("GET", url);
+ xhr.send();
+ });
+ }
+
+ _upgradeBillingAddressPicker() {
+ let addressRow = this.form.querySelector(".billingAddressRow");
+ let addressPicker = (this.billingAddressPicker = new BillingAddressPicker());
+
+ // Wrap the existing <select> that the formHandler manages
+ if (addressPicker.dropdown.popupBox) {
+ addressPicker.dropdown.popupBox.remove();
+ }
+ addressPicker.dropdown.popupBox = this.form.querySelector(
+ "#billingAddressGUID"
+ );
+
+ // Hide the original label as the address picker provide its own,
+ // but we'll copy the localized textContent from it when rendering
+ addressRow.querySelector(".label-text").hidden = true;
+
+ addressPicker.dataset.addLinkLabel = this.dataset.addressAddLinkLabel;
+ addressPicker.dataset.editLinkLabel = this.dataset.addressEditLinkLabel;
+ addressPicker.dataset.fieldSeparator = this.dataset.addressFieldSeparator;
+ addressPicker.dataset.addAddressTitle = this.dataset.billingAddressTitleAdd;
+ addressPicker.dataset.editAddressTitle = this.dataset.billingAddressTitleEdit;
+ addressPicker.dataset.invalidLabel = this.dataset.invalidAddressLabel;
+ // break-after-nth-field, address-fields not needed here
+
+ // this state is only used to carry the selected guid between pages;
+ // the select#billingAddressGUID is the source of truth for the current value
+ addressPicker.setAttribute(
+ "selected-state-key",
+ "basic-card-page|billingAddressGUID"
+ );
+
+ addressPicker.addLink.addEventListener("click", this);
+ addressPicker.editLink.addEventListener("click", this);
+
+ addressRow.appendChild(addressPicker);
+ }
+
+ connectedCallback() {
+ this.promiseReady.then(form => {
+ this.body.appendChild(form);
+
+ let record = {};
+ let addresses = [];
+ this.formHandler = new EditCreditCard(
+ {
+ form,
+ },
+ record,
+ addresses,
+ {
+ isCCNumber: PaymentDialogUtils.isCCNumber,
+ getAddressLabel: PaymentDialogUtils.getAddressLabel,
+ getSupportedNetworks: PaymentDialogUtils.getCreditCardNetworks,
+ }
+ );
+
+ // The EditCreditCard constructor adds `change` and `input` event listeners on the same
+ // element, which update field validity. By adding our event listeners after this
+ // constructor, validity will be updated before our handlers get the event
+ form.addEventListener("change", this);
+ form.addEventListener("input", this);
+ form.addEventListener("invalid", this);
+
+ this._upgradeBillingAddressPicker();
+
+ // The "invalid" event does not bubble and needs to be listened for on each
+ // form element.
+ for (let field of this.form.elements) {
+ field.addEventListener("invalid", this);
+ }
+
+ // Replace the form-autofill cc-csc fields with our csc-input.
+ let cscContainer = this.form.querySelector("#cc-csc-container");
+ cscContainer.textContent = "";
+ cscContainer.appendChild(this.cscInput);
+
+ let billingAddressRow = this.form.querySelector(".billingAddressRow");
+ form.insertBefore(this.persistCheckbox, billingAddressRow);
+ form.insertBefore(this.acceptedCardsList, billingAddressRow);
+ this.body.appendChild(this.genericErrorText);
+ // Only call the connected super callback(s) once our markup is fully
+ // connected, including the shared form fetched asynchronously.
+ super.connectedCallback();
+ });
+ }
+
+ render(state) {
+ let {
+ page,
+ selectedShippingAddress,
+ "basic-card-page": basicCardPage,
+ } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(
+ `BasicCardForm: no need to further render inactive page: ${page.id}`
+ );
+ return;
+ }
+
+ if (!basicCardPage.selectedStateKey) {
+ throw new Error("A `selectedStateKey` is required");
+ }
+
+ let editing = !!basicCardPage.guid;
+ this.cancelButton.textContent = this.dataset.cancelButtonLabel;
+ this.backButton.textContent = this.dataset.backButtonLabel;
+ if (editing) {
+ this.saveButton.textContent = this.dataset.updateButtonLabel;
+ } else {
+ this.saveButton.textContent = this.dataset.nextButtonLabel;
+ }
+
+ this.cscInput.placeholder = this.dataset.cscPlaceholder;
+ this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip;
+ this.cscInput.backTooltip = this.dataset.cscBackInfoTooltip;
+
+ // The label text from the form isn't available until render() time.
+ let labelText = this.form.querySelector(".billingAddressRow .label-text")
+ .textContent;
+ this.billingAddressPicker.setAttribute("label", labelText);
+
+ this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
+ this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
+
+ this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
+
+ // The next line needs an onboarding check since we don't set previousId
+ // when navigating to add/edit directly from the summary page.
+ this.backButton.hidden = !page.previousId && page.onboardingWizard;
+ this.cancelButton.hidden = !page.onboardingWizard;
+
+ let record = {};
+ let basicCards = paymentRequest.getBasicCards(state);
+ let addresses = paymentRequest.getAddresses(state);
+
+ this.genericErrorText.textContent = page.error;
+
+ this.form.querySelector("#cc-number").disabled = editing;
+
+ // The CVV fields should be hidden and disabled when editing.
+ this.form.querySelector("#cc-csc-container").hidden = editing;
+ this.cscInput.disabled = editing;
+
+ // If a card is selected we want to edit it.
+ if (editing) {
+ this.pageTitleHeading.textContent = this.dataset.editBasicCardTitle;
+ record = basicCards[basicCardPage.guid];
+ if (!record) {
+ throw new Error(
+ "Trying to edit a non-existing card: " + basicCardPage.guid
+ );
+ }
+ // When editing an existing record, prevent changes to persistence
+ this.persistCheckbox.hidden = true;
+ } else {
+ this.pageTitleHeading.textContent = this.dataset.addBasicCardTitle;
+ // Use a currently selected shipping address as the default billing address
+ record.billingAddressGUID = basicCardPage.billingAddressGUID;
+ if (!record.billingAddressGUID && selectedShippingAddress) {
+ record.billingAddressGUID = selectedShippingAddress;
+ }
+
+ let {
+ saveCreditCardDefaultChecked,
+ } = PaymentDialogUtils.getDefaultPreferences();
+ if (typeof saveCreditCardDefaultChecked != "boolean") {
+ throw new Error(`Unexpected non-boolean value for saveCreditCardDefaultChecked from
+ PaymentDialogUtils.getDefaultPreferences(): ${typeof saveCreditCardDefaultChecked}`);
+ }
+ // Adding a new record: default persistence to pref value when in a not-private session
+ this.persistCheckbox.hidden = false;
+ if (basicCardPage.hasOwnProperty("persistCheckboxValue")) {
+ // returning to this page, use previous checked state
+ this.persistCheckbox.checked = basicCardPage.persistCheckboxValue;
+ } else {
+ this.persistCheckbox.checked = state.isPrivate
+ ? false
+ : saveCreditCardDefaultChecked;
+ }
+ }
+
+ this.formHandler.loadRecord(
+ record,
+ addresses,
+ basicCardPage.preserveFieldValues
+ );
+
+ this.form.querySelector(".billingAddressRow").hidden = false;
+
+ let billingAddressSelect = this.billingAddressPicker.dropdown;
+ if (basicCardPage.billingAddressGUID) {
+ billingAddressSelect.value = basicCardPage.billingAddressGUID;
+ } else if (!editing) {
+ if (paymentRequest.getAddresses(state)[selectedShippingAddress]) {
+ billingAddressSelect.value = selectedShippingAddress;
+ } else {
+ let firstAddressGUID = Object.keys(addresses)[0];
+ if (firstAddressGUID) {
+ // Only set the value if we have a saved address to not mark the field
+ // dirty and invalid on an add form with no saved addresses.
+ billingAddressSelect.value = firstAddressGUID;
+ }
+ }
+ }
+ // Need to recalculate the populated state since
+ // billingAddressSelect is updated after loadRecord.
+ this.formHandler.updatePopulatedState(billingAddressSelect.popupBox);
+
+ this.updateRequiredState();
+ this.updateSaveButtonState();
+ }
+
+ onChange(evt) {
+ let ccType = this.form.querySelector("#cc-type");
+ this.cscInput.setAttribute("card-type", ccType.value);
+
+ this.updateSaveButtonState();
+ }
+
+ onClick(evt) {
+ switch (evt.target) {
+ case this.cancelButton: {
+ paymentRequest.cancel();
+ break;
+ }
+ case this.billingAddressPicker.addLink:
+ case this.billingAddressPicker.editLink: {
+ // The address-picker has set state for the page to advance to, now set up the
+ // necessary state for returning to and re-rendering this page
+ let {
+ "basic-card-page": basicCardPage,
+ page,
+ } = this.requestStore.getState();
+ let nextState = {
+ page: Object.assign({}, page, {
+ previousId: "basic-card-page",
+ }),
+ "basic-card-page": {
+ preserveFieldValues: true,
+ guid: basicCardPage.guid,
+ persistCheckboxValue: this.persistCheckbox.checked,
+ selectedStateKey: basicCardPage.selectedStateKey,
+ },
+ };
+ this.requestStore.setState(nextState);
+ break;
+ }
+ case this.backButton: {
+ let currentState = this.requestStore.getState();
+ let {
+ page,
+ request,
+ "shipping-address-page": shippingAddressPage,
+ "billing-address-page": billingAddressPage,
+ "basic-card-page": basicCardPage,
+ selectedShippingAddress,
+ } = currentState;
+
+ let nextState = {
+ page: {
+ id: page.previousId || "payment-summary",
+ onboardingWizard: page.onboardingWizard,
+ },
+ };
+
+ if (page.onboardingWizard) {
+ if (request.paymentOptions.requestShipping) {
+ shippingAddressPage = Object.assign({}, shippingAddressPage, {
+ guid: selectedShippingAddress,
+ });
+ Object.assign(nextState, {
+ "shipping-address-page": shippingAddressPage,
+ });
+ } else {
+ billingAddressPage = Object.assign({}, billingAddressPage, {
+ guid: basicCardPage.billingAddressGUID,
+ });
+ Object.assign(nextState, {
+ "billing-address-page": billingAddressPage,
+ });
+ }
+
+ let basicCardPageState = Object.assign({}, basicCardPage, {
+ preserveFieldValues: true,
+ });
+ delete basicCardPageState.persistCheckboxValue;
+
+ Object.assign(nextState, {
+ "basic-card-page": basicCardPageState,
+ });
+ }
+
+ this.requestStore.setState(nextState);
+ break;
+ }
+ case this.saveButton: {
+ if (this.form.checkValidity()) {
+ this.saveRecord();
+ }
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+
+ onInput(event) {
+ event.target.setCustomValidity("");
+ this.updateSaveButtonState();
+ }
+
+ onInvalid(event) {
+ if (event.target instanceof HTMLFormElement) {
+ this.onInvalidForm(event);
+ } else {
+ this.onInvalidField(event);
+ }
+ }
+
+ /**
+ * @param {Event} event - "invalid" event
+ * Note: Keep this in-sync with the equivalent version in address-form.js
+ */
+ onInvalidField(event) {
+ let field = event.target;
+ let container = field.closest(`#${field.id}-container`);
+ let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
+ errorTextSpan.textContent = field.validationMessage;
+ }
+
+ onInvalidForm() {
+ this.saveButton.disabled = true;
+ }
+
+ updateSaveButtonState() {
+ const INVALID_CLASS_NAME = "invalid-selected-option";
+ let isValid =
+ this.form.checkValidity() &&
+ !this.billingAddressPicker.classList.contains(INVALID_CLASS_NAME);
+ this.saveButton.disabled = !isValid;
+ }
+
+ updateRequiredState() {
+ for (let field of this.form.elements) {
+ let container = field.closest(".container");
+ let span = container.querySelector(".label-text");
+ if (!span) {
+ // The billing address field doesn't use a label inside the field.
+ continue;
+ }
+ span.setAttribute(
+ "fieldRequiredSymbol",
+ this.dataset.fieldRequiredSymbol
+ );
+ container.toggleAttribute("required", field.required && !field.disabled);
+ }
+ }
+
+ async saveRecord() {
+ let record = this.formHandler.buildFormObject();
+ let currentState = this.requestStore.getState();
+ let { tempBasicCards, "basic-card-page": basicCardPage } = currentState;
+ let editing = !!basicCardPage.guid;
+
+ if (
+ editing
+ ? basicCardPage.guid in tempBasicCards
+ : !this.persistCheckbox.checked
+ ) {
+ record.isTemporary = true;
+ }
+
+ for (let editableFieldName of [
+ "cc-name",
+ "cc-exp-month",
+ "cc-exp-year",
+ "cc-type",
+ ]) {
+ record[editableFieldName] = record[editableFieldName] || "";
+ }
+
+ // Only save the card number if we're saving a new record, otherwise we'd
+ // overwrite the unmasked card number with the masked one.
+ if (!editing) {
+ record["cc-number"] = record["cc-number"] || "";
+ }
+
+ // Never save the CSC in storage. Storage will throw and not save the record
+ // if it is passed.
+ delete record["cc-csc"];
+
+ try {
+ let { guid } = await paymentRequest.updateAutofillRecord(
+ "creditCards",
+ record,
+ basicCardPage.guid
+ );
+ let { selectedStateKey } = currentState["basic-card-page"];
+ if (!selectedStateKey) {
+ throw new Error(
+ `state["basic-card-page"].selectedStateKey is required`
+ );
+ }
+ this.requestStore.setState({
+ page: {
+ id: "payment-summary",
+ },
+ [selectedStateKey]: guid,
+ [selectedStateKey + "SecurityCode"]: this.cscInput.value,
+ });
+ } catch (ex) {
+ log.warn("saveRecord: error:", ex);
+ this.requestStore.setState({
+ page: {
+ id: "basic-card-page",
+ error: this.dataset.errorGenericSave,
+ },
+ });
+ }
+ }
+}
+
+customElements.define("basic-card-form", BasicCardForm);
diff --git a/browser/components/payments/res/containers/billing-address-picker.js b/browser/components/payments/res/containers/billing-address-picker.js
new file mode 100644
index 0000000000..57b70a2364
--- /dev/null
+++ b/browser/components/payments/res/containers/billing-address-picker.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import AddressPicker from "./address-picker.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <billing-address-picker></billing-address-picker>
+ * Extends AddressPicker to treat the <select>'s value as the source of truth
+ */
+
+export default class BillingAddressPicker extends AddressPicker {
+ constructor() {
+ super();
+ this._allowEmptyOption = true;
+ }
+
+ /**
+ * @param {object?} state - See `PaymentsStore.setState`
+ * The value of the picker is the child dropdown element's value
+ * @returns {string} guid
+ */
+ getCurrentValue() {
+ return this.dropdown.value;
+ }
+
+ onChange(event) {
+ this.render(this.requestStore.getState());
+ }
+}
+
+customElements.define("billing-address-picker", BillingAddressPicker);
diff --git a/browser/components/payments/res/containers/completion-error-page.js b/browser/components/payments/res/containers/completion-error-page.js
new file mode 100644
index 0000000000..9e8f7ce9f7
--- /dev/null
+++ b/browser/components/payments/res/containers/completion-error-page.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <completion-error-page></completion-error-page>
+ *
+ * XXX: Bug 1473772 - This page isn't fully localized when used via this custom element
+ * as it will be much easier to implement and share the logic once we switch to Fluent.
+ */
+
+export default class CompletionErrorPage extends HandleEventMixin(
+ PaymentStateSubscriberMixin(PaymentRequestPage)
+) {
+ constructor() {
+ super();
+
+ this.classList.add("error-page");
+ this.suggestionHeading = document.createElement("p");
+ this.body.append(this.suggestionHeading);
+ this.suggestionsList = document.createElement("ul");
+ this.suggestions = [];
+ this.body.append(this.suggestionsList);
+
+ this.brandingSpan = document.createElement("span");
+ this.brandingSpan.classList.add("branding");
+ this.footer.appendChild(this.brandingSpan);
+
+ this.doneButton = document.createElement("button");
+ this.doneButton.classList.add("done-button", "primary");
+ this.doneButton.addEventListener("click", this);
+
+ this.footer.appendChild(this.doneButton);
+ }
+
+ render(state) {
+ let { page } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(
+ `CompletionErrorPage: no need to further render inactive page: ${page.id}`
+ );
+ return;
+ }
+
+ let { request } = this.requestStore.getState();
+ let { displayHost } = request.topLevelPrincipal.URI;
+ for (let key of [
+ "pageTitle",
+ "suggestion-heading",
+ "suggestion-1",
+ "suggestion-2",
+ "suggestion-3",
+ ]) {
+ if (this.dataset[key] && displayHost) {
+ this.dataset[key] = this.dataset[key].replace(
+ "**host-name**",
+ displayHost
+ );
+ }
+ }
+
+ this.pageTitleHeading.textContent = this.dataset.pageTitle;
+ this.suggestionHeading.textContent = this.dataset.suggestionHeading;
+ this.brandingSpan.textContent = this.dataset.brandingLabel;
+ this.doneButton.textContent = this.dataset.doneButtonLabel;
+
+ this.suggestionsList.textContent = "";
+ if (this.dataset["suggestion-1"]) {
+ this.suggestions[0] = this.dataset["suggestion-1"];
+ }
+ if (this.dataset["suggestion-2"]) {
+ this.suggestions[1] = this.dataset["suggestion-2"];
+ }
+ if (this.dataset["suggestion-3"]) {
+ this.suggestions[2] = this.dataset["suggestion-3"];
+ }
+
+ let suggestionsFragment = document.createDocumentFragment();
+ for (let suggestionText of this.suggestions) {
+ let listNode = document.createElement("li");
+ listNode.textContent = suggestionText;
+ suggestionsFragment.appendChild(listNode);
+ }
+ this.suggestionsList.appendChild(suggestionsFragment);
+ }
+
+ onClick(event) {
+ switch (event.target) {
+ case this.doneButton: {
+ this.onDoneButtonClick(event);
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+
+ onDoneButtonClick(event) {
+ paymentRequest.closeDialog();
+ }
+}
+
+customElements.define("completion-error-page", CompletionErrorPage);
diff --git a/browser/components/payments/res/containers/cvv-hint-image-back.svg b/browser/components/payments/res/containers/cvv-hint-image-back.svg
new file mode 100644
index 0000000000..1e9f4ddb10
--- /dev/null
+++ b/browser/components/payments/res/containers/cvv-hint-image-back.svg
@@ -0,0 +1,27 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="46" height="27" version="1.1">
+ <defs>
+ <circle id="a" cx="10" cy="10" r="10"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
+ <path fill="#A1C6FF" d="M37 6.2a10.046 10.046 0 0 0 -2 -0.2c-5.523 0 -10 4.477 -10 10a9.983 9.983 0 0 0 3.999 8h-27.999a1 1 0 0 1 -1 -1v-22a1 1 0 0 1 1 -1h35a1 1 0 0 1 1 1v5.2zm-18 7.8c3.314 0 6 -1.567 6 -3.5s-2.686 -3.5 -6 -3.5 -6 1.567 -6 3.5 2.686 3.5 6 3.5z"/>
+ <path fill="#5F5F5F" d="M2 17h9v2h-9v-2zm0 -15h33v3h-33v-3zm0 18h15v2h-15v-2zm10 -3h13v2h-13v-2z"/>
+ <g transform="translate(25 6)">
+ <mask id="b" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <use stroke="#FFF" stroke-width="1.5" xlink:href="#a"/>
+ <g mask="url(#b)">
+ <g transform="translate(-77 -31)">
+ <rect width="99.39" height="69.141" x="0" y="0" fill="#A1C6FF" fill-rule="evenodd" rx="1"/>
+ <path fill="#5F5F5F" fill-rule="evenodd" d="M79 46h17v6h-17z"/>
+ <text fill="none" font-family="sans-serif" font-size="6">
+ <tspan x="80" y="42" fill="#5F5F5F">1234</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/browser/components/payments/res/containers/cvv-hint-image-front.svg b/browser/components/payments/res/containers/cvv-hint-image-front.svg
new file mode 100644
index 0000000000..5a758870a1
--- /dev/null
+++ b/browser/components/payments/res/containers/cvv-hint-image-front.svg
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="45" height="27" version="1.1">
+ <defs>
+ <circle id="a" cx="10" cy="10" r="10"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
+ <path fill="#62A0FF" d="M37 6.458a9.996 9.996 0 0 0 -3 -0.458c-3.701 0 -6.933 2.011 -8.662 5h-22.338v5h21a9.983 9.983 0 0 0 3.999 8h-26.999a1 1 0 0 1 -1 -1v-22a1 1 0 0 1 1 -1h35a1 1 0 0 1 1 1v5.458z"/>
+ <path fill="#5F5F5F" d="M37 6.458a9.996 9.996 0 0 0 -3 -0.458 9.97 9.97 0 0 0 -7.141 3h-26.859v-6h37v3.458z"/>
+ <g transform="translate(24 6)">
+ <mask id="b" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <use stroke="#FFF" stroke-width="1.5" xlink:href="#a"/>
+ <g mask="url(#b)">
+ <path fill="#62A0FF" fill-rule="evenodd" d="M-41.923 -15.615h64.476a1 1 0 0 1 1 1v44.244a1 1 0 0 1 -1 1h-64.476a1 1 0 0 1 -1 -1v-44.244a1 1 0 0 1 1 -1zm2.923 19.615v9h55v-9h-55z"/>
+ <path fill="#5F5F5F" fill-rule="evenodd" d="M-43 -10h66v12h-66z"/>
+ <text fill="none" font-family="sans-serif" font-size="6" transform="translate(-43.923 -15.615)">
+ <tspan x="47.676" y="26.104" fill="#5F5F5F">123</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/browser/components/payments/res/containers/error-page.css b/browser/components/payments/res/containers/error-page.css
new file mode 100644
index 0000000000..bd4bec96b5
--- /dev/null
+++ b/browser/components/payments/res/containers/error-page.css
@@ -0,0 +1,42 @@
+/* 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/. */
+
+.error-page.illustrated > .page-body {
+ display: flex;
+ justify-content: center;
+ min-height: 160px;
+ background-position: left center;
+ background-repeat: no-repeat;
+ background-size: 160px;
+ padding-inline-start: 160px;
+}
+
+.error-page.illustrated > .page-body:dir(rtl) {
+ background-position: right center;
+}
+
+.error-page.illustrated > .page-body > h2 {
+ background: none;
+ padding-inline-start: 0;
+ margin-inline-start: 0;
+ font-weight: lighter;
+ font-size: 2rem;
+}
+
+.error-page.illustrated > .page-body > p {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.error-page.illustrated > .page-body > ul {
+ margin-top: .5rem;
+}
+
+.error-page#completion-timeout-error > .page-body {
+ background-image: url("./timeout.svg");
+}
+
+.error-page#completion-fail-error > .page-body {
+ background-image: url("./warning.svg");
+}
diff --git a/browser/components/payments/res/containers/order-details.css b/browser/components/payments/res/containers/order-details.css
new file mode 100644
index 0000000000..beadeb3b88
--- /dev/null
+++ b/browser/components/payments/res/containers/order-details.css
@@ -0,0 +1,55 @@
+/* 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/. */
+
+order-details {
+ display: grid;
+ grid-template-columns: 20% auto 10rem;
+ grid-gap: 1em;
+ margin: 1px 4vw;
+}
+
+order-details > ul {
+ list-style-type: none;
+ margin: 1em 0;
+ padding: 0;
+ display: contents;
+}
+
+order-details payment-details-item {
+ margin: 1px 0;
+ display: contents;
+}
+payment-details-item .label {
+ grid-column-start: 1;
+ grid-column-end: 3;
+}
+payment-details-item currency-amount {
+ grid-column-start: 3;
+ grid-column-end: 4;
+}
+
+order-details .footer-items-list:not(:empty):before {
+ border: 1px solid GrayText;
+ display: block;
+ content: "";
+ grid-column-start: 1;
+ grid-column-end: 4;
+}
+
+order-details > .details-total {
+ margin: 1px 0;
+ display: contents;
+}
+
+.details-total > .label {
+ margin: 0;
+ font-size: large;
+ grid-column-start: 2;
+ grid-column-end: 3;
+ text-align: end;
+}
+.details-total > currency-amount {
+ font-size: large;
+ text-align: end;
+}
diff --git a/browser/components/payments/res/containers/order-details.js b/browser/components/payments/res/containers/order-details.js
new file mode 100644
index 0000000000..a458c14784
--- /dev/null
+++ b/browser/components/payments/res/containers/order-details.js
@@ -0,0 +1,143 @@
+/* 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/. */
+
+// <currency-amount> is used in the <template>
+import "../components/currency-amount.js";
+import PaymentDetailsItem from "../components/payment-details-item.js";
+import paymentRequest from "../paymentRequest.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+
+/**
+ * <order-details></order-details>
+ */
+
+export default class OrderDetails extends PaymentStateSubscriberMixin(
+ HTMLElement
+) {
+ connectedCallback() {
+ if (!this._contents) {
+ let template = document.getElementById("order-details-template");
+ let contents = (this._contents = document.importNode(
+ template.content,
+ true
+ ));
+
+ this._mainItemsList = contents.querySelector(".main-list");
+ this._footerItemsList = contents.querySelector(".footer-items-list");
+ this._totalAmount = contents.querySelector(
+ ".details-total > currency-amount"
+ );
+
+ this.appendChild(this._contents);
+ }
+ super.connectedCallback();
+ }
+
+ get mainItemsList() {
+ return this._mainItemsList;
+ }
+
+ get footerItemsList() {
+ return this._footerItemsList;
+ }
+
+ get totalAmountElem() {
+ return this._totalAmount;
+ }
+
+ static _emptyList(listEl) {
+ while (listEl.lastChild) {
+ listEl.removeChild(listEl.lastChild);
+ }
+ }
+
+ static _populateList(listEl, items) {
+ let fragment = document.createDocumentFragment();
+ for (let item of items) {
+ let row = new PaymentDetailsItem();
+ row.label = item.label;
+ row.amountValue = item.amount.value;
+ row.amountCurrency = item.amount.currency;
+ fragment.appendChild(row);
+ }
+ listEl.appendChild(fragment);
+ return listEl;
+ }
+
+ _getAdditionalDisplayItems(state) {
+ let methodId = state.selectedPaymentCard;
+ let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
+ if (modifier && modifier.additionalDisplayItems) {
+ return modifier.additionalDisplayItems;
+ }
+ return [];
+ }
+
+ render(state) {
+ let totalItem = paymentRequest.getTotalItem(state);
+
+ OrderDetails._emptyList(this.mainItemsList);
+ OrderDetails._emptyList(this.footerItemsList);
+
+ let mainItems = OrderDetails._getMainListItems(state);
+ if (mainItems.length) {
+ OrderDetails._populateList(this.mainItemsList, mainItems);
+ }
+
+ let footerItems = OrderDetails._getFooterListItems(state);
+ if (footerItems.length) {
+ OrderDetails._populateList(this.footerItemsList, footerItems);
+ }
+
+ this.totalAmountElem.value = totalItem.amount.value;
+ this.totalAmountElem.currency = totalItem.amount.currency;
+ }
+
+ /**
+ * Determine if a display item should belong in the footer list.
+ * This uses the proposed "type" property, tracked at:
+ * https://github.com/w3c/payment-request/issues/163
+ *
+ * @param {object} item - Data representing a PaymentItem
+ * @returns {boolean}
+ */
+ static isFooterItem(item) {
+ return item.type == "tax";
+ }
+
+ static _getMainListItems(state) {
+ let request = state.request;
+ let items = request.paymentDetails.displayItems;
+ if (Array.isArray(items) && items.length) {
+ let predicate = item => !OrderDetails.isFooterItem(item);
+ return request.paymentDetails.displayItems.filter(predicate);
+ }
+ return [];
+ }
+
+ static _getFooterListItems(state) {
+ let request = state.request;
+ let items = request.paymentDetails.displayItems;
+ let footerItems = [];
+ let methodId = state.selectedPaymentCard;
+ if (methodId) {
+ let modifier = paymentRequest.getModifierForPaymentMethod(
+ state,
+ methodId
+ );
+ if (modifier && Array.isArray(modifier.additionalDisplayItems)) {
+ footerItems.push(...modifier.additionalDisplayItems);
+ }
+ }
+ if (Array.isArray(items) && items.length) {
+ let predicate = OrderDetails.isFooterItem;
+ footerItems.push(
+ ...request.paymentDetails.displayItems.filter(predicate)
+ );
+ }
+ return footerItems;
+ }
+}
+
+customElements.define("order-details", OrderDetails);
diff --git a/browser/components/payments/res/containers/payment-dialog.js b/browser/components/payments/res/containers/payment-dialog.js
new file mode 100644
index 0000000000..7bf094e267
--- /dev/null
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -0,0 +1,593 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+
+import "../components/currency-amount.js";
+import "../components/payment-request-page.js";
+import "../components/accepted-cards.js";
+import "./address-picker.js";
+import "./address-form.js";
+import "./basic-card-form.js";
+import "./completion-error-page.js";
+import "./order-details.js";
+import "./payment-method-picker.js";
+import "./shipping-option-picker.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <payment-dialog></payment-dialog>
+ *
+ * Warning: Do not import this module from any other module as it will import
+ * everything else (see above) and ruin element independence. This can stop
+ * being exported once tests stop depending on it.
+ */
+
+export default class PaymentDialog extends HandleEventMixin(
+ PaymentStateSubscriberMixin(HTMLElement)
+) {
+ constructor() {
+ super();
+ this._template = document.getElementById("payment-dialog-template");
+ this._cachedState = {};
+ }
+
+ 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.cancelRequest);
+
+ this._payButton = contents.querySelector("#pay");
+ this._payButton.addEventListener("click", this);
+
+ this._viewAllButton = contents.querySelector("#view-all");
+ this._viewAllButton.addEventListener("click", this);
+
+ this._mainContainer = contents.getElementById("main-container");
+ this._orderDetailsOverlay = contents.querySelector(
+ "#order-details-overlay"
+ );
+
+ this._shippingAddressPicker = contents.querySelector(
+ "address-picker.shipping-related"
+ );
+ this._shippingOptionPicker = contents.querySelector(
+ "shipping-option-picker"
+ );
+ this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
+ this._payerRelatedEls = contents.querySelectorAll(".payer-related");
+ this._payerAddressPicker = contents.querySelector(
+ "address-picker.payer-related"
+ );
+ this._paymentMethodPicker = contents.querySelector("payment-method-picker");
+ this._acceptedCardsList = contents.querySelector("accepted-cards");
+ this._manageText = contents.querySelector(".manage-text");
+ this._manageText.addEventListener("click", this);
+
+ this._header = contents.querySelector("header");
+
+ this._errorText = contents.querySelector("header > .page-error");
+
+ this._disabledOverlay = contents.getElementById("disabled-overlay");
+
+ this.appendChild(contents);
+
+ super.connectedCallback();
+ }
+
+ disconnectedCallback() {
+ this._cancelButton.removeEventListener("click", this.cancelRequest);
+ this._payButton.removeEventListener("click", this.pay);
+ this._viewAllButton.removeEventListener("click", this);
+ super.disconnectedCallback();
+ }
+
+ onClick(event) {
+ switch (event.currentTarget) {
+ case this._viewAllButton:
+ let orderDetailsShowing = !this.requestStore.getState()
+ .orderDetailsShowing;
+ this.requestStore.setState({ orderDetailsShowing });
+ break;
+ case this._payButton:
+ this.pay();
+ break;
+ case this._manageText:
+ if (event.target instanceof HTMLAnchorElement) {
+ this.openPreferences(event);
+ }
+ break;
+ }
+ }
+
+ openPreferences(event) {
+ paymentRequest.openPreferences();
+ event.preventDefault();
+ }
+
+ cancelRequest() {
+ paymentRequest.cancel();
+ }
+
+ pay() {
+ let state = this.requestStore.getState();
+ let {
+ selectedPayerAddress,
+ selectedPaymentCard,
+ selectedPaymentCardSecurityCode,
+ selectedShippingAddress,
+ } = state;
+
+ let data = {
+ selectedPaymentCardGUID: selectedPaymentCard,
+ selectedPaymentCardSecurityCode,
+ };
+
+ data.selectedShippingAddressGUID = state.request.paymentOptions
+ .requestShipping
+ ? selectedShippingAddress
+ : null;
+
+ data.selectedPayerAddressGUID = this._isPayerRequested(
+ state.request.paymentOptions
+ )
+ ? selectedPayerAddress
+ : null;
+
+ paymentRequest.pay(data);
+ }
+
+ /**
+ * Called when the selectedShippingAddress or its properties are changed.
+ * @param {string} shippingAddressGUID
+ */
+ changeShippingAddress(shippingAddressGUID) {
+ // Clear shipping address merchant errors when the shipping address changes.
+ let request = Object.assign({}, this.requestStore.getState().request);
+ request.paymentDetails = Object.assign({}, request.paymentDetails);
+ request.paymentDetails.shippingAddressErrors = {};
+ this.requestStore.setState({ request });
+
+ paymentRequest.changeShippingAddress({
+ shippingAddressGUID,
+ });
+ }
+
+ changeShippingOption(optionID) {
+ paymentRequest.changeShippingOption({
+ optionID,
+ });
+ }
+
+ /**
+ * Called when the selectedPaymentCard or its relevant properties or billingAddress are changed.
+ * @param {string} selectedPaymentCardBillingAddressGUID
+ */
+ changePaymentMethod(selectedPaymentCardBillingAddressGUID) {
+ // Clear paymentMethod merchant errors when the paymentMethod or billingAddress changes.
+ let request = Object.assign({}, this.requestStore.getState().request);
+ request.paymentDetails = Object.assign({}, request.paymentDetails);
+ request.paymentDetails.paymentMethodErrors = null;
+ this.requestStore.setState({ request });
+
+ paymentRequest.changePaymentMethod({
+ selectedPaymentCardBillingAddressGUID,
+ });
+ }
+
+ /**
+ * Called when the selectedPayerAddress or its relevant properties are changed.
+ * @param {string} payerAddressGUID
+ */
+ changePayerAddress(payerAddressGUID) {
+ // Clear payer address merchant errors when the payer address changes.
+ let request = Object.assign({}, this.requestStore.getState().request);
+ request.paymentDetails = Object.assign({}, request.paymentDetails);
+ request.paymentDetails.payerErrors = {};
+ this.requestStore.setState({ request });
+
+ paymentRequest.changePayerAddress({
+ payerAddressGUID,
+ });
+ }
+
+ _isPayerRequested(paymentOptions) {
+ return (
+ paymentOptions.requestPayerName ||
+ paymentOptions.requestPayerEmail ||
+ paymentOptions.requestPayerPhone
+ );
+ }
+
+ _getAdditionalDisplayItems(state) {
+ let methodId = state.selectedPaymentCard;
+ let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
+ if (modifier && modifier.additionalDisplayItems) {
+ return modifier.additionalDisplayItems;
+ }
+ return [];
+ }
+
+ _updateCompleteStatus(state) {
+ let { completeStatus } = state.request;
+ switch (completeStatus) {
+ case "fail":
+ case "timeout":
+ case "unknown":
+ state.page = {
+ id: `completion-${completeStatus}-error`,
+ };
+ state.changesPrevented = false;
+ break;
+ case "": {
+ // When we get a DOM update for an updateWith() or retry() the completeStatus
+ // is "" when we need to show non-final screens. Don't set the page as we
+ // may be on a form instead of payment-summary
+ state.changesPrevented = false;
+ break;
+ }
+ }
+ return 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`
+ */
+ // eslint-disable-next-line complexity
+ async setStateFromParent(state) {
+ let oldAddresses = paymentRequest.getAddresses(
+ this.requestStore.getState()
+ );
+ let oldBasicCards = paymentRequest.getBasicCards(
+ this.requestStore.getState()
+ );
+ if (state.request) {
+ state = this._updateCompleteStatus(state);
+ }
+ this.requestStore.setState(state);
+
+ // Check if any foreign-key constraints were invalidated.
+ state = this.requestStore.getState();
+ let {
+ selectedPayerAddress,
+ selectedPaymentCard,
+ selectedShippingAddress,
+ selectedShippingOption,
+ } = state;
+ let addresses = paymentRequest.getAddresses(state);
+ let { paymentOptions } = state.request;
+
+ if (paymentOptions.requestShipping) {
+ let shippingOptions = state.request.paymentDetails.shippingOptions;
+ let shippingAddress =
+ selectedShippingAddress && addresses[selectedShippingAddress];
+ let oldShippingAddress =
+ selectedShippingAddress && oldAddresses[selectedShippingAddress];
+
+ // Ensure `selectedShippingAddress` never refers to a deleted address.
+ // We also compare address timestamps to notify about changes
+ // made outside the payments UI.
+ if (shippingAddress) {
+ // invalidate the cached value if the address was modified
+ if (
+ oldShippingAddress &&
+ shippingAddress.guid == oldShippingAddress.guid &&
+ shippingAddress.timeLastModified !=
+ oldShippingAddress.timeLastModified
+ ) {
+ delete this._cachedState.selectedShippingAddress;
+ }
+ } else if (selectedShippingAddress !== null) {
+ // null out the `selectedShippingAddress` property if it is undefined,
+ // or if the address it pointed to was removed from storage.
+ log.debug("resetting invalid/deleted shipping address");
+ this.requestStore.setState({
+ selectedShippingAddress: null,
+ });
+ }
+
+ // Ensure `selectedShippingOption` never refers to a deleted shipping option and
+ // matches the merchant's selected option if the user hasn't made a choice.
+ if (
+ shippingOptions &&
+ (!selectedShippingOption ||
+ !shippingOptions.find(opt => opt.id == selectedShippingOption))
+ ) {
+ this._cachedState.selectedShippingOption = selectedShippingOption;
+ this.requestStore.setState({
+ // Use the DOM's computed selected shipping option:
+ selectedShippingOption: state.request.shippingOption,
+ });
+ }
+ }
+
+ let basicCards = paymentRequest.getBasicCards(state);
+ let oldPaymentMethod =
+ selectedPaymentCard && oldBasicCards[selectedPaymentCard];
+ let paymentMethod = selectedPaymentCard && basicCards[selectedPaymentCard];
+ if (
+ oldPaymentMethod &&
+ paymentMethod.guid == oldPaymentMethod.guid &&
+ paymentMethod.timeLastModified != oldPaymentMethod.timeLastModified
+ ) {
+ delete this._cachedState.selectedPaymentCard;
+ } else {
+ // Changes to the billing address record don't change the `timeLastModified`
+ // on the card record so we have to check for changes to the address separately.
+
+ let billingAddressGUID =
+ paymentMethod && paymentMethod.billingAddressGUID;
+ let billingAddress = billingAddressGUID && addresses[billingAddressGUID];
+ let oldBillingAddress =
+ billingAddressGUID && oldAddresses[billingAddressGUID];
+
+ if (
+ oldBillingAddress &&
+ billingAddress &&
+ billingAddress.timeLastModified != oldBillingAddress.timeLastModified
+ ) {
+ delete this._cachedState.selectedPaymentCard;
+ }
+ }
+
+ // Ensure `selectedPaymentCard` never refers to a deleted payment card.
+ if (selectedPaymentCard && !basicCards[selectedPaymentCard]) {
+ this.requestStore.setState({
+ selectedPaymentCard: null,
+ selectedPaymentCardSecurityCode: null,
+ });
+ }
+
+ if (this._isPayerRequested(state.request.paymentOptions)) {
+ let payerAddress =
+ selectedPayerAddress && addresses[selectedPayerAddress];
+ let oldPayerAddress =
+ selectedPayerAddress && oldAddresses[selectedPayerAddress];
+
+ if (
+ oldPayerAddress &&
+ payerAddress &&
+ ((paymentOptions.requestPayerName &&
+ payerAddress.name != oldPayerAddress.name) ||
+ (paymentOptions.requestPayerEmail &&
+ payerAddress.email != oldPayerAddress.email) ||
+ (paymentOptions.requestPayerPhone &&
+ payerAddress.tel != oldPayerAddress.tel))
+ ) {
+ // invalidate the cached value if the payer address fields were modified
+ delete this._cachedState.selectedPayerAddress;
+ }
+
+ // Ensure `selectedPayerAddress` never refers to a deleted address and refers
+ // to an address if one exists.
+ if (!addresses[selectedPayerAddress]) {
+ this.requestStore.setState({
+ selectedPayerAddress: Object.keys(addresses)[0] || null,
+ });
+ }
+ }
+ }
+
+ _renderPayButton(state) {
+ let completeStatus = state.request.completeStatus;
+ switch (completeStatus) {
+ case "processing":
+ case "success":
+ case "unknown": {
+ this._payButton.disabled = true;
+ this._payButton.textContent = this._payButton.dataset[
+ completeStatus + "Label"
+ ];
+ break;
+ }
+ case "": {
+ // initial/default state
+ this._payButton.textContent = this._payButton.dataset.label;
+ const INVALID_CLASS_NAME = "invalid-selected-option";
+ this._payButton.disabled =
+ (state.request.paymentOptions.requestShipping &&
+ (!this._shippingAddressPicker.selectedOption ||
+ this._shippingAddressPicker.classList.contains(
+ INVALID_CLASS_NAME
+ ) ||
+ !this._shippingOptionPicker.selectedOption)) ||
+ (this._isPayerRequested(state.request.paymentOptions) &&
+ (!this._payerAddressPicker.selectedOption ||
+ this._payerAddressPicker.classList.contains(
+ INVALID_CLASS_NAME
+ ))) ||
+ !this._paymentMethodPicker.securityCodeInput.isValid ||
+ !this._paymentMethodPicker.selectedOption ||
+ this._paymentMethodPicker.classList.contains(INVALID_CLASS_NAME) ||
+ state.changesPrevented;
+ break;
+ }
+ case "fail":
+ case "timeout": {
+ // pay button is hidden in fail/timeout states.
+ this._payButton.textContent = this._payButton.dataset.label;
+ this._payButton.disabled = true;
+ break;
+ }
+ default: {
+ throw new Error(`Invalid completeStatus: ${completeStatus}`);
+ }
+ }
+ }
+
+ _renderPayerFields(state) {
+ let paymentOptions = state.request.paymentOptions;
+ let payerRequested = this._isPayerRequested(paymentOptions);
+ let payerAddressForm = this.querySelector(
+ "address-form[selected-state-key='selectedPayerAddress']"
+ );
+
+ for (let element of this._payerRelatedEls) {
+ element.hidden = !payerRequested;
+ }
+
+ if (payerRequested) {
+ let fieldNames = new Set();
+ if (paymentOptions.requestPayerName) {
+ fieldNames.add("name");
+ }
+ if (paymentOptions.requestPayerEmail) {
+ fieldNames.add("email");
+ }
+ if (paymentOptions.requestPayerPhone) {
+ fieldNames.add("tel");
+ }
+ let addressFields = [...fieldNames].join(" ");
+ this._payerAddressPicker.setAttribute("address-fields", addressFields);
+ if (payerAddressForm.form) {
+ payerAddressForm.form.dataset.extraRequiredFields = addressFields;
+ }
+
+ // For the payer picker we want to have a line break after the name field (#1)
+ // if all three fields are requested.
+ if (fieldNames.size == 3) {
+ this._payerAddressPicker.setAttribute("break-after-nth-field", 1);
+ } else {
+ this._payerAddressPicker.removeAttribute("break-after-nth-field");
+ }
+ } else {
+ this._payerAddressPicker.removeAttribute("address-fields");
+ }
+ }
+
+ stateChangeCallback(state) {
+ super.stateChangeCallback(state);
+
+ // Don't dispatch change events for initial selectedShipping* changes at initialization
+ // if requestShipping is false.
+ if (state.request.paymentOptions.requestShipping) {
+ if (
+ state.selectedShippingAddress !=
+ this._cachedState.selectedShippingAddress
+ ) {
+ this.changeShippingAddress(state.selectedShippingAddress);
+ }
+
+ if (
+ state.selectedShippingOption != this._cachedState.selectedShippingOption
+ ) {
+ this.changeShippingOption(state.selectedShippingOption);
+ }
+ }
+
+ let selectedPaymentCard = state.selectedPaymentCard;
+ let basicCards = paymentRequest.getBasicCards(state);
+ let billingAddressGUID = (basicCards[selectedPaymentCard] || {})
+ .billingAddressGUID;
+ if (
+ selectedPaymentCard != this._cachedState.selectedPaymentCard &&
+ billingAddressGUID
+ ) {
+ // Update _cachedState to prevent an infinite loop when changePaymentMethod updates state.
+ this._cachedState.selectedPaymentCard = state.selectedPaymentCard;
+ this.changePaymentMethod(billingAddressGUID);
+ }
+
+ if (this._isPayerRequested(state.request.paymentOptions)) {
+ if (
+ state.selectedPayerAddress != this._cachedState.selectedPayerAddress
+ ) {
+ this.changePayerAddress(state.selectedPayerAddress);
+ }
+ }
+
+ this._cachedState.selectedShippingAddress = state.selectedShippingAddress;
+ this._cachedState.selectedShippingOption = state.selectedShippingOption;
+ this._cachedState.selectedPayerAddress = state.selectedPayerAddress;
+ }
+
+ render(state) {
+ let request = state.request;
+ let paymentDetails = request.paymentDetails;
+ this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
+
+ let displayItems = request.paymentDetails.displayItems || [];
+ let additionalItems = this._getAdditionalDisplayItems(state);
+ this._viewAllButton.hidden =
+ !displayItems.length && !additionalItems.length;
+
+ let shippingType = state.request.paymentOptions.shippingType || "shipping";
+ let addressPickerLabel = this._shippingAddressPicker.dataset[
+ shippingType + "AddressLabel"
+ ];
+ this._shippingAddressPicker.setAttribute("label", addressPickerLabel);
+ let optionPickerLabel = this._shippingOptionPicker.dataset[
+ shippingType + "OptionsLabel"
+ ];
+ this._shippingOptionPicker.setAttribute("label", optionPickerLabel);
+
+ let shippingAddressForm = this.querySelector(
+ "address-form[selected-state-key='selectedShippingAddress']"
+ );
+ shippingAddressForm.dataset.titleAdd = this.dataset[
+ shippingType + "AddressTitleAdd"
+ ];
+ shippingAddressForm.dataset.titleEdit = this.dataset[
+ shippingType + "AddressTitleEdit"
+ ];
+
+ let totalItem = paymentRequest.getTotalItem(state);
+ let totalAmountEl = this.querySelector("#total > currency-amount");
+ totalAmountEl.value = totalItem.amount.value;
+ totalAmountEl.currency = totalItem.amount.currency;
+
+ // Show the total header on the address and basic card pages only during
+ // on-boarding(FTU) and on the payment summary page.
+ this._header.hidden =
+ !state.page.onboardingWizard && state.page.id != "payment-summary";
+
+ this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
+ let genericError = "";
+ if (
+ this._shippingAddressPicker.selectedOption &&
+ (!request.paymentDetails.shippingOptions ||
+ !request.paymentDetails.shippingOptions.length)
+ ) {
+ genericError = this._errorText.dataset[shippingType + "GenericError"];
+ }
+ this._errorText.textContent = paymentDetails.error || genericError;
+
+ let paymentOptions = request.paymentOptions;
+ for (let element of this._shippingRelatedEls) {
+ element.hidden = !paymentOptions.requestShipping;
+ }
+
+ this._renderPayerFields(state);
+
+ let isMac = /mac/i.test(navigator.platform);
+ for (let manageTextEl of this._manageText.children) {
+ manageTextEl.hidden = manageTextEl.dataset.os == "mac" ? !isMac : isMac;
+ let link = manageTextEl.querySelector("a");
+ // The href is only set to be exposed to accessibility tools so users know what will open.
+ // The actual opening happens from the click event listener.
+ link.href = "about:preferences#privacy-form-autofill";
+ }
+
+ this._renderPayButton(state);
+
+ for (let page of this._mainContainer.querySelectorAll(":scope > .page")) {
+ page.hidden = state.page.id != page.id;
+ }
+
+ this.toggleAttribute("changes-prevented", state.changesPrevented);
+ this.setAttribute("complete-status", request.completeStatus);
+ this._disabledOverlay.hidden = !state.changesPrevented;
+ }
+}
+
+customElements.define("payment-dialog", PaymentDialog);
diff --git a/browser/components/payments/res/containers/payment-method-picker.js b/browser/components/payments/res/containers/payment-method-picker.js
new file mode 100644
index 0000000000..3693d352a5
--- /dev/null
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import BasicCardOption from "../components/basic-card-option.js";
+import CscInput from "../components/csc-input.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+import RichPicker from "./rich-picker.js";
+import paymentRequest from "../paymentRequest.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <payment-method-picker></payment-method-picker>
+ * Container around add/edit links and <rich-select> with
+ * <basic-card-option> listening to savedBasicCards.
+ */
+
+export default class PaymentMethodPicker extends HandleEventMixin(RichPicker) {
+ constructor() {
+ super();
+ this.dropdown.setAttribute("option-type", "basic-card-option");
+ this.securityCodeInput = new CscInput();
+ this.securityCodeInput.className = "security-code-container";
+ this.securityCodeInput.placeholder = this.dataset.cscPlaceholder;
+ this.securityCodeInput.backTooltip = this.dataset.cscBackTooltip;
+ this.securityCodeInput.frontTooltip = this.dataset.cscFrontTooltip;
+ this.securityCodeInput.addEventListener("change", this);
+ this.securityCodeInput.addEventListener("input", this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.dropdown.after(this.securityCodeInput);
+ }
+
+ get fieldNames() {
+ let fieldNames = [...BasicCardOption.recordAttributes];
+ return fieldNames;
+ }
+
+ render(state) {
+ let basicCards = paymentRequest.getBasicCards(state);
+ let desiredOptions = [];
+ for (let [guid, basicCard] of Object.entries(basicCards)) {
+ let optionEl = this.dropdown.getOptionByValue(guid);
+ if (!optionEl) {
+ optionEl = document.createElement("option");
+ optionEl.value = guid;
+ }
+
+ for (let key of BasicCardOption.recordAttributes) {
+ let val = basicCard[key];
+ if (val) {
+ optionEl.setAttribute(key, val);
+ } else {
+ optionEl.removeAttribute(key);
+ }
+ }
+
+ optionEl.textContent = BasicCardOption.formatSingleLineLabel(basicCard);
+ desiredOptions.push(optionEl);
+ }
+
+ this.dropdown.popupBox.textContent = "";
+ for (let option of desiredOptions) {
+ this.dropdown.popupBox.appendChild(option);
+ }
+
+ // Update selectedness after the options are updated
+ let selectedPaymentCardGUID = state[this.selectedStateKey];
+ if (selectedPaymentCardGUID) {
+ this.dropdown.value = selectedPaymentCardGUID;
+
+ if (selectedPaymentCardGUID !== this.dropdown.value) {
+ throw new Error(
+ `The option ${selectedPaymentCardGUID} ` +
+ `does not exist in the payment method picker`
+ );
+ }
+ } else {
+ this.dropdown.value = "";
+ }
+
+ let securityCodeState = state[this.selectedStateKey + "SecurityCode"];
+ if (
+ securityCodeState &&
+ securityCodeState != this.securityCodeInput.value
+ ) {
+ this.securityCodeInput.defaultValue = securityCodeState;
+ }
+
+ let selectedCardType =
+ (basicCards[selectedPaymentCardGUID] &&
+ basicCards[selectedPaymentCardGUID]["cc-type"]) ||
+ "";
+ this.securityCodeInput.cardType = selectedCardType;
+
+ super.render(state);
+ }
+
+ errorForSelectedOption(state) {
+ let superError = super.errorForSelectedOption(state);
+ if (superError) {
+ return superError;
+ }
+ let selectedOption = this.selectedOption;
+ if (!selectedOption) {
+ return "";
+ }
+
+ let basicCardMethod = state.request.paymentMethods.find(
+ method => method.supportedMethods == "basic-card"
+ );
+ let merchantNetworks =
+ basicCardMethod &&
+ basicCardMethod.data &&
+ basicCardMethod.data.supportedNetworks;
+ let acceptedNetworks =
+ merchantNetworks || PaymentDialogUtils.getCreditCardNetworks();
+ let selectedCard = paymentRequest.getBasicCards(state)[
+ selectedOption.value
+ ];
+ let isSupported =
+ selectedCard["cc-type"] &&
+ acceptedNetworks.includes(selectedCard["cc-type"]);
+ return isSupported ? "" : this.dataset.invalidLabel;
+ }
+
+ get selectedStateKey() {
+ return this.getAttribute("selected-state-key");
+ }
+
+ onInput(event) {
+ this.onInputOrChange(event);
+ }
+
+ onChange(event) {
+ this.onInputOrChange(event);
+ }
+
+ onInputOrChange({ currentTarget }) {
+ let selectedKey = this.selectedStateKey;
+ let stateChange = {};
+
+ if (!selectedKey) {
+ return;
+ }
+
+ switch (currentTarget) {
+ case this.dropdown: {
+ stateChange[selectedKey] = this.dropdown.value;
+ break;
+ }
+ case this.securityCodeInput: {
+ stateChange[
+ selectedKey + "SecurityCode"
+ ] = this.securityCodeInput.value;
+ break;
+ }
+ default: {
+ return;
+ }
+ }
+
+ this.requestStore.setState(stateChange);
+ }
+
+ onClick({ target }) {
+ let nextState = {
+ page: {
+ id: "basic-card-page",
+ },
+ "basic-card-page": {
+ selectedStateKey: this.selectedStateKey,
+ },
+ };
+
+ switch (target) {
+ case this.addLink: {
+ nextState["basic-card-page"].guid = null;
+ break;
+ }
+ case this.editLink: {
+ let state = this.requestStore.getState();
+ let selectedPaymentCardGUID = state[this.selectedStateKey];
+ nextState["basic-card-page"].guid = selectedPaymentCardGUID;
+ break;
+ }
+ default: {
+ throw new Error("Unexpected onClick");
+ }
+ }
+
+ this.requestStore.setState(nextState);
+ }
+}
+
+customElements.define("payment-method-picker", PaymentMethodPicker);
diff --git a/browser/components/payments/res/containers/rich-picker.css b/browser/components/payments/res/containers/rich-picker.css
new file mode 100644
index 0000000000..7b232518d0
--- /dev/null
+++ b/browser/components/payments/res/containers/rich-picker.css
@@ -0,0 +1,83 @@
+/* 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-picker {
+ display: grid;
+ grid-template-columns: 5fr auto auto;
+ grid-template-areas:
+ "label edit add"
+ "dropdown dropdown dropdown"
+ "invalid invalid invalid";
+ padding-top: 8px;
+}
+
+.rich-picker > label {
+ color: #0c0c0d;
+ font-weight: 700;
+ grid-area: label;
+}
+
+.rich-picker > .add-link,
+.rich-picker > .edit-link {
+ padding: 0 8px;
+}
+
+.rich-picker > .add-link {
+ grid-area: add;
+}
+
+.rich-picker > .edit-link {
+ grid-area: edit;
+ border-inline-end: 1px solid #0C0C0D33;
+}
+
+.rich-picker > rich-select {
+ grid-area: dropdown;
+}
+
+.invalid-selected-option > rich-select > select {
+ border: 1px solid #c70011;
+}
+
+.rich-picker > .invalid-label {
+ grid-area: invalid;
+ font-weight: normal;
+ color: #c70011;
+}
+
+:not(.invalid-selected-option) > .invalid-label {
+ display: none;
+}
+
+/* Payment Method Picker */
+payment-method-picker.rich-picker {
+ grid-template-columns: 20fr 1fr auto auto;
+ grid-template-areas:
+ "label spacer edit add"
+ "dropdown csc csc csc"
+ "invalid invalid invalid invalid";
+}
+
+.security-code-container {
+ display: flex;
+ flex-grow: 1;
+ grid-area: csc;
+ margin: 10px 0; /* Has to be same as rich-select */
+}
+
+.rich-picker .security-code {
+ border: 1px solid #0C0C0D33;
+ /* Underlap the 1px border from common.css */
+ margin-inline-start: -1px;
+ flex-grow: 1;
+ padding: 8px;
+}
+
+.rich-picker .security-code:-moz-ui-invalid,
+.rich-picker .security-code:focus {
+ /* So the error outline and focus ring appear above the adjacent dropdown when appropriate. */
+ /* We don't want to always be on top or we will cover the error outline or focus outline from the
+ dropdown. */
+ z-index: 1;
+}
diff --git a/browser/components/payments/res/containers/rich-picker.js b/browser/components/payments/res/containers/rich-picker.js
new file mode 100644
index 0000000000..3752f67942
--- /dev/null
+++ b/browser/components/payments/res/containers/rich-picker.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import RichSelect from "../components/rich-select.js";
+
+export default class RichPicker extends PaymentStateSubscriberMixin(
+ HTMLElement
+) {
+ static get observedAttributes() {
+ return ["label"];
+ }
+
+ constructor() {
+ super();
+ this.classList.add("rich-picker");
+
+ this.dropdown = new RichSelect();
+ this.dropdown.addEventListener("change", this);
+
+ this.labelElement = document.createElement("label");
+
+ this.addLink = document.createElement("a");
+ this.addLink.className = "add-link";
+ this.addLink.href = "javascript:void(0)";
+ this.addLink.addEventListener("click", this);
+
+ this.editLink = document.createElement("a");
+ this.editLink.className = "edit-link";
+ this.editLink.href = "javascript:void(0)";
+ this.editLink.addEventListener("click", this);
+
+ this.invalidLabel = document.createElement("label");
+ this.invalidLabel.className = "invalid-label";
+ }
+
+ connectedCallback() {
+ if (!this.dropdown.popupBox.id) {
+ this.dropdown.popupBox.id =
+ "select-" + Math.floor(Math.random() * 1000000);
+ }
+ this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
+ this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
+
+ // The document order, by default, controls tab order so keep that in mind if changing this.
+ this.appendChild(this.labelElement);
+ this.appendChild(this.dropdown);
+ this.appendChild(this.editLink);
+ this.appendChild(this.addLink);
+ this.appendChild(this.invalidLabel);
+ super.connectedCallback();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name == "label") {
+ this.labelElement.textContent = newValue;
+ }
+ }
+
+ render(state) {
+ this.editLink.hidden = !this.dropdown.value;
+
+ let errorText = this.errorForSelectedOption(state);
+ this.classList.toggle("invalid-selected-option", !!errorText);
+ this.invalidLabel.textContent = errorText;
+ this.addLink.textContent = this.dataset.addLinkLabel;
+ this.editLink.textContent = this.dataset.editLinkLabel;
+ }
+
+ get selectedOption() {
+ return this.dropdown.selectedOption;
+ }
+
+ get selectedRichOption() {
+ return this.dropdown.selectedRichOption;
+ }
+
+ get requiredFields() {
+ return this.selectedOption ? this.selectedOption.requiredFields || [] : [];
+ }
+
+ get fieldNames() {
+ return [];
+ }
+
+ /**
+ * @param {object} state Application state
+ * @returns {string} Containing an error message for the picker or "" for no error.
+ */
+ errorForSelectedOption(state) {
+ if (!this.selectedOption) {
+ return "";
+ }
+ if (!this.dataset.invalidLabel) {
+ throw new Error("data-invalid-label is required");
+ }
+ return this.missingFieldsOfSelectedOption().length
+ ? this.dataset.invalidLabel
+ : "";
+ }
+
+ missingFieldsOfSelectedOption() {
+ let selectedOption = this.selectedOption;
+ if (!selectedOption) {
+ return [];
+ }
+
+ let fieldNames = this.selectedRichOption.requiredFields || [];
+
+ // Return all field names that are empty or missing from the option.
+ return fieldNames.filter(name => !selectedOption.getAttribute(name));
+ }
+}
diff --git a/browser/components/payments/res/containers/shipping-option-picker.js b/browser/components/payments/res/containers/shipping-option-picker.js
new file mode 100644
index 0000000000..01a97f1ac4
--- /dev/null
+++ b/browser/components/payments/res/containers/shipping-option-picker.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import RichPicker from "./rich-picker.js";
+import ShippingOption from "../components/shipping-option.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+
+/**
+ * <shipping-option-picker></shipping-option-picker>
+ * Container around <rich-select> with
+ * <option> listening to shippingOptions.
+ */
+
+export default class ShippingOptionPicker extends HandleEventMixin(RichPicker) {
+ constructor() {
+ super();
+ this.dropdown.setAttribute("option-type", "shipping-option");
+ }
+
+ render(state) {
+ this.addLink.hidden = true;
+ this.editLink.hidden = true;
+
+ // If requestShipping is true but paymentDetails.shippingOptions isn't defined
+ // then use an empty array as a fallback.
+ let shippingOptions = state.request.paymentDetails.shippingOptions || [];
+ let desiredOptions = [];
+ for (let option of shippingOptions) {
+ let optionEl = this.dropdown.getOptionByValue(option.id);
+ if (!optionEl) {
+ optionEl = document.createElement("option");
+ optionEl.value = option.id;
+ }
+
+ optionEl.setAttribute("label", option.label);
+ optionEl.setAttribute("amount-currency", option.amount.currency);
+ optionEl.setAttribute("amount-value", option.amount.value);
+
+ optionEl.textContent = ShippingOption.formatSingleLineLabel(option);
+ desiredOptions.push(optionEl);
+ }
+
+ this.dropdown.popupBox.textContent = "";
+ for (let option of desiredOptions) {
+ this.dropdown.popupBox.appendChild(option);
+ }
+
+ // Update selectedness after the options are updated
+ let selectedShippingOption = state.selectedShippingOption;
+ this.dropdown.value = selectedShippingOption;
+
+ if (
+ selectedShippingOption &&
+ selectedShippingOption !== this.dropdown.popupBox.value
+ ) {
+ throw new Error(
+ `The option ${selectedShippingOption} ` +
+ `does not exist in the shipping option picker`
+ );
+ }
+ }
+
+ onChange(event) {
+ let selectedOptionId = this.dropdown.value;
+ this.requestStore.setState({
+ selectedShippingOption: selectedOptionId,
+ });
+ }
+}
+
+customElements.define("shipping-option-picker", ShippingOptionPicker);
diff --git a/browser/components/payments/res/containers/timeout.svg b/browser/components/payments/res/containers/timeout.svg
new file mode 100644
index 0000000000..a9c96ababd
--- /dev/null
+++ b/browser/components/payments/res/containers/timeout.svg
@@ -0,0 +1,84 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="183" height="159" version="1.1">
+ <defs>
+ <linearGradient id="a" x1="-58.737%" x2="177.192%" y1="-3.847%" y2="112.985%">
+ <stop offset="0%" stop-color="#CCFBFF"/>
+ <stop offset="100%" stop-color="#C9E4FF"/>
+ </linearGradient>
+ <linearGradient id="b" x1="-62.081%" x2="144.194%" y1="-14.656%" y2="104.338%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#008EA4"/>
+ </linearGradient>
+ <linearGradient id="c" x1="-93.784%" x2="130.325%" y1="-512.631%" y2="364.268%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="d" x1="-632.1%" x2="878.563%" y1="-1341.641%" y2="1656.303%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="e" x1="-145.225%" x2="166.502%" y1="-230.02%" y2="260.392%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="f" x1="-16.485%" x2="216.233%" y1="-373.776%" y2="1088.73%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="g" x1="-13.212%" x2="220.924%" y1="-398.815%" y2="1263.59%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="h" x1="56.524%" x2="154.312%" y1="90.974%" y2="705.12%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#008EA4"/>
+ </linearGradient>
+ <linearGradient id="i" x1="-2629.906%" x2="2946.182%" y1="-2629.905%" y2="2946.183%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="j" x1="-2788.189%" x2="2787.9%" y1="-2788.189%" y2="2787.9%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="k" x1="-2975.533%" x2="2600.555%" y1="-2975.533%" y2="2600.555%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="l" x1="-2968.553%" x2="2607.535%" y1="-2968.552%" y2="2607.537%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="m" x1="-47.612%" x2="165.227%" y1="-4.394%" y2="113.507%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ </defs>
+ <g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1">
+ <path fill="#EAEAEE" d="M41.548 94.855h110.678a1 1 0 1 0 0 -2h-110.678a1 1 0 0 0 0 2zm14.952 -5h27.764a0.5 0.5 0 1 0 0 -1h-27.765a0.5 0.5 0 1 0 0 1zm-35.386 8.869a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5z"/>
+ <path fill="#FFF" d="M1.474 63.811h25.423s-7.955 -17.777 8.932 -20.076c15.062 -2.05 21.014 13.427 21.014 13.427s1.786 -8.93 10.743 -7.221c8.832 1.684 15.353 15.889 15.353 15.889h22.137"/>
+ <path fill="#EAEAEE" d="M105.51 61.633h-6.544a0.588 0.588 0 1 1 0 -1.176h6.545a0.588 0.588 0 0 1 0 1.176zm-17.132 0h-1.176a0.588 0.588 0 1 1 0 -1.176h1.176a0.588 0.588 0 1 1 0 1.176zm-61.046 -0.712h-1.893a0.588 0.588 0 0 1 0 -1.176h1.023a24.94 24.94 0 0 1 -0.269 -0.756 0.588 0.588 0 0 1 1.115 -0.377 18.812 18.812 0 0 0 0.56 1.48 0.588 0.588 0 0 1 -0.536 0.829zm-11.305 0h-14.117a0.588 0.588 0 1 1 0 -1.176h14.117a0.588 0.588 0 0 1 0 1.176zm66.928 -0.064a0.588 0.588 0 0 1 -0.514 -0.301 44.273 44.273 0 0 0 -1.828 -2.974 0.588 0.588 0 1 1 0.977 -0.654 45.65 45.65 0 0 1 1.878 3.053 0.588 0.588 0 0 1 -0.513 0.876zm-57.304 -6.042a0.588 0.588 0 0 1 -0.582 -0.507 20.77 20.77 0 0 1 -0.134 -1.205 0.588 0.588 0 0 1 1.173 -0.094 19.39 19.39 0 0 0 0.126 1.136 0.588 0.588 0 0 1 -0.583 0.67zm30.647 -2.594a0.587 0.587 0 0 1 -0.519 -0.31 26.496 26.496 0 0 0 -0.57 -1.001 0.588 0.588 0 1 1 1.01 -0.605 27.467 27.467 0 0 1 0.596 1.048 0.588 0.588 0 0 1 -0.517 0.868zm18.676 -1.503a0.586 0.586 0 0 1 -0.383 -0.143 14.722 14.722 0 0 0 -6.68 -3.535 8.578 8.578 0 0 0 -5.628 0.61 0.588 0.588 0 1 1 -0.54 -1.046 9.747 9.747 0 0 1 6.389 -0.72 15.855 15.855 0 0 1 7.225 3.8 0.588 0.588 0 0 1 -0.383 1.034zm-22.019 -3.335a0.587 0.587 0 0 1 -0.443 -0.202 21.943 21.943 0 0 0 -2.45 -2.41 0.588 0.588 0 0 1 0.755 -0.903 23.254 23.254 0 0 1 2.582 2.54 0.588 0.588 0 0 1 -0.444 0.975zm-24.258 -3.43a0.589 0.589 0 0 1 -0.395 -1.025 14.421 14.421 0 0 1 7.882 -3.255 19.345 19.345 0 0 1 5.96 0.08 0.588 0.588 0 1 1 -0.203 1.158 18.263 18.263 0 0 0 -5.597 -0.073 13.284 13.284 0 0 0 -7.253 2.963 0.59 0.59 0 0 1 -0.394 0.152z"/>
+ <path fill="#FFF" d="M106.242 66.149h-104.771a1.176 1.176 0 1 1 0 -2.353h104.771a1.176 1.176 0 0 1 0 2.353zm1.978 -51.759h14.187s-4.44 -9.92 4.985 -11.203c8.404 -1.144 11.726 7.493 11.726 7.493s0.907 -4.089 5.995 -4.03c4.756 0.055 8.38 7.914 8.568 8.867h12.353"/>
+ <path fill="#EAEAEE" d="M122.823 12.8h-14.167a0.59 0.59 0 1 1 0 -1.18h14.167a0.59 0.59 0 0 1 0 1.18zm43.647 -0.184h-0.545a0.59 0.59 0 1 1 0 -1.18h0.545a0.59 0.59 0 1 1 0 1.18zm-5.267 0h-3.542a0.59 0.59 0 1 1 0 -1.18h3.542a0.59 0.59 0 1 1 0 1.18zm-21.648 -3.527a0.614 0.614 0 0 1 -0.553 -0.384l-0.088 -0.207a0.59 0.59 0 0 1 0.23 -0.74 5.483 5.483 0 0 1 5.186 -4 7.111 7.111 0 0 1 1.33 0.132 10.622 10.622 0 0 1 5.267 3.045 0.59 0.59 0 0 1 -0.822 0.848 9.484 9.484 0 0 0 -4.666 -2.733 5.935 5.935 0 0 0 -1.109 -0.111c-3.402 0 -4.166 3.527 -4.196 3.678a0.592 0.592 0 0 1 -0.579 0.472zm-16.422 -5.27a0.59 0.59 0 0 1 -0.45 -0.973 6.799 6.799 0 0 1 3.236 -2.026 0.59 0.59 0 1 1 0.351 1.127 5.632 5.632 0 0 0 -2.688 1.665 0.589 0.589 0 0 1 -0.45 0.208zm8.784 -1.987a0.601 0.601 0 0 1 -0.156 -0.02 9.055 9.055 0 0 0 -1.083 -0.225 0.59 0.59 0 0 1 -0.5 -0.668 0.6 0.6 0 0 1 0.668 -0.5 10.36 10.36 0 0 1 1.224 0.253 0.59 0.59 0 0 1 -0.153 1.16z"/>
+ <path fill="#FFF" d="M166.948 16.76h-58.468a1.18 1.18 0 0 1 0 -2.36h58.468a1.18 1.18 0 0 1 0 2.36z"/>
+ <ellipse cx="99.859" cy="152.41" fill="#EAEAEE" rx="30" ry="5.802"/>
+ <path fill="#F9F9FA" d="M116.421 66.559c-4.126 4.126 -9.051 9.488 -9.656 16.465 0.605 6.977 5.53 12.339 9.656 16.465a26.199 26.199 0 0 1 9.126 19.9v11.242h-52.517v-11.242a26.199 26.199 0 0 1 9.126 -19.9c4.126 -4.126 9.051 -9.488 9.656 -16.465 -0.605 -6.977 -5.53 -12.339 -9.656 -16.465a26.199 26.199 0 0 1 -9.126 -19.9v-11.369h52.517v11.369a26.199 26.199 0 0 1 -9.126 19.9z"/>
+ <path fill="url(#a)" d="M35.304 21.239c-3.349 4.126 -8.666 10.808 -9.157 17.785 0.491 6.977 5.808 13.659 9.157 17.785a25.765 25.765 0 0 1 7.408 18.58v11.242h-42.627v-11.242a25.765 25.765 0 0 1 7.407 -18.58c3.35 -4.126 8.667 -10.808 9.157 -17.785 -0.49 -6.977 -5.808 -13.659 -9.157 -17.785a25.765 25.765 0 0 1 -7.407 -18.58v-2.13a95.816 95.816 0 0 0 22.369 2.64c12.527 0 20.258 -2.64 20.258 -2.64v2.13a25.765 25.765 0 0 1 -7.408 18.58z" transform="translate(78 44)"/>
+ <path fill="url(#b)" d="M55.548 6.222h-52.518a2.932 2.932 0 0 1 0 -5.864h52.518a2.932 2.932 0 0 1 0 5.864zm2.932 92.409a2.932 2.932 0 0 0 -2.932 -2.932h-52.518a2.932 2.932 0 0 0 0 5.863h52.518a2.932 2.932 0 0 0 2.932 -2.931z" transform="translate(70 32)"/>
+ <path fill="url(#c)" d="M129.028 130.684c0.334 5 -13.767 7.667 -29.749 7.667 -15.981 0 -28.937 -3.358 -28.937 -7.5 0 -4.143 12.956 -7.5 28.937 -7.5 15.982 0 29.474 3.2 29.75 7.333z"/>
+ <path fill="#F9F9FA" d="M125.654 127.755c0 2.185 -11.233 5.596 -25.792 5.596 -14.56 0 -26.932 -3.41 -26.932 -5.596 0 -2.185 11.802 -3.956 26.362 -3.956 14.56 0 26.362 1.771 26.362 3.956z"/>
+ <path fill="url(#d)" d="M95.233 76.435s1.978 4.121 4.286 4.286c2.307 0.165 4.43 -4.388 4.43 -4.388l-8.716 0.102z"/>
+ <path fill="url(#e)" d="M99.288 102.48c7.502 0 21.137 23.169 21.137 23.169s-1.442 3.702 -20.563 3.702c-19.122 0 -21.71 -3.702 -21.71 -3.702s13.763 -23.17 21.136 -23.17z"/>
+ <path fill="url(#f)" d="M127.852 37.313c0 2.185 -12.049 5.038 -27.657 5.038s-28.864 -2.853 -28.864 -5.038 12.653 -3.956 28.26 -3.956c15.609 0 28.261 1.771 28.261 3.956z"/>
+ <ellipse cx="99.42" cy="33.357" fill="url(#g)" rx="28.089" ry="3.956"/>
+ <ellipse cx="99.603" cy="76.333" fill="url(#h)" rx="4.346" ry="1"/>
+ <circle cx="99.393" cy="84.717" r="1.181" fill="url(#i)"/>
+ <circle cx="98.996" cy="92.589" r="1.181" fill="url(#j)"/>
+ <circle cx="98.145" cy="102.288" r="1.181" fill="url(#k)"/>
+ <circle cx="101.388" cy="98.715" r="1.181" fill="url(#l)"/>
+ <path fill="#F9F9FA" d="M84.628 53.363a1.833 1.833 0 0 1 -0.264 -0.026l-0.237 -0.08a41.18 41.18 0 0 0 -0.238 -0.118 2.238 2.238 0 0 1 -0.197 -0.172 1.325 1.325 0 0 1 -0.382 -0.922 1.359 1.359 0 0 1 0.105 -0.515 1.297 1.297 0 0 1 0.277 -0.422 1.012 1.012 0 0 1 0.197 -0.158 2.196 2.196 0 0 1 0.238 -0.132 1.55 1.55 0 0 1 0.237 -0.066 1.331 1.331 0 0 1 1.2 0.356 1.297 1.297 0 0 1 0.278 0.422 1.169 1.169 0 0 1 0.105 0.515 1.329 1.329 0 0 1 -1.319 1.318zm-0.221 69.007a1.319 1.319 0 0 1 -1.318 -1.282c-0.227 -8.439 2.93 -13.501 3.064 -13.712a1.319 1.319 0 0 1 2.227 1.413c-0.057 0.092 -2.858 4.684 -2.653 12.227a1.32 1.32 0 0 1 -1.283 1.355h-0.037zm8.793 -54.507a1.319 1.319 0 0 1 -1.002 -0.46 102.116 102.116 0 0 1 -6.712 -9.614 1.319 1.319 0 0 1 2.24 -1.39 101.694 101.694 0 0 0 6.476 9.287 1.32 1.32 0 0 1 -1.002 2.177z"/>
+ <path fill="url(#m)" d="M127.377 126.244v-6.856a28.144 28.144 0 0 0 -9.656 -21.217c-4.074 -4.092 -8.47 -8.976 -9.052 -15.1 0.582 -6.218 4.978 -11.103 9.026 -15.17a28.118 28.118 0 0 0 9.682 -21.242v-6.529a3.426 3.426 0 0 0 2.27 -2.456 4.696 4.696 0 0 0 -0.888 -5.862c-3.257 -4.026 -24.92 -4.23 -29.23 -4.23 -10.758 0 -24.656 0.91 -28.356 3.436a4.777 4.777 0 0 0 -2.674 4.272 4.71 4.71 0 0 0 2.19 3.98 4.54 4.54 0 0 0 0.73 0.528v6.86a28.09 28.09 0 0 0 9.657 21.218c4.074 4.093 8.47 8.98 9.051 15.101 -0.582 6.215 -4.977 11.102 -9.025 15.169a28.149 28.149 0 0 0 -9.68 21.317l-0.003 6.78a4.741 4.741 0 0 0 -2.92 4.387 4.455 4.455 0 0 0 0.319 1.685c1.459 6.64 27.596 6.83 30.571 6.83 2.951 0 28.885 -0.189 30.526 -6.661a4.734 4.734 0 0 0 -2.538 -6.24zm0.365 5.42l-0.041 0.132c-0.48 3.01 -15.028 5.032 -28.312 5.032 -16.33 0 -28.03 -2.665 -28.315 -5.058l-0.04 -0.145a2.426 2.426 0 0 1 2.205 -3.426h0.5v-8.81a25.817 25.817 0 0 1 8.946 -19.548c4.404 -4.42 9.154 -9.727 9.763 -16.86 -0.609 -7.047 -5.359 -12.354 -9.79 -16.8a25.792 25.792 0 0 1 -8.92 -19.522v-8.348l-0.318 -0.124a3.457 3.457 0 0 1 -1.232 -0.69l-0.117 -0.091a2.422 2.422 0 0 1 0.19 -4.336l0.094 -0.056c2.392 -1.774 14.074 -3.113 27.175 -3.113 15.231 0 26.482 1.747 27.434 3.379l0.115 0.134a2.396 2.396 0 0 1 0.333 3.42l-0.165 0.2 0.094 0.32c-0.057 0.126 -0.362 0.534 -1.938 1.048l-0.345 0.112v8.145a25.817 25.817 0 0 1 -8.946 19.547c-4.404 4.421 -9.154 9.728 -9.763 16.86 0.609 7.048 5.359 12.354 9.79 16.8a25.813 25.813 0 0 1 8.92 19.564v8.769h0.5a2.42 2.42 0 0 1 2.183 3.464z"/>
+ </g>
+</svg>
diff --git a/browser/components/payments/res/containers/warning.svg b/browser/components/payments/res/containers/warning.svg
new file mode 100644
index 0000000000..0b25be0f91
--- /dev/null
+++ b/browser/components/payments/res/containers/warning.svg
@@ -0,0 +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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="162" height="147" version="1.1">
+ <defs>
+ <linearGradient id="a" x1="-5.18%" x2="85.305%" y1="13.831%" y2="117.402%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ <linearGradient id="b" x1="-26.306%" x2="110.545%" y1="-9.375%" y2="148.477%">
+ <stop offset="0%" stop-color="#CCFBFF"/>
+ <stop offset="100%" stop-color="#C9E4FF"/>
+ </linearGradient>
+ <linearGradient id="c" x1="-335.989%" x2="397.876%" y1="-66.454%" y2="146.763%">
+ <stop offset="0%" stop-color="#00C8D7"/>
+ <stop offset="100%" stop-color="#0A84FF"/>
+ </linearGradient>
+ </defs>
+ <g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1">
+ <path fill="#EAEAEE" d="M20.53 83.44h110.678a1 1 0 1 0 0 -2h-110.679a1 1 0 0 0 0 2zm14.951 -5h27.765a0.5 0.5 0 1 0 0 -1h-27.765a0.5 0.5 0 1 0 0 1zm-35.385 8.87a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5z"/>
+ <path fill="#FFF" d="M36.206 58.897h25.423s-7.955 -17.777 8.932 -20.077c15.062 -2.05 21.014 13.427 21.014 13.427s1.786 -8.93 10.743 -7.221c8.832 1.684 15.352 15.889 15.352 15.889h22.137"/>
+ <path fill="#EAEAEE" d="M140.243 56.719h-6.545a0.588 0.588 0 0 1 0 -1.177h6.545a0.588 0.588 0 0 1 0 1.177zm-17.133 0h-1.177a0.588 0.588 0 0 1 0 -1.177h1.177a0.588 0.588 0 0 1 0 1.177zm-61.047 -0.713h-1.892a0.588 0.588 0 1 1 0 -1.176h1.023a24.95 24.95 0 0 1 -0.27 -0.756 0.588 0.588 0 0 1 1.115 -0.377 18.81 18.81 0 0 0 0.562 1.48 0.588 0.588 0 0 1 -0.538 0.83zm-11.304 0h-14.118a0.588 0.588 0 1 1 0 -1.176h14.118a0.588 0.588 0 1 1 0 1.176zm66.928 -0.064a0.588 0.588 0 0 1 -0.515 -0.301 44.263 44.263 0 0 0 -1.827 -2.973 0.588 0.588 0 1 1 0.977 -0.655 45.639 45.639 0 0 1 1.878 3.053 0.588 0.588 0 0 1 -0.513 0.876zm-57.304 -6.042a0.588 0.588 0 0 1 -0.582 -0.507 20.768 20.768 0 0 1 -0.134 -1.205 0.588 0.588 0 0 1 1.173 -0.094 19.388 19.388 0 0 0 0.126 1.136 0.588 0.588 0 0 1 -0.583 0.67zm30.646 -2.594a0.587 0.587 0 0 1 -0.518 -0.31 26.496 26.496 0 0 0 -0.57 -1.001 0.588 0.588 0 1 1 1.01 -0.604 27.467 27.467 0 0 1 0.595 1.047 0.588 0.588 0 0 1 -0.517 0.868zm18.677 -1.503a0.586 0.586 0 0 1 -0.383 -0.142 14.722 14.722 0 0 0 -6.68 -3.536 8.578 8.578 0 0 0 -5.628 0.61 0.588 0.588 0 1 1 -0.54 -1.046 9.747 9.747 0 0 1 6.389 -0.72 15.855 15.855 0 0 1 7.225 3.8 0.588 0.588 0 0 1 -0.383 1.034zm-22.019 -3.335a0.587 0.587 0 0 1 -0.443 -0.201 21.943 21.943 0 0 0 -2.45 -2.41 0.588 0.588 0 0 1 0.755 -0.904 23.255 23.255 0 0 1 2.582 2.54 0.588 0.588 0 0 1 -0.444 0.975zm-24.259 -3.43a0.589 0.589 0 0 1 -0.394 -1.025 14.421 14.421 0 0 1 7.882 -3.254 19.345 19.345 0 0 1 5.959 0.079 0.588 0.588 0 1 1 -0.202 1.158 18.263 18.263 0 0 0 -5.598 -0.072 13.284 13.284 0 0 0 -7.252 2.963 0.59 0.59 0 0 1 -0.395 0.151z"/>
+ <path fill="#FFF" d="M140.974 61.234h-104.772a1.176 1.176 0 0 1 0 -2.353h104.772a1.176 1.176 0 0 1 0 2.353zm-120.522 -46.508h14.187s-4.44 -9.92 4.984 -11.204c8.405 -1.144 11.727 7.493 11.727 7.493s0.907 -4.089 5.995 -4.03c4.755 0.055 8.38 7.915 8.567 8.867h12.354"/>
+ <path fill="#EAEAEE" d="M35.055 13.136h-14.167a0.59 0.59 0 1 1 0 -1.18h14.167a0.59 0.59 0 1 1 0 1.18zm43.647 -0.185h-0.545a0.59 0.59 0 0 1 0 -1.18h0.545a0.59 0.59 0 0 1 0 1.18zm-5.267 0h-3.542a0.59 0.59 0 0 1 0 -1.18h3.542a0.59 0.59 0 0 1 0 1.18zm-21.648 -3.526a0.614 0.614 0 0 1 -0.554 -0.384l-0.087 -0.208a0.59 0.59 0 0 1 0.23 -0.739 5.483 5.483 0 0 1 5.186 -4 7.111 7.111 0 0 1 1.33 0.131 10.622 10.622 0 0 1 5.266 3.045 0.59 0.59 0 0 1 -0.822 0.848 9.484 9.484 0 0 0 -4.665 -2.733 5.934 5.934 0 0 0 -1.109 -0.11c-3.403 0 -4.166 3.526 -4.196 3.677a0.592 0.592 0 0 1 -0.58 0.473zm-16.422 -5.27a0.59 0.59 0 0 1 -0.45 -0.973 6.799 6.799 0 0 1 3.235 -2.027 0.59 0.59 0 0 1 0.352 1.127 5.632 5.632 0 0 0 -2.688 1.665 0.589 0.589 0 0 1 -0.45 0.208zm8.783 -1.988a0.601 0.601 0 0 1 -0.155 -0.02 9.053 9.053 0 0 0 -1.083 -0.224 0.59 0.59 0 0 1 -0.5 -0.669 0.6 0.6 0 0 1 0.668 -0.5 10.36 10.36 0 0 1 1.224 0.253 0.59 0.59 0 0 1 -0.154 1.16z"/>
+ <path fill="#FFF" d="M79.18 17.096h-58.468a1.18 1.18 0 1 1 0 -2.361h58.468a1.18 1.18 0 1 1 0 2.36z"/>
+ <path fill="url(#a)" d="M125.948 119.063h-93.75a2.821 2.821 0 0 1 -2.442 -4.232l46.874 -81.19a2.82 2.82 0 0 1 4.887 0.001l46.874 81.19 -0.001 -0.001a2.821 2.821 0 0 1 -2.442 4.232zm-46.875 -84.333a0.32 0.32 0 0 0 -0.277 0.16l-46.875 81.192a0.32 0.32 0 0 0 0.276 0.481h93.751a0.32 0.32 0 0 0 0.278 -0.48l-0.001 -0.001 -46.874 -81.19a0.32 0.32 0 0 0 -0.278 -0.162z"/>
+ <path fill="#F9F9FA" d="M79.073 34.73a0.32 0.32 0 0 0 -0.277 0.16l-46.875 81.192a0.32 0.32 0 0 0 0.276 0.481h93.751a0.32 0.32 0 0 0 0.278 -0.48l-0.001 -0.001 -46.874 -81.19a0.32 0.32 0 0 0 -0.278 -0.162z"/>
+ <ellipse cx="79.424" cy="140.995" fill="#EAEAEE" rx="49.833" ry="5.802"/>
+ <path fill="url(#b)" d="M79.073 42.313a0.275 0.275 0 0 0 -0.238 0.138l-40.24 69.699a0.275 0.275 0 0 0 0.237 0.413h80.481a0.275 0.275 0 0 0 0.238 -0.412v-0.001l-40.24 -69.699a0.274 0.274 0 0 0 -0.238 -0.138z"/>
+ <path fill="url(#c)" d="M83.67 87.928h-9.19l-1.535 -25.095h12.255l-1.53 25.095zm1.473 11.018c0 3.35 -2.717 6.067 -6.068 6.067 -3.35 0 -6.067 -2.716 -6.067 -6.067s2.716 -6.068 6.067 -6.068a6.1 6.1 0 0 1 6.068 6.068z"/>
+ </g>
+</svg>
diff --git a/browser/components/payments/res/debugging.css b/browser/components/payments/res/debugging.css
new file mode 100644
index 0000000000..6ce0dcbcf9
--- /dev/null
+++ b/browser/components/payments/res/debugging.css
@@ -0,0 +1,35 @@
+/* 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 {
+ color: -moz-DialogText;
+ font: message-box;
+ /* Make sure the background ends to the bottom if there is unused space */
+ height: 100%;
+}
+
+h1 {
+ font-size: 1em;
+}
+
+fieldset > label {
+ white-space: nowrap;
+}
+
+.group {
+ margin: 0.5em 0;
+}
+
+label.block {
+ display: block;
+ margin: 0.3em 0;
+}
+
+button.wide {
+ width: 100%;
+}
+
+#complete-status {
+ column-count: 2;
+}
diff --git a/browser/components/payments/res/debugging.html b/browser/components/payments/res/debugging.html
new file mode 100644
index 0000000000..9b5b80c9e6
--- /dev/null
+++ b/browser/components/payments/res/debugging.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!-- 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>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
+ <link rel="stylesheet" href="debugging.css"/>
+ <script src="debugging.js"></script>
+ </head>
+ <body>
+ <div>
+ <section class="group">
+ <button id="refresh">Refresh</button>
+ <button id="rerender">Re-render</button>
+ <button id="logState">Log state</button>
+ <button id="debugFrame" hidden>Debug frame</button>
+ <button id="toggleDirectionality">Toggle :dir</button>
+ <button id="toggleBranding">Toggle branding</button>
+ </section>
+ <section class="group">
+ <h1>Requests</h1>
+ <button id="setRequest1">Request 1</button>
+ <button id="setRequest2">Request 2</button>
+ <fieldset id="paymentOptions">
+ <legend>Payment Options</legend>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerName" id="setRequestPayerName">requestPayerName</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerEmail" id="setRequestPayerEmail">requestPayerEmail</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerPhone" id="setRequestPayerPhone">requestPayerPhone</label>
+ <label><input type="checkbox" autocomplete="off" name="requestShipping" id="setRequestShipping">requestShipping</label>
+ </fieldset>
+ </section>
+
+ <section class="group">
+ <h1>Addresses</h1>
+ <button id="setAddresses1">Set Addreses 1</button>
+ <button id="setDupesAddresses">Set Duped Addresses</button>
+ <button id="delete1Address">Delete 1 Address</button>
+ </section>
+
+ <section class="group">
+ <h1>Payment Methods</h1>
+ <button id="setBasicCards1">Set Basic Cards 1</button>
+ <button id="delete1Card">Delete 1 Card</button>
+ </section>
+
+ <section class="group">
+ <h1>States</h1>
+ <fieldset id="complete-status">
+ <legend>Complete Status</legend>
+ <label class="block"><input type="radio" name="setCompleteStatus" value="">(default)</label>
+ <label class="block"><input type="radio" name="setCompleteStatus" value="processing">Processing</label>
+ <label class="block"><input type="radio" name="setCompleteStatus" value="fail">Fail</label>
+ <label class="block"><input type="radio" name="setCompleteStatus" value="unknown">Unknown</label>
+ <label class="block"><input type="radio" name="setCompleteStatus" value="timeout">Timeout</label>
+ </fieldset>
+ <label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
+
+
+ <section class="group">
+ <fieldset>
+ <legend>User Data Errors</legend>
+ <button id="saveVisibleForm" title="Bypasses field validation">Save Visible Form</button>
+ <button id="setBasicCardErrors">Basic Card Errors</button>
+ <button id="setPayerErrors">Payer Errors</button>
+ <button id="setShippingError">Shipping Error</button>
+ <button id="setShippingAddressErrors">Shipping Address Errors</button>
+
+ </fieldset>
+ </section>
+ </section>
+ </div>
+ </body>
+</html>
diff --git a/browser/components/payments/res/debugging.js b/browser/components/payments/res/debugging.js
new file mode 100644
index 0000000000..fffc85dd11
--- /dev/null
+++ b/browser/components/payments/res/debugging.js
@@ -0,0 +1,664 @@
+/* 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/. */
+
+const paymentDialog = window.parent.document.querySelector("payment-dialog");
+// The requestStore should be manipulated for most changes but autofill storage changes
+// happen through setStateFromParent which includes some consistency checks.
+const requestStore = paymentDialog.requestStore;
+
+// keep the payment options checkboxes in sync w. actual state
+const paymentOptionsUpdater = {
+ stateChangeCallback(state) {
+ this.render(state);
+ },
+ render(state) {
+ let { completeStatus, paymentOptions } = state.request;
+
+ document.getElementById("setChangesPrevented").checked =
+ state.changesPrevented;
+
+ let paymentOptionInputs = document.querySelectorAll(
+ "#paymentOptions input[type='checkbox']"
+ );
+ for (let input of paymentOptionInputs) {
+ if (paymentOptions.hasOwnProperty(input.name)) {
+ input.checked = paymentOptions[input.name];
+ }
+ }
+
+ let completeStatusInputs = document.querySelectorAll(
+ "input[type='radio'][name='setCompleteStatus']"
+ );
+ for (let input of completeStatusInputs) {
+ input.checked = input.value == completeStatus;
+ }
+ },
+};
+
+let REQUEST_1 = {
+ tabId: 9,
+ topLevelPrincipal: { URI: { displayHost: "debugging.example.com" } },
+ requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
+ completeStatus: "",
+ paymentMethods: [],
+ paymentDetails: {
+ id: "",
+ totalItem: {
+ label: "Demo total",
+ amount: { currency: "EUR", value: "1.00" },
+ pending: false,
+ },
+ displayItems: [
+ {
+ label: "Square",
+ amount: {
+ currency: "USD",
+ value: "5",
+ },
+ },
+ ],
+ payerErrors: {},
+ paymentMethodErrors: {},
+ shippingAddressErrors: {},
+ shippingOptions: [
+ {
+ id: "std",
+ label: "Standard (3-5 business days)",
+ amount: {
+ currency: "USD",
+ value: 10,
+ },
+ selected: false,
+ },
+ {
+ id: "super-slow",
+ // Long to test truncation
+ label: "Ssssssssuuuuuuuuupppppeeeeeeerrrrr sssssllllllloooooowwwwww",
+ amount: {
+ currency: "USD",
+ value: 1.5,
+ },
+ selected: true,
+ },
+ ],
+ modifiers: null,
+ error: "",
+ },
+ paymentOptions: {
+ requestPayerName: true,
+ requestPayerEmail: false,
+ requestPayerPhone: false,
+ requestShipping: true,
+ shippingType: "shipping",
+ },
+ shippingOption: "std",
+};
+
+let REQUEST_2 = {
+ tabId: 9,
+ topLevelPrincipal: { URI: { displayHost: "example.com" } },
+ requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
+ completeStatus: "",
+ paymentMethods: [
+ {
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: ["amex", "discover", "mastercard", "visa"],
+ },
+ },
+ ],
+ paymentDetails: {
+ id: "",
+ totalItem: {
+ label: "",
+ amount: { currency: "CAD", value: "25.75" },
+ pending: false,
+ },
+ displayItems: [
+ {
+ label: "Triangle",
+ amount: {
+ currency: "CAD",
+ value: "3",
+ },
+ },
+ {
+ label: "Circle",
+ amount: {
+ currency: "EUR",
+ value: "10.50",
+ },
+ },
+ {
+ label: "Tax",
+ type: "tax",
+ amount: {
+ currency: "USD",
+ value: "1.50",
+ },
+ },
+ ],
+ payerErrors: {},
+ paymentMethoErrors: {},
+ shippingAddressErrors: {},
+ shippingOptions: [
+ {
+ id: "123",
+ label: "Fast (default)",
+ amount: {
+ currency: "USD",
+ value: 10,
+ },
+ selected: true,
+ },
+ {
+ id: "947",
+ label: "Slow",
+ amount: {
+ currency: "USD",
+ value: 1,
+ },
+ selected: false,
+ },
+ ],
+ modifiers: [
+ {
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total",
+ amount: {
+ currency: "CAD",
+ value: "28.75",
+ },
+ pending: false,
+ },
+ additionalDisplayItems: [
+ {
+ label: "Credit card fee",
+ amount: {
+ currency: "CAD",
+ value: "1.50",
+ },
+ },
+ ],
+ data: {},
+ },
+ ],
+ error: "",
+ },
+ paymentOptions: {
+ requestPayerName: false,
+ requestPayerEmail: false,
+ requestPayerPhone: false,
+ requestShipping: true,
+ shippingType: "shipping",
+ },
+ shippingOption: "123",
+};
+
+let ADDRESSES_1 = {
+ "48bnds6854t": {
+ "address-level1": "MI",
+ "address-level2": "Some City",
+ country: "US",
+ email: "foo@bar.com",
+ "family-name": "Smith",
+ "given-name": "John",
+ guid: "48bnds6854t",
+ name: "John Smith",
+ "postal-code": "90210",
+ "street-address": "123 Sesame Street,\nApt 40",
+ tel: "+1 519 555-5555",
+ timeLastUsed: 50000,
+ },
+ "68gjdh354j": {
+ "additional-name": "Z.",
+ "address-level1": "CA",
+ "address-level2": "Mountain View",
+ country: "US",
+ "family-name": "Doe",
+ "given-name": "Jane",
+ guid: "68gjdh354j",
+ name: "Jane Z. Doe",
+ "postal-code": "94041",
+ "street-address": "P.O. Box 123",
+ tel: "+1 650 555-5555",
+ timeLastUsed: 30000,
+ },
+ abcde12345: {
+ "address-level2": "Mountain View",
+ country: "US",
+ "family-name": "Fields",
+ "given-name": "Mrs.",
+ guid: "abcde12345",
+ name: "Mrs. Fields",
+ timeLastUsed: 70000,
+ },
+ german1: {
+ "additional-name": "Y.",
+ "address-level1": "",
+ "address-level2": "Berlin",
+ country: "DE",
+ email: "de@example.com",
+ "family-name": "Mouse",
+ "given-name": "Anon",
+ guid: "german1",
+ name: "Anon Y. Mouse",
+ organization: "Mozilla",
+ "postal-code": "10997",
+ "street-address": "Schlesische Str. 27",
+ tel: "+49 30 983333002",
+ timeLastUsed: 10000,
+ },
+ "missing-country": {
+ "address-level1": "ON",
+ "address-level2": "Toronto",
+ "family-name": "Bogard",
+ "given-name": "Kristin",
+ guid: "missing-country",
+ name: "Kristin Bogard",
+ "postal-code": "H0H 0H0",
+ "street-address": "123 Yonge Street\nSuite 2300",
+ tel: "+1 416 555-5555",
+ timeLastUsed: 90000,
+ },
+ TimBR: {
+ "given-name": "Timothy",
+ "additional-name": "João",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "Rua Adalberto Pajuaba, 404",
+ "address-level3": "Campos Elísios",
+ "address-level2": "Ribeirão Preto",
+ "address-level1": "SP",
+ "postal-code": "14055-220",
+ country: "BR",
+ tel: "+0318522222222",
+ email: "timbr@example.org",
+ timeLastUsed: 110000,
+ },
+};
+
+let DUPED_ADDRESSES = {
+ a9e830667189: {
+ "street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n",
+ "address-level2": "Greenup",
+ "address-level1": "KY",
+ "postal-code": "41144",
+ country: "US",
+ email: "bob@example.com",
+ "family-name": "Smith",
+ "given-name": "Bob",
+ guid: "a9e830667189",
+ tel: "+19871234567",
+ name: "Bob Smith",
+ timeLastUsed: 10001,
+ },
+ "72a15aed206d": {
+ "street-address": "1 New St",
+ "address-level2": "York",
+ "address-level1": "SC",
+ "postal-code": "29745",
+ country: "US",
+ "given-name": "Mary Sue",
+ guid: "72a15aed206d",
+ tel: "+19871234567",
+ name: "Mary Sue",
+ "address-line1": "1 New St",
+ timeLastUsed: 10009,
+ },
+ "2b4dce0fbc1f": {
+ "street-address": "123 Park St",
+ "address-level2": "Springfield",
+ "address-level1": "OR",
+ "postal-code": "97403",
+ country: "US",
+ email: "rita@foo.com",
+ "family-name": "Foo",
+ "given-name": "Rita",
+ guid: "2b4dce0fbc1f",
+ name: "Rita Foo",
+ "address-line1": "123 Park St",
+ timeLastUsed: 10005,
+ },
+ "46b2635a5b26": {
+ "street-address": "432 Another St",
+ "address-level2": "Springfield",
+ "address-level1": "OR",
+ "postal-code": "97402",
+ country: "US",
+ email: "rita@foo.com",
+ "family-name": "Foo",
+ "given-name": "Rita",
+ guid: "46b2635a5b26",
+ name: "Rita Foo",
+ "address-line1": "432 Another St",
+ timeLastUsed: 10003,
+ },
+};
+
+let BASIC_CARDS_1 = {
+ "53f9d009aed2": {
+ billingAddressGUID: "68gjdh354j",
+ methodName: "basic-card",
+ "cc-number": "************5461",
+ guid: "53f9d009aed2",
+ version: 3,
+ timeCreated: 1505240896213,
+ timeLastModified: 1515609524588,
+ timeLastUsed: 10000,
+ timesUsed: 0,
+ "cc-name": "John Smith",
+ "cc-exp-month": 6,
+ "cc-exp-year": 2024,
+ "cc-type": "visa",
+ "cc-given-name": "John",
+ "cc-additional-name": "",
+ "cc-family-name": "Smith",
+ "cc-exp": "2024-06",
+ },
+ "9h5d4h6f4d1s": {
+ methodName: "basic-card",
+ "cc-number": "************0954",
+ guid: "9h5d4h6f4d1s",
+ version: 3,
+ timeCreated: 1517890536491,
+ timeLastModified: 1517890564518,
+ timeLastUsed: 50000,
+ timesUsed: 0,
+ "cc-name": "Jane Doe",
+ "cc-exp-month": 5,
+ "cc-exp-year": 2023,
+ "cc-type": "mastercard",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Doe",
+ "cc-exp": "2023-05",
+ },
+ "123456789abc": {
+ methodName: "basic-card",
+ "cc-number": "************1234",
+ guid: "123456789abc",
+ version: 3,
+ timeCreated: 1517890536491,
+ timeLastModified: 1517890564518,
+ timeLastUsed: 90000,
+ timesUsed: 0,
+ "cc-name": "Jane Fields",
+ "cc-given-name": "Jane",
+ "cc-additional-name": "",
+ "cc-family-name": "Fields",
+ "cc-type": "discover",
+ },
+ "amex-card": {
+ methodName: "basic-card",
+ billingAddressGUID: "68gjdh354j",
+ "cc-number": "************1941",
+ guid: "amex-card",
+ version: 1,
+ timeCreated: 1517890536491,
+ timeLastModified: 1517890564518,
+ timeLastUsed: 70000,
+ timesUsed: 0,
+ "cc-name": "Capt America",
+ "cc-given-name": "Capt",
+ "cc-additional-name": "",
+ "cc-family-name": "America",
+ "cc-type": "amex",
+ "cc-exp-month": 6,
+ "cc-exp-year": 2023,
+ "cc-exp": "2023-06",
+ },
+ "missing-cc-name": {
+ methodName: "basic-card",
+ "cc-number": "************8563",
+ guid: "missing-cc-name",
+ version: 3,
+ timeCreated: 1517890536491,
+ timeLastModified: 1517890564518,
+ timeLastUsed: 30000,
+ timesUsed: 0,
+ "cc-exp-month": 8,
+ "cc-exp-year": 2024,
+ "cc-exp": "2024-08",
+ },
+};
+
+let buttonActions = {
+ debugFrame() {
+ let event = new CustomEvent("paymentContentToChrome", {
+ bubbles: true,
+ detail: {
+ messageType: "debugFrame",
+ },
+ });
+ document.dispatchEvent(event);
+ },
+
+ delete1Address() {
+ let savedAddresses = Object.assign(
+ {},
+ requestStore.getState().savedAddresses
+ );
+ delete savedAddresses[Object.keys(savedAddresses)[0]];
+ // Use setStateFromParent since it ensures there is no dangling
+ // `selectedShippingAddress` foreign key (FK) reference.
+ paymentDialog.setStateFromParent({
+ savedAddresses,
+ });
+ },
+
+ delete1Card() {
+ let savedBasicCards = Object.assign(
+ {},
+ requestStore.getState().savedBasicCards
+ );
+ delete savedBasicCards[Object.keys(savedBasicCards)[0]];
+ // Use setStateFromParent since it ensures there is no dangling
+ // `selectedPaymentCard` foreign key (FK) reference.
+ paymentDialog.setStateFromParent({
+ savedBasicCards,
+ });
+ },
+
+ 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);
+ },
+
+ rerender() {
+ requestStore.setState({});
+ },
+
+ saveVisibleForm() {
+ // Bypasses field validation which is useful to test error handling.
+ paymentDialog
+ .querySelector("#main-container > .page:not([hidden])")
+ .saveRecord();
+ },
+
+ setAddresses1() {
+ paymentDialog.setStateFromParent({ savedAddresses: ADDRESSES_1 });
+ },
+
+ setDupesAddresses() {
+ paymentDialog.setStateFromParent({ savedAddresses: DUPED_ADDRESSES });
+ },
+
+ setBasicCards1() {
+ paymentDialog.setStateFromParent({ savedBasicCards: BASIC_CARDS_1 });
+ },
+
+ setBasicCardErrors() {
+ let request = Object.assign({}, requestStore.getState().request);
+ request.paymentDetails = Object.assign(
+ {},
+ requestStore.getState().request.paymentDetails
+ );
+ request.paymentDetails.paymentMethodErrors = {
+ cardNumber: "",
+ cardholderName: "",
+ cardSecurityCode: "",
+ expiryMonth: "",
+ expiryYear: "",
+ billingAddress: {
+ addressLine:
+ "Can only buy from ROADS, not DRIVES, BOULEVARDS, or STREETS",
+ city: "Can only buy from CITIES, not TOWNSHIPS or VILLAGES",
+ country: "Can only buy from US, not CA",
+ dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
+ organization: "Can only buy from CORPORATIONS, not CONSORTIUMS",
+ phone: "Only allowed to buy from area codes that start with 9",
+ postalCode: "Only allowed to buy from postalCodes that start with 0",
+ recipient: "Can only buy from names that start with J",
+ region: "Can only buy from regions that start with M",
+ regionCode: "Regions must be 1 to 3 characters in length",
+ },
+ };
+ requestStore.setState({
+ request,
+ });
+ },
+
+ setChangesPrevented(evt) {
+ requestStore.setState({
+ changesPrevented: evt.target.checked,
+ });
+ },
+
+ setCompleteStatus() {
+ let input = document.querySelector("[name='setCompleteStatus']:checked");
+ let completeStatus = input.value;
+ let request = requestStore.getState().request;
+ paymentDialog.setStateFromParent({
+ request: Object.assign({}, request, { completeStatus }),
+ });
+ },
+
+ setPayerErrors() {
+ let request = Object.assign({}, requestStore.getState().request);
+ request.paymentDetails = Object.assign(
+ {},
+ requestStore.getState().request.paymentDetails
+ );
+ request.paymentDetails.payerErrors = {
+ email: "Only @mozilla.com emails are supported",
+ name: "Payer name must start with M",
+ phone: "Payer area codes must start with 1",
+ };
+ requestStore.setState({
+ request,
+ });
+ },
+
+ setPaymentOptions() {
+ let options = {};
+ let checkboxes = document.querySelectorAll(
+ "#paymentOptions input[type='checkbox']"
+ );
+ for (let input of checkboxes) {
+ options[input.name] = input.checked;
+ }
+ let req = Object.assign({}, requestStore.getState().request, {
+ paymentOptions: options,
+ });
+ requestStore.setState({ request: req });
+ },
+
+ setRequest1() {
+ paymentDialog.setStateFromParent({ request: REQUEST_1 });
+ },
+
+ setRequest2() {
+ paymentDialog.setStateFromParent({ request: REQUEST_2 });
+ },
+
+ setRequestPayerName() {
+ buttonActions.setPaymentOptions();
+ },
+ setRequestPayerEmail() {
+ buttonActions.setPaymentOptions();
+ },
+ setRequestPayerPhone() {
+ buttonActions.setPaymentOptions();
+ },
+ setRequestShipping() {
+ buttonActions.setPaymentOptions();
+ },
+
+ setShippingError() {
+ let request = Object.assign({}, requestStore.getState().request);
+ request.paymentDetails = Object.assign(
+ {},
+ requestStore.getState().request.paymentDetails
+ );
+ request.paymentDetails.error = "Shipping Error!";
+ request.paymentDetails.shippingOptions = [];
+ requestStore.setState({
+ request,
+ });
+ },
+
+ setShippingAddressErrors() {
+ let request = Object.assign({}, requestStore.getState().request);
+ request.paymentDetails = Object.assign(
+ {},
+ requestStore.getState().request.paymentDetails
+ );
+ request.paymentDetails.shippingAddressErrors = {
+ addressLine: "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
+ city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
+ country: "Can only ship to USA, not CA",
+ dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
+ organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
+ phone: "Only allowed to ship to area codes that start with 9",
+ postalCode: "Only allowed to ship to postalCodes that start with 0",
+ recipient: "Can only ship to names that start with J",
+ region: "Can only ship to regions that start with M",
+ regionCode: "Regions must be 1 to 3 characters in length",
+ };
+ requestStore.setState({
+ request,
+ });
+ },
+
+ toggleDirectionality() {
+ let body = paymentDialog.ownerDocument.body;
+ body.dir = body.dir == "rtl" ? "ltr" : "rtl";
+ },
+
+ toggleBranding() {
+ for (let container of paymentDialog.querySelectorAll("accepted-cards")) {
+ container.classList.toggle("branded");
+ }
+ },
+};
+
+window.addEventListener("click", function onButtonClick(evt) {
+ let id = evt.target.id || evt.target.name;
+ if (!id || typeof buttonActions[id] != "function") {
+ return;
+ }
+
+ buttonActions[id](evt);
+});
+
+window.addEventListener("DOMContentLoaded", function onDCL() {
+ if (window.location.protocol == "resource:") {
+ // Only show the debug frame button if we're running from a resource URI
+ // so it doesn't show during development over file: or http: since it won't work.
+ // Note that the button still won't work if resource://payments/paymentRequest.xhtml
+ // is manually loaded in a tab but will be shown.
+ document.getElementById("debugFrame").hidden = false;
+ }
+
+ requestStore.subscribe(paymentOptionsUpdater);
+ paymentOptionsUpdater.render(requestStore.getState());
+});
diff --git a/browser/components/payments/res/mixins/HandleEventMixin.js b/browser/components/payments/res/mixins/HandleEventMixin.js
new file mode 100644
index 0000000000..8d09ac2207
--- /dev/null
+++ b/browser/components/payments/res/mixins/HandleEventMixin.js
@@ -0,0 +1,28 @@
+/* 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/. */
+
+/**
+ * A mixin to forward events to on* methods if defined.
+ *
+ * @param {string} superclass The class to extend.
+ * @returns {class}
+ */
+export default function HandleEventMixin(superclass) {
+ return class HandleEvent extends superclass {
+ handleEvent(evt) {
+ function capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+ if (super.handleEvent) {
+ super.handleEvent(evt);
+ }
+ // Check whether event name is a defined function in object.
+ let fn = "on" + capitalize(evt.type);
+ if (this[fn] && typeof this[fn] === "function") {
+ return this[fn](evt);
+ }
+ return null;
+ }
+ };
+}
diff --git a/browser/components/payments/res/mixins/ObservedPropertiesMixin.js b/browser/components/payments/res/mixins/ObservedPropertiesMixin.js
new file mode 100644
index 0000000000..5fe71af90c
--- /dev/null
+++ b/browser/components/payments/res/mixins/ObservedPropertiesMixin.js
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * Define getters and setters for observedAttributes converted to camelCase and
+ * trigger a batched aynchronous call to `render` upon observed
+ * attribute/property changes.
+ */
+
+export default function ObservedPropertiesMixin(superClass) {
+ return class ObservedProperties extends superClass {
+ static kebabToCamelCase(name) {
+ return name.replace(/-([a-z])/g, ($0, $1) => $1.toUpperCase());
+ }
+
+ constructor() {
+ super();
+
+ this._observedPropertiesMixin = {
+ pendingRender: false,
+ };
+
+ // Reflect property changes for `observedAttributes` to attributes.
+ for (let name of this.constructor.observedAttributes || []) {
+ if (name in this) {
+ // Don't overwrite existing properties.
+ continue;
+ }
+ // Convert attribute names from kebab-case to camelCase properties
+ Object.defineProperty(this, ObservedProperties.kebabToCamelCase(name), {
+ configurable: true,
+ get() {
+ return this.getAttribute(name);
+ },
+ set(value) {
+ if (value === null || value === undefined || value === false) {
+ this.removeAttribute(name);
+ } else {
+ this.setAttribute(name, value);
+ }
+ },
+ });
+ }
+ }
+
+ async _invalidateFromObservedPropertiesMixin() {
+ if (this._observedPropertiesMixin.pendingRender) {
+ return;
+ }
+
+ this._observedPropertiesMixin.pendingRender = true;
+ await Promise.resolve();
+ try {
+ this.render();
+ } finally {
+ this._observedPropertiesMixin.pendingRender = false;
+ }
+ }
+
+ attributeChangedCallback(attr, oldValue, newValue) {
+ if (super.attributeChangedCallback) {
+ super.attributeChangedCallback(attr, oldValue, newValue);
+ }
+ if (oldValue === newValue) {
+ return;
+ }
+ this._invalidateFromObservedPropertiesMixin();
+ }
+ };
+}
diff --git a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
new file mode 100644
index 0000000000..ede9e1bfc5
--- /dev/null
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import PaymentsStore from "../PaymentsStore.js";
+
+/**
+ * A mixin for a custom element to observe store changes to information about a payment request.
+ */
+
+/**
+ * State of the payment request dialog.
+ */
+export let requestStore = new PaymentsStore({
+ changesPrevented: false,
+ orderDetailsShowing: false,
+ "basic-card-page": {
+ guid: null,
+ // preserveFieldValues: true,
+ selectedStateKey: "selectedPaymentCard",
+ },
+ "shipping-address-page": {
+ guid: null,
+ },
+ "payer-address-page": {
+ guid: null,
+ },
+ "billing-address-page": {
+ guid: null,
+ },
+ "payment-summary": {},
+ page: {
+ id: "payment-summary",
+ previousId: null,
+ // onboardingWizard: true,
+ // error: "",
+ },
+ request: {
+ completeStatus: "",
+ tabId: null,
+ topLevelPrincipal: { URI: { displayHost: null } },
+ requestId: null,
+ paymentMethods: [],
+ paymentDetails: {
+ id: null,
+ totalItem: { label: null, amount: { currency: null, value: 0 } },
+ displayItems: [],
+ payerErrors: {},
+ paymentMethodErrors: null,
+ shippingAddressErrors: {},
+ shippingOptions: [],
+ modifiers: null,
+ error: "",
+ },
+ paymentOptions: {
+ requestPayerName: false,
+ requestPayerEmail: false,
+ requestPayerPhone: false,
+ requestShipping: false,
+ shippingType: "shipping",
+ },
+ shippingOption: null,
+ },
+ selectedPayerAddress: null,
+ selectedPaymentCard: null,
+ selectedPaymentCardSecurityCode: null,
+ selectedShippingAddress: null,
+ selectedShippingOption: null,
+ savedAddresses: {},
+ savedBasicCards: {},
+ tempAddresses: {},
+ tempBasicCards: {},
+});
+
+/**
+ * A mixin to render UI based upon the requestStore and get updated when that store changes.
+ *
+ * Attaches `requestStore` to the element to give access to the store.
+ * @param {class} superClass The class to extend
+ * @returns {class}
+ */
+export default function PaymentStateSubscriberMixin(superClass) {
+ return class PaymentStateSubscriber extends superClass {
+ constructor() {
+ super();
+ this.requestStore = requestStore;
+ }
+
+ connectedCallback() {
+ this.requestStore.subscribe(this);
+ this.render(this.requestStore.getState());
+ if (super.connectedCallback) {
+ super.connectedCallback();
+ }
+ }
+
+ disconnectedCallback() {
+ this.requestStore.unsubscribe(this);
+ if (super.disconnectedCallback) {
+ super.disconnectedCallback();
+ }
+ }
+
+ /**
+ * Called by the store upon state changes.
+ * @param {object} state The current state
+ */
+ stateChangeCallback(state) {
+ this.render(state);
+ }
+ };
+}
diff --git a/browser/components/payments/res/paymentRequest.css b/browser/components/payments/res/paymentRequest.css
new file mode 100644
index 0000000000..ef03c745ae
--- /dev/null
+++ b/browser/components/payments/res/paymentRequest.css
@@ -0,0 +1,265 @@
+/* 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/. */
+
+:root {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ margin: 0;
+ /* Override font-size from in-content/common.css which is too large */
+ font-size: inherit;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+#debugging-console {
+ /* include the default borders in the max-height */
+ box-sizing: border-box;
+ float: right;
+ height: 100vh;
+ /* Float above the other overlays */
+ position: relative;
+ z-index: 99;
+}
+
+payment-dialog {
+ box-sizing: border-box;
+ display: grid;
+ grid-template: "header" auto
+ "main" 1fr
+ "disabled-overlay" auto;
+ height: 100%;
+}
+
+payment-dialog > header,
+.page > .page-body,
+.page > footer {
+ padding: 0 10%;
+}
+
+payment-dialog > header {
+ border-bottom: 1px solid rgba(0,0,0,0.1);
+ display: flex;
+ /* Wrap so that the error text appears full-width above the rest of the contents */
+ flex-wrap: wrap;
+ /* from visual spec: */
+ padding-bottom: 19px;
+ padding-top: 19px;
+}
+
+payment-dialog > header > .page-error:empty {
+ display: none;
+}
+
+payment-dialog > header > .page-error {
+ background: #D70022;
+ border-radius: 3px;
+ color: white;
+ padding: 6px;
+ width: 100%;
+}
+
+#main-container {
+ display: flex;
+ grid-area: main;
+ position: relative;
+ max-height: 100%;
+}
+
+.page {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ position: relative;
+ width: 100%;
+}
+
+.page > .page-body {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ /* The area above the footer should scroll, if necessary. */
+ overflow: auto;
+ padding-top: 18px;
+}
+
+.page > .page-body > h2:empty {
+ display: none;
+}
+
+.page-error {
+ color: #D70022;
+}
+
+.manage-text {
+ margin: 0;
+ padding: 18px 0;
+}
+
+.page > footer {
+ align-items: center;
+ justify-content: end;
+ background-color: #eaeaee;
+ display: flex;
+ /* from visual spec: */
+ padding-top: 20px;
+ padding-bottom: 18px;
+}
+
+#order-details-overlay {
+ background-color: var(--in-content-page-background);
+ overflow: auto;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+}
+
+#total {
+ flex: 1 1 auto;
+ margin: 5px;
+}
+
+#total > currency-amount {
+ color: var(--in-content-link-color);
+ font-size: 1.5em;
+}
+
+#total > currency-amount > .currency-code {
+ color: GrayText;
+ font-size: 1rem;
+}
+
+#total > div {
+ color: GrayText;
+}
+
+#view-all {
+ flex: 0 1 auto;
+}
+
+payment-dialog[complete-status="processing"] #pay {
+ /* Force opacity to 1 even when disabled in the processing state. */
+ opacity: 1;
+}
+
+payment-dialog #pay::before {
+ -moz-context-properties: fill;
+ content: url(chrome://browser/skin/connection-secure.svg);
+ fill: currentColor;
+ height: 16px;
+ margin-inline-end: 0.5em;
+ vertical-align: text-bottom;
+ width: 16px;
+}
+
+payment-dialog[changes-prevented][complete-status="fail"] #pay,
+payment-dialog[changes-prevented][complete-status="unknown"] #pay,
+payment-dialog[changes-prevented][complete-status="processing"] #pay,
+payment-dialog[changes-prevented][complete-status="success"] #pay {
+ /* Show the pay button above #disabled-overlay */
+ position: relative;
+ z-index: 51;
+}
+
+#disabled-overlay {
+ background: white;
+ grid-area: disabled-overlay;
+ opacity: 0.6;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ /* z-index must be greater than some positioned fields and #pay with z-index
+ but less than 99, the z-index of the debugging console. */
+ z-index: 50;
+}
+
+.persist-checkbox {
+ padding: 5px 0;
+}
+
+.persist-checkbox > label {
+ display: flex;
+ align-items: center;
+}
+
+.info-tooltip {
+ display: inline-block;
+ background-image: url(chrome://global/skin/icons/help.svg);
+ width: 16px;
+ height: 16px;
+ padding: 2px 4px;
+ background-repeat: no-repeat;
+ background-position: center;
+ position: relative;
+}
+
+.info-tooltip:focus::after,
+.info-tooltip:hover::after {
+ content: attr(aria-label);
+ display: block;
+ position: absolute;
+ padding: 3px 5px;
+ background-color: #fff;
+ border: 1px solid #bebebf;
+ box-shadow: 1px 1px 3px #bebebf;
+ font-size: smaller;
+ line-height: normal;
+ width: 188px;
+ /* Center the tooltip over the (i) icon (188px / 2 - 5px (padding) - 1px (border)). */
+ left: -86px;
+ bottom: 20px;
+}
+
+.info-tooltip:dir(rtl):focus::after,
+.info-tooltip:dir(rtl):hover::after {
+ left: auto;
+ right: -86px;
+}
+
+.csc.info-tooltip:focus::after,
+.csc.info-tooltip:hover::after {
+ /* Right-align the tooltip over the (i) icon (-188px - 60px (padding) - 2px (border) + 4px ((i) start padding) + 16px ((i) icon width)). */
+ left: -226px;
+ background-position: top 5px left 5px;
+ background-image: url(./containers/cvv-hint-image-back.svg);
+ background-repeat: no-repeat;
+ padding-inline-start: 55px;
+}
+
+.csc.info-tooltip[cc-type="amex"]::after {
+ background-image: url(./containers/cvv-hint-image-front.svg);
+}
+
+.csc.info-tooltip:dir(rtl):focus::after,
+.csc.info-tooltip:dir(rtl):hover::after {
+ left: auto;
+ /* Left-align the tooltip over the (i) icon (-188px - 60px (padding) - 2px (border) + 4px ((i) start padding) + 16px ((i) icon width)). */
+ right: -226px;
+ background-position: top 5px right 5px;
+}
+
+.branding {
+ background-image: url(chrome://branding/content/icon32.png);
+ background-size: 16px;
+ background-repeat: no-repeat;
+ background-position: left center;
+ padding-inline-start: 20px;
+ line-height: 20px;
+ margin-inline-end: auto;
+}
+
+.branding:dir(rtl) {
+ background-position: right center;
+}
diff --git a/browser/components/payments/res/paymentRequest.js b/browser/components/payments/res/paymentRequest.js
new file mode 100644
index 0000000000..1a08184615
--- /dev/null
+++ b/browser/components/payments/res/paymentRequest.js
@@ -0,0 +1,356 @@
+/* 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/. */
+
+/**
+ * Loaded in the unprivileged frame of each payment dialog.
+ *
+ * Communicates with privileged code via DOM Events.
+ */
+
+/* import-globals-from unprivileged-fallbacks.js */
+
+var paymentRequest = {
+ _nextMessageID: 1,
+ domReadyPromise: null,
+
+ init() {
+ // listen to content
+ window.addEventListener("paymentChromeToContent", this);
+
+ window.addEventListener("keydown", this);
+
+ this.domReadyPromise = new Promise(function dcl(resolve) {
+ window.addEventListener("DOMContentLoaded", resolve, { once: true });
+ }).then(this.handleEvent.bind(this));
+
+ // This scope is now ready to listen to the initialization data
+ this.sendMessageToChrome("initializeRequest");
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.onPaymentRequestLoad();
+ break;
+ }
+ case "keydown": {
+ if (event.code != "KeyD" || !event.altKey || !event.ctrlKey) {
+ break;
+ }
+ this.toggleDebuggingConsole();
+ break;
+ }
+ case "unload": {
+ this.onPaymentRequestUnload();
+ break;
+ }
+ case "paymentChromeToContent": {
+ this.onChromeToContent(event);
+ break;
+ }
+ default: {
+ throw new Error("Unexpected event type");
+ }
+ }
+ },
+
+ /**
+ * @param {string} messageType
+ * @param {[object]} detail
+ * @returns {number} message ID to be able to identify a reply (where applicable).
+ */
+ sendMessageToChrome(messageType, detail = {}) {
+ let messageID = this._nextMessageID++;
+ log.debug("sendMessageToChrome:", messageType, messageID, detail);
+ let event = new CustomEvent("paymentContentToChrome", {
+ bubbles: true,
+ detail: Object.assign(
+ {
+ messageType,
+ messageID,
+ },
+ detail
+ ),
+ });
+ document.dispatchEvent(event);
+ return messageID;
+ },
+
+ toggleDebuggingConsole() {
+ let debuggingConsole = document.getElementById("debugging-console");
+ if (debuggingConsole.hidden && !debuggingConsole.src) {
+ debuggingConsole.src = "debugging.html";
+ }
+ debuggingConsole.hidden = !debuggingConsole.hidden;
+ },
+
+ onChromeToContent({ detail }) {
+ let { messageType } = detail;
+ log.debug("onChromeToContent:", messageType);
+
+ switch (messageType) {
+ case "responseSent": {
+ let { request } = document
+ .querySelector("payment-dialog")
+ .requestStore.getState();
+ document.querySelector("payment-dialog").requestStore.setState({
+ changesPrevented: true,
+ request: Object.assign({}, request, { completeStatus: "processing" }),
+ });
+ break;
+ }
+ case "showPaymentRequest": {
+ this.onShowPaymentRequest(detail);
+ break;
+ }
+ case "updateState": {
+ document.querySelector("payment-dialog").setStateFromParent(detail);
+ break;
+ }
+ }
+ },
+
+ onPaymentRequestLoad() {
+ log.debug("onPaymentRequestLoad");
+ window.addEventListener("unload", this, { once: true });
+
+ // Automatically show the debugging console if loaded with a truthy `debug` query parameter.
+ if (new URLSearchParams(location.search).get("debug")) {
+ this.toggleDebuggingConsole();
+ }
+ },
+
+ async onShowPaymentRequest(detail) {
+ // Handle getting called before the DOM is ready.
+ log.debug("onShowPaymentRequest:", detail);
+ await this.domReadyPromise;
+
+ log.debug("onShowPaymentRequest: domReadyPromise resolved");
+ log.debug("onShowPaymentRequest, isPrivate?", detail.isPrivate);
+
+ let paymentDialog = document.querySelector("payment-dialog");
+ let state = {
+ request: detail.request,
+ savedAddresses: detail.savedAddresses,
+ savedBasicCards: detail.savedBasicCards,
+ // Temp records can exist upon a reload during development.
+ tempAddresses: detail.tempAddresses,
+ tempBasicCards: detail.tempBasicCards,
+ isPrivate: detail.isPrivate,
+ page: {
+ id: "payment-summary",
+ },
+ };
+
+ let hasSavedAddresses = !!Object.keys(this.getAddresses(state)).length;
+ let hasSavedCards = !!Object.keys(this.getBasicCards(state)).length;
+ let shippingRequested = state.request.paymentOptions.requestShipping;
+
+ // Onboarding wizard flow.
+ if (!hasSavedAddresses && shippingRequested) {
+ state.page = {
+ id: "shipping-address-page",
+ onboardingWizard: true,
+ };
+
+ state["shipping-address-page"] = {
+ guid: null,
+ };
+ } else if (!hasSavedAddresses && !hasSavedCards) {
+ state.page = {
+ id: "billing-address-page",
+ onboardingWizard: true,
+ };
+
+ state["billing-address-page"] = {
+ guid: null,
+ };
+ } else if (!hasSavedCards) {
+ state.page = {
+ id: "basic-card-page",
+ onboardingWizard: true,
+ };
+ state["basic-card-page"] = {
+ selectedStateKey: "selectedPaymentCard",
+ };
+ }
+
+ await paymentDialog.setStateFromParent(state);
+
+ this.sendMessageToChrome("paymentDialogReady");
+ },
+
+ openPreferences() {
+ this.sendMessageToChrome("openPreferences");
+ },
+
+ cancel() {
+ this.sendMessageToChrome("paymentCancel");
+ },
+
+ pay(data) {
+ this.sendMessageToChrome("pay", data);
+ },
+
+ closeDialog() {
+ this.sendMessageToChrome("closeDialog");
+ },
+
+ changePaymentMethod(data) {
+ this.sendMessageToChrome("changePaymentMethod", data);
+ },
+
+ changeShippingAddress(data) {
+ this.sendMessageToChrome("changeShippingAddress", data);
+ },
+
+ changeShippingOption(data) {
+ this.sendMessageToChrome("changeShippingOption", data);
+ },
+
+ changePayerAddress(data) {
+ this.sendMessageToChrome("changePayerAddress", data);
+ },
+
+ /**
+ * Add/update an autofill storage record.
+ *
+ * If the the `guid` argument is provided update the record; otherwise, add it.
+ * @param {string} collectionName The autofill collection that record belongs to.
+ * @param {object} record The autofill record to add/update
+ * @param {string} [guid] The guid of the autofill record to update
+ * @returns {Promise} when the update response is received
+ */
+ updateAutofillRecord(collectionName, record, guid) {
+ return new Promise((resolve, reject) => {
+ let messageID = this.sendMessageToChrome("updateAutofillRecord", {
+ collectionName,
+ guid,
+ record,
+ });
+
+ window.addEventListener("paymentChromeToContent", function onMsg({
+ detail,
+ }) {
+ if (
+ detail.messageType != "updateAutofillRecord:Response" ||
+ detail.messageID != messageID
+ ) {
+ return;
+ }
+ log.debug("updateAutofillRecord: response:", detail);
+ window.removeEventListener("paymentChromeToContent", onMsg);
+ document
+ .querySelector("payment-dialog")
+ .setStateFromParent(detail.stateChange);
+ if (detail.error) {
+ reject(detail);
+ } else {
+ resolve(detail);
+ }
+ });
+ });
+ },
+
+ /**
+ * @param {object} state object representing the UI state
+ * @param {string} selectedMethodID (GUID) uniquely identifying the selected payment method
+ * @returns {object?} the applicable modifier for the payment method
+ */
+ getModifierForPaymentMethod(state, selectedMethodID) {
+ let basicCards = this.getBasicCards(state);
+ let selectedMethod = basicCards[selectedMethodID] || null;
+ if (selectedMethod && selectedMethod.methodName !== "basic-card") {
+ throw new Error(
+ `${selectedMethod.methodName} (${selectedMethodID}) ` +
+ `is not a supported payment method`
+ );
+ }
+ let modifiers = state.request.paymentDetails.modifiers;
+ if (!selectedMethod || !modifiers || !modifiers.length) {
+ return null;
+ }
+ let appliedModifier = modifiers.find(modifier => {
+ // take the first matching modifier
+ if (
+ modifier.supportedMethods &&
+ modifier.supportedMethods != selectedMethod.methodName
+ ) {
+ return false;
+ }
+ let supportedNetworks =
+ (modifier.data && modifier.data.supportedNetworks) || [];
+ return (
+ !supportedNetworks.length ||
+ supportedNetworks.includes(selectedMethod["cc-type"])
+ );
+ });
+ return appliedModifier || null;
+ },
+
+ /**
+ * @param {object} state object representing the UI state
+ * @returns {object} in the shape of `nsIPaymentItem` representing the total
+ * that applies to the selected payment method.
+ */
+ getTotalItem(state) {
+ let methodID = state.selectedPaymentCard;
+ if (methodID) {
+ let modifier = paymentRequest.getModifierForPaymentMethod(
+ state,
+ methodID
+ );
+ if (modifier && modifier.hasOwnProperty("total")) {
+ return modifier.total;
+ }
+ }
+ return state.request.paymentDetails.totalItem;
+ },
+
+ onPaymentRequestUnload() {
+ // remove listeners that may be used multiple times here
+ window.removeEventListener("paymentChromeToContent", this);
+ },
+
+ _sortObjectsByTimeLastUsed(objects) {
+ let sortedValues = Object.values(objects).sort((a, b) => {
+ let aLastUsed = a.timeLastUsed || a.timeLastModified;
+ let bLastUsed = b.timeLastUsed || b.timeLastModified;
+ return bLastUsed - aLastUsed;
+ });
+ let sortedObjects = {};
+ for (let obj of sortedValues) {
+ sortedObjects[obj.guid] = obj;
+ }
+ return sortedObjects;
+ },
+
+ getAddresses(state) {
+ let addresses = Object.assign(
+ {},
+ state.savedAddresses,
+ state.tempAddresses
+ );
+ return this._sortObjectsByTimeLastUsed(addresses);
+ },
+
+ getBasicCards(state) {
+ let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
+ return this._sortObjectsByTimeLastUsed(cards);
+ },
+
+ maybeCreateFieldErrorElement(container) {
+ let span = container.querySelector(".error-text");
+ if (!span) {
+ span = document.createElement("span");
+ span.className = "error-text";
+ container.appendChild(span);
+ }
+ return span;
+ },
+};
+
+paymentRequest.init();
+
+export default paymentRequest;
diff --git a/browser/components/payments/res/paymentRequest.xhtml b/browser/components/payments/res/paymentRequest.xhtml
new file mode 100644
index 0000000000..e88b529333
--- /dev/null
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -0,0 +1,303 @@
+<?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 [
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+
+ <!ENTITY viewAllItems "View All Items">
+ <!ENTITY paymentSummaryTitle "Your Payment">
+ <!ENTITY header.payTo "Pay to">
+ <!ENTITY fieldRequiredSymbol "*">
+
+ <!ENTITY shippingAddressLabel "Shipping Address">
+ <!ENTITY deliveryAddressLabel "Delivery Address">
+ <!ENTITY pickupAddressLabel "Pickup Address">
+ <!ENTITY shippingOptionsLabel "Shipping Options">
+ <!ENTITY deliveryOptionsLabel "Delivery Options">
+ <!ENTITY pickupOptionsLabel "Pickup Options">
+ <!ENTITY shippingGenericError "Can’t ship to this address. Select a different address.">
+ <!ENTITY deliveryGenericError "Can’t deliver to this address. Select a different address.">
+ <!ENTITY pickupGenericError "Can’t pick up from this address. Select a different address.">
+ <!ENTITY paymentMethodsLabel "Payment Method">
+ <!ENTITY address.fieldSeparator ", ">
+ <!ENTITY address.addLink.label "Add">
+ <!ENTITY address.editLink.label "Edit">
+ <!ENTITY basicCard.addLink.label "Add">
+ <!ENTITY basicCard.editLink.label "Edit">
+ <!ENTITY payer.addLink.label "Add">
+ <!ENTITY payer.editLink.label "Edit">
+ <!ENTITY shippingAddress.addPage.title "Add Shipping Address">
+ <!ENTITY shippingAddress.editPage.title "Edit Shipping Address">
+ <!ENTITY deliveryAddress.addPage.title "Add Delivery Address">
+ <!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
+ <!ENTITY pickupAddress.addPage.title "Add Pickup Address">
+ <!ENTITY pickupAddress.editPage.title "Edit Pickup Address">
+ <!ENTITY billingAddress.addPage.title "Add Billing Address">
+ <!ENTITY billingAddress.editPage.title "Edit Billing Address">
+ <!ENTITY basicCard.addPage.title "Add Credit Card">
+ <!ENTITY basicCard.editPage.title "Edit Credit Card">
+ <!ENTITY basicCard.csc.placeholder "CVV">
+ <!ENTITY basicCard.csc.back.infoTooltip "3 digit number found on the back of your credit card.">
+ <!ENTITY basicCard.csc.front.infoTooltip "3 digit number found on the front of your credit card.">
+ <!ENTITY payer.addPage.title "Add Payer Contact">
+ <!ENTITY payer.editPage.title "Edit Payer Contact">
+ <!ENTITY payerLabel "Contact Information">
+ <!ENTITY manageInPreferences "Manage saved address and credit card information in <a>&brandShortName; Preferences</a>.">
+ <!ENTITY manageInOptions "Manage saved address and credit card information in <a>&brandShortName; Options</a>.">
+ <!ENTITY cancelPaymentButton.label "Cancel">
+ <!ENTITY approvePaymentButton.label "Pay">
+ <!ENTITY processingPaymentButton.label "Processing">
+ <!ENTITY successPaymentButton.label "Done">
+ <!ENTITY unknownPaymentButton.label "Unknown">
+ <!ENTITY orderDetailsLabel "Order Details">
+ <!ENTITY orderTotalLabel "Total">
+ <!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
+ <!ENTITY basicCardPage.addressAddLink.label "Add">
+ <!ENTITY basicCardPage.addressEditLink.label "Edit">
+ <!ENTITY basicCardPage.backButton.label "Back">
+ <!ENTITY basicCardPage.nextButton.label "Next">
+ <!ENTITY basicCardPage.updateButton.label "Update">
+ <!ENTITY basicCardPage.persistCheckbox.label "Save credit card to &brandShortName; (CVV will not be saved)">
+ <!ENTITY basicCardPage.persistCheckbox.infoTooltip "&brandShortName; can securely store your credit card information to use in forms like this, so you don’t have to enter it every time.">
+ <!ENTITY addressPage.error.genericSave "There was an error saving the address.">
+ <!ENTITY addressPage.cancelButton.label "Cancel">
+ <!ENTITY addressPage.backButton.label "Back">
+ <!ENTITY addressPage.nextButton.label "Next">
+ <!ENTITY addressPage.updateButton.label "Update">
+ <!ENTITY addressPage.persistCheckbox.label "Save address to &brandShortName;">
+ <!ENTITY addressPage.persistCheckbox.infoTooltip "&brandShortName; can add your address to forms like this, so you don’t have to type it every time.">
+ <!ENTITY failErrorPage.title "We couldn’t complete your payment to **host-name**">
+ <!ENTITY failErrorPage.suggestionHeading "The most likely cause is a hiccup with your credit card.">
+ <!ENTITY failErrorPage.suggestion1 "Make sure the card you’re using hasn’t expired">
+ <!ENTITY failErrorPage.suggestion2 "Double check the card number and expiration date">
+ <!ENTITY failErrorPage.suggestion3 "If your credit card information is correct, contact the merchant for more information">
+ <!ENTITY failErrorPage.doneButton.label "Close">
+ <!ENTITY timeoutErrorPage.title "**host-name** is taking too long to respond.">
+ <!ENTITY timeoutErrorPage.suggestionHeading "The most likely cause is a temporary connection hiccup. Open a new tab to check your network connection or click “Close” to try again.">
+ <!ENTITY timeoutErrorPage.doneButton.label "Close">
+ <!ENTITY webPaymentsBranding.label "&brandShortName; Checkout">
+ <!ENTITY invalidOption.label "Missing or invalid information">
+ <!ENTITY acceptedCards.label "Merchant accepts:">
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&paymentSummaryTitle;</title>
+
+ <!-- chrome: is needed for global.dtd -->
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"/>
+ <link rel="stylesheet" href="chrome://formautofill/content/skin/editDialog-shared.css"/>
+ <link rel="stylesheet" href="chrome://formautofill/content/skin/editAddress.css"/>
+ <link rel="stylesheet" href="chrome://formautofill/content/skin/editCreditCard.css"/>
+ <link rel="stylesheet" href="chrome://formautofill/content/skin/editDialog.css"/>
+ <link rel="stylesheet" href="paymentRequest.css"/>
+ <link rel="stylesheet" href="components/rich-select.css"/>
+ <link rel="stylesheet" href="components/address-option.css"/>
+ <link rel="stylesheet" href="components/basic-card-option.css"/>
+ <link rel="stylesheet" href="components/shipping-option.css"/>
+ <link rel="stylesheet" href="components/payment-details-item.css"/>
+ <link rel="stylesheet" href="components/accepted-cards.css"/>
+ <link rel="stylesheet" href="containers/address-form.css"/>
+ <link rel="stylesheet" href="containers/basic-card-form.css"/>
+ <link rel="stylesheet" href="containers/order-details.css"/>
+ <link rel="stylesheet" href="containers/rich-picker.css"/>
+ <link rel="stylesheet" href="containers/error-page.css"/>
+
+ <script src="unprivileged-fallbacks.js"></script>
+
+ <script src="formautofill/autofillEditForms.js"></script>
+
+ <script type="module" src="containers/payment-dialog.js"></script>
+ <script type="module" src="paymentRequest.js"></script>
+
+ <template id="payment-dialog-template">
+ <header>
+ <div class="page-error"
+ data-shipping-generic-error="&shippingGenericError;"
+ data-delivery-generic-error="&deliveryGenericError;"
+ data-pickup-generic-error="&pickupGenericError;"
+ aria-live="polite"></div>
+ <div id="total">
+ <currency-amount display-code="display-code"></currency-amount>
+ <div>&header.payTo; <span id="host-name"></span></div>
+ </div>
+ <div id="top-buttons" hidden="hidden">
+ <button id="view-all" class="closed">&viewAllItems;</button>
+ </div>
+ </header>
+
+ <div id="main-container">
+ <payment-request-page id="payment-summary">
+ <div class="page-body">
+ <address-picker class="shipping-related"
+ data-add-link-label="&address.addLink.label;"
+ data-edit-link-label="&address.editLink.label;"
+ data-field-separator="&address.fieldSeparator;"
+ data-shipping-address-label="&shippingAddressLabel;"
+ data-delivery-address-label="&deliveryAddressLabel;"
+ data-pickup-address-label="&pickupAddressLabel;"
+ data-invalid-label="&invalidOption.label;"
+ selected-state-key="selectedShippingAddress"></address-picker>
+
+ <shipping-option-picker class="shipping-related"
+ data-shipping-options-label="&shippingOptionsLabel;"
+ data-delivery-options-label="&deliveryOptionsLabel;"
+ data-pickup-options-label="&pickupOptionsLabel;"></shipping-option-picker>
+
+ <payment-method-picker selected-state-key="selectedPaymentCard"
+ data-add-link-label="&basicCard.addLink.label;"
+ data-edit-link-label="&basicCard.editLink.label;"
+ data-csc-placeholder="&basicCard.csc.placeholder;"
+ data-csc-back-tooltip="&basicCard.csc.back.infoTooltip;"
+ data-csc-front-tooltip="&basicCard.csc.front.infoTooltip;"
+ data-invalid-label="&invalidOption.label;"
+ label="&paymentMethodsLabel;">
+ </payment-method-picker>
+ <accepted-cards hidden="hidden" label="&acceptedCards.label;"></accepted-cards>
+ <address-picker class="payer-related"
+ label="&payerLabel;"
+ data-add-link-label="&payer.addLink.label;"
+ data-edit-link-label="&payer.editLink.label;"
+ data-field-separator="&address.fieldSeparator;"
+ data-invalid-label="&invalidOption.label;"
+ selected-state-key="selectedPayerAddress"></address-picker>
+
+ <p class="manage-text">
+ <span hidden="hidden" data-os="mac">&manageInPreferences;</span>
+ <span hidden="hidden">&manageInOptions;</span>
+ </p>
+ </div>
+
+ <footer>
+ <span class="branding">&webPaymentsBranding.label;</span>
+ <button id="cancel">&cancelPaymentButton.label;</button>
+ <button id="pay"
+ class="primary"
+ data-label="&approvePaymentButton.label;"
+ data-processing-label="&processingPaymentButton.label;"
+ data-unknown-label="&unknownPaymentButton.label;"
+ data-success-label="&successPaymentButton.label;"></button>
+ </footer>
+ </payment-request-page>
+ <section id="order-details-overlay" hidden="hidden">
+ <h2>&orderDetailsLabel;</h2>
+ <order-details></order-details>
+ </section>
+
+ <basic-card-form id="basic-card-page"
+ data-add-basic-card-title="&basicCard.addPage.title;"
+ data-edit-basic-card-title="&basicCard.editPage.title;"
+ data-error-generic-save="&basicCardPage.error.genericSave;"
+
+ data-address-add-link-label="&basicCardPage.addressAddLink.label;"
+ data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
+
+ data-invalid-address-label="&invalidOption.label;"
+ data-address-field-separator="&address.fieldSeparator;"
+ data-back-button-label="&basicCardPage.backButton.label;"
+ data-next-button-label="&basicCardPage.nextButton.label;"
+ data-update-button-label="&basicCardPage.updateButton.label;"
+ data-cancel-button-label="&cancelPaymentButton.label;"
+ data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
+ data-persist-checkbox-info-tooltip="&basicCardPage.persistCheckbox.infoTooltip;"
+ data-csc-placeholder="&basicCard.csc.placeholder;"
+ data-csc-back-info-tooltip="&basicCard.csc.back.infoTooltip;"
+ data-csc-front-info-tooltip="&basicCard.csc.front.infoTooltip;"
+ data-accepted-cards-label="&acceptedCards.label;"
+ data-field-required-symbol="&fieldRequiredSymbol;"
+ hidden="hidden"></basic-card-form>
+
+ <address-form id="shipping-address-page"
+ data-title-add="&shippingAddress.addPage.title;"
+ data-title-edit="&shippingAddress.editPage.title;"
+ data-error-generic-save="&addressPage.error.genericSave;"
+ data-cancel-button-label="&addressPage.cancelButton.label;"
+ data-back-button-label="&addressPage.backButton.label;"
+ data-next-button-label="&addressPage.nextButton.label;"
+ data-update-button-label="&addressPage.updateButton.label;"
+ data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
+ data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
+ data-field-required-symbol="&fieldRequiredSymbol;"
+ hidden="hidden"
+ selected-state-key="selectedShippingAddress"></address-form>
+
+ <address-form id="payer-address-page"
+ data-title-add="&payer.addPage.title;"
+ data-title-edit="&payer.editPage.title;"
+ data-error-generic-save="&addressPage.error.genericSave;"
+ data-cancel-button-label="&addressPage.cancelButton.label;"
+ data-back-button-label="&addressPage.backButton.label;"
+ data-next-button-label="&addressPage.nextButton.label;"
+ data-update-button-label="&addressPage.updateButton.label;"
+ data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
+ data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
+ data-field-required-symbol="&fieldRequiredSymbol;"
+ hidden="hidden"
+ selected-state-key="selectedPayerAddress"></address-form>
+
+ <address-form id="billing-address-page"
+ data-title-add="&billingAddress.addPage.title;"
+ data-title-edit="&billingAddress.editPage.title;"
+ data-error-generic-save="&addressPage.error.genericSave;"
+ data-cancel-button-label="&addressPage.cancelButton.label;"
+ data-back-button-label="&addressPage.backButton.label;"
+ data-next-button-label="&addressPage.nextButton.label;"
+ data-update-button-label="&addressPage.updateButton.label;"
+ data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
+ data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
+ data-field-required-symbol="&fieldRequiredSymbol;"
+ hidden="hidden"
+ selected-state-key="basic-card-page|billingAddressGUID"></address-form>
+
+ <completion-error-page id="completion-timeout-error" class="illustrated"
+ data-page-title="&timeoutErrorPage.title;"
+ data-suggestion-heading="&timeoutErrorPage.suggestionHeading;"
+ data-branding-label="&webPaymentsBranding.label;"
+ data-done-button-label="&timeoutErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
+ <completion-error-page id="completion-fail-error" class="illustrated"
+ data-page-title="&failErrorPage.title;"
+ data-suggestion-heading="&failErrorPage.suggestionHeading;"
+ data-suggestion-1="&failErrorPage.suggestion1;"
+ data-suggestion-2="&failErrorPage.suggestion2;"
+ data-suggestion-3="&failErrorPage.suggestion3;"
+ data-branding-label="&webPaymentsBranding.label;"
+ data-done-button-label="&failErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
+ </div>
+
+ <div id="disabled-overlay" hidden="hidden">
+ <!-- overlay to prevent changes while waiting for a response from the merchant -->
+ </div>
+ </template>
+
+ <template id="order-details-template">
+ <ul class="main-list"></ul>
+ <ul class="footer-items-list"></ul>
+
+ <div class="details-total">
+ <h2 class="label">&orderTotalLabel;</h2>
+ <currency-amount></currency-amount>
+ </div>
+ </template>
+</head>
+<body dir="&locale.dir;">
+ <iframe id="debugging-console"
+ hidden="hidden">
+ </iframe>
+ <payment-dialog data-shipping-address-title-add="&shippingAddress.addPage.title;"
+ data-shipping-address-title-edit="&shippingAddress.editPage.title;"
+ data-delivery-address-title-add="&deliveryAddress.addPage.title;"
+ data-delivery-address-title-edit="&deliveryAddress.editPage.title;"
+ data-pickup-address-title-add="&pickupAddress.addPage.title;"
+ data-pickup-address-title-edit="&pickupAddress.editPage.title;"
+ data-billing-address-title-add="&billingAddress.addPage.title;"
+ data-payer-title-add="&payer.addPage.title;"
+ data-payer-title-edit="&payer.editPage.title;"></payment-dialog>
+</body>
+</html>
diff --git a/browser/components/payments/res/unprivileged-fallbacks.js b/browser/components/payments/res/unprivileged-fallbacks.js
new file mode 100644
index 0000000000..ee7e47a7df
--- /dev/null
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+/**
+ * This file defines fallback objects to be used during development outside
+ * of the paymentDialogWrapper. When loaded in the wrapper, a frame script
+ * overwrites these methods. Since these methods need to get overwritten in the
+ * global scope, it can't be converted into an ES module.
+ */
+
+/* eslint-disable no-console */
+/* exported log, PaymentDialogUtils */
+
+"use strict";
+
+var log = {
+ error: console.error.bind(console, "paymentRequest.xhtml:"),
+ warn: console.warn.bind(console, "paymentRequest.xhtml:"),
+ info: console.info.bind(console, "paymentRequest.xhtml:"),
+ debug: console.debug.bind(console, "paymentRequest.xhtml:"),
+};
+
+var PaymentDialogUtils = {
+ getAddressLabel(address, addressFields = null) {
+ if (addressFields) {
+ let requestedFields = addressFields.trim().split(/\s+/);
+ return (
+ requestedFields
+ .filter(f => f && address[f])
+ .map(f => address[f])
+ .join(", ") + ` (${address.guid})`
+ );
+ }
+ return `${address.name} (${address.guid})`;
+ },
+
+ getCreditCardNetworks() {
+ // Shim for list of known and supported credit card network ids as exposed by
+ // toolkit/modules/CreditCard.jsm
+ return [
+ "amex",
+ "cartebancaire",
+ "diners",
+ "discover",
+ "jcb",
+ "mastercard",
+ "mir",
+ "unionpay",
+ "visa",
+ ];
+ },
+ isCCNumber(str) {
+ return !!str.replace(/[-\s]/g, "").match(/^\d{9,}$/);
+ },
+ DEFAULT_REGION: "US",
+ countries: new Map([
+ ["US", "United States"],
+ ["CA", "Canada"],
+ ["DE", "Germany"],
+ ]),
+ getFormFormat(country) {
+ if (country == "DE") {
+ return {
+ addressLevel3Label: "suburb",
+ addressLevel2Label: "city",
+ addressLevel1Label: "province",
+ addressLevel1Options: null,
+ postalCodeLabel: "postalCode",
+ fieldsOrder: [
+ {
+ fieldId: "name",
+ newLine: true,
+ },
+ {
+ fieldId: "organization",
+ newLine: true,
+ },
+ {
+ fieldId: "street-address",
+ newLine: true,
+ },
+ { fieldId: "postal-code" },
+ { fieldId: "address-level2" },
+ ],
+ postalCodePattern: "\\d{5}",
+ countryRequiredFields: [
+ "street-address",
+ "address-level2",
+ "postal-code",
+ ],
+ };
+ }
+
+ let addressLevel1Options = null;
+ if (country == "US") {
+ addressLevel1Options = new Map([
+ ["CA", "California"],
+ ["MA", "Massachusetts"],
+ ["MI", "Michigan"],
+ ]);
+ } else if (country == "CA") {
+ addressLevel1Options = new Map([
+ ["NS", "Nova Scotia"],
+ ["ON", "Ontario"],
+ ["YT", "Yukon"],
+ ]);
+ }
+
+ let fieldsOrder = [
+ { fieldId: "name", newLine: true },
+ { fieldId: "street-address", newLine: true },
+ { fieldId: "address-level2" },
+ { fieldId: "address-level1" },
+ { fieldId: "postal-code" },
+ { fieldId: "organization" },
+ ];
+ if (country == "BR") {
+ fieldsOrder.splice(2, 0, { fieldId: "address-level3" });
+ }
+
+ return {
+ addressLevel3Label: "suburb",
+ addressLevel2Label: "city",
+ addressLevel1Label: country == "US" ? "state" : "province",
+ addressLevel1Options,
+ postalCodeLabel: country == "US" ? "zip" : "postalCode",
+ fieldsOrder,
+ // The following values come from addressReferences.js and should not be changed.
+ /* eslint-disable-next-line max-len */
+ postalCodePattern:
+ country == "US"
+ ? "(\\d{5})(?:[ \\-](\\d{4}))?"
+ : "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
+ countryRequiredFields:
+ country == "US" || country == "CA"
+ ? [
+ "street-address",
+ "address-level2",
+ "address-level1",
+ "postal-code",
+ ]
+ : ["street-address", "address-level2", "postal-code"],
+ };
+ },
+ findAddressSelectOption(selectEl, address, fieldName) {
+ return null;
+ },
+ getDefaultPreferences() {
+ let prefValues = {
+ saveCreditCardDefaultChecked: false,
+ saveAddressDefaultChecked: true,
+ };
+ return prefValues;
+ },
+ isOfficialBranding() {
+ return false;
+ },
+};