Bug 1429189 - Show shipping address errors on the summary screen. r?mattn
MozReview-Commit-ID: LaXrvWliWna
--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -245,16 +245,27 @@ var paymentDialogWrapper = {
sendMessageToContent(messageType, data = {}) {
this.mm.sendAsyncMessage("paymentChromeToContent", {
data,
messageType,
});
},
+ onDetailsUpdated() {
+ let requestSerialized = this._serializeRequest(this.request);
+
+ this.mm.sendAsyncMessage("paymentChromeToContent", {
+ messageType: "updateState",
+ data: {
+ request: requestSerialized,
+ },
+ });
+ },
+
/**
* Recursively convert and filter input to the subset of data types supported by JSON
*
* @param {*} value - any type of input to serialize
* @param {string?} name - name or key associated with this input.
* E.g. property name or array index.
* @returns {*} serialized deep copy of the value
*/
--- a/toolkit/components/payments/paymentUIService.js
+++ b/toolkit/components/payments/paymentUIService.js
@@ -75,37 +75,51 @@ PaymentUIService.prototype = {
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));
},
updatePayment(requestId) {
+ let dialog = this.findDialog(requestId);
this.log.debug("updatePayment:", requestId);
+ if (!dialog) {
+ this.log.debug("updatePayment: no dialog found");
+ return;
+ }
+ dialog.paymentDialogWrapper.onDetailsUpdated();
},
// 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) {
+ this.log.debug(`closing: ${win.name}`);
+ win.close();
+ return true;
+ }
+ return false;
+ },
+
+ findDialog(requestId) {
let enu = Services.wm.getEnumerator(null);
let win;
while ((win = enu.getNext())) {
if (win.name == `${this.REQUEST_ID_PREFIX}${requestId}`) {
- this.log.debug(`closing: ${win.name}`);
- win.close();
- return true;
+ return win;
}
}
- return false;
+ 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;
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -27,16 +27,17 @@ class PaymentDialog extends PaymentState
this._payButton = contents.querySelector("#pay");
this._payButton.addEventListener("click", this);
this._viewAllButton = contents.querySelector("#view-all");
this._viewAllButton.addEventListener("click", this);
this._orderDetailsOverlay = contents.querySelector("#order-details-overlay");
this._shippingRequestedEls = contents.querySelectorAll(".shippingRequested");
+ this._errorText = contents.querySelector("#error-text");
this._disabledOverlay = contents.getElementById("disabled-overlay");
this.appendChild(contents);
super.connectedCallback();
}
@@ -173,24 +174,26 @@ class PaymentDialog extends PaymentState
}
this._cachedState.selectedShippingAddress = state.selectedShippingAddress;
this._cachedState.selectedShippingOption = state.selectedShippingOption;
}
render(state) {
let request = state.request;
+ let paymentDetails = request.paymentDetails;
this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
- let totalItem = request.paymentDetails.totalItem;
+ let totalItem = paymentDetails.totalItem;
let totalAmountEl = this.querySelector("#total > currency-amount");
totalAmountEl.value = totalItem.amount.value;
totalAmountEl.currency = totalItem.amount.currency;
this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
+ this._errorText.textContent = paymentDetails.error;
for (let element of this._shippingRequestedEls) {
element.hidden = !request.paymentOptions.requestShipping;
}
this._renderPayButton(state);
let {
changesPrevented,
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -66,16 +66,17 @@
<section>
<div class="shippingRequested"><label>&shippingAddressLabel;</label></div>
<address-picker class="shippingRequested" selected-state-key="selectedShippingAddress"></address-picker>
<div class="shippingRequested"><label>&shippingOptionsLabel;</label></div>
<shipping-option-picker class="shippingRequested"></shipping-option-picker>
<div><label>&paymentMethodsLabel;</label></div>
<payment-method-picker selected-state-key="selectedPaymentCard"></payment-method-picker>
+ <div><label id="error-text"></label></div>
</section>
<footer id="controls-container">
<button id="cancel">&cancelPaymentButton.label;</button>
<button id="pay"
data-initial-label="&approvePaymentButton.label;"
data-processing-label="&processingPaymentButton.label;"></button>
</footer>
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -31,16 +31,35 @@ this.PaymentTestUtils = {
});
},
awaitPaymentRequestEventPromise: async ({eventName}) => {
await content[eventName + "Promise"];
return true;
},
+ updateWith: async ({eventName, details}) => {
+ /* globals ok */
+ if (details.error &&
+ (!details.shippingOptions || details.shippingOptions.length)) {
+ ok(false, "Need to clear the shipping options to show error text");
+ }
+ if (!details.total) {
+ ok(false, "`total: { label, amount: { value, currency } }` is required for updateWith");
+ }
+
+ content[eventName + "Promise"] =
+ new Promise(resolve => {
+ content.rq.addEventListener(eventName, event => {
+ event.updateWith(details);
+ resolve();
+ }, {once: true});
+ });
+ },
+
/**
* Create a new payment request and cache it as `rq`.
*
* @param {Object} args
* @param {PaymentMethodData[]} methodData
* @param {PaymentDetailsInit} details
* @param {PaymentOptions} options
*/
@@ -82,16 +101,24 @@ this.PaymentTestUtils = {
selectedOptionIndex,
selectedOptionValue: selectedOption.getAttribute("value"),
selectedOptionLabel: selectedOption.getAttribute("label"),
selectedOptionCurrency: currencyAmount.getAttribute("currency"),
selectedOptionAmount: currencyAmount.getAttribute("amount"),
};
},
+ getErrorDetails: () => {
+ let doc = content.document;
+ let errorText = doc.querySelector("#error-text");
+ return {
+ text: errorText.textContent,
+ };
+ },
+
selectShippingAddressByCountry: country => {
let doc = content.document;
let addressPicker =
doc.querySelector("address-picker[selected-state-key='selectedShippingAddress']");
let select = addressPicker.querySelector("rich-select");
let option = select.querySelector(`[country="${country}"]`);
select.click();
option.click();
@@ -263,9 +290,47 @@ this.PaymentTestUtils = {
},
},
Options: {
requestShippingOption: {
requestShipping: true,
},
},
+
+ Addresses: {
+ TimBL: {
+ "given-name": "Timothy",
+ "additional-name": "John",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+16172535702",
+ email: "timbl@example.org",
+ },
+ TimBL2: {
+ "given-name": "Timothy",
+ "additional-name": "John",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "1 Pommes Frittes Place",
+ "address-level2": "Berlin",
+ "address-level1": "BE",
+ "postal-code": "02138",
+ country: "DE",
+ tel: "+16172535702",
+ email: "timbl@example.org",
+ },
+ },
+
+ BasicCards: {
+ JohnDoe: {
+ "cc-exp-month": 1,
+ "cc-exp-year": 9999,
+ "cc-name": "John Doe",
+ "cc-number": "999999999999",
+ },
+ },
};
--- a/toolkit/components/payments/test/browser/browser.ini
+++ b/toolkit/components/payments/test/browser/browser.ini
@@ -7,11 +7,12 @@ support-files =
blank_page.html
[browser_change_shipping.js]
[browser_host_name.js]
[browser_profile_storage.js]
[browser_request_serialization.js]
[browser_request_shipping.js]
[browser_request_summary.js]
+[browser_shippingaddresschange_error.js]
[browser_show_dialog.js]
skip-if = os == 'win' && debug # bug 1418385
[browser_total.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -0,0 +1,87 @@
+"use strict";
+
+add_task(addSampleAddressesAndBasicCard);
+
+add_task(async function test_show_error_on_addresschange() {
+ 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: [PTU.MethodData.basicCard],
+ details: PTU.Details.twoShippingOptions,
+ options: PTU.Options.requestShippingOption,
+ },
+ 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");
+
+ info("setting up the event handler for shippingoptionchange");
+ let EXPECTED_ERROR_TEXT = "Cannot ship with option 1 on days that end with Y";
+ await ContentTask.spawn(browser, {
+ eventName: "shippingoptionchange",
+ details: {
+ error: EXPECTED_ERROR_TEXT,
+ shippingOptions: [],
+ total: {
+ label: "Grand total is!!!!!: ",
+ amount: {
+ value: "12",
+ currency: "USD",
+ },
+ },
+ },
+ }, PTU.ContentTasks.updateWith);
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectShippingOptionById,
+ "1");
+ info("awaiting the shippingoptionchange event");
+ await ContentTask.spawn(browser, {
+ eventName: "shippingoptionchange",
+ }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+
+ let errors = await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getErrorDetails);
+ is(errors.text, EXPECTED_ERROR_TEXT, "Error text should be present on dialog");
+
+ info("setting up the event handler for shippingaddresschange");
+ await ContentTask.spawn(browser, {
+ eventName: "shippingaddresschange",
+ details: {
+ error: "",
+ shippingOptions: PTU.Details.twoShippingOptions.shippingOptions,
+ total: {
+ label: "Grand total is now: ",
+ amount: {
+ value: "24",
+ currency: "USD",
+ },
+ },
+ },
+ }, PTU.ContentTasks.updateWith);
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectShippingAddressByCountry,
+ "DE");
+ info("awaiting the shippingaddresschange event");
+ await ContentTask.spawn(browser, {
+ eventName: "shippingaddresschange",
+ }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+ errors = await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getErrorDetails);
+ is(errors.text, "", "Error text should not be present on dialog");
+
+ info("clicking cancel");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+ await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+ });
+});
--- a/toolkit/components/payments/test/browser/browser_show_dialog.js
+++ b/toolkit/components/payments/test/browser/browser_show_dialog.js
@@ -148,17 +148,17 @@ add_task(async function test_show_comple
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);
- ContentTask.spawn(browser, {
+ await ContentTask.spawn(browser, {
eventName: "shippingoptionchange",
}, PTU.ContentTasks.promisePaymentRequestEvent);
info("changing shipping option to '1' from default selected option of '2'");
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.selectShippingOptionById, "1");
await ContentTask.spawn(browser, {
eventName: "shippingoptionchange",
--- a/toolkit/components/payments/test/browser/head.js
+++ b/toolkit/components/payments/test/browser/head.js
@@ -127,16 +127,33 @@ function withNewDialogFrame(requestId, t
* @returns {Promise}
*/
function spawnTaskInNewDialog(requestId, contentTaskFn, args = null) {
return withNewDialogFrame(requestId, async function spawnTaskInNewDialog_tabTask(reqFrame) {
await spawnPaymentDialogTask(reqFrame, contentTaskFn, args);
});
}
+async function addSampleAddressesAndBasicCard() {
+ let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+ profileStorage.addresses.add(PTU.Addresses.TimBL);
+ await onChanged;
+
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+ profileStorage.addresses.add(PTU.Addresses.TimBL2);
+ await onChanged;
+
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+ profileStorage.creditCards.add(PTU.BasicCards.JohnDoe);
+ await onChanged;
+}
+
/**
* Open a merchant tab with the given merchantTaskFn to create a PaymentRequest
* and then open the associated PaymentRequest dialog in a new tab and run the
* associated dialogTaskFn. The same taskArgs are passed to both functions.
*
* @param {Function} merchantTaskFn
* @param {Function} dialogTaskFn
* @param {Object} taskArgs