Bug 1429189 - Show shipping address errors on the summary screen. r?mattn draft
authorJared Wein <jwein@mozilla.com>
Wed, 21 Feb 2018 14:16:35 -0800
changeset 758770 304f67528299cd9a5e63c124109be848691c3052
parent 758766 c1cb302fefc619400e90bc2c01156023ea71ff75
child 758802 564b804f848919583c4b368bf920b21ec58ada36
push id100166
push userbmo:jaws@mozilla.com
push dateFri, 23 Feb 2018 00:30:05 +0000
reviewersmattn
bugs1429189
milestone60.0a1
Bug 1429189 - Show shipping address errors on the summary screen. r?mattn MozReview-Commit-ID: LaXrvWliWna
toolkit/components/payments/content/paymentDialogWrapper.js
toolkit/components/payments/paymentUIService.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/PaymentTestUtils.jsm
toolkit/components/payments/test/browser/browser.ini
toolkit/components/payments/test/browser/browser_shippingaddresschange_error.js
toolkit/components/payments/test/browser/browser_show_dialog.js
toolkit/components/payments/test/browser/head.js
--- 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