Bug 1470584 - Send PAYMENT_REJECTED response if window is unexpectedly closed. r?MattN draft
authorSam Foster <sfoster@mozilla.com>
Mon, 16 Jul 2018 17:21:51 -0700
changeset 823256 53e3e82f9656f8f8824acaf644fdcf9a44dac458
parent 822981 4e6486b672b32aba075b704c6b1e41e8ccf7a135
push id117631
push userbmo:sfoster@mozilla.com
push dateThu, 26 Jul 2018 23:20:44 +0000
reviewersMattN
bugs1470584
milestone63.0a1
Bug 1470584 - Send PAYMENT_REJECTED response if window is unexpectedly closed. r?MattN * Use onWindowClose to spot closing windows associated with live payment requests and send a reject response * Test to verify expected show() promise rejection behavior when closing the PR dialog MozReview-Commit-ID: 2TJYN5NMrE6
browser/components/payments/paymentUIService.js
browser/components/payments/test/PaymentTestUtils.jsm
browser/components/payments/test/browser/browser_show_dialog.js
--- a/browser/components/payments/paymentUIService.js
+++ b/browser/components/payments/paymentUIService.js
@@ -27,25 +27,39 @@ function PaymentUIService() {
   this.wrappedJSObject = this;
   XPCOMUtils.defineLazyGetter(this, "log", () => {
     let {ConsoleAPI} = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
     return new ConsoleAPI({
       maxLogLevelPref: "dom.payments.loglevel",
       prefix: "Payment UI Service",
     });
   });
+  Services.wm.addListener(this);
   this.log.debug("constructor");
 }
 
 PaymentUIService.prototype = {
   classID: Components.ID("{01f8bd55-9017-438b-85ec-7c15d2b35cdc}"),
   QueryInterface: ChromeUtils.generateQI([Ci.nsIPaymentUIService]),
   DIALOG_URL: "chrome://payments/content/paymentDialogWrapper.xul",
   REQUEST_ID_PREFIX: "paymentRequest-",
 
+  // nsIWindowMediatorListener implementation:
+
+  onOpenWindow(aWindow) {},
+  onCloseWindow(aWindow) {
+    let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+    let requestId = this.requestIdForWindow(domWindow);
+    if (!requestId || !paymentSrv.getPaymentRequestById(requestId)) {
+      return;
+    }
+    this.log.debug(`onCloseWindow, close of window for active requestId: ${requestId}`);
+    this.rejectPaymentForClosedDialog(requestId);
+  },
+
   // nsIPaymentUIService implementation:
 
   showPayment(requestId) {
     this.log.debug("showPayment:", requestId);
     let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
     chromeWindow.openDialog(`${this.DIALOG_URL}?requestId=${requestId}`,
                             `${this.REQUEST_ID_PREFIX}${requestId}`,
                             "modal,dialog,centerscreen,resizable=no");
@@ -62,16 +76,30 @@ PaymentUIService.prototype = {
     let response = found ?
       Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED :
       Ci.nsIPaymentActionResponse.ABORT_FAILED;
 
     abortResponse.init(requestId, response);
     paymentSrv.respondPayment(abortResponse);
   },
 
+  rejectPaymentForClosedDialog(requestId) {
+    this.log.debug("rejectPaymentForClosedDialog:", requestId);
+    const rejectResponse = Cc["@mozilla.org/dom/payments/payment-show-action-response;1"]
+                            .createInstance(Ci.nsIPaymentShowActionResponse);
+    rejectResponse.init(requestId,
+                        Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
+                        "", // payment method
+                        null, // payment method data
+                        "", // payer name
+                        "", // payer email
+                        "");// payer phone
+    paymentSrv.respondPayment(rejectResponse);
+  },
+
   completePayment(requestId) {
     // completeStatus should be one of "timeout", "success", "fail", ""
     let {completeStatus} = paymentSrv.getPaymentRequestById(requestId);
     this.log.debug(`completePayment: requestId: ${requestId}, completeStatus: ${completeStatus}`);
 
     let closed;
     switch (completeStatus) {
       case "fail":
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -86,16 +86,29 @@ var PaymentTestUtils = {
       const rq = new content.PaymentRequest(methodData, details, options);
       content.rq = rq; // assign it so we can retrieve it later
 
       const handle = content.windowUtils.setHandlingUserInput(true);
       content.showPromise = rq.show();
 
       handle.destruct();
     },
+
+    /**
+     * Add a rejection handler for the `showPromise` created by createAndShowRequest
+     * and stash details of any eventual exception or response in `rqResult`
+     */
+    catchShowPromiseRejection: () => {
+      content.rqResult = {};
+      content.showPromise.then(res => content.rqResult.response = res)
+                         .catch(ex => content.rqResult.showException = {
+                           name: ex.name,
+                           message: ex.message,
+                         });
+    },
   },
 
   DialogContentTasks: {
     getShippingOptions: () => {
       let picker = content.document.querySelector("shipping-option-picker");
       let popupBox = Cu.waiveXrays(picker).dropdown.popupBox;
       let selectedOptionIndex = popupBox.selectedIndex;
       let selectedOption = Cu.waiveXrays(picker).dropdown.selectedOption;
--- a/browser/components/payments/test/browser/browser_show_dialog.js
+++ b/browser/components/payments/test/browser/browser_show_dialog.js
@@ -129,8 +129,33 @@ add_task(async function test_show_comple
     info("acknowledging the completion from the merchant page");
     let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
 
     is(result.response.shippingOption, "1", "Check shipping option");
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
+
+add_task(async function test_show_closeReject_dialog() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win} =
+      await setupPaymentDialog(browser, {
+        methodData,
+        details,
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+    await ContentTask.spawn(browser, null, PTU.ContentTasks.catchShowPromiseRejection);
+
+    info("Closing the dialog to reject the payment request");
+    BrowserTestUtils.closeWindow(win);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+
+    let result = await ContentTask.spawn(browser, null, async () => content.rqResult);
+    ok(result.showException, "Expected promise rejection from the rq.show() promise");
+    ok(!result.response,
+       "rq.show() shouldn't resolve to a response");
+  });
+});