forked from mirrors/gecko-dev
		
	Differential Revision: https://phabricator.services.mozilla.com/D13740 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			818 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			818 lines
		
	
	
	
		
			26 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);
 | 
						|
 | 
						|
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 | 
						|
ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
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], 1);
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * 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;
 | 
						|
  },
 | 
						|
 | 
						|
  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.temporaryStore.creditCards.getAll(),
 | 
						|
      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.temporaryStore.creditCards.getAll(),
 | 
						|
        });
 | 
						|
      }
 | 
						|
    } 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}`);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 |