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:
parent
263165b495
commit
d3e652bce3
1 changed files with 284 additions and 262 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue