feat: update CashOutView UI for noffer-based flow
- Redesign cash-out view for noffer (CLINK Offers) workflow - Add states: generatingNoffer, displayingNoffer, validatingRequest, sendingInvoice, awaitingPayment - Remove old selectingAmount state (user specifies amount in wallet) - Add debug controls to simulate offer requests for testing - Wire up offer request callback in ATM store to state machine Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
94785f8d9d
commit
66a3c054e7
2 changed files with 124 additions and 39 deletions
|
|
@ -156,10 +156,23 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
paymentReceived(preimage)
|
paymentReceived(preimage)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wire up offer request callback (for logging/debugging)
|
// Wire up offer request callback - send to state machine for cash-out flow
|
||||||
services.onOfferRequest((request, sender) => {
|
services.onOfferRequest((request, sender) => {
|
||||||
console.log('[ATM] Offer request from:', sender.slice(0, 16) + '...')
|
console.log('[ATM] Offer request from:', sender.slice(0, 16) + '...')
|
||||||
console.log('[ATM] Amount requested:', request.amount_sats, 'sats')
|
console.log('[ATM] Amount requested:', request.amount_sats, 'sats')
|
||||||
|
|
||||||
|
// Send to state machine if we're in the displayingNoffer state
|
||||||
|
if (nestedState.value === 'displayingNoffer') {
|
||||||
|
send({
|
||||||
|
type: 'OFFER_REQUEST_RECEIVED',
|
||||||
|
request: {
|
||||||
|
eventId: request.offer || 'unknown',
|
||||||
|
payerPubkey: sender,
|
||||||
|
amountSats: request.amount_sats,
|
||||||
|
description: request.description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize state machine with real services
|
// Initialize state machine with real services
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,6 @@ watch(
|
||||||
const nestedState = computed(() => atmStore.nestedState)
|
const nestedState = computed(() => atmStore.nestedState)
|
||||||
const context = computed(() => atmStore.context)
|
const context = computed(() => atmStore.context)
|
||||||
|
|
||||||
// Available denominations for cash-out
|
|
||||||
const amounts = [20, 40, 60, 80, 100]
|
|
||||||
|
|
||||||
function selectAmount(amount: number) {
|
|
||||||
atmStore.selectAmount(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
atmStore.cancel()
|
atmStore.cancel()
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +40,10 @@ function skipReceipt() {
|
||||||
function formatSats(sats: number): string {
|
function formatSats(sats: number): string {
|
||||||
return sats.toLocaleString()
|
return sats.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatFiat(cents: number): string {
|
||||||
|
return (cents / 100).toFixed(2)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -68,56 +65,131 @@ function formatSats(sats: number): string {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Select Amount -->
|
<!-- Generating Noffer -->
|
||||||
<Card v-else-if="nestedState === 'selectingAmount'" class="border-0 bg-white/5">
|
<Card v-else-if="nestedState === 'generatingNoffer'" class="w-80 border-0 bg-white/5">
|
||||||
<CardHeader class="text-center">
|
<CardContent class="flex flex-col items-center gap-4 p-8">
|
||||||
<CardTitle>Select Amount</CardTitle>
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
<CardDescription v-if="context">
|
<p class="text-muted-foreground">Preparing payment code...</p>
|
||||||
Rate: {{ formatSats(context.exchangeRate) }} sats/$
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<Card
|
|
||||||
v-for="amount in amounts"
|
|
||||||
:key="amount"
|
|
||||||
class="cursor-pointer border-2 border-transparent bg-white/5 transition hover:border-orange-500 hover:bg-white/10"
|
|
||||||
@click="selectAmount(amount)"
|
|
||||||
>
|
|
||||||
<CardContent class="flex flex-col items-center p-4">
|
|
||||||
<span class="text-2xl font-bold">${{ amount }}</span>
|
|
||||||
<span v-if="context" class="mt-1 text-sm text-muted-foreground">
|
|
||||||
≈ {{ formatSats(Math.floor(amount * context.exchangeRate)) }} sats
|
|
||||||
</span>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- Display Noffer QR - Main cash-out screen -->
|
||||||
|
<Card v-else-if="nestedState === 'displayingNoffer'" class="w-[480px] border-0 bg-white/5">
|
||||||
|
<CardHeader class="text-center">
|
||||||
|
<CardTitle>Scan to Sell Bitcoin</CardTitle>
|
||||||
|
<CardDescription v-if="context">
|
||||||
|
Current rate:
|
||||||
|
<Badge variant="secondary">{{ formatSats(context.exchangeRate) }} sats/$</Badge>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-6">
|
||||||
|
<!-- Noffer QR Code -->
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<QRCode v-if="context?.nofferString" :value="context.nofferString" :size="220" />
|
||||||
|
<p class="mt-4 text-center text-sm text-muted-foreground">
|
||||||
|
Scan with your Lightning wallet
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<Alert class="border-orange-500/50 bg-orange-500/10">
|
||||||
|
<AlertDescription>
|
||||||
|
<p class="mb-2 text-sm font-medium text-orange-400">How it works:</p>
|
||||||
|
<ol class="space-y-1 text-xs text-muted-foreground">
|
||||||
|
<li>1. Scan the QR code with Shock Wallet or compatible wallet</li>
|
||||||
|
<li>2. Enter the amount you want to sell (in sats)</li>
|
||||||
|
<li>3. Confirm the payment in your wallet</li>
|
||||||
|
<li>4. Receive cash from the ATM</li>
|
||||||
|
</ol>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<!-- Info about limits -->
|
||||||
|
<div class="rounded-lg bg-white/5 p-4 text-center">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Limits: <Badge variant="outline">$1 - $500</Badge>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">Waiting for payment request...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug: Simulate offer request -->
|
||||||
|
<Alert v-if="atmStore.debugMode" class="border-red-500/50 bg-red-500/10">
|
||||||
|
<AlertDescription class="text-center">
|
||||||
|
<p class="mb-2 text-xs text-red-400">Dev: Simulate offer request</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="
|
||||||
|
atmStore.send({
|
||||||
|
type: 'OFFER_REQUEST_RECEIVED',
|
||||||
|
request: {
|
||||||
|
eventId: 'mock-event-' + Date.now(),
|
||||||
|
payerPubkey: 'mock-pubkey-' + Date.now(),
|
||||||
|
amountSats: 250000,
|
||||||
|
description: 'Mock cash out',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Simulate $100 Request (250k sats)
|
||||||
|
</Button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Validating Request -->
|
||||||
|
<Card v-else-if="nestedState === 'validatingRequest'" class="w-80 border-0 bg-white/5">
|
||||||
|
<CardContent class="flex flex-col items-center gap-4 p-8">
|
||||||
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
|
<p class="text-muted-foreground">Validating amount...</p>
|
||||||
|
<p v-if="context" class="text-sm text-orange-400">
|
||||||
|
{{ formatSats(context.satsAmount) }} sats requested
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Generating Invoice -->
|
<!-- Generating Invoice -->
|
||||||
<Card
|
<Card v-else-if="nestedState === 'generatingInvoice'" class="w-80 border-0 bg-white/5">
|
||||||
v-else-if="nestedState === 'calculatingInvoice' || nestedState === 'generatingInvoice'"
|
|
||||||
class="w-80 border-0 bg-white/5"
|
|
||||||
>
|
|
||||||
<CardContent class="flex flex-col items-center gap-4 p-8">
|
<CardContent class="flex flex-col items-center gap-4 p-8">
|
||||||
<Skeleton class="h-10 w-10 rounded-full" />
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
<p class="text-muted-foreground">Generating invoice...</p>
|
<p class="text-muted-foreground">Creating invoice...</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Display Invoice QR -->
|
<!-- Sending Invoice -->
|
||||||
<Card v-else-if="nestedState === 'displayingInvoice'" class="w-96 border-0 bg-white/5">
|
<Card v-else-if="nestedState === 'sendingInvoice'" class="w-80 border-0 bg-white/5">
|
||||||
|
<CardContent class="flex flex-col items-center gap-4 p-8">
|
||||||
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
|
<p class="text-muted-foreground">Sending invoice to wallet...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Awaiting Payment -->
|
||||||
|
<Card v-else-if="nestedState === 'awaitingPayment'" class="w-96 border-0 bg-white/5">
|
||||||
<CardHeader class="text-center">
|
<CardHeader class="text-center">
|
||||||
<CardTitle>Scan to Pay</CardTitle>
|
<CardTitle>Waiting for Payment</CardTitle>
|
||||||
<CardDescription v-if="context">
|
<CardDescription v-if="context">
|
||||||
Pay <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> to
|
Pay <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> to
|
||||||
receive <Badge class="bg-green-600">${{ context.fiatAmount / 100 }}</Badge>
|
receive <Badge class="bg-green-600">${{ formatFiat(context.fiatAmount) }}</Badge>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<QRCode v-if="context?.invoice" :value="context.invoice" :size="200" />
|
<div class="flex flex-col items-center gap-4 py-8">
|
||||||
<p class="text-center text-sm text-muted-foreground">Waiting for payment...</p>
|
<div class="h-16 w-16 animate-pulse rounded-full bg-orange-500/20">
|
||||||
|
<div class="flex h-full items-center justify-center text-3xl">⏳</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted-foreground">
|
||||||
|
Please confirm the payment in your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invoice info (collapsed) -->
|
||||||
|
<div v-if="context?.invoice" class="rounded-lg bg-white/5 p-3 text-center">
|
||||||
|
<p class="truncate font-mono text-xs text-muted-foreground">
|
||||||
|
{{ context.invoice.slice(0, 40) }}...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Dev controls -->
|
<!-- Dev controls -->
|
||||||
<Alert v-if="atmStore.debugMode" class="border-red-500/50 bg-red-500/10">
|
<Alert v-if="atmStore.debugMode" class="border-red-500/50 bg-red-500/10">
|
||||||
|
|
@ -150,7 +222,7 @@ function formatSats(sats: number): string {
|
||||||
<CardTitle class="text-green-400">Cash Ready!</CardTitle>
|
<CardTitle class="text-green-400">Cash Ready!</CardTitle>
|
||||||
<p v-if="context">
|
<p v-if="context">
|
||||||
Please take your
|
Please take your
|
||||||
<Badge class="bg-green-600 text-lg">${{ context.fiatAmount / 100 }}</Badge>
|
<Badge class="bg-green-600 text-lg">${{ formatFiat(context.fiatAmount) }}</Badge>
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black hover:from-orange-600 hover:to-yellow-500"
|
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black hover:from-orange-600 hover:to-yellow-500"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue