Bug 1440499 - Implement the payerName/payerEmail/payerPhone contact picker. r?MattN draft
authorSam Foster <sfoster@mozilla.com>
Tue, 06 Mar 2018 14:00:05 -0800
changeset 765134 40caa98dee7e4f4a84d3bd56f04085a3a384326d
parent 765133 41e80b5da8290578684ccf8728a7bebc6b455c4f
push id101977
push userbmo:sfoster@mozilla.com
push dateFri, 09 Mar 2018 02:59:21 +0000
reviewersMattN
bugs1440499
milestone60.0a1
Bug 1440499 - Implement the payerName/payerEmail/payerPhone contact picker. r?MattN * Based on original patch by MattN * Make stored contacts available as payer data (MattN) * Add the address-picker element to the dialog for selecting payer details from stored contacts (MattN) * Add a field-names attribute to the payer address-picker, populated from the request paymentOptions * Basic CSS to selectively render address fields * Add mochitests to verify paymentOptions result in the correct payment picker behavior MozReview-Commit-ID: Br8i5MVyeQ3
toolkit/components/payments/content/paymentDialogWrapper.js
toolkit/components/payments/res/components/address-option.css
toolkit/components/payments/res/containers/address-picker.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/debugging.js
toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/payments_common.js
toolkit/components/payments/test/mochitest/test_order_details.html
toolkit/components/payments/test/mochitest/test_payer_address_picker.html
--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -42,16 +42,43 @@ var paymentDialogWrapper = {
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
   ]),
 
   /**
    * Note: This method is async because formAutofillStorage plans to become async.
    *
    * @param {string} guid
+   * @returns {object} containing only the requested payer values.
+   */
+  async _convertProfileAddressToPayerData(guid) {
+    let addressData = formAutofillStorage.addresses.get(guid);
+    if (!addressData) {
+      throw new Error(`Payer address not found: ${guid}`);
+    }
+
+    let {
+      requestPayerName,
+      requestPayerEmail,
+      requestPayerPhone,
+    } = this.request.paymentOptions;
+
+    let payerData = {
+      payerName: requestPayerName ? addressData.name : "",
+      payerEmail: requestPayerEmail ? addressData.email : "",
+      payerPhone: requestPayerPhone ? addressData.tel : "",
+    };
+
+    return payerData;
+  },
+
+  /**
+   * Note: This method is async because formAutofillStorage plans to become async.
+   *
+   * @param {string} guid
    * @returns {nsIPaymentAddress}
    */
   async _convertProfileAddressToPaymentAddress(guid) {
     let addressData = formAutofillStorage.addresses.get(guid);
     if (!addressData) {
       throw new Error(`Shipping address not found: ${guid}`);
     }
 
@@ -370,32 +397,42 @@ var paymentDialogWrapper = {
     const showResponse = this.createShowResponse({
       acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
     });
     paymentSrv.respondPayment(showResponse);
     window.close();
   },
 
   async onPay({
+    selectedPayerAddressGUID: payerGUID,
     selectedPaymentCardGUID: paymentCardGUID,
     selectedPaymentCardSecurityCode: cardSecurityCode,
   }) {
     let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
                                                                             cardSecurityCode);
 
     if (!methodData) {
       // TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
       // Master Password dialog.
       Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
       return;
     }
 
+    let {
+      payerName,
+      payerEmail,
+      payerPhone,
+    } = await this._convertProfileAddressToPayerData(payerGUID);
+
     this.pay({
       methodName: "basic-card",
       methodData,
+      payerName,
+      payerEmail,
+      payerPhone,
     });
   },
 
   pay({
     payerName,
     payerEmail,
     payerPhone,
     methodName,
--- a/toolkit/components/payments/res/components/address-option.css
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -12,16 +12,22 @@ address-option {
 
 rich-select[open] > .rich-select-popup-box > address-option {
   grid-template-areas:
     "name           name          "
     "street-address street-address"
     "email          tel           ";
 }
 
+address-picker.payer-related > rich-select address-option {
+  grid-template-areas:
+    "name name"
+    "tel email";
+}
+
 address-option > .name {
   grid-area: name;
 }
 
 address-option > .street-address {
   grid-area: street-address;
 }
 
@@ -35,12 +41,28 @@ address-option > .tel {
 
 address-option > .name,
 address-option > .street-address,
 address-option > .email,
 address-option > .tel {
   white-space: nowrap;
 }
 
-.rich-select-selected-clone > .email,
-.rich-select-selected-clone > .tel {
+address-picker.shipping-related address-option > .email,
+address-picker.shipping-related address-option.rich-select-selected-clone > .tel {
   display: none;
 }
+
+/* for payer contact details:
+ * display fields selectively based on the contents of the address-fields attribute
+ */
+address-picker.payer-related address-option > .name,
+address-picker.payer-related address-option > .street-address,
+address-picker.payer-related address-option > .email,
+address-picker.payer-related address-option > .tel {
+  display: none;
+}
+
+address-picker[address-fields~='name'].payer-related address-option  > .name,
+address-picker[address-fields~='email'].payer-related address-option  > .email,
+address-picker[address-fields~='tel'].payer-related address-option  > .tel {
+  display: inline-block;
+}
--- a/toolkit/components/payments/res/containers/address-picker.js
+++ b/toolkit/components/payments/res/containers/address-picker.js
@@ -28,21 +28,23 @@ class AddressPicker extends PaymentState
     let {savedAddresses} = state;
     let desiredOptions = [];
     for (let [guid, address] of Object.entries(savedAddresses)) {
       let optionEl = this.dropdown.getOptionByValue(guid);
       if (!optionEl) {
         optionEl = document.createElement("address-option");
         optionEl.value = guid;
       }
+
       for (let [key, val] of Object.entries(address)) {
         optionEl.setAttribute(key, val);
       }
       desiredOptions.push(optionEl);
     }
+
     let el = null;
     while ((el = this.dropdown.popupBox.querySelector(":scope > address-option"))) {
       el.remove();
     }
     for (let option of desiredOptions) {
       this.dropdown.popupBox.appendChild(option);
     }
 
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -28,16 +28,19 @@ class PaymentDialog extends PaymentState
     this._payButton.addEventListener("click", this);
 
     this._viewAllButton = contents.querySelector("#view-all");
     this._viewAllButton.addEventListener("click", this);
 
     this._orderDetailsOverlay = contents.querySelector("#order-details-overlay");
     this._shippingTypeLabel = contents.querySelector("#shipping-type-label");
     this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
+    this._payerRelatedEls = contents.querySelectorAll(".payer-related");
+    this._payerAddressPicker = contents.querySelector("address-picker.payer-related");
+
     this._errorText = contents.querySelector("#error-text");
 
     this._disabledOverlay = contents.getElementById("disabled-overlay");
 
     this.appendChild(contents);
 
     super.connectedCallback();
   }
@@ -64,21 +67,23 @@ class PaymentDialog extends PaymentState
   }
 
   cancelRequest() {
     paymentRequest.cancel();
   }
 
   pay() {
     let {
+      selectedPayerAddress,
       selectedPaymentCard,
       selectedPaymentCardSecurityCode,
     } = this.requestStore.getState();
 
     paymentRequest.pay({
+      selectedPayerAddressGUID: selectedPayerAddress,
       selectedPaymentCardGUID: selectedPaymentCard,
       selectedPaymentCardSecurityCode,
     });
   }
 
   changeShippingAddress(shippingAddressGUID) {
     paymentRequest.changeShippingAddress({
       shippingAddressGUID,
@@ -101,16 +106,17 @@ class PaymentDialog extends PaymentState
   setStateFromParent(state) {
     this.requestStore.setState(state);
 
     // Check if any foreign-key constraints were invalidated.
     state = this.requestStore.getState();
     let {
       savedAddresses,
       savedBasicCards,
+      selectedPayerAddress,
       selectedPaymentCard,
       selectedShippingAddress,
       selectedShippingOption,
     } = state;
     let shippingOptions = state.request.paymentDetails.shippingOptions;
 
     // Ensure `selectedShippingAddress` never refers to a deleted address and refers
     // to an address if one exists.
@@ -143,16 +149,25 @@ class PaymentDialog extends PaymentState
       if (!selectedShippingOption && shippingOptions.length) {
         selectedShippingOption = shippingOptions[0].id;
       }
       this._cachedState.selectedShippingOption = selectedShippingOption;
       this.requestStore.setState({
         selectedShippingOption,
       });
     }
+
+
+    // Ensure `selectedPayerAddress` never refers to a deleted address and refers
+    // to an address if one exists.
+    if (!savedAddresses[selectedPayerAddress]) {
+      this.requestStore.setState({
+        selectedPayerAddress: Object.keys(savedAddresses)[0] || null,
+      });
+    }
   }
 
   _renderPayButton(state) {
     this._payButton.disabled = state.changesPrevented;
     switch (state.completionState) {
       case "initial":
       case "processing":
       case "success":
@@ -191,20 +206,44 @@ class PaymentDialog extends PaymentState
 
     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;
+
     let paymentOptions = request.paymentOptions;
     for (let element of this._shippingRelatedEls) {
       element.hidden = !paymentOptions.requestShipping;
     }
+    let payerRequested = paymentOptions.requestPayerName ||
+                         paymentOptions.requestPayerEmail ||
+                         paymentOptions.requestPayerPhone;
+    for (let element of this._payerRelatedEls) {
+      element.hidden = !payerRequested;
+    }
+
+    if (payerRequested) {
+      let fieldNames = new Set(); // default: ["name", "tel", "email"]
+      if (paymentOptions.requestPayerName) {
+        fieldNames.add("name");
+      }
+      if (paymentOptions.requestPayerEmail) {
+        fieldNames.add("email");
+      }
+      if (paymentOptions.requestPayerPhone) {
+        fieldNames.add("tel");
+      }
+      this._payerAddressPicker.setAttribute("address-fields", [...fieldNames].join(" "));
+    } else {
+      this._payerAddressPicker.removeAttribute("address-fields");
+    }
+
     let shippingType = paymentOptions.shippingType || "shipping";
     this._shippingTypeLabel.querySelector("label").textContent =
       this._shippingTypeLabel.dataset[shippingType + "AddressLabel"];
 
     this._renderPayButton(state);
 
     let {
       changesPrevented,
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -138,16 +138,17 @@ let REQUEST_2 = {
   },
 };
 
 let ADDRESSES_1 = {
   "48bnds6854t": {
     "address-level1": "MI",
     "address-level2": "Some City",
     "country": "US",
+    "email": "foo@bar.com",
     "guid": "48bnds6854t",
     "name": "Mr. Foo",
     "postal-code": "90210",
     "street-address": "123 Sesame Street,\nApt 40",
     "tel": "+1 519 555-5555",
   },
   "68gjdh354j": {
     "address-level1": "CA",
--- a/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -33,16 +33,17 @@ let requestStore = new PaymentsStore({
     paymentOptions: {
       requestPayerName: false,
       requestPayerEmail: false,
       requestPayerPhone: false,
       requestShipping: false,
       shippingType: "shipping",
     },
   },
+  selectedPayerAddress: null,
   selectedPaymentCard: null,
   selectedPaymentCardSecurityCode: null,
   selectedShippingAddress: null,
   selectedShippingOption: null,
   savedAddresses: {},
   savedBasicCards: {},
 });
 
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -5,16 +5,17 @@
 <!DOCTYPE html [
   <!ENTITY viewAllItems               "View All Items">
   <!ENTITY paymentSummaryTitle        "Your Payment">
   <!ENTITY shippingAddressLabel       "Shipping Address">
   <!ENTITY deliveryAddressLabel       "Delivery Address">
   <!ENTITY pickupAddressLabel         "Pickup Address">
   <!ENTITY shippingOptionsLabel       "Shipping Options">
   <!ENTITY paymentMethodsLabel        "Payment Method">
+  <!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 orderDetailsLabel          "Order Details">
   <!ENTITY orderTotalLabel            "Total">
 ]>
@@ -78,16 +79,21 @@
                data-pickup-address-label="&pickupAddressLabel;"><label></label></div>
           <address-picker class="shipping-related" selected-state-key="selectedShippingAddress"></address-picker>
 
           <div class="shipping-related"><label>&shippingOptionsLabel;</label></div>
           <shipping-option-picker class="shipping-related"></shipping-option-picker>
 
           <div><label>&paymentMethodsLabel;</label></div>
           <payment-method-picker selected-state-key="selectedPaymentCard"></payment-method-picker>
+
+          <div class="payer-related"><label>&payerLabel;</label></div>
+          <address-picker class="payer-related"
+                          selected-state-key="selectedPayerAddress"></address-picker>
+          <div id="error-text"></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;"
                   data-fail-label="&failPaymentButton.label;"
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -26,15 +26,16 @@ support-files =
    ../../res/mixins/PaymentStateSubscriberMixin.js
    ../../res/vendor/custom-elements.min.js
    ../../res/vendor/custom-elements.min.js.map
    payments_common.js
 
 [test_address_picker.html]
 [test_currency_amount.html]
 [test_order_details.html]
+[test_payer_address_picker.html]
 [test_payment_dialog.html]
 [test_payment_details_item.html]
 [test_payment_method_picker.html]
 [test_rich_select.html]
 [test_shipping_option_picker.html]
 [test_ObservedPropertiesMixin.html]
 [test_PaymentStateSubscriberMixin.html]
--- a/toolkit/components/payments/test/mochitest/payments_common.js
+++ b/toolkit/components/payments/test/mochitest/payments_common.js
@@ -1,11 +1,11 @@
 "use strict";
 
-/* exported asyncElementRendered, promiseStateChange */
+/* exported asyncElementRendered, promiseStateChange, deepClone */
 
 /**
  * A helper to await on while waiting for an asynchronous rendering of a Custom
  * Element.
  * @returns {Promise}
  */
 function asyncElementRendered() {
   return Promise.resolve();
@@ -16,8 +16,12 @@ function promiseStateChange(store) {
     store.subscribe({
       stateChangeCallback(state) {
         store.unsubscribe(this);
         resolve(state);
       },
     });
   });
 }
+
+function deepClone(obj) {
+  return JSON.parse(JSON.stringify(obj));
+}
--- a/toolkit/components/payments/test/mochitest/test_order_details.html
+++ b/toolkit/components/payments/test/mochitest/test_order_details.html
@@ -44,20 +44,16 @@
 let orderDetails = document.querySelector("order-details");
 let emptyState = requestStore.getState();
 
 function setup() {
   let initialState = deepClone(emptyState);
   requestStore.setState(initialState);
 }
 
-function deepClone(obj) {
-  return JSON.parse(JSON.stringify(obj));
-}
-
 add_task(async function isFooterItem() {
   ok(OrderDetails.isFooterItem({
     label: "Levy",
     type: "tax",
     amount: { currency: "USD", value: "1" },
   }, "items with type of 'tax' are footer items"));
   ok(!OrderDetails.isFooterItem({
     label: "Levis",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_payer_address_picker.html
@@ -0,0 +1,207 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the paymentOptions address-picker
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the paymentOptions address-picker</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="payments_common.js"></script>
+
+  <script src="custom-elements.min.js"></script>
+  <script src="PaymentsStore.js"></script>
+  <script src="ObservedPropertiesMixin.js"></script>
+  <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="payment-dialog.js"></script>
+
+  <script src="rich-select.js"></script>
+  <script src="address-picker.js"></script>
+  <script src="rich-option.js"></script>
+  <script src="address-option.js"></script>
+  <script src="currency-amount.js"></script>
+  <link rel="stylesheet" type="text/css" href="rich-select.css"/>
+  <link rel="stylesheet" type="text/css" href="address-option.css"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <link rel="stylesheet" type="text/css" href="paymentRequest.css"/>
+</head>
+<body>
+  <p id="display">
+    <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"></iframe>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the payer requested details functionality **/
+
+/* import-globals-from payments_common.js */
+
+function getVisiblePickerOptions(picker) {
+  let select = picker.querySelector(":scope > rich-select");
+  let options = select.querySelectorAll("address-option");
+  let visibleOptions = Array.from(options).filter(isVisible);
+  return visibleOptions;
+}
+
+function isVisible(elem) {
+  let result = elem.getBoundingClientRect().height > 0;
+  return result;
+}
+
+function setPaymentOptions(requestStore, options) {
+  let {request} = requestStore.getState();
+  request = Object.assign({}, request, {
+    paymentOptions: options,
+  });
+  return requestStore.setState({ request });
+}
+
+const SAVED_ADDRESSES = {
+  "48bnds6854t": {
+    "address-level1": "MI",
+    "address-level2": "Some City",
+    "country": "US",
+    "guid": "48bnds6854t",
+    "name": "Mr. Foo",
+    "postal-code": "90210",
+    "street-address": "123 Sesame Street,\nApt 40",
+    "tel": "+1 519 555-5555",
+    "email": "foo@example.com",
+  },
+  "68gjdh354j": {
+    "address-level1": "CA",
+    "address-level2": "Mountain View",
+    "country": "US",
+    "guid": "68gjdh354j",
+    "name": "Mrs. Bar",
+    "postal-code": "94041",
+    "street-address": "P.O. Box 123",
+    "tel": "+1 650 555-5555",
+    "email": "bar@example.com",
+  },
+};
+
+let elPicker;
+let elDialog;
+let initialState;
+
+add_task(async function setup_once() {
+  let templateFrame = document.getElementById("templateFrame");
+  await SimpleTest.promiseFocus(templateFrame.contentWindow);
+
+  let displayEl = document.getElementById("display");
+  // Import the templates from the real shipping dialog to avoid duplication.
+  for (let template of templateFrame.contentDocument.querySelectorAll("template")) {
+    let imported = document.importNode(template, true);
+    displayEl.appendChild(imported);
+  }
+
+  elDialog = document.createElement("payment-dialog");
+  displayEl.appendChild(elDialog);
+  elPicker = elDialog.querySelector("address-picker.payer-related");
+
+  initialState = Object.assign({}, elDialog.requestStore.getState(), {
+    changesPrevented: false,
+    completionState: "initial",
+    orderDetailsShowing: false,
+  });
+});
+
+async function setup() {
+  // reset the store back to a known, default state
+  elDialog.requestStore.setState(deepClone(initialState));
+  await asyncElementRendered();
+}
+
+add_task(async function test_empty() {
+  await setup();
+
+  let {request, savedAddresses} = elPicker.requestStore.getState();
+  ok(!savedAddresses || !savedAddresses.length,
+     "Check initial state has no saved addresses");
+
+  let {paymentOptions} = request;
+  let payerRequested = paymentOptions.requestPayerName ||
+                   paymentOptions.requestPayerEmail ||
+                   paymentOptions.requestPayerPhone;
+  ok(!payerRequested, "Check initial state has no payer details requested");
+  ok(elPicker, "Check elPicker exists");
+  is(elPicker.dropdown.popupBox.children.length, 0, "Check dropdown is empty");
+  is(isVisible(elPicker), false, "The address-picker is not visible");
+});
+
+// paymentOptions properties are acurately reflected in the address-fields attribute
+add_task(async function test_visible_fields() {
+  await setup();
+  let requestStore = elPicker.requestStore;
+  setPaymentOptions(requestStore, {
+    requestPayerName: true,
+    requestPayerEmail: true,
+    requestPayerPhone: true,
+  });
+
+  requestStore.setState({
+    savedAddresses: SAVED_ADDRESSES,
+    selectedPayerAddress: "48bnds6854t",
+  });
+
+  await asyncElementRendered();
+
+  let visibleOptions = getVisiblePickerOptions(elPicker);
+  let visibleOption = visibleOptions[0];
+
+  is(elPicker.dropdown.popupBox.children.length, 2, "Check dropdown has 2 addresses");
+  is(visibleOptions.length, 1, "One option should be visible");
+  is(visibleOption.getAttribute("guid"), "48bnds6854t", "expected option is visible");
+
+  for (let fieldName of ["name", "email", "tel"]) {
+    let elem = visibleOption.querySelector(`.${fieldName}`);
+    ok(elem, `field ${fieldName} exists`);
+    ok(isVisible(elem), `field ${fieldName} is visible`);
+  }
+  ok(!isVisible(visibleOption.querySelector(".street-address")), "street-address is not visible");
+});
+
+add_task(async function test_selective_fields() {
+  await setup();
+  let requestStore = elPicker.requestStore;
+
+  requestStore.setState({
+    savedAddresses: SAVED_ADDRESSES,
+    selectedPayerAddress: "48bnds6854t",
+  });
+
+  let payerFieldVariations = [
+    {requestPayerName: true, requestPayerEmail: false, requestPayerPhone: false },
+    {requestPayerName: false, requestPayerEmail: true, requestPayerPhone: false },
+    {requestPayerName: false, requestPayerEmail: false, requestPayerPhone: true },
+    {requestPayerName: true, requestPayerEmail: true, requestPayerPhone: false },
+    {requestPayerName: false, requestPayerEmail: true, requestPayerPhone: true },
+    {requestPayerName: true, requestPayerEmail: false, requestPayerPhone: true },
+  ];
+
+  for (let payerFields of payerFieldVariations) {
+    setPaymentOptions(requestStore, payerFields);
+    await asyncElementRendered();
+
+    let visibleOption = getVisiblePickerOptions(elPicker)[0];
+    let elName = visibleOption.querySelector(".name");
+    let elEmail = visibleOption.querySelector(".email");
+    let elPhone = visibleOption.querySelector(".tel");
+
+    is(isVisible(elName), payerFields.requestPayerName,
+       "name field is correctly toggled");
+    is(isVisible(elEmail), payerFields.requestPayerEmail,
+       "email field is correctly toggled");
+    is(isVisible(elPhone), payerFields.requestPayerPhone,
+       "tel field is correctly toggled");
+  }
+});
+</script>
+
+</body>
+</html>