Bug 1427939 - Use total and any additionalDisplayItems when a modifier matches the payment method. r=jaws
* getModifierForPaymentMethod helper
* Use selectedPaymentMethod from state store.
* Update and add tests for the modifier case for the main total in PaymentDialog
* Update and add tests for handling modifiers and additionalDisplayItems in the OrderDetails component
MozReview-Commit-ID: FmlovZjP0t1
--- a/toolkit/components/payments/res/containers/order-details.js
+++ b/toolkit/components/payments/res/containers/order-details.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/. */
// <currency-amount> is used in the <template>
import "../components/currency-amount.js";
import PaymentDetailsItem from "../components/payment-details-item.js";
+import paymentRequest from "../paymentRequest.js";
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
/**
* <order-details></order-details>
*/
export default class OrderDetails extends PaymentStateSubscriberMixin(HTMLElement) {
connectedCallback() {
@@ -52,29 +53,37 @@ export default class OrderDetails extend
row.amountValue = item.amount.value;
row.amountCurrency = item.amount.currency;
fragment.appendChild(row);
}
listEl.appendChild(fragment);
return listEl;
}
+ _getAdditionalDisplayItems(state) {
+ let methodId = state.selectedPaymentCard;
+ let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
+ if (modifier && modifier.additionalDisplayItems) {
+ return modifier.additionalDisplayItems;
+ }
+ return [];
+ }
+
render(state) {
- let request = state.request;
- let totalItem = request.paymentDetails.totalItem;
+ let totalItem = paymentRequest.getTotalItem(state);
OrderDetails._emptyList(this.mainItemsList);
OrderDetails._emptyList(this.footerItemsList);
- let mainItems = OrderDetails._getMainListItems(request);
+ let mainItems = OrderDetails._getMainListItems(state);
if (mainItems.length) {
OrderDetails._populateList(this.mainItemsList, mainItems);
}
- let footerItems = OrderDetails._getFooterListItems(request);
+ let footerItems = OrderDetails._getFooterListItems(state);
if (footerItems.length) {
OrderDetails._populateList(this.footerItemsList, footerItems);
}
this.totalAmountElem.value = totalItem.amount.value;
this.totalAmountElem.currency = totalItem.amount.currency;
}
@@ -85,28 +94,38 @@ export default class OrderDetails extend
*
* @param {object} item - Data representing a PaymentItem
* @returns {boolean}
*/
static isFooterItem(item) {
return item.type == "tax";
}
- static _getMainListItems(request) {
+ static _getMainListItems(state) {
+ let request = state.request;
let items = request.paymentDetails.displayItems;
if (Array.isArray(items) && items.length) {
let predicate = item => !OrderDetails.isFooterItem(item);
return request.paymentDetails.displayItems.filter(predicate);
}
return [];
}
- static _getFooterListItems(request) {
+ static _getFooterListItems(state) {
+ let request = state.request;
let items = request.paymentDetails.displayItems;
+ let footerItems = [];
+ let methodId = state.selectedPaymentCard;
+ if (methodId) {
+ let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
+ if (modifier && Array.isArray(modifier.additionalDisplayItems)) {
+ footerItems.push(...modifier.additionalDisplayItems);
+ }
+ }
if (Array.isArray(items) && items.length) {
let predicate = OrderDetails.isFooterItem;
- return request.paymentDetails.displayItems.filter(predicate);
+ footerItems.push(...request.paymentDetails.displayItems.filter(predicate));
}
- return [];
+ return footerItems;
}
}
customElements.define("order-details", OrderDetails);
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -228,17 +228,17 @@ export default class PaymentDialog exten
this._cachedState.selectedShippingOption = state.selectedShippingOption;
}
render(state) {
let request = state.request;
let paymentDetails = request.paymentDetails;
this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
- let totalItem = paymentDetails.totalItem;
+ let totalItem = paymentRequest.getTotalItem(state);
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;
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -122,17 +122,41 @@ let REQUEST_2 = {
label: "Slow",
amount: {
currency: "USD",
value: 10,
},
selected: false,
},
],
- modifiers: null,
+ modifiers: [
+ {
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total",
+ amount: {
+ currency: "CAD",
+ value: "28.75",
+ },
+ pending: false,
+ },
+ additionalDisplayItems: [
+ {
+ label: "Credit card fee",
+ amount: {
+ currency: "CAD",
+ value: "1.50",
+ },
+ },
+ ],
+ data: {
+ supportedTypes: "credit",
+ },
+ },
+ ],
error: "",
},
paymentOptions: {
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: true,
shippingType: "shipping",
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -156,16 +156,54 @@ var paymentRequest = {
record,
errorStateChange,
preserveOldProperties,
selectedStateKey,
successStateChange,
});
},
+ /**
+ * @param {object} state object representing the UI state
+ * @param {string} methodID (GUID) uniquely identifying the selected payment method
+ * @returns {object?} the applicable modifier for the payment method
+ */
+ getModifierForPaymentMethod(state, methodID) {
+ let method = state.savedBasicCards[methodID] || null;
+ if (method && method.methodName !== "basic-card") {
+ throw new Error(`${method.methodName} (${methodID}) is not a supported payment method`);
+ }
+ let modifiers = state.request.paymentDetails.modifiers;
+ if (!modifiers || !modifiers.length) {
+ return null;
+ }
+ let modifier = modifiers.find(m => {
+ // take the first matching modifier
+ // TODO (bug 1429198): match on supportedTypes and supportedNetworks
+ return m.supportedMethods == "basic-card";
+ });
+ return modifier || null;
+ },
+
+ /**
+ * @param {object} state object representing the UI state
+ * @returns {object} in the shape of `nsIPaymentItem` representing the total
+ * that applies to the selected payment method.
+ */
+ getTotalItem(state) {
+ let methodID = state.selectedPaymentCard;
+ if (methodID) {
+ let modifier = paymentRequest.getModifierForPaymentMethod(state, methodID);
+ if (modifier && modifier.hasOwnProperty("total")) {
+ return modifier.total;
+ }
+ }
+ return state.request.paymentDetails.totalItem;
+ },
+
onPaymentRequestUnload() {
// remove listeners that may be used multiple times here
window.removeEventListener("paymentChromeToContent", this);
},
};
paymentRequest.init();
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -289,17 +289,21 @@ var PaymentTestUtils = {
bobPayPaymentModifier: {
total: {
label: "Total due",
amount: { currency: "USD", value: "2.00" },
},
displayItems: [
{
label: "First",
- amount: { currency: "USD", value: "1" },
+ amount: { currency: "USD", value: "1.75" },
+ },
+ {
+ label: "Second",
+ amount: { currency: "USD", value: "0.25" },
},
],
modifiers: [
{
additionalDisplayItems: [
{
label: "Credit card fee",
amount: { currency: "USD", value: "0.50" },
--- a/toolkit/components/payments/test/browser/browser_total.js
+++ b/toolkit/components/payments/test/browser/browser_total.js
@@ -7,8 +7,40 @@ add_task(async function test_total() {
"Check total currency amount");
};
const args = {
methodData: [PTU.MethodData.basicCard],
details: PTU.Details.total60USD,
};
await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
});
+
+add_task(async function test_modifier_with_no_method_selected() {
+ const testTask = async ({methodData, details}) => {
+ // There are no payment methods installed/setup so we expect the original (unmodified) total.
+ is(content.document.querySelector("#total > currency-amount").textContent,
+ "$2.00",
+ "Check unmodified total currency amount");
+ };
+ const args = {
+ methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
+ details: PTU.Details.bobPayPaymentModifier,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_modifier_with_no_method_selected() {
+ info("adding a basic-card");
+ await addSampleAddressesAndBasicCard();
+
+ const testTask = async ({methodData, details}) => {
+ // We expect the *only* payment method (the one basic-card) to be selected initially.
+ is(content.document.querySelector("#total > currency-amount").textContent,
+ "$2.50",
+ "Check modified total currency amount");
+ };
+ const args = {
+ methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
+ details: PTU.Details.bobPayPaymentModifier,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+ await cleanupFormAutofillStorage();
+});
--- a/toolkit/components/payments/test/mochitest/test_order_details.html
+++ b/toolkit/components/payments/test/mochitest/test_order_details.html
@@ -5,18 +5,20 @@
-->
<head>
<meta charset="utf-8">
<title>Test the order-details component</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
<script src="payments_common.js"></script>
<script src="../../res/vendor/custom-elements.min.js"></script>
+ <script src="../../res/unprivileged-fallbacks.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" type="text/css" href="../../res/containers/order-details.css"/>
<template id="order-details-template">
<ul class="main-list"></ul>
<ul class="footer-items-list"></ul>
<div class="details-total">
<h2 class="label">Total</h2>
<currency-amount></currency-amount>
@@ -40,56 +42,69 @@
import OrderDetails from "../../res/containers/order-details.js";
import {requestStore} from "../../res/mixins/PaymentStateSubscriberMixin.js";
let orderDetails = document.querySelector("order-details");
let emptyState = requestStore.getState();
function setup() {
let initialState = deepClone(emptyState);
- requestStore.setState(initialState);
+ let cardGUID = "john-doe";
+ let johnDoeCard = deepClone(PTU.BasicCards.JohnDoe);
+ johnDoeCard.methodName = "basic-card";
+ let savedBasicCards = {
+ [cardGUID]: johnDoeCard,
+ };
+ initialState.selectedPaymentCard = cardGUID;
+ requestStore.setState(Object.assign(initialState, {savedBasicCards}));
}
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",
amount: { currency: "USD", value: "1" },
- }, "items without type of 'tax' arent footer items"));
+ }, "items without type of 'tax' aren't footer items"));
});
add_task(async function test_initial_state() {
setup();
is(orderDetails.mainItemsList.childElementCount, 0, "main items list is initially empty");
is(orderDetails.footerItemsList.childElementCount, 0, "footer items list is initially empty");
is(orderDetails.totalAmountElem.value, "0", "total amount is 0");
});
add_task(async function test_list_population() {
setup();
- let paymentDetails = requestStore.getState().request.paymentDetails;
+ let state = requestStore.getState();
+ let request = state.request;
+ let paymentDetails = deepClone(request.paymentDetails);
paymentDetails.displayItems = [
{
label: "One",
amount: { currency: "USD", value: "5" },
},
{
label: "Two",
amount: { currency: "USD", value: "6" },
},
{
label: "Three",
amount: { currency: "USD", value: "7" },
},
];
- requestStore.setState({ paymentDetails });
+
+ requestStore.setState({
+ request: Object.assign(deepClone(request), { paymentDetails }),
+ });
+
await asyncElementRendered();
is(orderDetails.mainItemsList.childElementCount, 3, "main items list has correct # children");
is(orderDetails.footerItemsList.childElementCount, 0, "footer items list has 0 children");
paymentDetails.displayItems = [
{
label: "Levy",
type: "tax",
@@ -99,29 +114,86 @@ add_task(async function test_list_popula
label: "Item",
amount: { currency: "USD", value: "6" },
},
{
label: "Thing",
amount: { currency: "USD", value: "7" },
},
];
- requestStore.setState({ paymentDetails });
+ Object.assign(request, { paymentDetails });
+ requestStore.setState({ request });
await asyncElementRendered();
is(orderDetails.mainItemsList.childElementCount, 2, "main list has correct # children");
is(orderDetails.footerItemsList.childElementCount, 1, "footer list has correct # children");
});
+add_task(async function test_additionalDisplayItems() {
+ setup();
+ let state = requestStore.getState();
+ let request = state.request;
+ let paymentDetails = request.paymentDetails;
+
+ paymentDetails.modifiers = [{
+ additionalDisplayItems: [
+ {
+ label: "Card fee",
+ amount: { currency: "USD", value: "1.50" },
+ },
+ ],
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "3.50" },
+ },
+ }];
+
+ Object.assign(request, { paymentDetails });
+ requestStore.setState(state);
+ await asyncElementRendered();
+
+ is(orderDetails.mainItemsList.childElementCount, 0,
+ "main list added 0 children from additionalDisplayItems");
+ is(orderDetails.footerItemsList.childElementCount, 1,
+ "footer list added children from additionalDisplayItems");
+});
+
+
add_task(async function test_total() {
- let paymentDetails = requestStore.getState().request.paymentDetails;
- paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: 5 }};
- requestStore.setState({ paymentDetails });
+ setup();
+ let request = requestStore.getState().request;
+ let paymentDetails = request.paymentDetails;
+ paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: "5" }};
+
+ Object.assign(request, { paymentDetails });
+ requestStore.setState({ request });
await asyncElementRendered();
is(orderDetails.totalAmountElem.value, "5", "total amount gets updated");
is(orderDetails.totalAmountElem.currency, "JPY", "total currency gets updated");
});
+add_task(async function test_modified_total() {
+ setup();
+ let state = requestStore.getState();
+ let request = state.request;
+ let paymentDetails = request.paymentDetails;
+ paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: "5" }};
+ paymentDetails.modifiers = [{
+ supportedMethods: "basic-card",
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "3.5" },
+ },
+ }];
+ Object.assign(request, { paymentDetails });
+ requestStore.setState(state);
+ await asyncElementRendered();
+
+ is(orderDetails.totalAmountElem.value, "3.5", "total amount uses modifier total");
+ is(orderDetails.totalAmountElem.currency, "USD", "total currency uses modifier currency");
+});
+
</script>
</body>
</html>