Bug 1447777 - Add completion fail and timeout error pages. r?MattN
* A new CompletionErrorPage / completion-error-page element which represents the content of the completion error
* Leave the dialog open when complete() results in a 'fail' or 'timeout'.
* The 'done' button on the fail & timeout error page closes the dialog by sending a message up to the paymentDialogWrapper.
* Rewrite the pay button rendering logic to ensure it is disabled when it should be
* Retry handling and UI not addressed here. Will need a new bug when the DOM support has landed.
* Extend completeStatus support in debugging.html and group like actions to tidy up a bit
MozReview-Commit-ID: GDhJqrj14uT
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -552,16 +552,21 @@ var paymentDialogWrapper = {
onChangeShippingOption({optionID}) {
// Note, failing here on browser_host_name.js because the test closes
// 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();
+ },
+
async onUpdateAutofillRecord(collectionName, record, guid, {
errorStateChange,
preserveOldProperties,
selectedStateKey,
successStateChange,
}) {
if (collectionName == "creditCards" && !guid && !record.isTemporary) {
// We need to be logged in so we can encrypt the credit card number and
@@ -657,16 +662,20 @@ var paymentDialogWrapper = {
case "changeShippingAddress": {
this.onChangeShippingAddress(data);
break;
}
case "changeShippingOption": {
this.onChangeShippingOption(data);
break;
}
+ case "closeDialog": {
+ this.onCloseDialogMessage();
+ break;
+ }
case "paymentCancel": {
this.onPaymentCancel();
break;
}
case "pay": {
this.onPay(data);
break;
}
--- a/browser/components/payments/jar.mn
+++ b/browser/components/payments/jar.mn
@@ -10,16 +10,17 @@ browser.jar:
content/payments/paymentDialogWrapper.xul (content/paymentDialogWrapper.xul)
% resource payments %res/payments/
res/payments (res/paymentRequest.*)
res/payments/components/ (res/components/*.css)
res/payments/components/ (res/components/*.js)
res/payments/containers/ (res/containers/*.js)
res/payments/containers/ (res/containers/*.css)
+ res/payments/containers/ (res/containers/*.svg)
res/payments/debugging.css (res/debugging.css)
res/payments/debugging.html (res/debugging.html)
res/payments/debugging.js (res/debugging.js)
res/payments/formautofill/autofillEditForms.js (../../../browser/extensions/formautofill/content/autofillEditForms.js)
res/payments/formautofill/editAddress.xhtml (../../../browser/extensions/formautofill/content/editAddress.xhtml)
res/payments/formautofill/editCreditCard.xhtml (../../../browser/extensions/formautofill/content/editCreditCard.xhtml)
res/payments/unprivileged-fallbacks.js (res/unprivileged-fallbacks.js)
res/payments/mixins/ (res/mixins/*.js)
--- a/browser/components/payments/paymentUIService.js
+++ b/browser/components/payments/paymentUIService.js
@@ -63,25 +63,45 @@ PaymentUIService.prototype = {
Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED :
Ci.nsIPaymentActionResponse.ABORT_FAILED;
abortResponse.init(requestId, response);
paymentSrv.respondPayment(abortResponse);
},
completePayment(requestId) {
- this.log.debug("completePayment:", requestId);
- let closed = this.closeDialog(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":
+ case "timeout":
+ break;
+ default:
+ closed = this.closeDialog(requestId);
+ break;
+ }
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();
+ }
},
updatePayment(requestId) {
let dialog = this.findDialog(requestId);
this.log.debug("updatePayment:", requestId);
if (!dialog) {
this.log.error("updatePayment: no dialog found");
return;
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/completion-error-page.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <completion-error-page></completion-error-page>
+ *
+ * XXX: Bug xxxxx - This page isn't fully localized when used via this custom element
+ * as it will be much easier to implement and share the logic once we switch to Fluent.
+ */
+
+export default class CompletionErrorPage extends PaymentStateSubscriberMixin(PaymentRequestPage) {
+ constructor() {
+ super();
+
+ this.classList.add("error-page");
+ this.suggestionsList = document.createElement("ul");
+ this.suggestions = [];
+ this.body.append(this.suggestionsList);
+
+ this.doneButton = document.createElement("button");
+ this.doneButton.classList.add("done-button", "primary");
+ this.doneButton.addEventListener("click", this);
+
+ this.footer.appendChild(this.doneButton);
+ }
+
+ render(state) {
+ let { page } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`CompletionErrorPage: no need to further render inactive page: ${page.id}`);
+ return;
+ }
+
+ this.pageTitleHeading.textContent = this.dataset.pageTitle;
+ this.doneButton.textContent = this.dataset.doneButtonLabel;
+
+ this.suggestionsList.textContent = "";
+
+ // FIXME: should come from this.dataset.suggestionN when those strings are created
+ this.suggestions[0] = "First suggestion";
+
+ let suggestionsFragment = document.createDocumentFragment();
+ for (let suggestionText of this.suggestions) {
+ let listNode = document.createElement("li");
+ listNode.textContent = suggestionText;
+ suggestionsFragment.appendChild(listNode);
+ }
+ this.suggestionsList.appendChild(suggestionsFragment);
+ }
+
+ handleEvent(event) {
+ if (event.type == "click") {
+ switch (event.target) {
+ case this.doneButton: {
+ this.onDoneButtonClick(event);
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+ }
+
+ onDoneButtonClick(event) {
+ paymentRequest.closeDialog();
+ }
+}
+
+customElements.define("completion-error-page", CompletionErrorPage);
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/error-page.css
@@ -0,0 +1,22 @@
+.error-page.illustrated > .page-body {
+ min-height: 300px;
+ background-position: left center;
+ background-repeat: no-repeat;
+ background-size: 38%;
+ padding-inline-start: 38%;
+}
+
+[dir="rtl"] .error-page.illustrated > .page-body {
+ background-position: right center;
+}
+
+.error-page.illustrated > .page-body > h2 {
+ background: none;
+ padding-inline-start: 0;
+ margin-inline-start: 0;
+}
+
+.error-page#completion-timeout-error .page-body,
+.error-page#completion-fail-error .page-body {
+ background-image: url("./placeholder.svg");
+}
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -7,16 +7,17 @@ import "../vendor/custom-elements.min.js
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
import paymentRequest from "../paymentRequest.js";
import "../components/currency-amount.js";
import "../components/payment-request-page.js";
import "./address-picker.js";
import "./address-form.js";
import "./basic-card-form.js";
+import "./completion-error-page.js";
import "./order-details.js";
import "./payment-method-picker.js";
import "./shipping-option-picker.js";
/* import-globals-from ../unprivileged-fallbacks.js */
/**
* <payment-dialog></payment-dialog>
@@ -117,25 +118,43 @@ export default class PaymentDialog exten
let methodId = state.selectedPaymentCard;
let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
if (modifier && modifier.additionalDisplayItems) {
return modifier.additionalDisplayItems;
}
return [];
}
+ _updateCompleteStatus(state) {
+ let {completeStatus} = state.request;
+ switch (completeStatus) {
+ case "fail":
+ case "timeout":
+ case "unknown":
+ state.page = {
+ id: `completion-${completeStatus}-error`,
+ };
+ state.changesPrevented = false;
+ break;
+ }
+ return state;
+ }
+
/**
* 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) {
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();
let {
selectedPayerAddress,
selectedPaymentCard,
selectedShippingAddress,
@@ -199,32 +218,37 @@ export default class PaymentDialog exten
if (!addresses[selectedPayerAddress]) {
this.requestStore.setState({
selectedPayerAddress: Object.keys(addresses)[0] || null,
});
}
}
_renderPayButton(state) {
- this._payButton.disabled = state.changesPrevented;
let completeStatus = state.request.completeStatus;
switch (completeStatus) {
case "initial":
case "processing":
case "success":
+ case "unknown":
+ this._payButton.disabled = state.changesPrevented;
+ this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
+ break;
case "fail":
- case "unknown":
+ case "timeout":
+ // pay button is hidden in these states. Reset its label and disable it
+ this._payButton.textContent = this._payButton.dataset.initialLabel;
+ this._payButton.disabled = true;
break;
case "":
completeStatus = "initial";
break;
default:
throw new Error(`Invalid completeStatus: ${completeStatus}`);
}
-
this._payButton.textContent = this._payButton.dataset[completeStatus + "Label"];
}
stateChangeCallback(state) {
super.stateChangeCallback(state);
// Don't dispatch change events for initial selectedShipping* changes at initialization
// if requestShipping is false.
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/placeholder.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300">
+ <circle cx="150" cy="150" r="100" stroke="#0a84ff" fill="#c9e4ff"/>
+</svg>
--- a/browser/components/payments/res/debugging.css
+++ b/browser/components/payments/res/debugging.css
@@ -11,8 +11,21 @@ html {
h1 {
font-size: 1em;
}
fieldset > label {
white-space: nowrap;
}
+
+.group {
+ margin: 0.5em 0;
+}
+
+label.block {
+ display: block;
+ margin: 0.3em 0;
+}
+
+button.wide {
+ width: 100%;
+}
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -6,42 +6,65 @@
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<link rel="stylesheet" href="debugging.css"/>
<script src="debugging.js"></script>
</head>
<body>
<div>
- <button id="refresh">Refresh</button>
- <button id="rerender">Re-render</button>
- <button id="logState">Log state</button>
- <button id="debugFrame" hidden>Debug frame</button>
- <h1>Requests</h1>
- <button id="setRequest1">Request 1</button>
- <button id="setRequest2">Request 2</button>
- <fieldset id="paymentOptions">
- <legend>Payment Options</legend>
- <label><input type="checkbox" autocomplete="off" name="requestPayerName" id="setRequestPayerName">requestPayerName</label>
- <label><input type="checkbox" autocomplete="off" name="requestPayerEmail" id="setRequestPayerEmail">requestPayerEmail</label>
- <label><input type="checkbox" autocomplete="off" name="requestPayerPhone" id="setRequestPayerPhone">requestPayerPhone</label>
- <label><input type="checkbox" autocomplete="off" name="requestShipping" id="setRequestShipping">requestShipping</label>
- </fieldset>
- <h1>Addresses</h1>
- <button id="setAddresses1">Set Addreses 1</button>
- <button id="setDupesAddresses">Set Duped Addresses</button>
- <button id="delete1Address">Delete 1 Address</button>
- <h1>Payment Methods</h1>
- <button id="setBasicCards1">Set Basic Cards 1</button>
- <button id="delete1Card">Delete 1 Card</button>
- <h1>States</h1>
- <button id="setChangesPrevented">Prevent changes</button>
- <button id="setChangesAllowed">Allow changes</button>
- <button id="setShippingError">Shipping Error</button>
- <button id="setAddressErrors">Address Errors</button>
- <button id="setStateDefault">Default</button>
- <button id="setStateProcessing">Processing</button>
- <button id="setStateSuccess">Success</button>
- <button id="setStateFail">Fail</button>
- <button id="setStateUnknown">Unknown</button>
+ <section class="group">
+ <button id="refresh">Refresh</button>
+ <button id="rerender">Re-render</button>
+ <button id="logState">Log state</button>
+ <button id="debugFrame" hidden>Debug frame</button>
+ </section>
+ <section class="group">
+ <h1>Requests</h1>
+ <button id="setRequest1">Request 1</button>
+ <button id="setRequest2">Request 2</button>
+ <fieldset id="paymentOptions">
+ <legend>Payment Options</legend>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerName" id="setRequestPayerName">requestPayerName</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerEmail" id="setRequestPayerEmail">requestPayerEmail</label>
+ <label><input type="checkbox" autocomplete="off" name="requestPayerPhone" id="setRequestPayerPhone">requestPayerPhone</label>
+ <label><input type="checkbox" autocomplete="off" name="requestShipping" id="setRequestShipping">requestShipping</label>
+ </fieldset>
+ </section>
+
+ <section class="group">
+ <h1>Addresses</h1>
+ <button id="setAddresses1">Set Addreses 1</button>
+ <button id="setDupesAddresses">Set Duped Addresses</button>
+ <button id="delete1Address">Delete 1 Address</button>
+ </section>
+
+ <section class="group">
+ <h1>Payment Methods</h1>
+ <button id="setBasicCards1">Set Basic Cards 1</button>
+ <button id="delete1Card">Delete 1 Card</button>
+ </section>
+
+ <section class="group">
+ <h1>States</h1>
+ <fieldset>
+ <legend>Complete Status</legend>
+ <label class="block"><input type="radio" name="completeStatus" value="initial" checked="checked">Initial (default)</label>
+ <label class="block"><input type="radio" name="completeStatus" value="processing">Processing</label>
+ <label class="block"><input type="radio" name="completeStatus" value="success">Success</label>
+ <label class="block"><input type="radio" name="completeStatus" value="fail">Fail</label>
+ <label class="block"><input type="radio" name="completeStatus" value="unknown">Unknown</label>
+ <label class="block"><input type="radio" name="completeStatus" value="timeout">Timeout</label>
+ </fieldset>
+ <label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
+ <button id="setCompleteStatus" class="wide">Set Complete Status</button>
+
+ <section class="group">
+ <fieldset>
+ <legend>User Data Errors</legend>
+ <button id="setShippingError">Shipping Error</button>
+ <button id="setAddressErrors">Address Errors</button>
+ </fieldset>
+ </section>
+ </section>
</div>
</body>
</html>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -401,44 +401,23 @@ let buttonActions = {
recipient: "Can only ship to names that start with J",
region: "Can only ship to regions that start with M",
};
requestStore.setState({
request,
});
},
- setStateDefault() {
- let {request} = requestStore.getState();
- request.completeStatus = "initial";
- requestStore.setState({ request });
- },
-
- setStateProcessing() {
- let {request} = requestStore.getState();
- request.completeStatus = "processing";
- requestStore.setState({ request });
- },
-
- setStateSuccess() {
+ setCompleteStatus(e) {
+ let input = document.querySelector("[name='completionState']:checked");
+ let completeStatus = input.value;
let {request} = requestStore.getState();
- request.completeStatus = "success";
- requestStore.setState({ request });
- },
-
- setStateFail() {
- let {request} = requestStore.getState();
- request.completeStatus = "fail";
- requestStore.setState({ request });
- },
-
- setStateUnknown() {
- let {request} = requestStore.getState();
- request.completeStatus = "unknown";
- requestStore.setState({ request });
+ requestStore.setStateFromParent({
+ request: Object.assign(request, { completeStatus }),
+ });
},
};
window.addEventListener("click", function onButtonClick(evt) {
let id = evt.target.id;
if (!id || typeof(buttonActions[id]) != "function") {
return;
}
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -165,16 +165,20 @@ var paymentRequest = {
cancel() {
this.sendMessageToChrome("paymentCancel");
},
pay(data) {
this.sendMessageToChrome("pay", data);
},
+ closeDialog() {
+ this.sendMessageToChrome("closeDialog");
+ },
+
changeShippingAddress(data) {
this.sendMessageToChrome("changeShippingAddress", data);
},
changeShippingOption(data) {
this.sendMessageToChrome("changeShippingOption", data);
},
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -35,31 +35,40 @@
<!ENTITY basicCard.editPage.title "Edit Credit Card">
<!ENTITY payer.addPage.title "Add Payer Contact">
<!ENTITY payer.editPage.title "Edit Payer Contact">
<!ENTITY payerLabel "Contact Information">
<!ENTITY cancelPaymentButton.label "Cancel">
<!ENTITY approvePaymentButton.label "Pay">
<!ENTITY processingPaymentButton.label "Processing">
<!ENTITY successPaymentButton.label "Done">
- <!ENTITY failPaymentButton.label "Fail">
<!ENTITY unknownPaymentButton.label "Unknown">
<!ENTITY orderDetailsLabel "Order Details">
<!ENTITY orderTotalLabel "Total">
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
<!ENTITY basicCardPage.addressAddLink.label "Add">
<!ENTITY basicCardPage.addressEditLink.label "Edit">
<!ENTITY basicCardPage.backButton.label "Back">
<!ENTITY basicCardPage.saveButton.label "Save">
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to &brandShortName; (Security code will not be saved)">
<!ENTITY addressPage.error.genericSave "There was an error saving the address.">
<!ENTITY addressPage.cancelButton.label "Cancel">
<!ENTITY addressPage.backButton.label "Back">
<!ENTITY addressPage.saveButton.label "Save">
<!ENTITY addressPage.persistCheckbox.label "Save address to &brandShortName;">
+ <!ENTITY failErrorPage.title "Sorry! Something went wrong with the payment process.">
+ <!ENTITY failErrorPage.suggestion1 "Check your credit card has not expired.">
+ <!ENTITY failErrorPage.suggestion2 "Make sure your credit card information is accurate.">
+ <!ENTITY failErrorPage.suggestion3 "If no other solutions work, check with shopping.com.">
+ <!ENTITY failErrorPage.doneButton.label "OK">
+ <!ENTITY timeoutErrorPage.title "Whoops! Shopping.com took too long to respond.">
+ <!ENTITY timeoutErrorPage.suggestion1 "Try again later.">
+ <!ENTITY timeoutErrorPage.suggestion2 "Check your network connection." >
+ <!ENTITY timeoutErrorPage.suggestion3 "If no other solutions work, check with shopping.com.">
+ <!ENTITY timeoutErrorPage.doneButton.label "OK">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&paymentSummaryTitle;</title>
<!-- chrome: is needed for global.dtd -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
@@ -68,16 +77,17 @@
<link rel="stylesheet" href="components/rich-select.css"/>
<link rel="stylesheet" href="components/address-option.css"/>
<link rel="stylesheet" href="components/basic-card-option.css"/>
<link rel="stylesheet" href="components/shipping-option.css"/>
<link rel="stylesheet" href="components/payment-details-item.css"/>
<link rel="stylesheet" href="containers/address-form.css"/>
<link rel="stylesheet" href="containers/basic-card-form.css"/>
<link rel="stylesheet" href="containers/order-details.css"/>
+ <link rel="stylesheet" href="containers/error-page.css"/>
<script src="unprivileged-fallbacks.js"></script>
<script src="formautofill/autofillEditForms.js"></script>
<script type="module" src="containers/payment-dialog.js"></script>
<script type="module" src="paymentRequest.js"></script>
@@ -126,17 +136,16 @@
</div>
<footer>
<button id="cancel">&cancelPaymentButton.label;</button>
<button id="pay"
class="primary"
data-initial-label="&approvePaymentButton.label;"
data-processing-label="&processingPaymentButton.label;"
- data-fail-label="&failPaymentButton.label;"
data-unknown-label="&unknownPaymentButton.label;"
data-success-label="&successPaymentButton.label;"></button>
</footer>
</payment-request-page>
<section id="order-details-overlay" hidden="hidden">
<h2>&orderDetailsLabel;</h2>
<order-details></order-details>
</section>
@@ -157,16 +166,25 @@
<address-form id="address-page"
data-error-generic-save="&addressPage.error.genericSave;"
data-cancel-button-label="&addressPage.cancelButton.label;"
data-back-button-label="&addressPage.backButton.label;"
data-save-button-label="&addressPage.saveButton.label;"
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
hidden="hidden"></address-form>
+
+ <completion-error-page id="completion-timeout-error" class="illustrated"
+ data-page-title="&timeoutErrorPage.title;"
+ data-done-button-label="&timeoutErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
+ <completion-error-page id="completion-fail-error" class="illustrated"
+ data-page-title="&failErrorPage.title;"
+ data-done-button-label="&failErrorPage.doneButton.label;"
+ hidden="hidden"></completion-error-page>
</div>
<div id="disabled-overlay" hidden="hidden">
<!-- overlay to prevent changes while waiting for a response from the merchant -->
</div>
</template>
<template id="order-details-template">