feat(machine): unified QR code with LNURL + CLINK payment options
Redesign cash-in QR display with cleaner UI inspired by payment gateways: - Unified QR encodes both lightning:LNURL and clink:ndebit on separate lines - Pill buttons to select Universal/LNURL/CLINK mode - Generate LNURL-withdraw immediately when entering QR state - QR code with lightning bolt overlay - Truncated URI with copy button - Amount display with sats and fiat Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d650647d8f
commit
30a2eb2199
1 changed files with 137 additions and 97 deletions
|
|
@ -22,15 +22,16 @@ const invoiceInput = ref('')
|
|||
// Copy state for ndebit URI
|
||||
const copied = ref(false)
|
||||
|
||||
// Tab state for QR display (ndebit vs LNURL-withdraw)
|
||||
const activeTab = ref<'ndebit' | 'lnurl'>('ndebit')
|
||||
// QR display mode: unified (default), lnurl-only, or clink-only
|
||||
const qrMode = ref<'unified' | 'lnurl' | 'clink'>('unified')
|
||||
const lnurlWithdraw = ref<string | null>(null)
|
||||
const isGeneratingLnurl = ref(false)
|
||||
|
||||
// Switch to LNURL tab and generate LNURL-withdraw if needed
|
||||
async function switchToLnurl() {
|
||||
activeTab.value = 'lnurl'
|
||||
if (!lnurlWithdraw.value && !isGeneratingLnurl.value) {
|
||||
// Generate LNURL-withdraw when entering displayingQR state
|
||||
watch(
|
||||
() => atmStore.nestedState,
|
||||
async (state) => {
|
||||
if (state === 'displayingQR' && !lnurlWithdraw.value && !isGeneratingLnurl.value) {
|
||||
isGeneratingLnurl.value = true
|
||||
try {
|
||||
lnurlWithdraw.value = await atmStore.generateLnurlWithdraw()
|
||||
|
|
@ -40,11 +41,41 @@ async function switchToLnurl() {
|
|||
isGeneratingLnurl.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Copy ndebit URI to clipboard
|
||||
async function copyNdebit() {
|
||||
const uri = atmStore.context?.ndebitUri
|
||||
// Compute the unified QR value (lightning + clink on separate lines)
|
||||
const unifiedQrValue = computed(() => {
|
||||
const ndebit = atmStore.context?.ndebitUri
|
||||
const lnurl = lnurlWithdraw.value
|
||||
|
||||
if (lnurl && ndebit) {
|
||||
// Unified: lightning scheme first (more universal), then clink
|
||||
return `lightning:${lnurl}\n${ndebit}`
|
||||
} else if (lnurl) {
|
||||
return `lightning:${lnurl}`
|
||||
} else if (ndebit) {
|
||||
return ndebit
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Get the appropriate QR value based on mode
|
||||
const currentQrValue = computed(() => {
|
||||
switch (qrMode.value) {
|
||||
case 'unified':
|
||||
return unifiedQrValue.value
|
||||
case 'lnurl':
|
||||
return lnurlWithdraw.value ? `lightning:${lnurlWithdraw.value}` : null
|
||||
case 'clink':
|
||||
return atmStore.context?.ndebitUri || null
|
||||
}
|
||||
})
|
||||
|
||||
// Copy current QR value to clipboard
|
||||
async function copyQrValue() {
|
||||
const uri = currentQrValue.value
|
||||
if (!uri) return
|
||||
|
||||
try {
|
||||
|
|
@ -223,93 +254,102 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Display QR (ndebit or LNURL-withdraw) -->
|
||||
<Card v-else-if="nestedState === 'displayingQR'" class="w-[480px] border-0 bg-white/5">
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle>Scan to Receive Sats</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
<Badge variant="secondary" class="text-lg">
|
||||
{{ formatSats(context.satsAmount) }} sats
|
||||
</Badge>
|
||||
</CardDescription>
|
||||
<!-- Tab buttons for switching between ndebit and LNURL -->
|
||||
<div class="flex gap-2 mt-3 justify-center">
|
||||
<!-- Display QR (unified lightning + clink) -->
|
||||
<Card v-else-if="nestedState === 'displayingQR'" class="w-[420px] border-0 bg-white/5">
|
||||
<CardContent class="pt-6 space-y-5">
|
||||
<!-- Amount Display -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Withdraw</p>
|
||||
<p class="text-3xl font-bold text-orange-400">
|
||||
{{ formatSats(context?.satsAmount || 0) }} sats
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
${{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Scan with selector -->
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-muted-foreground mb-2">Scan with</p>
|
||||
<div class="flex flex-wrap gap-2 justify-center">
|
||||
<Button
|
||||
:variant="activeTab === 'ndebit' ? 'default' : 'outline'"
|
||||
:variant="qrMode === 'unified' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="activeTab = 'ndebit'"
|
||||
class="rounded-full px-4"
|
||||
@click="qrMode = 'unified'"
|
||||
>
|
||||
CLINK (Shock Wallet)
|
||||
Universal
|
||||
</Button>
|
||||
<Button
|
||||
:variant="activeTab === 'lnurl' ? 'default' : 'outline'"
|
||||
:variant="qrMode === 'lnurl' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="switchToLnurl"
|
||||
class="rounded-full px-4"
|
||||
@click="qrMode = 'lnurl'"
|
||||
>
|
||||
LNURL (Any Wallet)
|
||||
LNURL
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- NDebit QR Code (shown when ndebit tab is active) -->
|
||||
<div v-if="activeTab === 'ndebit'" class="flex flex-col items-center">
|
||||
<QRCode v-if="context?.ndebitUri" :value="context.ndebitUri" :size="220" />
|
||||
<p class="mt-4 text-center text-sm text-muted-foreground">
|
||||
Scan with Shock Wallet or compatible CLINK wallet
|
||||
</p>
|
||||
<!-- Copyable NDebit URI -->
|
||||
<div v-if="context?.ndebitUri" class="mt-4 w-full space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Input :model-value="context.ndebitUri" readonly class="flex-1 font-mono text-xs" />
|
||||
<Button variant="outline" size="sm" @click="copyNdebit">
|
||||
{{ copied ? '✓ Copied' : 'Copy' }}
|
||||
<Button
|
||||
:variant="qrMode === 'clink' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="rounded-full px-4"
|
||||
@click="qrMode = 'clink'"
|
||||
>
|
||||
CLINK
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
Or copy and paste into your wallet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LNURL-withdraw QR Code (shown when lnurl tab is active) -->
|
||||
<div v-else-if="activeTab === 'lnurl'" class="flex flex-col items-center">
|
||||
<Skeleton v-if="isGeneratingLnurl" class="h-[220px] w-[220px]" />
|
||||
<QRCode v-else-if="lnurlWithdraw" :value="lnurlWithdraw" :size="220" />
|
||||
<!-- QR Code -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="relative">
|
||||
<Skeleton
|
||||
v-if="isGeneratingLnurl && !context?.ndebitUri"
|
||||
class="h-[240px] w-[240px] rounded-xl"
|
||||
/>
|
||||
<div v-else-if="currentQrValue" class="rounded-xl overflow-hidden bg-white p-3">
|
||||
<QRCode :value="currentQrValue" :size="216" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-[220px] w-[220px] items-center justify-center rounded-lg bg-white/10"
|
||||
class="flex h-[240px] w-[240px] items-center justify-center rounded-xl bg-white/10"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">Failed to generate LNURL</p>
|
||||
<p class="text-sm text-muted-foreground">Generating...</p>
|
||||
</div>
|
||||
<!-- Lightning bolt overlay -->
|
||||
<div
|
||||
v-if="currentQrValue"
|
||||
class="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 rounded-full bg-orange-500 flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<span class="text-2xl">⚡</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 text-center text-sm text-muted-foreground">
|
||||
Scan with any Lightning wallet (Phoenix, Zeus, Wallet of Satoshi, etc.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Instructions (different based on active tab) -->
|
||||
<Alert class="border-green-500/50 bg-green-500/10">
|
||||
<AlertDescription>
|
||||
<p class="mb-2 text-sm font-medium text-green-400">How it works:</p>
|
||||
<ol v-if="activeTab === 'ndebit'" class="space-y-1 text-xs text-muted-foreground">
|
||||
<li>1. Scan the QR code with your CLINK-compatible wallet</li>
|
||||
<li>
|
||||
2. Your wallet creates an invoice for
|
||||
{{ formatSats(context?.satsAmount || 0) }} sats
|
||||
</li>
|
||||
<li>3. Confirm the claim in your wallet</li>
|
||||
<li>4. Receive your sats instantly!</li>
|
||||
</ol>
|
||||
<ol v-else class="space-y-1 text-xs text-muted-foreground">
|
||||
<li>1. Scan the QR code with any Lightning wallet</li>
|
||||
<li>
|
||||
2. Your wallet shows a withdrawal for
|
||||
{{ formatSats(context?.satsAmount || 0) }} sats
|
||||
</li>
|
||||
<li>3. Confirm the withdrawal in your wallet</li>
|
||||
<li>4. Sats arrive in your wallet!</li>
|
||||
</ol>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<!-- Mode label + truncated URI -->
|
||||
<div class="text-center space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
{{
|
||||
qrMode === 'unified'
|
||||
? 'Universal'
|
||||
: qrMode === 'lnurl'
|
||||
? 'LNURL-withdraw'
|
||||
: 'CLINK ndebit'
|
||||
}}
|
||||
</p>
|
||||
<div v-if="currentQrValue" class="flex items-center justify-center gap-2">
|
||||
<code class="text-xs text-muted-foreground truncate max-w-[280px]">
|
||||
{{ currentQrValue.split('\n')[0].slice(0, 32) }}...
|
||||
</code>
|
||||
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" @click="copyQrValue">
|
||||
<span v-if="copied">✓</span>
|
||||
<span v-else class="text-muted-foreground">📋</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alternative: Manual Options (collapsed) -->
|
||||
<details class="rounded-lg bg-white/5 p-3">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue