Bug 1429195 - Send the selected payment card to the wrapper and DOM. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 08 Feb 2018 13:23:23 -0800
changeset 403023 61eeb80f413cab34f9f7783a21259cb86517dad8
parent 403022 5c40672bc6de26ae79abec4eee3a166887c8034a
child 403024 1f2d0e869605d682309d5d3c910a4a8e35ed2268
push id59328
push usermozilla@noorenberghe.ca
push dateThu, 08 Feb 2018 21:38:17 +0000
treeherderautoland@61eeb80f413c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1429195
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -10,16 +10,19 @@
 "use strict";
 
 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;
@@ -35,17 +38,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"),
@@ -55,16 +64,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}`);
@@ -243,37 +287,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
@@ -306,17 +369,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
@@ -65,16 +65,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");
   });
 });