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">
<CardContent class="flex flex-col items-center gap-4 p-8">
<Skeleton class="h-10 w-10 rounded-full" />
<p class="text-muted-foreground">Fetching exchange rate...</p>
</CardContent>
</Card>
<!-- Inserting Bills -->
<Card v-else-if="nestedState === 'insertingBills'" class="w-full max-w-96 border-0 bg-card">
<CardHeader class="text-center">
<CardTitle>Insert Cash</CardTitle>
<CardDescription v-if="context">
Rate: {{ formatSats(context.exchangeRate) }} sats/{{ atmStore.fiatSymbol }}
</CardDescription>
</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 -->
<div <div
v-if="context && context.availableBalance > 0" v-if="nestedState === 'fetchingRate'"
class="flex justify-between text-sm" key="fetchingRate"
class="flex flex-1 flex-col items-center justify-center gap-6"
> >
<span class="text-muted-foreground">Available:</span> <BounceDots class="scale-150" />
<span :class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'"> <p class="text-2xl text-muted-foreground">Fetching exchange rate...</p>
{{ formatSats(context.availableBalance) }} sats
</span>
</div> </div>
<!-- Bill Acceptor Status --> <!-- Inserting Bills (split-screen) -->
<p v-if="balanceLimitReached" class="text-center text-sm text-muted-foreground"> <div v-else-if="nestedState === 'insertingBills'" key="insertingBills" class="flex flex-1">
<!-- Left panel (52%) — amounts and controls -->
<div class="flex w-[52%] flex-col justify-center gap-6 px-[4vw] py-6">
<!-- 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 }}
</p>
<!-- Available balance -->
<p
v-if="context && context.availableBalance > 0"
class="text-xl"
:class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'"
>
Available: {{ formatSats(context.availableBalance) }} sats
</p>
<!-- Status -->
<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>
</Card>
<!-- Generating NDebit -->
<Card
v-else-if="nestedState === 'generatingNdebit'"
class="w-full max-w-80 border-0 bg-card"
>
<CardContent class="flex flex-col items-center gap-4 p-8">
<span class="animate-hourglass text-5xl">⏳</span>
<p class="text-muted-foreground">Preparing your withdraw code...</p>
</CardContent>
</Card>
<!-- Display QR (unified lightning + clink) -->
<Card
v-else-if="nestedState === 'displayingQR'"
class="w-full max-w-[420px] border-0 bg-card"
>
<CardContent class="pt-6 space-y-5">
<!-- Amount Display -->
<div class="text-center">
<p class="text-lg font-semibold text-success">Scan to receive your sats</p>
<p class="text-3xl font-bold text-bitcoin">
{{ formatSats(context?.satsAmount || 0) }} sats
</p>
<p class="text-sm text-muted-foreground">
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
</p>
</div> </div>
<!-- Scan with selector (web-ui only — kiosk users just scan) --> <!-- Right panel (48%) — bill visual -->
<div v-if="!isElectron" class="text-center"> <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'"
key="generatingNdebit"
class="flex flex-1 flex-col items-center justify-center gap-6"
>
<BounceDots class="scale-150" />
<p class="text-2xl text-muted-foreground">Preparing your withdraw code...</p>
</div>
<!-- Display QR (split-screen) -->
<div v-else-if="nestedState === 'displayingQR'" key="displayingQR" class="flex flex-1">
<!-- Left panel (52%) — info -->
<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>
<p class="text-[7vh] font-bold text-bitcoin leading-tight">
{{ formatSats(context?.satsAmount || 0) }} sats
</p>
<p class="text-2xl text-muted-foreground">
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
</p>
<!-- Waiting indicator -->
<div class="flex items-center gap-3 pt-4">
<BounceDots />
<p class="text-xl text-muted-foreground">Waiting for wallet scan...</p>
</div>
<!-- Mode selector (web-ui only) -->
<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,8 +409,147 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div> </div>
</div> </div>
<!-- Alternative: Manual Options (web-ui only, no keyboard on kiosk) --> <!-- Debug: Simulate payment button -->
<details v-if="!isElectron" class="rounded-lg bg-card p-3"> <div v-if="atmStore.debugMode" class="pt-2">
<Button
variant="ghost"
size="sm"
class="text-xs text-muted-foreground"
@click="atmStore.paymentReceived('mock-preimage-' + Date.now())"
>
Dev: Skip to Success
</Button>
</div>
</div>
<!-- Right panel (48%) — QR code -->
<div class="flex w-[48%] flex-col items-center justify-center bg-foreground/5 p-6">
<div v-if="isGeneratingLnurl && !context?.ndebitUri">
<BounceDots class="scale-150" />
</div>
<div v-else-if="currentQrValue">
<QRCode :value="currentQrValue" :size="380" />
</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>
<!-- Confirm Abandon (centered, destructive accent) -->
<div
v-else-if="nestedState === 'confirmAbandon'"
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))"
>
<div class="text-[8vh]">⚠️</div>
<h3 class="text-[3rem] font-bold text-destructive">Cash Already Accepted</h3>
<p class="max-w-lg text-center text-2xl text-muted-foreground">
Your {{ atmStore.fiatSymbol
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted and
cannot be returned. Please scan the QR code to receive your Bitcoin.
</p>
<Button
class="w-full max-w-md bg-success text-success-foreground"
size="kiosk-lg"
@click="atmStore.send({ type: 'RETRY' })"
>
Show QR Code Again
</Button>
<Button
variant="ghost"
size="kiosk"
class="w-full max-w-md text-destructive"
@click="cancel"
>
Abandon (lose cash)
</Button>
</div>
<!-- Ask for Receipt (centered) -->
<div
v-else-if="nestedState === 'askForReceipt'"
key="askForReceipt"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
>
<div class="text-[8vh] text-success">✅</div>
<h3 class="text-[3rem] font-bold">Sats Sent!</h3>
<p class="text-2xl 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>
<Button variant="outline" size="kiosk" @click="skipReceipt">No Thanks</Button>
</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
>
</p>
<p class="text-2xl text-muted-foreground">Thank you!</p>
</div>
<!-- Error (centered, destructive accent) -->
<div
v-else-if="nestedState === 'error'"
key="error"
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))"
>
<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 -->
<div v-else key="fallback" class="flex flex-1 items-center justify-center">
<p class="text-xl text-muted-foreground">State: {{ nestedState }}</p>
</div>
</Transition>
</main>
<!-- 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"> <summary class="cursor-pointer text-sm text-muted-foreground">
Alternative: Manual withdraw options Alternative: Manual withdraw options
</summary> </summary>
@ -432,7 +561,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<Input <Input
v-model="pubkeyInput" v-model="pubkeyInput"
placeholder="npub1... or hex pubkey" placeholder="npub1... or hex pubkey"
class="flex-1 font-mono text-xs" class="flex-1 font-mono-code text-xs"
:disabled="isProcessing" :disabled="isProcessing"
/> />
<Button <Button
@ -453,7 +582,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<Input <Input
v-model="invoiceInput" v-model="invoiceInput"
placeholder="lnbc... or lntb..." placeholder="lnbc... or lntb..."
class="flex-1 font-mono text-xs" class="flex-1 font-mono-code text-xs"
:disabled="isProcessing" :disabled="isProcessing"
/> />
<Button <Button
@ -472,112 +601,5 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</p> </p>
</div> </div>
</details> </details>
<!-- Debug: Simulate payment button -->
<div v-if="atmStore.debugMode" class="pt-2 text-center">
<Button
variant="ghost"
size="sm"
class="text-xs text-muted-foreground"
@click="atmStore.paymentReceived('mock-preimage-' + Date.now())"
>
Dev: Skip to Success
</Button>
</div>
</CardContent>
</Card>
<!-- Ask for Receipt -->
<Card v-else-if="nestedState === 'askForReceipt'" class="w-full max-w-80 border-0 bg-card">
<CardContent class="flex flex-col items-center gap-4 p-8">
<div class="text-6xl text-success">✅</div>
<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>
<Button variant="outline" @click="skipReceipt">No Thanks</Button>
</CardContent>
</Card>
<!-- Sending Receipt -->
<Card v-else-if="nestedState === 'sendingReceipt'" class="w-full max-w-80 border-0 bg-card">
<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'"
class="w-full max-w-80 border-0 bg-destructive/20"
>
<CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">⚠️</div>
<CardTitle class="text-destructive">Cash Already Accepted</CardTitle>
<p class="text-muted-foreground">
Your {{ atmStore.fiatSymbol
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted
and cannot be returned. Please scan the QR code to receive your Bitcoin.
</p>
<Button
class="w-full bg-success text-success-foreground"
@click="atmStore.send({ type: 'RETRY' })"
>
Show QR Code Again
</Button>
<Button variant="ghost" class="w-full text-destructive" @click="cancel">
Abandon (lose cash)
</Button>
</CardContent>
</Card>
<!-- Error -->
<Card
v-else-if="nestedState === 'error'"
class="w-full max-w-80 border-0 bg-destructive/20"
>
<CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">⚠️</div>
<CardTitle class="text-destructive">Error</CardTitle>
<p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<Button
v-if="context && context.billsInserted.length > 0"
class="w-full bg-success text-success-foreground"
@click="atmStore.send({ type: 'RETRY' })"
>
Try Again
</Button>
<Button
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black"
@click="cancel"
>
Return to Start
</Button>
</CardContent>
</Card>
<!-- Fallback -->
<Card v-else class="w-full max-w-80 border-0 bg-card">
<CardContent class="p-8 text-center">
<p class="text-muted-foreground">State: {{ nestedState }}</p>
</CardContent>
</Card>
</main>
</ScrollArea>
</div> </div>
</template> </template>