feat(ui): Words / QR code tabs on LND seed reveal + onboarding seed
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m54s

Both seed screens get the wallet-settings segmented tab style: words
stay the default first view; the QR tab renders the space-joined seed
words for wallets that support seed import by scan. The LND reveal QR
sits behind the same tap-to-reveal blur as the words, and both panes
warn that the code is equivalent to the words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-26 07:34:19 -04:00
parent 57c6a4d512
commit c8d0dda656
2 changed files with 93 additions and 15 deletions

View File

@ -44,7 +44,19 @@
<!-- Word Grid -->
<div v-if="words.length > 0" class="w-full max-w-[600px]">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-1 sm:gap-1.5">
<!-- Words / QR tabs words first; QR for wallets that import by scan -->
<div class="flex gap-1 mb-2 p-1 bg-white/5 rounded-lg">
<button
v-for="tab in ([{ key: 'words', label: 'Words' }, { key: 'qr', label: 'QR code' }] as const)"
:key="tab.key"
type="button"
@click="seedTab = tab.key"
class="flex-1 px-2 py-1.5 rounded text-xs font-medium transition-colors"
:class="seedTab === tab.key ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/80'"
>{{ tab.label }}</button>
</div>
<div v-if="seedTab === 'words'" class="grid grid-cols-2 sm:grid-cols-4 gap-1 sm:gap-1.5">
<div
v-for="(word, i) in words"
:key="i"
@ -55,6 +67,14 @@
</div>
</div>
<div v-else class="flex flex-col items-center gap-2 py-2">
<canvas ref="seedQrCanvas" class="rounded-lg bg-white p-2"></canvas>
<p class="text-xs text-white/50 text-center max-w-[420px]">
The code contains your seed words scan only into a wallet you trust,
and treat it exactly like the words themselves.
</p>
</div>
<!-- Warning -->
<div class="mt-3 bg-orange-500/10 border border-orange-500/20 rounded-lg px-3 py-2.5">
<p class="text-xs sm:text-sm text-orange-300/90">
@ -99,6 +119,19 @@ import { playNavSound } from '@/composables/useNavSounds'
const router = useRouter()
const continueButton = ref<HTMLButtonElement | null>(null)
const words = ref<string[]>([])
// Words / QR code view of the seed words are the default first view.
const seedTab = ref<'words' | 'qr'>('words')
const seedQrCanvas = ref<HTMLCanvasElement | null>(null)
watch(seedTab, async (tab) => {
if (tab !== 'qr') return
await nextTick()
if (!seedQrCanvas.value || words.value.length === 0) return
try {
const QRCode = await import('qrcode')
await QRCode.toCanvas(seedQrCanvas.value, words.value.join(' '), { width: 240, margin: 1 })
} catch { /* QR is a convenience — the words remain authoritative */ }
})
const confirmed = ref(false)
const loading = ref(false)
const waitingForServer = ref(false)

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, watch, nextTick, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
@ -65,9 +65,25 @@ function openReveal() {
revealError.value = ''
revealedWords.value = []
wordsHidden.value = true
seedTab.value = 'words' // words first QR is the opt-in second view
showRevealModal.value = true
}
// Words / QR code tabs (wallet-settings segmented style). The QR encodes the
// space-joined seed words for wallets that support seed import by scan.
const seedTab = ref<'words' | 'qr'>('words')
const seedQrCanvas = ref<HTMLCanvasElement | null>(null)
async function renderSeedQr() {
await nextTick()
if (!seedQrCanvas.value || revealedWords.value.length === 0) return
try {
const QRCode = await import('qrcode')
await QRCode.toCanvas(seedQrCanvas.value, revealedWords.value.join(' '), { width: 260, margin: 1 })
} catch { /* QR is a convenience — the words remain authoritative */ }
}
watch(seedTab, (t) => { if (t === 'qr') void renderSeedQr() })
async function submitReveal() {
if (revealing.value || !revealPassword.value) return
revealing.value = true
@ -166,20 +182,49 @@ async function confirmBackedUp() {
</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 class="flex gap-1 mb-3 p-1 bg-white/5 rounded-lg">
<button
v-for="tab in ([{ key: 'words', label: 'Words' }, { key: 'qr', label: 'QR code' }] as const)"
:key="tab.key"
type="button"
@click="seedTab = tab.key"
class="flex-1 px-2 py-1.5 rounded text-xs font-medium transition-colors"
:class="seedTab === tab.key ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/80'"
>{{ tab.label }}</button>
</div>
<template v-if="seedTab === 'words'">
<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>
</template>
<template v-else>
<p class="text-sm text-white/60 mb-3">Scan with a wallet that supports seed import by QR. Tap to {{ wordsHidden ? 'reveal' : 'hide' }}.</p>
<div class="relative">
<div
class="flex justify-center p-3 bg-white/5 rounded-lg transition-all"
:class="wordsHidden ? 'blur-md' : ''"
@click="wordsHidden = !wordsHidden"
>
<canvas ref="seedQrCanvas" class="rounded-lg bg-white p-2"></canvas>
</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>
<p class="text-xs text-white/40 mt-2">The code contains your seed words treat it exactly like the words themselves.</p>
</template>
<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" :disabled="acking" @click="confirmBackedUp" 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">