Bug 1423053 - Support accepting a payment request from the UI (with dummy data). r=jaws
☠☠ backed out by cd534162b8f4 ☠ ☠
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Wed, 20 Dec 2017 16:58:56 -0500
changeset 448801 131e43affb8f85b5a1a91fe7011d46457ee1b453
parent 448800 04fc5cd2091681b2e50b758827cb0b8669af8e64
child 448802 6e444fc086b4572af21707c35ca6711ea1c1d0a0
push id8527
push userCallek@gmail.com
push dateThu, 11 Jan 2018 21:05:50 +0000
treeherdermozilla-beta@95342d212a7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1423053
milestone59.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 1423053 - Support accepting a payment request from the UI (with dummy data). r=jaws MozReview-Commit-ID: 8OZzdvy1as
toolkit/components/payments/content/paymentDialog.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/paymentRequest.css
toolkit/components/payments/res/paymentRequest.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/PaymentTestUtils.jsm
toolkit/components/payments/test/browser/browser_show_dialog.js
toolkit/components/payments/test/unit/test_response_creation.js
toolkit/components/payments/test/unit/xpcshell.ini
--- a/toolkit/components/payments/content/paymentDialog.js
+++ b/toolkit/components/payments/content/paymentDialog.js
@@ -10,17 +10,17 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-let PaymentDialog = {
+var PaymentDialog = {
   componentsLoaded: new Map(),
   frame: null,
   mm: null,
   request: null,
 
   init(requestId, frame) {
     if (!requestId || typeof(requestId) != "string") {
       throw new Error("Invalid PaymentRequest ID");
@@ -33,31 +33,90 @@ let PaymentDialog = {
 
     this.frame = frame;
     this.mm = frame.frameLoader.messageManager;
     this.mm.addMessageListener("paymentContentToChrome", this);
     this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
     this.frame.src = "resource://payments/paymentRequest.xhtml";
   },
 
-  createShowResponse({acceptStatus, methodName = "", data = null,
-                      payerName = "", payerEmail = "", payerPhone = ""}) {
+  createShowResponse({
+    acceptStatus,
+    methodName = "",
+    methodData = null,
+    payerName = "",
+    payerEmail = "",
+    payerPhone = "",
+  }) {
     let showResponse = this.createComponentInstance(Ci.nsIPaymentShowActionResponse);
-    let methodData = this.createComponentInstance(Ci.nsIGeneralResponseData);
 
     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({
+    country = "",
+    addressLines = [],
+    region = "",
+    city = "",
+    dependentLocality = "",
+    postalCode = "",
+    sortingCode = "",
+    languageCode = "",
+    organization = "",
+    recipient = "",
+    phone = "",
+  }) {
+    const billingAddress = 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);
+    }
+    billingAddress.init(country,
+                        addressLine,
+                        region,
+                        city,
+                        dependentLocality,
+                        postalCode,
+                        sortingCode,
+                        languageCode,
+                        organization,
+                        recipient,
+                        phone);
+    return billingAddress;
+  },
+
   createComponentInstance(componentInterface) {
     let componentName;
     switch (componentInterface) {
       case Ci.nsIPaymentShowActionResponse: {
         componentName = "@mozilla.org/dom/payments/payment-show-action-response;1";
         break;
       }
       case Ci.nsIGeneralResponseData: {
@@ -78,16 +137,35 @@ let PaymentDialog = {
   onPaymentCancel() {
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
     });
     paymentSrv.respondPayment(showResponse);
     window.close();
   },
 
+  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,
+    });
+    paymentSrv.respondPayment(showResponse);
+  },
+
   receiveMessage({data}) {
     let {messageType} = data;
 
     switch (messageType) {
       case "initializeRequest": {
         let requestSerialized = JSON.parse(JSON.stringify(this.request));
 
         // Manually serialize the nsIPrincipal.
@@ -105,15 +183,22 @@ let PaymentDialog = {
           },
         });
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
+      case "pay": {
+        this.pay(data);
+        break;
+      }
     }
   },
 };
 
-let frame = document.getElementById("paymentRequestFrame");
-let requestId = (new URLSearchParams(window.location.search)).get("requestId");
-PaymentDialog.init(requestId, frame);
+if ("document" in this) {
+  // Running in a browser, not a unit test
+  let frame = document.getElementById("paymentRequestFrame");
+  let requestId = (new URLSearchParams(window.location.search)).get("requestId");
+  PaymentDialog.init(requestId, frame);
+}
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -18,30 +18,47 @@ class PaymentDialog extends PaymentState
 
   connectedCallback() {
     let contents = document.importNode(this._template.content, true);
     this._hostNameEl = contents.querySelector("#host-name");
 
     this._cancelButton = contents.querySelector("#cancel");
     this._cancelButton.addEventListener("click", this.cancelRequest);
 
+    this._payButton = contents.querySelector("#pay");
+    this._payButton.addEventListener("click", this.pay);
+
     this.appendChild(contents);
 
     super.connectedCallback();
   }
 
   disconnectedCallback() {
     this._cancelButtonEl.removeEventListener("click", this.cancelRequest);
+    this._cancelButtonEl.removeEventListener("click", this.pay);
     super.disconnectedCallback();
   }
 
   cancelRequest() {
     PaymentRequest.cancel();
   }
 
+  pay() {
+    PaymentRequest.pay({
+      methodName: "basic-card",
+      methodData: {
+        cardholderName: "John Doe",
+        cardNumber: "9999999999",
+        expiryMonth: "01",
+        expiryYear: "9999",
+        cardSecurityCode: "999",
+      },
+    });
+  }
+
   setLoadingState(state) {
     this.requestStore.setState(state);
   }
 
   render(state) {
     let request = state.request;
     this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
 
--- a/toolkit/components/payments/res/paymentRequest.css
+++ b/toolkit/components/payments/res/paymentRequest.css
@@ -15,14 +15,8 @@ html {
   margin: 5px;
   text-align: center;
 }
 
 #total .label {
   font-size: 15px;
   font-weight: bold;
 }
-
-#cancel {
-  position: absolute;
-  bottom: 10px;
-  left: 10px;
-}
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -91,15 +91,19 @@ let PaymentRequest = {
       savedBasicCards: detail.savedBasicCards,
     });
   },
 
   cancel() {
     this.sendMessageToChrome("paymentCancel");
   },
 
+  pay(data) {
+    this.sendMessageToChrome("pay", data);
+  },
+
   onPaymentRequestUnload() {
     // remove listeners that may be used multiple times here
     window.removeEventListener("paymentChromeToContent", this);
   },
 };
 
 PaymentRequest.init();
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -24,17 +24,18 @@
   <template id="payment-dialog-template">
     <div id="host-name"></div>
 
     <div id="total">
       <h2 class="label"></h2>
       <currency-amount></currency-amount>
     </div>
     <div id="controls-container">
-      <button id="cancel">Cancel payment</button>
+      <button id="cancel">Cancel</button>
+      <button id="pay">Pay</button>
     </div>
   </template>
 </head>
 <body>
   <iframe id="debugging-console" hidden="hidden" src="debugging.html"></iframe>
 
   <payment-dialog></payment-dialog>
 </body>
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -6,16 +6,30 @@ const { classes: Cc, interfaces: Ci, res
 
 this.PaymentTestUtils = {
   /**
    * Common content tasks functions to be used with ContentTask.spawn.
    */
   ContentTasks: {
     /* eslint-env mozilla/frame-script */
     /**
+     * Add a completion handler to the existing `showPromise` to call .complete().
+     * @returns {Object} representing the PaymentResponse
+     */
+    addCompletionHandler: async () => {
+      let response = await content.showPromise;
+      response.complete();
+      return {
+        response: response.toJSON(),
+        // XXX: Bug NNN: workaround for `details` not being included in `toJSON`.
+        methodDetails: response.details,
+      };
+    },
+
+    /**
      * Create a new payment request and cache it as `rq`.
      *
      * @param {Object} args
      * @param {PaymentMethodData[]} methodData
      * @param {PaymentDetailsInit} details
      * @param {PaymentOptions} options
      */
     createRequest: ({methodData, details, options}) => {
@@ -29,30 +43,40 @@ this.PaymentTestUtils = {
      * @param {Object} args
      * @param {PaymentMethodData[]} methodData
      * @param {PaymentDetailsInit} details
      * @param {PaymentOptions} options
      */
     createAndShowRequest: ({methodData, details, options}) => {
       const rq = new content.PaymentRequest(methodData, details, options);
       content.rq = rq; // assign it so we can retrieve it later
-      rq.show();
+      content.showPromise = rq.show();
     },
+  },
 
+  DialogContentTasks: {
     /**
      * Click the cancel button
      *
      * Don't await on this task since the cancel can close the dialog before
      * ContentTask can resolve the promise.
      *
      * @returns {undefined}
      */
     manuallyClickCancel: () => {
       content.document.getElementById("cancel").click();
     },
+
+    /**
+     * Do the minimum possible to complete the payment succesfully.
+     * @returns {undefined}
+     */
+    completePayment: () => {
+      content.document.getElementById("pay").click();
+    },
   },
 
   /**
    * 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
@@ -39,12 +39,50 @@ add_task(async function test_show_manual
     ok(requestId, "requestId should be defined");
     is(win.closed, false, "dialog should not be closed");
 
     // abort the payment request manually
     let frame = await getPaymentFrame(win);
     ok(frame, "Got payment frame");
     await dialogReadyPromise;
     info("dialog ready");
-    spawnPaymentDialogTask(frame, PTU.ContentTasks.manuallyClickCancel);
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
+
+add_task(async function test_show_completePayment() {
+  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);
+
+    // get a reference to the UI dialog and the requestId
+    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");
+    await dialogReadyPromise;
+    info("dialog ready, 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 methodDetails = result.methodDetails;
+    is(methodDetails.cardholderName, "John Doe", "Check cardholderName");
+    is(methodDetails.cardNumber, "9999999999", "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");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/unit/test_response_creation.js
@@ -0,0 +1,141 @@
+"use strict";
+
+/**
+ * Basic checks to ensure that helpers constructing responses map their
+ * destructured arguments properly to the `init` methods. Full testing of the init
+ * methods is left to the DOM code.
+ */
+
+/* import-globals-from ../../content/PaymentDialog.js */
+let dialogGlobal = {};
+Services.scriptloader.loadSubScript("chrome://payments/content/paymentDialog.js", dialogGlobal);
+
+/**
+ * @param {Object} responseData with properties in the order matching `nsIBasicCardResponseData`
+ *                              init method args.
+ * @returns {string} serialized card data
+ */
+function serializeBasicCardResponseData(responseData) {
+  return [...Object.entries(responseData)].map(array => array.join(":")).join(";") + ";";
+}
+
+
+add_task(async function test_createBasicCardResponseData_basic() {
+  let expected = {
+    cardholderName: "John Smith",
+    cardNumber: "1234567890",
+    expiryMonth: "01",
+    expiryYear: "2017",
+    cardSecurityCode: "0123",
+  };
+  let actual = dialogGlobal.PaymentDialog.createBasicCardResponseData(expected);
+  let expectedSerialized = serializeBasicCardResponseData(expected);
+  do_check_eq(actual.data, expectedSerialized, "Check data");
+});
+
+add_task(async function test_createBasicCardResponseData_minimal() {
+  let expected = {
+    cardNumber: "1234567890",
+  };
+  let actual = dialogGlobal.PaymentDialog.createBasicCardResponseData(expected);
+  let expectedSerialized = serializeBasicCardResponseData(expected);
+  do_print(actual.data);
+  do_check_eq(actual.data, expectedSerialized, "Check data");
+});
+
+add_task(async function test_createBasicCardResponseData_withoutNumber() {
+  let data = {
+    cardholderName: "John Smith",
+    expiryMonth: "01",
+    expiryYear: "2017",
+    cardSecurityCode: "0123",
+  };
+  Assert.throws(() => dialogGlobal.PaymentDialog.createBasicCardResponseData(data),
+                /NS_ERROR_FAILURE/,
+                "Check cardNumber is required");
+});
+
+function checkAddress(actual, expected) {
+  for (let [propName, propVal] of Object.entries(expected)) {
+    if (propName == "addressLines") {
+      // Note the singular vs. plural here.
+      do_check_eq(actual.addressLine.length, propVal.length, "Check number of address lines");
+      for (let [i, line] of expected.addressLines.entries()) {
+        do_check_eq(actual.addressLine.queryElementAt(i, Ci.nsISupportsString).data, line,
+                    `Check ${propName} line ${i}`);
+      }
+      continue;
+    }
+    do_check_eq(actual[propName], propVal, `Check ${propName}`);
+  }
+}
+
+add_task(async function test_createPaymentAddress_minimal() {
+  let data = {
+    country: "CA",
+  };
+  let actual = dialogGlobal.PaymentDialog.createPaymentAddress(data);
+  checkAddress(actual, data);
+});
+
+add_task(async function test_createPaymentAddress_basic() {
+  let data = {
+    country: "CA",
+    addressLines: [
+      "123 Sesame Street",
+      "P.O. Box ABC",
+    ],
+    region: "ON",
+    city: "Delhi",
+    dependentLocality: "N/A",
+    postalCode: "94041",
+    sortingCode: "1234",
+    languageCode: "en-CA",
+    organization: "Mozilla Corporation",
+    recipient: "John Smith",
+    phone: "+15195555555",
+  };
+  let actual = dialogGlobal.PaymentDialog.createPaymentAddress(data);
+  checkAddress(actual, data);
+});
+
+add_task(async function test_createShowResponse_basic() {
+  let requestId = "876hmbvfd45hb";
+  dialogGlobal.PaymentDialog.request = {
+    requestId,
+  };
+
+  let cardData = {
+    cardholderName: "John Smith",
+    cardNumber: "1234567890",
+    expiryMonth: "01",
+    expiryYear: "2099",
+    cardSecurityCode: "0123",
+  };
+  let methodData = dialogGlobal.PaymentDialog.createBasicCardResponseData(cardData);
+
+  let responseData = {
+    acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED,
+    methodName: "basic-card",
+    methodData,
+    payerName: "My Name",
+    payerEmail: "my.email@example.com",
+    payerPhone: "+15195555555",
+  };
+  let actual = dialogGlobal.PaymentDialog.createShowResponse(responseData);
+  for (let [propName, propVal] of Object.entries(actual)) {
+    if (typeof(propVal) != "string") {
+      continue;
+    }
+    if (propName == "requestId") {
+      do_check_eq(propVal, requestId, `Check ${propName}`);
+      continue;
+    }
+    if (propName == "data") {
+      do_check_eq(propVal, serializeBasicCardResponseData(cardData), `Check ${propName}`);
+      continue;
+    }
+
+    do_check_eq(propVal, responseData[propName], `Check ${propName}`);
+  }
+});
--- a/toolkit/components/payments/test/unit/xpcshell.ini
+++ b/toolkit/components/payments/test/unit/xpcshell.ini
@@ -1,4 +1,5 @@
 [DEFAULT]
 head = head.js
 
 [test_PaymentsStore.js]
+[test_response_creation.js]