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/inputs/Input/Input.jsx b/app/components/inputs/Input/Input.jsx index 35511b3ba6..a8df3f3985 100644 --- a/app/components/inputs/Input/Input.jsx +++ b/app/components/inputs/Input/Input.jsx @@ -25,6 +25,7 @@ const Input = ({ onChange, showErrors, showSuccess, + hideIcons, invalidMessage, successMessage, requiredMessage, @@ -81,12 +82,16 @@ const Input = ({ placeholder }} type={type ?? "text"} - success={showSuccess && successMessage} + success={showSuccess ? successMessage : ""} value={value ?? ""} onChange={(e) => onChange?.(e)} onFocus={(e) => onFocus?.(e)} onBlur={(e) => onBlur?.(e)} - wrapperClassNames={classNames(className, styles.wrapper)} + wrapperClassNames={classNames( + className, + styles.wrapper, + hideIcons && styles.hideIcons + )} inputClassNames={classNames( inputClassNames, newBiggerFontStyle ? styles.newBiggerFontStyleInput : styles.input diff --git a/app/components/inputs/Input/Input.module.css b/app/components/inputs/Input/Input.module.css index b7de2adb40..458ff1a499 100644 --- a/app/components/inputs/Input/Input.module.css +++ b/app/components/inputs/Input/Input.module.css @@ -30,5 +30,15 @@ .input:not(:focus), .newBiggerFontStyleInput:not(:focus) { - border-bottom: 1px var(--select-stroke-color) solid !important; + border-bottom: 1px var(--select-stroke-color) solid; +} + +.wrapper.hideIcons > div > svg { + display: none; +} +.wrapper.hideIcons > div > div { + right: 0 !important; +} +.wrapper.hideIcons input { + padding-right: 26px !important; } diff --git a/app/components/modals/LNInvoiceModal/LNInvoiceModal.jsx b/app/components/modals/LNInvoiceModal/LNInvoiceModal.jsx index 9c9025f78c..cc821808bd 100644 --- a/app/components/modals/LNInvoiceModal/LNInvoiceModal.jsx +++ b/app/components/modals/LNInvoiceModal/LNInvoiceModal.jsx @@ -1,12 +1,13 @@ import Modal from "../Modal"; import { FormattedMessage as T } from "react-intl"; -import { CopyableText, classNames } from "pi-ui"; +import { classNames } from "pi-ui"; import styles from "./LNInvoiceModal.module.css"; import { Balance, LNInvoiceStatus, FormattedRelative, - DetailsTable + DetailsTable, + CopyableText } from "shared"; import { PiUiButton } from "buttons"; import { diff --git a/app/components/modals/LNInvoiceModal/helpers.js b/app/components/modals/LNInvoiceModal/helpers.js index ba5584fabd..06408708a7 100644 --- a/app/components/modals/LNInvoiceModal/helpers.js +++ b/app/components/modals/LNInvoiceModal/helpers.js @@ -1,19 +1,11 @@ import { FormattedMessage as T } from "react-intl"; -import { SmallButton } from "buttons"; -import { CopyToClipboard, TruncatedText } from "shared"; export const getInvoiceDetails = (invoice, tsDate) => { const details = [ { label: , - value: ( - <> - - - - ) + value: invoice?.rHashHex, + copyable: true, + truncate: 40 }, { label: , diff --git a/app/components/modals/LNPaymentModal/LNPaymentModal.jsx b/app/components/modals/LNPaymentModal/LNPaymentModal.jsx new file mode 100644 index 0000000000..de78c6265c --- /dev/null +++ b/app/components/modals/LNPaymentModal/LNPaymentModal.jsx @@ -0,0 +1,71 @@ +import Modal from "../Modal"; +import { FormattedMessage as T } from "react-intl"; +import { Message } from "pi-ui"; +import styles from "./LNPaymentModal.module.css"; +import { Balance, LNPaymentStatus, DetailsTable, CopyableText } from "shared"; +import { getPaymentDetails } from "./helpers"; + +const LNPaymentModal = ({ show, onCancelModal, tsDate, payment }) => ( + +
+
+ +
+
+ + + + + +
+ +
+
+ +
+ +
+ {payment?.paymentRequest && ( + <> +
+ +
+ + {payment.paymentRequest} + + + )} + {payment.paymentError && ( + {payment.paymentError} + )} + } + expandable + /> + +); +export default LNPaymentModal; diff --git a/app/components/modals/LNPaymentModal/LNPaymentModal.module.css b/app/components/modals/LNPaymentModal/LNPaymentModal.module.css new file mode 100644 index 0000000000..c4baeada9f --- /dev/null +++ b/app/components/modals/LNPaymentModal/LNPaymentModal.module.css @@ -0,0 +1,96 @@ +.modal { + background-image: none; + padding: 30px 40px; + width: 764px; + overflow-x: hidden; + position: relative; + margin: 0; + max-height: calc(100% - 130px); + display: flex; + flex-direction: column; +} +.closeButton { + position: absolute; + top: 20px; + right: 20px; + height: 10px; + width: 10px; + background-image: var(--x-grey); + background-size: 10px 10px; + background-repeat: no-repeat; + cursor: pointer; +} +.title { + font-size: 28px; + color: var(--grey-7); + line-height: 35px; +} +.date { + font-size: 13px; + line-height: 16px; + color: var(--main-dark-blue); +} +.dataGrid { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + font-size: 13px; + line-height: 16px; + color: var(--main-dark-blue); + align-items: center; + grid-row-gap: 5px; + margin: 19px 0 30px 0; +} +.dataGrid label { + color: var(--grey-7); +} +.amount { + font-size: 16px; + line-height: 22px; +} +.requestCodeLabel { + font-size: 13px; + line-height: 16px; + color: var(--grey-7); + margin-bottom: 10px; +} +.paymentRequest { + padding: 0.3rem 1rem; + word-break: break-all; + background-color: var(--copyable-text-background-color); + border-radius: 0.4rem; + color: var(--text-color); + font-size: var(--font-size-normal); + line-height: var(--spacing-2); +} + +.details { + margin: 20px 0 0 0; +} + +@media screen and (max-width: 768px) { + .modal { + width: 355px; + padding: 30px 20px; + } + .title { + font-size: 24px; + } + .dataGrid { + grid-template-columns: 2fr; + } + .dataGrid label { + grid-column: 1; + } + .amount { + grid-column: 2; + grid-row: 1; + } + .status { + grid-column: 2; + grid-row: 2; + } + .date { + grid-column: 2; + grid-row: 3; + } +} diff --git a/app/components/modals/LNPaymentModal/helpers.js b/app/components/modals/LNPaymentModal/helpers.js new file mode 100644 index 0000000000..9bbaff5907 --- /dev/null +++ b/app/components/modals/LNPaymentModal/helpers.js @@ -0,0 +1,78 @@ +import { FormattedMessage as T } from "react-intl"; +import { Balance } from "shared"; +export const getPaymentDetails = (payment) => { + const details = [ + { + label: , + value: payment?.paymentHash, + copyable: true, + truncate: 40 + } + ]; + + 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: ( + <> + {index} + + ), + value: [ + { + label: , + value: htlc.status + }, + { + label: , + value: + }, + { + label: , + value: + } + ] + }; + // hopList + htlc.route?.hopsList?.forEach((hop, hopIndex) => { + const hopResponse = { + label: ( + <> + {hopIndex} + + ), + value: [ + { + label: , + value: + }, + { + label: , + value: hop.pubKey, + copyable: true, + truncate: 20 + } + ] + }; + response.value.push(hopResponse); + }); + + details.push(response); + }); + + return details; +}; diff --git a/app/components/modals/LNPaymentModal/index.js b/app/components/modals/LNPaymentModal/index.js new file mode 100644 index 0000000000..d725428c9a --- /dev/null +++ b/app/components/modals/LNPaymentModal/index.js @@ -0,0 +1 @@ +export { default } from "./LNPaymentModal"; diff --git a/app/components/modals/index.js b/app/components/modals/index.js index 10f1935c62..8848674b47 100644 --- a/app/components/modals/index.js +++ b/app/components/modals/index.js @@ -21,3 +21,4 @@ export { default as SetNewPassphraseModal } from "./SetNewPassphraseModal/SetNew export { default as AppPassAndPassphraseModal } from "./AppPassAndPassphraseModal/AppPassAndPassphraseModal"; export { default as ConfirmationDialogModal } from "./ConfirmationDialogModal"; export { default as LNInvoiceModal } from "./LNInvoiceModal"; +export { default as LNPaymentModal } from "./LNPaymentModal"; diff --git a/app/components/shared/CopyToClipboard/CopyToClipboard.module.css b/app/components/shared/CopyToClipboard/CopyToClipboard.module.css index d621ed168e..0e792da009 100644 --- a/app/components/shared/CopyToClipboard/CopyToClipboard.module.css +++ b/app/components/shared/CopyToClipboard/CopyToClipboard.module.css @@ -20,6 +20,7 @@ } .success { font-size: 12px; + line-height: 16px; color: var(--background-back-color); background-color: var(--accent-color); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); diff --git a/app/components/shared/CopyableText/CopyableText.jsx b/app/components/shared/CopyableText/CopyableText.jsx new file mode 100644 index 0000000000..c205cc17fa --- /dev/null +++ b/app/components/shared/CopyableText/CopyableText.jsx @@ -0,0 +1,11 @@ +import styles from "./CopyableText.module.css"; +import { CopyableText as PiUiCopyableText, classNames } from "pi-ui"; + +const CopyableText = (props) => ( + +); + +export default CopyableText; diff --git a/app/components/shared/CopyableText/CopyableText.module.css b/app/components/shared/CopyableText/CopyableText.module.css new file mode 100644 index 0000000000..d36bbb50dd --- /dev/null +++ b/app/components/shared/CopyableText/CopyableText.module.css @@ -0,0 +1,3 @@ +.copyableText > span { + padding: 10px; +} diff --git a/app/components/shared/CopyableText/index.js b/app/components/shared/CopyableText/index.js new file mode 100644 index 0000000000..8297c7e493 --- /dev/null +++ b/app/components/shared/CopyableText/index.js @@ -0,0 +1 @@ +export { default } from "./CopyableText"; diff --git a/app/components/shared/DetailsTable/DetailsTable.jsx b/app/components/shared/DetailsTable/DetailsTable.jsx index e9586d205e..7ab75082f8 100644 --- a/app/components/shared/DetailsTable/DetailsTable.jsx +++ b/app/components/shared/DetailsTable/DetailsTable.jsx @@ -1,6 +1,50 @@ -import { useState, Fragment } from "react"; +import { useState } from "react"; import { classNames } from "pi-ui"; import styles from "./DetailsTable.module.css"; +import { SmallButton } from "buttons"; +import { CopyToClipboard, TruncatedText } from "shared"; + +const ValueField = ({ data }) => { + const { value, copyable, truncate } = data; + const truncatedText = truncate ? ( + + ) : ( + value + ); + return ( +
+ {copyable ? ( + <> +
{truncatedText}
+ + + ) : ( + truncatedText + )} +
+ ); +}; + +const SubTable = ({ data }) => ( + <> + +
+ {data.value.map((node, subIndex) => ( + + ))} +
+ +); + +const Row = ({ data }) => + !Array.isArray(data.value) ? ( + <> + + + + ) : ( + + ); const DetailsTable = ({ data, @@ -26,31 +70,9 @@ const DetailsTable = ({
{showDetails && (
- {data?.map(({ label, value }, index) => { - return !Array.isArray(value) ? ( - - -
{value}
-
- ) : ( - - -
- {value.map( - ( - { label: secondaryLabel, value: secondaryValue }, - secondaryIndex - ) => ( - - -
{secondaryValue}
-
- ) - )} -
-
- ); - })} + {data?.map((node, index) => ( + + ))}
)} diff --git a/app/components/shared/DetailsTable/DetailsTable.module.css b/app/components/shared/DetailsTable/DetailsTable.module.css index 7ef483c1ab..dfd815de63 100644 --- a/app/components/shared/DetailsTable/DetailsTable.module.css +++ b/app/components/shared/DetailsTable/DetailsTable.module.css @@ -38,23 +38,27 @@ .grid label { border-bottom: 1px solid var(--grey-2); - padding: 0 0 0 25px; - height: 40px; + padding: 10px 0 10px 25px; color: var(--grey-6); - display: flex; - align-items: center; } .grid .value { border-bottom: 1px solid var(--grey-2); color: var(--main-dark-blue); - height: 40px; + padding: 10px 0; +} + +.grid .value.copyable { + padding: 0; display: flex; align-items: center; + line-height: 16px; } -.grid .value > div { + +.grid .value.copyable .copyableText { + display: flex; + align-items: center; margin-right: 10px; - display: inline; } .secondaryGrid { @@ -68,8 +72,8 @@ border: none; } -.grid .secondaryGrid label:first-of-type, -.grid .secondaryGrid .value:first-of-type { +.grid .secondaryGrid:first-of-type label:first-of-type, +.grid .secondaryGrid:first-of-type .value:first-of-type { padding-top: 10px; } diff --git a/app/components/shared/LNPaymentStatus.jsx b/app/components/shared/LNPaymentStatus.jsx new file mode 100644 index 0000000000..5a941360ea --- /dev/null +++ b/app/components/shared/LNPaymentStatus.jsx @@ -0,0 +1,38 @@ +import { StatusTag } from "pi-ui"; +import { defineMessages } from "react-intl"; +import { useIntl } from "react-intl"; +import { PAYMENT_STATUS_PENDING, PAYMENT_STATUS_FAILED } from "constants"; + +const messages = defineMessages({ + confirmed: { + id: "ln.LNPaymentStatus.confirmed", + defaultMessage: "Confirmed" + }, + failed: { + id: "ln.LNPaymentStatus.failed", + defaultMessage: "Failed" + }, + pending: { + id: "ln.LNPaymentStatus.pending", + defaultMessage: "Pending" + } +}); + +const LNPaymentStatus = ({ status }) => { + const intl = useIntl(); + return status === PAYMENT_STATUS_PENDING ? ( + + ) : status === PAYMENT_STATUS_FAILED ? ( + + ) : ( + + ); +}; + +export default LNPaymentStatus; diff --git a/app/components/shared/index.js b/app/components/shared/index.js index 9bc306f375..48077a4fb1 100644 --- a/app/components/shared/index.js +++ b/app/components/shared/index.js @@ -32,5 +32,7 @@ export { default as TicketAutoBuyerForm } from "./TicketAutoBuyerForm"; export { default as PurchaseTicketsForm } from "./PurchaseTicketsForm"; export { default as AnimatedContainer } from "./AnimatedContainer"; export { default as LNInvoiceStatus } from "./LNInvoiceStatus"; +export { default as LNPaymentStatus } from "./LNPaymentStatus"; export { default as DetailsTable } from "./DetailsTable"; export { default as TruncatedText } from "./TruncatedText"; +export { default as CopyableText } from "./CopyableText"; diff --git a/app/components/views/LNPage/LNPage.jsx b/app/components/views/LNPage/LNPage.jsx index 0f47961426..630d64ae0e 100644 --- a/app/components/views/LNPage/LNPage.jsx +++ b/app/components/views/LNPage/LNPage.jsx @@ -5,7 +5,7 @@ import { ConnectPage } from "./ConnectPage"; import WalletTab, { WalletTabHeader } from "./WalletTab/WalletTab"; import ChannelsTab, { ChannelsTabHeader } from "./ChannelsTab/ChannelsTab"; import { ReceiveTab, ReceiveTabHeader } from "./ReceiveTab"; -import PaymentsTab, { PaymentsTabHeader } from "./PaymentsTab/PaymentsTab"; +import { SendTab, SendTabHeader } from "./SendTab"; import WatchtowersTab, { WatchtowersTabHeader } from "./WatchtowersTab/WatchtowersTab"; @@ -37,18 +37,18 @@ const LNActivePage = () => ( header={ChannelsTabHeader} link={} /> + } + /> } /> - } - /> ( -
- {decoded.numAtoms ? ( -
- -
- ) : ( - - )} - {decoded.description ? ( -
{decoded.description}
- ) : ( - - )} - -
- - - {decoded.destination} - - -
{decoded.paymentHash}
-
-
-); - -export default DecodedPayRequest; diff --git a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/DecodedPayRequest.module.css b/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/DecodedPayRequest.module.css deleted file mode 100644 index 24e782866e..0000000000 --- a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/DecodedPayRequest.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.decodedPayreq { - margin-top: 2em; - margin-bottom: 2em; - display: grid; - grid-template-columns: 1fr 2fr 1fr; - grid-column-gap: 10px; -} - -.numAtoms { - font-size: 32px; - align-self: center; -} - -.description { - max-width: 100%; - text-overflow: ellipsis; - overflow: hidden; -} - -.destDetails { - grid-column: 1 / 4; - display: grid; - grid-template-columns: 1fr 4fr; - margin-top: 1em; - color: var(--stroke-color-hovered); -} - -.copyableText span { - font-size: 14px; - background: none; - padding: 0; -} diff --git a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/EmptyDescription/EmptyDescription.jsx b/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/EmptyDescription/EmptyDescription.jsx deleted file mode 100644 index 2b5b66b109..0000000000 --- a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/EmptyDescription/EmptyDescription.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; - -const EmptyDescription = () => ( -
- -
-); - -export default EmptyDescription; diff --git a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.jsx b/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.jsx deleted file mode 100644 index a4f1d86f34..0000000000 --- a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { FormattedRelative } from "shared"; -import styles from "./ExpiryTime.module.css"; - -const ExpiryTime = ({ expired, decoded, tsDate }) => ( -
- {expired ? ( - - ) - }} - /> - ) : ( - - ) - }} - /> - )} -
-); - -export default ExpiryTime; diff --git a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.module.css b/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.module.css deleted file mode 100644 index 82bc8310b7..0000000000 --- a/app/components/views/LNPage/PaymentsTab/DecodedPayRequest/ExpiryTime/ExpiryTime.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.expiry { - align-self: center; -} diff --git a/app/components/views/LNPage/PaymentsTab/FailedPayment/FailedPayment.jsx b/app/components/views/LNPage/PaymentsTab/FailedPayment/FailedPayment.jsx deleted file mode 100644 index 1865f05a33..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/FailedPayment/FailedPayment.module.css b/app/components/views/LNPage/PaymentsTab/FailedPayment/FailedPayment.module.css deleted file mode 100644 index 172aabf356..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/OutstandingPayment/OutstandingPayment.jsx b/app/components/views/LNPage/PaymentsTab/OutstandingPayment/OutstandingPayment.jsx deleted file mode 100644 index 5edf0b911a..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/OutstandingPayment/OutstandingPayment.module.css b/app/components/views/LNPage/PaymentsTab/OutstandingPayment/OutstandingPayment.module.css deleted file mode 100644 index 94d8750f50..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/Payment/Payment.jsx b/app/components/views/LNPage/PaymentsTab/Payment/Payment.jsx deleted file mode 100644 index 90ef26cce4..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/Payment/Payment.module.css b/app/components/views/LNPage/PaymentsTab/Payment/Payment.module.css deleted file mode 100644 index 94d8750f50..0000000000 --- a/app/components/views/LNPage/PaymentsTab/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/PaymentsTab/PaymentsTab.jsx b/app/components/views/LNPage/PaymentsTab/PaymentsTab.jsx deleted file mode 100644 index 1a49c52e0c..0000000000 --- a/app/components/views/LNPage/PaymentsTab/PaymentsTab.jsx +++ /dev/null @@ -1,161 +0,0 @@ -import { usePaymentsTab } from "./hooks"; -import { FormattedMessage as T } from "react-intl"; -import { KeyBlueButton } from "buttons"; -import { TextInput } from "inputs"; -import styles from "./PaymentsTab.module.css"; -import { Subtitle, Balance, VerticalAccordion } from "shared"; -import { DescriptionHeader } from "layout"; -import ReactTimeout from "react-timeout"; -import BalanceHeader from "../BalanceHeader/BalanceHeader"; -import DecodedPayRequest from "./DecodedPayRequest/DecodedPayRequest"; -import OutstandingPayment from "./OutstandingPayment/OutstandingPayment"; -import FailedPayment from "./FailedPayment/FailedPayment"; -import Payment from "./Payment/Payment"; - -export const PaymentsTabHeader = () => ( - - } - /> -); - -const PaymentsTab = ({ setTimeout }) => { - const { - payments, - outstandingPayments, - failedPayments, - tsDate, - payRequest, - decodedPayRequest, - decodingError, - expired, - sendValue, - onPayRequestChanged, - onSendPayment, - onSendValueChanged, - isShowingDetails, - selectedPaymentDetails, - onToggleShowDetails, - channelBalances - } = usePaymentsTab(setTimeout); - - return ( - <> - } /> - - } - /> -
-
- - -
- {!!decodingError && ( -
{decodingError || ""}
- )} - {!!decodedPayRequest && ( - <> - - - - - - )} -
- {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} - - ))} -
-
- ))} -
-
- ))} -
- - ); -}; - -export default ReactTimeout(PaymentsTab); diff --git a/app/components/views/LNPage/PaymentsTab/PaymentsTab.module.css b/app/components/views/LNPage/PaymentsTab/PaymentsTab.module.css deleted file mode 100644 index 191635d071..0000000000 --- a/app/components/views/LNPage/PaymentsTab/PaymentsTab.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.lnSendPayment { - background-color: var(--background-back-color); - padding: 30px 40px; - margin-bottom: 4em; - min-height: 18em; -} - -.decodingError { - margin-top: 2em; - 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; -} diff --git a/app/components/views/LNPage/PaymentsTab/hooks.js b/app/components/views/LNPage/PaymentsTab/hooks.js deleted file mode 100644 index ecd1fd7f96..0000000000 --- a/app/components/views/LNPage/PaymentsTab/hooks.js +++ /dev/null @@ -1,104 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useLNPage } from "../hooks"; - -export function usePaymentsTab(setTimeout) { - const [sendValueAtom, setSendValueAtom] = useState(0); - const [payRequest, setPayRequest] = useState(""); - const [decodedPayRequest, setDecodedPayRequest] = useState(null); - 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 { - payments, - outstandingPayments, - failedPayments, - tsDate, - channelBalances, - decodePayRequest, - sendPayment - } = useLNPage(); - - const onToggleShowDetails = useCallback( - (paymentHash) => { - setSelectedPaymentDetails(paymentHash); - setIsShowingDetails(!isShowingDetails); - }, - [isShowingDetails] - ); - - const checkExpired = useCallback(() => { - if (!decodedPayRequest) return; - const timeToExpire = - (decodedPayRequest.timestamp + decodedPayRequest.expiry) * 1000 - - Date.now(); - if (timeToExpire < 0) { - setExpired(true); - } - }, [decodedPayRequest]); - - useEffect(() => { - if (!payRequest) { - setDecodingError(null); - setDecodedPayRequest(null); - return; - } - decodePayRequest(payRequest) - .then((resp) => { - const timeToExpire = (resp.timestamp + resp.expiry) * 1000 - Date.now(); - const expired = timeToExpire < 0; - if (!expired) { - setTimeout(checkExpired, timeToExpire + 1000); - } - setDecodedPayRequest(resp); - setDecodingError(null); - setExpired(expired); - }) - .catch((error) => { - setDecodedPayRequest(null); - setDecodingError(String(error)); - }); - }, [payRequest, decodePayRequest, checkExpired, setTimeout]); - - const onPayRequestChanged = (e) => { - setPayRequest((e.target.value || "").trim()); - setDecodedPayRequest(null); - setExpired(false); - }; - - const onSendValueChanged = ({ atomValue }) => { - setSendValueAtom(atomValue); - }; - - const onSendPayment = () => { - if (!payRequest || !decodedPayRequest) { - return; - } - setPayRequest(""); - setDecodedPayRequest(null); - setSendValue(0); - sendPayment(payRequest, sendValueAtom); - }; - - return { - payments, - outstandingPayments, - failedPayments, - tsDate, - payRequest, - decodedPayRequest, - decodingError, - expired, - sending, - sendValueAtom, - onPayRequestChanged, - onSendPayment, - onSendValueChanged, - isShowingDetails, - selectedPaymentDetails, - onToggleShowDetails, - channelBalances - }; -} diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx new file mode 100644 index 0000000000..6c50907166 --- /dev/null +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.jsx @@ -0,0 +1,118 @@ +import { FormattedMessage as T } from "react-intl"; +import { + Balance, + TruncatedText, + CopyToClipboard, + FormattedRelative, + DetailsTable +} from "shared"; +import { DcrInput } from "inputs"; +import { SmallButton } from "buttons"; +import styles from "./DecodedPayRequest.module.css"; +import { getDecodedPayRequestDetails } from "./helpers"; +import { classNames } from "pi-ui"; + +const DecodedPayRequest = ({ + decoded, + expired, + tsDate, + sendValue, + onSendValueChanged +}) => ( +
+
+
+ + {decoded.numAtoms ? ( + + ) : ( + + )} +
+
+
+ +
+
+ +
+ +
+
+
+ + {expired ? ( + + ) + }} + /> + ) : ( + + ) + }} + /> + )} +
+
+
+ + {decoded.description ? ( +
{decoded.description}
+ ) : ( + + )} +
+
+ +
+ {decoded.paymentHash} + +
+
+ + } + expandable + /> +
+); + +export default DecodedPayRequest; diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css new file mode 100644 index 0000000000..46d13348c1 --- /dev/null +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/DecodedPayRequest.module.css @@ -0,0 +1,83 @@ +.decodedPayreq { + margin-top: 23px; + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; +} + +.propContainer { + display: flex; + flex-direction: column; +} + +.amount { + font-size: 20px; + line-height: 30px; + color: var(--main-dark-blue); +} + +.destination { + font-size: 20px; + line-height: 16px; + color: var(--main-dark-blue); + display: flex; + align-items: center; +} + +.decodedPayreq .propContainer label { + font-size: 13px; + font-weight: 400; + line-height: 16px; + color: var(--grey-7); + display: block; + margin-bottom: 5px; +} + +.arrow { + background-image: var(--right-arrow); + background-repeat: no-repeat; + width: 43px; + height: 35px; + margin: 0 50px; +} + +.destinationText { + margin-right: 10px; +} + +.expiryContainer { + margin-left: 45px; + padding-left: 40px; + border-left: 1px solid var(--grey-3); +} + +.details { + margin-top: 20px; +} + +.paymentHashWrapper { + display: flex; + flex-direction: row; + align-items: center; +} + +.paymentHash { + padding-right: 10px; +} + +.descriptionContainer { + margin-top: 30px; +} + +.paymentHashContainer { + margin-top: 20px; +} + +.descriptionContainer label, +.paymentHashContainer label { + margin-bottom: 10px; +} diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/helpers.js b/app/components/views/LNPage/SendTab/DecodedPayRequest/helpers.js new file mode 100644 index 0000000000..a4a9fcb367 --- /dev/null +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/helpers.js @@ -0,0 +1,29 @@ +import { FormattedMessage as T } from "react-intl"; +export const getDecodedPayRequestDetails = (decoded) => { + const details = [ + { + label: , + value: decoded.cltvExpiry + }, + { + label: ( + + ), + value: decoded.fallbackAddr || ( + + ) + }, + { + label: ( + + ), + value: decoded.paymentAddrHex, + truncate: 40 + } + ]; + + return details; +}; diff --git a/app/components/views/LNPage/SendTab/DecodedPayRequest/index.js b/app/components/views/LNPage/SendTab/DecodedPayRequest/index.js new file mode 100644 index 0000000000..6fa6ec7fd5 --- /dev/null +++ b/app/components/views/LNPage/SendTab/DecodedPayRequest/index.js @@ -0,0 +1 @@ +export { default } from "./DecodedPayRequest"; diff --git a/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.jsx b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.jsx new file mode 100644 index 0000000000..62db702cfe --- /dev/null +++ b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.jsx @@ -0,0 +1,39 @@ +import { FormattedMessage as T } from "react-intl"; +import { Balance, LNPaymentStatus, TruncatedText } from "shared"; +import styles from "./PaymentRow.module.css"; + +const PaymentRow = ({ payment, tsDate, onClick }) => ( +
+
+
+ + ) + }} + /> +
+ +
+
+
+
+ +
+
+ +
+
+); + +export default PaymentRow; diff --git a/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css new file mode 100644 index 0000000000..24629438d3 --- /dev/null +++ b/app/components/views/LNPage/SendTab/PaymentRow/PaymentRow.module.css @@ -0,0 +1,60 @@ +.payment { + display: grid; + background-color: var(--background-back-color); + padding: 15px 50px 13px 0; + grid-template-columns: 422px auto max-content; + min-height: 63px; + align-items: center; + background-image: var(--arrow-right-gray-icon); + background-position: right 26px center; + background-size: 5px; + background-repeat: no-repeat; + border-bottom: 1px solid var(--disabled-background-color); + cursor: pointer; +} + +.payment:hover { + background-image: var(--arrow-right-blue-icon); + background-color: var(--tx-history-background-hover); +} + +.value { + padding-left: 57px; + background-image: var(--minus-small-icon); + background-size: 22px 22px; + background-position: 20px center; + background-repeat: no-repeat; + font-size: 16px; + line-height: 22px; + color: var(--main-dark-blue); +} + +.value .balance { + display: inline; +} + +.value .paymentHash { + font-size: 13px; + line-height: 16px; + color: var(--grey-5); +} + +.date { + font-size: 13px; + line-height: 16px; + text-align: right; + color: var(--main-dark-blue); +} + +@media screen and (max-width: 768px) { + .value .paymentHash { + max-width: 242px; + } + .payment { + grid-template-columns: 1fr; + grid-row-gap: 5px; + } + .status { + margin-left: 55px; + } +} diff --git a/app/components/views/LNPage/SendTab/PaymentRow/index.js b/app/components/views/LNPage/SendTab/PaymentRow/index.js new file mode 100644 index 0000000000..784ea07edf --- /dev/null +++ b/app/components/views/LNPage/SendTab/PaymentRow/index.js @@ -0,0 +1 @@ +export { default } from "./PaymentRow"; diff --git a/app/components/views/LNPage/SendTab/SendTab.jsx b/app/components/views/LNPage/SendTab/SendTab.jsx new file mode 100644 index 0000000000..1b61a8d967 --- /dev/null +++ b/app/components/views/LNPage/SendTab/SendTab.jsx @@ -0,0 +1,245 @@ +import { useSendTab } from "./hooks"; +import { FormattedMessage as T, defineMessages } from "react-intl"; +import { KeyBlueButton, EyeFilterMenu } from "buttons"; +import { TextInput } from "inputs"; +import styles from "./SendTab.module.css"; +import { Subtitle } from "shared"; +import { DescriptionHeader } from "layout"; +import ReactTimeout from "react-timeout"; +import DecodedPayRequest from "./DecodedPayRequest"; +import BalancesHeader from "../BalancesHeader"; +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: { + id: "ln.paymentsTab.payReq", + defaultMessage: "Lightning Payment Request Code" + }, + payReqInputPlaceholder: { + id: "ln.paymentsTab.payReqPlaceholder", + defaultMessage: "Request Code from an invoice" + }, + payReqDecodeSuccessMsg: { + id: "ln.paymentsTab.payReqDecodeSuccessMsg", + defaultMessage: "Valid Lightning Request" + }, + filterByHashPlaceholder: { + id: "ln.paymentsTab.filterByHashPlaceholder", + defaultMessage: "Filter by Payment Hash" + }, + expiredErrorMsg: { + id: "ln.paymentsTab.expiredErrorMsg", + defaultMessage: "Invoice expired" + } +}); + +export const SendTabHeader = () => ( + + + + + } + /> +); + +const subtitleMenu = ({ + sortTypes, + paymentTypes, + listDirection, + selectedPaymentType, + searchText, + intl, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText +}) => ( +
+
+ onChangeSearchText(e.target.value)} + /> +
+ }> + + + }> + + +
+); + +const SendTab = ({ setTimeout }) => { + const { + payments, + tsDate, + payRequest, + decodedPayRequest, + decodingError, + expired, + sendValue, + onPayRequestChanged, + onSendPayment, + onSendValueChanged, + selectedPayment, + setSelectedPayment, + intl, + searchText, + listDirection, + selectedPaymentType, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText + } = useSendTab(setTimeout); + + return ( +
+ } /> +
+ onPayRequestChanged(e.target.value)} + successMessage={intl.formatMessage(messages.payReqDecodeSuccessMsg)} + showSuccess={!!decodedPayRequest && !expired} + showErrors={!!decodingError || expired} + invalid={!!decodingError || expired} + invalidMessage={ + decodingError + ? decodingError + : expired + ? intl.formatMessage(messages.expiredErrorMsg) + : null + }> + {!payRequest ? ( + + ) : ( + + )} + + {!!decodedPayRequest && ( + <> + + + )} +
+ {!!decodedPayRequest && !expired && ( +
+ + + +
+ )} + + } + children={subtitleMenu({ + sortTypes: getSortTypes(), + paymentTypes: getPaymentTypes(), + listDirection, + selectedPaymentType, + searchText, + intl, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText + })} + /> + {payments && payments.length > 0 ? ( +
+ {payments.map((payment) => ( + setSelectedPayment(payment)} + /> + ))} +
+ ) : ( +
+ +
+ )} + {selectedPayment && ( + setSelectedPayment(null)} + payment={selectedPayment} + tsDate={tsDate} + /> + )} +
+ ); +}; + +export default ReactTimeout(SendTab); diff --git a/app/components/views/LNPage/SendTab/SendTab.module.css b/app/components/views/LNPage/SendTab/SendTab.module.css new file mode 100644 index 0000000000..474e3c691c --- /dev/null +++ b/app/components/views/LNPage/SendTab/SendTab.module.css @@ -0,0 +1,96 @@ +.container { + width: 764px; +} + +.lnSendPayment { + background-color: var(--background-back-color); + padding: 30px 40px; + margin-bottom: 20px; +} + +.decodingError { + margin-top: 2em; + color: var(--error-red); +} + +.addressInput { + padding-right: 22px; +} +.addressInput.error { + padding-right: 39px; + color: var(--color-orange); +} +.addressInput.success { + padding-right: 39px; +} +.pasteButton { + font-family: var(--font-family-regular); + padding: 3px 10px !important; + font-size: 13px !important; + line-height: 16px !important; + transition: none !important; +} +.clearAddressButton { + width: 26px; + background-image: var(--indicator-invalid-icon); + height: 16px; + border: none !important; + padding: 0 !important; + transition: none !important; + background-size: 21px; + background-repeat: no-repeat; + background-position: center; + background-color: var(--background-back-color) !important; +} +.clearAddressButton:hover { + opacity: 0.85; +} + +.buttonContainer { + text-align: right; +} + +.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) { + .container { + width: 355px; + } + .lnSendPayment { + padding: 30px 20px; + } + .arrow { + margin: 0 36px; + } +} 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 new file mode 100644 index 0000000000..b065511224 --- /dev/null +++ b/app/components/views/LNPage/SendTab/hooks.js @@ -0,0 +1,228 @@ +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); + const [payRequest, setPayRequest] = useState(""); + const [decodedPayRequest, setDecodedPayRequest] = useState(null); + const [decodingError, setDecodingError] = useState(null); + const [expired, setExpired] = useState(false); + const [sending, setSendValue] = useState(); + const [selectedPayment, setSelectedPayment] = useState(null); + const paymentFilter = useSelector(sel.lnPaymentFilter); + const intl = useIntl(); + + const { + payments, + outstandingPayments, + failedPayments, + tsDate, + channelBalances, + decodePayRequest, + sendPayment + } = useLNPage(); + + 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; + const timeToExpire = + (decodedPayRequest.timestamp + decodedPayRequest.expiry) * 1000 - + Date.now(); + if (timeToExpire < 0) { + setExpired(true); + } + }, [decodedPayRequest]); + + useEffect(() => { + if (!payRequest) { + setDecodingError(null); + setDecodedPayRequest(null); + return; + } + decodePayRequest(payRequest) + .then((resp) => { + const timeToExpire = (resp.timestamp + resp.expiry) * 1000 - Date.now(); + const expired = timeToExpire < 0; + if (!expired) { + setTimeout(checkExpired, timeToExpire + 1000); + } + resp.paymentAddrHex = Buffer.from(resp.paymentAddr, "base64").toString( + "hex" + ); + setDecodedPayRequest(resp); + setDecodingError(null); + setExpired(expired); + }) + .catch((error) => { + setDecodedPayRequest(null); + setDecodingError(String(error)); + }); + }, [payRequest, decodePayRequest, checkExpired, setTimeout]); + + const onPayRequestChanged = (payRequest) => { + setPayRequest(payRequest.trim()); + setDecodedPayRequest(null); + setExpired(false); + }; + + const onSendValueChanged = ({ atomValue }) => { + setSendValueAtom(atomValue); + }; + + const onSendPayment = () => { + if (!payRequest || !decodedPayRequest) { + return; + } + setPayRequest(""); + setDecodedPayRequest(null); + setSendValue(0); + 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: getPayments(), + tsDate, + payRequest, + decodedPayRequest, + decodingError, + expired, + sending, + sendValueAtom, + onPayRequestChanged, + onSendPayment, + onSendValueChanged, + intl, + selectedPayment, + setSelectedPayment, + channelBalances, + searchText, + listDirection, + selectedPaymentType, + onChangeSelectedType, + onChangeSortType, + onChangeSearchText, + onChangePaymentFilter + }; +} diff --git a/app/components/views/LNPage/SendTab/index.js b/app/components/views/LNPage/SendTab/index.js new file mode 100644 index 0000000000..f8ecadcd28 --- /dev/null +++ b/app/components/views/LNPage/SendTab/index.js @@ -0,0 +1 @@ +export { default as SendTab, SendTabHeader } from "./SendTab"; diff --git a/app/constants/decrediton.js b/app/constants/decrediton.js index 5aed8e2f91..46321bb2ca 100644 --- a/app/constants/decrediton.js +++ b/app/constants/decrediton.js @@ -151,3 +151,8 @@ export const INVOICE_STATUS_OPEN = "open"; export const INVOICE_STATUS_SETTLED = "settled"; export const INVOICE_STATUS_EXPIRED = "expired"; export const INVOICE_STATUS_CANCELED = "canceled"; + +// ln payment status +export const PAYMENT_STATUS_CONFIRMED = "confirmed"; +export const PAYMENT_STATUS_FAILED = "failed"; +export const PAYMENT_STATUS_PENDING = "pending"; 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/app/style/icons/rightArrow.svg b/app/style/icons/rightArrow.svg new file mode 100644 index 0000000000..4fbfc1b86f --- /dev/null +++ b/app/style/icons/rightArrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/style/icons/rightArrowDark.svg b/app/style/icons/rightArrowDark.svg new file mode 100644 index 0000000000..0c9bad474f --- /dev/null +++ b/app/style/icons/rightArrowDark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/style/themes/darkTheme.js b/app/style/themes/darkTheme.js index ad6d1d1709..7f6bab55a3 100644 --- a/app/style/themes/darkTheme.js +++ b/app/style/themes/darkTheme.js @@ -317,7 +317,8 @@ const darkTheme = { ), "self-transaction-icon": url(require("style/icons/sentToSelfTxDark.svg")), "proposals-refresh-icon": url(require("style/icons/menuMixerDark.svg")), - "ln-invoice-icon": url(require("style/icons/lnInvoiceIcon.svg")) + "ln-invoice-icon": url(require("style/icons/lnInvoiceIcon.svg")), + "right-arrow": url(require("style/icons/rightArrowDark.svg")) }; export default darkTheme; diff --git a/app/style/themes/lightTheme.js b/app/style/themes/lightTheme.js index f8805cefd8..82ecdc5690 100644 --- a/app/style/themes/lightTheme.js +++ b/app/style/themes/lightTheme.js @@ -305,7 +305,8 @@ const lightTheme = { ), "self-transaction-icon": url(require("style/icons/sentToSelfTx.svg")), "proposals-refresh-icon": url(require("style/icons/menuMixer.svg")), - "ln-invoice-icon": url(require("style/icons/lnInvoiceIcon.svg")) + "ln-invoice-icon": url(require("style/icons/lnInvoiceIcon.svg")), + "right-arrow": url(require("style/icons/rightArrow.svg")) }; export default lightTheme; diff --git a/test/unit/components/shared/DetailsTable.spec.js b/test/unit/components/shared/DetailsTable.spec.js index 9eaa4dbf10..76ecfb0460 100644 --- a/test/unit/components/shared/DetailsTable.spec.js +++ b/test/unit/components/shared/DetailsTable.spec.js @@ -13,7 +13,7 @@ const mockData = [ value:
value-1
}, { - label: "label-for-secondary-grid", + label: "label-for-secondary-grid-0", value: [ { label: "label-sec-0", @@ -24,6 +24,28 @@ const mockData = [ value: "value-sec-1" } ] + }, + { + label: "label-for-secondary-grid-1", + value: [ + { + label: "label-sec-11", + value: "value-sec-11" + }, + { + label: "label-for-secondary-grid-12", + value: [ + { + label: "label-sec-121", + value: "value-sec-121" + }, + { + label: "label-sec-122", + value: "value-sec-122" + } + ] + } + ] } ]; const mockTitle = "mock-title"; @@ -72,6 +94,21 @@ test("check expandable table", () => { expect(screen.getByText(mockData[2].value[0].value)).toBeInTheDocument(); expect(screen.getByText(mockData[2].value[1].value)).toBeInTheDocument(); + // secondary grid in a secondary grid + expect(screen.getByText(`${mockData[3].label}:`)).toBeInTheDocument(); + expect( + screen.getByText(`${mockData[3].value[0].label}:`) + ).toBeInTheDocument(); + expect( + screen.getByText(`${mockData[3].value[1].label}:`) + ).toBeInTheDocument(); + expect( + screen.getByText(mockData[3].value[1].value[0].value) + ).toBeInTheDocument(); + expect( + screen.getByText(mockData[3].value[1].value[1].value) + ).toBeInTheDocument(); + //close details user.click(title); expect(screen.queryByText(`${mockData[0].label}:`)).not.toBeInTheDocument(); diff --git a/test/unit/components/views/LNPage/SendTab.spec.js b/test/unit/components/views/LNPage/SendTab.spec.js new file mode 100644 index 0000000000..d08fc44fd9 --- /dev/null +++ b/test/unit/components/views/LNPage/SendTab.spec.js @@ -0,0 +1,451 @@ +import { SendTab } from "components/views/LNPage/SendTab"; +import { render } from "test-utils.js"; +import user from "@testing-library/user-event"; +import { screen, wait } from "@testing-library/react"; +import { DCR } from "constants"; +import * as sel from "selectors"; +import * as lna from "actions/LNActions"; +import * as wl from "wallet"; + +const selectors = sel; +const lnActions = lna; +const wallet = wl; + +const mockLnChannelBalance = { + balance: 99997360, + pendingOpenBalance: 0, + maxInboundAmount: 97999000, + maxOutboundAmount: 97997360 +}; + +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", + value: 20000000, + creationDate: 1627810765, + fee: 0, + paymentPreimage: "mock-preimage-0", + valueAtoms: 20000000, + valueMAtoms: 20000000000, + paymentRequest: "mock-payment-request-0", + status: 2, + feeAtoms: 0, + feeMAtoms: 0, + creationTimeNs: 1627810765912116500, + htlcsList: [ + { + status: 1, + route: { + totalTimeLock: 738888, + totalFees: 0, + totalAmt: 20000000, + hopsList: [ + { + chanId: "810928308391837696", + chanCapacity: 200000000, + amtToForward: 20000000, + fee: 0, + expiry: 738888, + amtToForwardMAtoms: 20000000000, + feeMAtoms: 0, + pubKey: "mock-pubkey-0", + tlvPayload: true, + mppRecord: { + paymentAddr: "mock-payment-address-0", + totalAmtMAtoms: 20000000000 + }, + customRecordsMap: [] + } + ], + totalFeesMAtoms: 0, + totalAmtMAtoms: 20000000000 + }, + attemptTimeNs: 1627810765956084200, + resolveTimeNs: 1627810766343210800, + preimage: "mock-preimage-htlc-0" + } + ], + paymentIndex: 4, + 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 now = Math.floor(Date.now() / 1000); +const mockValidDecodedPayRequest = { + destination: "mock-destination", + paymentHash: "mock-payment-hash", + numAtoms: 1000000, + timestamp: now, + expiry: 3600, + description: "mock-description", + descriptionHash: "", + fallbackAddr: "mock-fallbackAddr", + cltvExpiry: 80, + routeHintsList: [], + paymentAddr: "mock-payment-address", + paymentAddrHex: "9a8724fa96b299e9edfa16ac", + 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 mockExpiredDecodedPayRequest = { + ...mockValidDecodedPayRequest, + timestamp: now - 8000, + fallbackAddr: "" +}; + +let mockDecodePayRequest; +let mockSendPayment; + +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) + ); + mockSendPayment = lnActions.sendPayment = jest.fn(() => () => + Promise.resolve() + ); +}); + +const getReqCodeInput = () => + screen.getByLabelText("Lightning Payment Request Code"); +const getSendButton = () => screen.getByRole("button", { name: "Send" }); +const querySendButton = () => screen.queryByRole("button", { name: "Send" }); +const getPasteButton = () => screen.getByRole("button", { name: "Paste" }); +const getClearButton = () => + screen.getByRole("button", { name: "Clear Address" }); + +test("test send form with valid lightning request", async () => { + render(); + + const reqCodeInput = getReqCodeInput(); + user.type(reqCodeInput, mockReqCode); + await wait(() => + expect(mockDecodePayRequest).toHaveBeenCalledWith(mockReqCode) + ); + expect(screen.getByText("Valid Lightning Request")).toBeInTheDocument(); + expect(screen.getByText("Amount").parentNode.textContent).toMatch( + "Amount0.01000 DCR" + ); + expect(screen.getByText("Destination").parentNode.textContent).toMatch( + "Destinationmock-...ation" + ); + expect(screen.getByText("Expiration Time").parentNode.textContent).toMatch( + "Expiration TimeExpires in 1 hour" + ); + expect(screen.getByText("Description").parentNode.textContent).toMatch( + `Description${mockValidDecodedPayRequest.description}` + ); + expect(screen.getByText("Payment Hash").parentNode.textContent).toMatch( + `Payment Hash${mockValidDecodedPayRequest.paymentHash}` + ); + + expect(screen.queryByText("CLTV Expiry:")).not.toBeInTheDocument(); + // open details + const details = screen.getByText("Details"); + user.click(details); + + expect(screen.getByText("CLTV Expiry:").parentNode.textContent).toMatch( + `CLTV Expiry:${mockValidDecodedPayRequest.cltvExpiry}` + ); + expect(screen.getByText("Fallback Address:").parentNode.textContent).toMatch( + `Fallback Address:${mockValidDecodedPayRequest.fallbackAddr}` + ); + expect(screen.getByText("Payment Address:").parentNode.textContent).toMatch( + `Payment Address:${mockValidDecodedPayRequest.paymentAddrHex}` + ); + + // close details + user.click(details); + expect(screen.queryByText("CLTV Expiry:")).not.toBeInTheDocument(); + + user.click(getSendButton()); + expect(mockSendPayment).toHaveBeenCalledWith(mockReqCode, 0); +}); + +test("test send form with expired lightning request (with empty fallbackAddr)", async () => { + mockDecodePayRequest = lnActions.decodePayRequest = jest.fn(() => () => + Promise.resolve(mockExpiredDecodedPayRequest) + ); + render(); + + const reqCodeInput = getReqCodeInput(); + user.type(reqCodeInput, mockReqCode); + await wait(() => + expect(mockDecodePayRequest).toHaveBeenCalledWith(mockReqCode) + ); + expect(screen.getByText("Invoice expired")).toBeInTheDocument(); + expect(screen.getByText("Expiration Time").parentNode.textContent).toMatch( + "Expiration TimeExpired 1 hour ago" + ); + + expect(screen.queryByText("CLTV Expiry:")).not.toBeInTheDocument(); + // open details + const details = screen.getByText("Details"); + user.click(details); + + expect(screen.getByText("Fallback Address:").parentNode.textContent).toMatch( + "Fallback Address:(empty fallback address)" + ); + + // close details + user.click(details); + expect(screen.queryByText("CLTV Expiry:")).not.toBeInTheDocument(); + + expect(querySendButton()).not.toBeInTheDocument(); +}); + +test("test send form with invalid lightning request", async () => { + const mockErrorResp = "mock-error-resp"; + mockDecodePayRequest = lnActions.decodePayRequest = jest.fn(() => () => + Promise.reject(mockErrorResp) + ); + render(); + + const reqCodeInput = getReqCodeInput(); + user.type(reqCodeInput, mockReqCode); + await wait(() => + expect(mockDecodePayRequest).toHaveBeenCalledWith(mockReqCode) + ); + expect(screen.getByText(mockErrorResp)).toBeInTheDocument(); +}); + +test("test paste and clear button", async () => { + render(); + + const mockPastedPayReq = "mockPastedPayReq"; + wallet.readFromClipboard.mockImplementation(() => mockPastedPayReq); + + user.click(getPasteButton()); + await wait(() => expect(getReqCodeInput().value).toBe(mockPastedPayReq)); + + user.click(getClearButton()); + await wait(() => expect(getReqCodeInput().value).toBe("")); +}); + +test("test payment list and modal ", 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}` + ]); + + // 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(); +});