Bug 1429195 - Send the selected payment card to the wrapper and DOM. r=jaws draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 08 Feb 2018 13:23:23 -0800
changeset 752749 17a759cf1cd416c070d35ab5a219b0259c59c0dc
parent 752748 6b652b070213f3c2a8315f3fe81bf7a2b52418a0
push id98362
push usermozilla@noorenberghe.ca
push dateThu, 08 Feb 2018 21:24:56 +0000
reviewersjaws
bugs1429195
milestone60.0a1
Bug 1429195 - Send the selected payment card to the wrapper and DOM. r=jaws MozReview-Commit-ID: 8SqXrnvenGB
toolkit/components/payments/content/paymentDialogWrapper.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/test/PaymentTestUtils.jsm
toolkit/components/payments/test/browser/browser_show_dialog.js
--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -11,16 +11,19 @@
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+ChromeUtils.defineModuleGetter(this, "MasterPassword",
+                               "resource://formautofill/MasterPassword.jsm");
+
 XPCOMUtils.defineLazyGetter(this, "profileStorage", () => {
   let profileStorage;
   try {
     profileStorage = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {})
                                 .profileStorage;
     profileStorage.initialize();
   } catch (ex) {
     profileStorage = null;
@@ -36,17 +39,23 @@ var paymentDialogWrapper = {
   mm: null,
   request: null,
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
   ]),
 
-  _convertProfileAddressToPaymentAddress(guid) {
+  /**
+   * Note: This method is async because profileStorage plans to become async.
+   *
+   * @param {string} guid
+   * @returns {nsIPaymentAddress}
+   */
+  async _convertProfileAddressToPaymentAddress(guid) {
     let addressData = profileStorage.addresses.get(guid);
     if (!addressData) {
       throw new Error(`Shipping address not found: ${guid}`);
     }
 
     let address = this.createPaymentAddress({
       country: addressData.country,
       addressLines: addressData["street-address"].split("\n"),
@@ -56,16 +65,51 @@ var paymentDialogWrapper = {
       organization: addressData.organization,
       recipient: addressData.name,
       phone: addressData.tel,
     });
 
     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 the user cancels entering their master password or an error decrypting
+   * @returns {nsIBasicCardResponseData?} returns response data or null (if the
+   *                                      master password dialog was cancelled);
+   */
+  async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
+    let cardData = profileStorage.creditCards.get(guid);
+    if (!cardData) {
+      throw new Error(`Basic card not found in storage: ${guid}`);
+    }
+
+    let cardNumber;
+    try {
+      cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+    } catch (ex) {
+      if (ex.result != Cr.NS_ERROR_ABORT) {
+        throw ex;
+      }
+      // User canceled master password entry
+      return null;
+    }
+
+    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,
+    });
+
+    return methodData;
+  },
+
   init(requestId, frame) {
     if (!requestId || typeof(requestId) != "string") {
       throw new Error("Invalid PaymentRequest ID");
     }
     this.request = paymentSrv.getPaymentRequestById(requestId);
 
     if (!this.request) {
       throw new Error(`PaymentRequest not found: ${requestId}`);
@@ -244,37 +288,56 @@ var paymentDialogWrapper = {
   onPaymentCancel() {
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
     });
     paymentSrv.respondPayment(showResponse);
     window.close();
   },
 
+  async onPay({
+    selectedPaymentCardGUID: paymentCardGUID,
+    selectedPaymentCardSecurityCode: cardSecurityCode,
+  }) {
+    let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
+                                                                            cardSecurityCode);
+
+    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;
+    }
+
+    this.pay({
+      methodName: "basic-card",
+      methodData,
+    });
+  },
+
   pay({
     payerName,
     payerEmail,
     payerPhone,
     methodName,
     methodData,
   }) {
-    let basicCardData = this.createBasicCardResponseData(methodData);
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED,
       payerName,
       payerEmail,
       payerPhone,
       methodName,
-      methodData: basicCardData,
+      methodData,
     });
     paymentSrv.respondPayment(showResponse);
   },
 
-  onChangeShippingAddress({shippingAddressGUID}) {
-    let address = this._convertProfileAddressToPaymentAddress(shippingAddressGUID);
+  async onChangeShippingAddress({shippingAddressGUID}) {
+    let address = await this._convertProfileAddressToPaymentAddress(shippingAddressGUID);
     paymentSrv.changeShippingAddress(this.request.requestId, address);
   },
 
   /**
    * @implements {nsIObserver}
    * @param {nsISupports} subject
    * @param {string} topic
    * @param {string} data
@@ -307,17 +370,17 @@ var paymentDialogWrapper = {
         this.onChangeShippingAddress(data);
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
       case "pay": {
-        this.pay(data);
+        this.onPay(data);
         break;
       }
     }
   },
 };
 
 if ("document" in this) {
   // Running in a browser, not a unit test
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -58,25 +58,24 @@ class PaymentDialog extends PaymentState
     }
   }
 
   cancelRequest() {
     paymentRequest.cancel();
   }
 
   pay() {
+    let {
+      selectedPaymentCard,
+      selectedPaymentCardSecurityCode,
+    } = this.requestStore.getState();
+
     paymentRequest.pay({
-      methodName: "basic-card",
-      methodData: {
-        cardholderName: "John Doe",
-        cardNumber: "9999999999",
-        expiryMonth: "01",
-        expiryYear: "9999",
-        cardSecurityCode: "999",
-      },
+      selectedPaymentCardGUID: selectedPaymentCard,
+      selectedPaymentCardSecurityCode,
     });
   }
 
   changeShippingAddress(shippingAddressGUID) {
     paymentRequest.changeShippingAddress({
       shippingAddressGUID,
     });
   }
@@ -107,16 +106,17 @@ class PaymentDialog extends PaymentState
       });
     }
 
     // Ensure `selectedPaymentCard` never refers to a deleted payment card and refers
     // to a payment card if one exists.
     if (!savedBasicCards[selectedPaymentCard]) {
       this.requestStore.setState({
         selectedPaymentCard: Object.keys(savedBasicCards)[0] || null,
+        selectedPaymentCardSecurityCode: null,
       });
     }
   }
 
   stateChangeCallback(state) {
     super.stateChangeCallback(state);
 
     if (state.selectedShippingAddress != this._cachedState.selectedShippingAddress) {
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -67,16 +67,24 @@ this.PaymentTestUtils = {
 
     /**
      * Do the minimum possible to complete the payment succesfully.
      * @returns {undefined}
      */
     completePayment: () => {
       content.document.getElementById("pay").click();
     },
+
+    setSecurityCode: ({securityCode}) => {
+      // Waive the xray to access the untrusted `securityCodeInput` property
+      let picker = Cu.waiveXrays(content.document.querySelector("payment-method-picker"));
+      // Unwaive to access the ChromeOnly `setUserInput` API.
+      // setUserInput dispatches changes events.
+      Cu.unwaiveXrays(picker.securityCodeInput).setUserInput(securityCode);
+    },
   },
 
   /**
    * Common PaymentMethodData for testing
    */
   MethodData: {
     basicCard: {
       supportedMethods: "basic-card",
--- a/toolkit/components/payments/test/browser/browser_show_dialog.js
+++ b/toolkit/components/payments/test/browser/browser_show_dialog.js
@@ -61,16 +61,28 @@ add_task(async function test_show_comple
     "postal-code": "02139",
     country: "US",
     tel: "+16172535702",
     email: "timbl@example.org",
   };
   profileStorage.addresses.add(address);
   await onChanged;
 
+  onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                      (subject, data) => data == "add");
+  let card = {
+    "cc-exp-month": 1,
+    "cc-exp-year": 9999,
+    "cc-name": "John Doe",
+    "cc-number": "999999999999",
+  };
+
+  profileStorage.creditCards.add(card);
+  await onChanged;
+
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
     let dialogReadyPromise = waitForWidgetReady();
     // start by creating a PaymentRequest, and show it
     await ContentTask.spawn(browser, {methodData, details}, PTU.ContentTasks.createAndShowRequest);
 
@@ -78,40 +90,44 @@ add_task(async function test_show_comple
     let [win] = await Promise.all([getPaymentWidget(), dialogReadyPromise]);
     ok(win, "Got payment widget");
     let requestId = paymentUISrv.requestIdForWindow(win);
     ok(requestId, "requestId should be defined");
     is(win.closed, false, "dialog should not be closed");
 
     let frame = await getPaymentFrame(win);
     ok(frame, "Got payment frame");
+    info("entering CSC");
+    await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
+      securityCode: "999",
+    });
     info("clicking pay");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
 
     // Add a handler to complete the payment above.
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
-    is(result.response.methodName, "basic-card", "Check methodName");
 
     let addressLines = address["street-address"].split("\n");
     let actualShippingAddress = result.response.shippingAddress;
     is(actualShippingAddress.addressLine[0], addressLines[0], "Address line 1 should match");
     is(actualShippingAddress.addressLine[1], addressLines[1], "Address line 2 should match");
     is(actualShippingAddress.country, address.country, "Country should match");
     is(actualShippingAddress.region, address["address-level1"], "Region should match");
     is(actualShippingAddress.city, address["address-level2"], "City should match");
     is(actualShippingAddress.postalCode, address["postal-code"], "Zip code should match");
     is(actualShippingAddress.organization, address.organization, "Org should match");
     is(actualShippingAddress.recipient,
        `${address["given-name"]} ${address["additional-name"]} ${address["family-name"]}`,
        "Recipient country should match");
     is(actualShippingAddress.phone, address.tel, "Phone should match");
 
+    is(result.response.methodName, "basic-card", "Check methodName");
     let methodDetails = result.methodDetails;
     is(methodDetails.cardholderName, "John Doe", "Check cardholderName");
-    is(methodDetails.cardNumber, "9999999999", "Check cardNumber");
+    is(methodDetails.cardNumber, "999999999999", "Check cardNumber");
     is(methodDetails.expiryMonth, "01", "Check expiryMonth");
     is(methodDetails.expiryYear, "9999", "Check expiryYear");
     is(methodDetails.cardSecurityCode, "999", "Check cardSecurityCode");
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });