Bug 1435163 - Show merchant address errors on the summary screen. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Sat, 29 Sep 2018 00:17:52 +0000
changeset 494601 ea8385466c064003f60995f428a5a15d660a2bb2
parent 494600 e782c165223aa344fceb958409ede76b7797b6f2
child 494602 8152c0b1017c1d77dc1d1e7ef9f5dce850e2c492
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1435163
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1435163 - Show merchant address errors on the summary screen. r=jaws Differential Revision: https://phabricator.services.mozilla.com/D7160
browser/components/payments/res/components/rich-select.css
browser/components/payments/res/containers/address-picker.js
browser/components/payments/res/containers/payment-method-picker.js
browser/components/payments/res/containers/rich-picker.css
browser/components/payments/res/containers/rich-picker.js
browser/components/payments/test/mochitest/test_address_picker.html
browser/components/payments/test/mochitest/test_payment_method_picker.html
--- a/browser/components/payments/res/components/rich-select.css
+++ b/browser/components/payments/res/components/rich-select.css
@@ -2,17 +2,18 @@
  * 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/. */
 
 rich-select {
   /* Include the padding in the max-width calculation so that we truncate rather
      than grow wider than 100% of the parent. */
   box-sizing: border-box;
   display: block;
-  margin: 14px 0;
+  /* Has to be the same as `payment-method-picker > input`: */
+  margin: 10px 0;
   /* Padding for the dropmarker (copied from common.css) */
   padding-inline-end: 24px;
   position: relative;
   /* Don't allow the <rich-select> to grow wider than the container so that we
      truncate with text-overflow for long options instead. */
   max-width: 100%;
 }
 
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -1,12 +1,13 @@
 /* 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 AddressForm from "./address-form.js";
 import AddressOption from "../components/address-option.js";
 import RichPicker from "./rich-picker.js";
 import paymentRequest from "../paymentRequest.js";
 
 /**
  * <address-picker></address-picker>
  * Container around add/edit links and <rich-select> with
  * <address-option> listening to savedAddresses & tempAddresses.
@@ -145,16 +146,34 @@ export default class AddressPicker exten
 
     super.render(state);
   }
 
   get selectedStateKey() {
     return this.getAttribute("selected-state-key");
   }
 
+  errorForSelectedOption(state) {
+    let superError = super.errorForSelectedOption(state);
+    if (superError) {
+      return superError;
+    }
+
+    if (!this.selectedOption) {
+      return "";
+    }
+
+    let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(state,
+                                                                     [this.selectedStateKey]);
+    // TODO: errors in priority order.
+    return Object.values(merchantFieldErrors).find(msg => {
+      return typeof(msg) == "string" && msg.length;
+    }) || "";
+  }
+
   handleEvent(event) {
     switch (event.type) {
       case "change": {
         this.onChange(event);
         break;
       }
       case "click": {
         this.onClick(event);
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -79,35 +79,35 @@ export default class PaymentMethodPicker
     let securityCodeState = state[this.selectedStateKey + "SecurityCode"];
     if (securityCodeState && securityCodeState != this.securityCodeInput.value) {
       this.securityCodeInput.defaultValue = securityCodeState;
     }
 
     super.render(state);
   }
 
-  isSelectedOptionValid(state) {
-    let hasMissingFields = this.missingFieldsOfSelectedOption().length;
-    if (hasMissingFields) {
-      return false;
+  errorForSelectedOption(state) {
+    let superError = super.errorForSelectedOption(state);
+    if (superError) {
+      return superError;
     }
     let selectedOption = this.selectedOption;
     if (!selectedOption) {
-      return true;
+      return "";
     }
 
     let basicCardMethod = state.request.paymentMethods
       .find(method => method.supportedMethods == "basic-card");
     let merchantNetworks = basicCardMethod && basicCardMethod.data &&
                            basicCardMethod.data.supportedNetworks;
     let acceptedNetworks = merchantNetworks || PaymentDialogUtils.getCreditCardNetworks();
     let selectedCard = paymentRequest.getBasicCards(state)[selectedOption.value];
     let isSupported = selectedCard["cc-type"] &&
                       acceptedNetworks.includes(selectedCard["cc-type"]);
-    return isSupported;
+    return isSupported ? "" : this.dataset.invalidLabel;
   }
 
   get selectedStateKey() {
     return this.getAttribute("selected-state-key");
   }
 
   handleEvent(event) {
     switch (event.type) {
--- a/browser/components/payments/res/containers/rich-picker.css
+++ b/browser/components/payments/res/containers/rich-picker.css
@@ -4,16 +4,17 @@
 
 .rich-picker {
   display: grid;
   grid-template-columns: 5fr auto auto;
   grid-template-areas:
     "label    edit     add"
     "dropdown dropdown dropdown"
     "invalid  invalid  invalid";
+  padding-top: 8px;
 }
 
 .rich-picker > label {
   color: #0c0c0d;
   font-weight: 700;
   grid-area: label;
 }
 
@@ -57,13 +58,13 @@ payment-method-picker.rich-picker {
     "dropdown cvv    cvv  cvv"
     "invalid  invalid invalid invalid";
 }
 
 payment-method-picker > input {
   border: 1px solid #0C0C0D33;
   border-inline-start: none;
   grid-area: cvv;
-  margin: 14px 0; /* Has to be same as rich-select */
+  margin: 10px 0; /* Has to be same as rich-select */
   padding: 8px;
   /* So the error outline appears above the adjacent dropdown */
   z-index: 1;
 }
--- a/browser/components/payments/res/containers/rich-picker.js
+++ b/browser/components/payments/res/containers/rich-picker.js
@@ -31,17 +31,16 @@ export default class RichPicker extends 
     this.editLink.className = "edit-link";
     this.editLink.href = "javascript:void(0)";
     this.editLink.textContent = this.dataset.editLinkLabel;
     this.editLink.addEventListener("click", this);
 
     this.invalidLabel = document.createElement("label");
     this.invalidLabel.className = "invalid-label";
     this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
-    this.invalidLabel.textContent = this.dataset.invalidLabel;
   }
 
   connectedCallback() {
     // The document order, by default, controls tab order so keep that in mind if changing this.
     this.appendChild(this.labelElement);
     this.appendChild(this.dropdown);
     this.appendChild(this.editLink);
     this.appendChild(this.addLink);
@@ -53,18 +52,20 @@ export default class RichPicker extends 
     if (name == "label") {
       this.labelElement.textContent = newValue;
     }
   }
 
   render(state) {
     this.editLink.hidden = !this.dropdown.value;
 
+    let errorText = this.errorForSelectedOption(state);
     this.classList.toggle("invalid-selected-option",
-                          !this.isSelectedOptionValid(state));
+                          !!errorText);
+    this.invalidLabel.textContent = errorText;
   }
 
   get selectedOption() {
     return this.dropdown.selectedOption;
   }
 
   get selectedRichOption() {
     return this.dropdown.selectedRichOption;
@@ -73,18 +74,28 @@ export default class RichPicker extends 
   get requiredFields() {
     return this.selectedOption ? this.selectedOption.requiredFields || [] : [];
   }
 
   get fieldNames() {
     return [];
   }
 
-  isSelectedOptionValid() {
-    return !this.missingFieldsOfSelectedOption().length;
+  /**
+   * @param {object} state Application state
+   * @returns {string} Containing an error message for the picker or "" for no error.
+   */
+  errorForSelectedOption(state) {
+    if (!this.selectedOption) {
+      return "";
+    }
+    if (!this.dataset.invalidLabel) {
+      throw new Error("data-invalid-label is required");
+    }
+    return this.missingFieldsOfSelectedOption().length ? this.dataset.invalidLabel : "";
   }
 
   missingFieldsOfSelectedOption() {
     let selectedOption = this.selectedOption;
     if (!selectedOption) {
       return [];
     }
 
--- a/browser/components/payments/test/mochitest/test_address_picker.html
+++ b/browser/components/payments/test/mochitest/test_address_picker.html
@@ -18,16 +18,17 @@ Test the address-picker component
   <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
     <address-picker id="picker1"
                     data-field-separator=", "
+                    data-invalid-label="Picker1: Missing or Invalid"
                     selected-state-key="selectedShippingAddress"></address-picker>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
@@ -145,16 +146,17 @@ add_task(async function test_change_sele
   let selectedOption = picker1.dropdown.selectedOption;
   is(selectedOption, options[2], "Selected option should now be the third option");
   selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
   is(selectedShippingAddress, selectedOption.getAttribute("guid"),
      "store should have third option selected");
   // The third option is missing some fields. Make sure that it is marked as such.
   ok(picker1.classList.contains("invalid-selected-option"), "The third option is missing fields");
   ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible");
+  is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text");
 
   picker1.dropdown.popupBox.focus();
   synthesizeKey(options[1].getAttribute("name"), {});
   await asyncElementRendered();
 
   selectedOption = picker1.dropdown.selectedOption;
   is(selectedOption, options[1], "Selected option should now be the second option");
   selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
@@ -204,12 +206,72 @@ add_task(async function test_delete() {
       },
     },
   });
   await asyncElementRendered();
   let options = picker1.dropdown.popupBox.children;
   is(options.length, 1, "Check dropdown has one remaining address");
   ok(options[0].textContent.includes("Mrs. Bar"), "Check remaining address");
 });
+
+add_task(async function test_merchantError() {
+  picker1.requestStore.setState({
+    selectedShippingAddress: "68gjdh354j",
+  });
+  await asyncElementRendered();
+
+  is(picker1.selectedStateKey, "selectedShippingAddress", "Check selectedStateKey");
+
+  let state = picker1.requestStore.getState();
+  let {
+    request,
+  } = state;
+  ok(!picker1.classList.contains("invalid-selected-option"), "No validation on a valid option");
+  ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
+
+  let requestWithShippingAddressErrors = deepClone(request);
+  Object.assign(requestWithShippingAddressErrors.paymentDetails, {
+    shippingAddressErrors: {
+      country: "Your country is not supported",
+    },
+  });
+  picker1.requestStore.setState({
+    request: requestWithShippingAddressErrors,
+  });
+  await asyncElementRendered();
+
+  ok(picker1.classList.contains("invalid-selected-option"), "The merchant error applies");
+  ok(!isHidden(picker1.invalidLabel), "The merchant error should be visible");
+  is(picker1.invalidLabel.innerText, "Your country is not supported", "Check displayed error text");
+
+  info("update the request to remove the errors");
+  picker1.requestStore.setState({
+    request,
+  });
+  await asyncElementRendered();
+  ok(!picker1.classList.contains("invalid-selected-option"),
+     "No errors visible when merchant errors cleared");
+  ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
+
+  info("Set billing address and payer errors which aren't relevant to this picker");
+  let requestWithNonShippingAddressErrors = deepClone(request);
+  Object.assign(requestWithNonShippingAddressErrors.paymentDetails, {
+    payer: {
+      name: "Your name is too short",
+    },
+    paymentMethod: {
+      billingAddress: {
+        country: "Your billing country is not supported",
+      },
+    },
+    shippingAddressErrors: {},
+  });
+  picker1.requestStore.setState({
+    request: requestWithNonShippingAddressErrors,
+  });
+  await asyncElementRendered();
+  ok(!picker1.classList.contains("invalid-selected-option"), "No errors on a shipping picker");
+  ok(isHidden(picker1.invalidLabel), "The invalid label should still be hidden");
+});
 </script>
 
 </body>
 </html>
--- a/browser/components/payments/test/mochitest/test_payment_method_picker.html
+++ b/browser/components/payments/test/mochitest/test_payment_method_picker.html
@@ -16,16 +16,17 @@ Test the payment-method-picker component
   <link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
     <payment-method-picker id="picker1"
+                           data-invalid-label="picker1: Missing or invalid"
                            selected-state-key="selectedPaymentCard"></payment-method-picker>
   </p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
@@ -148,16 +149,17 @@ add_task(async function test_change_sele
   is(selectedOption, options[2], "Selected option should now be the third option");
   selectedPaymentCard = picker1.requestStore.getState().selectedPaymentCard;
   is(selectedPaymentCard, selectedOption.getAttribute("guid"),
      "store should have third option selected");
   selectedPaymentCardSecurityCode = picker1.requestStore.getState().selectedPaymentCardSecurityCode;
   is(selectedPaymentCardSecurityCode, null, "store should have empty security code");
   ok(picker1.classList.contains("invalid-selected-option"), "Missing fields for the third option");
   ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible");
+  is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text");
 
   await SimpleTest.promiseFocus();
   picker1.dropdown.popupBox.focus();
   synthesizeKey("visa", {});
   await asyncElementRendered();
   ok(true, "Focused the security code field");
   ok(!picker1.open, "Picker should be closed");
 
@@ -244,16 +246,17 @@ add_task(async function test_supportedNe
   });
   await asyncElementRendered();
   let options = picker1.dropdown.popupBox.children;
   is(options.length, 1, "Check dropdown has one card");
   ok(options[0].textContent.includes("J Smith"), "Check remaining card #1");
 
   ok(picker1.classList.contains("invalid-selected-option"),
      "Check discover is recognized as not supported");
+  is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text");
 
   info("change the card to be a visa");
   await picker1.requestStore.setState({
     tempBasicCards: {
       "68gjdh354j": {
         "cc-exp": "2017-08",
         "cc-exp-month": 8,
         "cc-exp-year": 2017,