From efca53d0d86a94fc71ba85edc4d94c8bcb50f8ff Mon Sep 17 00:00:00 2001 From: escapedcat Date: Sat, 5 Oct 2024 17:55:09 +0200 Subject: [PATCH] refactor(home): modals (#777) * refactor(home): unlock modal * test(unlockModal): update test and cleanup * refactor(home): list-channel modal * refactor(home): list-channel modal - spinner and no channel info * refactor(home): open-channel modal * refactor(home): open-channel modal * refactor(home): send modal * refactor(home): receive modal * refactor(home): tx-details modal * test(modal): update test and remove close test functionality is covered by next-ui modal * chore: debugging * refactor(login): use new components * fix(home): avoid double effect execution * refactor(home): modal usage and receive-modal * refactor(home): onchain receive * refactor(home): lightning receive * refactor(home): cleanup receive * test(receiveModal): update tests to use new tabs * fix(receiveModal): minor improvements * refactor(home): send modal * chore: remove light buttons * fix(setup): import statement * refactor(confirmModal): use explicit export * fix(home): send modal - onchain design * fix(settings): change pw modal usage * use vertical tabList on small screen size --------- Co-authored-by: Christoph Stenglein --- src/components/ConfirmModal.tsx | 106 +++---- src/hooks/use-modalmanager.ts | 33 +++ src/hooks/use-sse.tsx | 8 +- src/i18n/langs/en.json | 1 + src/pages/Apps/AppCardAlby.tsx | 12 +- .../ListChannelModal/ListChannelModal.tsx | 64 ++--- src/pages/Home/OpenChannelModal.tsx | 135 ++++----- src/pages/Home/ReceiveModal/QRCode.tsx | 47 ++++ src/pages/Home/ReceiveModal/ReceiveLN.tsx | 97 +++++++ src/pages/Home/ReceiveModal/ReceiveModal.tsx | 258 +++++++----------- .../Home/ReceiveModal/ReceiveOnChain.tsx | 78 ------ .../__tests__/ReceiveModal.test.tsx | 10 +- .../{ConfirmSendModal.tsx => ConfirmSend.tsx} | 206 +++++++------- src/pages/Home/SendModal/SendLN.tsx | 63 ++--- src/pages/Home/SendModal/SendModal.tsx | 138 +++++----- src/pages/Home/SendModal/SendOnChain.tsx | 147 +++++----- ...endModal.test.tsx => ConfirmSend.test.tsx} | 66 ++--- .../SendModal/__tests__/SendModal.test.tsx | 29 +- .../TransactionDetailModal.tsx | 30 +- src/pages/Home/UnlockModal.tsx | 105 +++---- src/pages/Home/__tests__/UnlockModal.test.tsx | 6 +- src/pages/Home/index.tsx | 152 ++++++----- src/pages/Login/index.tsx | 16 +- src/pages/Settings/ChangePwModal.tsx | 156 ++++++----- src/pages/Settings/RebootModal.tsx | 2 +- src/pages/Settings/ShutdownModal.tsx | 2 +- src/pages/Setup/FormatDialog.tsx | 1 - src/pages/Setup/InputNodeName.tsx | 1 - src/pages/Setup/InputPassword.tsx | 3 +- src/pages/Setup/MigrationDialog.tsx | 3 +- src/pages/Setup/RecoveryDialog.tsx | 6 +- src/pages/Setup/StartDoneDialog.tsx | 4 +- src/pages/Setup/SyncScreen.tsx | 1 - src/utils/test-utils.tsx | 10 + 34 files changed, 1024 insertions(+), 972 deletions(-) create mode 100644 src/hooks/use-modalmanager.ts create mode 100644 src/pages/Home/ReceiveModal/QRCode.tsx create mode 100644 src/pages/Home/ReceiveModal/ReceiveLN.tsx delete mode 100644 src/pages/Home/ReceiveModal/ReceiveOnChain.tsx rename src/pages/Home/SendModal/{ConfirmSendModal.tsx => ConfirmSend.tsx} (54%) rename src/pages/Home/SendModal/__tests__/{ConfirmSendModal.test.tsx => ConfirmSend.test.tsx} (82%) diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index f911fff8..e00fd73e 100644 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -7,68 +7,78 @@ import { ModalFooter, } from "@nextui-org/react"; import type { UseDisclosureReturn } from "@nextui-org/use-disclosure"; -import { ReactNode } from "react"; +import { type ReactNode } from "react"; import { useTranslation } from "react-i18next"; export type Props = { - headline: string; - body?: ReactNode; - onConfirm?: () => void; disclosure: UseDisclosureReturn; + headline?: string; + children?: ReactNode; + onConfirm?: () => void; + confirmText?: string; + cancelText?: string; isLoading?: boolean; - isFormModal?: ReactNode; +} & ({ custom: true } | { custom?: false; body?: ReactNode }); + +type ConfirmModalComponent = { + (props: Props): JSX.Element; + Header: typeof ModalHeader; + Body: typeof ModalBody; + Footer: typeof ModalFooter; }; -export const ConfirmModal = ({ +export const ConfirmModalHeader = ModalHeader; +export const ConfirmModalBody = ModalBody; +export const ConfirmModalFooter = ModalFooter; + +export const ConfirmModal: ConfirmModalComponent = ({ + disclosure, headline, - body, + children, onConfirm, - disclosure, - isLoading, - isFormModal, -}: Props) => { + confirmText, + cancelText, + isLoading = false, + ...props +}) => { const { t } = useTranslation(); const { isOpen, onOpenChange, onClose } = disclosure; - return ( - <> - - - {(onClose) => ( - <> - - {headline} - + const renderContent = () => { + if ("custom" in props && props.custom) { + return children; + } + + return ( + <> + {headline && {headline}} - {isFormModal || ( - <> - {!!body && {body}} + {children || props.body} - - - - - - )} - - )} - - - + + + + + + ); + }; + + return ( + + {renderContent()} + ); }; -export default ConfirmModal; +ConfirmModal.Header = ConfirmModalHeader; +ConfirmModal.Body = ConfirmModalBody; +ConfirmModal.Footer = ConfirmModalFooter; diff --git a/src/hooks/use-modalmanager.ts b/src/hooks/use-modalmanager.ts new file mode 100644 index 00000000..b40b303a --- /dev/null +++ b/src/hooks/use-modalmanager.ts @@ -0,0 +1,33 @@ +import { useDisclosure } from "@nextui-org/use-disclosure"; +import { useState } from "react"; + +export type ModalType = + | "SEND" + | "RECEIVE" + | "DETAIL" + | "OPEN_CHANNEL" + | "LIST_CHANNEL" + | "UNLOCK" + | null; + +export function useModalManager() { + const [activeModal, setActiveModal] = useState(null); + const disclosure = useDisclosure(); + + const openModal = (modalType: ModalType) => { + setActiveModal(modalType); + disclosure.onOpen(); + }; + + const closeModal = () => { + setActiveModal(null); + disclosure.onClose(); + }; + + return { + activeModal, + disclosure, + openModal, + closeModal, + }; +} diff --git a/src/hooks/use-sse.tsx b/src/hooks/use-sse.tsx index a6c96507..27b37eb8 100644 --- a/src/hooks/use-sse.tsx +++ b/src/hooks/use-sse.tsx @@ -30,9 +30,13 @@ function useSSE() { const appInstallSuccessHandler = useCallback( (installData: InstallAppData, appName: string) => { if (installData.mode === "on") { - toast.success(t("apps.install_success", { appName })); + toast.success(t("apps.install_success", { appName }), { + theme: "dark", + }); } else { - toast.success(t("apps.uninstall_success", { appName })); + toast.success(t("apps.uninstall_success", { appName }), { + theme: "dark", + }); } }, [t], diff --git a/src/i18n/langs/en.json b/src/i18n/langs/en.json index ac73d511..fd85d176 100644 --- a/src/i18n/langs/en.json +++ b/src/i18n/langs/en.json @@ -362,6 +362,7 @@ "invoice": "Invoice", "on_chain": "On-chain", "receive": "Receive", + "receive_aria_options": "Receive options Lightning or On-chain", "refresh": "Refresh address", "scan_qr": "Scan this QR code or copy the address below to receive funds", "send": "Send", diff --git a/src/pages/Apps/AppCardAlby.tsx b/src/pages/Apps/AppCardAlby.tsx index f7fc17b3..54fa48c5 100644 --- a/src/pages/Apps/AppCardAlby.tsx +++ b/src/pages/Apps/AppCardAlby.tsx @@ -51,12 +51,18 @@ export const AppCardAlby: FC = () => { }); if (result.success) { - toast.success(t(`appInfo.${id}.action.connection.success`)); + toast.success(t(`appInfo.${id}.action.connection.success`), { + theme: "dark", + }); } else { - toast.error(t(`appInfo.${id}.action.connection.error`)); + toast.error(t(`appInfo.${id}.action.connection.error`), { + theme: "dark", + }); } } catch (e) { - toast.error(t(`appInfo.${id}.action.connection.error`)); + toast.error(t(`appInfo.${id}.action.connection.error`), { + theme: "dark", + }); } }; diff --git a/src/pages/Home/ListChannelModal/ListChannelModal.tsx b/src/pages/Home/ListChannelModal/ListChannelModal.tsx index 44a95031..a3b22cef 100644 --- a/src/pages/Home/ListChannelModal/ListChannelModal.tsx +++ b/src/pages/Home/ListChannelModal/ListChannelModal.tsx @@ -1,22 +1,22 @@ import ChannelList from "./ChannelList"; -import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; +import { Alert } from "@/components/Alert"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import Message from "@/components/Message"; -import ModalDialog from "@/layouts/ModalDialog"; import { LightningChannel } from "@/models/lightning-channel"; -import { MODAL_ROOT } from "@/utils"; import { checkError } from "@/utils/checkError"; import { instance } from "@/utils/interceptor"; +import { Spinner } from "@nextui-org/react"; import { useCallback, useEffect, useState } from "react"; -import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; -type Props = { - onClose: () => void; -}; - const theme = "dark"; -export default function ListChannelModal({ onClose }: Props) { +export default function ListChannelModal({ + disclosure, +}: Pick) { const { t } = useTranslation(); const [openChannels, setOpenChannels] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -64,28 +64,28 @@ export default function ListChannelModal({ onClose }: Props) { .finally(() => setIsLoading(false)); }; - return createPortal( - -

- {t("home.current_open_channels")} -

- {isLoading && ( -
- -
- )} - {!isLoading && openChannels.length === 0 && ( -

{t("home.no_open_channels")}

- )} - {openChannels.length > 0 && ( - - )} - {error && } -
, - MODAL_ROOT, + return ( + + + {isLoading && } + + {!isLoading && openChannels.length === 0 && ( + {t("home.no_open_channels")} + )} + + {openChannels.length > 0 && ( + + )} + {error && } + + ); } diff --git a/src/pages/Home/OpenChannelModal.tsx b/src/pages/Home/OpenChannelModal.tsx index 69ac4bfa..8ca68117 100644 --- a/src/pages/Home/OpenChannelModal.tsx +++ b/src/pages/Home/OpenChannelModal.tsx @@ -1,16 +1,16 @@ +import { Alert } from "@/components/Alert"; import AmountInput from "@/components/AmountInput"; import AvailableBalance from "@/components/AvailableBalance"; -import ButtonWithSpinner from "@/components/ButtonWithSpinner/ButtonWithSpinner"; +import { Button } from "@/components/Button"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import InputField from "@/components/InputField"; -import Message from "@/components/Message"; -import ModalDialog from "@/layouts/ModalDialog"; -import { MODAL_ROOT } from "@/utils"; import { checkError } from "@/utils/checkError"; import { convertMSatToSat, stringToNumber } from "@/utils/format"; import { instance } from "@/utils/interceptor"; -import { LinkIcon } from "@heroicons/react/24/outline"; import { ChangeEvent, useState } from "react"; -import { createPortal } from "react-dom"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -21,13 +21,11 @@ interface IFormInputs { feeRate: string; } -type Props = { +interface Props extends Pick { balance: number; - onClose: () => void; -}; -const theme = "dark"; +} -export default function OpenChannelModal({ balance, onClose }: Props) { +export default function OpenChannelModal({ balance, disclosure }: Props) { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -56,8 +54,8 @@ export default function OpenChannelModal({ balance, onClose }: Props) { }, ) .then(() => { - toast.success(t("home.channel_opened"), { theme }); - onClose(); + toast.success(t("home.channel_opened"), { theme: "dark" }); + disclosure.onClose(); }) .catch((err) => setError(checkError(err))) .finally(() => setIsLoading(false)); @@ -69,62 +67,69 @@ export default function OpenChannelModal({ balance, onClose }: Props) { const convertedBalance = balance ? convertMSatToSat(balance) : 0; - return createPortal( - -

{t("home.open_channel")}

-
- + return ( + +
+ {t("home.open_channel")} + + + +
+ + + stringToNumber(val) > 0 || + t("forms.validation.amount.required"), + }, + onChange: changeAmountHandler, + })} + /> +
+ + +
+
+ + {error && {error}} +
- - - - stringToNumber(val) > 0 || - t("forms.validation.amount.required"), - }, - onChange: changeAmountHandler, - })} - /> -
- - -
+ + - } + disabled={isLoading || !isValid} + isLoading={isLoading} > {t("home.open_channel")} - - - {error && } -
-
, - MODAL_ROOT, + + + + ); } diff --git a/src/pages/Home/ReceiveModal/QRCode.tsx b/src/pages/Home/ReceiveModal/QRCode.tsx new file mode 100644 index 00000000..b9453545 --- /dev/null +++ b/src/pages/Home/ReceiveModal/QRCode.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/Button"; +import useClipboard from "@/hooks/use-clipboard"; +import { QRCodeSVG } from "qrcode.react"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Tooltip } from "react-tooltip"; + +type Props = { + address: string; + onRefreshHandler?: () => void; +}; + +const ReceiveOnChain: FC = ({ address, onRefreshHandler }) => { + const { t } = useTranslation(); + const [copyAddress, addressCopied] = useClipboard(address); + + return ( + <> + + +

{t("wallet.scan_qr")}

+ +
+

+ {address} +

+ +
+ {addressCopied ? t("wallet.copied") : t("wallet.copy_clipboard")} +
+
+
+ + {onRefreshHandler && ( +
+ +
+ )} + + ); +}; + +export default ReceiveOnChain; diff --git a/src/pages/Home/ReceiveModal/ReceiveLN.tsx b/src/pages/Home/ReceiveModal/ReceiveLN.tsx new file mode 100644 index 00000000..3768be6b --- /dev/null +++ b/src/pages/Home/ReceiveModal/ReceiveLN.tsx @@ -0,0 +1,97 @@ +import { Alert } from "@/components/Alert"; +import AmountInput from "@/components/AmountInput"; +import { Button } from "@/components/Button"; +import { ConfirmModal } from "@/components/ConfirmModal"; +import InputField from "@/components/InputField"; +import { stringToNumber } from "@/utils/format"; +import type { ChangeEvent, FC } from "react"; +import { useState } from "react"; +import type { SubmitHandler } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +export interface IFormInputs { + amountInput: string; + commentInput: string; +} + +export type Props = { + isLoading: boolean; + error: string; + onSubmitHandler: (data: IFormInputs) => void; +}; + +const ReceiveLN: FC = ({ isLoading, error, onSubmitHandler }) => { + const { t } = useTranslation(); + + const [amount, setAmount] = useState(0); + const [comment, setComment] = useState(""); + + const commentChangeHandler = (event: ChangeEvent) => { + setComment(event.target.value); + }; + + const amountChangeHandler = (event: ChangeEvent) => { + setAmount(+event.target.value); + }; + + const { + register, + handleSubmit, + formState: { errors, isValid, submitCount }, + } = useForm({ + mode: "onChange", + }); + + const onSubmit: SubmitHandler = (data) => onSubmitHandler(data); + + return ( +
+ +
+
+ + stringToNumber(val) > 0 || + t("forms.validation.chainAmount.required"), + }, + onChange: amountChangeHandler, + })} + errorMessage={errors.amountInput} + /> + +
+ +
+
+
+ + {error && {error}} +
+ + + + +
+ ); +}; + +export default ReceiveLN; diff --git a/src/pages/Home/ReceiveModal/ReceiveModal.tsx b/src/pages/Home/ReceiveModal/ReceiveModal.tsx index 4f28d4cd..b4e5fc72 100644 --- a/src/pages/Home/ReceiveModal/ReceiveModal.tsx +++ b/src/pages/Home/ReceiveModal/ReceiveModal.tsx @@ -1,86 +1,52 @@ -import SwitchTxType, { TxType } from "../SwitchTxType"; -import ReceiveOnChain from "./ReceiveOnChain"; -import AmountInput from "@/components/AmountInput"; -import InputField from "@/components/InputField"; -import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; -import Message from "@/components/Message"; +import { TxType } from "../SwitchTxType"; +import QRCode from "./QRCode"; +import ReceiveLN, { type IFormInputs } from "./ReceiveLN"; +import { Alert } from "@/components/Alert"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import { AppContext, Unit } from "@/context/app-context"; -import ModalDialog from "@/layouts/ModalDialog"; -import { MODAL_ROOT } from "@/utils"; import { checkError } from "@/utils/checkError"; -import { convertBtcToSat, stringToNumber } from "@/utils/format"; +import { convertBtcToSat } from "@/utils/format"; import { instance } from "@/utils/interceptor"; -import { PlusCircleIcon } from "@heroicons/react/24/outline"; -import type { ChangeEvent, FC } from "react"; +import { Tabs, Tab } from "@nextui-org/tabs"; +import type { FC } from "react"; import { useContext, useState } from "react"; -import { createPortal } from "react-dom"; -import type { SubmitHandler } from "react-hook-form"; -import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -interface IFormInputs { - amountInput: string; - commentInput: string; -} - -type Props = { - onClose: () => void; -}; - -const ReceiveModal: FC = ({ onClose }) => { - const { unit } = useContext(AppContext); +const ReceiveModal: FC> = ({ + disclosure, +}) => { const { t } = useTranslation(); - const [invoiceType, setInvoiceType] = useState(TxType.LIGHTNING); - const [address, setAddress] = useState(""); - const [amount, setAmount] = useState(0); - const [comment, setComment] = useState(""); + const { unit } = useContext(AppContext); + + const [invoiceType, setInvoiceType] = useState(TxType.LIGHTNING); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const [invoice, setInvoice] = useState(""); + const [address, setAddress] = useState(""); - const lnInvoice = invoiceType === TxType.LIGHTNING; - - const changeInvoiceHandler = async (txType: TxType) => { - setAddress(""); - setAmount(0); - setComment(""); - setError(""); - - setInvoiceType(txType); - - if (txType === TxType.ONCHAIN) { - setIsLoading(true); - await instance - .post("lightning/new-address", { - type: "p2wkh", - }) - .then((resp) => { - setAddress(resp.data); - }) - .catch((err) => { - setError(checkError(err)); - }) - .finally(() => { - setIsLoading(false); - }); - } - }; - - const commentChangeHandler = (event: ChangeEvent) => { - setComment(event.target.value); - }; - - const amountChangeHandler = (event: ChangeEvent) => { - setAmount(+event.target.value); - }; + const generateInvoiceHandler = (data: IFormInputs) => { + const { commentInput, amountInput } = { + ...data, + amountInput: Number(data.amountInput), + }; - const generateInvoiceHandler = () => { + setInvoice(""); setIsLoading(true); + const mSatAmount = - unit === Unit.BTC ? convertBtcToSat(amount) * 1000 : amount * 1000; + unit === Unit.BTC + ? convertBtcToSat(amountInput) * 1000 + : amountInput * 1000; + instance - .post(`lightning/add-invoice?value_msat=${mSatAmount}&memo=${comment}`) + .post( + `lightning/add-invoice?value_msat=${mSatAmount}&memo=${commentInput}`, + ) .then((resp) => { - setAddress(resp.data.payment_request); + setInvoice(resp.data.payment_request); }) .catch((err) => { setError(checkError(err)); @@ -90,99 +56,79 @@ const ReceiveModal: FC = ({ onClose }) => { }); }; - const showLnInvoice = lnInvoice && !isLoading; - - const { - register, - handleSubmit, - formState: { errors, isValid, submitCount }, - } = useForm({ - mode: "onChange", - }); - - const onSubmit: SubmitHandler = (_data) => - generateInvoiceHandler(); - - return createPortal( - -
- {showLnInvoice ? t("wallet.create_invoice_ln") : t("wallet.fund")} -
+ const generateOnChainAddressHandler = async () => { + setAddress(""); + setIsLoading(true); -
- -
+ await instance + .post("lightning/new-address", { + type: "p2wkh", + }) + .then((resp) => { + setAddress(resp.data); + }) + .catch((err) => { + setError(checkError(err)); + }) + .finally(() => { + setIsLoading(false); + }); + }; -
-
- {isLoading && ( -
- -
- )} + const handleTabChange = (key: React.Key) => { + setInvoiceType(key as TxType); + setError(""); - {showLnInvoice && !address && ( -
- - stringToNumber(val) > 0 || - t("forms.validation.chainAmount.required"), - }, - onChange: amountChangeHandler, - })} - errorMessage={errors.amountInput} - /> + // eslint-disable-next-line eqeqeq + if (key == TxType.ONCHAIN && !address) { + generateOnChainAddressHandler(); + } + }; -
- + <> + {t("wallet.receive")} + +
+ + + {invoice ? ( + + + + ) : ( + -
-
- )} - - {error && } - - {!address && showLnInvoice && ( -
- -
- )} -
-
- - {address && ( - - )} -
, - MODAL_ROOT, + )} + + + + {!address && error && {error}} + + {address && !error && ( + + )} + + + + + + ); }; diff --git a/src/pages/Home/ReceiveModal/ReceiveOnChain.tsx b/src/pages/Home/ReceiveModal/ReceiveOnChain.tsx deleted file mode 100644 index ad6ea17e..00000000 --- a/src/pages/Home/ReceiveModal/ReceiveOnChain.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import useClipboard from "@/hooks/use-clipboard"; -import { ApiError, checkError } from "@/utils/checkError"; -import { instance } from "@/utils/interceptor"; -import { RefreshIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; -import { AxiosError } from "axios"; -import { QRCodeSVG } from "qrcode.react"; -import { Dispatch, FC, SetStateAction } from "react"; -import { useTranslation } from "react-i18next"; -import { Tooltip } from "react-tooltip"; - -type Props = { - address: string; - setAddress: Dispatch>; - setIsLoading: Dispatch>; - setError: Dispatch>; -}; - -const ReceiveOnChain: FC = ({ - address, - setAddress, - setIsLoading, - setError, -}) => { - const { t } = useTranslation(); - const [copyAddress, addressCopied] = useClipboard(address); - - const refreshAddressHandler = async () => { - setAddress(""); - setError(""); - - try { - setIsLoading(true); - const response = await instance.post("lightning/new-address", { - type: "p2wkh", - }); - setAddress(response.data); - } catch (error) { - setError(checkError(error as AxiosError)); - } finally { - setIsLoading(false); - } - }; - - return ( - <> - -

{t("wallet.scan_qr")}

-
-

- {address} -

- -
- {addressCopied ? t("wallet.copied") : t("wallet.copy_clipboard")} -
-
-
-
- -
- - ); -}; - -export default ReceiveOnChain; diff --git a/src/pages/Home/ReceiveModal/__tests__/ReceiveModal.test.tsx b/src/pages/Home/ReceiveModal/__tests__/ReceiveModal.test.tsx index bbf663c3..5763f34a 100644 --- a/src/pages/Home/ReceiveModal/__tests__/ReceiveModal.test.tsx +++ b/src/pages/Home/ReceiveModal/__tests__/ReceiveModal.test.tsx @@ -1,7 +1,7 @@ import ReceiveModal from "../ReceiveModal"; import { http, server, HttpResponse } from "@/testServer"; import userEvent from "@testing-library/user-event"; -import { render, screen } from "test-utils"; +import { render, screen, mockedDisclosure } from "test-utils"; beforeEach(() => { server.use( @@ -16,9 +16,9 @@ beforeEach(() => { describe("ReceiveModal", () => { test("Retrieves new on-chain address on click of on-chain button", async () => { const user = userEvent.setup(); - render( {}} />); + render(); - const onChainBtn = await screen.findByText("wallet.on_chain"); + const onChainBtn = screen.getByRole("tab", { name: "wallet.fund" }); await user.click(onChainBtn); @@ -27,9 +27,9 @@ describe("ReceiveModal", () => { test("Retrieves a new address upon clicking the refresh button", async () => { const user = userEvent.setup(); - render( {}} />); + render(); - const onChainBtn = screen.getByRole("button", { name: "wallet.on_chain" }); + const onChainBtn = screen.getByRole("tab", { name: "wallet.fund" }); await user.click(onChainBtn); diff --git a/src/pages/Home/SendModal/ConfirmSendModal.tsx b/src/pages/Home/SendModal/ConfirmSend.tsx similarity index 54% rename from src/pages/Home/SendModal/ConfirmSendModal.tsx rename to src/pages/Home/SendModal/ConfirmSend.tsx index 0cb311d6..44fe8eca 100644 --- a/src/pages/Home/SendModal/ConfirmSendModal.tsx +++ b/src/pages/Home/SendModal/ConfirmSend.tsx @@ -1,9 +1,10 @@ import { TxType } from "../SwitchTxType"; import { SendLnForm } from "./SendModal"; import { SendOnChainForm } from "./SendOnChain"; +import { Alert } from "@/components/Alert"; import AmountInput from "@/components/AmountInput"; -import ButtonWithSpinner from "@/components/ButtonWithSpinner/ButtonWithSpinner"; -import Message from "@/components/Message"; +import { Button } from "@/components/Button"; +import { ConfirmModal } from "@/components/ConfirmModal"; import { AppContext, Unit } from "@/context/app-context"; import { checkError } from "@/utils/checkError"; import { @@ -13,11 +14,7 @@ import { stringToNumber, } from "@/utils/format"; import { instance } from "@/utils/interceptor"; -import { - CheckIcon, - ChevronLeftIcon, - XMarkIcon, -} from "@heroicons/react/24/outline"; +import { ChevronLeftIcon } from "@heroicons/react/24/outline"; import type { ChangeEvent } from "react"; import { FC, useContext, useState } from "react"; import { useForm } from "react-hook-form"; @@ -35,7 +32,7 @@ export type Props = { close: () => void; }; -const ConfirmSendModal: FC = ({ confirmData, back, balance, close }) => { +const ConfirmSend: FC = ({ confirmData, back, balance, close }) => { const { t } = useTranslation(); const { unit } = useContext(AppContext); const [amountInput, setAmountInput] = useState(0); @@ -133,117 +130,120 @@ const ConfirmSendModal: FC = ({ confirmData, back, balance, close }) => { return (
- -

- {t("tx.confirm_info")}:{" "} -

- -
-

{addressTitle}:

-

{confirmData.address}

- {isInvoiceExpired && ( -

- {t("forms.validation.lnInvoice.expired")}:{" "} - {invoiceExpiryDateDecorated} -

- )} -
- -
-

{t("wallet.amount")}:

- {isLnTx && Number(confirmData.amount) !== 0 && ( - - {formatAmount( - convertMSatToSat(+confirmData.amount)?.toString()!, - Unit.SAT, - )}{" "} - Sat - - )} - - {!isLnTx && ( - - {confirmData.spendAll && t("tx.all_onchain")} - {!confirmData.spendAll && - `${formatAmount(confirmData.amount.toString(), Unit.SAT)} Sat`} - - )} + + + - {isInvoiceAmountBiggerThanBalance && ( -

{t("forms.validation.lnInvoice.max")}

- )} + +

+ {t("tx.confirm_info")}:{" "} +

- {Number(confirmData.amount) === 0 && !onChainData.spendAll && ( -
-

{t("forms.hint.invoiceAmountZero")}

- - - stringToNumber(val) > 0 || - t("forms.validation.chainAmount.required"), - }, - onChange: amountChangeHandler, - })} - /> -
- )} -
- - {!isLnTx && (
-

{t("tx.fee")}:

{confirmData.fee}{" "} - sat/vByte +

{addressTitle}:

+

+ {confirmData.address} +

+ {isInvoiceExpired && ( +

+ {t("forms.validation.lnInvoice.expired")}:{" "} + {invoiceExpiryDateDecorated} +

+ )}
- )} - {confirmData.comment && (
-

{commentHeading}:

{confirmData.comment} +

{t("wallet.amount")}:

+ {isLnTx && Number(confirmData.amount) !== 0 && ( + + {formatAmount( + convertMSatToSat(+confirmData.amount)?.toString()!, + Unit.SAT, + )}{" "} + Sat + + )} + + {!isLnTx && ( + + {confirmData.spendAll && t("tx.all_onchain")} + {!confirmData.spendAll && + `${formatAmount(confirmData.amount.toString(), Unit.SAT)} Sat`} + + )} + + {isInvoiceAmountBiggerThanBalance && ( +

+ {t("forms.validation.lnInvoice.max")} +

+ )} + + {Number(confirmData.amount) === 0 && !onChainData.spendAll && ( +
+

{t("forms.hint.invoiceAmountZero")}

+ + + stringToNumber(val) > 0 || + t("forms.validation.chainAmount.required"), + }, + onChange: amountChangeHandler, + })} + /> +
+ )}
- )} - {error && } + {!isLnTx && ( +
+

{t("tx.fee")}:

{confirmData.fee}{" "} + sat/vByte +
+ )} + + {confirmData.comment && ( +
+

{commentHeading}:

{" "} + {confirmData.comment} +
+ )} -
- + {error && {error}} + + + + - } disabled={ !isValid || !isValidLnInvoice || isInvoiceAmountBiggerThanBalance } + isLoading={isLoading} > - {t("settings.confirm")} - -
+ {t("settings.confirm")} + + ); }; -export default ConfirmSendModal; +export default ConfirmSend; diff --git a/src/pages/Home/SendModal/SendLN.tsx b/src/pages/Home/SendModal/SendLN.tsx index 24d89fdf..60344dd6 100644 --- a/src/pages/Home/SendModal/SendLN.tsx +++ b/src/pages/Home/SendModal/SendLN.tsx @@ -1,12 +1,12 @@ import { TxType } from "../SwitchTxType"; import { SendLnForm } from "./SendModal"; import { SendOnChainForm } from "./SendOnChain"; +import { Alert } from "@/components/Alert"; import AvailableBalance from "@/components/AvailableBalance"; -import ButtonWithSpinner from "@/components/ButtonWithSpinner/ButtonWithSpinner"; +import { Button } from "@/components/Button"; +import { ConfirmModal } from "@/components/ConfirmModal"; import InputField from "@/components/InputField"; -import Message from "@/components/Message"; import { convertMSatToSat } from "@/utils/format"; -import { ShareIcon } from "@bitcoin-design/bitcoin-icons-react/filled"; import { FC, useState } from "react"; import type { SubmitHandler } from "react-hook-form"; import { useForm } from "react-hook-form"; @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; export type Props = { lnBalance: number; - loading: boolean; + isLoading: boolean; onConfirm: (data: LnInvoiceForm) => void; error: string; confirmData?: SendOnChainForm | SendLnForm | null; @@ -25,7 +25,7 @@ export interface LnInvoiceForm { } const SendLn: FC = ({ - loading, + isLoading, lnBalance, onConfirm, error, @@ -57,35 +57,36 @@ const SendLn: FC = ({ return (
-

{t("wallet.send_lightning")}

+ + - + - + {error && {error}} + - {error && } - - } - > - {t("wallet.send")} - + + + ); }; diff --git a/src/pages/Home/SendModal/SendModal.tsx b/src/pages/Home/SendModal/SendModal.tsx index f8a5d6af..ccd04fa4 100644 --- a/src/pages/Home/SendModal/SendModal.tsx +++ b/src/pages/Home/SendModal/SendModal.tsx @@ -1,21 +1,23 @@ -import SwitchTxType, { TxType } from "../SwitchTxType"; -import ConfirmSendModal from "./ConfirmSendModal"; +import { TxType } from "../SwitchTxType"; +import ConfirmSend from "./ConfirmSend"; import SendLn, { LnInvoiceForm } from "./SendLN"; import SendOnChain, { SendOnChainForm } from "./SendOnChain"; -import ModalDialog from "@/layouts/ModalDialog"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import { DecodePayRequest } from "@/models/decode-pay-req"; -import { MODAL_ROOT } from "@/utils"; import { checkError } from "@/utils/checkError"; import { instance } from "@/utils/interceptor"; +import { Tabs, Tab } from "@nextui-org/tabs"; import { AxiosResponse } from "axios"; import { FC, useState } from "react"; -import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; -export type Props = { +export interface Props extends Pick { lnBalance: number; onchainBalance: number; - onClose: () => void; -}; +} export interface SendLnForm { invoiceType: TxType.LIGHTNING; @@ -27,15 +29,23 @@ export interface SendLnForm { expiry: number; } -const SendModal: FC = ({ lnBalance, onClose, onchainBalance }) => { +const SendModal: FC = ({ lnBalance, disclosure, onchainBalance }) => { + const { t } = useTranslation(); + const [invoiceType, setInvoiceType] = useState(TxType.LIGHTNING); const [confirmData, setConfirmData] = useState< SendOnChainForm | SendLnForm | null >(null); const [isBack, setIsBack] = useState(false); - const [loading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const handleTabChange = (key: React.Key) => { + setInvoiceType(key as TxType); + setConfirmData(null); + setError(""); + }; + const confirmLnHandler = async (data: LnInvoiceForm) => { setIsLoading(true); setIsBack(false); @@ -75,66 +85,56 @@ const SendModal: FC = ({ lnBalance, onClose, onchainBalance }) => { setConfirmData(data); }; - const changeTransactionHandler = (txType: TxType): void => { - setInvoiceType(txType); - setConfirmData(null); - setError(""); - }; - - // confirm send - if (!isBack && confirmData) { - return ( - onClose()}> - - - ); - } - - // Send LN - if (invoiceType === TxType.LIGHTNING) { - return createPortal( - onClose()} closeable={!loading}> - - - - , - MODAL_ROOT, - ); - } - - // Send On-Chain - return createPortal( - onClose()}> - + return ( + + <> + {t("wallet.send")} - - , - MODAL_ROOT, +
+ + + {!isBack && confirmData ? ( + + ) : ( + + )} + + + {!isBack && confirmData ? ( + + ) : ( + + )} + + +
+ + ); }; diff --git a/src/pages/Home/SendModal/SendOnChain.tsx b/src/pages/Home/SendModal/SendOnChain.tsx index 62214dc3..6ef339e1 100644 --- a/src/pages/Home/SendModal/SendOnChain.tsx +++ b/src/pages/Home/SendModal/SendOnChain.tsx @@ -2,6 +2,8 @@ import { TxType } from "../SwitchTxType"; import { SendLnForm } from "./SendModal"; import AmountInput from "@/components/AmountInput"; import AvailableBalance from "@/components/AvailableBalance"; +import { Button } from "@/components/Button"; +import { ConfirmModal } from "@/components/ConfirmModal"; import InputField from "@/components/InputField"; import { stringToNumber } from "@/utils/format"; import { ChangeEvent, FC, useState } from "react"; @@ -40,7 +42,6 @@ const SendOnChain: FC = ({ balance, onConfirm, confirmData }) => { const [amount, setAmount] = useState(0); if (!updated && confirmData?.invoiceType === TxType.ONCHAIN) { - console.log("updating"); setUpdated(true); setAmount(confirmData.amount); reset({ @@ -60,90 +61,86 @@ const SendOnChain: FC = ({ balance, onConfirm, confirmData }) => { }; return ( -
-

{t("wallet.send_onchain")}

+ + + - - -
-
- -
- - {!spendAll && ( -
- - //@ts-ignore - stringToNumber(val) > 0 || - t("forms.validation.chainAmount.required"), +
+
+
- )} -
- -
+ {!spendAll && ( +
+ + //@ts-ignore + stringToNumber(val) > 0 || + t("forms.validation.chainAmount.required"), + }, + onChange: changeAmountHandler, + })} + /> +
+ )} -
- -
+
+ +
-
- -
-
+
+ +
+ +
+ +
+
+
-
- -
+ +
); }; diff --git a/src/pages/Home/SendModal/__tests__/ConfirmSendModal.test.tsx b/src/pages/Home/SendModal/__tests__/ConfirmSend.test.tsx similarity index 82% rename from src/pages/Home/SendModal/__tests__/ConfirmSendModal.test.tsx rename to src/pages/Home/SendModal/__tests__/ConfirmSend.test.tsx index 9dd25b6d..35b1d7ab 100644 --- a/src/pages/Home/SendModal/__tests__/ConfirmSendModal.test.tsx +++ b/src/pages/Home/SendModal/__tests__/ConfirmSend.test.tsx @@ -1,14 +1,12 @@ import { TxType } from "../../SwitchTxType"; -import type { Props } from "../ConfirmSendModal"; -import ConfirmSendModal from "../ConfirmSendModal"; +import type { Props } from "../ConfirmSend"; +import ConfirmSend from "../ConfirmSend"; import { SendLnForm } from "../SendModal"; -import { SendOnChainForm } from "../SendOnChain"; -import i18n from "@/i18n/test_config"; +import type { SendOnChainForm } from "../SendOnChain"; +import { ConfirmModal } from "@/components/ConfirmModal"; import { http, server, HttpResponse } from "@/testServer"; import userEvent from "@testing-library/user-event"; -import { I18nextProvider } from "react-i18next"; -import { toast } from "react-toastify"; -import { render, screen, waitFor } from "test-utils"; +import { render, screen, waitFor, mockedDisclosure } from "test-utils"; const closeSpy = vi.fn(); @@ -39,13 +37,13 @@ const basicOnChainTxProps: Props = { close: closeSpy, }; -describe("ConfirmSendModal", () => { +describe("ConfirmSend", () => { describe("ln-invoice with zero amount", () => { const setup = () => { render( - - - , + + + , ); }; @@ -54,7 +52,6 @@ describe("ConfirmSendModal", () => { setup(); let amountInput = screen.getByLabelText("wallet.amount"); - await user.clear(amountInput); await user.type(amountInput, "999"); @@ -139,10 +136,11 @@ describe("ConfirmSendModal", () => { timestamp: 1640995200, // Sat Jan 01 2022 08:00:00 expiry: 36000, }; + render( - - - , + + + , ); expect( @@ -162,11 +160,10 @@ describe("ConfirmSendModal", () => { amount: 111, }; render( - - - , + + + , ); - expect( await screen.findByText("forms.validation.lnInvoice.max"), ).toBeInTheDocument(); @@ -180,15 +177,16 @@ describe("ConfirmSendModal", () => { ...basicLnTxProps.confirmData, amount: 100, }; + render( - - - , + + + , ); const submitButton = screen.queryByText("wallet.amount"); - expect(submitButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); expect( await screen.findByRole("button", { name: "settings.confirm", @@ -203,13 +201,11 @@ describe("ConfirmSendModal", () => { ...basicOnChainTxProps.confirmData, amount: 111, }; + render( - - - , + + + , ); expect( @@ -225,18 +221,16 @@ describe("ConfirmSendModal", () => { ...basicOnChainTxProps.confirmData, amount: 50, }; + render( - - - , + + + , ); const submitButton = screen.queryByText("wallet.amount"); - expect(submitButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); expect( await screen.findByRole("button", { name: "settings.confirm", diff --git a/src/pages/Home/SendModal/__tests__/SendModal.test.tsx b/src/pages/Home/SendModal/__tests__/SendModal.test.tsx index 3375deb0..dae68667 100644 --- a/src/pages/Home/SendModal/__tests__/SendModal.test.tsx +++ b/src/pages/Home/SendModal/__tests__/SendModal.test.tsx @@ -1,14 +1,13 @@ -import SendModal, { Props } from "../SendModal"; +import SendModal, { type Props } from "../SendModal"; import { HttpResponse, http, server } from "@/testServer"; import userEvent from "@testing-library/user-event"; import type { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; -import { render, screen } from "test-utils"; +import { render, screen, mockedDisclosure } from "test-utils"; -const handleClose = vi.fn(); const basicProps: Props = { lnBalance: 0, onchainBalance: 0, - onClose: handleClose, + disclosure: mockedDisclosure, }; const setup = () => { @@ -20,24 +19,7 @@ describe("SendModal", () => { setup(); const addressInput = screen.getByLabelText("wallet.invoice"); - const lnTypeBtn = screen.getByRole("button", { - name: "home.lightning", - }); - const onChainBtn = screen.getByRole("button", { - name: "wallet.on_chain", - }); - expect(addressInput).toBeInTheDocument(); - expect(lnTypeBtn).toBeDisabled(); - expect(onChainBtn).not.toBeDisabled(); - }); - - it("should close on click of X button", async () => { - const user = userEvent.setup(); - setup(); - const closeBtn = screen.getByRole("button", { name: "" }); - await user.click(closeBtn); - expect(handleClose).toHaveBeenCalled(); }); describe("SendLN", () => { @@ -98,6 +80,7 @@ describe("SendModal", () => { } }), ); + const user = userEvent.setup(); setup(); @@ -121,8 +104,8 @@ describe("SendModal", () => { user = userEvent.setup(); setup(); - const onChainBtn = screen.getByRole("button", { - name: "wallet.on_chain", + const onChainBtn = screen.getByRole("tab", { + name: "wallet.send_onchain", }); await user.click(onChainBtn); diff --git a/src/pages/Home/TransactionCard/TransactionDetailModal/TransactionDetailModal.tsx b/src/pages/Home/TransactionCard/TransactionDetailModal/TransactionDetailModal.tsx index 29eff8f5..3574f999 100644 --- a/src/pages/Home/TransactionCard/TransactionDetailModal/TransactionDetailModal.tsx +++ b/src/pages/Home/TransactionCard/TransactionDetailModal/TransactionDetailModal.tsx @@ -1,18 +1,21 @@ import LNDetails from "./LNDetails"; import OnchainDetails from "./OnchainDetails"; -import ModalDialog from "@/layouts/ModalDialog"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import { Transaction } from "@/models/transaction.model"; -import { MODAL_ROOT } from "@/utils"; import { FC } from "react"; -import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; -type Props = { +interface Props extends Pick { transaction: Transaction; - close: () => void; -}; +} -export const TransactionDetailModal: FC = ({ transaction, close }) => { +export const TransactionDetailModal: FC = ({ + transaction, + disclosure, +}) => { const { t } = useTranslation(); // prevent error when closing via 'Esc' key @@ -22,15 +25,14 @@ export const TransactionDetailModal: FC = ({ transaction, close }) => { const { category } = transaction; - return createPortal( - -
-

{t("tx.tx_details")}

+ return ( + + {t("tx.tx_details")} + {category === "onchain" && } {category === "ln" && } -
-
, - MODAL_ROOT, + + ); }; diff --git a/src/pages/Home/UnlockModal.tsx b/src/pages/Home/UnlockModal.tsx index 50734016..28a0a98d 100644 --- a/src/pages/Home/UnlockModal.tsx +++ b/src/pages/Home/UnlockModal.tsx @@ -1,15 +1,15 @@ -import ButtonWithSpinner from "@/components/ButtonWithSpinner/ButtonWithSpinner"; +import { Alert } from "@/components/Alert"; +import { Button } from "@/components/Button"; import CapsLockWarning from "@/components/CapsLockWarning"; +import { + ConfirmModal, + type Props as ConfirmModalProps, +} from "@/components/ConfirmModal"; import InputField from "@/components/InputField"; -import Message from "@/components/Message"; import { AppContext } from "@/context/app-context"; import useCapsLock from "@/hooks/use-caps-lock"; -import ModalDialog, { disableScroll } from "@/layouts/ModalDialog"; -import { MODAL_ROOT } from "@/utils"; import { instance } from "@/utils/interceptor"; -import { LockOpenIcon } from "@heroicons/react/24/outline"; -import { useContext, useEffect, useState } from "react"; -import { createPortal } from "react-dom"; +import { useContext, useState } from "react"; import type { SubmitHandler } from "react-hook-form"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -19,16 +19,13 @@ interface IFormInputs { passwordInput: string; } -type Props = { - onClose: () => void; -}; - -const theme = "dark"; -export default function UnlockModal({ onClose }: Props) { +export default function UnlockModal({ + disclosure, +}: Pick) { const { t } = useTranslation(); const { setWalletLocked } = useContext(AppContext); const [isLoading, setIsLoading] = useState(false); - const [passwordWrong, setPasswordWrong] = useState(false); + const [isServerError, setIsServerError] = useState(false); const { isCapsLockEnabled, keyHandlers } = useCapsLock(); const { @@ -41,61 +38,65 @@ export default function UnlockModal({ onClose }: Props) { passwordInput: string; }) => { setIsLoading(true); - setPasswordWrong(false); + setIsServerError(false); instance .post("/lightning/unlock-wallet", { password: data.passwordInput }) .then((res) => { if (res.data) { setWalletLocked(false); - toast.success(t("wallet.unlock_success"), { theme }); - onClose(); + toast.success(t("wallet.unlock_success"), { theme: "dark" }); + disclosure.onClose(); } }) .catch((_) => { setIsLoading(false); - setPasswordWrong(true); + setIsServerError(true); }); }; - useEffect(() => { - return () => disableScroll.off(); - }, []); + return ( + +
+ +

{t("wallet.unlock_subtitle")}

- return createPortal( - onClose()}> -

{t("wallet.unlock_title")}

+ {isCapsLockEnabled && } -
-

{t("wallet.unlock_subtitle")}

+
+ +
- - - {isCapsLockEnabled && } - {t("login.invalid_pass")} + )} + + + +
- - {passwordWrong && } -
, - MODAL_ROOT, + + + +
); } diff --git a/src/pages/Home/__tests__/UnlockModal.test.tsx b/src/pages/Home/__tests__/UnlockModal.test.tsx index 58baf640..8a357585 100644 --- a/src/pages/Home/__tests__/UnlockModal.test.tsx +++ b/src/pages/Home/__tests__/UnlockModal.test.tsx @@ -1,13 +1,11 @@ import UnlockModal from "../UnlockModal"; import { http, server, HttpResponse } from "@/testServer"; import userEvent from "@testing-library/user-event"; -import { render, screen } from "test-utils"; - -const handleClose = vi.fn(); +import { render, screen, mockedDisclosure } from "test-utils"; describe("UnlockModal", () => { const setup = () => { - render(); + render(); }; test("renders", () => { diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index aa85af64..63b189fb 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -13,6 +13,7 @@ import WalletCard from "./WalletCard"; import { AppContext } from "@/context/app-context"; import { SSEContext } from "@/context/sse-context"; import { useInterval } from "@/hooks/use-interval"; +import { useModalManager } from "@/hooks/use-modalmanager"; import PageLoadingScreen from "@/layouts/PageLoadingScreen"; import { Transaction } from "@/models/transaction.model"; import { enableGutter } from "@/utils"; @@ -25,25 +26,19 @@ import { toast } from "react-toastify"; const startupToastId = "startup-toast"; -type ModalType = - | "SEND" - | "RECEIVE" - | "DETAIL" - | "OPEN_CHANNEL" - | "LIST_CHANNEL" - | "UNLOCK"; - const Home: FC = () => { + const { activeModal, disclosure, openModal, closeModal } = useModalManager(); + const { t } = useTranslation(); const { walletLocked, setWalletLocked } = useContext(AppContext); const { balance, lnInfo, systemStartupInfo } = useContext(SSEContext); - const [showModal, setShowModal] = useState(false); const [detailTx, setDetailTx] = useState(null); const [transactions, setTransactions] = useState([]); const [isLoadingTransactions, setIsLoadingTransactions] = useState(false); const [txError, setTxError] = useState(""); const { implementation } = lnInfo; + const { lightning: lightningState, bitcoin, @@ -51,6 +46,19 @@ const Home: FC = () => { lightning_msg, } = systemStartupInfo || {}; + useEffect(() => { + if (walletLocked && activeModal !== "UNLOCK") { + openModal("UNLOCK"); + } else if (!walletLocked && activeModal === "UNLOCK") { + closeModal(); + } + }, [walletLocked, activeModal, openModal, closeModal]); + + const closeModalHandler = useCallback(() => { + setDetailTx(null); + closeModal(); + }, [closeModal]); + useEffect(() => { const statusToastContent = (
@@ -149,93 +157,88 @@ const Home: FC = () => { setIsLoadingTransactions, ]); - useInterval(getTransactions, 20000); - - const closeModalHandler = () => { - setShowModal(false); - setDetailTx(null); - }; - - const showDetailHandler = (index: number) => { - const tx = transactions.find((tx) => tx.index === index); - if (!tx) { - console.error("Could not find transaction with index ", index); - return; - } - setDetailTx(tx); - setShowModal("DETAIL"); - }; - - if (walletLocked && showModal !== "UNLOCK") { - setShowModal("UNLOCK"); - } + const showDetailHandler = useCallback( + (index: number) => { + const tx = transactions.find((tx) => tx.index === index); + if (!tx) { + console.error("Could not find transaction with index ", index); + return; + } + setDetailTx(tx); + openModal("DETAIL"); + }, + [transactions, openModal], + ); - if (!walletLocked && showModal === "UNLOCK") { - setShowModal(false); - } + useInterval(getTransactions, 20000); - const determineModal = () => { - switch (showModal) { - case "DETAIL": - return ( - - ); - case "SEND": - return ( - - ); - case "RECEIVE": - return ; - case "OPEN_CHANNEL": - return ( - - ); - case "LIST_CHANNEL": - return ; - case "UNLOCK": - return ; - case false: - default: - return undefined; - } - }; + const modalComponent = () => ( + <> + {activeModal === "UNLOCK" && ( + + )} + {activeModal === "LIST_CHANNEL" && ( + + )} + {activeModal === "OPEN_CHANNEL" && ( + + )} + {activeModal === "SEND" && ( + + )} + {activeModal === "RECEIVE" && ( + + )} + {activeModal === "DETAIL" && ( + + )} + + ); if (implementation === null && lightningState !== "disabled") { return ( <> - {determineModal()} + {modalComponent()} ); } + const height = btcOnlyMode ? "h-full md:h-1/2" : "h-full"; return ( <> - {determineModal()} + {modalComponent()}
{!btcOnlyMode && (
setShowModal("RECEIVE")} - onSend={() => setShowModal("SEND")} - onOpenChannel={() => setShowModal("OPEN_CHANNEL")} - onCloseChannel={() => setShowModal("LIST_CHANNEL")} + onReceive={() => openModal("RECEIVE")} + onSend={() => openModal("SEND")} + onOpenChannel={() => openModal("OPEN_CHANNEL")} + onCloseChannel={() => openModal("LIST_CHANNEL")} />
)} + {!btcOnlyMode && (
{ />
)} +
+
+ {!btcOnlyMode && (
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 256f54da..005d854c 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,12 +1,12 @@ import RaspiBlitzLogoDark from "@/assets/RaspiBlitz_Logo_Main_Negative.svg?react"; +import { Alert } from "@/components/Alert"; import I18nSelect from "@/components/I18nDropdown"; -import LoadingSpinner from "@/components/LoadingSpinner/LoadingSpinner"; -import Message from "@/components/Message"; import { AppContext } from "@/context/app-context"; import { ACCESS_TOKEN, enableGutter } from "@/utils"; import { ApiError, checkError } from "@/utils/checkError"; import { instance } from "@/utils/interceptor"; import { Button } from "@nextui-org/button"; +import { Spinner } from "@nextui-org/react"; import { AxiosError } from "axios"; import { FC, FormEvent, useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -71,12 +71,11 @@ const Login: FC = () => {
+ - {isLoading && ( -
- -
- )} + + {isLoading && } + {!isLoading && ( <>
{ {t("login.login")}
- {error && } + + {error && {error}} )}
diff --git a/src/pages/Settings/ChangePwModal.tsx b/src/pages/Settings/ChangePwModal.tsx index 29bbf61d..2d5e6a2e 100644 --- a/src/pages/Settings/ChangePwModal.tsx +++ b/src/pages/Settings/ChangePwModal.tsx @@ -1,12 +1,11 @@ import ActionBox from "./ActionBox"; import { Button } from "@/components/Button"; import CapsLockWarning from "@/components/CapsLockWarning"; -import ConfirmModal from "@/components/ConfirmModal"; +import { ConfirmModal } from "@/components/ConfirmModal"; import useCapsLock from "@/hooks/use-caps-lock"; import { checkError } from "@/utils/checkError"; import { instance } from "@/utils/interceptor"; import { Input, useDisclosure } from "@nextui-org/react"; -import { ModalFooter, ModalBody } from "@nextui-org/react"; import { type FC, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -36,7 +35,7 @@ const ChangePwModal: FC = () => { instance .post("/system/change-password", {}, { params }) .then(() => { - toast.success(t("settings.pass_a_changed")); + toast.success(t("settings.pass_a_changed"), { theme: "dark" }); confirmModal.onClose(); }) .catch((err) => { @@ -63,85 +62,84 @@ const ChangePwModal: FC = () => { disclosure={confirmModal} headline={t("settings.change_pw_a")} isLoading={isLoading} - isFormModal={ -
- - {isCapsLockEnabled && } + custom + > + + + {isCapsLockEnabled && } -
- - - -
-
+
+ - - - - - - } - /> + type="password" + label={t("settings.new_pw")} + isInvalid={!!errors.newPassword} + errorMessage={errors.newPassword?.message} + {...register("newPassword", { + required: t("setup.password_error_empty"), + pattern: { + value: /^[a-zA-Z0-9.-]*$/, + message: t("setup.password_error_chars"), + }, + minLength: { + value: 8, + message: t("setup.password_error_length"), + }, + })} + {...keyHandlers} + /> +
+ + + + + + + + confirmModal.onOpen()} > {t("setup.cancel")} diff --git a/src/pages/Setup/InputNodeName.tsx b/src/pages/Setup/InputNodeName.tsx index 1a7c0f77..6503196f 100644 --- a/src/pages/Setup/InputNodeName.tsx +++ b/src/pages/Setup/InputNodeName.tsx @@ -92,7 +92,6 @@ export default function InputNodeName({ callback }: Props) { - diff --git a/src/pages/Setup/StartDoneDialog.tsx b/src/pages/Setup/StartDoneDialog.tsx index ebfe3043..06edf82b 100644 --- a/src/pages/Setup/StartDoneDialog.tsx +++ b/src/pages/Setup/StartDoneDialog.tsx @@ -1,5 +1,5 @@ import { Button } from "@/components/Button"; -import ConfirmModal from "@/components/ConfirmModal"; +import { ConfirmModal } from "@/components/ConfirmModal"; import { Headline } from "@/components/Headline"; import SetupContainer from "@/layouts/SetupContainer"; import { SetupPhase } from "@/models/setup.model"; @@ -51,7 +51,7 @@ export default function StartDoneDialog({ setupPhase, callback }: Props) { - diff --git a/src/pages/Setup/SyncScreen.tsx b/src/pages/Setup/SyncScreen.tsx index 223098a5..9bc5d105 100644 --- a/src/pages/Setup/SyncScreen.tsx +++ b/src/pages/Setup/SyncScreen.tsx @@ -224,7 +224,6 @@ export default function SyncScreen({ data, callback }: Props) { onClick={() => callback("shutdown", null)} color="primary" title={t("setup.sync_restartinfo")} - variant="light" startContent={} > {t("settings.shutdown")} diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx index 9b75fe7c..78e3146e 100644 --- a/src/utils/test-utils.tsx +++ b/src/utils/test-utils.tsx @@ -63,3 +63,13 @@ const customRender = ( export * from "@testing-library/react"; export { customRender as render }; + +export const mockedDisclosure = { + isOpen: true, + onOpen: vi.fn(), + onClose: vi.fn(), + onOpenChange: vi.fn(), + isControlled: false, + getButtonProps: vi.fn(), + getDisclosureProps: vi.fn(), +};