diff --git a/src/assets/icons/tinyArrow.svg b/src/assets/icons/tinyArrow.svg new file mode 100644 index 0000000..4e0a5a0 --- /dev/null +++ b/src/assets/icons/tinyArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 94b7622..91c8dc1 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -174,7 +174,9 @@ export function CombinedActivity(props: { limit?: number }) { - No activity to show +
+ Receive some sats get started +
= 0}> @@ -198,5 +200,5 @@ export function CombinedActivity(props: { limit?: number }) { - ) + ); } diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index 289d4c6..af76752 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -10,17 +10,17 @@ function prettyPrintAmount(n?: number | bigint): string { } export function Amount(props: { - amountSats: bigint | number | undefined - showFiat?: boolean - loading?: boolean + amountSats: bigint | number | undefined; + showFiat?: boolean; + loading?: boolean; + centered?: boolean; }) { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); - const amountInUsd = () => - satsToUsd(state.price, Number(props.amountSats) || 0, true) + const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true); return ( -
+

{props.loading ? "..." : prettyPrintAmount(props.amountSats)}  SATS @@ -32,7 +32,7 @@ export function Amount(props: {

- ) + ); } export function AmountSmall(props: { diff --git a/src/components/App.tsx b/src/components/App.tsx index 7dcc14c..b5b54d6 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -9,6 +9,7 @@ import { CombinedActivity } from './Activity'; import userClock from '~/assets/icons/user-clock.svg'; import { useMegaStore } from '~/state/megaStore'; import { Show } from 'solid-js'; +import { ExternalLink } from "./layout/ExternalLink"; export default function App() { const [state, _actions] = useMegaStore(); @@ -44,13 +45,11 @@ export default function App() {

Bugs? Feedback?{" "} - - Create an issue - + + + Create an issue + +

diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index 28b17c6..9cf09e6 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -1,73 +1,76 @@ -import { Show, Suspense } from "solid-js"; +import { Show } from "solid-js"; import { Button, FancyCard, Indicator } from "~/components/layout"; import { useMegaStore } from "~/state/megaStore"; import { Amount } from "./Amount"; import { A, useNavigate } from "solid-start"; -import shuffle from "~/assets/icons/shuffle.svg" - -function prettyPrintAmount(n?: number | bigint): string { - if (!n || n.valueOf() === 0) { - return "0" - } - return n.toLocaleString() -} +import shuffle from "~/assets/icons/shuffle.svg"; export function LoadingShimmer() { - return (
-

-
-

-

-
-

-
) + return ( +
+

+
+

+

+
+

+
+ ); } -const STYLE = "px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold" +const STYLE = + "px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold"; export default function BalanceBox(props: { loading?: boolean }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore(); - const emptyBalance = () => (state.balance?.confirmed || 0n) === 0n && (state.balance?.lightning || 0n) === 0n + const emptyBalance = () => + (state.balance?.confirmed || 0n) === 0n && + (state.balance?.lightning || 0n) === 0n && + (state.balance?.unconfirmed || 0n) === 0n; - const navigate = useNavigate() + const navigate = useNavigate(); - return ( - <> - - }> - - - + const totalOnchain = () => (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n); - Syncing}> - }> -
- -
- - swap - -
-
-
- - -
-
- Unconfirmed Balance -
-
- {prettyPrintAmount(state.balance?.unconfirmed)} SATS -
-
-
-
-
-
- - -
- - ) + return ( + <> + + }> + + + + + Syncing} + > + }> +
+ + +
+ + swap + +
+
+
+
+
+
+ + +
+ + ); } diff --git a/src/components/KitchenSink.tsx b/src/components/KitchenSink.tsx index bfa9cef..7b90dfd 100644 --- a/src/components/KitchenSink.tsx +++ b/src/components/KitchenSink.tsx @@ -11,170 +11,187 @@ import { ConfirmDialog } from "~/components/Dialog"; import { showToast } from "~/components/Toaster"; import { ImportExport } from "~/components/ImportExport"; import { Network } from "~/logic/mutinyWalletSetup"; +import { ExternalLink } from "./layout/ExternalLink"; // TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise -type RefetchPeersType = (info?: unknown) => MutinyPeer[] | Promise | null | undefined +type RefetchPeersType = ( + info?: unknown +) => MutinyPeer[] | Promise | null | undefined; function PeerItem(props: { peer: MutinyPeer }) { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); - const handleDisconnectPeer = async () => { - const nodes = await state.mutiny_wallet?.list_nodes(); - const firstNode = nodes[0] as string || "" + const handleDisconnectPeer = async () => { + const nodes = await state.mutiny_wallet?.list_nodes(); + const firstNode = (nodes[0] as string) || ""; - if (props.peer.is_connected) { - await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey); - } else { - await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey); - } - }; + if (props.peer.is_connected) { + await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey); + } else { + await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey); + } + }; - return ( - - -

- {">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey} -

-
- - -
-                        {JSON.stringify(props.peer, null, 2)}
-                    
- -
-
-
- ) + return ( + + +

+ {">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey} +

+
+ + +
+            {JSON.stringify(props.peer, null, 2)}
+          
+ +
+
+
+ ); } function PeersList() { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); - const getPeers = async () => { - return await state.mutiny_wallet?.list_peers() as Promise - }; + const getPeers = async () => { + return (await state.mutiny_wallet?.list_peers()) as Promise; + }; - const [peers, { refetch }] = createResource(getPeers); + const [peers, { refetch }] = createResource(getPeers); - createEffect(() => { - // refetch peers every 5 seconds - const interval = setTimeout(() => { - refetch(); - }, 5000); - onCleanup(() => { - clearInterval(interval); - }); - }) + createEffect(() => { + // refetch peers every 5 seconds + const interval = setTimeout(() => { + refetch(); + }, 5000); + onCleanup(() => { + clearInterval(interval); + }); + }); - return ( - <> - - Peers - - {/* By wrapping this in a suspense I don't cause the page to jump to the top */} - - - No peers}> - {(peer) => ( - - )} - - - - - - - ) + return ( + <> + Peers + {/* By wrapping this in a suspense I don't cause the page to jump to the top */} + + + No peers}> + {(peer) => } + + + + + + + ); } function ConnectPeer(props: { refetchPeers: RefetchPeersType }) { - const [state, _] = useMegaStore() + const [state, _] = useMegaStore(); - const [value, setValue] = createSignal(""); + const [value, setValue] = createSignal(""); - const onSubmit = async (e: SubmitEvent) => { - e.preventDefault(); + const onSubmit = async (e: SubmitEvent) => { + e.preventDefault(); - const peerConnectString = value().trim(); - const nodes = await state.mutiny_wallet?.list_nodes(); - const firstNode = nodes[0] as string || "" + const peerConnectString = value().trim(); + const nodes = await state.mutiny_wallet?.list_nodes(); + const firstNode = (nodes[0] as string) || ""; - await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString) + await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString); - await props.refetchPeers() + await props.refetchPeers(); - setValue(""); - }; + setValue(""); + }; - return ( - -
- - Connect Peer - - Expecting something like mutiny:abc123... - - -
-
- ) + return ( + +
+ + Connect Peer + + + Expecting something like mutiny:abc123... + + + +
+
+ ); } +type RefetchChannelsListType = ( + info?: unknown +) => MutinyChannel[] | Promise | null | undefined; -type RefetchChannelsListType = (info?: unknown) => MutinyChannel[] | Promise | null | undefined +function ChannelItem(props: { channel: MutinyChannel; network?: Network }) { + const [state, _] = useMegaStore(); -function ChannelItem(props: { channel: MutinyChannel, network?: Network }) { - const [state, _] = useMegaStore() + const [confirmOpen, setConfirmOpen] = createSignal(false); + const [confirmLoading, setConfirmLoading] = createSignal(false); - const [confirmOpen, setConfirmOpen] = createSignal(false); - const [confirmLoading, setConfirmLoading] = createSignal(false); + function handleCloseChannel() { + setConfirmOpen(true); + } - function handleCloseChannel() { - setConfirmOpen(true); + async function confirmCloseChannel() { + setConfirmLoading(true); + try { + await state.mutiny_wallet?.close_channel(props.channel.outpoint as string); + } catch (e) { + console.error(e); + showToast(eify(e)); } + setConfirmLoading(false); + setConfirmOpen(false); + } - async function confirmCloseChannel() { - setConfirmLoading(true); - try { - await state.mutiny_wallet?.close_channel(props.channel.outpoint as string) - } catch (e) { - console.error(e); - showToast(eify(e)); - } - setConfirmLoading(false); - setConfirmOpen(false); - } - - return ( - - -

- {">"} {props.channel.peer} -

-
- - -
-                        {JSON.stringify(props.channel, null, 2)}
-                    
- - Mempool Link - - - -
- setConfirmOpen(false)} loading={confirmLoading()}> -

Are you sure you want to close this channel?

-
-
-
- ) + return ( + + +

+ {">"} {props.channel.peer} +

+
+ + +
+            {JSON.stringify(props.channel, null, 2)}
+          
+ + View Transaction + + +
+ setConfirmOpen(false)} + loading={confirmLoading()} + > +

Are you sure you want to close this channel?

+
+
+
+ ); } function ChannelsList() { @@ -258,44 +275,40 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) { const network = state.mutiny_wallet?.get_network() as Network; return ( - <> - -
- - Pubkey - - - - Amount - - - -
-
- -
-                    {JSON.stringify(newChannel()?.outpoint, null, 2)}
-                
-
{newChannel()?.outpoint}
- - Mempool Link - -
- -
{creationError()?.message}
-
- - ) + <> + +
+ + Pubkey + + + + Amount + + + +
+
+ +
+            {JSON.stringify(newChannel()?.outpoint, null, 2)}
+          
+
{newChannel()?.outpoint}
+ + View Transaction + +
+ +
{creationError()?.message}
+
+ + ); } function LnUrlAuth() { diff --git a/src/components/layout/Button.tsx b/src/components/layout/Button.tsx index 30b60d3..f92e999 100644 --- a/src/components/layout/Button.tsx +++ b/src/components/layout/Button.tsx @@ -4,29 +4,33 @@ import { Dynamic } from "solid-js/web"; import { A } from "solid-start"; import { LoadingSpinner } from "."; -const button = cva("p-3 rounded-xl font-semibold disabled:opacity-50 disabled:grayscale transition", { +const button = cva( + "p-3 rounded-xl font-semibold disabled:opacity-20 disabled:grayscale transition", + { variants: { - // TODO: button hover has to work different than buttonlinks (like disabled state) - intent: { - active: "bg-white text-black border border-white hover:text-[#3B6CCC]", - inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]", - glowy: "bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue", - blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button", - red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button", - green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button", - }, - layout: { - flex: "flex-1 text-xl", - pad: "px-8 text-xl", - small: "px-4 py-2 w-auto text-lg", - xs: "px-4 py-2 w-auto rounded-lg text-base" - }, + // TODO: button hover has to work different than buttonlinks (like disabled state) + intent: { + active: "bg-white text-black border border-white hover:text-[#3B6CCC]", + inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]", + glowy: + "bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue", + blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button", + red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button", + green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button" + }, + layout: { + flex: "flex-1 text-xl", + pad: "px-8 text-xl", + small: "px-4 py-2 w-auto text-lg", + xs: "px-4 py-2 w-auto rounded-lg text-base" + } }, defaultVariants: { - intent: "inactive", - layout: "flex" - }, -}); + intent: "inactive", + layout: "flex" + } + } +); // Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx diff --git a/src/components/layout/ExternalLink.tsx b/src/components/layout/ExternalLink.tsx new file mode 100644 index 0000000..7d0e7ab --- /dev/null +++ b/src/components/layout/ExternalLink.tsx @@ -0,0 +1,21 @@ +import { ParentComponent } from "solid-js"; + +export const ExternalLink: ParentComponent<{ href: string }> = (props) => { + return ( + + {props.children}{" "} + + + + + ); +}; diff --git a/src/components/layout/FullscreenModal.tsx b/src/components/layout/FullscreenModal.tsx deleted file mode 100644 index 0c50348..0000000 --- a/src/components/layout/FullscreenModal.tsx +++ /dev/null @@ -1,49 +0,0 @@ - -import { Dialog } from "@kobalte/core"; -import { JSX } from "solid-js"; -import { Button, LargeHeader } from "~/components/layout"; -import close from "~/assets/icons/close.svg"; -import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs"; - -type FullscreenModalProps = { - title: string, - open: boolean, - setOpen: (open: boolean) => void, - children?: JSX.Element, - onConfirm?: () => void - confirmText?: string -} - -export function FullscreenModal(props: FullscreenModalProps) { - - const onNice = () => { - props.onConfirm ? props.onConfirm() : props.setOpen(false) - } - - return ( - - -
- -
- - - {props.title} - - - - Close - -
- - {props.children} - -
- -
-
-
-
-
- ) -} \ No newline at end of file diff --git a/src/components/layout/Radio.tsx b/src/components/layout/Radio.tsx index cf837f9..5a78c8a 100644 --- a/src/components/layout/Radio.tsx +++ b/src/components/layout/Radio.tsx @@ -6,34 +6,49 @@ type Choices = { value: string, label: string, caption: string }[] // TODO: how could would it be if we could just pass the estimated fees in here? export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) { return ( - // TODO: rewrite this with CVA, props are bad for tailwind - - - {choice => - + + {(choice) => ( + +
+ + + + + +
+
-
- - - - - -
-
{choice.label}
- -
{choice.caption}
-
-
-
-
- - } - - - ) + {choice.label} +
+ +
{choice.caption}
+
+
+
+
+
+ )} +
+
+ ); } \ No newline at end of file diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index d7cd3f8..92c4da8 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -37,17 +37,24 @@ export const InnerCard: ParentComponent<{ title?: string }> = (props) => { ) } -export const FancyCard: ParentComponent<{ title?: string, tag?: JSX.Element }> = (props) => { - return ( -
-
- {props.title && {props.title}} - {props.tag && props.tag} -
- {props.children} +export const FancyCard: ParentComponent<{ + title?: string; + subtitle?: string; + tag?: JSX.Element; +}> = (props) => { + return ( +
+
+
+ {props.title && {props.title}} + {props.subtitle && {props.subtitle}}
- ) -} + {props.tag && props.tag} +
+ {props.children} +
+ ); +}; export const SafeArea: ParentComponent = (props) => { return ( diff --git a/src/components/successfail/MegaCheck.tsx b/src/components/successfail/MegaCheck.tsx new file mode 100644 index 0000000..a681a62 --- /dev/null +++ b/src/components/successfail/MegaCheck.tsx @@ -0,0 +1,5 @@ +import megacheck from "~/assets/icons/megacheck.png"; + +export function MegaCheck() { + return success; +} diff --git a/src/components/successfail/MegaEx.tsx b/src/components/successfail/MegaEx.tsx new file mode 100644 index 0000000..7468d15 --- /dev/null +++ b/src/components/successfail/MegaEx.tsx @@ -0,0 +1,5 @@ +import megaex from "~/assets/icons/megaex.png"; + +export function MegaEx() { + return fail; +} diff --git a/src/components/successfail/SuccessModal.tsx b/src/components/successfail/SuccessModal.tsx new file mode 100644 index 0000000..1c3e13f --- /dev/null +++ b/src/components/successfail/SuccessModal.tsx @@ -0,0 +1,43 @@ +import { Dialog } from "@kobalte/core"; +import { JSX } from "solid-js"; +import { Button, LargeHeader } from "~/components/layout"; +import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs"; + +type SuccessModalProps = { + title: string; + open: boolean; + setOpen: (open: boolean) => void; + children?: JSX.Element; + onConfirm?: () => void; + confirmText?: string; +}; + +export function SuccessModal(props: SuccessModalProps) { + const onNice = () => { + props.onConfirm ? props.onConfirm() : props.setOpen(false); + }; + + //
+ return ( + + +
+ +
+ + {props.title} + +
+
+ + {props.children} + +
+ +
+ +
+ + + ); +} diff --git a/src/root.css b/src/root.css index a69d595..f5540cc 100644 --- a/src/root.css +++ b/src/root.css @@ -50,4 +50,4 @@ select { background-position: right 0.75rem center; background-size: 20px 20px; background-repeat: no-repeat; -} +} \ No newline at end of file diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index 17129c6..0a56d0d 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -7,254 +7,285 @@ import { useMegaStore } from "~/state/megaStore"; import { objectToSearchParams } from "~/utils/objectToSearchParams"; import mempoolTxUrl from "~/utils/mempoolTxUrl"; import { Amount } from "~/components/Amount"; -import { FullscreenModal } from "~/components/layout/FullscreenModal"; import { BackLink } from "~/components/layout/BackLink"; import { TagEditor } from "~/components/TagEditor"; import { StyledRadioGroup } from "~/components/layout/Radio"; import { showToast } from "~/components/Toaster"; import { useNavigate } from "solid-start"; -import megacheck from "~/assets/icons/megacheck.png"; import { AmountCard } from "~/components/AmountCard"; import { ShareCard } from "~/components/ShareCard"; import { BackButton } from "~/components/layout/BackButton"; import { MutinyTagItem } from "~/utils/tags"; import { Network } from "~/logic/mutinyWalletSetup"; +import { SuccessModal } from "~/components/successfail/SuccessModal"; +import { MegaCheck } from "~/components/successfail/MegaCheck"; +import { ExternalLink } from "~/components/layout/ExternalLink"; type OnChainTx = { - transaction: { - version: number - lock_time: number - input: Array<{ - previous_output: string - script_sig: string - sequence: number - witness: Array - }> - output: Array<{ - value: number - script_pubkey: string - }> - } - txid: string - received: number - sent: number - confirmation_time: { - height: number - timestamp: number - } -} + transaction: { + version: number; + lock_time: number; + input: Array<{ + previous_output: string; + script_sig: string; + sequence: number; + witness: Array; + }>; + output: Array<{ + value: number; + script_pubkey: string; + }>; + }; + txid: string; + received: number; + sent: number; + confirmation_time: { + height: number; + timestamp: number; + }; +}; -const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }] +const RECEIVE_FLAVORS = [ + { value: "unified", label: "Unified", caption: "Sender decides" }, + { value: "lightning", label: "Lightning", caption: "Fast and cool" }, + { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" } +]; -type ReceiveFlavor = "unified" | "lightning" | "onchain" -type ReceiveState = "edit" | "show" | "paid" +type ReceiveFlavor = "unified" | "lightning" | "onchain"; +type ReceiveState = "edit" | "show" | "paid"; type PaidState = "lightning_paid" | "onchain_paid"; export default function Receive() { - const [state, _actions] = useMegaStore() - const navigate = useNavigate(); + const [state, _actions] = useMegaStore(); + const navigate = useNavigate(); - const [amount, setAmount] = createSignal("") - const [receiveState, setReceiveState] = createSignal("edit") - const [bip21Raw, setBip21Raw] = createSignal(); - const [unified, setUnified] = createSignal("") - const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true) + const [amount, setAmount] = createSignal(""); + const [receiveState, setReceiveState] = createSignal("edit"); + const [bip21Raw, setBip21Raw] = createSignal(); + const [unified, setUnified] = createSignal(""); + const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true); - // Tagging stuff - const [selectedValues, setSelectedValues] = createSignal([]); + // Tagging stuff + const [selectedValues, setSelectedValues] = createSignal([]); - // The data we get after a payment - const [paymentTx, setPaymentTx] = createSignal(); - const [paymentInvoice, setPaymentInvoice] = createSignal(); + // The data we get after a payment + const [paymentTx, setPaymentTx] = createSignal(); + const [paymentInvoice, setPaymentInvoice] = createSignal(); - // The flavor of the receive - const [flavor, setFlavor] = createSignal("unified"); + // The flavor of the receive + const [flavor, setFlavor] = createSignal("unified"); - const receiveString = createMemo(() => { - if (unified() && receiveState() === "show") { - if (flavor() === "unified") { - return unified(); - } else if (flavor() === "lightning") { - return bip21Raw()?.invoice ?? ""; - } else if (flavor() === "onchain") { - return bip21Raw()?.address ?? ""; - } + const receiveString = createMemo(() => { + if (unified() && receiveState() === "show") { + if (flavor() === "unified") { + return unified(); + } else if (flavor() === "lightning") { + return bip21Raw()?.invoice ?? ""; + } else if (flavor() === "onchain") { + return bip21Raw()?.address ?? ""; + } + } + }); + function clearAll() { + setAmount(""); + setReceiveState("edit"); + setBip21Raw(undefined); + setUnified(""); + setPaymentTx(undefined); + setPaymentInvoice(undefined); + setSelectedValues([]); + } + + async function processContacts(contacts: Partial[]): Promise { + console.log("Processing contacts", contacts); + + if (contacts.length) { + const first = contacts![0]; + + if (!first.name) { + console.error("Something went wrong with contact creation, proceeding anyway"); + return []; + } + + if (!first.id && first.name) { + console.error("Creating new contact", first.name); + const c = new Contact(first.name, undefined, undefined, undefined); + const newContactId = await state.mutiny_wallet?.create_new_contact(c); + if (newContactId) { + return [newContactId]; } - }) + } - function clearAll() { - setAmount("") - setReceiveState("edit") - setBip21Raw(undefined) - setUnified("") - setPaymentTx(undefined) - setPaymentInvoice(undefined) - setSelectedValues([]) + if (first.id) { + console.error("Using existing contact", first.name, first.id); + return [first.id]; + } } - async function processContacts(contacts: Partial[]): Promise { - console.log("Processing contacts", contacts) + console.error("Something went wrong with contact creation, proceeding anyway"); + return []; + } - if (contacts.length) { - const first = contacts![0]; + async function getUnifiedQr(amount: string) { + const bigAmount = BigInt(amount); + try { + const tags = await processContacts(selectedValues()); + const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags); + // Save the raw info so we can watch the address and invoice + setBip21Raw(raw); - if (!first.name) { - console.error("Something went wrong with contact creation, proceeding anyway") - return [] - } - - if (!first.id && first.name) { - console.error("Creating new contact", first.name) - const c = new Contact(first.name, undefined, undefined, undefined); - const newContactId = await state.mutiny_wallet?.create_new_contact(c); - if (newContactId) { - return [newContactId]; - } - } - - if (first.id) { - console.error("Using existing contact", first.name, first.id) - return [first.id]; - } - - } - - console.error("Something went wrong with contact creation, proceeding anyway") - return [] + const params = objectToSearchParams({ + amount: raw?.btc_amount, + lightning: raw?.invoice + }); + return `bitcoin:${raw?.address}?${params}`; + } catch (e) { + showToast(new Error("Couldn't create invoice. Are you asking for enough?")); + console.error(e); } + } - async function getUnifiedQr(amount: string) { - const bigAmount = BigInt(amount); - try { - const tags = await processContacts(selectedValues()); - const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags); - // Save the raw info so we can watch the address and invoice - setBip21Raw(raw); + async function onSubmit(e: Event) { + e.preventDefault(); - const params = objectToSearchParams({ - amount: raw?.btc_amount, - lightning: raw?.invoice - }) + const unifiedQr = await getUnifiedQr(amount()); - return `bitcoin:${raw?.address}?${params}` + setUnified(unifiedQr || ""); + setReceiveState("show"); + setShouldShowAmountEditor(false); + } - } catch (e) { - showToast(new Error("Couldn't create invoice. Are you asking for enough?")) - console.error(e) - } + async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise { + if (bip21) { + console.log("checking if paid..."); + const lightning = bip21.invoice; + const address = bip21.address; + + const invoice = await state.mutiny_wallet?.get_invoice(lightning); + + if (invoice && invoice.paid) { + setReceiveState("paid"); + setPaymentInvoice(invoice); + return "lightning_paid"; + } + + const tx = (await state.mutiny_wallet?.check_address(address)) as OnChainTx | undefined; + + if (tx) { + setReceiveState("paid"); + setPaymentTx(tx); + return "onchain_paid"; + } } + } - async function onSubmit(e: Event) { - e.preventDefault(); + const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); - const unifiedQr = await getUnifiedQr(amount()) + const network = state.mutiny_wallet?.get_network() as Network; - setUnified(unifiedQr || "") - setReceiveState("show") - setShouldShowAmountEditor(false) - } - - async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise { - if (bip21) { - console.log("checking if paid...") - const lightning = bip21.invoice - const address = bip21.address - - const invoice = await state.mutiny_wallet?.get_invoice(lightning) - - if (invoice && invoice.paid) { - setReceiveState("paid") - setPaymentInvoice(invoice) - return "lightning_paid" - } - - const tx = await state.mutiny_wallet?.check_address(address) as OnChainTx | undefined; - - if (tx) { - setReceiveState("paid") - setPaymentTx(tx) - return "onchain_paid" - } - } - } - - const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); - - const network = state.mutiny_wallet?.get_network() as Network; - - createEffect(() => { - const interval = setInterval(() => { - if (receiveState() === "show") refetch(); - }, 1000); // Poll every second - onCleanup(() => { - clearInterval(interval); - }); + createEffect(() => { + const interval = setInterval(() => { + if (receiveState() === "show") refetch(); + }, 1000); // Poll every second + onCleanup(() => { + clearInterval(interval); }); + }); - return ( - - - - }> - setReceiveState("edit")} title="Edit" /> - - Checking}>Receive Bitcoin - - -
- + return ( + + + + }> + setReceiveState("edit")} title="Edit" /> + + Checking}> + Receive Bitcoin + + + +
+ - - - + + + -
- -
- - - -
- -
-

Show or share this code with the sender

- -
- - { if (!open) clearAll() }} - onConfirm={() => { clearAll(); navigate("/"); }} - > -
- success - -
-
-
- - { if (!open) clearAll() }} - onConfirm={() => { clearAll(); navigate("/"); }} - > - - - - - - - - - ) +
+ +
+ + + +
+ +
+

Show or share this code with the sender

+ +
+ + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + > + + + + + + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + > + + + + View Transaction + + + + + + + + + ); } \ No newline at end of file diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 36c06f2..b4c8f3f 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -21,7 +21,6 @@ import { StyledRadioGroup } from "~/components/layout/Radio"; import { ParsedParams, toParsedParams } from "./Scanner"; import { showToast } from "~/components/Toaster"; import eify from "~/utils/eify"; -import { FullscreenModal } from "~/components/layout/FullscreenModal"; import megacheck from "~/assets/icons/megacheck.png"; import megaex from "~/assets/icons/megaex.png"; import mempoolTxUrl from "~/utils/mempoolTxUrl"; @@ -33,6 +32,8 @@ import { AmountCard } from "~/components/AmountCard"; import { MutinyTagItem } from "~/utils/tags"; import { BackButton } from "~/components/layout/BackButton"; import { Network } from "~/logic/mutinyWalletSetup"; +import { SuccessModal } from "~/components/successfail/SuccessModal"; +import { ExternalLink } from "~/components/layout/ExternalLink"; export type SendSource = "lightning" | "onchain"; @@ -121,7 +122,7 @@ function DestinationInput(props: { class="p-2 rounded-lg bg-white/10 placeholder-neutral-400" />