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();
+});