mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-05 15:24:28 +01:00
backup and restore prompts
This commit is contained in:
@@ -6,6 +6,7 @@ import ReloadPrompt from "~/components/Reload";
|
||||
import { A } from 'solid-start';
|
||||
import { Activity } from './Activity';
|
||||
import settings from '~/assets/icons/settings.svg';
|
||||
import { OnboardWarning } from './OnboardWarning';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -16,6 +17,7 @@ export default function App() {
|
||||
<img src={logo} class="h-10" alt="logo" />
|
||||
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>
|
||||
</header>
|
||||
<OnboardWarning />
|
||||
<ReloadPrompt />
|
||||
<BalanceBox />
|
||||
<Activity />
|
||||
|
||||
@@ -17,54 +17,26 @@ function SyncingIndicator() {
|
||||
}
|
||||
|
||||
export default function BalanceBox() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const fetchOnchainBalance = async () => {
|
||||
console.log("Refetching onchain balance");
|
||||
await state.node_manager?.sync();
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
return balance
|
||||
};
|
||||
|
||||
// TODO: it's hacky to do these separately, but ln doesn't need the sync so I don't want to wait
|
||||
const fetchLnBalance = async () => {
|
||||
console.log("Refetching ln balance");
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
return balance
|
||||
};
|
||||
|
||||
const [onChainBalance, { refetch: refetchOnChainBalance }] = createResource(fetchOnchainBalance);
|
||||
const [lnBalance, { refetch: refetchLnBalance }] = createResource(fetchLnBalance);
|
||||
|
||||
function refetchBalance() {
|
||||
refetchLnBalance();
|
||||
refetchOnChainBalance();
|
||||
}
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FancyCard title="Lightning">
|
||||
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||
<Show when={lnBalance()}>
|
||||
<Amount amountSats={lnBalance()?.lightning} showFiat />
|
||||
</Show>
|
||||
</Suspense>
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
</FancyCard>
|
||||
|
||||
<FancyCard title="On-Chain" tag={onChainBalance.loading && <SyncingIndicator />}>
|
||||
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||
<div onClick={refetchBalance}>
|
||||
<Amount amountSats={onChainBalance()?.confirmed} showFiat loading={onChainBalance.loading} />
|
||||
</div>
|
||||
</Suspense>
|
||||
<FancyCard title="On-Chain" tag={state.is_syncing && <SyncingIndicator />}>
|
||||
<div onClick={actions.sync}>
|
||||
<Amount amountSats={state.balance?.confirmed} showFiat />
|
||||
</div>
|
||||
<Suspense>
|
||||
<Show when={onChainBalance()?.unconfirmed}>
|
||||
<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(onChainBalance()?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||
{prettyPrintAmount(state.balance?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
48
src/components/OnboardWarning.tsx
Normal file
48
src/components/OnboardWarning.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Show, createSignal, onMount } from "solid-js";
|
||||
import { Button, ButtonLink, SmallHeader, VStack } from "./layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function OnboardWarning() {
|
||||
const [state, actions] = useMegaStore();
|
||||
const [dismissedBackup, setDismissedBackup] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
actions.sync()
|
||||
})
|
||||
|
||||
function hasMoney() {
|
||||
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: show this once we have a restore flow */}
|
||||
<Show when={!state.dismissed_restore_prompt && false}>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
|
||||
<SmallHeader>Welcome!</SmallHeader>
|
||||
<VStack>
|
||||
<p class="text-2xl font-light">
|
||||
Do you want to restore an existing Mutiny Wallet?
|
||||
</p>
|
||||
<div class="w-full flex gap-2">
|
||||
<Button intent="green" onClick={() => { }}>Restore</Button>
|
||||
<Button onClick={actions.dismissRestorePrompt}>Nope</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
|
||||
<SmallHeader>Secure your funds</SmallHeader>
|
||||
<p class="text-2xl font-light">
|
||||
You have money stored in this browser. Let's make sure you have a backup.
|
||||
</p>
|
||||
<div class="w-full flex gap-2">
|
||||
<ButtonLink intent="blue" href="/backup">Backup</ButtonLink>
|
||||
<Button onClick={() => { setDismissedBackup(true) }}>Nope</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,36 @@
|
||||
import { Match, Switch, createSignal } from "solid-js"
|
||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js"
|
||||
|
||||
export function SeedWords(props: { words: string }) {
|
||||
export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean) => void }) {
|
||||
const [shouldShow, setShouldShow] = createSignal(false)
|
||||
|
||||
function toggleShow() {
|
||||
setShouldShow(!shouldShow())
|
||||
if (shouldShow()) {
|
||||
props.setHasSeen?.(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (<pre class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden">
|
||||
const splitWords = createMemo(() => props.words.split(" "))
|
||||
|
||||
return (<button class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden" onClick={toggleShow}>
|
||||
<Switch>
|
||||
<Match when={!shouldShow()}>
|
||||
<div onClick={toggleShow} class="cursor-pointer">
|
||||
<div class="cursor-pointer">
|
||||
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={shouldShow()}>
|
||||
<div onClick={toggleShow} class="cursor-pointer overflow-hidden">
|
||||
<p class="font-mono w-full whitespace-pre-wrap">
|
||||
{props.words}
|
||||
</p>
|
||||
</div>
|
||||
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
||||
<For each={splitWords()}>
|
||||
{(word) => (
|
||||
<li class="font-mono text-left">
|
||||
{word}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Match>
|
||||
</Switch>
|
||||
</pre >)
|
||||
</button >)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
||||
return (
|
||||
<Toast.Root toastId={props.toastId} class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${props.isError ? "border-m-red/50" : "border-white/10"} `}>
|
||||
<div class="flex gap-4 w-full justify-between items-start">
|
||||
<div>
|
||||
<div class="flex-1">
|
||||
<Toast.Title>
|
||||
<SmallHeader>
|
||||
{props.title}
|
||||
@@ -43,7 +43,7 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
||||
</p>
|
||||
</Toast.Description>
|
||||
</div>
|
||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem]">
|
||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem] flex-0">
|
||||
<img src={close} alt="Close" />
|
||||
</Toast.CloseButton>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,10 @@ const SmallAmount: ParentComponent<{ amount: number | bigint }> = (props) => {
|
||||
return (<h2 class="font-light text-lg">{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
|
||||
}
|
||||
|
||||
export const NiceP: ParentComponent = (props) => {
|
||||
return (<p class="text-2xl font-light">{props.children}</p>)
|
||||
}
|
||||
|
||||
export {
|
||||
SmallHeader,
|
||||
Card,
|
||||
|
||||
44
src/routes/Backup.tsx
Normal file
44
src/routes/Backup.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button, DefaultMain, LargeHeader, NiceP, NodeManagerGuard, SafeArea, VStack } from "~/components/layout";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { useNavigate } from 'solid-start';
|
||||
import { BackButton } from '~/components/layout/BackButton';
|
||||
import { SeedWords } from '~/components/SeedWords';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { Show, createSignal } from 'solid-js';
|
||||
|
||||
export default function App() {
|
||||
const [store, actions] = useMegaStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hasSeenBackup, setHasSeenBackup] = createSignal(false);
|
||||
|
||||
function wroteDownTheWords() {
|
||||
actions.setHasBackedUp()
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeManagerGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<BackButton />
|
||||
<LargeHeader>Backup</LargeHeader>
|
||||
<VStack>
|
||||
<NiceP>Let's get these funds secured.</NiceP>
|
||||
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
|
||||
<NiceP>
|
||||
If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.
|
||||
</NiceP>
|
||||
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
|
||||
<SeedWords words={store.node_manager?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
|
||||
<Show when={hasSeenBackup()}>
|
||||
<NiceP>You are responsible for your funds!</NiceP>
|
||||
</Show>
|
||||
<Button disabled={!hasSeenBackup()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="none" />
|
||||
</SafeArea>
|
||||
</NodeManagerGuard>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||
/* @refresh reload */
|
||||
|
||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||
import { ParentComponent, createContext, createEffect, onCleanup, onMount, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { NodeManagerSettingStrings, setupNodeManager } from "~/logic/nodeManagerSetup";
|
||||
@@ -16,14 +17,19 @@ export type MegaStore = [{
|
||||
user_status: UserStatus;
|
||||
scan_result?: ParsedParams;
|
||||
balance?: MutinyBalance;
|
||||
is_syncing?: boolean;
|
||||
last_sync?: number;
|
||||
price: number
|
||||
has_backed_up: boolean,
|
||||
dismissed_restore_prompt: boolean
|
||||
}, {
|
||||
fetchUserStatus(): Promise<UserStatus>;
|
||||
setupNodeManager(settings?: NodeManagerSettingStrings): Promise<void>;
|
||||
setWaitlistId(waitlist_id: string): void;
|
||||
setScanResult(scan_result: ParsedParams | undefined): void;
|
||||
sync(): Promise<void>;
|
||||
dismissRestorePrompt(): void;
|
||||
setHasBackedUp(): void;
|
||||
}];
|
||||
|
||||
export const Provider: ParentComponent = (props) => {
|
||||
@@ -33,7 +39,12 @@ export const Provider: ParentComponent = (props) => {
|
||||
user_status: undefined as UserStatus,
|
||||
scan_result: undefined as ParsedParams | undefined,
|
||||
// TODO: wire this up to real price once we have caching
|
||||
price: 30000
|
||||
price: 30000,
|
||||
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
||||
balance: undefined as MutinyBalance | undefined,
|
||||
last_sync: undefined as number | undefined,
|
||||
is_syncing: false,
|
||||
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true"
|
||||
});
|
||||
|
||||
const actions = {
|
||||
@@ -69,14 +80,29 @@ export const Provider: ParentComponent = (props) => {
|
||||
async sync(): Promise<void> {
|
||||
console.time("BDK Sync Time")
|
||||
try {
|
||||
await state.node_manager?.sync()
|
||||
if (state.node_manager && !state.is_syncing) {
|
||||
setState({ is_syncing: true })
|
||||
await state.node_manager?.sync()
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
setState({ balance, last_sync: Date.now() })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setState({ is_syncing: false })
|
||||
}
|
||||
console.timeEnd("BDK Sync Time")
|
||||
},
|
||||
setScanResult(scan_result: ParsedParams) {
|
||||
setState({ scan_result })
|
||||
},
|
||||
setHasBackedUp() {
|
||||
localStorage.setItem("has_backed_up", "true")
|
||||
setState({ has_backed_up: true })
|
||||
},
|
||||
dismissRestorePrompt() {
|
||||
localStorage.setItem("dismissed_restore_prompt", "true")
|
||||
setState({ dismissed_restore_prompt: true })
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,8 +127,8 @@ export const Provider: ParentComponent = (props) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (state.node_manager) actions.sync();
|
||||
const interval = setInterval(async () => {
|
||||
await actions.sync();
|
||||
}, 60 * 1000); // Poll every minute
|
||||
|
||||
onCleanup(() => {
|
||||
|
||||
Reference in New Issue
Block a user