feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes
- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card - Buy a peer's paid file (ecash / node Lightning / on-chain / external QR) - Recovery-phrase reveal + backup section; onboarding seed retry resilience - NetBird HTTPS launch, remote-control two-finger scroll + external-open - Shared BackButton, single-v version label, mesh Bitcoin header toggles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bd567cd165
commit
87769cbfbf
@@ -126,6 +126,61 @@ async function deleteBackup(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery phrase reveal — re-auth gated (password + 2FA when enabled).
|
||||
const showRevealModal = ref(false)
|
||||
const revealPassword = ref('')
|
||||
const revealCode = ref('')
|
||||
const revealPassphrase = ref('')
|
||||
const revealing = ref(false)
|
||||
const revealError = ref('')
|
||||
const revealedWords = ref<string[]>([])
|
||||
const wordsHidden = ref(true)
|
||||
const wordsCopied = ref(false)
|
||||
|
||||
function openReveal() {
|
||||
revealPassword.value = ''
|
||||
revealCode.value = ''
|
||||
revealPassphrase.value = ''
|
||||
revealError.value = ''
|
||||
revealedWords.value = []
|
||||
wordsHidden.value = true
|
||||
showRevealModal.value = true
|
||||
}
|
||||
|
||||
async function submitReveal() {
|
||||
if (revealing.value || !revealPassword.value) return
|
||||
revealing.value = true
|
||||
revealError.value = ''
|
||||
try {
|
||||
const params: Record<string, string> = { password: revealPassword.value }
|
||||
if (revealCode.value) params.code = revealCode.value
|
||||
if (revealPassphrase.value) params.passphrase = revealPassphrase.value
|
||||
const res = await rpcClient.call<{ words: string[] }>({ method: 'seed.reveal', params })
|
||||
revealedWords.value = res.words || []
|
||||
wordsHidden.value = true
|
||||
} catch (e: unknown) {
|
||||
revealError.value = e instanceof Error ? e.message : t('settings.revealSeedFailed')
|
||||
} finally {
|
||||
revealing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeReveal() {
|
||||
showRevealModal.value = false
|
||||
revealedWords.value = []
|
||||
revealPassword.value = ''
|
||||
revealCode.value = ''
|
||||
revealPassphrase.value = ''
|
||||
}
|
||||
|
||||
async function copyRevealedWords() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(revealedWords.value.join(' '))
|
||||
wordsCopied.value = true
|
||||
setTimeout(() => { wordsCopied.value = false }, 2000)
|
||||
} catch { /* clipboard unavailable */ }
|
||||
}
|
||||
|
||||
// USB Drive Backup
|
||||
interface UsbDriveInfo {
|
||||
device: string
|
||||
@@ -198,6 +253,80 @@ defineExpose({ loadBackups })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Recovery phrase Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-xl font-semibold text-white/96 mb-1">Recovery phrase</h2>
|
||||
<p class="text-sm text-white/60">
|
||||
View this node's 24-word recovery phrase. You'll need to confirm your password
|
||||
(and 2FA code, if enabled). Only reveal it somewhere private — anyone with these
|
||||
words controls this node.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
@click="openReveal"
|
||||
>Reveal</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reveal recovery phrase modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showRevealModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="closeReveal">
|
||||
<div class="glass-card p-6 w-full max-w-md" role="dialog" aria-modal="true" aria-labelledby="reveal-seed-title">
|
||||
<h3 id="reveal-seed-title" class="text-lg font-semibold text-white mb-1">Reveal recovery phrase</h3>
|
||||
|
||||
<template v-if="revealedWords.length === 0">
|
||||
<p class="text-sm text-white/60 mb-4">Confirm your credentials to display the 24-word phrase.</p>
|
||||
<form @submit.prevent="submitReveal" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Password</label>
|
||||
<input v-model="revealPassword" type="password" autocomplete="current-password" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:border-white/30" placeholder="Your login password" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">2FA code <span class="text-white/30">(if enabled)</span></label>
|
||||
<input v-model="revealCode" inputmode="numeric" autocomplete="one-time-code" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm font-mono tracking-widest focus:outline-none focus:border-white/30" placeholder="123456" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Backup passphrase <span class="text-white/30">(only if different from password)</span></label>
|
||||
<input v-model="revealPassphrase" type="password" class="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:border-white/30" placeholder="Leave blank to use password" />
|
||||
</div>
|
||||
<p v-if="revealError" class="text-xs text-red-300 bg-red-500/10 border border-red-400/20 rounded-lg px-3 py-2">{{ revealError }}</p>
|
||||
<div class="flex gap-2 pt-1">
|
||||
<button type="button" @click="closeReveal" class="flex-1 glass-button rounded-lg px-4 py-2 text-sm font-medium">Cancel</button>
|
||||
<button type="submit" :disabled="revealing || !revealPassword" class="flex-1 glass-button rounded-lg px-4 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 disabled:opacity-50">
|
||||
{{ revealing ? 'Verifying…' : 'Reveal' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p class="text-sm text-white/60 mb-3">Write these down and store them offline. Tap to {{ wordsHidden ? 'reveal' : 'hide' }}.</p>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 gap-2 p-3 bg-white/5 rounded-lg transition-all select-text"
|
||||
:class="wordsHidden ? 'blur-md' : ''"
|
||||
@click="wordsHidden = !wordsHidden"
|
||||
>
|
||||
<div v-for="(w, i) in revealedWords" :key="i" class="flex items-center gap-1.5 text-sm">
|
||||
<span class="text-white/30 text-xs w-5 text-right">{{ i + 1 }}.</span>
|
||||
<span class="text-white font-mono">{{ w }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="wordsHidden" type="button" class="absolute inset-0 flex items-center justify-center text-xs text-white/70 font-medium" @click="wordsHidden = false">Tap to reveal</button>
|
||||
</div>
|
||||
<div class="flex gap-2 pt-4">
|
||||
<button type="button" @click="copyRevealedWords" class="flex-1 glass-button rounded-lg px-4 py-2 text-sm font-medium">{{ wordsCopied ? 'Copied!' : 'Copy' }}</button>
|
||||
<button type="button" @click="closeReveal" class="flex-1 glass-button rounded-lg px-4 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30">Done</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Backup & Restore Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user