--- 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]