From 1920a00cd65651f057cc44f4e38d14f8353cfa09 Mon Sep 17 00:00:00 2001 From: protom Date: Wed, 19 Aug 2026 23:55:59 +0200 Subject: [PATCH] feat: backup and security settings pages, nostr restore onboarding, passkey unlock entry --- src/boot/wallet.ts | 4 + src/components/RelaysEditor.vue | 78 ++++++++++ src/components/UnlockForm.vue | 31 ++++ src/lnurlcash/storage/settings.ts | 13 +- src/pages/BackupPage.vue | 243 ++++++++++++++++++++++++++++++ src/pages/SecurityPage.vue | 233 ++++++++++++++++++++++++++++ src/pages/SettingsPage.vue | 8 +- src/pages/WelcomePage.vue | 188 ++++++++++++++++++++++- src/router/routes.ts | 8 + src/stores/nostrBackup.ts | 167 ++++++++++++++++++++ src/stores/wallet.ts | 24 +++ 11 files changed, 990 insertions(+), 7 deletions(-) create mode 100644 src/components/RelaysEditor.vue create mode 100644 src/pages/BackupPage.vue create mode 100644 src/pages/SecurityPage.vue create mode 100644 src/stores/nostrBackup.ts diff --git a/src/boot/wallet.ts b/src/boot/wallet.ts index 6df9724..acea818 100644 --- a/src/boot/wallet.ts +++ b/src/boot/wallet.ts @@ -1,5 +1,6 @@ import { defineBoot } from '#q-app'; import { useWalletStore } from '@/stores/wallet'; +import { useNostrBackupStore } from '@/stores/nostrBackup'; // Wallet lifecycle bootstrap: reflects whatever is on this device into the // wallet store at app start - a plaintext-stored key unlocks straight away, @@ -8,4 +9,7 @@ import { useWalletStore } from '@/stores/wallet'; export default defineBoot(async () => { const wallet = useWalletStore(); await wallet.init(); + // instantiating the store arms its watchers: while the wallet is unlocked + // and nostr backup is enabled, store changes schedule debounced publishes + useNostrBackupStore(); }); diff --git a/src/components/RelaysEditor.vue b/src/components/RelaysEditor.vue new file mode 100644 index 0000000..a1512b0 --- /dev/null +++ b/src/components/RelaysEditor.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/components/UnlockForm.vue b/src/components/UnlockForm.vue index 9b6a355..7bbba3d 100644 --- a/src/components/UnlockForm.vue +++ b/src/components/UnlockForm.vue @@ -39,6 +39,17 @@ @click="unlock" /> + +
(); @@ -69,6 +81,25 @@ const showPassword = ref(false); const busy = ref(false); const error = ref(''); +// slots live in plain localStorage - a sync read at setup is enough; they +// can only change from the security page while unlocked +const passkeyAvailable = hasPasskeySlots(); +const passkeyBusy = ref(false); + +const unlockViaPasskey = async () => { + if (passkeyBusy.value) return; + passkeyBusy.value = true; + error.value = ''; + try { + await wallet.unlockWithPasskey(); + emit('unlocked'); + } catch (err) { + error.value = err instanceof Error ? err.message : 'Passkey unlock failed.'; + } finally { + passkeyBusy.value = false; + } +}; + const unlock = async () => { if (busy.value || password.value === '') return; busy.value = true; diff --git a/src/lnurlcash/storage/settings.ts b/src/lnurlcash/storage/settings.ts index e61a1ad..fa44848 100644 --- a/src/lnurlcash/storage/settings.ts +++ b/src/lnurlcash/storage/settings.ts @@ -4,6 +4,10 @@ export type WalletSettings = { defaultMint?: string + // nostr backup (see nostrBackup.ts): off unless the holder turns it on; + // relays are only persisted once edited - absent means the UI's defaults + nostrBackupEnabled?: boolean + nostrBackupRelays?: string[] } const SETTINGS_STORAGE_KEY = 'sattle_settings' @@ -17,7 +21,14 @@ export const loadSettings = (): WalletSettings => { const s = parsed as Record return { defaultMint: - typeof s.defaultMint === 'string' ? s.defaultMint : undefined + typeof s.defaultMint === 'string' ? s.defaultMint : undefined, + nostrBackupEnabled: + typeof s.nostrBackupEnabled === 'boolean' + ? s.nostrBackupEnabled + : undefined, + nostrBackupRelays: Array.isArray(s.nostrBackupRelays) + ? s.nostrBackupRelays.filter((r): r is string => typeof r === 'string') + : undefined } } catch { return {} diff --git a/src/pages/BackupPage.vue b/src/pages/BackupPage.vue new file mode 100644 index 0000000..52ec6f3 --- /dev/null +++ b/src/pages/BackupPage.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/pages/SecurityPage.vue b/src/pages/SecurityPage.vue new file mode 100644 index 0000000..f5192a6 --- /dev/null +++ b/src/pages/SecurityPage.vue @@ -0,0 +1,233 @@ + + + diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index d657e91..cdbe6dd 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -50,7 +50,13 @@ const router = useRouter(); type SettingsItem = { label: string; to?: string }; const groups: { label: string; items: SettingsItem[] }[] = [ - { label: 'Wallet', items: [{ label: 'Backup' }, { label: 'Security' }] }, + { + label: 'Wallet', + items: [ + { label: 'Backup', to: '/settings/backup' }, + { label: 'Security', to: '/settings/security' }, + ], + }, { label: 'Connections', items: [{ label: 'Nostr Wallet Connect' }, { label: 'Nostr' }] }, { label: 'Mints', diff --git a/src/pages/WelcomePage.vue b/src/pages/WelcomePage.vue index 1dac0a8..e071e2e 100644 --- a/src/pages/WelcomePage.vue +++ b/src/pages/WelcomePage.vue @@ -32,6 +32,7 @@ { label: 'Create new', value: 'create' }, { label: 'Restore seed', value: 'restore' }, { label: 'Backup file', value: 'backup' }, + { label: 'Nostr backup', value: 'nostr' }, ]" /> @@ -199,6 +200,107 @@ />
+ +
+
+ If this wallet used nostr backup before, your notes, mints and settings are + waiting on your relays - encrypted so only your recovery phrase can read + them. +
+ +
Relays to look on
+ + +
{{ nostrError }}
+ + + + +
+
@@ -269,13 +371,22 @@ +};