Bug 1447777 - Add completion fail and timeout error pages. r?MattN draft
authorSam Foster <sfoster@mozilla.com>
Fri, 22 Jun 2018 16:57:26 -0700
changeset 820924 6b0220927264e6ec8356736189883462a2b05ae3
parent 820719 313a85e1611469b70cd0d696548c7fd8ff5e894c
child 820927 031daca53983572e0ab8c018d39e7007976b1c44
push id116987
push userbmo:sfoster@mozilla.com
push dateFri, 20 Jul 2018 17:52:05 +0000
reviewersMattN
bugs1447777
milestone63.0a1
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
browser/components/payments/content/paymentDialogWrapper.js
browser/components/payments/jar.mn
browser/components/payments/paymentUIService.js
browser/components/payments/res/containers/completion-error-page.js
browser/components/payments/res/containers/error-page.css
browser/components/payments/res/containers/payment-dialog.js
browser/components/payments/res/containers/placeholder.svg
browser/components/payments/res/debugging.css
browser/components/payments/res/debugging.html
browser/components/payments/res/debugging.js
browser/components/payments/res/paymentRequest.js
browser/components/payments/res/paymentRequest.xhtml
--- 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">