forked from mirrors/gecko-dev
		
	Differential Revision: https://phabricator.services.mozilla.com/D45629 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			356 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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;
 |