☠☠ backed out by 5fb1b1d9d669 ☠ ☠ | |
author | Sam Foster <sfoster@mozilla.com> |
Tue, 06 Mar 2018 14:00:05 -0800 | |
changeset 407276 | 4e2e081dad557a8c060a3751bb1986c9686d5fe2 |
parent 407275 | 479b143d88284a89d9d9bde4e69798de2a398fbd |
child 407277 | e1641b8d914d5cadcf9716b8dcaa6a08745fed6c |
push id | 60872 |
push user | sfoster@mozilla.com |
push date | Fri, 09 Mar 2018 00:26:12 +0000 |
treeherder | autoland@4e2e081dad55 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | MattN |
bugs | 1440499 |
milestone | 60.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
|
--- 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 = profileStorage.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 profileStorage 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>