diff options
Diffstat (limited to 'browser/components/payments/PaymentUIService.jsm')
-rw-r--r-- | browser/components/payments/PaymentUIService.jsm | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/browser/components/payments/PaymentUIService.jsm b/browser/components/payments/PaymentUIService.jsm new file mode 100644 index 0000000000..a0b60d8c89 --- /dev/null +++ b/browser/components/payments/PaymentUIService.jsm @@ -0,0 +1,352 @@ +/* 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/. */ + +/** + * Singleton service acting as glue between the DOM APIs and the payment dialog UI. + * + * Communication from the DOM to the UI happens via the nsIPaymentUIService interface. + * The UI talks to the DOM code via the nsIPaymentRequestService interface. + * PaymentUIService is started by the DOM code lazily. + * + * For now the UI is shown in a native dialog but that is likely to change. + * Tests should try to avoid relying on that implementation detail. + */ + +"use strict"; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "BrowserWindowTracker", + "resource:///modules/BrowserWindowTracker.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "paymentSrv", + "@mozilla.org/dom/payments/payment-request-service;1", + "nsIPaymentRequestService" +); + +function PaymentUIService() { + this.wrappedJSObject = this; + XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = ChromeUtils.import( + "resource://gre/modules/Console.jsm" + ); + return new ConsoleAPI({ + maxLogLevelPref: "dom.payments.loglevel", + prefix: "Payment UI Service", + }); + }); + this.log.debug("constructor"); +} + +PaymentUIService.prototype = { + classID: Components.ID("{01f8bd55-9017-438b-85ec-7c15d2b35cdc}"), + QueryInterface: ChromeUtils.generateQI(["nsIPaymentUIService"]), + + // nsIPaymentUIService implementation: + + showPayment(requestId) { + this.log.debug("showPayment:", requestId); + let request = paymentSrv.getPaymentRequestById(requestId); + let merchantBrowser = this.findBrowserByOuterWindowId( + request.topOuterWindowId + ); + let chromeWindow = merchantBrowser.ownerGlobal; + let { gBrowser } = chromeWindow; + let browserContainer = gBrowser.getBrowserContainer(merchantBrowser); + let container = chromeWindow.document.createElementNS(XHTML_NS, "div"); + container.dataset.requestId = requestId; + container.classList.add("paymentDialogContainer"); + container.hidden = true; + let paymentsBrowser = this._createPaymentFrame( + chromeWindow.document, + requestId + ); + + let pdwGlobal = {}; + Services.scriptloader.loadSubScript( + "chrome://payments/content/paymentDialogWrapper.js", + pdwGlobal + ); + + paymentsBrowser.paymentDialogWrapper = pdwGlobal.paymentDialogWrapper; + + // Create an <html:div> wrapper to absolutely position the <xul:browser> + // because XUL elements don't support position:absolute. + let absDiv = chromeWindow.document.createElementNS(XHTML_NS, "div"); + container.appendChild(absDiv); + + // append the frame to start the loading + absDiv.appendChild(paymentsBrowser); + browserContainer.prepend(container); + + // Initialize the wrapper once the <browser> is connected. + paymentsBrowser.paymentDialogWrapper.init(requestId, paymentsBrowser); + + this._attachBrowserEventListeners(merchantBrowser); + + // Only show the frame and change the UI when the dialog is ready to show. + paymentsBrowser.addEventListener( + "tabmodaldialogready", + function readyToShow() { + if (!container) { + // The dialog was closed by the DOM code before it was ready to be shown. + return; + } + container.hidden = false; + this._showDialog(merchantBrowser); + }.bind(this), + { + once: true, + } + ); + }, + + abortPayment(requestId) { + this.log.debug("abortPayment:", requestId); + let abortResponse = Cc[ + "@mozilla.org/dom/payments/payment-abort-action-response;1" + ].createInstance(Ci.nsIPaymentAbortActionResponse); + let found = this.closeDialog(requestId); + + // if `win` is falsy, then we haven't found the dialog, so the abort fails + // otherwise, the abort is successful + let response = found + ? Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED + : Ci.nsIPaymentActionResponse.ABORT_FAILED; + + abortResponse.init(requestId, response); + paymentSrv.respondPayment(abortResponse); + }, + + completePayment(requestId) { + // completeStatus should be one of "timeout", "success", "fail", "" + let { completeStatus } = paymentSrv.getPaymentRequestById(requestId); + this.log.debug( + `completePayment: requestId: ${requestId}, completeStatus: ${completeStatus}` + ); + + let closed; + switch (completeStatus) { + case "fail": + case "timeout": + break; + default: + closed = this.closeDialog(requestId); + break; + } + + let paymentFrame; + if (!closed) { + // We need to call findDialog before we respond below as getPaymentRequestById + // may fail due to the request being removed upon completion. + paymentFrame = this.findDialog(requestId).paymentFrame; + if (!paymentFrame) { + this.log.error("completePayment: no dialog found"); + return; + } + } + + let responseCode = closed + ? Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED + : Ci.nsIPaymentActionResponse.COMPLETE_FAILED; + let completeResponse = Cc[ + "@mozilla.org/dom/payments/payment-complete-action-response;1" + ].createInstance(Ci.nsIPaymentCompleteActionResponse); + completeResponse.init(requestId, responseCode); + paymentSrv.respondPayment( + completeResponse.QueryInterface(Ci.nsIPaymentActionResponse) + ); + + if (!closed) { + paymentFrame.paymentDialogWrapper.updateRequest(); + } + }, + + updatePayment(requestId) { + let { paymentFrame } = this.findDialog(requestId); + this.log.debug("updatePayment:", requestId); + if (!paymentFrame) { + this.log.error("updatePayment: no dialog found"); + return; + } + paymentFrame.paymentDialogWrapper.updateRequest(); + }, + + closePayment(requestId) { + this.closeDialog(requestId); + }, + + // other helper methods + + _createPaymentFrame(doc, requestId) { + let frame = doc.createXULElement("browser"); + frame.classList.add("paymentDialogContainerFrame"); + frame.setAttribute("type", "content"); + frame.setAttribute("remote", "true"); + frame.setAttribute("disablehistory", "true"); + frame.setAttribute("nodefaultsrc", "true"); + frame.setAttribute("transparent", "true"); + frame.setAttribute("selectmenulist", "ContentSelectDropdown"); + frame.setAttribute("autocompletepopup", "PopupAutoComplete"); + return frame; + }, + + _attachBrowserEventListeners(merchantBrowser) { + merchantBrowser.addEventListener("SwapDocShells", this); + }, + + _showDialog(merchantBrowser) { + let chromeWindow = merchantBrowser.ownerGlobal; + // Prevent focusing or interacting with the <browser>. + merchantBrowser.setAttribute("tabmodalPromptShowing", "true"); + + // Darken the merchant content area. + let tabModalBackground = chromeWindow.document.createXULElement("box"); + tabModalBackground.classList.add( + "tabModalBackground", + "paymentDialogBackground" + ); + // Insert the same way as <tabmodalprompt>. + merchantBrowser.parentNode.insertBefore( + tabModalBackground, + merchantBrowser.nextElementSibling + ); + }, + + /** + * @param {string} requestId - Payment Request ID of the dialog to close. + * @returns {boolean} whether the specified dialog was closed. + */ + closeDialog(requestId) { + let { browser, dialogContainer, paymentFrame } = this.findDialog(requestId); + if (!dialogContainer) { + return false; + } + this.log.debug(`closing: ${requestId}`); + paymentFrame.paymentDialogWrapper.uninit(); + dialogContainer.remove(); + browser.removeEventListener("SwapDocShells", this); + + if (!dialogContainer.hidden) { + // If the container is no longer hidden then the background was added after + // `tabmodaldialogready` so remove it. + browser.parentElement.querySelector(".paymentDialogBackground").remove(); + + if ( + !browser.tabModalPromptBox || + !browser.tabModalPromptBox.listPrompts().length + ) { + browser.removeAttribute("tabmodalPromptShowing"); + } + } + return true; + }, + + getDialogContainerForMerchantBrowser(merchantBrowser) { + return merchantBrowser.ownerGlobal.gBrowser + .getBrowserContainer(merchantBrowser) + .querySelector(".paymentDialogContainer"); + }, + + findDialog(requestId) { + for (let win of BrowserWindowTracker.orderedWindows) { + for (let dialogContainer of win.document.querySelectorAll( + ".paymentDialogContainer" + )) { + if (dialogContainer.dataset.requestId == requestId) { + return { + dialogContainer, + paymentFrame: dialogContainer.querySelector( + ".paymentDialogContainerFrame" + ), + browser: dialogContainer.parentElement.querySelector( + ".browserStack > browser" + ), + }; + } + } + } + return {}; + }, + + findBrowserByOuterWindowId(outerWindowId) { + for (let win of BrowserWindowTracker.orderedWindows) { + let browser = win.gBrowser.getBrowserForOuterWindowID(outerWindowId); + if (!browser) { + continue; + } + return browser; + } + + this.log.error( + "findBrowserByOuterWindowId: No browser found for outerWindowId:", + outerWindowId + ); + return null; + }, + + _moveDialogToNewBrowser(oldBrowser, newBrowser) { + // Re-attach event listeners to the new browser. + newBrowser.addEventListener("SwapDocShells", this); + + let dialogContainer = this.getDialogContainerForMerchantBrowser(oldBrowser); + let newBrowserContainer = newBrowser.ownerGlobal.gBrowser.getBrowserContainer( + newBrowser + ); + + // Clone the container tree + let newDialogContainer = newBrowserContainer.ownerDocument.importNode( + dialogContainer, + true + ); + + let oldFrame = dialogContainer.querySelector( + ".paymentDialogContainerFrame" + ); + let newFrame = newDialogContainer.querySelector( + ".paymentDialogContainerFrame" + ); + + // We need a document to be synchronously loaded in order to do the swap and + // there's no point in wasting resources loading a dialog we're going to replace. + newFrame.setAttribute("src", "about:blank"); + newFrame.setAttribute("nodefaultsrc", "true"); + + newBrowserContainer.prepend(newDialogContainer); + + // Force the <browser> to be created so that it'll have a document loaded and frame created. + // See `ourChildDocument` and `ourFrame` checks in nsFrameLoader::SwapWithOtherLoader. + /* eslint-disable-next-line no-unused-expressions */ + newFrame.clientTop; + + // Swap the frameLoaders to preserve the frame state + newFrame.swapFrameLoaders(oldFrame); + newFrame.paymentDialogWrapper = oldFrame.paymentDialogWrapper; + newFrame.paymentDialogWrapper.changeAttachedFrame(newFrame); + dialogContainer.remove(); + + this._showDialog(newBrowser); + }, + + handleEvent(event) { + switch (event.type) { + case "SwapDocShells": { + this._moveDialogToNewBrowser(event.target, event.detail); + break; + } + } + }, +}; + +var EXPORTED_SYMBOLS = ["PaymentUIService"]; |