summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/res/containers/payment-dialog.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/payments/res/containers/payment-dialog.js')
-rw-r--r--browser/components/payments/res/containers/payment-dialog.js593
1 files changed, 593 insertions, 0 deletions
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);