diff --git a/apps/desktop/src/app.css b/apps/desktop/src/app.css index e19f266..5081455 100644 --- a/apps/desktop/src/app.css +++ b/apps/desktop/src/app.css @@ -6,77 +6,78 @@ @custom-variant dark (&:is(.dark *)); :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); + --background: oklch(0.99 0 0); + --foreground: oklch(0.16 0 0); --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); + --card-foreground: oklch(0.16 0 0); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.553 0.195 38.402); - --primary-foreground: oklch(0.98 0.016 73.684); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.553 0.195 38.402); - --accent-foreground: oklch(0.98 0.016 73.684); + --popover-foreground: oklch(0.16 0 0); + --primary: oklch(0.4 0 0); + --primary-foreground: oklch(0.99 0 0); + --secondary: oklch(0.96 0 0); + --secondary-foreground: oklch(0.2 0 0); + --muted: oklch(0.965 0 0); + --muted-foreground: oklch(0.52 0 0); + --accent: oklch(0.94 0 0); + --accent-foreground: oklch(0.2 0 0); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.837 0.128 66.29); - --chart-2: oklch(0.705 0.213 47.604); - --chart-3: oklch(0.646 0.222 41.116); - --chart-4: oklch(0.553 0.195 38.402); - --chart-5: oklch(0.47 0.157 37.304); - --radius: 0rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.646 0.222 41.116); - --sidebar-primary-foreground: oklch(0.98 0.016 73.684); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --border: oklch(0.905 0 0); + --input: oklch(0.905 0 0); + --ring: oklch(0.6 0 0); + --chart-1: oklch(0.4 0 0); + --chart-2: oklch(0.55 0 0); + --chart-3: oklch(0.68 0 0); + --chart-4: oklch(0.78 0 0); + --chart-5: oklch(0.88 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.97 0 0); + --sidebar-foreground: oklch(0.2 0 0); + --sidebar-primary: oklch(0.35 0 0); + --sidebar-primary-foreground: oklch(0.99 0 0); + --sidebar-accent: oklch(0.92 0 0); + --sidebar-accent-foreground: oklch(0.2 0 0); + --sidebar-border: oklch(0.905 0 0); + --sidebar-ring: oklch(0.6 0 0); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.47 0.157 37.304); - --primary-foreground: oklch(0.98 0.016 73.684); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.47 0.157 37.304); - --accent-foreground: oklch(0.98 0.016 73.684); + --background: oklch(0.21 0 0); + --foreground: oklch(0.96 0 0); + --card: oklch(0.235 0 0); + --card-foreground: oklch(0.96 0 0); + --popover: oklch(0.25 0 0); + --popover-foreground: oklch(0.96 0 0); + --primary: oklch(0.5 0 0); + --primary-foreground: oklch(0.99 0 0); + --secondary: oklch(0.29 0 0); + --secondary-foreground: oklch(0.96 0 0); + --muted: oklch(0.27 0 0); + --muted-foreground: oklch(0.66 0 0); + --accent: oklch(0.3 0 0); + --accent-foreground: oklch(0.96 0 0); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.837 0.128 66.29); - --chart-2: oklch(0.705 0.213 47.604); - --chart-3: oklch(0.646 0.222 41.116); - --chart-4: oklch(0.553 0.195 38.402); - --chart-5: oklch(0.47 0.157 37.304); + --border: oklch(1 0 0 / 12%); + --input: oklch(1 0 0 / 16%); + --ring: oklch(0.55 0 0); + --chart-1: oklch(0.7 0 0); + --chart-2: oklch(0.6 0 0); + --chart-3: oklch(0.5 0 0); + --chart-4: oklch(0.4 0 0); + --chart-5: oklch(0.3 0 0); --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.705 0.213 47.604); - --sidebar-primary-foreground: oklch(0.98 0.016 73.684); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.92 0 0); + --sidebar-primary: oklch(0.7 0 0); + --sidebar-primary-foreground: oklch(0.1 0 0); + --sidebar-accent: oklch(0.28 0 0); + --sidebar-accent-foreground: oklch(0.96 0 0); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --sidebar-ring: oklch(0.55 0 0); } @theme inline { - --font-mono: 'Geist Mono Variable', monospace; - --font-heading: 'Geist Mono Variable', monospace; + --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", system-ui, sans-serif; + --font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace; + --font-heading: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -121,10 +122,41 @@ * { @apply border-border outline-ring/50; } + html { + @apply font-sans; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "ss01", "cv01"; + } body { @apply bg-background text-foreground; } - html { - @apply font-mono; + ::selection { + background: oklch(0.5 0 0 / 25%); } } + +@layer base { + * { + scrollbar-width: thin; + scrollbar-color: oklch(0.5 0 0 / 25%) transparent; + } + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: oklch(0.5 0 0 / 22%); + border-radius: 9999px; + border: 2px solid transparent; + background-clip: content-box; + } + ::-webkit-scrollbar-thumb:hover { + background: oklch(0.5 0 0 / 38%); + background-clip: content-box; + } +} \ No newline at end of file diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index 29d8637..4eb2739 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -353,16 +353,44 @@ export async function getTransferDetails( txid: string, ): Promise { if (!state.wallet) throw new Error('Wallet RPC not started') - const result = await rawRpc(state, 'get_transfer_by_txid', { txid }) - if (!result?.transfer) return null - const t = result.transfer + let t: any = null + let dests: any[] = [] + try { + const result = await rawRpc(state, 'get_transfer_by_txid', { txid }) + if (result?.transfer) { + t = result.transfer + dests = + t.destinations?.map((d: any) => ({ + address: d.address, + amount: d.amount?.toString() ?? '0', + })) ?? [] + } + } catch { + // get_transfer_by_txid can fail for various reasons; fall back + } + if (!t) { + const params: Record = { + in: true, + out: true, + pending: true, + pool: true, + } + const result = await rawRpc(state, 'get_transfers', params) + let found: any = null + for (const dir of ['in', 'out', 'pending', 'pool'] as const) { + const transfers = result[dir] ?? [] + for (const tr of transfers) { + if (tr.txid === txid) { + found = { ...tr, type: dir === 'in' ? 'in' : 'out' } + break + } + } + if (found) break + } + if (!found) return null + t = found + } const isIn = t.type === 'in' - const isOut = t.type === 'out' - const dests = - t.destinations?.map((d: any) => ({ - address: d.address, - amount: d.amount?.toString() ?? '0', - })) ?? [] const subaddrIndices = isIn ? [ { diff --git a/apps/desktop/src/bun/native/biometric-helper b/apps/desktop/src/bun/native/biometric-helper index cf4ea97..d6875f9 100755 Binary files a/apps/desktop/src/bun/native/biometric-helper and b/apps/desktop/src/bun/native/biometric-helper differ diff --git a/apps/desktop/src/lib/states/monero-wallet.svelte.ts b/apps/desktop/src/lib/states/monero-wallet.svelte.ts index 054ff79..7b9c0fc 100644 --- a/apps/desktop/src/lib/states/monero-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/monero-wallet.svelte.ts @@ -246,6 +246,22 @@ export const MoneroWallet = () => { await refresh() } + const autoUnlock = async (name?: string) => { + if (!electrobun.rpc) return false + const target = name ?? wallets[0] + if (!target) return false + const pw = await moneroGetStoredPassword(target) + if (!pw) return false + await electrobun.rpc.request.moneroOpenWallet({ + name: target, + password: pw, + }) + walletName = target + walletOpen = true + await refresh() + return true + } + const refresh = async () => { if (!electrobun.rpc) return loading = true @@ -430,6 +446,7 @@ export const MoneroWallet = () => { createWallet, restoreWallet, openWallet, + autoUnlock, refresh, send, sendAll, diff --git a/apps/desktop/src/lib/states/session.svelte.ts b/apps/desktop/src/lib/states/session.svelte.ts new file mode 100644 index 0000000..a8cfe37 --- /dev/null +++ b/apps/desktop/src/lib/states/session.svelte.ts @@ -0,0 +1,59 @@ +import { electrobun } from '$lib/electrobun' + +let unlocked = $state(false) +let biometricAvailable = $state(false) +let unlocking = $state(false) +let error = $state('') + +const checkBiometric = async () => { + if (!electrobun.rpc) return + try { + biometricAvailable = await electrobun.rpc.request.biometricCanAuth({}) + } catch { /* ignore */ } +} + +const unlock = async (): Promise => { + if (!electrobun.rpc) return false + unlocking = true + error = '' + try { + const ok = await electrobun.rpc.request.biometricAuth({ + reason: 'Unlock Koins', + }) + if (ok) { + unlocked = true + } else { + error = 'Authentication failed' + } + return ok + } catch (e) { + error = e instanceof Error ? e.message : 'Unlock failed' + return false + } finally { + unlocking = false + } +} + +const lock = () => { + unlocked = false + error = '' +} + +checkBiometric() + +export const session = { + get unlocked() { + return unlocked + }, + get biometricAvailable() { + return biometricAvailable + }, + get unlocking() { + return unlocking + }, + get error() { + return error + }, + unlock, + lock, +} \ No newline at end of file diff --git a/apps/desktop/src/lib/states/xrp-wallet.svelte.ts b/apps/desktop/src/lib/states/xrp-wallet.svelte.ts index a5f4b7d..eaa33b5 100644 --- a/apps/desktop/src/lib/states/xrp-wallet.svelte.ts +++ b/apps/desktop/src/lib/states/xrp-wallet.svelte.ts @@ -179,6 +179,28 @@ export const XrpWallet = () => { return false } + const autoUnlock = async (walletId?: string) => { + const id = walletId ?? currentWalletId ?? wallets[0]?.id + if (!id) return false + currentWalletId = id + const wallet = wallets.find((w) => w.id === id) + if (!wallet) return false + if (electrobun.rpc && wallet.hasPassword) { + const [ph] = await tryCatch( + electrobun.rpc.request.getSecret({ + service: 'koins', + name: `xrp_auth_${id}`, + }), + ) + if (ph) currentPasswordHash = ph + } else { + currentPasswordHash = null + } + const ok = await loadSeedForWallet(id) + if (ok) accountType = 'xrp' + return ok + } + const saveWallet = async ( kind: 'create' | 'import', name: string, @@ -412,6 +434,7 @@ export const XrpWallet = () => { hashPassword, selectWallet, selectAndUnlockWallet, + autoUnlock, loadSeedForWallet, deleteWallet, clearSelection, diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 888bf36..1f5a505 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -1,31 +1,125 @@ + +{#if !session.unlocked} +
+
+
+ +
+
+

Koins

+

+ Use Touch ID to unlock +

+
+
+ + {#if session.biometricAvailable} + + {:else} +

+ Biometric authentication isn't available on this device. +

+ {/if} + + {#if session.error} +

{session.error}

+ {/if} +
+{:else} +
+ + +
+
+
+ {title} +
+
+ +
+
+ {@render children?.()} +
+
+
+
+{/if} \ No newline at end of file diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index 73e4fd7..7d488bc 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -1,5 +1,6 @@ -
+
{#if !w.ready}

@@ -59,9 +60,6 @@ to get started

{:else if w.accountType === 'monero'} - - ← Back to home - {#if w.downloading} @@ -134,7 +132,19 @@
{#if moneroSelectedWallet}
- {#if w.biometricAvailable} + {#if session.unlocked} + + {:else if w.biometricAvailable} - +
Send XMR diff --git a/apps/desktop/src/routes/monero/settings.svelte b/apps/desktop/src/routes/monero/settings.svelte index 8a00758..a5b025d 100644 --- a/apps/desktop/src/routes/monero/settings.svelte +++ b/apps/desktop/src/routes/monero/settings.svelte @@ -21,7 +21,7 @@ }) -
+
@@ -82,14 +82,7 @@

{/if} -
- -
+
diff --git a/apps/desktop/src/routes/monero/tx/[hash].svelte b/apps/desktop/src/routes/monero/tx/[hash].svelte index cf433cf..9fadf77 100644 --- a/apps/desktop/src/routes/monero/tx/[hash].svelte +++ b/apps/desktop/src/routes/monero/tx/[hash].svelte @@ -35,15 +35,7 @@ }) -
- - +
Transaction Details diff --git a/apps/desktop/src/routes/monero/wallet/[name].svelte b/apps/desktop/src/routes/monero/wallet/[name].svelte index 22a7f9d..feba7f4 100644 --- a/apps/desktop/src/routes/monero/wallet/[name].svelte +++ b/apps/desktop/src/routes/monero/wallet/[name].svelte @@ -146,7 +146,7 @@ }) -
+
{@const a = chunkAddress(w.address)} diff --git a/apps/desktop/src/routes/settings.svelte b/apps/desktop/src/routes/settings.svelte index c1c3e19..2b348c4 100644 --- a/apps/desktop/src/routes/settings.svelte +++ b/apps/desktop/src/routes/settings.svelte @@ -33,23 +33,9 @@ } -
+
-
- -
- {#if w.wallets.length > 0} +{#if w.wallets.length > 0} Wallets diff --git a/apps/desktop/src/routes/xrp/index.svelte b/apps/desktop/src/routes/xrp/index.svelte index ee25b52..fbeed31 100644 --- a/apps/desktop/src/routes/xrp/index.svelte +++ b/apps/desktop/src/routes/xrp/index.svelte @@ -1,5 +1,6 @@ -
+
{#if !w.ready}

@@ -75,11 +76,6 @@ to get started

{:else} - - ← Back to home - {#if newSeed} @@ -177,7 +173,9 @@ {#each w.wallets as wal} - +
Send XRP diff --git a/apps/desktop/src/routes/xrp/tx/[hash].svelte b/apps/desktop/src/routes/xrp/tx/[hash].svelte index 3c7e22a..3c41faa 100644 --- a/apps/desktop/src/routes/xrp/tx/[hash].svelte +++ b/apps/desktop/src/routes/xrp/tx/[hash].svelte @@ -35,18 +35,7 @@ }) -
- - +
Transaction Details diff --git a/apps/desktop/src/routes/xrp/wallet/[id].svelte b/apps/desktop/src/routes/xrp/wallet/[id].svelte index 3002344..61d92c7 100644 --- a/apps/desktop/src/routes/xrp/wallet/[id].svelte +++ b/apps/desktop/src/routes/xrp/wallet/[id].svelte @@ -55,7 +55,7 @@ }) -
+