Bug 1435871 - Implement a basic tab-modal dialog container for Payment Request. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Sat, 20 Oct 2018 03:39:32 +0000
changeset 490568 a1db0baedde03d00e96ddd577b8d3672f115a726
parent 490567 24a03346eaf5e5306a1c4d983e30c5102629560c
child 490569 d0f1450799b56b9960ad6df2da432a5fd5849417
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjaws
bugs1435871
milestone64.0a1
Bug 1435871 - Implement a basic tab-modal dialog container for Payment Request. r=jaws Differential Revision: https://phabricator.services.mozilla.com/D7934
browser/base/content/tabbrowser.js
browser/components/payments/content/paymentDialogWrapper.js
browser/components/payments/content/paymentDialogWrapper.xul
browser/components/payments/paymentUIService.js
browser/components/payments/res/containers/payment-dialog.js
browser/components/payments/res/paymentRequest.js
browser/components/payments/test/PaymentTestUtils.jsm
browser/components/payments/test/browser/browser.ini
browser/components/payments/test/browser/browser_dropdowns.js
browser/components/payments/test/browser/browser_show_dialog.js
browser/components/payments/test/browser/head.js
toolkit/components/prompts/content/tabprompts.css
toolkit/themes/osx/global/tabprompts.css
toolkit/themes/windows/global/tabprompts.css
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1089,18 +1089,23 @@ window._gBrowser = {
       return;
 
     let newBrowser = this.getBrowserForTab(newTab);
 
     // If there's a tabmodal prompt showing, focus it.
     if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
       let prompts = newBrowser.parentNode.getElementsByTagNameNS(this._XUL_NS, "tabmodalprompt");
       let prompt = prompts[prompts.length - 1];
-      prompt.Dialog.setDefaultFocus();
-      return;
+      // @tabmodalPromptShowing is also set for other tab modal prompts
+      // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
+      // Bug 1492814 will implement this for the Payment Request dialog.
+      if (prompt) {
+        prompt.Dialog.setDefaultFocus();
+        return;
+      }
     }
 
     // Focus the location bar if it was previously focused for that tab.
     // In full screen mode, only bother making the location bar visible
     // if the tab is a blank one.
     if (newBrowser._urlbarFocused && gURLBar) {
       // Explicitly close the popup if the URL bar retains focus
       gURLBar.closePopup();
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -7,16 +7,19 @@
  * own scope.
  */
 
 "use strict";
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 
+const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
+                     .getService(Ci.nsIPaymentUIService);
+
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "MasterPassword",
                                "resource://formautofill/MasterPassword.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -212,17 +215,17 @@ var paymentDialogWrapper = {
     this.mm.addMessageListener("paymentContentToChrome", this);
     this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
     // Until we have bug 1446164 and bug 1407418 we use form autofill's temporary
     // shim for data-localization* attributes.
     this.mm.loadFrameScript("chrome://formautofill/content/l10n.js", true);
     if (AppConstants.platform == "win") {
       this.frame.setAttribute("selectmenulist", "ContentSelectDropdown-windows");
     }
-    this.frame.loadURI("resource://payments/paymentRequest.xhtml");
+    this.frame.setAttribute("src", "resource://payments/paymentRequest.xhtml");
 
     this.temporaryStore = {
       addresses: new TempCollection("addresses"),
       creditCards: new TempCollection("creditCards"),
     };
   },
 
   createShowResponse({
@@ -448,17 +451,17 @@ var paymentDialogWrapper = {
     }
     return obj;
   },
 
   async initializeFrame() {
     Services.obs.addObserver(this, "formautofill-storage-changed", true);
 
     let requestSerialized = this._serializeRequest(this.request);
-    let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    let chromeWindow = window.frameElement.ownerGlobal;
     let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
 
     let [savedAddresses, savedBasicCards] =
       await Promise.all([this.fetchSavedAddresses(), this.fetchSavedPaymentCards()]);
 
     this.sendMessageToContent("showPaymentRequest", {
       request: requestSerialized,
       savedAddresses,
@@ -470,38 +473,38 @@ var paymentDialogWrapper = {
   },
 
   debugFrame() {
     // To avoid self-XSS-type attacks, ensure that Browser Chrome debugging is enabled.
     if (!Services.prefs.getBoolPref("devtools.chrome.enabled", false)) {
       Cu.reportError("devtools.chrome.enabled must be enabled to debug the frame");
       return;
     }
-    let chromeWindow = Services.wm.getMostRecentWindow(null);
     let {
       gDevToolsBrowser,
     } = ChromeUtils.import("resource://devtools/client/framework/gDevTools.jsm", {});
     gDevToolsBrowser.openContentProcessToolbox({
-      selectedBrowser: chromeWindow.document.getElementById("paymentRequestFrame").frameLoader,
+      selectedBrowser: document.getElementById("paymentRequestFrame").frameLoader,
     });
   },
 
   onOpenPreferences() {
     let prefsURL = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
     prefsURL.data = "about:preferences#privacy-form-autofill";
     Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, "_blank", "chrome,all,dialog=no",
                            prefsURL);
   },
 
   onPaymentCancel() {
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
     });
+
     paymentSrv.respondPayment(showResponse);
-    window.close();
+    paymentUISrv.closePayment(this.request.requestId);
   },
 
   async onPay({
     selectedPayerAddressGUID: payerGUID,
     selectedPaymentCardGUID: paymentCardGUID,
     selectedPaymentCardSecurityCode: cardSecurityCode,
     selectedShippingAddressGUID: shippingGUID,
   }) {
@@ -580,17 +583,17 @@ var paymentDialogWrapper = {
     // the dialog before the onChangeShippingOption is called, thus
     // deleting the request and making the requestId invalid. Unclear
     // why we aren't seeing the same issue with onChangeShippingAddress.
     paymentSrv.changeShippingOption(this.request.requestId, optionID);
   },
 
   onCloseDialogMessage() {
     // The PR is complete(), just close the dialog
-    window.close();
+    paymentUISrv.closePayment(this.request.requestId);
   },
 
   async onUpdateAutofillRecord(collectionName, record, guid, messageID) {
     let responseMessage = {
       guid,
       messageID,
       stateChange: {},
     };
@@ -693,24 +696,33 @@ var paymentDialogWrapper = {
       case "openPreferences": {
         this.onOpenPreferences();
         break;
       }
       case "paymentCancel": {
         this.onPaymentCancel();
         break;
       }
+      case "paymentDialogReady": {
+        window.dispatchEvent(new Event("tabmodaldialogready", {
+          bubbles: true,
+        }));
+        break;
+      }
       case "pay": {
         this.onPay(data);
         break;
       }
       case "updateAutofillRecord": {
         this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, data.messageID);
         break;
       }
+      default: {
+        throw new Error(`paymentDialogWrapper: Unexpected messageType: ${messageType}`);
+      }
     }
   },
 };
 
 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");
--- a/browser/components/payments/content/paymentDialogWrapper.xul
+++ b/browser/components/payments/content/paymentDialogWrapper.xul
@@ -33,12 +33,12 @@
   </popupset>
 
   <browser type="content"
            id="paymentRequestFrame"
            disablehistory="true"
            nodefaultsrc="true"
            remote="true"
            selectmenulist="ContentSelectDropdown"
-           style="height:500px;width:600px"
+           style="height:100vh;width:100vw"
            transparent="true"></browser>
   <script type="application/javascript" src="chrome://payments/content/paymentDialogWrapper.js"></script>
 </window>
--- a/browser/components/payments/paymentUIService.js
+++ b/browser/components/payments/paymentUIService.js
@@ -10,18 +10,22 @@
  * PaymentUIService is started by the DOM code lazily.
  *
  * For now the UI is shown in a native dialog but that is likely to change.
  * Tests should try to avoid relying on that implementation detail.
  */
 
 "use strict";
 
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
+                               "resource:///modules/BrowserWindowTracker.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "paymentSrv",
                                    "@mozilla.org/dom/payments/payment-request-service;1",
                                    "nsIPaymentRequestService");
 
 function PaymentUIService() {
   this.wrappedJSObject = this;
@@ -40,20 +44,54 @@ PaymentUIService.prototype = {
   QueryInterface: ChromeUtils.generateQI([Ci.nsIPaymentUIService]),
   DIALOG_URL: "chrome://payments/content/paymentDialogWrapper.xul",
   REQUEST_ID_PREFIX: "paymentRequest-",
 
   // 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");
+    let request = paymentSrv.getPaymentRequestById(requestId);
+    let merchantBrowser = this.findBrowserByTabId(request.tabId);
+    let chromeWindow = merchantBrowser.ownerGlobal;
+    let {gBrowser} = chromeWindow;
+    let browserContainer = gBrowser.getBrowserContainer(merchantBrowser);
+    let container = chromeWindow.document.createElementNS(XHTML_NS, "div");
+    container.dataset.requestId = requestId;
+    container.classList.add("paymentDialogContainer");
+    container.hidden = true;
+    let paymentsBrowser = chromeWindow.document.createElementNS(XHTML_NS, "iframe");
+    paymentsBrowser.classList.add("paymentDialogContainerFrame");
+    paymentsBrowser.setAttribute("type", "content");
+    paymentsBrowser.setAttribute("remote", "true");
+    paymentsBrowser.setAttribute("src", `${this.DIALOG_URL}?requestId=${requestId}`);
+    // append the frame to start the loading
+    container.appendChild(paymentsBrowser);
+    browserContainer.prepend(container);
+
+    // Only show the frame and change the UI when the dialog is ready to show.
+    paymentsBrowser.addEventListener("tabmodaldialogready", function readyToShow() {
+      if (!container) {
+        // The dialog was closed by the DOM code before it was ready to be shown.
+        return;
+      }
+      container.hidden = false;
+
+      // Prevent focusing or interacting with the <browser>.
+      merchantBrowser.setAttribute("tabmodalPromptShowing", "true");
+
+      // Darken the merchant content area.
+      let tabModalBackground = chromeWindow.document.createElement("box");
+      tabModalBackground.classList.add("tabModalBackground", "paymentDialogBackground");
+      // Insert the same way as <tabmodalprompt>.
+      merchantBrowser.parentNode.insertBefore(tabModalBackground,
+                                              merchantBrowser.nextElementSibling);
+    }, {
+      once: true,
+    });
   },
 
   abortPayment(requestId) {
     this.log.debug("abortPayment:", requestId);
     let abortResponse = Cc["@mozilla.org/dom/payments/payment-abort-action-response;1"]
                           .createInstance(Ci.nsIPaymentAbortActionResponse);
     let found = this.closeDialog(requestId);
 
@@ -76,76 +114,107 @@ PaymentUIService.prototype = {
     switch (completeStatus) {
       case "fail":
       case "timeout":
         break;
       default:
         closed = this.closeDialog(requestId);
         break;
     }
+
+    let dialogContainer;
+    if (!closed) {
+      // We need to call findDialog before we respond below as getPaymentRequestById
+      // may fail due to the request being removed upon completion.
+      dialogContainer = this.findDialog(requestId).dialogContainer;
+      if (!dialogContainer) {
+        this.log.error("completePayment: no dialog found");
+        return;
+      }
+    }
+
     let responseCode = closed ?
         Ci.nsIPaymentActionResponse.COMPLETE_SUCCEEDED :
         Ci.nsIPaymentActionResponse.COMPLETE_FAILED;
     let completeResponse = Cc["@mozilla.org/dom/payments/payment-complete-action-response;1"]
                              .createInstance(Ci.nsIPaymentCompleteActionResponse);
     completeResponse.init(requestId, responseCode);
     paymentSrv.respondPayment(completeResponse.QueryInterface(Ci.nsIPaymentActionResponse));
 
     if (!closed) {
-      let dialog = this.findDialog(requestId);
-      if (!dialog) {
-        this.log.error("completePayment: no dialog found");
-        return;
-      }
-      dialog.paymentDialogWrapper.updateRequest();
+      dialogContainer.querySelector("iframe").contentWindow.paymentDialogWrapper.updateRequest();
     }
   },
 
   updatePayment(requestId) {
-    let dialog = this.findDialog(requestId);
+    let {dialogContainer} = this.findDialog(requestId);
     this.log.debug("updatePayment:", requestId);
-    if (!dialog) {
+    if (!dialogContainer) {
       this.log.error("updatePayment: no dialog found");
       return;
     }
-    dialog.paymentDialogWrapper.updateRequest();
+    dialogContainer.querySelector("iframe").contentWindow.paymentDialogWrapper.updateRequest();
   },
 
   closePayment(requestId) {
     this.closeDialog(requestId);
   },
 
   // other helper methods
 
   /**
    * @param {string} requestId - Payment Request ID of the dialog to close.
    * @returns {boolean} whether the specified dialog was closed.
    */
   closeDialog(requestId) {
-    let win = this.findDialog(requestId);
-    if (!win) {
+    let {
+      browser,
+      dialogContainer,
+    } = this.findDialog(requestId);
+    if (!dialogContainer) {
       return false;
     }
-    this.log.debug(`closing: ${win.name}`);
-    win.close();
+    this.log.debug(`closing: ${requestId}`);
+    dialogContainer.remove();
+    if (!dialogContainer.hidden) {
+      // If the container is no longer hidden then the background was added after
+      // `tabmodaldialogready` so remove it.
+      browser.parentElement.querySelector(".paymentDialogBackground").remove();
+
+      if (!browser.tabModalPromptBox || browser.tabModalPromptBox.listPrompts().length == 0) {
+        browser.removeAttribute("tabmodalPromptShowing");
+      }
+    }
     return true;
   },
 
   findDialog(requestId) {
-    for (let win of Services.wm.getEnumerator(null)) {
-      if (win.name == `${this.REQUEST_ID_PREFIX}${requestId}`) {
-        return win;
+    for (let win of BrowserWindowTracker.orderedWindows) {
+      for (let dialogContainer of win.document.querySelectorAll(".paymentDialogContainer")) {
+        if (dialogContainer.dataset.requestId == requestId) {
+          return {
+            dialogContainer,
+            browser: dialogContainer.parentElement.querySelector("browser"),
+          };
+        }
+      }
+    }
+    return {};
+  },
+
+  findBrowserByTabId(tabId) {
+    for (let win of BrowserWindowTracker.orderedWindows) {
+      for (let browser of win.gBrowser.browsers) {
+        if (!browser.frameLoader || !browser.frameLoader.tabParent) {
+          continue;
+        }
+        if (browser.frameLoader.tabParent.tabId == tabId) {
+          return browser;
+        }
       }
     }
 
+    this.log.error("findBrowserByTabId: No browser found for tabId:", tabId);
     return null;
   },
-
-  requestIdForWindow(window) {
-    let windowName = window.name;
-
-    return windowName.startsWith(this.REQUEST_ID_PREFIX) ?
-      windowName.replace(this.REQUEST_ID_PREFIX, "") : // returns suffix, which is the requestId
-      null;
-  },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PaymentUIService]);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -179,17 +179,17 @@ export default class PaymentDialog exten
 
   /**
    * Set some state from the privileged parent process.
    * Other elements that need to set state should use their own `this.requestStore.setState`
    * method provided by the `PaymentStateSubscriberMixin`.
    *
    * @param {object} state - See `PaymentsStore.setState`
    */
-  setStateFromParent(state) {
+  async setStateFromParent(state) {
     let oldAddresses = paymentRequest.getAddresses(this.requestStore.getState());
     if (state.request) {
       state = this._updateCompleteStatus(state);
     }
     this.requestStore.setState(state);
 
     // Check if any foreign-key constraints were invalidated.
     state = this.requestStore.getState();
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -104,17 +104,16 @@ var paymentRequest = {
         break;
       }
     }
   },
 
   onPaymentRequestLoad() {
     log.debug("onPaymentRequestLoad");
     window.addEventListener("unload", this, {once: true});
-    this.sendMessageToChrome("paymentDialogReady");
 
     // Automatically show the debugging console if loaded with a truthy `debug` query parameter.
     if (new URLSearchParams(location.search).get("debug")) {
       this.toggleDebuggingConsole();
     }
   },
 
   async onShowPaymentRequest(detail) {
@@ -167,17 +166,19 @@ var paymentRequest = {
         id: "basic-card-page",
         onboardingWizard: true,
       };
       state["basic-card-page"] = {
         selectedStateKey: "selectedPaymentCard",
       };
     }
 
-    paymentDialog.setStateFromParent(state);
+    await paymentDialog.setStateFromParent(state);
+
+    this.sendMessageToChrome("paymentDialogReady");
   },
 
   openPreferences() {
     this.sendMessageToChrome("openPreferences");
   },
 
   cancel() {
     this.sendMessageToChrome("paymentCancel");
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -107,25 +107,29 @@ var PaymentTestUtils = {
 
     /**
      * Create a new payment request cached as `rq` and then show it.
      *
      * @param {Object} args
      * @param {PaymentMethodData[]} methodData
      * @param {PaymentDetailsInit} details
      * @param {PaymentOptions} options
+     * @returns {Object}
      */
     createAndShowRequest: ({methodData, details, options}) => {
       const rq = new content.PaymentRequest(Cu.cloneInto(methodData, content), 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();
+      return {
+        requestId: rq.id,
+      };
     },
   },
 
   DialogContentTasks: {
     getShippingOptions: () => {
       let picker = content.document.querySelector("shipping-option-picker");
       let popupBox = Cu.waiveXrays(picker).dropdown.popupBox;
       let selectedOptionIndex = popupBox.selectedIndex;
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -5,17 +5,17 @@ prefs =
   dom.payments.request.enabled=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
 [browser_address_edit.js]
 skip-if = verify && debug && os == 'mac'
 [browser_card_edit.js]
-skip-if = os == 'linux' && debug # bug 1465673
+skip-if = (verify && debug && os == 'mac') || (os == 'linux' && debug) # bug 1465673
 [browser_change_shipping.js]
 [browser_dropdowns.js]
 [browser_host_name.js]
 [browser_onboarding_wizard.js]
 [browser_openPreferences.js]
 [browser_payment_completion.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
--- a/browser/components/payments/test/browser/browser_dropdowns.js
+++ b/browser/components/payments/test/browser/browser_dropdowns.js
@@ -40,14 +40,16 @@ add_task(async function test_dropdown() 
 
     let event = await popupshownPromise;
     let expectedPopupID = "ContentSelectDropdown";
     if (AppConstants.platform == "win") {
       expectedPopupID = "ContentSelectDropdown-windows";
     }
     is(event.target.parentElement.id, expectedPopupID, "Checked menulist of opened popup");
 
+    event.target.hidePopup(true);
+
     info("clicking cancel");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
 
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
--- a/browser/components/payments/test/browser/browser_show_dialog.js
+++ b/browser/components/payments/test/browser/browser_show_dialog.js
@@ -246,8 +246,80 @@ add_task(async function test_supportedNe
     await spawnPaymentDialogTask(frame, async () => {
       ok(!content.document.getElementById("pay").disabled, "pay button should not be disabled");
     });
 
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
 });
+
+add_task(async function test_tab_modal() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win, frame} = await setupPaymentDialog(browser, {
+      methodData,
+      details,
+      merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+    });
+
+    await TestUtils.waitForCondition(() => {
+      return !document.querySelector(".paymentDialogContainer").hidden;
+    }, "Waiting for container to be visible after the dialog's ready");
+
+    ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
+
+    let {
+      bottom: toolboxBottom,
+    } = document.getElementById("navigator-toolbox").getBoundingClientRect();
+
+    let {x, y} = win.frameElement.getBoundingClientRect();
+    ok(y > 0, "Frame should have y > 0");
+    // Inset by 10px since the corner point doesn't return the frame due to the
+    // border-radius.
+    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
+       "Check .paymentDialogContainerFrame is visible");
+
+    info("Click to the left of the dialog over the content area");
+    isnot(document.elementFromPoint(x - 10, y + 50), browser,
+          "Check clicks on the merchant content area don't go to the browser");
+    is(document.elementFromPoint(x - 10, y + 50),
+       document.querySelector(".paymentDialogBackground"),
+       "Check clicks on the merchant content area go to the payment dialog background");
+
+    ok(y < toolboxBottom - 2, "Dialog should overlap the toolbox by at least 2px");
+
+    ok(browser.hasAttribute("tabmodalPromptShowing"), "Check browser has @tabmodalPromptShowing");
+
+    await BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: BLANK_PAGE_URL,
+    }, async newBrowser => {
+      let {
+        x: x2,
+        y: y2,
+      } = win.frameElement.getBoundingClientRect();
+      is(x2, x, "Check x-coordinate is the same");
+      is(y2, y, "Check y-coordinate is the same");
+      isnot(document.elementFromPoint(x + 10, y + 10), win.frameElement,
+            "Check .paymentDialogContainerFrame is hidden");
+      ok(!newBrowser.hasAttribute("tabmodalPromptShowing"),
+         "Check second browser doesn't have @tabmodalPromptShowing");
+    });
+
+    let {
+      x: x3,
+      y: y3,
+    } = win.frameElement.getBoundingClientRect();
+    is(x3, x, "Check x-coordinate is the same again");
+    is(y3, y, "Check y-coordinate is the same again");
+    is(document.elementFromPoint(x + 10, y + 10), win.frameElement,
+       "Check .paymentDialogContainerFrame is visible again");
+
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+
+    await BrowserTestUtils.waitForCondition(() => !browser.hasAttribute("tabmodalPromptShowing"),
+                                            "Check @tabmodalPromptShowing was removed");
+  });
+});
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -12,43 +12,48 @@ const BLANK_PAGE_PATH = "/browser/browse
 const BLANK_PAGE_URL = "https://example.com" + BLANK_PAGE_PATH;
 const RESPONSE_TIMEOUT_PREF = "dom.payments.response.timeout";
 const SAVE_CREDITCARD_DEFAULT_PREF = "dom.payments.defaults.saveCreditCard";
 const SAVE_ADDRESS_DEFAULT_PREF = "dom.payments.defaults.saveAddress";
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
-                     .getService().wrappedJSObject;
+                     .getService(Ci.nsIPaymentUIService).wrappedJSObject;
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
 const {formAutofillStorage} = ChromeUtils.import(
   "resource://formautofill/FormAutofillStorage.jsm", {});
 const {PaymentTestUtils: PTU} = ChromeUtils.import(
   "resource://testing-common/PaymentTestUtils.jsm", {});
+ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 function getPaymentRequests() {
   return Array.from(paymentSrv.enumerate());
 }
 
 /**
  * Return the container (e.g. dialog or overlay) that the payment request contents are shown in.
- * This abstracts away the details of the widget used so that this can more earily transition from a
- * dialog to another kind of overlay.
- * Consumers shouldn't rely on a dialog window being returned.
+ * This abstracts away the details of the widget used so that this can more easily transition to
+ * another kind of dialog/overlay.
+ * @param {string} requestId
  * @returns {Promise}
  */
-async function getPaymentWidget() {
-  let win;
-  await BrowserTestUtils.waitForCondition(() => {
-    win = Services.wm.getMostRecentWindow(null);
-    return win.name.startsWith(paymentUISrv.REQUEST_ID_PREFIX);
-  }, "payment dialog should be the most recent");
-
-  return win;
+async function getPaymentWidget(requestId) {
+  return BrowserTestUtils.waitForCondition(() => {
+    let {dialogContainer} = paymentUISrv.findDialog(requestId);
+    if (!dialogContainer) {
+      return false;
+    }
+    let browserIFrame = dialogContainer.querySelector("iframe");
+    if (!browserIFrame) {
+      return false;
+    }
+    return browserIFrame.contentWindow;
+  }, "payment dialog should be opened");
 }
 
 async function getPaymentFrame(widget) {
   return widget.document.getElementById("paymentRequestFrame");
 }
 
 function waitForMessageFromWidget(messageType, widget = null) {
   info("waitForMessageFromWidget: " + messageType);
@@ -232,29 +237,28 @@ function checkPaymentMethodDetailsMatche
  * @param {Object} options.methodData
  * @param {Object} options.details
  * @param {Object} options.options
  * @param {Function} options.merchantTaskFn
  * @returns {Object} References to the window, requestId, and frame
  */
 async function setupPaymentDialog(browser, {methodData, details, options, merchantTaskFn}) {
   let dialogReadyPromise = waitForWidgetReady();
-  await ContentTask.spawn(browser,
-                          {
-                            methodData,
-                            details,
-                            options,
-                          },
-                          merchantTaskFn);
+  let {requestId} = await ContentTask.spawn(browser,
+                                            {
+                                              methodData,
+                                              details,
+                                              options,
+                                            },
+                                            merchantTaskFn);
+  ok(requestId, "requestId should be defined");
 
   // get a reference to the UI dialog and the requestId
-  let [win] = await Promise.all([getPaymentWidget(), dialogReadyPromise]);
+  let [win] = await Promise.all([getPaymentWidget(requestId), 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");
 
@@ -654,17 +658,17 @@ async function fillInCardForm(frame, aCa
  * but is missing from content task scope.
  * You should call this method only once per <browser> tag
  *
  * @param {xul:browser} browser
  *        Reference to the browser in which we load content task
  */
 /* eslint-enable valid-jsdoc */
 async function injectEventUtilsInContentTask(browser) {
-  await ContentTask.spawn(browser, {}, async function() {
+  await spawnPaymentDialogTask(browser, async function injectEventUtils() {
     if ("EventUtils" in this) {
       return;
     }
 
     const EventUtils = this.EventUtils = {};
 
     EventUtils.window = {};
     EventUtils.parent = EventUtils.window;
--- a/toolkit/components/prompts/content/tabprompts.css
+++ b/toolkit/components/prompts/content/tabprompts.css
@@ -1,13 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Tab Modal Prompt boxes */
+.tabModalBackground,
 tabmodalprompt {
   width: 100%;
   height: 100%;
   -moz-box-pack: center;
   -moz-box-orient: vertical;
 }
 
 .tabmodalprompt-mainContainer {
@@ -28,8 +29,39 @@ tabmodalprompt {
   cursor: text !important;
   white-space: pre-wrap;
   unicode-bidi: plaintext;
 }
 
 tabmodalprompt label[value=""] {
   visibility: collapse;
 }
+
+/* Tab-Modal Payment Request widget */
+.paymentDialogContainer:not([hidden]) {
+  /* Center the .paymentDialogContainerFrame horizontally with flexbox. */
+  display: flex;
+  flex-direction: column;
+  position: absolute;
+}
+
+.paymentDialogContainerFrame {
+  align-self: center;
+  box-sizing: border-box;
+  height: 500px;
+  /* By setting `left` & `right` to `auto` with `position: absolute`, the
+   * horizontal position from the `align-self: center` is used.
+   * See https://developer.mozilla.org/en-US/docs/Web/CSS/right#Values. */
+  left: auto;
+  /* Shrink the height for small browser window sizes so the dialog footer
+     remains visible.
+     Ideally this would be 100vh minus the #navigator-toolbox height. */
+  max-height: 75vh;
+  /* Leave a 16px border on each side when the normal dialog width can't fit in
+   * the browser window. This ensure that the dialog still looks like a dialog
+   * (with content showing beside) instead of a full-width overlay. */
+  max-width: calc(100vw - 16px - 16px);
+  position: absolute;
+  right: auto;
+  /* Vertically overlap the browser chrome. */
+  top: -3px;
+  width: 600px;
+}
--- a/toolkit/themes/osx/global/tabprompts.css
+++ b/toolkit/themes/osx/global/tabprompts.css
@@ -1,19 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Tab Modal Prompt boxes */
+.tabModalBackground,
 tabmodalprompt {
   background-color: hsla(0,0%,10%,.5);
+}
+
+tabmodalprompt {
   font-family: sans-serif; /* use content font not system UI font */
   font-size: 110%;
 }
 
+.paymentDialogContainerFrame,
 .tabmodalprompt-mainContainer {
   color: black;
   background-color: hsla(0,0%,100%,.95);
   background-clip: padding-box;
   border-radius: 2px;
   border: 1px solid hsla(0,0%,0%,.5);
 }
 
--- a/toolkit/themes/windows/global/tabprompts.css
+++ b/toolkit/themes/windows/global/tabprompts.css
@@ -1,18 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Tab Modal Prompt boxes */
+.tabModalBackground,
 tabmodalprompt {
   background-color: hsla(0,0%,10%,.5);
+}
+
+tabmodalprompt {
   font-family: sans-serif; /* use content font not system UI font */
 }
 
+.paymentDialogContainerFrame,
 .tabmodalprompt-mainContainer {
   color: -moz-fieldText;
   background-color: -moz-field;
   border-radius: 2px;
   border: 1px solid threeDDarkShadow;
 }
 
 .tabmodalprompt-topContainer {