diff --git a/package.json b/package.json index b1b9765b33..cefca37e89 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "puppeteer": "^21.0.3", "stream-browserify": "^3.0.0", "swc-loader": "^0.2.3", + "tailwindcss-3d": "^1.0.0", "terser-webpack-plugin": "^5.3.9", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.2.2", diff --git a/scripts/check-changed-translations.js b/scripts/check-changed-translations.js index afa6f26622..8607a0827b 100644 --- a/scripts/check-changed-translations.js +++ b/scripts/check-changed-translations.js @@ -18,7 +18,7 @@ function compareObjects(obj1, obj2, parentPath = "") { obj2[key], parentPath ? `${parentPath}.${key}` : key ); - } else if (obj1[key] !== obj2[key]) { + } else if (obj1[key] !== obj2[key] && obj2[key] !== undefined) { const title = `Translation source ${parentPath}.${key} has changed`; const message = `Consider running \`node scripts/remove-outdated-translations.js ${parentPath}.${key}\` to reset existing translations.`; console.log( diff --git a/src/app/components/Avatar/index.tsx b/src/app/components/Avatar/index.tsx index a53718a533..169c28789f 100644 --- a/src/app/components/Avatar/index.tsx +++ b/src/app/components/Avatar/index.tsx @@ -1,10 +1,12 @@ import { useEffect, useRef } from "react"; import { generateSvgGAvatar } from "~/app/components/Avatar/generator"; +import { classNames } from "~/app/utils"; type Props = { name: string; size: number; url?: string; + className?: string; }; const Avatar = (props: Props) => { @@ -20,6 +22,7 @@ const Avatar = (props: Props) => { const AvatarImage = (props: Props) => { return (
) => { return ( {accountLoading ? ( - + ) : ( -
+
{balancesDecorated.fiatBalance && ( <>~{balancesDecorated.fiatBalance} )} diff --git a/src/app/components/Header/index.tsx b/src/app/components/Header/index.tsx index 70e0eed289..5d3c0ade01 100644 --- a/src/app/components/Header/index.tsx +++ b/src/app/components/Header/index.tsx @@ -6,7 +6,7 @@ type Props = { function Header({ children, headerLeft, headerRight }: Props) { return ( -
+
{headerLeft}

{children}

diff --git a/src/app/components/IconButton/index.tsx b/src/app/components/IconButton/index.tsx index 8a0bc572cb..9e882212c7 100644 --- a/src/app/components/IconButton/index.tsx +++ b/src/app/components/IconButton/index.tsx @@ -6,7 +6,7 @@ type Props = { function IconButton({ onClick, icon }: Props) { return (
-
+
- )} - {!paid && ( - <> -
-
- -
- {pollingForPayment && ( -
- - {t("payment.waiting")} -
- )} - - {!pollingForPayment && ( -
- - )} - {paid && ( - { - confetti && confetti.reset(); - }} - style={{ pointerEvents: "none" }} - /> - )} - - ); - } + setLoadingLightningAddress(false); + })(); + }, [auth.account]); return (
@@ -251,7 +54,7 @@ function Receive() { headerLeft={ { - invoice ? setDefaults() : navigate(-1); + navigate(-1); }} icon={} /> @@ -259,114 +62,97 @@ function Receive() { > {t("title")} - {invoice ? ( - {renderInvoice()} - ) : ( -
-
-
- -
-
- -
-
- +
+ +
+ {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 && ( +
+
+
+ +
+

{t("success")}

+
+
+ )} +
+ {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={} />
-
+