mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-20 15:54: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 { BetaWarningModal } from "~/components/BetaWarningModal";
|
||||||
import settings from "~/assets/icons/settings.svg";
|
import settings from "~/assets/icons/settings.svg";
|
||||||
import pixelLogo from "~/assets/mutiny-pixel-logo.png";
|
import pixelLogo from "~/assets/mutiny-pixel-logo.png";
|
||||||
|
import { PendingNwc } from "./PendingNwc";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
@@ -49,6 +50,9 @@ export default function App() {
|
|||||||
<ReloadPrompt />
|
<ReloadPrompt />
|
||||||
</Show>
|
</Show>
|
||||||
<BalanceBox loading={state.wallet_loading} />
|
<BalanceBox loading={state.wallet_loading} />
|
||||||
|
<Show when={!state.wallet_loading}>
|
||||||
|
<PendingNwc />
|
||||||
|
</Show>
|
||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
@@ -58,12 +62,11 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
<CombinedActivity limit={3} />
|
<CombinedActivity limit={3} />
|
||||||
</Show>
|
</Show>
|
||||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
<Show when={state.activity && state.activity.length > 0}>
|
<Show when={state.activity && state.activity.length > 0}>
|
||||||
<A
|
<A
|
||||||
href="/activity"
|
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
|
View All
|
||||||
</A>
|
</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";
|
if (!ts || ts === 0) return "Pending";
|
||||||
const timestamp = Number(ts) * 1000;
|
const timestamp = Number(ts) * 1000;
|
||||||
const now = Date.now();
|
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 elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
|
||||||
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
||||||
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
||||||
const elapsedDays = Math.floor(elapsedHours / 24);
|
const elapsedDays = Math.floor(elapsedHours / 24);
|
||||||
|
|
||||||
if (elapsedSeconds < 60) {
|
if (elapsedSeconds < 60) {
|
||||||
return "Just now";
|
return negative ? "seconds from now" : "Just now";
|
||||||
} else if (elapsedMinutes < 60) {
|
} else if (elapsedMinutes < 60) {
|
||||||
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? "s" : ""} ago`;
|
return `${elapsedMinutes} minute${
|
||||||
|
elapsedMinutes > 1 ? "s" : ""
|
||||||
|
} ${nowOrAgo}`;
|
||||||
} else if (elapsedHours < 24) {
|
} else if (elapsedHours < 24) {
|
||||||
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ago`;
|
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ${nowOrAgo}`;
|
||||||
} else if (elapsedDays < 7) {
|
} else if (elapsedDays < 7) {
|
||||||
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ago`;
|
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ${nowOrAgo}`;
|
||||||
} else {
|
} else {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
|||||||
Reference in New Issue
Block a user