From 0f066eddb792a79f55773cbda05c75e4c98058de Mon Sep 17 00:00:00 2001 From: bgptr Date: Fri, 13 Aug 2021 12:50:46 +0200 Subject: [PATCH] update payment list --- app/actions/LNActions.js | 10 + .../modals/LNPaymentModal/LNPaymentModal.jsx | 32 ++- .../modals/LNPaymentModal/helpers.js | 15 ++ .../shared/DetailsTable/DetailsTable.jsx | 2 +- app/components/shared/LNPaymentStatus.jsx | 17 +- .../DecodedPayRequest/DecodedPayRequest.jsx | 6 +- .../DecodedPayRequest.module.css | 3 - .../SendTab/FailedPayment/FailedPayment.jsx | 27 --- .../FailedPayment/FailedPayment.module.css | 28 --- .../LNPage/SendTab/FailedPayment/index.js | 1 - .../OutstandingPayment/OutstandingPayment.jsx | 27 --- .../OutstandingPayment.module.css | 23 -- .../SendTab/OutstandingPayment/index.js | 1 - .../views/LNPage/SendTab/Payment/Payment.jsx | 28 --- .../LNPage/SendTab/Payment/Payment.module.css | 23 -- .../views/LNPage/SendTab/Payment/index.js | 1 - .../SendTab/PaymentRow/PaymentRow.module.css | 5 +- .../views/LNPage/SendTab/SendTab.jsx | 184 +++++++--------- .../views/LNPage/SendTab/SendTab.module.css | 63 +++--- .../views/LNPage/SendTab/helpers.js | 43 ++++ app/components/views/LNPage/SendTab/hooks.js | 147 +++++++++++-- app/index.js | 5 + app/reducers/ln.js | 8 +- app/selectors.js | 1 + .../components/views/LNPage/SendTab.spec.js | 200 +++++++++++++++++- 25 files changed, 553 insertions(+), 347 deletions(-) delete mode 100644 app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.jsx delete mode 100644 app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.module.css delete mode 100644 app/components/views/LNPage/SendTab/FailedPayment/index.js delete mode 100644 app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.jsx delete mode 100644 app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.module.css delete mode 100644 app/components/views/LNPage/SendTab/OutstandingPayment/index.js delete mode 100644 app/components/views/LNPage/SendTab/Payment/Payment.jsx delete mode 100644 app/components/views/LNPage/SendTab/Payment/Payment.module.css delete mode 100644 app/components/views/LNPage/SendTab/Payment/index.js create mode 100644 app/components/views/LNPage/SendTab/helpers.js diff --git a/app/actions/LNActions.js b/app/actions/LNActions.js index 0666904205..12a7bf4349 100644 --- a/app/actions/LNActions.js +++ b/app/actions/LNActions.js @@ -1075,3 +1075,13 @@ export const changeInvoiceFilter = (newFilter) => (dispatch) => }); resolve(); }); + +export const LNWALLET_CHANGE_PAYMENT_FILTER = "LNWALLET_CHANGE_PAYMENT_FILTER"; +export const changePaymentFilter = (newFilter) => (dispatch) => + new Promise((resolve) => { + dispatch({ + paymentFilter: newFilter, + type: LNWALLET_CHANGE_PAYMENT_FILTER + }); + resolve(); + }); diff --git a/app/components/modals/LNPaymentModal/LNPaymentModal.jsx b/app/components/modals/LNPaymentModal/LNPaymentModal.jsx index 9e7de725c5..90e7ba3bc6 100644 --- a/app/components/modals/LNPaymentModal/LNPaymentModal.jsx +++ b/app/components/modals/LNPaymentModal/LNPaymentModal.jsx @@ -1,6 +1,6 @@ import Modal from "../Modal"; import { FormattedMessage as T } from "react-intl"; -import { CopyableText } from "pi-ui"; +import { CopyableText, Message } from "pi-ui"; import styles from "./LNPaymentModal.module.css"; import { Balance, LNPaymentStatus, DetailsTable } from "shared"; import { getPaymentDetails } from "./helpers"; @@ -10,7 +10,7 @@ const LNPaymentModal = ({ show, onCancelModal, tsDate, payment }) => (
@@ -41,15 +41,25 @@ const LNPaymentModal = ({ show, onCancelModal, tsDate, payment }) => (
-
- -
- - {payment?.paymentRequest} - + {payment?.paymentRequest && ( + <> +
+ +
+ + {payment.paymentRequest} + + + )} + {payment.paymentError && ( + {payment.paymentError} + )} { } ]; + if (payment?.description) { + details.push({ + label: , + value: payment.description + }); + } + + if (payment?.destination) { + details.push({ + label: , + value: payment.destination, + truncate: 40 + }); + } + payment?.htlcsList?.forEach((htlc, index) => { const response = { label: ( diff --git a/app/components/shared/DetailsTable/DetailsTable.jsx b/app/components/shared/DetailsTable/DetailsTable.jsx index 7c8f44287c..7ab75082f8 100644 --- a/app/components/shared/DetailsTable/DetailsTable.jsx +++ b/app/components/shared/DetailsTable/DetailsTable.jsx @@ -5,7 +5,7 @@ import { SmallButton } from "buttons"; import { CopyToClipboard, TruncatedText } from "shared"; const ValueField = ({ data }) => { - const { value, copyable, truncate } = data; + const { value, copyable, truncate } = data; const truncatedText = truncate ? ( ) : ( diff --git a/app/components/shared/LNPaymentStatus.jsx b/app/components/shared/LNPaymentStatus.jsx index 655ac3f813..5a941360ea 100644 --- a/app/components/shared/LNPaymentStatus.jsx +++ b/app/components/shared/LNPaymentStatus.jsx @@ -1,10 +1,7 @@ import { StatusTag } from "pi-ui"; import { defineMessages } from "react-intl"; import { useIntl } from "react-intl"; -import { - PAYMENT_STATUS_CONFIRMED, - PAYMENT_STATUS_FAILED, -} from "constants"; +import { PAYMENT_STATUS_PENDING, PAYMENT_STATUS_FAILED } from "constants"; const messages = defineMessages({ confirmed: { @@ -23,18 +20,18 @@ const messages = defineMessages({ const LNPaymentStatus = ({ status }) => { const intl = useIntl(); - return status === PAYMENT_STATUS_CONFIRMED ? ( - + return status === PAYMENT_STATUS_PENDING ? ( + ) : status === PAYMENT_STATUS_FAILED ? ( ) : ( - + ); }; diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx index 2254ce86d6..837ddc4ade 100644 --- a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx @@ -1,5 +1,5 @@ import { FormattedMessage as T } from "react-intl"; -import { Balance } from "shared"; +import { Balance, TruncatedText } from "shared"; import { DcrInput } from "inputs"; import styles from "./DecodedPayRequest.module.css"; @@ -24,7 +24,9 @@ const DecodedPayRequest = ({ decoded, sendValue, onSendValueChanged }) => ( -
{decoded.destination}
+
+ +
); diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css index 8124a00f04..98140d3e0c 100644 --- a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css @@ -21,9 +21,6 @@ } .destination { - max-width: 113px; - text-overflow: ellipsis; - overflow: hidden; font-size: 20px; line-height: 25px; color: var(--main-dark-blue); diff --git a/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.jsx b/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.jsx deleted file mode 100644 index 1865f05a33..0000000000 --- a/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { Balance } from "shared"; -import styles from "./FailedPayment.module.css"; - -const FailedPayment = ({ payment, paymentError, tsDate }) => ( -
-
-
- -
-
-
-
- -
-
{payment.paymentHash}
-
-
-
{paymentError}
-
-); - -export default FailedPayment; diff --git a/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.module.css b/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.module.css deleted file mode 100644 index 172aabf356..0000000000 --- a/app/components/views/LNPage/SendTab/FailedPayment/FailedPayment.module.css +++ /dev/null @@ -1,28 +0,0 @@ -.lnPayment > .spinner { - align-self: center; -} - -.lnPayment { - margin-top: 1em; - background-color: var(--background-back-color); - padding: 1em 1em 1em 3em; - display: grid; - grid-template-columns: 4fr 4fr 1fr; - min-height: 40px; -} - -.value { - font-size: 32px; -} - -.rhash { - text-overflow: ellipsis; - overflow: hidden; - width: 10em; - color: var(--stroke-color-default); -} - -.paymentError { - grid-column: 1 / 3; - color: var(--error-message-color); -} diff --git a/app/components/views/LNPage/SendTab/FailedPayment/index.js b/app/components/views/LNPage/SendTab/FailedPayment/index.js deleted file mode 100644 index f7361a2c90..0000000000 --- a/app/components/views/LNPage/SendTab/FailedPayment/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./FailedPayment"; diff --git a/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.jsx b/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.jsx deleted file mode 100644 index 5edf0b911a..0000000000 --- a/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { Balance } from "shared"; -import { SimpleLoading } from "indicators"; -import styles from "./OutstandingPayment.module.css"; - -const OutstandingPayment = ({ payment, tsDate }) => ( -
-
-
- -
-
-
-
- -
-
{payment.paymentHash}
-
- -
-); - -export default OutstandingPayment; diff --git a/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.module.css b/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.module.css deleted file mode 100644 index 94d8750f50..0000000000 --- a/app/components/views/LNPage/SendTab/OutstandingPayment/OutstandingPayment.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.lnPayment > .spinner { - align-self: center; -} - -.lnPayment { - margin-top: 1em; - background-color: var(--background-back-color); - padding: 1em 1em 1em 3em; - display: grid; - grid-template-columns: 4fr 4fr 1fr; - min-height: 40px; -} - -.value { - font-size: 32px; -} - -.rhash { - text-overflow: ellipsis; - overflow: hidden; - width: 10em; - color: var(--stroke-color-default); -} diff --git a/app/components/views/LNPage/SendTab/OutstandingPayment/index.js b/app/components/views/LNPage/SendTab/OutstandingPayment/index.js deleted file mode 100644 index 74ae44a6ad..0000000000 --- a/app/components/views/LNPage/SendTab/OutstandingPayment/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./OutstandingPayment"; diff --git a/app/components/views/LNPage/SendTab/Payment/Payment.jsx b/app/components/views/LNPage/SendTab/Payment/Payment.jsx deleted file mode 100644 index 90ef26cce4..0000000000 --- a/app/components/views/LNPage/SendTab/Payment/Payment.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { Balance } from "shared"; -import styles from "./Payment.module.css"; - -const Payment = ({ payment, tsDate }) => ( -
-
-
- -
-
- -
-
-
-
- -
-
{payment.paymentHash}
-
-
-); - -export default Payment; diff --git a/app/components/views/LNPage/SendTab/Payment/Payment.module.css b/app/components/views/LNPage/SendTab/Payment/Payment.module.css deleted file mode 100644 index 94d8750f50..0000000000 --- a/app/components/views/LNPage/SendTab/Payment/Payment.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.lnPayment > .spinner { - align-self: center; -} - -.lnPayment { - margin-top: 1em; - background-color: var(--background-back-color); - padding: 1em 1em 1em 3em; - display: grid; - grid-template-columns: 4fr 4fr 1fr; - min-height: 40px; -} - -.value { - font-size: 32px; -} - -.rhash { - text-overflow: ellipsis; - overflow: hidden; - width: 10em; - color: var(--stroke-color-default); -} diff --git a/app/components/views/LNPage/SendTab/Payment/index.js b/app/components/views/LNPage/SendTab/Payment/index.js deleted file mode 100644 index 1b278aa850..0000000000 --- a/app/components/views/LNPage/SendTab/Payment/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Payment"; diff --git a/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css index 2fb4a70c55..24629438d3 100644 --- a/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css +++ b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css @@ -20,7 +20,7 @@ .value { padding-left: 57px; - background-image: var(--ln-invoice-icon); + background-image: var(--minus-small-icon); background-size: 22px 22px; background-position: 20px center; background-repeat: no-repeat; @@ -36,10 +36,7 @@ .value .paymentHash { font-size: 13px; line-height: 16px; - text-overflow: ellipsis; - overflow: hidden; color: var(--grey-5); - max-width: 267px; } .date { diff --git a/app/components/views/LNPage/SendTab/SendTab.jsx b/app/components/views/LNPage/SendTab/SendTab.jsx index 35a3cb0c57..6717179e19 100644 --- a/app/components/views/LNPage/SendTab/SendTab.jsx +++ b/app/components/views/LNPage/SendTab/SendTab.jsx @@ -1,20 +1,18 @@ import { useSendTab } from "./hooks"; import { FormattedMessage as T, defineMessages } from "react-intl"; -import { KeyBlueButton } from "buttons"; +import { KeyBlueButton, EyeFilterMenu } from "buttons"; import { TextInput } from "inputs"; import styles from "./SendTab.module.css"; -import { Subtitle, Balance, VerticalAccordion } from "shared"; +import { Subtitle } from "shared"; import { DescriptionHeader } from "layout"; import ReactTimeout from "react-timeout"; import DecodedPayRequest from "./DecodedPayRequest"; -import OutstandingPayment from "./OutstandingPayment"; -import FailedPayment from "./FailedPayment"; -import Payment from "./Payment"; import BalancesHeader from "../BalancesHeader"; -import { Button, classNames } from "pi-ui"; +import { Button, classNames, Tooltip } from "pi-ui"; import { wallet } from "wallet-preload-shim"; import PaymentRow from "./PaymentRow"; import { LNPaymentModal } from "modals"; +import { getSortTypes, getPaymentTypes } from "./helpers"; const messages = defineMessages({ payReqInputLabel: { @@ -28,6 +26,10 @@ const messages = defineMessages({ payReqDecodeSuccessMsg: { id: "ln.paymentsTab.payReqDecodeSuccessMsg", defaultMessage: "Valid Lightning Request" + }, + filterByHashPlaceholder: { + id: "ln.paymentsTab.filterByHashPlaceholder", + defaultMessage: "Filter by Payment Hash" } }); @@ -45,11 +47,57 @@ export const SendTabHeader = () => ( /> ); +const subtitleMenu = ({ + sortTypes, + paymentTypes, + listDirection, + selectedPaymentType, + searchText, + intl, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText +}) => ( +
+
+ onChangeSearchText(e.target.value)} + /> +
+ }> + + + }> + + +
+); + const SendTab = ({ setTimeout }) => { const { payments, - outstandingPayments, - failedPayments, tsDate, payRequest, decodedPayRequest, @@ -58,12 +106,15 @@ const SendTab = ({ setTimeout }) => { onPayRequestChanged, onSendPayment, onSendValueChanged, - isShowingDetails, - selectedPaymentDetails, - onToggleShowDetails, selectedPayment, setSelectedPayment, - intl + intl, + searchText, + listDirection, + selectedPaymentType, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText } = useSendTab(setTimeout); return ( @@ -133,14 +184,27 @@ const SendTab = ({ setTimeout }) => { )} } + className={styles.paymentHistorySubtitle} + title={ + + } + children={subtitleMenu({ + sortTypes: getSortTypes(), + paymentTypes: getPaymentTypes(), + listDirection, + selectedPaymentType, + searchText, + intl, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText + })} /> {payments && payments.length > 0 ? (
{payments.map((payment) => ( setSelectedPayment(payment)} @@ -149,7 +213,7 @@ const SendTab = ({ setTimeout }) => {
) : (
- +
)} {selectedPayment && ( @@ -160,94 +224,6 @@ const SendTab = ({ setTimeout }) => { tsDate={tsDate} /> )} - {Object.keys(outstandingPayments).length > 0 && ( -
- } - /> -
- {Object.keys(outstandingPayments).map((ph) => ( - - ))} -
-
- )} - {failedPayments.length > 0 && ( -
- } - /> -
- {failedPayments.map((p) => ( - - ))} -
-
- )} - {payments.length > 0 && ( -
- } - /> -
- {payments.map((p) => ( - } - onToggleAccordion={() => onToggleShowDetails(p.paymentHash)} - show={ - p.paymentHash === selectedPaymentDetails && isShowingDetails - }> -
- PayReq -
- {p.paymentRequest} -
- {p.htlcsList.map((htlc, i) => ( -
- HTLC {i} -
-
- Status - {htlc.status} - Total Amount - - Total fees - -
- Route -
- Hop - Fee - PubKey - {htlc.route.hopsList.map((hop, i) => ( - - {i} - - - - {hop.pubKey} - - ))} -
-
- ))} -
-
- ))} -
-
- )} ); }; diff --git a/app/components/views/LNPage/SendTab/SendTab.module.css b/app/components/views/LNPage/SendTab/SendTab.module.css index 0d5ada1476..95aa084c95 100644 --- a/app/components/views/LNPage/SendTab/SendTab.module.css +++ b/app/components/views/LNPage/SendTab/SendTab.module.css @@ -13,36 +13,6 @@ color: var(--error-red); } -.lnPaymentsList { - margin-bottom: 1em; -} - -.paymentDetails { - word-break: break-all; - background-color: var(--background-back-color); - padding: 1em; -} - -.paymentDetailsGrid { - display: grid; - grid-template-columns: 1fr 3fr; -} - -.verticalAccordionArrow { - transform: rotate(180deg); - top: 30px; - right: 30px; -} - -.htlc { - padding-top: 1em; -} - -.paymentRouteGrid { - display: grid; - grid-template-columns: 1fr 1fr 2fr; -} - .addressInput { padding-right: 22px; } @@ -82,8 +52,37 @@ text-align: right; } -.listWrapper { - margin-top: 30px; +.filterContainer { + margin-left: auto; +} + +.paymentSearch { + display: inline-block; +} + +.sortByTooltip { + font-size: 14px; + width: max-content; +} + +.typeTooltip { + font-size: 14px; + width: max-content; +} + +.searchInput input { + width: 180px; +} + +.paymentHistorySubtitle { + display: flex; + flex-direction: row; + width: initial; + margin-top: 40px; +} + +.empty { + text-align: center; } @media screen and (max-width: 768px) { diff --git a/app/components/views/LNPage/SendTab/helpers.js b/app/components/views/LNPage/SendTab/helpers.js new file mode 100644 index 0000000000..843cdf7373 --- /dev/null +++ b/app/components/views/LNPage/SendTab/helpers.js @@ -0,0 +1,43 @@ +import { FormattedMessage as T } from "react-intl"; +import { + PAYMENT_STATUS_FAILED, + PAYMENT_STATUS_PENDING, + PAYMENT_STATUS_CONFIRMED +} from "constants"; + +export const getSortTypes = () => [ + { + value: "desc", + key: "desc", + label: + }, + { + value: "asc", + key: "asc", + label: + } +]; + +// -1 cleans the filter types +export const getPaymentTypes = () => [ + { + key: "all", + value: { type: "all" }, + label: + }, + { + key: "confirmed", + value: { type: PAYMENT_STATUS_CONFIRMED }, + label: + }, + { + key: "failed", + value: { type: PAYMENT_STATUS_FAILED }, + label: + }, + { + key: "pending", + value: { type: PAYMENT_STATUS_PENDING }, + label: + } +]; diff --git a/app/components/views/LNPage/SendTab/hooks.js b/app/components/views/LNPage/SendTab/hooks.js index 810917255e..852bddfff5 100644 --- a/app/components/views/LNPage/SendTab/hooks.js +++ b/app/components/views/LNPage/SendTab/hooks.js @@ -1,6 +1,14 @@ import { useState, useCallback, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { useLNPage } from "../hooks"; +import { + PAYMENT_STATUS_FAILED, + PAYMENT_STATUS_PENDING, + PAYMENT_STATUS_CONFIRMED +} from "constants"; import { useIntl } from "react-intl"; +import * as sel from "selectors"; +import * as lna from "actions/LNActions"; export function useSendTab(setTimeout) { const [sendValueAtom, setSendValueAtom] = useState(0); @@ -9,9 +17,8 @@ export function useSendTab(setTimeout) { const [decodingError, setDecodingError] = useState(null); const [expired, setExpired] = useState(false); const [sending, setSendValue] = useState(); - const [isShowingDetails, setIsShowingDetails] = useState(false); - const [selectedPaymentDetails, setSelectedPaymentDetails] = useState(null); const [selectedPayment, setSelectedPayment] = useState(null); + const paymentFilter = useSelector(sel.lnPaymentFilter); const intl = useIntl(); const { @@ -24,13 +31,77 @@ export function useSendTab(setTimeout) { sendPayment } = useLNPage(); - const onToggleShowDetails = useCallback( - (paymentHash) => { - setSelectedPaymentDetails(paymentHash); - setIsShowingDetails(!isShowingDetails); - }, - [isShowingDetails] - ); + const getPayments = useCallback(() => { + const mergedPayments = [...payments].map((p) => { + p.status = PAYMENT_STATUS_CONFIRMED; + return p; + }); + const findPayment = (paymentHash) => + mergedPayments.find((p) => p.paymentHash == paymentHash); + + Object.keys(outstandingPayments).forEach((ph) => { + if (!findPayment(outstandingPayments[ph].decoded.paymentHash)) { + mergedPayments.push({ + ...outstandingPayments[ph].decoded, + creationDate: outstandingPayments[ph].decoded.timestamp, + status: PAYMENT_STATUS_PENDING, + valueAtoms: outstandingPayments[ph].decoded.numAtoms + }); + } + }); + + failedPayments.forEach((payment) => { + if (!findPayment(payment.decoded.paymentHash)) { + mergedPayments.push({ + ...payment.decoded, + creationDate: payment.decoded.timestamp, + status: PAYMENT_STATUS_FAILED, + valueAtoms: payment.decoded.numAtoms, + paymentError: + payment.paymentError && + payment.paymentError.charAt(0).toUpperCase() + + payment.paymentError.slice(1) + }); + } + }); + + return mergedPayments + .filter( + (payment) => + !paymentFilter || + !paymentFilter.type || + paymentFilter.type === "all" || + paymentFilter.type === payment.status + ) + .filter( + (payment) => + !paymentFilter || + !paymentFilter.search || + payment.paymentHash + .toLowerCase() + .indexOf(paymentFilter.search.toLowerCase()) !== -1 + ) + .sort((a, b) => { + const at = a.timestamp || a.creationDate; + const bt = b.timestamp || b.creationDate; + if (paymentFilter && paymentFilter.listDirection == "asc") { + if (at > bt) { + return 1; + } + if (at < bt) { + return -1; + } + } else { + if (at < bt) { + return 1; + } + if (at > bt) { + return -1; + } + } + return 0; + }); + }, [payments, failedPayments, outstandingPayments, paymentFilter]); const checkExpired = useCallback(() => { if (!decodedPayRequest) return; @@ -85,10 +156,50 @@ export function useSendTab(setTimeout) { sendPayment(payRequest, sendValueAtom); }; + const dispatch = useDispatch(); + + const onChangePaymentFilter = useCallback( + (newFilter) => dispatch(lna.changePaymentFilter(newFilter)), + [dispatch] + ); + + const searchText = paymentFilter?.search ?? ""; + const listDirection = paymentFilter?.listDirection; + const selectedPaymentType = paymentFilter?.type; + + const [isChangingFilterTimer, setIsChangingFilterTimer] = useState(null); + + const onChangeSelectedType = (type) => { + onChangeFilter(type.value); + }; + + const onChangeSortType = (type) => { + onChangeFilter({ listDirection: type.value }); + }; + + const onChangeSearchText = (searchText) => { + onChangeFilter({ search: searchText }); + }; + + const onChangeFilter = (value) => { + return new Promise((resolve) => { + if (isChangingFilterTimer) { + clearTimeout(isChangingFilterTimer); + } + const changeFilter = (newFilterOpt) => { + const newFilter = { ...paymentFilter, ...newFilterOpt }; + clearTimeout(isChangingFilterTimer); + onChangePaymentFilter(newFilter); + return newFilter; + }; + setIsChangingFilterTimer( + setTimeout(() => resolve(changeFilter(value)), 100) + ); + }); + }; + return { - payments, - outstandingPayments, - failedPayments, + payments: getPayments(), tsDate, payRequest, decodedPayRequest, @@ -99,12 +210,16 @@ export function useSendTab(setTimeout) { onPayRequestChanged, onSendPayment, onSendValueChanged, - isShowingDetails, - selectedPaymentDetails, - onToggleShowDetails, intl, selectedPayment, setSelectedPayment, - channelBalances + channelBalances, + searchText, + listDirection, + selectedPaymentType, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText, + onChangePaymentFilter }; } diff --git a/app/index.js b/app/index.js index 3f83dd95df..8bae0fcdcf 100644 --- a/app/index.js +++ b/app/index.js @@ -458,6 +458,11 @@ const initialState = { search: null, // The freeform text in the Search box listDirection: "desc", // asc = oldest -> newest, desc => newest -> oldest type: null // desired invoice type (code). + }, + paymentFilter: { + search: null, // The freeform text in the Search box + listDirection: "desc", // asc = oldest -> newest, desc => newest -> oldest + type: null // desired payment type (code). } }, dex: { diff --git a/app/reducers/ln.js b/app/reducers/ln.js index f81d6d2c71..fa50832aea 100644 --- a/app/reducers/ln.js +++ b/app/reducers/ln.js @@ -37,7 +37,8 @@ import { LNWALLET_GETROUTESINFO_SUCCESS, LNWALLET_GETROUTESINFO_FAILED, LNWALLET_LISTWATCHTOWERS_SUCCESS, - LNWALLET_CHANGE_INVOICE_FILTER + LNWALLET_CHANGE_INVOICE_FILTER, + LNWALLET_CHANGE_PAYMENT_FILTER } from "actions/LNActions"; function addOutstandingPayment(oldOut, rhashHex, payData) { @@ -311,6 +312,11 @@ export default function ln(state = {}, action) { ...state, invoiceFilter: action.invoiceFilter }; + case LNWALLET_CHANGE_PAYMENT_FILTER: + return { + ...state, + paymentFilter: action.paymentFilter + }; default: return state; diff --git a/app/selectors.js b/app/selectors.js index 742c992192..3fe535c2df 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -1862,6 +1862,7 @@ export const lnSCBPath = get(["ln", "scbPath"]); export const lnSCBUpdatedTime = get(["ln", "scbUpdatedTime"]); export const lnTowersList = get(["ln", "towersList"]); export const lnInvoiceFilter = get(["ln", "invoiceFilter"]); +export const lnPaymentFilter = get(["ln", "paymentFilter"]); // end of ln selectors // start of dex selectors diff --git a/test/unit/components/views/LNPage/SendTab.spec.js b/test/unit/components/views/LNPage/SendTab.spec.js index ea2f2020f8..279b0e91f9 100644 --- a/test/unit/components/views/LNPage/SendTab.spec.js +++ b/test/unit/components/views/LNPage/SendTab.spec.js @@ -18,7 +18,51 @@ const mockLnChannelBalance = { maxOutboundAmount: 97997360 }; -const mockOutstandingPayments = {}; +const mockOutstandingPayments = { + "mock-outstanding-payment-hash-0": { + decoded: { + destination: "mock-destination-0", + paymentHash: "mock-outstanding-payment-hash-0", + numAtoms: 1000000, + timestamp: 1628688648, + expiry: 3600, + description: "mock-outstanding-desc-0", + descriptionHash: "", + fallbackAddr: "", + cltvExpiry: 80, + routeHintsList: [], + paymentAddr: "mock-payment-address-0", + numMAtoms: 1000000000, + featuresMap: [ + [ + 15, + { + name: "payment-addr", + isRequired: false, + isKnown: true + } + ], + [ + 17, + { + name: "multi-path-payments", + isRequired: false, + isKnown: true + } + ], + [ + 9, + { + name: "tlv-onion", + isRequired: false, + isKnown: true + } + ] + ] + } + } +}; + const mockPayments = [ { paymentHash: "mock-payment-hash-0", @@ -70,6 +114,52 @@ const mockPayments = [ failureReason: 0 } ]; +const mockFailedPayment = [ + { + paymentError: "mock-payment-error", + decoded: { + destination: "mock-destination", + paymentHash: "mock-payment-hash", + numAtoms: 10, + timestamp: 1628512835, + expiry: 3600, + description: "mock-failed-desc", + descriptionHash: "", + fallbackAddr: "", + cltvExpiry: 80, + routeHintsList: [], + paymentAddr: "mock-payment-address", + numMAtoms: 10000, + featuresMap: [ + [ + 15, + { + name: "payment-addr", + isRequired: false, + isKnown: true + } + ], + [ + 17, + { + name: "multi-path-payments", + isRequired: false, + isKnown: true + } + ], + [ + 9, + { + name: "tlv-onion", + isRequired: false, + isKnown: true + } + ] + ] + } + } +]; + const mockReqCode = "mock-req-code"; const mockValidDecodedPayRequest = { destination: "mock-destination", @@ -119,6 +209,7 @@ beforeEach(() => { selectors.currencyDisplay = jest.fn(() => DCR); selectors.lnChannelBalances = jest.fn(() => mockLnChannelBalance); selectors.lnOutstandingPayments = jest.fn(() => mockOutstandingPayments); + selectors.lnFailedPayments = jest.fn(() => mockFailedPayment); selectors.lnPayments = jest.fn(() => mockPayments); mockDecodePayRequest = lnActions.decodePayRequest = jest.fn(() => () => Promise.resolve(mockValidDecodedPayRequest) @@ -148,7 +239,7 @@ test("test send form with valid lightning request", async () => { "Amount0.01000 DCR" ); expect(screen.getByText("Destination").parentNode.textContent).toMatch( - `Destination${mockValidDecodedPayRequest.destination}` + "Destinationmock-...ation" ); user.click(getSendButton()); @@ -183,8 +274,109 @@ test("test paste and clear button", async () => { await wait(() => expect(getReqCodeInput().value).toBe("")); }); -test("test payments list", () => { +test("test payment list and modal ", async () => { render(); - screen.debug(); + expect( + screen + .getAllByText(/Sent payment/i) + .map((node) => node.parentElement.textContent) + ).toStrictEqual([ + `Sent Payment 0.01000 DCR${mockOutstandingPayments["mock-outstanding-payment-hash-0"].decoded.paymentHash}`, + `Sent Payment 0.0000001 DCR${mockFailedPayment[0].decoded.paymentHash}`, + `Sent Payment 0.20000 DCR${mockPayments[0].paymentHash}` + ]); + + // click on the first (outstanding) payment and check modal + user.click(screen.getByText("Pending")); + expect(screen.getAllByText("Pending").length).toBe(2); + //modal has been closed + user.click(screen.getByTestId("lnpayment-close-button")); + await wait(() => + expect(screen.queryByText("Lightning Payment")).not.toBeInTheDocument() + ); + + // click on the second (failed) payment and check modal + user.click(screen.getByText("Failed")); + expect(screen.getAllByText("Failed").length).toBe(2); + expect( + screen.getByText(mockFailedPayment[0].decoded.paymentHash) + ).toBeInTheDocument(); + user.click(screen.getByTestId("lnpayment-close-button")); + expect(screen.queryByText("Lightning Payment")).not.toBeInTheDocument(); + + // click on the second (confirmed) payment and check modal + user.click(screen.getByText("Confirmed")); + expect(screen.getAllByText("Confirmed").length).toBe(2); + expect(screen.getByText(mockPayments[0].paymentHash)).toBeInTheDocument(); + user.click(screen.getByTestId("lnpayment-close-button")); + expect(screen.queryByText("Lightning Payment")).not.toBeInTheDocument(); +}); + +test("test sort control", async () => { + render(); + + expect( + screen + .getAllByText(/Sent Payment/i) + .map((node) => node.parentElement.textContent) + ).toStrictEqual([ + `Sent Payment 0.01000 DCR${mockOutstandingPayments["mock-outstanding-payment-hash-0"].decoded.paymentHash}`, + `Sent Payment 0.0000001 DCR${mockFailedPayment[0].decoded.paymentHash}`, + `Sent Payment 0.20000 DCR${mockPayments[0].paymentHash}` + ]); + + const sortMenuButton = screen.getAllByRole("button", { + name: "EyeFilterMenu" + })[0]; + + user.click(sortMenuButton); + user.click(screen.getByText("Oldest")); + + await wait(() => + expect( + screen + .getAllByText(/Sent Payment/i) + .map((node) => node.parentElement.textContent) + ).toStrictEqual([ + `Sent Payment 0.20000 DCR${mockPayments[0].paymentHash}`, + `Sent Payment 0.0000001 DCR${mockFailedPayment[0].decoded.paymentHash}`, + `Sent Payment 0.01000 DCR${mockOutstandingPayments["mock-outstanding-payment-hash-0"].decoded.paymentHash}` + ]) + ); +}); + +test("test search control", async () => { + render(); + + expect( + screen + .getAllByText(/Sent Payment/i) + .map((node) => node.parentElement.textContent) + ).toStrictEqual([ + `Sent Payment 0.01000 DCR${mockOutstandingPayments["mock-outstanding-payment-hash-0"].decoded.paymentHash}`, + `Sent Payment 0.0000001 DCR${mockFailedPayment[0].decoded.paymentHash}`, + `Sent Payment 0.20000 DCR${mockPayments[0].paymentHash}` + ]); + + const searchInput = screen.getByPlaceholderText("Filter by Payment Hash"); + user.type(searchInput, "payment-hash-0"); + + await wait(() => + expect( + screen + .getAllByText(/Sent Payment/i) + .map((node) => node.parentElement.textContent) + ).toStrictEqual([ + `Sent Payment 0.01000 DCR${mockOutstandingPayments["mock-outstanding-payment-hash-0"].decoded.paymentHash}`, + `Sent Payment 0.20000 DCR${mockPayments[0].paymentHash}` + ]) + ); + + user.type(searchInput, "mock-hash-22-12"); + + await wait(() => + expect(screen.queryByText(/Sent Payment/i)).not.toBeInTheDocument() + ); + expect(screen.getByText(/no payment found/i)).toBeInTheDocument(); });