design touchup (#184)

This commit is contained in:
Paul Miller
2023-05-29 11:34:51 -05:00
committed by GitHub
parent b299eac544
commit caf5811637
18 changed files with 726 additions and 639 deletions

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -174,7 +174,9 @@ export function CombinedActivity(props: { limit?: number }) {
<LoadingSpinner wide />
</Match>
<Match when={activity.state === "ready" && activity().length === 0}>
<NiceP>No activity to show</NiceP>
<div class="w-full text-center">
<NiceP>Receive some sats get started</NiceP>
</div>
</Match>
<Match when={activity.state === "ready" && activity().length >= 0}>
<For each={activity.latest}>
@@ -198,5 +200,5 @@ export function CombinedActivity(props: { limit?: number }) {
</For>
</Match>
</Switch>
)
);
}

View File

@@ -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 (
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2" classList={{ "items-center": props.centered }}>
<h1 class="text-4xl font-light">
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}&nbsp;
<span class="text-xl">SATS</span>
@@ -32,7 +32,7 @@ export function Amount(props: {
</h2>
</Show>
</div>
)
);
}
export function AmountSmall(props: {

View File

@@ -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() {
</Card>
<p class="self-center text-neutral-500 mt-4">
Bugs? Feedback?{" "}
<a
class="text-neutral-400"
target="_blank"
href="https://github.com/MutinyWallet/mutiny-web/issues"
>
Create an issue
</a>
<span class="text-neutral-400">
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
Create an issue
</ExternalLink>
</span>
</p>
</DefaultMain>
<NavBar activeTab="home" />

View File

@@ -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 (<div class="flex flex-col gap-2 animate-pulse">
<h1 class="text-4xl font-light">
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
</h1>
<h2 class="text-xl font-light text-white/70" >
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
</h2>
</div>)
return (
<div class="flex flex-col gap-2 animate-pulse">
<h1 class="text-4xl font-light">
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
</h1>
<h2 class="text-xl font-light text-white/70">
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
</h2>
</div>
);
}
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 (
<>
<FancyCard title="Lightning">
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Amount amountSats={state.balance?.lightning || 0} showFiat />
</Show>
</FancyCard>
const totalOnchain = () => (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div class="flex justify-between">
<Amount amountSats={state.balance?.confirmed} showFiat />
<div class="self-end justify-self-end">
<A href="/swap" class={STYLE}>
<img src={shuffle} alt="swap" class="h-8 w-8" />
</A>
</div>
</div>
</Show>
<Suspense>
<Show when={state.balance?.unconfirmed}>
<div class="flex flex-col gap-2">
<header class='text-sm font-semibold uppercase text-white/50'>
Unconfirmed Balance
</header>
<div class="text-white/50">
{prettyPrintAmount(state.balance?.unconfirmed)} <span class='text-sm'>SATS</span>
</div>
</div>
</Show>
</Suspense>
</FancyCard>
<div class="flex gap-2 py-4">
<Button onClick={() => navigate("/send")} disabled={emptyBalance() || props.loading} intent="green">Send</Button>
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">Receive</Button>
</div>
</>
)
return (
<>
<FancyCard title="Lightning">
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Amount amountSats={state.balance?.lightning || 0} showFiat />
</Show>
</FancyCard>
<FancyCard
title="On-Chain"
subtitle={state.balance?.unconfirmed ? "Unconfirmed" : undefined}
tag={state.is_syncing && <Indicator>Syncing</Indicator>}
>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div class="flex justify-between">
<Amount amountSats={totalOnchain()} showFiat />
<Show when={!emptyBalance()}>
<div class="self-end justify-self-end">
<A href="/swap" class={STYLE}>
<img src={shuffle} alt="swap" class="h-8 w-8" />
</A>
</div>
</Show>
</div>
</Show>
</FancyCard>
<div class="flex gap-2 py-4">
<Button
onClick={() => navigate("/send")}
disabled={emptyBalance() || props.loading}
intent="green"
>
Send
</Button>
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">
Receive
</Button>
</div>
</>
);
}

View File

@@ -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<MutinyPeer[] | undefined> | null | undefined
type RefetchPeersType = (
info?: unknown
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | 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 (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.peer, null, 2)}
</pre>
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>Disconnect</Button>
</VStack>
</Collapsible.Content>
</Collapsible.Root>
)
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.peer, null, 2)}
</pre>
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>
Disconnect
</Button>
</VStack>
</Collapsible.Content>
</Collapsible.Root>
);
}
function PeersList() {
const [state, _] = useMegaStore()
const [state, _] = useMegaStore();
const getPeers = async () => {
return await state.mutiny_wallet?.list_peers() as Promise<MutinyPeer[]>
};
const getPeers = async () => {
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
};
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 (
<>
<SmallHeader>
Peers
</SmallHeader>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense>
<VStack>
<For each={peers()} fallback={<code>No peers</code>}>
{(peer) => (
<PeerItem peer={peer} />
)}
</For>
</VStack>
</Suspense>
<Button layout="small" onClick={refetch}>Refresh Peers</Button>
<ConnectPeer refetchPeers={refetch} />
</>
)
return (
<>
<SmallHeader>Peers</SmallHeader>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense>
<VStack>
<For each={peers()} fallback={<code>No peers</code>}>
{(peer) => <PeerItem peer={peer} />}
</For>
</VStack>
</Suspense>
<Button layout="small" onClick={refetch}>
Refresh Peers
</Button>
<ConnectPeer refetchPeers={refetch} />
</>
);
}
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 (
<InnerCard>
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={value()}
onChange={setValue}
validationState={(value() == "") ? "valid" : "invalid"}
class="flex flex-col gap-4"
>
<TextField.Label class="text-sm font-semibold uppercase" >Connect Peer</TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" placeholder="mutiny:028241..." />
<TextField.ErrorMessage class="text-red-500">Expecting something like mutiny:abc123...</TextField.ErrorMessage>
</TextField.Root>
<Button layout="small" type="submit">Connect</Button>
</form >
</InnerCard>
)
return (
<InnerCard>
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
<TextField.Root
value={value()}
onChange={setValue}
validationState={value() == "" ? "valid" : "invalid"}
class="flex flex-col gap-4"
>
<TextField.Label class="text-sm font-semibold uppercase">Connect Peer</TextField.Label>
<TextField.Input
class="w-full p-2 rounded-lg text-black"
placeholder="mutiny:028241..."
/>
<TextField.ErrorMessage class="text-red-500">
Expecting something like mutiny:abc123...
</TextField.ErrorMessage>
</TextField.Root>
<Button layout="small" type="submit">
Connect
</Button>
</form>
</InnerCard>
);
}
type RefetchChannelsListType = (
info?: unknown
) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined;
type RefetchChannelsListType = (info?: unknown) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | 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 (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.channel.peer}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.channel, null, 2)}
</pre>
<a class="" href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
</VStack>
<ConfirmDialog open={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
<p>Are you sure you want to close this channel?</p>
</ConfirmDialog>
</Collapsible.Content>
</Collapsible.Root>
)
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.channel.peer}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.channel, null, 2)}
</pre>
<ExternalLink href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)}>
View Transaction
</ExternalLink>
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>
Close Channel
</Button>
</VStack>
<ConfirmDialog
open={confirmOpen()}
onConfirm={confirmCloseChannel}
onCancel={() => setConfirmOpen(false)}
loading={confirmLoading()}
>
<p>Are you sure you want to close this channel?</p>
</ConfirmDialog>
</Collapsible.Content>
</Collapsible.Root>
);
}
function ChannelsList() {
@@ -258,44 +275,40 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
const network = state.mutiny_wallet?.get_network() as Network;
return (
<>
<InnerCard>
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root
value={peerPubkey()}
onChange={setPeerPubkey}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<TextField.Root
value={amount()}
onChange={setAmount}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label>
<TextField.Input
type="number"
class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<Button layout="small" type="submit">Open Channel</Button>
</form >
</InnerCard>
<Show when={newChannel()}>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(newChannel()?.outpoint, null, 2)}
</pre>
<pre>{newChannel()?.outpoint}</pre>
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
</Show>
<Show when={creationError()}>
<pre>{creationError()?.message}</pre>
</Show>
</>
)
<>
<InnerCard>
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
<TextField.Root
value={peerPubkey()}
onChange={setPeerPubkey}
class="flex flex-col gap-2"
>
<TextField.Label class="text-sm font-semibold uppercase">Pubkey</TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<TextField.Root value={amount()} onChange={setAmount} class="flex flex-col gap-2">
<TextField.Label class="text-sm font-semibold uppercase">Amount</TextField.Label>
<TextField.Input type="number" class="w-full p-2 rounded-lg text-black" />
</TextField.Root>
<Button layout="small" type="submit">
Open Channel
</Button>
</form>
</InnerCard>
<Show when={newChannel()}>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(newChannel()?.outpoint, null, 2)}
</pre>
<pre>{newChannel()?.outpoint}</pre>
<ExternalLink href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)}>
View Transaction
</ExternalLink>
</Show>
<Show when={creationError()}>
<pre>{creationError()?.message}</pre>
</Show>
</>
);
}
function LnUrlAuth() {

View File

@@ -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

View File

@@ -0,0 +1,21 @@
import { ParentComponent } from "solid-js";
export const ExternalLink: ParentComponent<{ href: string }> = (props) => {
return (
<a target="_blank" rel="noopener noreferrer" href={props.href}>
{props.children}{" "}
<svg
class="inline-block"
width="16"
height="16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z"
fill="currentColor"
/>
</svg>
</a>
);
};

View File

@@ -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 (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between items-center mb-2">
<Dialog.Title>
<LargeHeader>
{props.title}
</LargeHeader>
</Dialog.Title>
<Dialog.CloseButton class="p-2 hover:bg-white/10 rounded-lg active:bg-m-blue">
<img src={close} alt="Close" />
</Dialog.CloseButton>
</div>
<Dialog.Description class="flex flex-col gap-4">
{props.children}
</Dialog.Description>
<div class="w-full flex">
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root >
)
}

View File

@@ -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
<RadioGroup.Root value={props.value} onChange={props.onValueChange}
class={"grid w-full gap-4"}
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
>
<For each={props.choices}>
{choice =>
<RadioGroup.Item value={choice.value}
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
classList={{ "ui-checked:outline-m-red": props.accent === "red", "ui-checked:outline-white": props.accent === "white", "ui-checked:outline-black/50 ui-checked:bg-white/10": props.choices.length === 1 }}
// TODO: rewrite this with CVA, props are bad for tailwind
<RadioGroup.Root
value={props.value}
onChange={props.onValueChange}
class={"grid w-full gap-4"}
classList={{
"grid-cols-2": props.choices.length === 2,
"grid-cols-3": props.choices.length === 3,
"gap-2": props.small
}}
>
<For each={props.choices}>
{(choice) => (
<RadioGroup.Item
value={choice.value}
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
classList={{
"ui-checked:outline-m-red": props.accent === "red",
"ui-checked:outline-white": props.accent === "white"
}}
>
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
<RadioGroup.ItemInput />
<RadioGroup.ItemControl>
<RadioGroup.ItemIndicator />
</RadioGroup.ItemControl>
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
<div class="block">
<div
classList={{ "text-base": props.small, "text-lg": !props.small }}
class={`font-semibold max-sm:text-sm`}
>
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
<RadioGroup.ItemInput />
<RadioGroup.ItemControl >
<RadioGroup.ItemIndicator />
</RadioGroup.ItemControl>
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
<div class="block">
<div classList={{ "text-base": props.small, "text-lg": !props.small }} class={`font-semibold max-sm:text-sm`}>{choice.label}</div>
<Show when={!props.small}>
<div class="text-sm font-light">{choice.caption}</div>
</Show>
</div>
</RadioGroup.ItemLabel>
</div>
</RadioGroup.Item>
}
</For>
</RadioGroup.Root>
)
{choice.label}
</div>
<Show when={!props.small}>
<div class="text-sm font-light">{choice.caption}</div>
</Show>
</div>
</RadioGroup.ItemLabel>
</div>
</RadioGroup.Item>
)}
</For>
</RadioGroup.Root>
);
}

View File

@@ -37,17 +37,24 @@ export const InnerCard: ParentComponent<{ title?: string }> = (props) => {
)
}
export const FancyCard: ParentComponent<{ title?: string, tag?: JSX.Element }> = (props) => {
return (
<div class='border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card'>
<div class="w-full flex justify-between items-center">
{props.title && <SmallHeader>{props.title}</SmallHeader>}
{props.tag && props.tag}
</div>
{props.children}
export const FancyCard: ParentComponent<{
title?: string;
subtitle?: string;
tag?: JSX.Element;
}> = (props) => {
return (
<div class="border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card">
<div class="w-full flex justify-between items-center">
<div class="flex gap-2">
{props.title && <SmallHeader>{props.title}</SmallHeader>}
{props.subtitle && <SmallHeader class="text-neutral-500">{props.subtitle}</SmallHeader>}
</div>
)
}
{props.tag && props.tag}
</div>
{props.children}
</div>
);
};
export const SafeArea: ParentComponent = (props) => {
return (

View File

@@ -0,0 +1,5 @@
import megacheck from "~/assets/icons/megacheck.png";
export function MegaCheck() {
return <img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] flex-shrink" />;
}

View File

@@ -0,0 +1,5 @@
import megaex from "~/assets/icons/megaex.png";
export function MegaEx() {
return <img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />;
}

View File

@@ -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);
};
// <div class="flex flex-col items-center gap-8 h-full max-w-[400px]">
return (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between items-center mb-2">
<Dialog.Title>
<LargeHeader>{props.title}</LargeHeader>
</Dialog.Title>
<div />
</div>
<Dialog.Description class="flex flex-col items-center justify-center gap-8 pb-4 h-full w-full max-w-[400px] mx-auto">
{props.children}
</Dialog.Description>
<div class="w-full flex max-w-[300px] mx-auto">
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -50,4 +50,4 @@ select {
background-position: right 0.75rem center;
background-size: 20px 20px;
background-repeat: no-repeat;
}
}

View File

@@ -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<string>
}>
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<string>;
}>;
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<ReceiveState>("edit")
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
const [unified, setUnified] = createSignal("")
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
const [amount, setAmount] = createSignal("");
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit");
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
const [unified, setUnified] = createSignal("");
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true);
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
// The data we get after a payment
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
// The data we get after a payment
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
// The flavor of the receive
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
// The flavor of the receive
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("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<MutinyTagItem>[]): Promise<string[]> {
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<MutinyTagItem>[]): Promise<string[]> {
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<PaidState | undefined> {
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<PaidState | undefined> {
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 (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<Show when={receiveState() === "show"} fallback={<BackLink />}>
<BackButton onClick={() => setReceiveState("edit")} title="Edit" />
</Show>
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>Receive Bitcoin</LargeHeader>
<Switch>
<Match when={!unified() || receiveState() === "edit"}>
<div class="flex flex-col flex-1 gap-8">
<AmountCard initialOpen={shouldShowAmountEditor()} amountSats={amount() || "0"} setAmountSats={setAmount} isAmountEditable />
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<Show when={receiveState() === "show"} fallback={<BackLink />}>
<BackButton onClick={() => setReceiveState("edit")} title="Edit" />
</Show>
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>
Receive Bitcoin
</LargeHeader>
<Switch>
<Match when={!unified() || receiveState() === "edit"}>
<div class="flex flex-col flex-1 gap-8">
<AmountCard
initialOpen={shouldShowAmountEditor()}
amountSats={amount() || "0"}
setAmountSats={setAmount}
isAmountEditable
/>
<Card title="Private tags">
<TagEditor selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Add the sender for your records" />
</Card>
<Card title="Private tags">
<TagEditor
selectedValues={selectedValues()}
setSelectedValues={setSelectedValues}
placeholder="Add the sender for your records"
/>
</Card>
<div class="flex-1" />
<Button class="w-full flex-grow-0" disabled={!amount()} intent="green" onClick={onSubmit}>Create Request</Button>
</div>
</Match>
<Match when={unified() && receiveState() === "show"}>
<StyledRadioGroup small value={flavor()} onValueChange={setFlavor} choices={RECEIVE_FLAVORS} accent="white" />
<div class="w-full bg-white rounded-xl">
<QRCodeSVG value={receiveString() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
</div>
<p class="text-neutral-400 text-center">Show or share this code with the sender</p>
<ShareCard text={receiveString() ?? ""} />
</Match>
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
<FullscreenModal
title="Payment Received"
open={!!paidState()}
setOpen={(open: boolean) => { if (!open) clearAll() }}
onConfirm={() => { clearAll(); navigate("/"); }}
>
<div class="flex flex-col items-center gap-8">
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
</div>
</FullscreenModal>
</Match>
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
<FullscreenModal
title="Payment Received"
open={!!paidState()}
setOpen={(open: boolean) => { if (!open) clearAll() }}
onConfirm={() => { clearAll(); navigate("/"); }}
>
<div class="flex flex-col items-center gap-8">
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
<Amount amountSats={paymentTx()?.received} showFiat />
<a href={mempoolTxUrl(paymentTx()?.txid, network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
</div>
</FullscreenModal>
</Match>
</Switch>
</DefaultMain>
<NavBar activeTab="receive" />
</SafeArea >
</MutinyWalletGuard>
)
<div class="flex-1" />
<Button
class="w-full flex-grow-0"
disabled={!amount()}
intent="green"
onClick={onSubmit}
>
Continue
</Button>
</div>
</Match>
<Match when={unified() && receiveState() === "show"}>
<StyledRadioGroup
small
value={flavor()}
onValueChange={setFlavor}
choices={RECEIVE_FLAVORS}
accent="white"
/>
<div class="w-full bg-white rounded-xl">
<QRCodeSVG value={receiveString() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
</div>
<p class="text-neutral-400 text-center">Show or share this code with the sender</p>
<ShareCard text={receiveString() ?? ""} />
</Match>
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
<SuccessModal
title="Payment Received"
open={!!paidState()}
setOpen={(open: boolean) => {
if (!open) clearAll();
}}
onConfirm={() => {
clearAll();
navigate("/");
}}
>
<MegaCheck />
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat centered />
</SuccessModal>
</Match>
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
<SuccessModal
title="Payment Received"
open={!!paidState()}
setOpen={(open: boolean) => {
if (!open) clearAll();
}}
onConfirm={() => {
clearAll();
navigate("/");
}}
>
<MegaCheck />
<Amount amountSats={paymentTx()?.received} showFiat centered />
<ExternalLink href={mempoolTxUrl(paymentTx()?.txid, network)}>
View Transaction
</ExternalLink>
</SuccessModal>
</Match>
</Switch>
</DefaultMain>
<NavBar activeTab="receive" />
</SafeArea>
</MutinyWalletGuard>
);
}

View File

@@ -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"
/>
<Button disabled={!props.fieldDestination} intent="blue" onClick={props.handleDecode}>
Decode
Continue
</Button>
<HStack>
<Button onClick={props.handlePaste}>
@@ -408,8 +409,8 @@ export default function Send() {
const sendButtonDisabled = createMemo(() => {
return !destination() || sending() || amountSats() === 0n;
});
const network = state.mutiny_wallet?.get_network() as Network
const network = state.mutiny_wallet?.get_network() as Network;
return (
<MutinyWalletGuard>
@@ -419,7 +420,7 @@ export default function Send() {
<BackButton onClick={() => clearAll()} title="Start Over" />
</Show>
<LargeHeader>Send Bitcoin</LargeHeader>
<FullscreenModal
<SuccessModal
title={sentDetails()?.amount ? "Sent" : "Payment Failed"}
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
open={!!sentDetails()}
@@ -431,30 +432,24 @@ export default function Send() {
navigate("/");
}}
>
<div class="flex flex-col items-center gap-8 h-full">
<Switch>
<Match when={sentDetails()?.failure_reason}>
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
{sentDetails()?.failure_reason}
</p>
</Match>
<Match when={true}>
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
<Amount amountSats={sentDetails()?.amount} showFiat />
<Show when={sentDetails()?.txid}>
<a
href={mempoolTxUrl(sentDetails()?.txid, network)}
target="_blank"
rel="noreferrer"
>
Mempool Link
</a>
</Show>
</Match>
</Switch>
</div>
</FullscreenModal>
<Switch>
<Match when={sentDetails()?.failure_reason}>
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
{sentDetails()?.failure_reason}
</p>
</Match>
<Match when={true}>
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
<Amount amountSats={sentDetails()?.amount} showFiat centered />
<Show when={sentDetails()?.txid}>
<ExternalLink href={mempoolTxUrl(sentDetails()?.txid, network)}>
View Transaction
</ExternalLink>
</Show>
</Match>
</Switch>
</SuccessModal>
<VStack biggap>
<Switch>
<Match when={address() || invoice() || nodePubkey() || lnurlp()}>
@@ -463,7 +458,7 @@ export default function Send() {
setSource={setSource}
both={!!address() && !!invoice()}
/>
<Card>
<Card title="Destination">
<VStack>
<DestinationShower
source={source()}

View File

@@ -7,12 +7,11 @@ import { showToast } from "~/components/Toaster";
import {
Button,
Card,
Checkbox,
DefaultMain,
LargeHeader,
MutinyWalletGuard,
SafeArea,
VStack,
VStack
} from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink";
import { TextField } from "~/components/layout/TextField";
@@ -22,9 +21,11 @@ import eify from "~/utils/eify";
import megaex from "~/assets/icons/megaex.png";
import megacheck from "~/assets/icons/megacheck.png";
import { InfoBox } from "~/components/InfoBox";
import { FullscreenModal } from "~/components/layout/FullscreenModal";
import { useNavigate } from "solid-start";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { SuccessModal } from "~/components/successfail/SuccessModal";
import { ExternalLink } from "~/components/layout/ExternalLink";
import { Network } from "~/logic/mutinyWalletSetup";
const CHANNEL_FEE_ESTIMATE_ADDRESS =
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
@@ -44,7 +45,6 @@ export default function Swap() {
const [source, setSource] = createSignal<SendSource>("onchain");
const [amountSats, setAmountSats] = createSignal(0n);
const [useLsp, setUseLsp] = createSignal(true);
const [isConnecting, setIsConnecting] = createSignal(false);
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
@@ -121,7 +121,7 @@ export default function Swap() {
const nodes = await state.mutiny_wallet?.list_nodes();
const firstNode = (nodes[0] as string) || "";
if (useLsp()) {
if (hasLsp()) {
const new_channel = await state.mutiny_wallet?.open_channel(
firstNode,
undefined,
@@ -147,7 +147,7 @@ export default function Swap() {
const canSwap = () => {
const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
return (!!selectedPeer() || !!useLsp()) && amountSats() >= 10000n && amountSats() <= balance;
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 10000n && amountSats() <= balance;
};
const amountWarning = () => {
@@ -165,14 +165,16 @@ export default function Swap() {
return undefined;
};
const network = state.mutiny_wallet?.get_network() as Network;
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<BackLink />
<LargeHeader>Swap to Lightning</LargeHeader>
<FullscreenModal
title={channelOpenResult()?.channel ? "Channel Opened" : "Channel Open Failed"}
<SuccessModal
title={channelOpenResult()?.channel ? "Swap Success" : "Swap Failed"}
confirmText={channelOpenResult()?.channel ? "Nice" : "Too Bad"}
open={!!channelOpenResult()}
setOpen={(open: boolean) => {
@@ -183,50 +185,38 @@ export default function Swap() {
navigate("/");
}}
>
<div class="flex flex-col items-center gap-8 pb-8">
<Switch>
<Match when={channelOpenResult()?.failure_reason}>
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
<Switch>
<Match when={channelOpenResult()?.failure_reason}>
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
{channelOpenResult()?.failure_reason?.message}
</p>
</Match>
<Match when={true}>
<img
src={megacheck}
alt="success"
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
/>
<AmountCard
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
/>
<Show when={channelOpenResult()?.channel?.outpoint}>
<a
class=""
href={mempoolTxUrl(
channelOpenResult()?.channel?.outpoint?.split(":")[0],
"signet"
)}
target="_blank"
rel="noreferrer"
>
Mempool Link
</a>
</Show>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
</Match>
</Switch>
</div>
</FullscreenModal>
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
{channelOpenResult()?.failure_reason?.message}
</p>
</Match>
<Match when={true}>
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
<AmountCard
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
/>
<Show when={channelOpenResult()?.channel?.outpoint}>
<ExternalLink
href={mempoolTxUrl(
channelOpenResult()?.channel?.outpoint?.split(":")[0],
network
)}
>
View Transaction
</ExternalLink>
</Show>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
</Match>
</Switch>
</SuccessModal>
<VStack biggap>
<MethodChooser source={source()} setSource={setSource} both={false} />
<VStack>
<Show when={hasLsp()}>
<Checkbox checked={useLsp()} onChange={setUseLsp} label="Use LSP" />
</Show>
<Show when={!useLsp()}>
<Show when={!hasLsp()}>
<Card>
<VStack>
<div class="w-full flex flex-col gap-2">