mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-19 07:14:22 +01:00
pending nwc approve / deny
This commit is contained in:
3
src/assets/icons/green-check.svg
Normal file
3
src/assets/icons/green-check.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="hsla(163, 70%, 38%, 1)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 443 B |
3
src/assets/icons/red-close.svg
Normal file
3
src/assets/icons/red-close.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14 14 20 20m-20 0 20-20" stroke="hsla(343, 92%, 54%, 1)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -11,6 +11,7 @@ import { ExternalLink } from "./layout/ExternalLink";
|
||||
import { BetaWarningModal } from "~/components/BetaWarningModal";
|
||||
import settings from "~/assets/icons/settings.svg";
|
||||
import pixelLogo from "~/assets/mutiny-pixel-logo.png";
|
||||
import { PendingNwc } from "./PendingNwc";
|
||||
|
||||
export default function App() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
@@ -49,6 +50,9 @@ export default function App() {
|
||||
<ReloadPrompt />
|
||||
</Show>
|
||||
<BalanceBox loading={state.wallet_loading} />
|
||||
<Show when={!state.wallet_loading}>
|
||||
<PendingNwc />
|
||||
</Show>
|
||||
<Card title="Activity">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
@@ -58,12 +62,11 @@ export default function App() {
|
||||
>
|
||||
<CombinedActivity limit={3} />
|
||||
</Show>
|
||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||
</VStack>
|
||||
<Show when={state.activity && state.activity.length > 0}>
|
||||
<A
|
||||
href="/activity"
|
||||
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
View All
|
||||
</A>
|
||||
|
||||
187
src/components/PendingNwc.tsx
Normal file
187
src/components/PendingNwc.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { NwcProfile } from "@mutinywallet/mutiny-wasm";
|
||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||
import { Card, LoadingSpinner, VStack } from "./layout";
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal
|
||||
} from "solid-js";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
import greenCheck from "~/assets/icons/green-check.svg";
|
||||
import redClose from "~/assets/icons/red-close.svg";
|
||||
import { ActivityAmount } from "./ActivityItem";
|
||||
import { InfoBox } from "./InfoBox";
|
||||
import eify from "~/utils/eify";
|
||||
import { A } from "solid-start";
|
||||
|
||||
type PendingItem = {
|
||||
id: string;
|
||||
name_of_connection: string;
|
||||
date?: bigint;
|
||||
amount_sats?: bigint;
|
||||
};
|
||||
|
||||
export function PendingNwc() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [error, setError] = createSignal<Error>();
|
||||
|
||||
const [pendingRequests, { refetch }] = createResource(async () => {
|
||||
const profiles: NwcProfile[] =
|
||||
await state.mutiny_wallet?.get_nwc_profiles();
|
||||
|
||||
const pending = await state.mutiny_wallet?.get_pending_nwc_invoices();
|
||||
|
||||
const pendingItems: PendingItem[] = [];
|
||||
|
||||
for (const p of pending) {
|
||||
const profile = profiles.find((pro) => pro.index === p.index);
|
||||
|
||||
if (profile) {
|
||||
pendingItems.push({
|
||||
id: p.id,
|
||||
name_of_connection: profile.name,
|
||||
date: p.expiry,
|
||||
amount_sats: p.amount_sats
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(pendingItems);
|
||||
|
||||
return pendingItems;
|
||||
});
|
||||
|
||||
const [paying, setPaying] = createSignal<string>("");
|
||||
|
||||
async function payItem(item: PendingItem) {
|
||||
try {
|
||||
setPaying(item.id);
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
await state.mutiny_wallet?.approve_invoice(item.id, nodes[0]);
|
||||
} catch (e) {
|
||||
setError(eify(e));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setPaying("");
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectItem(item: PendingItem) {
|
||||
try {
|
||||
setPaying(item.id);
|
||||
await state.mutiny_wallet?.deny_invoice(item.id);
|
||||
} catch (e) {
|
||||
setError(eify(e));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setPaying("");
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// When there's an error wait five seconds and then clear it
|
||||
if (error()) {
|
||||
setTimeout(() => {
|
||||
setError(undefined);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<Card title="Pending Requests">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||
</Show>
|
||||
<For each={pendingRequests()}>
|
||||
{(pendingItem) => (
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)_auto] items-center pb-4 gap-4 border-b border-neutral-800 last:border-b-0">
|
||||
<img
|
||||
class="w-[1rem]"
|
||||
src={bolt}
|
||||
alt="onchain"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-semibold truncate">
|
||||
{pendingItem.name_of_connection}
|
||||
</span>
|
||||
<time class="text-sm text-neutral-500">
|
||||
Expires {timeAgo(pendingItem.date)}
|
||||
</time>
|
||||
</div>
|
||||
<div>
|
||||
<ActivityAmount
|
||||
amount={
|
||||
pendingItem.amount_sats?.toString() ||
|
||||
"0"
|
||||
}
|
||||
price={state.price}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 w-[5rem]">
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
paying() !== pendingItem.id
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
payItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2.5rem] w-[2.5rem]"
|
||||
src={greenCheck}
|
||||
alt="Approve"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
rejectItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2rem] w-[2rem]"
|
||||
src={redClose}
|
||||
alt="Reject"
|
||||
/>
|
||||
</button>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
paying() === pendingItem.id
|
||||
}
|
||||
>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
<A
|
||||
href="/settings/connections"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
Configure
|
||||
</A>
|
||||
</Card>
|
||||
</Show>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -14,20 +14,24 @@ export function timeAgo(ts?: number | bigint): string {
|
||||
if (!ts || ts === 0) return "Pending";
|
||||
const timestamp = Number(ts) * 1000;
|
||||
const now = Date.now();
|
||||
const elapsedMilliseconds = now - timestamp;
|
||||
const negative = now - timestamp < 0;
|
||||
const nowOrAgo = negative ? "from now" : "ago";
|
||||
const elapsedMilliseconds = Math.abs(now - timestamp);
|
||||
const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
|
||||
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
||||
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
||||
const elapsedDays = Math.floor(elapsedHours / 24);
|
||||
|
||||
if (elapsedSeconds < 60) {
|
||||
return "Just now";
|
||||
return negative ? "seconds from now" : "Just now";
|
||||
} else if (elapsedMinutes < 60) {
|
||||
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? "s" : ""} ago`;
|
||||
return `${elapsedMinutes} minute${
|
||||
elapsedMinutes > 1 ? "s" : ""
|
||||
} ${nowOrAgo}`;
|
||||
} else if (elapsedHours < 24) {
|
||||
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ago`;
|
||||
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ${nowOrAgo}`;
|
||||
} else if (elapsedDays < 7) {
|
||||
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ago`;
|
||||
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ${nowOrAgo}`;
|
||||
} else {
|
||||
const date = new Date(timestamp);
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
Reference in New Issue
Block a user