feat(machine): redesign cash-in with split-screen layout

Split-screen for insertingBills (amounts left, bill visual right) and
displayingQR (info left, QR right). Add StepIndicator with success
flow accent header. Replace Skeleton with BounceDots, remove ScrollArea
and Card wrappers, wrap states in Transition for fade animations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-01 12:17:28 -05:00
commit d3e652bce3

View file

@ -3,18 +3,32 @@ import { watch, computed, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAtmStore } from '@/stores/atm' import { useAtmStore } from '@/stores/atm'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import QRCode from '@/components/QRCode.vue' import QRCode from '@/components/QRCode.vue'
import BounceDots from '@/components/BounceDots.vue'
import StepIndicator from '@/components/StepIndicator.vue'
const router = useRouter() const router = useRouter()
const atmStore = useAtmStore() const atmStore = useAtmStore()
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
const cashInSteps = ['Insert', 'Scan', 'Done']
const currentStepIndex = computed(() => {
switch (nestedState.value) {
case 'fetchingRate':
case 'insertingBills':
return 0
case 'generatingNdebit':
case 'displayingQR':
return 1
default:
return 2
}
})
// Whether the header cancel button should be visible // Whether the header cancel button should be visible
// State machine routes CANCEL to confirmAbandon when bills are present // State machine routes CANCEL to confirmAbandon when bills are present
const showCancelButton = computed(() => { const showCancelButton = computed(() => {
@ -158,13 +172,8 @@ function simulateBill(denomination: number) {
// Convert npub to hex pubkey if needed // Convert npub to hex pubkey if needed
function normalizePubkey(input: string): string { function normalizePubkey(input: string): string {
const trimmed = input.trim() const trimmed = input.trim()
// If it's an npub, we'd need to decode it - for now just pass through
// In production, use nostr-tools nip19.decode()
if (trimmed.startsWith('npub1')) { if (trimmed.startsWith('npub1')) {
// Simple check - in production decode properly
console.log('[CashIn] npub detected, would decode in production') console.log('[CashIn] npub detected, would decode in production')
// For now, just return as-is and let the CLINK client handle it
// TODO: Properly decode npub using nostr-tools
} }
return trimmed return trimmed
} }
@ -206,76 +215,80 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<template> <template>
<div class="flex min-h-0 flex-1 flex-col"> <div class="flex min-h-0 flex-1 flex-col">
<!-- Header --> <!-- Header with success flow accent -->
<header class="flex items-center bg-foreground/10 px-4 pb-3 pt-12 sm:px-8 sm:py-4"> <header
<Button v-if="showCancelButton" variant="ghost" class="text-muted-foreground" @click="cancel"> class="flex items-center px-8 py-4"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<Button
v-if="showCancelButton"
variant="ghost"
size="kiosk"
class="text-muted-foreground"
@click="cancel"
>
← Cancel ← Cancel
</Button> </Button>
<div v-else class="w-20"></div> <div v-else class="w-20"></div>
<h2 class="flex-1 text-center text-lg font-semibold sm:text-xl">Buy Bitcoin</h2> <div class="flex flex-1 flex-col items-center gap-2">
<h2 class="text-[2.5rem] font-semibold">Buy Bitcoin</h2>
<StepIndicator :steps="cashInSteps" :current-step="currentStepIndex" />
</div>
<div class="w-20"></div> <div class="w-20"></div>
</header> </header>
<!-- Content --> <!-- Content area -->
<ScrollArea class="min-h-0 flex-1"> <main class="flex min-h-0 flex-1">
<main class="flex min-h-0 flex-1 items-start justify-center p-4 sm:items-center sm:p-8"> <Transition name="state-fade" mode="out-in">
<!-- Fetching Rate --> <!-- Fetching Rate (centered) -->
<Card v-if="nestedState === 'fetchingRate'" class="w-full max-w-80 border-0 bg-card"> <div
<CardContent class="flex flex-col items-center gap-4 p-8"> v-if="nestedState === 'fetchingRate'"
<Skeleton class="h-10 w-10 rounded-full" /> key="fetchingRate"
<p class="text-muted-foreground">Fetching exchange rate...</p> class="flex flex-1 flex-col items-center justify-center gap-6"
</CardContent> >
</Card> <BounceDots class="scale-150" />
<p class="text-2xl text-muted-foreground">Fetching exchange rate...</p>
</div>
<!-- Inserting Bills --> <!-- Inserting Bills (split-screen) -->
<Card v-else-if="nestedState === 'insertingBills'" class="w-full max-w-96 border-0 bg-card"> <div v-else-if="nestedState === 'insertingBills'" key="insertingBills" class="flex flex-1">
<CardHeader class="text-center"> <!-- Left panel (52%) — amounts and controls -->
<CardTitle>Insert Cash</CardTitle> <div class="flex w-[52%] flex-col justify-center gap-6 px-[4vw] py-6">
<CardDescription v-if="context"> <!-- Amount display -->
<div v-if="context">
<p class="text-[6vh] font-bold text-success leading-tight">
{{ atmStore.fiatSymbol }}{{ (context.fiatAmount / 100).toFixed(2) }}
</p>
<p class="text-[3.5vh] font-semibold text-bitcoin">
{{ formatSats(context.satsAmount) }} sats
</p>
</div>
<!-- Rate -->
<p v-if="context" class="text-xl text-muted-foreground">
Rate: {{ formatSats(context.exchangeRate) }} sats/{{ atmStore.fiatSymbol }} Rate: {{ formatSats(context.exchangeRate) }} sats/{{ atmStore.fiatSymbol }}
</CardDescription> </p>
</CardHeader>
<CardContent class="space-y-6">
<!-- Bill Status -->
<div v-if="context" class="rounded-lg bg-card p-4">
<div class="mb-2 flex justify-between">
<span class="text-muted-foreground">Inserted:</span>
<span class="text-2xl font-bold text-success"
>{{ atmStore.fiatSymbol }}{{ context.fiatAmount / 100 }}</span
>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">You'll receive:</span>
<span class="font-semibold text-bitcoin"
>{{ formatSats(context.satsAmount) }} sats</span
>
</div>
</div>
<!-- Available balance indicator --> <!-- Available balance -->
<div <p
v-if="context && context.availableBalance > 0" v-if="context && context.availableBalance > 0"
class="flex justify-between text-sm" class="text-xl"
:class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'"
> >
<span class="text-muted-foreground">Available:</span> Available: {{ formatSats(context.availableBalance) }} sats
<span :class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'"> </p>
{{ formatSats(context.availableBalance) }} sats
</span>
</div>
<!-- Bill Acceptor Status --> <!-- Status -->
<p v-if="balanceLimitReached" class="text-center text-sm text-muted-foreground"> <p v-if="balanceLimitReached" class="text-xl text-muted-foreground">
Maximum amount reached — press Done to continue Maximum amount reached — press Done to continue
</p> </p>
<p v-else class="text-center text-sm text-muted-foreground"> <p v-else class="text-xl text-muted-foreground">💵 Insert bills into the acceptor</p>
💵 Insert bills into the acceptor
</p>
<!-- Dev: Simulate bill insertion --> <!-- Dev: Simulate bill insertion -->
<Alert v-if="atmStore.debugMode" class="border-destructive/50 bg-destructive/10"> <Alert v-if="atmStore.debugMode" class="border-destructive/50 bg-destructive/10">
<AlertDescription> <AlertDescription>
<p class="mb-2 text-xs text-destructive">Dev: Simulate bills</p> <p class="mb-2 text-xs text-destructive">Dev: Simulate bills</p>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap gap-2">
<Button <Button
v-for="denom in [1, 5, 10, 20, 50, 100]" v-for="denom in [1, 5, 10, 20, 50, 100]"
:key="denom" :key="denom"
@ -291,47 +304,60 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<!-- Done button -->
<Button <Button
class="w-full bg-gradient-to-r from-orange-500 to-yellow-400 text-black hover:from-orange-600 hover:to-yellow-500" class="w-full bg-gradient-to-r from-orange-500 to-yellow-400 text-black hover:from-orange-600 hover:to-yellow-500"
size="lg" size="kiosk-lg"
:disabled="!context || context.billsInserted.length === 0" :disabled="!context || context.billsInserted.length === 0"
@click="finishInserting" @click="finishInserting"
> >
Done Inserting Done Inserting
</Button> </Button>
</CardContent> </div>
</Card>
<!-- Generating NDebit --> <!-- Right panel (48%) — bill visual -->
<Card <div class="flex w-[48%] flex-col items-center justify-center gap-6 bg-foreground/5">
<span class="text-[14vh] leading-none">💵</span>
<p class="text-2xl text-muted-foreground">Feed bills here</p>
<p v-if="context" class="text-lg text-muted-foreground">
{{ context.billsInserted.length }} bill{{
context.billsInserted.length !== 1 ? 's' : ''
}}
inserted
</p>
</div>
</div>
<!-- Generating NDebit (centered) -->
<div
v-else-if="nestedState === 'generatingNdebit'" v-else-if="nestedState === 'generatingNdebit'"
class="w-full max-w-80 border-0 bg-card" key="generatingNdebit"
class="flex flex-1 flex-col items-center justify-center gap-6"
> >
<CardContent class="flex flex-col items-center gap-4 p-8"> <BounceDots class="scale-150" />
<span class="animate-hourglass text-5xl">⏳</span> <p class="text-2xl text-muted-foreground">Preparing your withdraw code...</p>
<p class="text-muted-foreground">Preparing your withdraw code...</p> </div>
</CardContent>
</Card>
<!-- Display QR (unified lightning + clink) --> <!-- Display QR (split-screen) -->
<Card <div v-else-if="nestedState === 'displayingQR'" key="displayingQR" class="flex flex-1">
v-else-if="nestedState === 'displayingQR'" <!-- Left panel (52%) — info -->
class="w-full max-w-[420px] border-0 bg-card" <div class="flex w-[52%] flex-col items-center justify-center gap-5 px-[4vw]">
> <p class="text-[2rem] font-semibold text-success">Scan to receive your sats</p>
<CardContent class="pt-6 space-y-5"> <p class="text-[7vh] font-bold text-bitcoin leading-tight">
<!-- Amount Display --> {{ formatSats(context?.satsAmount || 0) }} sats
<div class="text-center"> </p>
<p class="text-lg font-semibold text-success">Scan to receive your sats</p> <p class="text-2xl text-muted-foreground">
<p class="text-3xl font-bold text-bitcoin"> {{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
{{ formatSats(context?.satsAmount || 0) }} sats </p>
</p>
<p class="text-sm text-muted-foreground"> <!-- Waiting indicator -->
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }} <div class="flex items-center gap-3 pt-4">
</p> <BounceDots />
<p class="text-xl text-muted-foreground">Waiting for wallet scan...</p>
</div> </div>
<!-- Scan with selector (web-ui only — kiosk users just scan) --> <!-- Mode selector (web-ui only) -->
<div v-if="!isElectron" class="text-center"> <div v-if="!isElectron" class="pt-4 text-center">
<p class="text-xs text-muted-foreground mb-2">Scan with</p> <p class="text-xs text-muted-foreground mb-2">Scan with</p>
<div class="flex flex-wrap gap-2 justify-center"> <div class="flex flex-wrap gap-2 justify-center">
<Button <Button
@ -361,42 +387,6 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div> </div>
</div> </div>
<!-- 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">
<QRCode :value="currentQrValue" :size="216" />
</div>
<div
v-else
class="flex h-[240px] w-[240px] items-center justify-center rounded-xl bg-muted"
>
<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-bitcoin flex items-center justify-center shadow-lg"
>
<span class="text-2xl">⚡</span>
</div>
</div>
</div>
</div>
<!-- Waiting for withdraw indicator -->
<div v-if="currentQrValue" class="flex items-center justify-center gap-2">
<span class="animate-pulse text-lg">📲</span>
<p class="text-sm text-muted-foreground">Open your wallet and scan to claim</p>
</div>
<!-- Mode label + truncated URI (web-ui only) --> <!-- Mode label + truncated URI (web-ui only) -->
<div v-if="!isElectron" class="text-center space-y-1"> <div v-if="!isElectron" class="text-center space-y-1">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <p class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
@ -409,7 +399,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
}} }}
</p> </p>
<div v-if="currentQrValue" class="flex items-center justify-center gap-2"> <div v-if="currentQrValue" class="flex items-center justify-center gap-2">
<code class="text-xs text-muted-foreground truncate max-w-[280px]"> <code class="font-mono-code text-xs text-muted-foreground truncate max-w-[280px]">
{{ currentQrValue.split('\n')[0]?.slice(0, 32) }}... {{ currentQrValue.split('\n')[0]?.slice(0, 32) }}...
</code> </code>
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" @click="copyQrValue"> <Button variant="ghost" size="sm" class="h-6 w-6 p-0" @click="copyQrValue">
@ -419,62 +409,8 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div> </div>
</div> </div>
<!-- Alternative: Manual Options (web-ui only, no keyboard on kiosk) -->
<details v-if="!isElectron" class="rounded-lg bg-card p-3">
<summary class="cursor-pointer text-sm text-muted-foreground">
Alternative: Manual withdraw options
</summary>
<div class="mt-4 space-y-4">
<!-- CLINK Debit by pubkey -->
<div class="space-y-2">
<p class="text-xs font-medium text-accent">Enter your npub/pubkey:</p>
<div class="flex gap-2">
<Input
v-model="pubkeyInput"
placeholder="npub1... or hex pubkey"
class="flex-1 font-mono text-xs"
:disabled="isProcessing"
/>
<Button
variant="outline"
size="sm"
:disabled="!pubkeyInput.trim() || isProcessing"
@click="submitDebitRequest"
>
{{ atmStore.isRequestingDebit ? '...' : 'Send' }}
</Button>
</div>
</div>
<!-- Invoice paste -->
<div class="space-y-2">
<p class="text-xs font-medium text-bitcoin">Or paste your invoice:</p>
<div class="flex gap-2">
<Input
v-model="invoiceInput"
placeholder="lnbc... or lntb..."
class="flex-1 font-mono text-xs"
:disabled="isProcessing"
/>
<Button
variant="outline"
size="sm"
:disabled="!invoiceInput.trim() || isProcessing"
@click="submitInvoice"
>
{{ atmStore.isPayingInvoice ? '...' : 'Pay' }}
</Button>
</div>
</div>
<p v-if="atmStore.paymentError" class="text-xs text-destructive">
Error: {{ atmStore.paymentError }}
</p>
</div>
</details>
<!-- Debug: Simulate payment button --> <!-- Debug: Simulate payment button -->
<div v-if="atmStore.debugMode" class="pt-2 text-center"> <div v-if="atmStore.debugMode" class="pt-2">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -484,100 +420,186 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
Dev: Skip to Success Dev: Skip to Success
</Button> </Button>
</div> </div>
</CardContent> </div>
</Card>
<!-- Ask for Receipt --> <!-- Right panel (48%) — QR code -->
<Card v-else-if="nestedState === 'askForReceipt'" class="w-full max-w-80 border-0 bg-card"> <div class="flex w-[48%] flex-col items-center justify-center bg-foreground/5 p-6">
<CardContent class="flex flex-col items-center gap-4 p-8"> <div v-if="isGeneratingLnurl && !context?.ndebitUri">
<div class="text-6xl text-success">✅</div> <BounceDots class="scale-150" />
<CardTitle>Sats Sent!</CardTitle>
<p class="text-muted-foreground">Would you like a receipt?</p>
<div class="flex h-32 w-32 items-center justify-center rounded-lg bg-qr">
<p class="text-qr-foreground">📱 Scan npub</p>
</div> </div>
<Button variant="outline" @click="skipReceipt">No Thanks</Button> <div v-else-if="currentQrValue">
</CardContent> <QRCode :value="currentQrValue" :size="380" />
</Card> </div>
<div v-else class="flex flex-col items-center gap-4">
<BounceDots class="scale-150" />
<p class="text-xl text-muted-foreground">Generating...</p>
</div>
</div>
</div>
<!-- Sending Receipt --> <!-- Confirm Abandon (centered, destructive accent) -->
<Card v-else-if="nestedState === 'sendingReceipt'" class="w-full max-w-80 border-0 bg-card"> <div
<CardContent class="flex flex-col items-center gap-4 p-8">
<span class="animate-hourglass text-5xl">⏳</span>
<p class="text-muted-foreground">Sending receipt...</p>
</CardContent>
</Card>
<!-- Complete -->
<Card v-else-if="nestedState === 'complete'" class="w-full max-w-80 border-0 bg-success/20">
<CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">🎉</div>
<CardTitle class="text-success">Transaction Complete!</CardTitle>
<p v-if="context">
You received
<Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge>
</p>
<p class="text-muted-foreground">Thank you!</p>
</CardContent>
</Card>
<!-- Confirm Abandon Warning -->
<Card
v-else-if="nestedState === 'confirmAbandon'" v-else-if="nestedState === 'confirmAbandon'"
class="w-full max-w-80 border-0 bg-destructive/20" key="confirmAbandon"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
style="background: color-mix(in srgb, var(--destructive) 8%, var(--background))"
> >
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <div class="text-[8vh]">⚠️</div>
<div class="text-6xl">⚠️</div> <h3 class="text-[3rem] font-bold text-destructive">Cash Already Accepted</h3>
<CardTitle class="text-destructive">Cash Already Accepted</CardTitle> <p class="max-w-lg text-center text-2xl text-muted-foreground">
<p class="text-muted-foreground"> Your {{ atmStore.fiatSymbol
Your {{ atmStore.fiatSymbol }}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted and
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted cannot be returned. Please scan the QR code to receive your Bitcoin.
and cannot be returned. Please scan the QR code to receive your Bitcoin. </p>
</p> <Button
<Button class="w-full max-w-md bg-success text-success-foreground"
class="w-full bg-success text-success-foreground" size="kiosk-lg"
@click="atmStore.send({ type: 'RETRY' })" @click="atmStore.send({ type: 'RETRY' })"
> >
Show QR Code Again Show QR Code Again
</Button> </Button>
<Button variant="ghost" class="w-full text-destructive" @click="cancel"> <Button
Abandon (lose cash) variant="ghost"
</Button> size="kiosk"
</CardContent> class="w-full max-w-md text-destructive"
</Card> @click="cancel"
>
Abandon (lose cash)
</Button>
</div>
<!-- Error --> <!-- Ask for Receipt (centered) -->
<Card <div
v-else-if="nestedState === 'error'" v-else-if="nestedState === 'askForReceipt'"
class="w-full max-w-80 border-0 bg-destructive/20" key="askForReceipt"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
> >
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <div class="text-[8vh] text-success">✅</div>
<div class="text-6xl">⚠️</div> <h3 class="text-[3rem] font-bold">Sats Sent!</h3>
<CardTitle class="text-destructive">Error</CardTitle> <p class="text-2xl text-muted-foreground">Would you like a receipt?</p>
<p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p> <div class="flex h-32 w-32 items-center justify-center rounded-lg bg-qr">
<Button <p class="text-qr-foreground">📱 Scan npub</p>
v-if="context && context.billsInserted.length > 0" </div>
class="w-full bg-success text-success-foreground" <Button variant="outline" size="kiosk" @click="skipReceipt">No Thanks</Button>
@click="atmStore.send({ type: 'RETRY' })" </div>
<!-- Sending Receipt (centered) -->
<div
v-else-if="nestedState === 'sendingReceipt'"
key="sendingReceipt"
class="flex flex-1 flex-col items-center justify-center gap-6"
>
<BounceDots class="scale-150" />
<p class="text-2xl text-muted-foreground">Sending receipt...</p>
</div>
<!-- Complete (centered, success accent) -->
<div
v-else-if="nestedState === 'complete'"
key="complete"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<div class="text-[8vh]">🎉</div>
<h3 class="text-[3rem] font-bold text-success">Transaction Complete!</h3>
<p v-if="context" class="text-2xl">
You received
<Badge variant="secondary" class="text-2xl"
>{{ formatSats(context.satsAmount) }} sats</Badge
> >
Try Again </p>
</Button> <p class="text-2xl text-muted-foreground">Thank you!</p>
<Button </div>
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black"
@click="cancel" <!-- Error (centered, destructive accent) -->
> <div
Return to Start v-else-if="nestedState === 'error'"
</Button> key="error"
</CardContent> class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
</Card> style="background: color-mix(in srgb, var(--destructive) 8%, var(--background))"
>
<div class="text-[8vh]">⚠️</div>
<h3 class="text-[3rem] font-bold text-destructive">Error</h3>
<p class="text-2xl text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<Button
v-if="context && context.billsInserted.length > 0"
class="w-full max-w-md bg-success text-success-foreground"
size="kiosk-lg"
@click="atmStore.send({ type: 'RETRY' })"
>
Try Again
</Button>
<Button
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black"
size="kiosk"
@click="cancel"
>
Return to Start
</Button>
</div>
<!-- Fallback --> <!-- Fallback -->
<Card v-else class="w-full max-w-80 border-0 bg-card"> <div v-else key="fallback" class="flex flex-1 items-center justify-center">
<CardContent class="p-8 text-center"> <p class="text-xl text-muted-foreground">State: {{ nestedState }}</p>
<p class="text-muted-foreground">State: {{ nestedState }}</p> </div>
</CardContent> </Transition>
</Card> </main>
</main>
</ScrollArea> <!-- Manual withdraw options (web-ui only, below main content) -->
<details
v-if="!isElectron && nestedState === 'displayingQR'"
class="mx-8 mb-4 rounded-lg bg-card p-3"
>
<summary class="cursor-pointer text-sm text-muted-foreground">
Alternative: Manual withdraw options
</summary>
<div class="mt-4 space-y-4">
<!-- CLINK Debit by pubkey -->
<div class="space-y-2">
<p class="text-xs font-medium text-accent">Enter your npub/pubkey:</p>
<div class="flex gap-2">
<Input
v-model="pubkeyInput"
placeholder="npub1... or hex pubkey"
class="flex-1 font-mono-code text-xs"
:disabled="isProcessing"
/>
<Button
variant="outline"
size="sm"
:disabled="!pubkeyInput.trim() || isProcessing"
@click="submitDebitRequest"
>
{{ atmStore.isRequestingDebit ? '...' : 'Send' }}
</Button>
</div>
</div>
<!-- Invoice paste -->
<div class="space-y-2">
<p class="text-xs font-medium text-bitcoin">Or paste your invoice:</p>
<div class="flex gap-2">
<Input
v-model="invoiceInput"
placeholder="lnbc... or lntb..."
class="flex-1 font-mono-code text-xs"
:disabled="isProcessing"
/>
<Button
variant="outline"
size="sm"
:disabled="!invoiceInput.trim() || isProcessing"
@click="submitInvoice"
>
{{ atmStore.isPayingInvoice ? '...' : 'Pay' }}
</Button>
</div>
</div>
<p v-if="atmStore.paymentError" class="text-xs text-destructive">
Error: {{ atmStore.paymentError }}
</p>
</div>
</details>
</div> </div>
</template> </template>