-
-
-
-
-
+
+
+
+ {isAlbyOAuthUser && (
+
+ <>
+
+
+ {loadingLightningAddress ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+ {!auth.accountLoading && auth.account ? (
+
+ ) : (
+ auth.accountLoading && (
+
+ )
+ )}
+
-
-
-
-
-
-
-
-
-
-
- {tCommon("or")}
-
-
-
-
-
-
- {isAlbyOAuthUser && (
-
-
- )}
-
- {isAlbyUser && (
-
-
- )}
-
+ )}
+
}
+ onClick={() => {
+ navigate("/receive/invoice");
+ }}
+ />
+ {isAlbyUser && (
+
}
+ onClick={() => {
+ navigate("/onChainReceive");
+ }}
+ />
+ )}
+
}
+ onClick={() => {
+ navigate("/lnurlRedeem");
+ }}
+ />
-
- )}
+
+
);
}
diff --git a/src/app/screens/Receive/index.test.tsx b/src/app/screens/ReceiveInvoice/index.test.tsx
similarity index 95%
rename from src/app/screens/Receive/index.test.tsx
rename to src/app/screens/ReceiveInvoice/index.test.tsx
index ee0ac75f7d..d0a10dbcf0 100644
--- a/src/app/screens/Receive/index.test.tsx
+++ b/src/app/screens/ReceiveInvoice/index.test.tsx
@@ -5,7 +5,7 @@ import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings";
import { SettingsProvider } from "~/app/context/SettingsContext";
import api from "~/common/lib/api";
-import Receive from "./index";
+import ReceiveInvoice from "../ReceiveInvoice";
jest.mock("~/common/lib/api", () => {
const original = jest.requireActual("~/common/lib/api");
@@ -31,7 +31,7 @@ describe("Receive", () => {
render(
-
+
);
diff --git a/src/app/screens/ReceiveInvoice/index.tsx b/src/app/screens/ReceiveInvoice/index.tsx
new file mode 100644
index 0000000000..e0dfe387eb
--- /dev/null
+++ b/src/app/screens/ReceiveInvoice/index.tsx
@@ -0,0 +1,287 @@
+import {
+ CaretLeftIcon,
+ CheckIcon,
+ CopyIcon,
+} from "@bitcoin-design/bitcoin-icons-react/outline";
+import Button from "@components/Button";
+import Container from "@components/Container";
+import Header from "@components/Header";
+import IconButton from "@components/IconButton";
+import Loading from "@components/Loading";
+import DualCurrencyField from "@components/form/DualCurrencyField";
+import TextField from "@components/form/TextField";
+import { useEffect, useRef, useState } from "react";
+import Confetti from "react-confetti";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import QRCode from "~/app/components/QRCode";
+import toast from "~/app/components/Toast";
+import { useAccount } from "~/app/context/AccountContext";
+import { useSettings } from "~/app/context/SettingsContext";
+import api from "~/common/lib/api";
+import msg from "~/common/lib/msg";
+import { poll } from "~/common/utils/helpers";
+
+function ReceiveInvoice() {
+ const { t } = useTranslation("translation", { keyPrefix: "receive" });
+ const { t: tCommon } = useTranslation("common");
+
+ const auth = useAccount();
+ const {
+ isLoading: isLoadingSettings,
+ settings,
+ getFormattedFiat,
+ } = useSettings();
+ const showFiat = !isLoadingSettings && settings.showFiat;
+
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ amount: "0",
+ description: "",
+ expiration: "",
+ });
+ const [loadingInvoice, setLoadingInvoice] = useState(false);
+ const [invoice, setInvoice] = useState<{
+ paymentRequest: string;
+ rHash: string;
+ } | null>();
+ const [copyInvoiceLabel, setCopyInvoiceLabel] = useState(
+ tCommon("actions.copy_invoice") as string
+ );
+
+ const [paid, setPaid] = useState(false);
+ const [pollingForPayment, setPollingForPayment] = useState(false);
+ const mounted = useRef(false);
+
+ useEffect(() => {
+ mounted.current = true;
+
+ return () => {
+ mounted.current = false;
+ };
+ }, []);
+
+ const [fiatAmount, setFiatAmount] = useState("");
+
+ useEffect(() => {
+ if (formData.amount !== "" && showFiat) {
+ (async () => {
+ const res = await getFormattedFiat(formData.amount);
+ setFiatAmount(res);
+ })();
+ }
+ }, [formData, showFiat, getFormattedFiat]);
+
+ function handleChange(
+ event: React.ChangeEvent
+ ) {
+ setFormData({
+ ...formData,
+ [event.target.name]: event.target.value.trim(),
+ });
+ }
+
+ function checkPayment(paymentHash: string) {
+ setPollingForPayment(true);
+ poll({
+ fn: () =>
+ msg.request("checkPayment", { paymentHash }) as Promise<{
+ paid: boolean;
+ }>,
+ validate: (payment) => payment.paid,
+ interval: 3000,
+ maxAttempts: 20,
+ shouldStopPolling: () => !mounted.current,
+ })
+ .then(() => {
+ setPaid(true);
+ auth.fetchAccountInfo(); // Update balance.
+ })
+ .catch((err) => console.error(err))
+ .finally(() => {
+ setPollingForPayment(false);
+ });
+ }
+
+ function setDefaults() {
+ setFormData({
+ amount: "0",
+ description: "",
+ expiration: "",
+ });
+ setPaid(false);
+ setPollingForPayment(false);
+ setInvoice(null);
+ }
+
+ async function createInvoice() {
+ try {
+ setLoadingInvoice(true);
+ const response = await api.makeInvoice({
+ amount: formData.amount,
+ memo: formData.description,
+ });
+ setInvoice(response);
+ checkPayment(response.rHash);
+ } catch (e) {
+ if (e instanceof Error) {
+ toast.error(e.message);
+ }
+ } finally {
+ setLoadingInvoice(false);
+ }
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ createInvoice();
+ }
+
+ function renderInvoice() {
+ if (!invoice) return null;
+ return (
+ <>
+
+ {paid && (
+
+
+ )}
+ {!paid && (
+ <>
+
+
+
+
+ {pollingForPayment && (
+
+
+ {t("payment.waiting")}
+
+ )}
+
+ {!pollingForPayment && (
+
+ >
+ )}
+ {paid && (
+ {
+ confetti && confetti.reset();
+ }}
+ style={{ pointerEvents: "none" }}
+ />
+ )}
+ >
+ );
+ }
+
+ return (
+
+
{
+ invoice ? setDefaults() : navigate(-1);
+ }}
+ icon={}
+ />
+ }
+ >
+ {t("title")}
+
+ {invoice ? (
+
{renderInvoice()}
+ ) : (
+
+ )}
+
+ );
+}
+
+export default ReceiveInvoice;
diff --git a/src/app/screens/Send/index.tsx b/src/app/screens/Send/index.tsx
index 4c31a1f3b3..e5f52d95d7 100644
--- a/src/app/screens/Send/index.tsx
+++ b/src/app/screens/Send/index.tsx
@@ -154,7 +154,7 @@ function Send() {
endAdornment={}
/>
-
+