diff --git a/.eslintignore b/.eslintignore index 77ff185905..1704980586 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,8 +26,6 @@ browser/branding/**/firefox-branding.js # Gzipped test file. browser/base/content/test/general/gZipOfflineChild.html browser/base/content/test/urlbar/file_blank_but_not_blank.html -# Third-party code. -browser/components/payments/res/vendor/* # Test files that are really json not js, and don't need to be linted. browser/components/sessionstore/test/unit/data/sessionstore_valid.js browser/components/sessionstore/test/unit/data/sessionstore_invalid.js diff --git a/.eslintrc.js b/.eslintrc.js index 287ec913ea..caa53dd19e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -269,7 +269,6 @@ module.exports = { "dom/messagechannel/**", "dom/midi/**", "dom/network/**", - "dom/payments/**", "dom/performance/**", "dom/permission/**", "dom/quota/**", diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index 987e7d9024..b021c2c78f 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -30,9 +30,6 @@ var gExceptionPaths = [ "resource://gre/modules/commonjs/", "resource://gre/defaults/pref/", - // These resources are referenced using relative paths from html files. - "resource://payments/", - // https://github.com/mozilla/activity-stream/issues/3053 "resource://activity-stream/data/content/tippytop/images/", // https://github.com/mozilla/activity-stream/issues/3758 diff --git a/browser/base/content/test/static/browser_parsable_script.js b/browser/base/content/test/static/browser_parsable_script.js index 015542054d..db0231202d 100644 --- a/browser/base/content/test/static/browser_parsable_script.js +++ b/browser/base/content/test/static/browser_parsable_script.js @@ -12,13 +12,6 @@ const kWhitelist = new Set([ /browser\/content\/browser\/places\/controller.js$/, ]); -const kESModuleList = new Set([ - /browser\/res\/payments\/(components|containers|mixins)\/.*\.js$/, - /browser\/res\/payments\/paymentRequest\.js$/, - /browser\/res\/payments\/PaymentsStore\.js$/, - /browser\/aboutlogins\/components\/.*\.js$/, -]); - // Normally we would use reflect.jsm to get Reflect.parse. However, if // we do that, then all the AST data is allocated in reflect.jsm's // zone. That exposes a bug in our GC. The GC collects reflect.jsm's diff --git a/browser/components/moz.build b/browser/components/moz.build index aaa2c47ad2..2cf1f56357 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -56,7 +56,6 @@ DIRS += ['build'] if CONFIG['NIGHTLY_BUILD']: DIRS += [ 'aboutconfig', - 'payments', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': diff --git a/browser/components/payments/.eslintrc.js b/browser/components/payments/.eslintrc.js deleted file mode 100644 index e45b829271..0000000000 --- a/browser/components/payments/.eslintrc.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; - -module.exports = { - overrides: [ - { - files: [ - "res/components/*.js", - "res/containers/*.js", - "res/mixins/*.js", - "res/paymentRequest.js", - "res/PaymentsStore.js", - "test/mochitest/test_*.html", - ], - parserOptions: { - sourceType: "module", - }, - }, - { - "files": "test/unit/head.js", - "rules": { - "no-unused-vars": ["error", { - "args": "none", - "vars": "local", - }], - }, - }, - ], - rules: { - "mozilla/var-only-at-top-level": "error", - - "block-scoped-var": "error", - complexity: ["error", { - max: 20, - }], - "max-nested-callbacks": ["error", 4], - "no-console": ["error", { allow: ["error"] }], - "no-fallthrough": "error", - "no-multi-str": "error", - "no-proto": "error", - "no-unused-expressions": "error", - "no-unused-vars": ["error", { - args: "none", - vars: "all" - }], - "no-use-before-define": ["error", { - functions: false, - }], - radix: "error", - "valid-jsdoc": ["error", { - prefer: { - return: "returns", - }, - preferType: { - Boolean: "boolean", - Number: "number", - String: "string", - bool: "boolean", - }, - requireParamDescription: false, - requireReturn: false, - requireReturnDescription: false, - }], - yoda: "error", - }, -}; diff --git a/browser/components/payments/PaymentUIService.jsm b/browser/components/payments/PaymentUIService.jsm deleted file mode 100644 index ce48988bcb..0000000000 --- a/browser/components/payments/PaymentUIService.jsm +++ /dev/null @@ -1,352 +0,0 @@ -/* 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([Ci.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 wrapper to absolutely position the - // 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 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 . - 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 . - 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 == 0 - ) { - 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 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"]; diff --git a/browser/components/payments/components.conf b/browser/components/payments/components.conf deleted file mode 100644 index 138922c5f0..0000000000 --- a/browser/components/payments/components.conf +++ /dev/null @@ -1,12 +0,0 @@ -# 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/. - -Classes = [ - { - 'cid': '{01f8bd55-9017-438b-85ec-7c15d2b35cdc}', - 'contract_ids': ['@mozilla.org/dom/payments/payment-ui-service;1'], - 'jsm': 'resource:///modules/PaymentUIService.jsm', - 'constructor': 'PaymentUIService', - }, -] diff --git a/browser/components/payments/content/paymentDialogFrameScript.js b/browser/components/payments/content/paymentDialogFrameScript.js deleted file mode 100644 index 4397ba2f85..0000000000 --- a/browser/components/payments/content/paymentDialogFrameScript.js +++ /dev/null @@ -1,181 +0,0 @@ -/* 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 frame script only exists to mediate communications between the - * unprivileged frame in a content process and the privileged dialog wrapper - * in the UI process on the main thread. - * - * `paymentChromeToContent` messages from the privileged wrapper are converted - * into DOM events of the same name. - * `paymentContentToChrome` custom DOM events from the unprivileged frame are - * converted into messages of the same name. - * - * Business logic should stay out of this shim. - */ - -"use strict"; - -/* eslint-env mozilla/frame-script */ -/* global Services */ - -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); - -ChromeUtils.defineModuleGetter( - this, - "FormAutofill", - "resource://formautofill/FormAutofill.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "FormAutofillUtils", - "resource://formautofill/FormAutofillUtils.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "AppConstants", - "resource://gre/modules/AppConstants.jsm" -); - -const SAVE_CREDITCARD_DEFAULT_PREF = "dom.payments.defaults.saveCreditCard"; -const SAVE_ADDRESS_DEFAULT_PREF = "dom.payments.defaults.saveAddress"; - -let PaymentFrameScript = { - init() { - XPCOMUtils.defineLazyGetter(this, "log", () => { - let { ConsoleAPI } = ChromeUtils.import( - "resource://gre/modules/Console.jsm" - ); - return new ConsoleAPI({ - maxLogLevelPref: "dom.payments.loglevel", - prefix: "paymentDialogFrameScript", - }); - }); - - addEventListener("paymentContentToChrome", this, false, true); - - addMessageListener("paymentChromeToContent", this); - }, - - handleEvent(event) { - this.sendToChrome(event); - }, - - receiveMessage({ data: { messageType, data } }) { - this.sendToContent(messageType, data); - }, - - setupContentConsole() { - let privilegedLogger = content.window.console.createInstance({ - maxLogLevelPref: "dom.payments.loglevel", - prefix: "paymentDialogContent", - }); - - let contentLogObject = Cu.waiveXrays(content).log; - for (let name of ["error", "warn", "info", "debug"]) { - Cu.exportFunction( - privilegedLogger[name].bind(privilegedLogger), - contentLogObject, - { - defineAs: name, - } - ); - } - }, - - /** - * Expose privileged utility functions to the unprivileged page. - */ - exposeUtilityFunctions() { - let waivedContent = Cu.waiveXrays(content); - let PaymentDialogUtils = { - DEFAULT_REGION: FormAutofill.DEFAULT_REGION, - countries: FormAutofill.countries, - - getAddressLabel(address, addressFields = null) { - return FormAutofillUtils.getAddressLabel(address, addressFields); - }, - - getCreditCardNetworks() { - let networks = FormAutofillUtils.getCreditCardNetworks(); - return Cu.cloneInto(networks, waivedContent); - }, - - isCCNumber(value) { - return FormAutofillUtils.isCCNumber(value); - }, - - getFormFormat(country) { - let format = FormAutofillUtils.getFormFormat(country); - return Cu.cloneInto(format, waivedContent); - }, - - findAddressSelectOption(selectEl, address, fieldName) { - return FormAutofillUtils.findAddressSelectOption( - selectEl, - address, - fieldName - ); - }, - - getDefaultPreferences() { - let prefValues = Cu.cloneInto( - { - saveCreditCardDefaultChecked: Services.prefs.getBoolPref( - SAVE_CREDITCARD_DEFAULT_PREF, - false - ), - saveAddressDefaultChecked: Services.prefs.getBoolPref( - SAVE_ADDRESS_DEFAULT_PREF, - false - ), - }, - waivedContent - ); - return Cu.cloneInto(prefValues, waivedContent); - }, - - isOfficialBranding() { - return AppConstants.MOZILLA_OFFICIAL; - }, - }; - waivedContent.PaymentDialogUtils = Cu.cloneInto( - PaymentDialogUtils, - waivedContent, - { - cloneFunctions: true, - } - ); - }, - - sendToChrome({ detail }) { - let { messageType } = detail; - if (messageType == "initializeRequest") { - this.setupContentConsole(); - this.exposeUtilityFunctions(); - } - this.log.debug("sendToChrome:", messageType, detail); - this.sendMessageToChrome(messageType, detail); - }, - - sendToContent(messageType, detail = {}) { - this.log.debug("sendToContent", messageType, detail); - let response = Object.assign({ messageType }, detail); - let event = new content.CustomEvent("paymentChromeToContent", { - detail: Cu.cloneInto(response, content), - }); - content.dispatchEvent(event); - }, - - sendMessageToChrome(messageType, data = {}) { - sendAsyncMessage( - "paymentContentToChrome", - Object.assign(data, { messageType }) - ); - }, -}; - -PaymentFrameScript.init(); diff --git a/browser/components/payments/content/paymentDialogWrapper.js b/browser/components/payments/content/paymentDialogWrapper.js deleted file mode 100644 index 4475ff571f..0000000000 --- a/browser/components/payments/content/paymentDialogWrapper.js +++ /dev/null @@ -1,926 +0,0 @@ -/* 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/. */ - -/** - * Runs in the privileged outer dialog. Each dialog loads this script in its - * own scope. - */ - -/* exported paymentDialogWrapper */ - -"use strict"; - -const paymentSrv = Cc[ - "@mozilla.org/dom/payments/payment-request-service;1" -].getService(Ci.nsIPaymentRequestService); - -const paymentUISrv = Cc[ - "@mozilla.org/dom/payments/payment-ui-service;1" -].getService(Ci.nsIPaymentUIService); - -const { AppConstants } = ChromeUtils.import( - "resource://gre/modules/AppConstants.jsm" -); -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" -); -ChromeUtils.defineModuleGetter( - this, - "FormAutofillUtils", - "resource://formautofill/FormAutofillUtils.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "OSKeyStore", - "resource://formautofill/OSKeyStore.jsm" -); -ChromeUtils.defineModuleGetter( - this, - "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm" -); - -XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => { - let storage; - try { - storage = ChromeUtils.import( - "resource://formautofill/FormAutofillStorage.jsm", - {} - ).formAutofillStorage; - storage.initialize(); - } catch (ex) { - storage = null; - Cu.reportError(ex); - } - - return storage; -}); - -XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => { - const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName( - "brandShortName" - ); - return FormAutofillUtils.stringBundle.formatStringFromName( - `useCreditCardPasswordPrompt.${AppConstants.platform}`, - [brandShortName] - ); -}); - -/** - * Temporary/transient storage for address and credit card records - * - * Implements a subset of the FormAutofillStorage collection class interface, and delegates to - * those classes for some utility methods - */ -class TempCollection { - constructor(type, data = {}) { - /** - * The name of the collection. e.g. 'addresses' or 'creditCards' - * Used to access methods from the FormAutofillStorage collections - */ - this._type = type; - this._data = data; - } - - get _formAutofillCollection() { - // lazy getter for the formAutofill collection - to resolve on first access - Object.defineProperty(this, "_formAutofillCollection", { - value: formAutofillStorage[this._type], - writable: false, - configurable: true, - }); - return this._formAutofillCollection; - } - - get(guid) { - return this._data[guid]; - } - - async update(guid, record, preserveOldProperties) { - let recordToSave = Object.assign( - preserveOldProperties ? this._data[guid] : {}, - record - ); - await this._formAutofillCollection.computeFields(recordToSave); - return (this._data[guid] = recordToSave); - } - - async add(record) { - let guid = "temp-" + Math.abs((Math.random() * 0xffffffff) | 0); - let timeLastModified = Date.now(); - let recordToSave = Object.assign({ guid, timeLastModified }, record); - await this._formAutofillCollection.computeFields(recordToSave); - this._data[guid] = recordToSave; - return guid; - } - - getAll() { - return this._data; - } -} - -var paymentDialogWrapper = { - componentsLoaded: new Map(), - frameWeakRef: null, - mm: null, - request: null, - temporaryStore: null, - - QueryInterface: ChromeUtils.generateQI([ - Ci.nsIObserver, - Ci.nsISupportsWeakReference, - ]), - - /** - * @param {string} guid - * @returns {object} containing only the requested payer values. - */ - async _convertProfileAddressToPayerData(guid) { - let addressData = - this.temporaryStore.addresses.get(guid) || - (await formAutofillStorage.addresses.get(guid)); - if (!addressData) { - throw new Error(`Payer address not found: ${guid}`); - } - - let { - requestPayerName, - requestPayerEmail, - requestPayerPhone, - } = this.request.paymentOptions; - - let payerData = { - payerName: requestPayerName ? addressData.name : "", - payerEmail: requestPayerEmail ? addressData.email : "", - payerPhone: requestPayerPhone ? addressData.tel : "", - }; - - return payerData; - }, - - /** - * @param {string} guid - * @returns {nsIPaymentAddress} - */ - async _convertProfileAddressToPaymentAddress(guid) { - let addressData = - this.temporaryStore.addresses.get(guid) || - (await formAutofillStorage.addresses.get(guid)); - if (!addressData) { - throw new Error(`Address not found: ${guid}`); - } - - let address = this.createPaymentAddress({ - addressLines: addressData["street-address"].split("\n"), - city: addressData["address-level2"], - country: addressData.country, - dependentLocality: addressData["address-level3"], - organization: addressData.organization, - phone: addressData.tel, - postalCode: addressData["postal-code"], - recipient: addressData.name, - region: addressData["address-level1"], - // TODO (bug 1474905), The regionCode will be available when bug 1474905 is fixed - // and the region text box is changed to a dropdown with the regionCode being the - // value of the option and the region being the label for the option. - // A regionCode should be either the empty string or one to three code points - // that represent a region as the code element of an [ISO3166-2] country subdivision - // name (i.e., the characters after the hyphen in an ISO3166-2 country subdivision - // code element, such as "CA" for the state of California in the USA, or "11" for - // the Lisbon district of Portugal). - regionCode: "", - }); - - return address; - }, - - /** - * @param {string} guid The GUID of the basic card record from storage. - * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.) - * @throws If there is an error decrypting - * @returns {nsIBasicCardResponseData?} returns response data or null (if the - * master password dialog was cancelled); - */ - async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) { - let cardData = - this.temporaryStore.creditCards.get(guid) || - (await formAutofillStorage.creditCards.get(guid)); - if (!cardData) { - throw new Error(`Basic card not found in storage: ${guid}`); - } - - let cardNumber; - try { - cardNumber = await OSKeyStore.decrypt( - cardData["cc-number-encrypted"], - reauthPasswordPromptMessage - ); - } catch (ex) { - if (ex.result != Cr.NS_ERROR_ABORT) { - throw ex; - } - // User canceled master password entry - return null; - } - - let billingAddressGUID = cardData.billingAddressGUID; - let billingAddress; - try { - billingAddress = await this._convertProfileAddressToPaymentAddress( - billingAddressGUID - ); - } catch (ex) { - // The referenced address may not exist if it was deleted or hasn't yet synced to this profile - Cu.reportError(ex); - } - let methodData = this.createBasicCardResponseData({ - cardholderName: cardData["cc-name"], - cardNumber, - expiryMonth: cardData["cc-exp-month"].toString().padStart(2, "0"), - expiryYear: cardData["cc-exp-year"].toString(), - cardSecurityCode, - billingAddress, - }); - - return methodData; - }, - - init(requestId, frame) { - if (!requestId || typeof requestId != "string") { - throw new Error("Invalid PaymentRequest ID"); - } - - // The Request object returned by the Payment Service is live and - // will automatically get updated if event.updateWith is used. - this.request = paymentSrv.getPaymentRequestById(requestId); - - if (!this.request) { - throw new Error(`PaymentRequest not found: ${requestId}`); - } - - this._attachToFrame(frame); - this.mm.loadFrameScript( - "chrome://payments/content/paymentDialogFrameScript.js", - true - ); - // Until we have bug 1446164 and bug 1407418 we use form autofill's temporary - // shim for data-localization* attributes. - this.mm.loadFrameScript("chrome://formautofill/content/l10n.js", true); - frame.setAttribute("src", "resource://payments/paymentRequest.xhtml"); - - this.temporaryStore = { - addresses: new TempCollection("addresses"), - creditCards: new TempCollection("creditCards"), - }; - }, - - uninit() { - try { - Services.obs.removeObserver(this, "message-manager-close"); - Services.obs.removeObserver(this, "formautofill-storage-changed"); - } catch (ex) { - // Observers may not have been added yet - } - }, - - /** - * Code here will be re-run at various times, e.g. initial show and - * when a tab is detached to a different window. - * - * Code that should only run once belongs in `init`. - * Code to only run upon detaching should be in `changeAttachedFrame`. - * - * @param {Element} frame - */ - _attachToFrame(frame) { - this.frameWeakRef = Cu.getWeakReference(frame); - this.mm = frame.frameLoader.messageManager; - this.mm.addMessageListener("paymentContentToChrome", this); - Services.obs.addObserver(this, "message-manager-close", true); - }, - - /** - * Called only when a frame is changed from one to another. - * - * @param {Element} frame - */ - changeAttachedFrame(frame) { - this.mm.removeMessageListener("paymentContentToChrome", this); - this._attachToFrame(frame); - // This isn't in `attachToFrame` because we only want to do it once we've sent records. - Services.obs.addObserver(this, "formautofill-storage-changed", true); - }, - - createShowResponse({ - acceptStatus, - methodName = "", - methodData = null, - payerName = "", - payerEmail = "", - payerPhone = "", - }) { - let showResponse = this.createComponentInstance( - Ci.nsIPaymentShowActionResponse - ); - - showResponse.init( - this.request.requestId, - acceptStatus, - methodName, - methodData, - payerName, - payerEmail, - payerPhone - ); - return showResponse; - }, - - createBasicCardResponseData({ - cardholderName = "", - cardNumber, - expiryMonth = "", - expiryYear = "", - cardSecurityCode = "", - billingAddress = null, - }) { - const basicCardResponseData = Cc[ - "@mozilla.org/dom/payments/basiccard-response-data;1" - ].createInstance(Ci.nsIBasicCardResponseData); - basicCardResponseData.initData( - cardholderName, - cardNumber, - expiryMonth, - expiryYear, - cardSecurityCode, - billingAddress - ); - return basicCardResponseData; - }, - - createPaymentAddress({ - addressLines = [], - city = "", - country = "", - dependentLocality = "", - organization = "", - postalCode = "", - phone = "", - recipient = "", - region = "", - regionCode = "", - sortingCode = "", - }) { - const paymentAddress = Cc[ - "@mozilla.org/dom/payments/payment-address;1" - ].createInstance(Ci.nsIPaymentAddress); - const addressLine = Cc["@mozilla.org/array;1"].createInstance( - Ci.nsIMutableArray - ); - for (let line of addressLines) { - const address = Cc["@mozilla.org/supports-string;1"].createInstance( - Ci.nsISupportsString - ); - address.data = line; - addressLine.appendElement(address); - } - paymentAddress.init( - country, - addressLine, - region, - regionCode, - city, - dependentLocality, - postalCode, - sortingCode, - organization, - recipient, - phone - ); - return paymentAddress; - }, - - createComponentInstance(componentInterface) { - let componentName; - switch (componentInterface) { - case Ci.nsIPaymentShowActionResponse: { - componentName = - "@mozilla.org/dom/payments/payment-show-action-response;1"; - break; - } - case Ci.nsIGeneralResponseData: { - componentName = "@mozilla.org/dom/payments/general-response-data;1"; - break; - } - } - let component = this.componentsLoaded.get(componentName); - - if (!component) { - component = Cc[componentName]; - this.componentsLoaded.set(componentName, component); - } - - return component.createInstance(componentInterface); - }, - - async fetchSavedAddresses() { - let savedAddresses = {}; - for (let address of await formAutofillStorage.addresses.getAll()) { - savedAddresses[address.guid] = address; - } - return savedAddresses; - }, - - async fetchSavedPaymentCards() { - let savedBasicCards = {}; - for (let card of await formAutofillStorage.creditCards.getAll()) { - savedBasicCards[card.guid] = card; - // Filter out the encrypted card number since the dialog content is - // considered untrusted and runs in a content process. - delete card["cc-number-encrypted"]; - - // ensure each card has a methodName property - if (!card.methodName) { - card.methodName = "basic-card"; - } - } - return savedBasicCards; - }, - - fetchTempPaymentCards() { - let creditCards = this.temporaryStore.creditCards.getAll(); - for (let card of Object.values(creditCards)) { - // Ensure each card has a methodName property. - if (!card.methodName) { - card.methodName = "basic-card"; - } - } - return creditCards; - }, - - async onAutofillStorageChange() { - let [savedAddresses, savedBasicCards] = await Promise.all([ - this.fetchSavedAddresses(), - this.fetchSavedPaymentCards(), - ]); - - this.sendMessageToContent("updateState", { - savedAddresses, - savedBasicCards, - }); - }, - - sendMessageToContent(messageType, data = {}) { - this.mm.sendAsyncMessage("paymentChromeToContent", { - data, - messageType, - }); - }, - - updateRequest() { - // There is no need to update this.request since the object is live - // and will automatically get updated if event.updateWith is used. - let requestSerialized = this._serializeRequest(this.request); - - this.sendMessageToContent("updateState", { - request: requestSerialized, - }); - }, - - /** - * Recursively convert and filter input to the subset of data types supported by JSON - * - * @param {*} value - any type of input to serialize - * @param {string?} name - name or key associated with this input. - * E.g. property name or array index. - * @returns {*} serialized deep copy of the value - */ - _serializeRequest(value, name = null) { - // Primitives: String, Number, Boolean, null - let type = typeof value; - if ( - value === null || - type == "string" || - type == "number" || - type == "boolean" - ) { - return value; - } - if (name == "topLevelPrincipal") { - // Manually serialize the nsIPrincipal. - let displayHost = value.URI.displayHost; - return { - URI: { - displayHost, - }, - }; - } - if (type == "function" || type == "undefined") { - return undefined; - } - // Structures: nsIArray - if (value instanceof Ci.nsIArray) { - let iface; - let items = []; - switch (name) { - case "displayItems": // falls through - case "additionalDisplayItems": - iface = Ci.nsIPaymentItem; - break; - case "shippingOptions": - iface = Ci.nsIPaymentShippingOption; - break; - case "paymentMethods": - iface = Ci.nsIPaymentMethodData; - break; - case "modifiers": - iface = Ci.nsIPaymentDetailsModifier; - break; - } - if (!iface) { - throw new Error( - `No interface associated with the members of the ${name} nsIArray` - ); - } - for (let i = 0; i < value.length; i++) { - let item = value.queryElementAt(i, iface); - let result = this._serializeRequest(item, i); - if (result !== undefined) { - items.push(result); - } - } - return items; - } - // Structures: Arrays - if (Array.isArray(value)) { - let items = value - .map(item => this._serializeRequest(item)) - .filter(item => item !== undefined); - return items; - } - // Structures: Objects - let obj = {}; - for (let [key, item] of Object.entries(value)) { - let result = this._serializeRequest(item, key); - if (result !== undefined) { - obj[key] = result; - } - } - return obj; - }, - - async initializeFrame() { - // We don't do this earlier as it's only necessary once this function sends - // the initial saved records. - Services.obs.addObserver(this, "formautofill-storage-changed", true); - - let requestSerialized = this._serializeRequest(this.request); - let chromeWindow = this.frameWeakRef.get().ownerGlobal; - let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow); - - let [savedAddresses, savedBasicCards] = await Promise.all([ - this.fetchSavedAddresses(), - this.fetchSavedPaymentCards(), - ]); - - this.sendMessageToContent("showPaymentRequest", { - request: requestSerialized, - savedAddresses, - tempAddresses: this.temporaryStore.addresses.getAll(), - savedBasicCards, - tempBasicCards: this.fetchTempPaymentCards(), - isPrivate, - }); - }, - - debugFrame() { - // To avoid self-XSS-type attacks, ensure that Browser Chrome debugging is enabled. - if (!Services.prefs.getBoolPref("devtools.chrome.enabled", false)) { - Cu.reportError( - "devtools.chrome.enabled must be enabled to debug the frame" - ); - return; - } - let { gDevToolsBrowser } = ChromeUtils.import( - "resource://devtools/client/framework/gDevTools.jsm" - ); - gDevToolsBrowser.openContentProcessToolbox({ - selectedBrowser: this.frameWeakRef.get(), - }); - }, - - onOpenPreferences() { - BrowserWindowTracker.getTopWindow().openPreferences( - "privacy-form-autofill" - ); - }, - - onPaymentCancel() { - const showResponse = this.createShowResponse({ - acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED, - }); - - paymentSrv.respondPayment(showResponse); - paymentUISrv.closePayment(this.request.requestId); - }, - - async onPay({ - selectedPayerAddressGUID: payerGUID, - selectedPaymentCardGUID: paymentCardGUID, - selectedPaymentCardSecurityCode: cardSecurityCode, - selectedShippingAddressGUID: shippingGUID, - }) { - let methodData; - try { - methodData = await this._convertProfileBasicCardToPaymentMethodData( - paymentCardGUID, - cardSecurityCode - ); - } catch (ex) { - // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user - // to re-enter credit card # from management UI. - Cu.reportError(ex); - return; - } - - if (!methodData) { - // TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the - // Master Password dialog. - Cu.reportError( - "Bug 1429265/Bug 1429205: User canceled master password entry" - ); - return; - } - - let payerName = ""; - let payerEmail = ""; - let payerPhone = ""; - if (payerGUID) { - let payerData = await this._convertProfileAddressToPayerData(payerGUID); - payerName = payerData.payerName; - payerEmail = payerData.payerEmail; - payerPhone = payerData.payerPhone; - } - - // Update the lastUsedTime for the payerAddress and paymentCard. Check if - // the record exists in formAutofillStorage because it may be temporary. - if ( - shippingGUID && - (await formAutofillStorage.addresses.get(shippingGUID)) - ) { - formAutofillStorage.addresses.notifyUsed(shippingGUID); - } - if (payerGUID && (await formAutofillStorage.addresses.get(payerGUID))) { - formAutofillStorage.addresses.notifyUsed(payerGUID); - } - if (await formAutofillStorage.creditCards.get(paymentCardGUID)) { - formAutofillStorage.creditCards.notifyUsed(paymentCardGUID); - } - - this.pay({ - methodName: "basic-card", - methodData, - payerName, - payerEmail, - payerPhone, - }); - }, - - pay({ payerName, payerEmail, payerPhone, methodName, methodData }) { - const showResponse = this.createShowResponse({ - acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED, - payerName, - payerEmail, - payerPhone, - methodName, - methodData, - }); - paymentSrv.respondPayment(showResponse); - this.sendMessageToContent("responseSent"); - }, - - async onChangePayerAddress({ payerAddressGUID }) { - if (payerAddressGUID) { - // If a payer address was de-selected e.g. the selected address was deleted, we'll - // just wait to send the address change when the payer address is eventually selected - // before clicking Pay since it's a required field. - let { - payerName, - payerEmail, - payerPhone, - } = await this._convertProfileAddressToPayerData(payerAddressGUID); - paymentSrv.changePayerDetail( - this.request.requestId, - payerName, - payerEmail, - payerPhone - ); - } - }, - - async onChangePaymentMethod({ - selectedPaymentCardBillingAddressGUID: billingAddressGUID, - }) { - const methodName = "basic-card"; - let methodDetails; - try { - let billingAddress = await this._convertProfileAddressToPaymentAddress( - billingAddressGUID - ); - const basicCardChangeDetails = Cc[ - "@mozilla.org/dom/payments/basiccard-change-details;1" - ].createInstance(Ci.nsIBasicCardChangeDetails); - basicCardChangeDetails.initData(billingAddress); - methodDetails = basicCardChangeDetails.QueryInterface( - Ci.nsIMethodChangeDetails - ); - } catch (ex) { - // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user - // to re-enter credit card # from management UI. - Cu.reportError(ex); - return; - } - - paymentSrv.changePaymentMethod( - this.request.requestId, - methodName, - methodDetails - ); - }, - - async onChangeShippingAddress({ shippingAddressGUID }) { - if (shippingAddressGUID) { - // If a shipping address was de-selected e.g. the selected address was deleted, we'll - // just wait to send the address change when the shipping address is eventually selected - // before clicking Pay since it's a required field. - let address = await this._convertProfileAddressToPaymentAddress( - shippingAddressGUID - ); - paymentSrv.changeShippingAddress(this.request.requestId, address); - } - }, - - onChangeShippingOption({ optionID }) { - // Note, failing here on browser_host_name.js because the test closes - // the dialog before the onChangeShippingOption is called, thus - // deleting the request and making the requestId invalid. Unclear - // why we aren't seeing the same issue with onChangeShippingAddress. - paymentSrv.changeShippingOption(this.request.requestId, optionID); - }, - - onCloseDialogMessage() { - // The PR is complete(), just close the dialog - paymentUISrv.closePayment(this.request.requestId); - }, - - async onUpdateAutofillRecord(collectionName, record, guid, messageID) { - let responseMessage = { - guid, - messageID, - stateChange: {}, - }; - try { - let isTemporary = record.isTemporary; - let collection = isTemporary - ? this.temporaryStore[collectionName] - : formAutofillStorage[collectionName]; - - if (guid) { - // We want to preserve old properties since the edit forms are often - // shown without all fields visible/enabled and we don't want those - // fields to be blanked upon saving. Examples of hidden/disabled fields: - // email, cc-number, mailing-address on the payer forms, and payer fields - // not requested in the payer form. - let preserveOldProperties = true; - await collection.update(guid, record, preserveOldProperties); - } else { - responseMessage.guid = await collection.add(record); - } - - if (isTemporary && collectionName == "addresses") { - // there will be no formautofill-storage-changed event to update state - // so add updated collection here - Object.assign(responseMessage.stateChange, { - tempAddresses: this.temporaryStore.addresses.getAll(), - }); - } - if (isTemporary && collectionName == "creditCards") { - // there will be no formautofill-storage-changed event to update state - // so add updated collection here - Object.assign(responseMessage.stateChange, { - tempBasicCards: this.fetchTempPaymentCards(), - }); - } - } catch (ex) { - responseMessage.error = true; - Cu.reportError(ex); - } finally { - this.sendMessageToContent( - "updateAutofillRecord:Response", - responseMessage - ); - } - }, - - /** - * @implements {nsIObserver} - * @param {nsISupports} subject - * @param {string} topic - * @param {string} data - */ - observe(subject, topic, data) { - switch (topic) { - case "formautofill-storage-changed": { - if (data == "notifyUsed") { - break; - } - this.onAutofillStorageChange(); - break; - } - case "message-manager-close": { - if (this.mm && subject == this.mm) { - // Remove the observer to avoid message manager errors while the dialog - // is closing and tests are cleaning up autofill storage. - Services.obs.removeObserver(this, "formautofill-storage-changed"); - } - break; - } - } - }, - - receiveMessage({ data }) { - let { messageType } = data; - - switch (messageType) { - case "debugFrame": { - this.debugFrame(); - break; - } - case "initializeRequest": { - this.initializeFrame(); - break; - } - case "changePayerAddress": { - this.onChangePayerAddress(data); - break; - } - case "changePaymentMethod": { - this.onChangePaymentMethod(data); - break; - } - case "changeShippingAddress": { - this.onChangeShippingAddress(data); - break; - } - case "changeShippingOption": { - this.onChangeShippingOption(data); - break; - } - case "closeDialog": { - this.onCloseDialogMessage(); - break; - } - case "openPreferences": { - this.onOpenPreferences(); - break; - } - case "paymentCancel": { - this.onPaymentCancel(); - break; - } - case "paymentDialogReady": { - this.frameWeakRef.get().dispatchEvent( - new Event("tabmodaldialogready", { - bubbles: true, - }) - ); - break; - } - case "pay": { - this.onPay(data); - break; - } - case "updateAutofillRecord": { - this.onUpdateAutofillRecord( - data.collectionName, - data.record, - data.guid, - data.messageID - ); - break; - } - default: { - throw new Error( - `paymentDialogWrapper: Unexpected messageType: ${messageType}` - ); - } - } - }, -}; diff --git a/browser/components/payments/docs/index.rst b/browser/components/payments/docs/index.rst deleted file mode 100644 index e34d64577c..0000000000 --- a/browser/components/payments/docs/index.rst +++ /dev/null @@ -1,110 +0,0 @@ -============== -WebPayments UI -============== - -User Interface for the WebPayments `Payment Request API `_ and `Payment Handler API `_. - - - `Project Wiki `_ | - `#payments on IRC `_ | - `File a bug `_ - -JSDoc style comments are used within the JS files of the component. This document will focus on higher-level and shared concepts. - -.. toctree:: - :maxdepth: 5 - - -Debugging/Development -===================== - -Relevant preferences: ``dom.payments.*`` - -Must Have Electrolysis ----------------------- - -Web Payments `does not work without e10s `_! - -Logging -------- - -Set the pref ``dom.payments.loglevel`` to "Debug" to increase the verbosity of console messages. - -Unprivileged UI Development ---------------------------- -During development of the unprivileged custom elements, you can load the dialog in a tab with -the url `resource://payments/paymentRequest.xhtml`. -You can then use the debugging console to load sample data. Autofill add/edit form strings -will not appear when developing this way until they are converted to FTL. -You can force localization of Form Autofill strings using the following in the Browser Console when -the `paymentRequest.xhtml` tab is selected then reloading:: - - gBrowser.selectedBrowser.messageManager.loadFrameScript("chrome://formautofill/content/l10n.js", true) - - -Debugging Console ------------------ - -To open the debugging console in the dialog, use the keyboard shortcut -**Ctrl-Alt-d (Ctrl-Option-d on macOS)**. While loading `paymentRequest.xhtml` directly in the -browser, add `?debug=1` to have the debugging console open by default. - -Debugging the unprivileged frame with the developer tools -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To open a debugger in the context of the remote payment frame, click the "Debug frame" button in the -debugging console. - -Use the ``tabs`` variable in the Browser Content Toolbox's console to access the frame contents. -There can be multiple frames loaded in the same process so you will need to find the correct tab -in the array by checking the file name is `paymentRequest.xhtml` (e.g. ``tabs[0].content.location``). - - -Dialog Architecture -=================== - -Privileged wrapper XUL document (paymentDialogWrapper.xul) containing a remote ```` containing unprivileged XHTML (paymentRequest.xhtml). -Keeping the dialog contents unprivileged is useful since the dialog will render payment line items and shipping options that are provided by web developers and should therefore be considered untrusted. -In order to communicate across the process boundary a privileged frame script (`paymentDialogFrameScript.js`) is loaded into the iframe to relay messages. -This is because the unprivileged document cannot access message managers. -Instead, all communication across the privileged/unprivileged boundary is done via custom DOM events: - -* A ``paymentContentToChrome`` event is dispatched when the dialog contents want to communicate with the privileged dialog wrapper. -* A ``paymentChromeToContent`` event is dispatched on the ``window`` with the ``detail`` property populated when the privileged dialog wrapper communicates with the unprivileged dialog. - -These events are converted to/from message manager messages of the same name to communicate to the other process. -The purpose of `paymentDialogFrameScript.js` is to simply convert unprivileged DOM events to/from messages from the other process. - -The dialog depends on the add/edit forms and storage from :doc:`Form Autofill ` for addresses and credit cards. - -Communication with the DOM --------------------------- - -Communication from the DOM to the UI happens via the `paymentUIService.js` (implementing ``nsIPaymentUIService``). -The UI talks to the DOM code via the ``nsIPaymentRequestService`` interface. - - -Custom Elements ---------------- - -The Payment Request UI uses `Custom Elements `_ for the UI components. - -Some guidelines: - -* There are some `mixins `_ - to provide commonly needed functionality to a custom element. -* `res/containers/ `_ - contains elements that react to application state changes, - `res/components/ `_ - contains elements that aren't connected to the state directly. -* Elements should avoid having their own internal/private state and should react to state changes. - Containers primarily use the application state (``requestStore``) while components primarily use attributes. -* If you're overriding a lifecycle callback, don't forget to call that method on - ``super`` from the implementation to ensure that mixins and ancestor classes - work properly. -* From within a custom element, don't use ``document.getElementById`` or - ``document.querySelector*`` because they can return elements that are outside - of the component, thus breaking the modularization. It can also cause problems - if the elements you're looking for aren't attached to the document yet. Use - ``querySelector*`` on ``this`` (the custom element) or one of its descendants - instead. diff --git a/browser/components/payments/jar.mn b/browser/components/payments/jar.mn deleted file mode 100644 index 73e8b00a2a..0000000000 --- a/browser/components/payments/jar.mn +++ /dev/null @@ -1,26 +0,0 @@ -# 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/. - -browser.jar: -% content payments %content/payments/ - content/payments/paymentDialogFrameScript.js (content/paymentDialogFrameScript.js) - content/payments/paymentDialogWrapper.js (content/paymentDialogWrapper.js) - -% resource payments %res/payments/ - res/payments (res/paymentRequest.*) - res/payments/components/ (res/components/*.css) - res/payments/components/ (res/components/*.js) - res/payments/components/ (res/components/*.svg) - res/payments/containers/ (res/containers/*.js) - res/payments/containers/ (res/containers/*.css) - res/payments/containers/ (res/containers/*.svg) - res/payments/debugging.css (res/debugging.css) - res/payments/debugging.html (res/debugging.html) - res/payments/debugging.js (res/debugging.js) - res/payments/formautofill/autofillEditForms.js (../../../browser/extensions/formautofill/content/autofillEditForms.js) - res/payments/formautofill/editAddress.xhtml (../../../browser/extensions/formautofill/content/editAddress.xhtml) - res/payments/formautofill/editCreditCard.xhtml (../../../browser/extensions/formautofill/content/editCreditCard.xhtml) - res/payments/unprivileged-fallbacks.js (res/unprivileged-fallbacks.js) - res/payments/mixins/ (res/mixins/*.js) - res/payments/PaymentsStore.js (res/PaymentsStore.js) diff --git a/browser/components/payments/moz.build b/browser/components/payments/moz.build deleted file mode 100644 index 6a13d595a7..0000000000 --- a/browser/components/payments/moz.build +++ /dev/null @@ -1,34 +0,0 @@ -# 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/. - -BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini'] - -with Files('**'): - BUG_COMPONENT = ('Firefox', 'WebPayments UI') - -EXTRA_JS_MODULES += [ - 'PaymentUIService.jsm', -] - -XPCOM_MANIFESTS += [ - 'components.conf', -] - -JAR_MANIFESTS += ['jar.mn'] - -MOCHITEST_MANIFESTS += [ - 'test/mochitest/formautofill/mochitest.ini', - 'test/mochitest/mochitest.ini', -] - -SPHINX_TREES['docs'] = 'docs' - -with Files('docs/**'): - SCHEDULES.exclusive = ['docs'] - -TESTING_JS_MODULES += [ - 'test/PaymentTestUtils.jsm', -] - -XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] diff --git a/browser/components/payments/res/PaymentsStore.js b/browser/components/payments/res/PaymentsStore.js deleted file mode 100644 index 7e439076d8..0000000000 --- a/browser/components/payments/res/PaymentsStore.js +++ /dev/null @@ -1,97 +0,0 @@ -/* 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 deleted file mode 100644 index a45d6e2513..0000000000 --- a/browser/components/payments/res/components/accepted-cards.css +++ /dev/null @@ -1,104 +0,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 deleted file mode 100644 index c66e040576..0000000000 --- a/browser/components/payments/res/components/accepted-cards.js +++ /dev/null @@ -1,75 +0,0 @@ -/* 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 */ - -/** - * - */ - -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 deleted file mode 100644 index 9619048ae6..0000000000 --- a/browser/components/payments/res/components/address-option.css +++ /dev/null @@ -1,29 +0,0 @@ -/* 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 deleted file mode 100644 index f0c06a0268..0000000000 --- a/browser/components/payments/res/components/address-option.js +++ /dev/null @@ -1,159 +0,0 @@ -/* 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