0.0.2 loading states + wallet dialogs + monero unlock flow
This commit is contained in:
@@ -123,6 +123,17 @@
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (
|
||||
moneroWallet.ready &&
|
||||
!moneroWallet.walletOpen &&
|
||||
!moneroWallet.opening &&
|
||||
!moneroWallet.passwordRequired
|
||||
) {
|
||||
moneroWallet.login()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const handler = (event: ErrorEvent) => {
|
||||
@@ -181,7 +192,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col items-center pt-4 pb-2">
|
||||
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
||||
{#if loadingXrp || loadingXmr}
|
||||
{#if loadingXrp || loadingXmr || moneroWallet.opening || (moneroWallet.wallets.length > 0 && !moneroWallet.walletOpen)}
|
||||
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
||||
{:else}
|
||||
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||
@@ -240,9 +251,7 @@
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if loadingXmr && !moneroWallet.walletOpen}
|
||||
<div class="h-14 flex items-center"><Loader /></div>
|
||||
{:else if moneroWallet.walletOpen}
|
||||
{#if moneroWallet.walletOpen}
|
||||
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
||||
{#if xmrPrice}
|
||||
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||
@@ -252,8 +261,31 @@
|
||||
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
||||
</p>
|
||||
{/if}
|
||||
{:else if moneroWallet.passwordRequired && moneroWallet.wallets.length > 0}
|
||||
<p class="text-sm text-muted-foreground py-3">Wallet locked — unlock to view balance</p>
|
||||
{:else if moneroWallet.wallets.length > 0}
|
||||
<p class="text-sm text-muted-foreground py-3">Open wallet to view balance</p>
|
||||
<div class="h-14 flex items-center gap-2">
|
||||
<Loader />
|
||||
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||
</div>
|
||||
{:else if moneroWallet.opening}
|
||||
<div class="h-14 flex items-center gap-2">
|
||||
<Loader />
|
||||
<p class="text-sm text-muted-foreground">Starting wallet server...</p>
|
||||
</div>
|
||||
{:else if moneroWallet.error}
|
||||
<div class="py-3">
|
||||
<p class="text-xs text-destructive break-words">{moneroWallet.error}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-2 gap-1.5"
|
||||
onclick={() => moneroWallet.login()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
{:else if loadingXmr}
|
||||
<div class="h-14 flex items-center"><Loader /></div>
|
||||
{:else}
|
||||
<Button
|
||||
onclick={() => navigate('/monero')}
|
||||
|
||||
@@ -19,8 +19,27 @@
|
||||
let moneroMnemonic = $state('')
|
||||
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
||||
|
||||
let walletPassword = $state('')
|
||||
let rememberPassword = $state(true)
|
||||
let unlocking = $state(false)
|
||||
let unlockError = $state('')
|
||||
|
||||
let initStarted = false
|
||||
|
||||
async function unlockWithPassword() {
|
||||
if (unlocking || !walletPassword.trim()) return
|
||||
unlocking = true
|
||||
unlockError = ''
|
||||
try {
|
||||
await w.unlockWallet(walletPassword.trim(), rememberPassword)
|
||||
walletPassword = ''
|
||||
} catch (e) {
|
||||
unlockError = e instanceof Error ? e.message : 'Failed to unlock wallet'
|
||||
} finally {
|
||||
unlocking = false
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (w.walletOpen && w.walletName) {
|
||||
navigate('/monero/wallet/:name', {
|
||||
@@ -40,7 +59,7 @@
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading) {
|
||||
if (w.running && !w.walletOpen && w.wallets.length > 0 && !w.loading && !w.opening) {
|
||||
w.autoUnlock(w.wallets[0])
|
||||
}
|
||||
})
|
||||
@@ -66,6 +85,15 @@
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.opening}
|
||||
<Card>
|
||||
<CardContent class="flex items-center justify-center gap-2 py-8">
|
||||
<Loader />
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{!w.running ? 'Starting wallet server...' : 'Opening wallet...'}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if !w.installed}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -99,7 +127,48 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if !w.walletOpen}
|
||||
{#if w.wallets.length > 0}
|
||||
{#if w.opening}
|
||||
<Card>
|
||||
<CardContent class="flex items-center justify-center py-8">
|
||||
<Loader />
|
||||
<span class="ml-2 text-sm text-muted-foreground">
|
||||
Opening wallet...
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.passwordRequired && w.wallets.length > 0}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallet locked</CardTitle>
|
||||
<CardDescription>
|
||||
Enter the password for {w.wallets[0]} to open it
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Wallet password"
|
||||
bind:value={walletPassword} />
|
||||
<label class="flex items-center gap-2 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rememberPassword}
|
||||
class="h-3.5 w-3.5" />
|
||||
Remember password for auto-unlock
|
||||
</label>
|
||||
<Button
|
||||
onclick={unlockWithPassword}
|
||||
disabled={unlocking || !walletPassword.trim()}>
|
||||
{unlocking ? 'Opening...' : 'Unlock Wallet'}
|
||||
</Button>
|
||||
{#if unlockError}
|
||||
<p class="text-xs text-destructive">{unlockError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.wallets.length > 0}
|
||||
<Card>
|
||||
<CardContent class="flex items-center justify-center py-8">
|
||||
<Loader />
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
import Send from '@lucide/svelte/icons/send'
|
||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||
import Plus from '@lucide/svelte/icons/plus'
|
||||
import Users from '@lucide/svelte/icons/users'
|
||||
import Tags from '@lucide/svelte/icons/tags'
|
||||
import Loader from '$lib/components/loader.svelte'
|
||||
import QrCode from '$lib/components/qr.svelte'
|
||||
import { navigate, route } from 'sv-router/generated'
|
||||
@@ -97,9 +99,53 @@
|
||||
|
||||
let accountDialogOpen = $state(false)
|
||||
let accountLabel = $state('')
|
||||
let accountBusy = $state(false)
|
||||
let accountError = $state('')
|
||||
let subaddressDialogOpen = $state(false)
|
||||
let subaddressAccountIndex = $state('')
|
||||
let subaddressLabel = $state('')
|
||||
let subaddressBusy = $state(false)
|
||||
let subaddressError = $state('')
|
||||
|
||||
$effect(() => {
|
||||
if (!subaddressAccountIndex && w.accounts.length > 0) {
|
||||
subaddressAccountIndex = String(w.accounts[0].index)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleCreateAccount() {
|
||||
if (accountBusy) return
|
||||
accountBusy = true
|
||||
accountError = ''
|
||||
try {
|
||||
await w.createAccount(accountLabel || undefined)
|
||||
accountDialogOpen = false
|
||||
accountLabel = ''
|
||||
} catch (e) {
|
||||
accountError = e instanceof Error ? e.message : 'Failed to create account'
|
||||
} finally {
|
||||
accountBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateSubaddress() {
|
||||
if (subaddressBusy || !subaddressAccountIndex) return
|
||||
subaddressBusy = true
|
||||
subaddressError = ''
|
||||
try {
|
||||
await w.createSubaddress(
|
||||
parseInt(subaddressAccountIndex),
|
||||
subaddressLabel || undefined,
|
||||
)
|
||||
subaddressDialogOpen = false
|
||||
subaddressAccountIndex = ''
|
||||
subaddressLabel = ''
|
||||
} catch (e) {
|
||||
subaddressError = e instanceof Error ? e.message : 'Failed to create subaddress'
|
||||
} finally {
|
||||
subaddressBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
const walletName = $derived(route.params.name)
|
||||
|
||||
@@ -134,16 +180,27 @@
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!w.walletOpen || w.walletName !== walletName) {
|
||||
if (w.walletOpen && w.walletName === walletName) {
|
||||
w.refresh()
|
||||
electrobun.rpc?.request
|
||||
.fetchMoneroPrice({})
|
||||
.then((p) => (moneroPrice = p))
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!w.opening && (!w.walletOpen || w.walletName !== walletName)) {
|
||||
navigate('/monero')
|
||||
}
|
||||
w.refresh()
|
||||
electrobun.rpc?.request
|
||||
.fetchMoneroPrice({})
|
||||
.then((p) => (moneroPrice = p))
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if !w.walletOpen || w.walletName !== walletName}
|
||||
<div class="flex flex-col items-center justify-center gap-3 py-24">
|
||||
<Loader />
|
||||
<p class="text-sm text-muted-foreground">Opening wallet...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col items-center py-2">
|
||||
{#if w.loading}
|
||||
@@ -340,14 +397,44 @@
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create Account</Dialog.Title>
|
||||
<Dialog.Description>Optional label for the new account</Dialog.Description>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<Users size={16} class="text-muted-foreground" />
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<Dialog.Title>Create Account</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
New account with its own balance and addresses
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Header>
|
||||
<div class="px-6 pb-4 space-y-3">
|
||||
<Input placeholder="Account label (optional)" bind:value={accountLabel} />
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium" for="account-label">Label</label>
|
||||
<Input id="account-label" placeholder="Savings" bind:value={accountLabel} />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Optional — shown next to the account in your wallet
|
||||
</p>
|
||||
</div>
|
||||
{#if accountError}
|
||||
<p class="text-xs text-destructive">{accountError}</p>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onclick={() => { accountDialogOpen = false; accountLabel = '' }}>Cancel</Button>
|
||||
<Button size="sm" onclick={async () => { await w.createAccount(accountLabel || undefined); accountDialogOpen = false; accountLabel = '' }}>Create</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={accountBusy}
|
||||
onclick={() => {
|
||||
accountDialogOpen = false
|
||||
accountLabel = ''
|
||||
accountError = ''
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" disabled={accountBusy} onclick={handleCreateAccount}>
|
||||
{accountBusy ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
@@ -360,29 +447,66 @@
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create Subaddress</Dialog.Title>
|
||||
<Dialog.Description>Account index and optional label</Dialog.Description>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<Tags size={16} class="text-muted-foreground" />
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<Dialog.Title>Create Subaddress</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Receiving address tied to an account
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Header>
|
||||
<div class="px-6 pb-4 space-y-3">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Account</label>
|
||||
<label class="text-xs font-medium" for="subaddress-account">Account</label>
|
||||
<Select type="single" bind:value={subaddressAccountIndex}>
|
||||
<SelectTrigger>
|
||||
{subaddressAccountIndex ? `Account ${subaddressAccountIndex}` : 'Select account'}
|
||||
Account {subaddressAccountIndex}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each w.accounts as acct}
|
||||
<SelectItem value={String(acct.index)}>
|
||||
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''}
|
||||
Account {acct.index}{acct.label ? ` - ${acct.label}` : ''} ({atomicToXmr(acct.unlockedBalance)} XMR)
|
||||
</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Where the subaddress belongs
|
||||
</p>
|
||||
</div>
|
||||
<Input placeholder="Subaddress label (optional)" bind:value={subaddressLabel} />
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium" for="subaddress-label">Label</label>
|
||||
<Input id="subaddress-label" placeholder="Donations" bind:value={subaddressLabel} />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Optional — helps you track who sent you funds
|
||||
</p>
|
||||
</div>
|
||||
{#if subaddressError}
|
||||
<p class="text-xs text-destructive">{subaddressError}</p>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onclick={() => { subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Cancel</Button>
|
||||
<Button size="sm" disabled={!subaddressAccountIndex} onclick={async () => { await w.createSubaddress(parseInt(subaddressAccountIndex), subaddressLabel || undefined); subaddressDialogOpen = false; subaddressAccountIndex = ''; subaddressLabel = '' }}>Create</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={subaddressBusy}
|
||||
onclick={() => {
|
||||
subaddressDialogOpen = false
|
||||
subaddressAccountIndex = ''
|
||||
subaddressLabel = ''
|
||||
subaddressError = ''
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={subaddressBusy || !subaddressAccountIndex}
|
||||
onclick={handleCreateSubaddress}>
|
||||
{subaddressBusy ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
@@ -455,4 +579,5 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user