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
--- 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");
+ });
+});