summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/payments/content')
-rw-r--r--browser/components/payments/content/paymentDialogFrameScript.js181
-rw-r--r--browser/components/payments/content/paymentDialogWrapper.js931
2 files changed, 1112 insertions, 0 deletions
diff --git a/browser/components/payments/content/paymentDialogFrameScript.js b/browser/components/payments/content/paymentDialogFrameScript.js
new file mode 100644
index 0000000000..4397ba2f85
--- /dev/null
+++ b/browser/components/payments/content/paymentDialogFrameScript.js
@@ -0,0 +1,181 @@
+/* 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
new file mode 100644
index 0000000000..c72068961d
--- /dev/null
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -0,0 +1,931 @@
+/* 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://gre/modules/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"
+ );
+ // The string name for Mac is changed because the value needed updating.
+ const platform = AppConstants.platform.replace("macosx", "macos");
+ return FormAutofillUtils.stringBundle.formatStringFromName(
+ `useCreditCardPasswordPrompt.${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([
+ "nsIObserver",
+ "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;
+ }
+ const { require } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+ );
+ const {
+ gDevToolsBrowser,
+ } = require("devtools/client/framework/devtools-browser");
+ 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}`
+ );
+ }
+ }
+ },
+};