feat(machine): add Zeus and Lightning.Pub QR codes to mock UI

dev.sh atm auto-injects VITE_LNDCONNECT_URL from lnd-3 credentials.
Idle screen shows "Zeus QR" and "Lightning.Pub nprofile" buttons (debug
mode) that open fullscreen overlays with scannable 400px QR codes and
copy-to-clipboard support. Also makes the debug overlay collapsible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-17 12:11:55 -05:00
commit 60f4eb6639
6 changed files with 122 additions and 496 deletions

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { useAtmStore } from '@/stores/atm'
import { useTheme } from '@/composables/useTheme'
import { Badge } from '@/components/ui/badge'
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
const atmStore = useAtmStore()
const { current: currentTheme, themes } = useTheme()
const debugExpanded = ref(false)
function formatSats(sats: number): string {
return sats.toLocaleString()
@ -63,9 +64,17 @@ function toggleLiveServices() {
<!-- Debug overlay (dev only) -->
<div
v-if="atmStore.debugMode"
class="fixed inset-x-0 bottom-0 z-50 max-h-52 overflow-auto bg-popover p-4 font-mono text-xs text-popover-foreground"
class="fixed inset-x-0 bottom-0 z-50 bg-popover font-mono text-xs text-popover-foreground"
>
<div class="mb-2 flex items-center gap-4">
<div class="flex items-center gap-4 px-4 py-2">
<Button
variant="ghost"
size="sm"
class="h-5 px-1 text-xs text-muted-foreground"
@click="debugExpanded = !debugExpanded"
>
{{ debugExpanded ? '▼' : '▲' }}
</Button>
<span class="text-success">State: {{ atmStore.currentState }}</span>
<Button variant="outline" size="sm" class="h-6 text-xs" @click="toggleLiveServices">
{{ atmStore.useLiveServices ? 'Switch to Mock' : 'Connect Live' }}
@ -84,7 +93,9 @@ function toggleLiveServices() {
</Button>
</div>
</div>
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
<div v-if="debugExpanded" class="max-h-48 overflow-auto px-4 pb-4">
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
</div>
</div>
</div>
</template>

View file

@ -1,16 +1,41 @@
<script setup lang="ts">
import { watch } from 'vue'
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { nip19 } from 'nostr-tools'
import { useAtmStore } from '@/stores/atm'
import { useTheme } from '@/composables/useTheme'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import BitcoinIcon from '@/components/BitcoinIcon.vue'
import QRCode from '@/components/QRCode.vue'
const router = useRouter()
const atmStore = useAtmStore()
const { current: currentTheme, themes } = useTheme()
const lndconnectUrl = import.meta.env.VITE_LNDCONNECT_URL || ''
const showZeusQR = ref(false)
const showLpQR = ref(false)
const copied = ref(false)
// Build nprofile for Lightning.Pub (pubkey + relay hint)
const lpNprofile = computed(() => {
const pubkey = import.meta.env.VITE_LIGHTNING_PUB_PUBKEY
if (!pubkey) return ''
const relayUrl = import.meta.env.VITE_RELAY_URL
const relays = relayUrl ? [relayUrl.replace('ws://', 'wss://')] : []
return nip19.nprofileEncode({ pubkey, relays })
})
async function copyToClipboard(value: string) {
try {
await navigator.clipboard.writeText(value)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch {
// clipboard unavailable
}
}
// Watch for state changes and navigate accordingly
watch(
@ -79,19 +104,72 @@ function handleCashOut() {
<Badge variant="outline" class="border-bitcoin text-bitcoin">Just Bitcoin</Badge>
</div>
<!-- Theme switcher -->
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">Theme:</span>
<!-- Dev QR buttons -->
<div v-if="atmStore.debugMode" class="flex items-center gap-4">
<Button
v-for="theme in themes"
:key="theme.id"
:variant="currentTheme === theme.id ? 'default' : 'outline'"
v-if="lndconnectUrl"
variant="ghost"
size="sm"
class="h-7 text-xs"
@click="currentTheme = theme.id"
class="text-xs text-muted-foreground"
@click="showZeusQR = true"
>
{{ theme.label }}
Zeus QR (lnd-3)
</Button>
<Button
v-if="lpNprofile"
variant="ghost"
size="sm"
class="text-xs text-muted-foreground"
@click="showLpQR = true"
>
Lightning.Pub nprofile
</Button>
</div>
<!-- Zeus QR fullscreen overlay -->
<div
v-if="showZeusQR"
class="fixed inset-0 z-[100] flex flex-col items-center justify-center gap-4 bg-black/90"
@click.self="showZeusQR = false"
>
<p class="text-sm text-white/70">Scan with Zeus to connect to lnd-3</p>
<div class="rounded-2xl bg-white p-4">
<QRCode :value="lndconnectUrl" :size="400" />
</div>
<div class="flex items-center gap-2">
<Button
variant="outline"
size="sm"
class="text-white"
@click="copyToClipboard(lndconnectUrl)"
>
{{ copied ? 'Copied!' : 'Copy' }}
</Button>
<Button variant="outline" size="sm" class="text-white" @click="showZeusQR = false">
Close
</Button>
</div>
</div>
<!-- Lightning.Pub nprofile QR fullscreen overlay -->
<div
v-if="showLpQR"
class="fixed inset-0 z-[100] flex flex-col items-center justify-center gap-4 bg-black/90"
@click.self="showLpQR = false"
>
<p class="text-sm text-white/70">Lightning.Pub nprofile</p>
<div class="rounded-2xl bg-white p-4">
<QRCode :value="lpNprofile" :size="400" />
</div>
<code class="max-w-md truncate text-xs text-white/50">{{ lpNprofile }}</code>
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" class="text-white" @click="copyToClipboard(lpNprofile)">
{{ copied ? 'Copied!' : 'Copy' }}
</Button>
<Button variant="outline" size="sm" class="text-white" @click="showLpQR = false">
Close
</Button>
</div>
</div>
<!-- Debug toggle -->