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