feat(ui): Words / QR code tabs on LND seed reveal + onboarding seed
Demo images / Build & push demo images (push) Successful in 2m54s
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:
co-authored by
Claude Fable 5
parent
57c6a4d512
commit
c8d0dda656
@@ -44,7 +44,19 @@
|
|||||||
|
|
||||||
<!-- Word Grid -->
|
<!-- Word Grid -->
|
||||||
<div v-if="words.length > 0" class="w-full max-w-[600px]">
|
<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
|
<div
|
||||||
v-for="(word, i) in words"
|
v-for="(word, i) in words"
|
||||||
:key="i"
|
:key="i"
|
||||||
@@ -55,6 +67,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Warning -->
|
||||||
<div class="mt-3 bg-orange-500/10 border border-orange-500/20 rounded-lg px-3 py-2.5">
|
<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">
|
<p class="text-xs sm:text-sm text-orange-300/90">
|
||||||
@@ -99,6 +119,19 @@ import { playNavSound } from '@/composables/useNavSounds'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const continueButton = ref<HTMLButtonElement | null>(null)
|
const continueButton = ref<HTMLButtonElement | null>(null)
|
||||||
const words = ref<string[]>([])
|
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 confirmed = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const waitingForServer = ref(false)
|
const waitingForServer = ref(false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
|
||||||
@@ -65,9 +65,25 @@ function openReveal() {
|
|||||||
revealError.value = ''
|
revealError.value = ''
|
||||||
revealedWords.value = []
|
revealedWords.value = []
|
||||||
wordsHidden.value = true
|
wordsHidden.value = true
|
||||||
|
seedTab.value = 'words' // words first — QR is the opt-in second view
|
||||||
showRevealModal.value = true
|
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() {
|
async function submitReveal() {
|
||||||
if (revealing.value || !revealPassword.value) return
|
if (revealing.value || !revealPassword.value) return
|
||||||
revealing.value = true
|
revealing.value = true
|
||||||
@@ -166,6 +182,18 @@ async function confirmBackedUp() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<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>
|
<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="relative">
|
||||||
<div
|
<div
|
||||||
@@ -180,6 +208,23 @@ async function confirmBackedUp() {
|
|||||||
</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>
|
<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>
|
||||||
|
</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">
|
<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="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">
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user