feat: redesign payment id screen

This commit is contained in:
benalleng
2023-09-14 20:08:38 -04:00
committed by Paul Miller
parent 6f628cfb99
commit 1b1e924fe3
18 changed files with 857 additions and 681 deletions

View File

@@ -11,8 +11,8 @@ import {
import { A } from "solid-start";
import {
ActivityDetailsModal,
ActivityItem,
DetailsIdModal,
HackActivityType,
LoadingShimmer,
NiceP
@@ -30,7 +30,7 @@ export const REDSHIFT_LABEL =
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
interface IActivityItem {
export interface IActivityItem {
kind: HackActivityType;
id: string;
amount_sats: number;
@@ -50,7 +50,6 @@ function UnifiedActivityItem(props: {
props.item.kind as unknown as HackActivityType
);
};
return (
<ActivityItem
// This is actually the ActivityType enum but wasm is hard
@@ -77,7 +76,6 @@ export function CombinedActivity(props: { limit?: number }) {
function openDetailsModal(id: string, kind: HackActivityType) {
console.log("Opening details modal: ", id, kind);
// Some old channels don't have a channel id in the activity list
if (!id) {
console.warn("No id provided to openDetailsModal");
return;
@@ -109,7 +107,7 @@ export function CombinedActivity(props: { limit?: number }) {
fallback={<LoadingShimmer />}
>
<Show when={detailsId() && detailsKind()}>
<DetailsIdModal
<ActivityDetailsModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}

View File

@@ -0,0 +1,597 @@
import { Dialog } from "@kobalte/core";
import {
MutinyChannel,
MutinyInvoice,
TagItem
} from "@mutinywallet/mutiny-wasm";
import {
createEffect,
createMemo,
createResource,
Match,
Show,
Suspense,
Switch
} from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import copyIcon from "~/assets/icons/copy.svg";
import shuffle from "~/assets/icons/shuffle.svg";
import {
ActivityAmount,
AmountFiat,
AmountSats,
FancyCard,
HackActivityType,
Hr,
InfoBox,
KeyValue,
ModalCloseButton,
TinyButton,
TruncateMiddle,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { BalanceBar } from "~/routes/settings/Channels";
import { useMegaStore } from "~/state/megaStore";
import { mempoolTxUrl, prettyPrintTime, useCopy } from "~/utils";
interface ChannelClosure {
channel_id: string;
node_id: string;
reason: string;
timestamp: number;
}
interface OnChainTx {
txid: string;
received: number;
sent: number;
fee?: number;
confirmation_time?: {
Confirmed?: {
height: number;
time: number;
};
};
labels: string[];
}
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
export const DIALOG_POSITIONER =
"fixed inset-0 z-50 flex items-center justify-center";
export const DIALOG_CONTENT =
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
function LightningHeader(props: { info: MutinyInvoice }) {
const i18n = useI18n();
return (
<div class="flex flex-col items-center gap-4">
<div class="flex flex-row items-center justify-center gap-[4px] font-normal">
{props.info.inbound
? i18n.t("activity.transaction_details.lightning_receive")
: i18n.t("activity.transaction_details.lightning_send")}
<img src={bolt} alt="lightning bolt" class="h-4 w-4" />
</div>
<div class="flex flex-col items-center">
<div
class="text-2xl"
classList={{ "text-m-green": props.info.inbound }}
>
<AmountSats
amountSats={props.info.amount_sats}
icon={props.info.inbound ? "plus" : undefined}
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={props.info.amount_sats}
denominationSize="sm"
/>
</div>
</div>
</div>
);
}
function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
const i18n = useI18n();
const isSend = () => {
return props.info.sent > props.info.received;
};
const amount = () => {
if (isSend()) {
return (props.info.sent - props.info.received).toString();
} else {
return (props.info.received - props.info.sent).toString();
}
};
return (
<div class="flex flex-col items-center gap-4">
<div class="flex flex-row items-center justify-center gap-[4px] font-normal">
{props.kind === "ChannelOpen"
? i18n.t("activity.transaction_details.channel_open")
: props.kind === "ChannelClose"
? i18n.t("activity.transaction_details.channel_close")
: isSend()
? i18n.t("activity.transaction_details.onchain_send")
: i18n.t("activity.transaction_details.onchain_receive")}
<Switch>
<Match
when={
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<img src={shuffle} alt="swap" class="h-4 w-4" />
</Match>
<Match when={true}>
<img src={chain} alt="blockchain" class="h-4 w-4" />
</Match>
</Switch>
</div>
<Show when={props.kind !== "ChannelClose"}>
<div class="flex flex-col items-center">
<div
class="text-2xl"
classList={{ "text-m-green": !isSend() }}
>
<AmountSats
amountSats={Number(amount())}
icon={!isSend() ? "plus" : undefined}
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={Number(amount())}
denominationSize="sm"
/>
</div>
</div>
</Show>
</div>
);
}
export function MiniStringShower(props: { text: string }) {
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return (
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
<TruncateMiddle text={props.text} />
<button
class="w-[1.5rem] p-1"
classList={{ "bg-m-green rounded": copied() }}
onClick={() => copy(props.text)}
>
<img src={copyIcon} alt="copy" class="h-4 w-4" />
</button>
</div>
);
}
export function FormatPrettyPrint(props: { ts: number }) {
return (
<div>
{prettyPrintTime(props.ts).split(",", 2).join(",")}
<div class="text-right text-sm text-white/70">
{prettyPrintTime(props.ts).split(", ")[2]}
</div>
</div>
);
}
function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key={i18n.t("activity.transaction_details.fee")}>
<ActivityAmount
amount={props.info.fees_paid!.toString()}
price={state.price}
/>
</KeyValue>
<Show when={props.tags || props.info.labels[0]}>
<KeyValue
key={i18n.t("activity.transaction_details.tagged_to")}
>
<TinyButton
tag={props.tags?.value ?? undefined}
onClick={() => {
// noop
}}
>
{props.tags?.name || props.info.labels[0]}
</TinyButton>
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.status")}>
{props.info.paid
? i18n.t("activity.transaction_details.paid")
: i18n.t("activity.transaction_details.unpaid")}
</KeyValue>
<KeyValue key={i18n.t("activity.transaction_details.date")}>
<FormatPrettyPrint ts={Number(props.info.last_updated)} />
</KeyValue>
<Show when={props.info.description}>
<KeyValue
key={i18n.t("activity.transaction_details.description")}
>
<span class="pl-6">{props.info.description}</span>
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.invoice")}>
<MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue>
<KeyValue
key={i18n.t("activity.transaction_details.payment_hash")}
>
<MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue>
<KeyValue
key={i18n.t(
"activity.transaction_details.payment_preimage"
)}
>
<MiniStringShower text={props.info.preimage ?? ""} />
</KeyValue>
</ul>
</VStack>
);
}
function OnchainDetails(props: {
info: OnChainTx;
kind?: HackActivityType;
tags?: TagItem;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
const confirmationTime = () => {
return props.info.confirmation_time?.Confirmed?.time;
};
const network = state.mutiny_wallet?.get_network() as Network;
// Can return nothing if the channel is already closed
const [channelInfo] = createResource(async () => {
if (props.kind === "ChannelOpen") {
try {
const channels =
await (state.mutiny_wallet?.list_channels() as Promise<
MutinyChannel[]
>);
const channel = channels.find(
(channel) => channel.outpoint?.startsWith(props.info.txid)
);
return channel;
} catch (e) {
console.error(e);
}
} else {
return undefined;
}
});
return (
<VStack>
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<Switch>
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
<BalanceBar
inbound={
Number(channelInfo()?.size) -
(Number(channelInfo()?.balance) +
Number(channelInfo()?.reserve)) || 0
}
reserve={Number(channelInfo()?.reserve) || 0}
outbound={Number(channelInfo()?.balance) || 0}
/>
<KeyValue
key={i18n.t("activity.transaction_details.total")}
>
<ActivityAmount
amount={channelInfo()!.size.toString()}
price={state.price}
/>
</KeyValue>
<KeyValue
key={i18n.t(
"activity.transaction_details.onchain_fee"
)}
>
<ActivityAmount
amount={props.info.fee!.toString()}
price={state.price}
/>
</KeyValue>
</Match>
<Match when={props.kind === "ChannelOpen"}>
<InfoBox accent="blue">
{i18n.t("activity.transaction_details.no_details")}
</InfoBox>
</Match>
</Switch>
<Show
when={
props.kind !== "ChannelOpen" &&
props.info.fee &&
props.info.fee > 0
}
>
<KeyValue
key={i18n.t("activity.transaction_details.onchain_fee")}
>
<ActivityAmount
amount={props.info.fee!.toString()}
price={state.price}
/>
</KeyValue>
</Show>
<Show when={props.tags && props.kind === "OnChain"}>
<KeyValue
key={i18n.t("activity.transaction_details.tagged_to")}
>
<TinyButton
tag={props.tags?.value ?? undefined}
onClick={() => {
// noop
}}
>
{props.tags?.name || props.info.labels[0]}
</TinyButton>
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.status")}>
{confirmationTime()
? i18n.t("activity.transaction_details.confirmed")
: i18n.t("activity.transaction_details.unconfirmed")}
</KeyValue>
<KeyValue key={i18n.t("activity.transaction_details.date")}>
{confirmationTime() ? (
<FormatPrettyPrint ts={Number(confirmationTime())} />
) : (
"Pending"
)}
</KeyValue>
<Show when={props.kind === "ChannelOpen" && channelInfo()}>
<KeyValue key={i18n.t("activity.transaction_details.peer")}>
<MiniStringShower text={channelInfo()?.peer ?? ""} />
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.txid")}>
<div class="flex gap-1">
{/* Have to do all these shenanigans because css / html is hard */}
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
<a
target="_blank"
rel="noopener noreferrer"
href={mempoolTxUrl(props.info.txid, network)}
>
<div class="flex flex-nowrap items-center font-mono text-white">
<span class="truncate">
{props.info.txid}
</span>
<span class="-ml-5">
&nbsp;&nbsp;
{props.info.txid.length > 32
? props.info.txid.slice(-8)
: ""}
</span>
<svg
class="inline-block w-[16px] overflow-visible pl-0.5 text-white"
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>
</div>
</a>
</div>
<button
class="min-w-[1.5rem] p-1"
classList={{ "bg-m-green rounded": copied() }}
onClick={() => copy(props.info.txid)}
>
<img src={copyIcon} alt="copy" class="h-4 w-4" />
</button>
</div>
</KeyValue>
</ul>
</VStack>
);
}
function ChannelCloseDetails(props: { info: ChannelClosure }) {
const i18n = useI18n();
return (
<VStack>
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<InfoBox accent="blue">
<p>{i18n.t("activity.transaction_details.sweep_delay")}</p>
</InfoBox>
<KeyValue
key={i18n.t("activity.transaction_details.channel_id")}
>
<MiniStringShower text={props.info.channel_id ?? ""} />
</KeyValue>
<Show when={props.info.timestamp}>
<KeyValue key={i18n.t("activity.transaction_details.date")}>
{props.info.timestamp ? (
<FormatPrettyPrint
ts={Number(props.info.timestamp)}
/>
) : (
i18n.t("common.pending")
)}
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.reason")}>
<p class="whitespace-normal text-right text-neutral-300">
{props.info.reason ?? ""}
</p>
</KeyValue>
</ul>
</VStack>
);
}
export function ActivityDetailsModal(props: {
open: boolean;
kind?: HackActivityType;
id: string;
setOpen: (open: boolean) => void;
}) {
const [state, _actions] = useMegaStore();
const id = () => props.id;
const kind = () => props.kind;
const [data, { refetch }] = createResource(async () => {
try {
if (kind() === "Lightning") {
console.debug("reading invoice: ", id());
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
id()
);
return invoice;
} else if (kind() === "ChannelClose") {
console.debug("reading channel close: ", id());
const closeItem =
await state.mutiny_wallet?.get_channel_closure(id());
return closeItem;
} else {
console.debug("reading tx: ", id());
const tx = await state.mutiny_wallet?.get_transaction(id());
return tx;
}
} catch (e) {
console.error(e);
return undefined;
}
});
const tags = createMemo(() => {
if (
!!data() &&
data()?.labels !== undefined &&
typeof data()?.labels[0] === "string"
) {
try {
// find if there's just one for now
const contacts = state.mutiny_wallet?.get_contact(
data().labels[0]
);
if (contacts) {
return contacts;
} else {
return;
}
} catch (e) {
console.error(e);
}
} else {
return;
}
});
createEffect(() => {
if (props.id && props.kind && props.open) {
refetch();
}
});
return (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<Suspense>
<div class="p-4">
<div class="flex justify-between">
<div />
<Dialog.CloseButton>
<ModalCloseButton />
</Dialog.CloseButton>
</div>
<Dialog.Title>
<FancyCard>
<Switch>
<Match
when={kind() === "Lightning"}
>
<LightningHeader
info={
data() as MutinyInvoice
}
/>
</Match>
<Match
when={
kind() === "OnChain" ||
kind() === "ChannelOpen" ||
kind() === "ChannelClose"
}
>
<OnchainHeader
info={data() as OnChainTx}
kind={kind()}
/>
</Match>
</Switch>
</FancyCard>
</Dialog.Title>
<Hr />
<Switch>
<Match when={kind() === "Lightning"}>
<LightningDetails
info={data() as MutinyInvoice}
tags={tags()}
/>
</Match>
<Match
when={
kind() === "OnChain" ||
kind() === "ChannelOpen"
}
>
<OnchainDetails
info={data() as OnChainTx}
kind={kind()}
tags={tags()}
/>
</Match>
<Match when={kind() === "ChannelClose"}>
<ChannelCloseDetails
info={data() as ChannelClosure}
/>
</Match>
</Switch>
</div>
</Suspense>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -102,9 +102,9 @@ export function AmountSmall(props: {
}) {
const i18n = useI18n();
return (
<span class="font-light">
<span class="text-sm font-light md:text-base">
{prettyPrintAmount(props.amountSats)}&nbsp;
<span class="text-sm">
<span class="text-xs md:text-sm">
{props.amountSats === 1 || props.amountSats === 1n
? i18n.t("common.sat")
: i18n.t("common.sats")}

View File

@@ -9,7 +9,9 @@ const noop = () => {
// do nothing
};
const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
const AmountKeyValue: ParentComponent<{ key: string; gray?: boolean }> = (
props
) => {
return (
<div
class="flex items-center justify-between"
@@ -55,12 +57,12 @@ function USDShower(props: { amountSats: string; fee?: string }) {
return (
<Show when={!(props.amountSats === "0")}>
<KeyValue gray key="">
<AmountKeyValue gray key="">
<div class="self-end">
{amountInFiat()}&nbsp;
<span class="text-sm">{state.fiat.value}</span>
</div>
</KeyValue>
</AmountKeyValue>
</Show>
);
}
@@ -99,7 +101,7 @@ export function AmountCard(props: {
<Switch>
<Match when={props.fee}>
<div class="flex flex-col gap-1">
<KeyValue key={i18n.t("receive.amount")}>
<AmountKeyValue key={i18n.t("receive.amount")}>
<Show
when={props.isAmountEditable}
fallback={
@@ -124,16 +126,16 @@ export function AmountCard(props: {
fee={props.fee}
/>
</Show>
</KeyValue>
<KeyValue gray key={i18n.t("receive.fee")}>
</AmountKeyValue>
<AmountKeyValue gray key={i18n.t("receive.fee")}>
<InlineAmount amount={props.fee || "0"} />
</KeyValue>
</AmountKeyValue>
</div>
<hr class="border-white/20" />
<div class="flex flex-col gap-1">
<KeyValue key={i18n.t("receive.total")}>
<AmountKeyValue key={i18n.t("receive.total")}>
<InlineAmount amount={totalOrTotalLessFee()} />
</KeyValue>
</AmountKeyValue>
<USDShower
amountSats={props.amountSats}
fee={props.fee}
@@ -142,26 +144,28 @@ export function AmountCard(props: {
</Match>
<Match when={props.reserve}>
<div class="flex flex-col gap-1">
<KeyValue key={i18n.t("receive.channel_size")}>
<AmountKeyValue
key={i18n.t("receive.channel_size")}
>
<InlineAmount
amount={add(
props.amountSats,
props.reserve
).toString()}
/>
</KeyValue>
<KeyValue
</AmountKeyValue>
<AmountKeyValue
gray
key={i18n.t("receive.channel_reserve")}
>
<InlineAmount amount={props.reserve || "0"} />
</KeyValue>
</AmountKeyValue>
</div>
<hr class="border-white/20" />
<div class="flex flex-col gap-1">
<KeyValue key={i18n.t("receive.spendable")}>
<AmountKeyValue key={i18n.t("receive.spendable")}>
<InlineAmount amount={props.amountSats} />
</KeyValue>
</AmountKeyValue>
<USDShower
amountSats={props.amountSats}
fee={props.reserve}
@@ -170,7 +174,7 @@ export function AmountCard(props: {
</Match>
<Match when={!props.fee && !props.reserve}>
<div class="flex flex-col gap-1">
<KeyValue key={i18n.t("receive.amount")}>
<AmountKeyValue key={i18n.t("receive.amount")}>
<Show
when={props.isAmountEditable}
fallback={
@@ -195,7 +199,7 @@ export function AmountCard(props: {
fee={props.fee}
/>
</Show>
</KeyValue>
</AmountKeyValue>
<USDShower amountSats={props.amountSats} />
</div>
</Match>

View File

@@ -13,13 +13,19 @@ import {
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function LoadingShimmer() {
export function LoadingShimmer(props: { center?: boolean }) {
return (
<div class="flex animate-pulse flex-col gap-2">
<h1 class="text-4xl font-light">
<h1
class="text-4xl font-light"
classList={{ "flex justify-center": props.center }}
>
<div class="h-[2.5rem] w-[12rem] rounded bg-neutral-700" />
</h1>
<h2 class="text-xl font-light text-white/70">
<h2
class="text-xl font-light text-white/70"
classList={{ "flex justify-center": props.center }}
>
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
</h2>
</div>

View File

@@ -171,9 +171,9 @@ export function ContactViewer(props: {
}
>
<KeyValue
key={
"Lightning Address"
}
key={i18n.t(
"contacts.lightning_address"
)}
>
<MiniStringShower
text={

View File

@@ -1,532 +0,0 @@
import { Dialog } from "@kobalte/core";
import { MutinyChannel, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
import {
createEffect,
createMemo,
createResource,
For,
Match,
ParentComponent,
Show,
Suspense,
Switch
} from "solid-js";
import bolt from "~/assets/icons/bolt-black.svg";
import chain from "~/assets/icons/chain-black.svg";
import copyIcon from "~/assets/icons/copy.svg";
import shuffle from "~/assets/icons/shuffle-black.svg";
import {
ActivityAmount,
AmountSmall,
CopyButton,
ExternalLink,
HackActivityType,
Hr,
InfoBox,
ModalCloseButton,
TinyButton,
TruncateMiddle,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import {
mempoolTxUrl,
MutinyTagItem,
prettyPrintTime,
tagToMutinyTag,
useCopy
} from "~/utils";
interface ChannelClosure {
channel_id: string;
node_id: string;
reason: string;
timestamp: number;
}
interface OnChainTx {
txid: string;
received: number;
sent: number;
fee?: number;
confirmation_time?: {
Confirmed?: {
height: number;
time: number;
};
};
labels: string[];
}
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
export const DIALOG_POSITIONER =
"fixed inset-0 z-50 flex items-center justify-center";
export const DIALOG_CONTENT =
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-m-grey-800/75 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
function LightningHeader(props: {
info: MutinyInvoice;
tags: MutinyTagItem[];
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
return (
<div class="flex flex-col items-center gap-4">
<div class="rounded-full bg-neutral-100 p-4">
<img src={bolt} alt="lightning bolt" class="h-8 w-8" />
</div>
<h1 class="font-semibold uppercase">
{props.info.inbound
? i18n.t("modals.transaction_details.lightning_receive")
: i18n.t("modals.transaction_details.lightning_send")}
</h1>
<ActivityAmount
center
amount={props.info.amount_sats?.toString() ?? "0"}
price={state.price}
positive={props.info.inbound}
/>
<For each={props.tags}>
{(tag) => (
<TinyButton
tag={tag}
onClick={() => {
// noop
}}
>
{tag.name}
</TinyButton>
)}
</For>
</div>
);
}
function OnchainHeader(props: {
info: OnChainTx;
tags: MutinyTagItem[];
kind?: HackActivityType;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const isSend = () => {
return props.info.sent > props.info.received;
};
const amount = () => {
if (isSend()) {
return (props.info.sent - props.info.received).toString();
} else {
return (props.info.received - props.info.sent).toString();
}
};
return (
<div class="flex flex-col items-center gap-4">
<div class="rounded-full bg-neutral-100 p-4">
<Switch>
<Match
when={
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<img src={shuffle} alt="swap" class="h-8 w-8" />
</Match>
<Match when={true}>
<img src={chain} alt="blockchain" class="h-8 w-8" />
</Match>
</Switch>
</div>
<h1 class="font-semibold uppercase">
{props.kind === "ChannelOpen"
? i18n.t("modals.transaction_details.channel_open")
: props.kind === "ChannelClose"
? i18n.t("modals.transaction_details.channel_close")
: isSend()
? i18n.t("modals.transaction_details.onchain_send")
: i18n.t("modals.transaction_details.onchain_receive")}
</h1>
<Show when={props.kind !== "ChannelClose"}>
<ActivityAmount
center
amount={amount() ?? "0"}
price={state.price}
positive={!isSend()}
/>
</Show>
<For each={props.tags}>
{(tag) => (
<TinyButton
tag={tag}
onClick={() => {
// noop
}}
>
{tag.name}
</TinyButton>
)}
</For>
</div>
);
}
export const KeyValue: ParentComponent<{ key: string }> = (props) => {
return (
<li class="flex items-center justify-between gap-4">
<span class="whitespace-nowrap text-sm font-semibold uppercase">
{props.key}
</span>
<span class="font-light">{props.children}</span>
</li>
);
};
export function MiniStringShower(props: { text: string }) {
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return (
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
<TruncateMiddle text={props.text} />
{/* <pre class="truncate text-neutral-300 font-light">{props.text}</pre> */}
<button
class="w-[1.5rem] p-1"
classList={{ "bg-m-green rounded": copied() }}
onClick={() => copy(props.text)}
>
<img src={copyIcon} alt="copy" class="h-4 w-4" />
</button>
</div>
);
}
function LightningDetails(props: { info: MutinyInvoice }) {
const i18n = useI18n();
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key={i18n.t("modals.transaction_details.status")}>
<span class="text-neutral-300">
{props.info.paid
? i18n.t("modals.transaction_details.paid")
: i18n.t("modals.transaction_details.unpaid")}
</span>
</KeyValue>
<KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300">
{prettyPrintTime(Number(props.info.last_updated))}
</span>
</KeyValue>
<Show when={props.info.description}>
<KeyValue
key={i18n.t("modals.transaction_details.description")}
>
<span class="truncate text-neutral-300">
{props.info.description}
</span>
</KeyValue>
</Show>
<KeyValue key={i18n.t("modals.transaction_details.fees")}>
<span class="text-neutral-300">
<AmountSmall amountSats={props.info.fees_paid} />
</span>
</KeyValue>
<KeyValue key={i18n.t("modals.transaction_details.bolt11")}>
<MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue>
<KeyValue
key={i18n.t("modals.transaction_details.payment_hash")}
>
<MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue>
<KeyValue key={i18n.t("modals.transaction_details.preimage")}>
<MiniStringShower text={props.info.preimage ?? ""} />
</KeyValue>
</ul>
</VStack>
);
}
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const confirmationTime = () => {
return props.info.confirmation_time?.Confirmed?.time;
};
const network = state.mutiny_wallet?.get_network() as Network;
// Can return nothing if the channel is already closed
const [channelInfo] = createResource(async () => {
if (props.kind === "ChannelOpen") {
try {
const channels =
await (state.mutiny_wallet?.list_channels() as Promise<
MutinyChannel[]
>);
const channel = channels.find(
(channel) => channel.outpoint?.startsWith(props.info.txid)
);
return channel;
} catch (e) {
console.error(e);
}
} else {
return undefined;
}
});
return (
<VStack>
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<KeyValue key={i18n.t("modals.transaction_details.status")}>
<span class="text-neutral-300">
{confirmationTime()
? i18n.t("modals.transaction_details.confirmed")
: i18n.t("modals.transaction_details.unconfirmed")}
</span>
</KeyValue>
<Show when={confirmationTime()}>
<KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300">
{confirmationTime()
? prettyPrintTime(Number(confirmationTime()))
: "Pending"}
</span>
</KeyValue>
</Show>
<Show when={props.info.fee && props.info.fee > 0}>
<KeyValue key={i18n.t("modals.transaction_details.fee")}>
<span class="text-neutral-300">
<AmountSmall amountSats={props.info.fee} />
</span>
</KeyValue>
</Show>
<KeyValue key={i18n.t("modals.transaction_details.txid")}>
<MiniStringShower text={props.info.txid ?? ""} />
</KeyValue>
<Switch>
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
<KeyValue
key={i18n.t("modals.transaction_details.balance")}
>
<span class="text-neutral-300">
<AmountSmall
amountSats={channelInfo()?.balance}
/>
</span>
</KeyValue>
<KeyValue
key={i18n.t("modals.transaction_details.reserve")}
>
<span class="text-neutral-300">
<AmountSmall
amountSats={channelInfo()?.reserve}
/>
</span>
</KeyValue>
<KeyValue
key={i18n.t("modals.transaction_details.peer")}
>
<span class="text-neutral-300">
<MiniStringShower
text={channelInfo()?.peer ?? ""}
/>
</span>
</KeyValue>
</Match>
<Match when={props.kind === "ChannelOpen"}>
<InfoBox accent="blue">
{i18n.t("modals.transaction_details.no_details")}
</InfoBox>
</Match>
</Switch>
</ul>
<div class="text-center">
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
{i18n.t("common.view_transaction")}
</ExternalLink>
</div>
</VStack>
);
}
function ChannelCloseDetails(props: { info: ChannelClosure }) {
const i18n = useI18n();
return (
<VStack>
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<KeyValue key={i18n.t("modals.transaction_details.channel_id")}>
<MiniStringShower text={props.info.channel_id ?? ""} />
</KeyValue>
<Show when={props.info.timestamp}>
<KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300">
{props.info.timestamp
? prettyPrintTime(Number(props.info.timestamp))
: i18n.t("common.pending")}
</span>
</KeyValue>
</Show>
<KeyValue key={i18n.t("modals.transaction_details.reason")}>
<p class="text-right text-neutral-300">
{props.info.reason ?? ""}
</p>
</KeyValue>
</ul>
</VStack>
);
}
export function DetailsIdModal(props: {
open: boolean;
kind?: HackActivityType;
id: string;
setOpen: (open: boolean) => void;
}) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const id = () => props.id;
const kind = () => props.kind;
// TODO: is there a cleaner way to do refetch when id changes?
const [data, { refetch }] = createResource(async () => {
try {
if (kind() === "Lightning") {
console.debug("reading invoice: ", id());
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
id()
);
return invoice;
} else if (kind() === "ChannelClose") {
console.debug("reading channel close: ", id());
const closeItem =
await state.mutiny_wallet?.get_channel_closure(id());
return closeItem;
} else {
console.debug("reading tx: ", id());
const tx = await state.mutiny_wallet?.get_transaction(id());
return tx;
}
} catch (e) {
console.error(e);
return undefined;
}
});
const tags = createMemo(() => {
if (data() && data().labels && data().labels.length > 0) {
try {
const contact = state.mutiny_wallet?.get_contact(
data().labels[0]
);
if (contact) {
return [tagToMutinyTag(contact)];
} else {
return [];
}
} catch (e) {
console.error(e);
return [];
}
} else {
return [];
}
});
createEffect(() => {
if (props.id && props.kind && props.open) {
refetch();
}
});
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
return (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<Suspense>
<div class="mb-2 flex justify-between">
<div />
<Dialog.CloseButton>
<ModalCloseButton />
</Dialog.CloseButton>
</div>
<Dialog.Title>
<Switch>
<Match when={props.kind === "Lightning"}>
<LightningHeader
info={data() as MutinyInvoice}
tags={tags()}
/>
</Match>
<Match
when={
props.kind === "OnChain" ||
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<OnchainHeader
info={data() as OnChainTx}
tags={tags()}
kind={props.kind}
/>
</Match>
</Switch>
</Dialog.Title>
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<Switch>
<Match when={props.kind === "Lightning"}>
<LightningDetails
info={data() as MutinyInvoice}
/>
</Match>
<Match
when={
props.kind === "OnChain" ||
props.kind === "ChannelOpen"
}
>
<OnchainDetails
info={data() as OnChainTx}
kind={props.kind}
/>
</Match>
<Match when={props.kind === "ChannelClose"}>
<ChannelCloseDetails
info={data() as ChannelClosure}
/>
</Match>
</Switch>
<Show when={props.kind !== "ChannelClose"}>
<div class="flex justify-center">
<CopyButton
title={i18n.t("common.copy")}
text={json()}
/>
</div>
</Show>
</Dialog.Description>
</Suspense>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -46,8 +46,7 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
<div
class="flex font-mono"
classList={{
"text-black": props.whiteBg,
"text-neutral-300": !props.whiteBg
"text-black": props.whiteBg
}}
>
<span class="truncate">{props.text}</span>

View File

@@ -2,6 +2,7 @@ export * from "./layout";
export * from "./successfail";
export * from "./Activity";
export * from "./ActivityDetailsModal";
export * from "./ActivityItem";
export * from "./Amount";
export * from "./AmountCard";
@@ -14,7 +15,6 @@ export * from "./ContactForm";
export * from "./ContactViewer";
export * from "./DecryptDialog";
export * from "./DeleteEverything";
export * from "./DetailsModal";
export * from "./ErrorDisplay";
export * from "./Fee";
export * from "./I18nProvider";

View File

@@ -21,7 +21,11 @@ export function BackPop() {
return (
<BackButton
title={i18n.t("common.back")}
title={
backPath() === "/"
? i18n.t("common.home")
: i18n.t("common.back")
}
onClick={() => navigate(backPath())}
showOnDesktop
/>

View File

@@ -184,6 +184,17 @@ export const MutinyWalletGuard: ParentComponent = (props) => {
export const Hr = () => <Separator.Root class="my-4 border-m-grey-750" />;
export const KeyValue: ParentComponent<{ key: string }> = (props) => {
return (
<li class="flex items-center justify-between gap-6">
<span class="min-w-max text-sm font-semibold uppercase text-m-grey-400">
{props.key}
</span>
<span class="truncate font-light">{props.children}</span>
</li>
);
};
export const LargeHeader: ParentComponent<{
action?: JSX.Element;
centered?: boolean;

View File

@@ -17,7 +17,8 @@ export default {
error_unimplemented: "Unimplemented",
why: "Why?",
private_tags: "Private tags",
view_transaction: "View Transaction",
view_transaction: "View transaction",
view_payment_details: "View payment details",
pending: "Pending",
error_safe_mode:
"Mutiny is running in safe mode. Lightning is disabled.",
@@ -36,6 +37,7 @@ export default {
pay: "Pay",
name: "Name",
placeholder: "Satoshi",
lightning_address: "Lightning Address",
unimplemented: "Unimplemented",
not_available: "We don't do that yet",
error_name: "We at least need a name"
@@ -163,7 +165,40 @@ export default {
coming_soon: "Coming soon",
private: "Private",
anonymous: "Anonymous",
from: "From:"
from: "From:",
transaction_details: {
lightning_receive: "Received via Lightning",
lightning_send: "Sent via Lightning",
channel_open: "Channel open",
channel_close: "Channel close",
onchain_receive: "On-chain receive",
onchain_send: "On-chain send",
paid: "Paid",
unpaid: "Unpaid",
status: "Status",
date: "Date",
tagged_to: "Tagged to",
description: "Description",
fee: "Fee",
onchain_fee: "On-chain Fee",
invoice: "Invoice",
payment_hash: "Payment Hash",
payment_preimage: "Preimage",
txid: "Txid",
total: "Amount Requested",
balance: "Balance",
reserve: "Reserve",
peer: "Peer",
channel_id: "Channel ID",
reason: "Reason",
confirmed: "Confirmed",
unconfirmed: "Unconfirmed",
sweep_delay:
"Funds may take a few days to be swept back into the wallet",
no_details:
"No channel details found, which means this channel has likely been closed.",
back_home: "back home"
}
},
redshift: {
title: "Redshift",
@@ -625,34 +660,6 @@ export default {
"If you want to use pretend money to test out Mutiny without risk,",
signet_link: "check out our Signet version."
},
transaction_details: {
lightning_receive: "Lightning receive",
lightning_send: "Lightning send",
channel_open: "Channel open",
channel_close: "Channel close",
onchain_receive: "On-chain receive",
onchain_send: "On-chain send",
paid: "Paid",
unpaid: "Unpaid",
status: "Status",
when: "When",
description: "Description",
fee: "Fee",
fees: "Fees",
bolt11: "Bolt11",
payment_hash: "Payment Hash",
preimage: "Preimage",
txid: "Txid",
balance: "Balance",
reserve: "Reserve",
peer: "Peer",
channel_id: "Channel ID",
reason: "Reason",
confirmed: "Confirmed",
unconfirmed: "Unconfirmed",
no_details:
"No channel details found, which means this channel has likely been closed."
},
more_info: {
whats_with_the_fees: "What's with the fees?",
self_custodial:

View File

@@ -145,7 +145,35 @@ export default {
unknown: "알 수 없음",
import_contacts:
"Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.",
coming_soon: "곧 출시 예정"
coming_soon: "곧 출시 예정",
transaction_details: {
lightning_receive: "라이트닝 입금",
lightning_send: "라이트닝 송금",
channel_open: "채널 개설",
channel_close: "채널 종료",
onchain_receive: "체인상 입금",
onchain_send: "체인상 송금",
paid: "지불 완료",
unpaid: "미지불",
status: "상태",
when: "시간",
description: "설명",
fee: "수수료",
fees: "수수료",
bolt11: "Bolt11",
payment_hash: "지불 해시",
preimage: "사전 이미지",
txid: "거래 ID",
balance: "잔고",
reserve: "리저브",
peer: "피어",
channel_id: "채널 ID",
reason: "이유",
confirmed: "확인됨",
unconfirmed: "확인 대기",
no_details:
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
}
},
redshift: {
title: "레드시프트",
@@ -497,34 +525,6 @@ export default {
"위험 없이 Mutiny를 테스트하려면 가상 자금을 사용하려면",
signet_link: "Signet 버전을 확인하세요."
},
transaction_details: {
lightning_receive: "라이트닝 입금",
lightning_send: "라이트닝 송금",
channel_open: "채널 개설",
channel_close: "채널 종료",
onchain_receive: "체인상 입금",
onchain_send: "체인상 송금",
paid: "지불 완료",
unpaid: "미지불",
status: "상태",
when: "시간",
description: "설명",
fee: "수수료",
fees: "수수료",
bolt11: "Bolt11",
payment_hash: "지불 해시",
preimage: "사전 이미지",
txid: "거래 ID",
balance: "잔고",
reserve: "리저브",
peer: "피어",
channel_id: "채널 ID",
reason: "이유",
confirmed: "확인됨",
unconfirmed: "확인 대기",
no_details:
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
},
more_info: {
whats_with_the_fees: "수수료는 어떻게 되나요?",
self_custodial:

View File

@@ -29,7 +29,7 @@ html {
}
a {
@apply underline decoration-light-text hover:decoration-white;
@apply underline decoration-m-grey-400 hover:decoration-white;
}
p {

View File

@@ -17,6 +17,7 @@ import { useNavigate } from "solid-start";
import side2side from "~/assets/icons/side-to-side.svg";
import {
ActivityDetailsModal,
AmountCard,
AmountFiat,
AmountSats,
@@ -26,9 +27,9 @@ import {
Card,
Checkbox,
DefaultMain,
ExternalLink,
Fee,
FeesModal,
HackActivityType,
Indicator,
InfoBox,
IntegratedQr,
@@ -45,11 +46,9 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import {
eify,
mempoolTxUrl,
MutinyTagItem,
objectToSearchParams,
vibrateSuccess
@@ -142,6 +141,11 @@ export default function Receive() {
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<string>("");
// Details Modal
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal<string>("");
const RECEIVE_FLAVORS = [
{
value: "unified",
@@ -184,6 +188,30 @@ export default function Receive() {
setSelectedValues([]);
}
function openDetailsModal() {
const paymentTxId =
paidState() === "onchain_paid"
? paymentTx()
? paymentTx()?.txid
: undefined
: paymentInvoice()
? paymentInvoice()?.payment_hash
: undefined;
const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning";
console.log("Opening details modal: ", paymentTxId, kind);
if (!paymentTxId) {
console.warn("No id provided to openDetailsModal");
return;
}
if (paymentTxId !== undefined) {
setDetailsId(paymentTxId);
}
setDetailsKind(kind);
setDetailsOpen(true);
}
async function processContacts(
contacts: Partial<MutinyTagItem>[]
): Promise<string[]> {
@@ -346,8 +374,6 @@ export default function Receive() {
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
const network = state.mutiny_wallet?.get_network() as Network;
createEffect(() => {
const interval = setInterval(() => {
if (receiveState() === "show") refetch();
@@ -483,6 +509,14 @@ export default function Receive() {
navigate("/");
}}
>
<Show when={detailsId() && detailsKind()}>
<ActivityDetailsModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}
setOpen={setDetailsOpen}
/>
</Show>
<MegaCheck />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{receiveState() === "paid" &&
@@ -525,22 +559,14 @@ export default function Receive() {
>
<Fee amountSats={lspFee()} />
</Show>
{/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first
{/*TODO: add internal payment detail page* for lightning*/}
<Show
when={
receiveState() === "paid" &&
paidState() === "onchain_paid"
}
>
<ExternalLink
href={mempoolTxUrl(
paymentTx()?.txid,
network
)}
{/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}
<Show when={receiveState() === "paid"}>
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
>
{i18n.t("common.view_transaction")}
</ExternalLink>
{i18n.t("common.view_payment_details")}
</p>
</Show>
</SuccessModal>
</Match>

View File

@@ -15,6 +15,7 @@ import { useNavigate } from "solid-start";
import { Paste } from "~/assets/svg/Paste";
import { Scan } from "~/assets/svg/Scan";
import {
ActivityDetailsModal,
AmountCard,
AmountFiat,
AmountSats,
@@ -24,9 +25,9 @@ import {
ButtonLink,
Card,
DefaultMain,
ExternalLink,
Fee,
GiftLink,
HackActivityType,
HStack,
InfoBox,
LargeHeader,
@@ -44,10 +45,9 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { ParsedParams } from "~/logic/waila";
import { useMegaStore } from "~/state/megaStore";
import { eify, mempoolTxUrl, MutinyTagItem, vibrateSuccess } from "~/utils";
import { eify, MutinyTagItem, vibrateSuccess } from "~/utils";
export type SendSource = "lightning" | "onchain";
@@ -59,6 +59,7 @@ type SentDetails = {
amount?: bigint;
destination?: string;
txid?: string;
payment_hash?: string;
failure_reason?: string;
fee_estimate?: bigint | number;
};
@@ -227,6 +228,11 @@ export default function Send() {
Partial<MutinyTagItem>[]
>([]);
// Details Modal
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal("");
// Errors
const [error, setError] = createSignal<string>();
@@ -243,6 +249,29 @@ export default function Send() {
setFieldDestination("");
}
function openDetailsModal() {
const paymentTxId = sentDetails()?.txid
? sentDetails()
? sentDetails()?.txid
: undefined
: sentDetails()
? sentDetails()?.payment_hash
: undefined;
const kind = sentDetails()?.txid ? "OnChain" : "Lightning";
console.log("Opening details modal: ", paymentTxId, kind);
if (!paymentTxId) {
console.warn("No id provided to openDetailsModal");
return;
}
if (paymentTxId !== undefined) {
setDetailsId(paymentTxId);
}
setDetailsKind(kind);
setDetailsOpen(true);
}
// If we got here from a scan result we want to set the destination and clean up that scan result
onMount(() => {
if (state.scan_result) {
@@ -478,6 +507,7 @@ export default function Send() {
tags
);
sentDetails.amount = invoice()?.amount_sats;
sentDetails.payment_hash = invoice()?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
} else {
const payment = await state.mutiny_wallet?.pay_invoice(
@@ -487,6 +517,7 @@ export default function Send() {
tags
);
sentDetails.amount = amountSats();
sentDetails.payment_hash = invoice()?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "lightning" && nodePubkey()) {
@@ -505,6 +536,7 @@ export default function Send() {
throw new Error(i18n.t("send.error_keysend"));
} else {
sentDetails.amount = amountSats();
sentDetails.payment_hash = invoice()?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "lightning" && lnurlp()) {
@@ -517,11 +549,13 @@ export default function Send() {
undefined, // zap_npub
tags
);
sentDetails.payment_hash = invoice()?.payment_hash;
if (!payment?.paid) {
throw new Error(i18n.t("send.error_LNURL"));
} else {
sentDetails.amount = amountSats();
sentDetails.payment_hash = invoice()?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "onchain" && address()) {
@@ -568,8 +602,6 @@ export default function Send() {
return !destination() || sending() || amountSats() === 0n || !!error();
});
const network = state.mutiny_wallet?.get_network() as Network;
return (
<MutinyWalletGuard>
<SafeArea>
@@ -614,6 +646,14 @@ export default function Send() {
{/*TODO: add failure hint logic for different failure conditions*/}
</Match>
<Match when={true}>
<Show when={detailsId() && detailsKind()}>
<ActivityDetailsModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}
setOpen={setDetailsOpen}
/>
</Show>
<MegaCheck />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{sentDetails()?.amount
@@ -638,16 +678,12 @@ export default function Send() {
</div>
<hr class="w-16 bg-m-grey-400" />
<Fee amountSats={sentDetails()?.fee_estimate} />
<Show when={sentDetails()?.txid}>
<ExternalLink
href={mempoolTxUrl(
sentDetails()?.txid,
network
)}
>
{i18n.t("common.view_transaction")}
</ExternalLink>
</Show>
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
>
{i18n.t("common.view_payment_details")}
</p>
</Match>
</Switch>
</SuccessModal>

View File

@@ -12,13 +12,14 @@ import {
import { useNavigate } from "solid-start";
import {
ActivityDetailsModal,
AmountCard,
AmountFiat,
BackLink,
Button,
Card,
DefaultMain,
ExternalLink,
HackActivityType,
InfoBox,
LargeHeader,
MegaCheck,
@@ -35,7 +36,7 @@ import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { MethodChooser, SendSource } from "~/routes/Send";
import { useMegaStore } from "~/state/megaStore";
import { eify, mempoolTxUrl, vibrateSuccess } from "~/utils";
import { eify, vibrateSuccess } from "~/utils";
const CHANNEL_FEE_ESTIMATE_ADDRESS =
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
@@ -62,9 +63,32 @@ export default function Swap() {
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
// Details Modal
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal("");
const [channelOpenResult, setChannelOpenResult] =
createSignal<ChannelOpenDetails>();
function openDetailsModal() {
const paymentTxId =
channelOpenResult()?.channel?.outpoint?.split(":")[0];
const kind: HackActivityType = "ChannelOpen";
console.log("Opening details modal: ", paymentTxId, kind);
if (!paymentTxId) {
console.warn("No id provided to openDetailsModal");
return;
}
if (paymentTxId !== undefined) {
setDetailsId(paymentTxId);
}
setDetailsKind(kind);
setDetailsOpen(true);
}
function resetState() {
setSource("onchain");
setAmountSats(0n);
@@ -258,8 +282,6 @@ export default function Swap() {
return undefined;
});
const network = state.mutiny_wallet?.get_network() as Network;
return (
<MutinyWalletGuard>
<SafeArea>
@@ -293,6 +315,14 @@ export default function Swap() {
{/*TODO: Error hint needs to be added for possible failure reasons*/}
</Match>
<Match when={channelOpenResult()?.channel}>
<Show when={detailsId() && detailsKind()}>
<ActivityDetailsModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}
setOpen={setDetailsOpen}
/>
</Show>
<MegaCheck />
<div class="flex flex-col justify-center">
<h1 class="mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl">
@@ -328,22 +358,12 @@ export default function Swap() {
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
<Show
when={
channelOpenResult()?.channel?.outpoint
}
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
>
<ExternalLink
href={mempoolTxUrl(
channelOpenResult()?.channel?.outpoint?.split(
":"
)[0],
network
)}
>
{i18n.t("common.view_transaction")}
</ExternalLink>
</Show>
{i18n.t("common.view_payment_details")}
</p>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
</Match>
</Switch>

View File

@@ -17,7 +17,7 @@ import {
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
function BalanceBar(props: {
export function BalanceBar(props: {
inbound: number;
reserve: number;
outbound: number;