Bug 1427939 - Use total and any additionalDisplayItems when a modifier matches the payment method.
* WIP: A paymentRequestHelpers library, with a method to get the right modifier.
* Use selectedPaymentMethod from state store.
* Ensure saved cards always present with a paymentMethod property.
* 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/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -13,11 +13,12 @@ toolkit.jar:
res/payments (res/paymentRequest.*)
res/payments/components/ (res/components/*.css)
res/payments/components/ (res/components/*.js)
res/payments/containers/ (res/containers/*.js)
res/payments/containers/ (res/containers/*.css)
res/payments/debugging.css (res/debugging.css)
res/payments/debugging.html (res/debugging.html)
res/payments/debugging.js (res/debugging.js)
+ res/payments/paymentRequestHelpers.js (res/paymentRequestHelpers.js)
res/payments/mixins/ (res/mixins/*.js)
res/payments/PaymentsStore.js (res/PaymentsStore.js)
res/payments/vendor/ (res/vendor/*)
--- a/toolkit/components/payments/res/containers/order-details.js
+++ b/toolkit/components/payments/res/containers/order-details.js
@@ -1,13 +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/. */
-/* global PaymentStateSubscriberMixin, PaymentRequest */
+/* global PaymentStateSubscriberMixin, PaymentRequest, paymentRequestHelpers */
"use strict";
/**
* <order-details></order-details>
*/
class OrderDetails extends PaymentStateSubscriberMixin(HTMLElement) {
@@ -51,29 +51,48 @@ class OrderDetails extends PaymentStateS
row.amountValue = item.amount.value;
row.amountCurrency = item.amount.currency;
fragment.appendChild(row);
}
listEl.appendChild(fragment);
return listEl;
}
+ _getTotalItem(state) {
+ let methodId = state.selectedPaymentCard;
+ if (methodId) {
+ let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+ if (modifier && modifier.hasOwnProperty("total")) {
+ return modifier.total;
+ }
+ }
+ return state.request.paymentDetails.totalItem;
+ }
+
+ _getAdditionalDisplayItems(state) {
+ let methodId = state.selectedPaymentCard;
+ let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+ if (modifier && modifier.additionalDisplayItems) {
+ return modifier.additionalDisplayItems;
+ }
+ return [];
+ }
+
render(state) {
- let request = state.request;
- let totalItem = request.paymentDetails.totalItem;
+ let totalItem = this._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;
}
@@ -84,28 +103,38 @@ class OrderDetails extends PaymentStateS
*
* @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 = paymentRequestHelpers.getModifierForPaymentMethod(state.request, 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
@@ -1,13 +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/. */
-/* global PaymentStateSubscriberMixin, paymentRequest */
+/* global PaymentStateSubscriberMixin, paymentRequest, paymentRequestHelpers */
"use strict";
/**
* <payment-dialog></payment-dialog>
*/
class PaymentDialog extends PaymentStateSubscriberMixin(HTMLElement) {
@@ -177,22 +177,33 @@ class PaymentDialog extends PaymentState
this.changeShippingOption(state.selectedShippingOption);
}
}
this._cachedState.selectedShippingAddress = state.selectedShippingAddress;
this._cachedState.selectedShippingOption = state.selectedShippingOption;
}
+ _getTotalItem(state) {
+ let methodId = state.selectedPaymentCard;
+ if (methodId) {
+ let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+ if (modifier && modifier.hasOwnProperty("total")) {
+ return modifier.total;
+ }
+ }
+ return state.request.paymentDetails.totalItem;
+ }
+
render(state) {
let request = state.request;
let paymentDetails = request.paymentDetails;
this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
- let totalItem = paymentDetails.totalItem;
+ let totalItem = this._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;
for (let element of this._shippingRelatedEls) {
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -6,17 +6,21 @@ const paymentDialog = window.parent.docu
// The requestStore should be manipulated for most changes but autofill storage changes
// happen through setStateFromParent which includes some consistency checks.
const requestStore = paymentDialog.requestStore;
let REQUEST_1 = {
tabId: 9,
topLevelPrincipal: {URI: {displayHost: "tschaeff.github.io"}},
requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
- paymentMethods: [],
+ paymentMethods: [
+ {
+ supportedMethods: "basic-card",
+ },
+ ],
paymentDetails: {
id: "",
totalItem: {label: "Demo total", amount: {currency: "EUR", value: "1.00"}, pending: false},
displayItems: [
{
label: "Square",
amount: {
currency: "USD",
@@ -107,17 +111,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.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -29,16 +29,17 @@
<link rel="stylesheet" href="containers/order-details.css"/>
<script src="vendor/custom-elements.min.js"></script>
<script src="PaymentsStore.js"></script>
<script src="mixins/ObservedPropertiesMixin.js"></script>
<script src="mixins/PaymentStateSubscriberMixin.js"></script>
+ <script src="paymentRequestHelpers.js"></script>
<script src="components/currency-amount.js"></script>
<script src="containers/order-details.js"></script>
<script src="components/payment-details-item.js"></script>
<script src="components/rich-select.js"></script>
<script src="components/rich-option.js"></script>
<script src="components/address-option.js"></script>
<script src="components/shipping-option.js"></script>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/paymentRequestHelpers.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/**
+ * Define common helpers for working with payment requests
+ */
+
+/* exported paymentRequestHelpers */
+
+
+var paymentRequestHelpers = {
+ getModifierForPaymentMethod(request, methodId) {
+ if (methodId !== "basic-card") {
+ throw new Error(`${methodId} is not a supported payment method`);
+ }
+ let modifiers = request.paymentDetails.modifiers;
+ if (!modifiers || !modifiers.length) {
+ return null;
+ }
+ let modifier = modifiers.find(m => {
+ // take the first matching modifier
+ // TODO: match on supportedTypes and supportedNetworks
+ return (m.supportedMethods == "basic-card");
+ });
+ return modifier || null;
+ },
+};
+
--- a/toolkit/components/payments/test/browser/browser_total.js
+++ b/toolkit/components/payments/test/browser/browser_total.js
@@ -8,8 +8,22 @@ 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_modified_total() {
+ const testTask = ({methodData, details}) => {
+ // We expect the *first* payment method to be selected when applying modifiers
+ is(content.document.querySelector("#total > currency-amount").textContent,
+ "$3.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);
+});
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,15 +1,16 @@
[DEFAULT]
prefs =
dom.webcomponents.customelements.enabled=false
support-files =
../../../../../testing/modules/sinon-2.3.2.js
../../res/paymentRequest.css
../../res/paymentRequest.xhtml
+ ../../res/paymentRequestHelpers.js
../../res/PaymentsStore.js
../../res/components/currency-amount.js
../../res/components/address-option.js
../../res/components/address-option.css
../../res/components/basic-card-option.js
../../res/components/basic-card-option.css
../../res/components/payment-details-item.js
../../res/components/rich-option.js
--- a/toolkit/components/payments/test/mochitest/test_order_details.html
+++ b/toolkit/components/payments/test/mochitest/test_order_details.html
@@ -7,16 +7,17 @@
<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/SpawnTask.js"></script>
<script src="payments_common.js"></script>
<script src="custom-elements.min.js"></script>
<script src="PaymentsStore.js"></script>
<script src="PaymentStateSubscriberMixin.js"></script>
+ <script src="paymentRequestHelpers.js"></script>
<script src="order-details.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<template id="order-details-template">
<ul class="main-list"></ul>
<ul class="footer-items-list"></ul>
<div class="details-total">
@@ -41,16 +42,17 @@
/* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
/* import-globals-from ../../res/containers/order-details.js */
let orderDetails = document.querySelector("order-details");
let emptyState = requestStore.getState();
function setup() {
let initialState = deepClone(emptyState);
+ initialState.selectedPaymentCard = "basic-card";
requestStore.setState(initialState);
}
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
add_task(async function isFooterItem() {
@@ -69,32 +71,36 @@ add_task(async function test_initial_sta
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 = 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 });
+ Object.assign(request, { paymentDetails });
+ requestStore.setState(state);
+
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",
@@ -104,29 +110,89 @@ 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 paymentMethods = ["basic-card"];
+ 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, { paymentMethods, 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;
+ setup();
+ let request = requestStore.getState().request;
+ let paymentDetails = request.paymentDetails;
paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: 5 }};
- requestStore.setState({ paymentDetails });
+
+ 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 paymentMethods = ["basic-card"];
+ 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, { paymentMethods, paymentDetails });
+ info("state: ", JSON.stringify(state, null, 2));
+ 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>
--- a/toolkit/components/payments/test/mochitest/test_payment_dialog.html
+++ b/toolkit/components/payments/test/mochitest/test_payment_dialog.html
@@ -9,16 +9,17 @@ Test the payment-dialog custom element
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script src="sinon-2.3.2.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="paymentRequestHelpers.js"></script>
<script src="payment-dialog.js"></script>
<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>