Bug 1435871 - Implement a basic tab-modal dialog container for Payment Request. r=jaws

Differential Revision: https://phabricator.services.mozilla.com/D7934

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Matthew Noorenberghe 2018-10-17 18:46:27 +00:00
Родитель 1e439596ae
Коммит 72a245b99d
11 изменённых файлов: 237 добавлений и 60 удалений

Просмотреть файл

@ -1102,8 +1102,13 @@ window._gBrowser = {
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.

Просмотреть файл

@ -12,6 +12,9 @@
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");
@ -216,7 +219,7 @@ var paymentDialogWrapper = {
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"),
@ -452,7 +455,7 @@ var paymentDialogWrapper = {
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] =
@ -474,12 +477,11 @@ var paymentDialogWrapper = {
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,
});
},
@ -494,8 +496,9 @@ var paymentDialogWrapper = {
const showResponse = this.createShowResponse({
acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
});
paymentSrv.respondPayment(showResponse);
window.close();
paymentUISrv.closePayment(this.request.requestId);
},
async onPay({
@ -575,7 +578,7 @@ var paymentDialogWrapper = {
onCloseDialogMessage() {
// The PR is complete(), just close the dialog
window.close();
paymentUISrv.closePayment(this.request.requestId);
},
async onUpdateAutofillRecord(collectionName, record, guid, messageID) {
@ -685,6 +688,12 @@ var paymentDialogWrapper = {
this.onPaymentCancel();
break;
}
case "paymentDialogReady": {
window.dispatchEvent(new Event("tabmodaldialogready", {
bubbles: true,
}));
break;
}
case "pay": {
this.onPay(data);
break;
@ -693,6 +702,9 @@ var paymentDialogWrapper = {
this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, data.messageID);
break;
}
default: {
throw new Error(`paymentDialogWrapper: Unexpected messageType: ${messageType}`);
}
}
},
};

Просмотреть файл

@ -15,8 +15,12 @@
"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",
@ -45,10 +49,40 @@ PaymentUIService.prototype = {
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() {
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("tab-modal-background", "payment-dialog-background");
// Insert the same way as <tabmodalprompt>.
merchantBrowser.parentNode.insertBefore(tabModalBackground,
merchantBrowser.nextElementSibling);
}, {
once: true,
});
},
abortPayment(requestId) {
@ -81,6 +115,18 @@ PaymentUIService.prototype = {
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;
@ -90,23 +136,18 @@ PaymentUIService.prototype = {
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) {
@ -120,32 +161,48 @@ PaymentUIService.prototype = {
* @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();
browser.parentElement.querySelector(".payment-dialog-background").remove();
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]);

Просмотреть файл

@ -109,7 +109,6 @@ var paymentRequest = {
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")) {
@ -170,6 +169,8 @@ var paymentRequest = {
}
paymentDialog.setStateFromParent(state);
this.sendMessageToChrome("paymentDialogReady");
},
openPreferences() {

Просмотреть файл

@ -112,6 +112,7 @@ var PaymentTestUtils = {
* @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);
@ -121,6 +122,9 @@ var PaymentTestUtils = {
content.showPromise = rq.show();
handle.destruct();
return {
requestId: rq.id,
};
},
},

Просмотреть файл

@ -45,6 +45,8 @@ add_task(async function test_dropdown() {
}
is(event.target.parentElement.id, expectedPopupID, "Checked menulist of opened popup");
event.target.hidePopup(true);
info("clicking cancel");
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);

Просмотреть файл

@ -251,3 +251,68 @@ add_task(async function test_supportedNetworks() {
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(".payment-dialog-background"),
"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");
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");
});
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");
});
});

Просмотреть файл

@ -17,12 +17,13 @@ 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() {
@ -31,19 +32,23 @@ function getPaymentRequests() {
/**
* 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) {
@ -237,19 +242,18 @@ function checkPaymentMethodDetailsMatchesCard(methodDetails, card, msg) {
*/
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);

Просмотреть файл

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Tab Modal Prompt boxes */
.tab-modal-background,
tabmodalprompt {
width: 100%;
height: 100%;
@ -33,3 +34,19 @@ tabmodalprompt {
tabmodalprompt label[value=""] {
visibility: collapse;
}
/* Tab-Modal Payment Request widget */
.paymentDialogContainer {
position: absolute;
}
.paymentDialogContainerFrame {
box-sizing: border-box;
height: 500px;
/* Center the dialog with 20% on the left/right and 60% width for content */
left: 20%;
position: absolute;
/* Overlap the chrome */
top: -6px;
width: 60%;
}

Просмотреть файл

@ -3,12 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Tab Modal Prompt boxes */
.tab-modal-background,
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);

Просмотреть файл

@ -3,11 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Tab Modal Prompt boxes */
.tab-modal-background,
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;