forked from mirrors/gecko-dev
		
	 1f830c96da
			
		
	
	
		1f830c96da
		
	
	
	
	
		
			
			# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D36042 --HG-- extra : source : d3afcafdce650a6f36cebbc126ee93b17f13cf52
		
			
				
	
	
		
			926 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			926 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * 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}`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 |