Bug 1477102 - Toggle accepted cards list on add/edit cards. Add stub for using AppConstants.MOZILLA_OFFICIAL to fallback for non-branded builds. r=MattN
authorSam Foster <sfoster@mozilla.com>
Fri, 28 Sep 2018 16:53:37 +0000
changeset 438721 6ce23e4daf8d
parent 438720 634bcbff6b05
child 438722 04f74c53d537
push id70089
push usersfoster@mozilla.com
push dateFri, 28 Sep 2018 16:55:15 +0000
treeherderautoland@04f74c53d537 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1477102
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 1477102 - Toggle accepted cards list on add/edit cards. Add stub for using AppConstants.MOZILLA_OFFICIAL to fallback for non-branded builds. r=MattN Differential Revision: https://phabricator.services.mozilla.com/D7025
browser/components/payments/content/paymentDialogFrameScript.js
browser/components/payments/res/components/accepted-cards.css
browser/components/payments/res/components/accepted-cards.js
browser/components/payments/res/components/card-icon.svg
browser/components/payments/res/containers/basic-card-form.js
browser/components/payments/res/containers/payment-dialog.js
browser/components/payments/res/containers/payment-method-picker.js
browser/components/payments/res/debugging.html
browser/components/payments/res/debugging.js
browser/components/payments/res/paymentRequest.js
browser/components/payments/res/unprivileged-fallbacks.js
browser/components/payments/test/mochitest/payments_common.js
browser/components/payments/test/mochitest/test_accepted_cards.html
browser/components/payments/test/mochitest/test_basic_card_form.html
--- a/browser/components/payments/content/paymentDialogFrameScript.js
+++ b/browser/components/payments/content/paymentDialogFrameScript.js
@@ -98,16 +98,21 @@ let PaymentFrameScript = {
         let prefValues = Cu.cloneInto({
           saveCreditCardDefaultChecked:
             Services.prefs.getBoolPref(SAVE_CREDITCARD_DEFAULT_PREF, false),
           saveAddressDefaultChecked:
             Services.prefs.getBoolPref(SAVE_ADDRESS_DEFAULT_PREF, false),
         }, waivedContent);
         return Cu.cloneInto(prefValues, waivedContent);
       },
+
+      isOfficialBranding() {
+        // XXX: stub, will reflect AppConstants.MOZILLA_OFFICIAL when we have real logos
+        return false;
+      },
     };
     waivedContent.PaymentDialogUtils = Cu.cloneInto(PaymentDialogUtils, waivedContent, {
       cloneFunctions: true,
     });
   },
 
   sendToChrome({detail}) {
     let {messageType} = detail;
--- a/browser/components/payments/res/components/accepted-cards.css
+++ b/browser/components/payments/res/components/accepted-cards.css
@@ -1,44 +1,53 @@
 accepted-cards {
   margin: 1em 0;
+  display: flex;
+  flex-wrap: nowrap;
+  align-items: first baseline;
 }
 
 .accepted-cards-label {
   display: inline-block;
-  margin-inline-end: 1em;
-  color: GrayText;
   font-size: smaller;
+  flex: 0 2 content;
+  white-space: nowrap;
 }
 
 .accepted-cards-list {
   display: inline-block;
   list-style-type: none;
   margin: 0;
   padding: 0;
+  flex: 2 1 auto;
 }
 
 .accepted-cards-list > .accepted-cards-item {
   display: inline-block;
-  width: 50px;
-  height: 22px;
+  width: 32px;
+  height: 32px;
   padding: 0;
-  text-align: center;
-  margin-inline-end: 8px;
+  margin: 5px 0;
+  margin-inline-start: 10px;
+  vertical-align: middle;
   background-repeat: no-repeat;
   background-position: center;
-  vertical-align: middle;
+  background-size: contain;
 }
 
 /* placeholders for specific card icons we don't yet have assets for */
-.accepted-cards-item[data-network-id] {
+accepted-cards:not(.branded) .accepted-cards-item[data-network-id] {
+  width: 48px;
+  text-align: center;
   background-image: url("./card-icon.svg");
+  -moz-context-properties: fill-opacity;
+  fill-opacity: 0.5;
 }
-.accepted-cards-item[data-network-id]::after {
+accepted-cards:not(.branded) .accepted-cards-item[data-network-id]::after {
   box-sizing: border-box;
   content: attr(data-network-id);
-  padding: 4px;
+  padding: 8px 4px 0 4px;
   text-align: center;
   font-size: 0.7rem;
-  display: block;
+  display: inline-block;
   overflow: hidden;
   width: 100%;
 }
--- a/browser/components/payments/res/components/accepted-cards.js
+++ b/browser/components/payments/res/components/accepted-cards.js
@@ -1,14 +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 PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
-import paymentRequest from "../paymentRequest.js";
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <accepted-cards></accepted-cards>
  */
 
 export default class AcceptedCards extends PaymentStateSubscriberMixin(HTMLElement) {
   constructor() {
@@ -25,29 +24,42 @@ export default class AcceptedCards exten
     this.appendChild(this._labelEl);
 
     this._listEl.textContent = "";
     let allNetworks = PaymentDialogUtils.getCreditCardNetworks();
     for (let network of allNetworks) {
       let item = document.createElement("li");
       item.classList.add("accepted-cards-item");
       item.dataset.networkId = network;
+      item.setAttribute("aria-role", "image");
+      item.setAttribute("aria-label", network);
       this._listEl.appendChild(item);
     }
+    let isBranded = PaymentDialogUtils.isOfficialBranding();
+    this.classList.toggle("branded", isBranded);
     this.appendChild(this._listEl);
     // Only call the connected super callback(s) once our markup is fully
     // connected
     super.connectedCallback();
   }
 
   render(state) {
-    let acceptedNetworks = paymentRequest.getAcceptedNetworks(state.request);
-    for (let item of this._listEl.children) {
-      let network = item.dataset.networkId;
-      item.hidden = !(network && acceptedNetworks.includes(network));
+    let basicCardMethod = state.request.paymentMethods
+      .find(method => method.supportedMethods == "basic-card");
+    let merchantNetworks = basicCardMethod && basicCardMethod.data &&
+                           basicCardMethod.data.supportedNetworks;
+    if (merchantNetworks && merchantNetworks.length) {
+      for (let item of this._listEl.children) {
+        let network = item.dataset.networkId;
+        item.hidden = !(network && merchantNetworks.includes(network));
+      }
+      this.hidden = false;
+    } else {
+      // hide the whole list if the merchant didn't specify a preference
+      this.hidden = true;
     }
   }
 
   set label(value) {
     this._labelEl.textContent = value;
   }
 
   get acceptedItems() {
--- a/browser/components/payments/res/components/card-icon.svg
+++ b/browser/components/payments/res/components/card-icon.svg
@@ -1,6 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 47 22">
-  <rect x="0" y="0" width="47" height="22" rx="4" ry="4" fill="#000" fill-opacity="0.2">
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32">
+  <rect x="0" y="0" width="48" height="32" rx="4" ry="4" fill="#000" fill-opacity="context-fill-opacity">
   </rect>
-  <rect x="0" y="5" width="47" height="12" fill="#fff" fill-opacity="1">
+  <rect x="0" y="6" width="48" height="20" fill="#fff" fill-opacity="1">
   </rect>
 </svg>
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -149,17 +149,16 @@ export default class BasicCardForm exten
       this.saveButton.textContent = editing ? this.dataset.updateButtonLabel :
                                               this.dataset.addButtonLabel;
     }
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
     this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
     this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
     this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
     this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
-    this.acceptedCardsList.hidden = editing;
 
     // The next line needs an onboarding check since we don't set previousId
     // when navigating to add/edit directly from the summary page.
     this.backButton.hidden = !page.previousId && page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
     let record = {};
     let basicCards = paymentRequest.getBasicCards(state);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -369,23 +369,16 @@ export default class PaymentDialog exten
 
     let paymentOptions = request.paymentOptions;
     for (let element of this._shippingRelatedEls) {
       element.hidden = !paymentOptions.requestShipping;
     }
 
     this._renderPayerFields(state);
 
-    // hide the accepted cards list if the merchant didn't specify a preference
-    let basicCardMethod = request.paymentMethods
-      .find(method => method.supportedMethods == "basic-card");
-    let merchantNetworks = basicCardMethod && basicCardMethod.data &&
-                           basicCardMethod.data.supportedNetworks;
-    this._acceptedCardsList.hidden = !(merchantNetworks && merchantNetworks.length);
-
     let isMac = /mac/i.test(navigator.platform);
     for (let manageTextEl of this._manageText.children) {
       manageTextEl.hidden = manageTextEl.dataset.os == "mac" ? !isMac : isMac;
       let link = manageTextEl.querySelector("a");
       // The href is only set to be exposed to accessibility tools so users know what will open.
       // The actual opening happens from the click event listener.
       link.href = "about:preferences#privacy-address-autofill";
     }
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -1,15 +1,16 @@
 /* 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 BasicCardOption from "../components/basic-card-option.js";
 import RichPicker from "./rich-picker.js";
 import paymentRequest from "../paymentRequest.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
  * <payment-method-picker></payment-method-picker>
  * Container around add/edit links and <rich-select> with
  * <basic-card-option> listening to savedBasicCards.
  */
 
 export default class PaymentMethodPicker extends RichPicker {
@@ -88,17 +89,21 @@ export default class PaymentMethodPicker
     if (hasMissingFields) {
       return false;
     }
     let selectedOption = this.selectedOption;
     if (!selectedOption) {
       return true;
     }
 
-    let acceptedNetworks = paymentRequest.getAcceptedNetworks(state.request);
+    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;
   }
 
   get selectedStateKey() {
     return this.getAttribute("selected-state-key");
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -12,16 +12,17 @@
   <body>
     <div>
       <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>
         <button id="toggleDirectionality">Toggle :dir</button>
+        <button id="toggleBranding">Toggle branding</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>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -514,16 +514,22 @@ let buttonActions = {
       request: Object.assign({}, request, { completeStatus }),
     });
   },
 
   toggleDirectionality() {
     let body = paymentDialog.ownerDocument.body;
     body.dir = body.dir == "rtl" ? "ltr" : "rtl";
   },
+
+  toggleBranding() {
+    for (let container of paymentDialog.querySelectorAll("accepted-cards")) {
+      container.classList.toggle("branded");
+    }
+  },
 };
 
 window.addEventListener("click", function onButtonClick(evt) {
   let id = evt.target.id || evt.target.name;
   if (!id || typeof(buttonActions[id]) != "function") {
     return;
   }
 
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -276,25 +276,13 @@ var paymentRequest = {
     let addresses = Object.assign({}, state.savedAddresses, state.tempAddresses);
     return addresses;
   },
 
   getBasicCards(state) {
     let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
     return cards;
   },
-
-  getAcceptedNetworks(request) {
-    let basicCardMethod = request.paymentMethods
-      .find(method => method.supportedMethods == "basic-card");
-    let merchantNetworks = basicCardMethod && basicCardMethod.data &&
-                           basicCardMethod.data.supportedNetworks;
-    if (merchantNetworks && merchantNetworks.length) {
-      return merchantNetworks;
-    }
-    // fallback to the complete list if the merchant didn't specify
-    return PaymentDialogUtils.getCreditCardNetworks();
-  },
 };
 
 paymentRequest.init();
 
 export default paymentRequest;
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -98,9 +98,12 @@ var PaymentDialogUtils = {
   },
   getDefaultPreferences() {
     let prefValues = {
       saveCreditCardDefaultChecked: false,
       saveAddressDefaultChecked: true,
     };
     return prefValues;
   },
+  isOfficialBranding() {
+    return false;
+  },
 };
--- a/browser/components/payments/test/mochitest/payments_common.js
+++ b/browser/components/payments/test/mochitest/payments_common.js
@@ -106,19 +106,20 @@ SpecialPowers.registerConsoleListener(fu
   if (msg.isWarning || !msg.errorMessage || msg.errorMessage == "paymentRequest.xhtml:") {
     // Ignore warnings and non-errors.
     return;
   }
   if (msg.category == "CSP_CSPViolationWithURI" && msg.errorMessage.includes("at inline")) {
     // Ignore unknown CSP error.
     return;
   }
-  if (msg.message.includes("Security Error: Content at http://mochi.test:8888")) {
+  if (msg.message && msg.message.includes("Security Error: Content at http://mochi.test:8888")) {
     // Check for same-origin policy violations and ignore specific errors
     if (msg.message.includes("icon-credit-card-generic.svg") ||
+        msg.message.includes("accepted-cards.css") ||
         msg.message.includes("editDialog-shared.css") ||
         msg.message.includes("editAddress.css") ||
         msg.message.includes("editDialog.css") ||
         msg.message.includes("editCreditCard.css")) {
       return;
     }
   }
   if (msg.message == "SENTINEL") {
--- a/browser/components/payments/test/mochitest/test_accepted_cards.html
+++ b/browser/components/payments/test/mochitest/test_accepted_cards.html
@@ -25,16 +25,18 @@ Test the accepted-cards element
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
 /** Test the accepted-cards component **/
 
+/* global sinon, PaymentDialogUtils */
+
 import "../../res/components/accepted-cards.js";
 import {requestStore} from "../../res/mixins/PaymentStateSubscriberMixin.js";
 let emptyState = requestStore.getState();
 let acceptedElem = document.querySelector("accepted-cards");
 let allNetworks = PaymentDialogUtils.getCreditCardNetworks();
 
 add_task(async function test_reConnected() {
   let itemsCount = acceptedElem.querySelectorAll(".accepted-cards-item").length;
@@ -71,12 +73,41 @@ add_task(async function test_someAccepte
          `Item for the ${network} network expected to be visible`);
     } else {
       ok(acceptedElem.querySelector(`[data-network-id='${network}'][hidden]`),
          `Item for the ${network} network expected to be hidden`);
     }
   }
 });
 
+add_task(async function test_officialBranding() {
+  // verify we get the expected result when isOfficialBranding returns true
+  sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return true; });
+
+  let container = acceptedElem.parentNode;
+  let removed = container.removeChild(acceptedElem);
+  container.appendChild(removed);
+
+  ok(PaymentDialogUtils.isOfficialBranding.calledOnce,
+     "isOfficialBranding was called");
+  ok(acceptedElem.classList.contains("branded"),
+     "The branded class is added when isOfficialBranding returns true");
+  PaymentDialogUtils.isOfficialBranding.restore();
+
+  // verify we get the expected result when isOfficialBranding returns false
+  sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return false; });
+
+  // the branded class is toggled in the 'connectedCallback',
+  // so remove and re-add the element to re-evaluate branded-ness
+  removed = container.removeChild(acceptedElem);
+  container.appendChild(removed);
+
+  ok(PaymentDialogUtils.isOfficialBranding.calledOnce,
+     "isOfficialBranding was called");
+  ok(!acceptedElem.classList.contains("branded"),
+     "The branded class is removed when isOfficialBranding returns false");
+  PaymentDialogUtils.isOfficialBranding.restore();
+});
+
 </script>
 
 </body>
 </html>
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -32,16 +32,23 @@ Test the basic-card-form element
 <script type="module">
 /** Test the basic-card-form element **/
 
 /* global sinon */
 
 import BasicCardForm from "../../res/containers/basic-card-form.js";
 
 let display = document.getElementById("display");
+let supportedNetworks = ["discover", "amex"];
+let paymentMethods = [{
+  supportedMethods: "basic-card",
+  data: {
+    supportedNetworks,
+  },
+}];
 
 function checkCCForm(customEl, expectedCard) {
   const CC_PROPERTY_NAMES = [
     "billingAddressGUID",
     "cc-number",
     "cc-name",
     "cc-exp-month",
     "cc-exp-year",
@@ -112,25 +119,30 @@ add_task(async function test_saveButton(
   await form.promiseReady;
   display.appendChild(form);
 
   let address1 = deepClone(PTU.Addresses.TimBL);
   address1.guid = "TimBLGUID";
   let address2 = deepClone(PTU.Addresses.TimBL2);
   address2.guid = "TimBL2GUID";
 
+
   await form.requestStore.setState({
+    request: {
+      paymentMethods,
+    },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
       [address2.guid]: deepClone(address2),
     },
   });
 
   await asyncElementRendered();
 
+  // when merchant provides supportedNetworks, the accepted card list should be visible
   ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when adding a card");
 
   ok(form.saveButton.disabled, "Save button should initially be disabled");
   fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
   form.form.querySelector("#cc-name").focus();
   // Check .disabled after .focus() so that it's after both "input" and "change" events.
   ok(form.saveButton.disabled, "Save button should still be disabled without a name");
   sendString("J. Smith");
@@ -344,16 +356,19 @@ add_task(async function test_edit() {
 
   info("test year before current");
   let card1 = deepClone(PTU.BasicCards.JohnDoe);
   card1.guid = "9864798564";
   card1["cc-exp-year"] = 2011;
   card1.billingAddressGUID = address1.guid;
 
   await form.requestStore.setState({
+    request: {
+      paymentMethods,
+    },
     page: {
       id: "basic-card-page",
     },
     "basic-card-page": {
       guid: card1.guid,
     },
     savedAddresses: {
       [address1.guid]: deepClone(address1),
@@ -363,17 +378,17 @@ add_task(async function test_edit() {
     },
   });
   await asyncElementRendered();
   is(form.saveButton.textContent, "Update", "Check label");
   is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
      "Check no fields are visibly invalid on an 'edit' form with a complete card");
   checkCCForm(form, card1);
   ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card");
-  ok(form.acceptedCardsList.hidden, "Accepted card list should be hidden when editing a card");
+  ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when editing a card");
 
   let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
   ok(requiredElements.length, "There should be at least one required element");
   is(requiredElements.length, 5, "Number of required elements");
   for (let element of requiredElements) {
     if (element.id == "billingAddressGUID") {
       // The billing address has a different layout.
       continue;