refactor: Revamp PurchaseTicketDialog and introduce useTicketPurchase composable
- Update PurchaseTicketDialog.vue to integrate authentication checks and enhance ticket purchasing flow with wallet support. - Implement useTicketPurchase composable for managing ticket purchase logic, including payment handling and QR code generation. - Improve user experience by displaying user information and wallet status during the ticket purchase process. - Refactor API interactions in events.ts to streamline ticket purchasing and payment status checks.
This commit is contained in:
parent
ed51c95799
commit
f7450627bc
5 changed files with 555 additions and 113 deletions
|
|
@ -1,12 +1,12 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import QRCode from 'qrcode'
|
||||
import { config } from '@/lib/config'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useTicketPurchase } from '@/composables/useTicketPurchase'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { User, Wallet, CreditCard, Zap } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
event: {
|
||||
|
|
@ -25,139 +25,194 @@ interface Emits {
|
|||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
const isLoading = ref(false)
|
||||
const paymentHash = ref('')
|
||||
const paymentRequest = ref('')
|
||||
const qrCode = ref('')
|
||||
const error = ref('')
|
||||
const { isAuthenticated, userDisplay } = useAuth()
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
paymentHash,
|
||||
paymentRequest,
|
||||
qrCode,
|
||||
isPaymentPending,
|
||||
isPayingWithWallet,
|
||||
canPurchase,
|
||||
userWallets,
|
||||
hasWalletWithBalance,
|
||||
purchaseTicketForEvent,
|
||||
handleOpenLightningWallet,
|
||||
resetPaymentState,
|
||||
cleanup
|
||||
} = useTicketPurchase()
|
||||
|
||||
async function generateQRCode(bolt11: string) {
|
||||
try {
|
||||
qrCode.value = await QRCode.toDataURL(`lightning:${bolt11}`)
|
||||
} catch (err) {
|
||||
console.error('Failed to generate QR code:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!name.value || !email.value) {
|
||||
error.value = 'Please fill out all fields'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
const apiUrl = `${config.api.baseUrl}/events/api/v1/tickets/${props.event.id}/${encodeURIComponent(name.value)}/${encodeURIComponent(email.value)}`
|
||||
console.log('Calling API:', apiUrl)
|
||||
async function handlePurchase() {
|
||||
if (!canPurchase.value) return
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': config.api.key
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Response status:', response.status)
|
||||
const responseText = await response.text()
|
||||
console.log('Response body:', responseText)
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Failed to generate payment request'
|
||||
try {
|
||||
const errorData = JSON.parse(responseText)
|
||||
errorMessage = errorData.detail || errorMessage
|
||||
} catch (e) {
|
||||
console.error('Failed to parse error response:', e)
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(responseText)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response as JSON:', e)
|
||||
throw new Error('Invalid response format from server')
|
||||
}
|
||||
|
||||
if (!data.payment_hash || !data.payment_request) {
|
||||
console.error('Invalid response data:', data)
|
||||
throw new Error('Invalid payment data received')
|
||||
}
|
||||
|
||||
paymentHash.value = data.payment_hash
|
||||
paymentRequest.value = data.payment_request
|
||||
await generateQRCode(data.payment_request)
|
||||
await purchaseTicketForEvent(props.event.id)
|
||||
} catch (err) {
|
||||
console.error('Error generating ticket:', err)
|
||||
error.value = err instanceof Error ? err.message : 'An error occurred while processing your request'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenLightningWallet() {
|
||||
if (paymentRequest.value) {
|
||||
window.location.href = `lightning:${paymentRequest.value}`
|
||||
console.error('Error purchasing ticket:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:isOpen', false)
|
||||
// Reset form state
|
||||
name.value = ''
|
||||
email.value = ''
|
||||
paymentHash.value = ''
|
||||
paymentRequest.value = ''
|
||||
qrCode.value = ''
|
||||
error.value = ''
|
||||
resetPaymentState()
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="isOpen" @update:open="handleClose">
|
||||
<DialogContent class="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Purchase Ticket</DialogTitle>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<CreditCard class="w-5 h-5" />
|
||||
Purchase Ticket
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Purchase a ticket for {{ event.name }} for {{ event.price_per_ticket }} {{ event.currency }}
|
||||
Purchase a ticket for <strong>{{ event.name }}</strong> for {{ event.price_per_ticket }} {{ event.currency }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="!paymentHash" class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input id="name" v-model="name" placeholder="Enter your name" :disabled="isLoading" />
|
||||
<!-- Authentication Check -->
|
||||
<div v-if="!isAuthenticated" class="py-4 text-center space-y-4">
|
||||
<div class="flex justify-center">
|
||||
<User class="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input id="email" v-model="email" type="email" placeholder="Enter your email" :disabled="isLoading" />
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold">Login Required</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Please log in to your account to purchase tickets using your wallet.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="error" class="text-sm text-destructive">
|
||||
<Button @click="handleClose" variant="outline">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- User Info and Purchase -->
|
||||
<div v-else-if="!paymentHash" class="py-4 space-y-4">
|
||||
<div class="bg-muted/50 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<User class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">Purchasing as:</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">Name:</span>
|
||||
<span class="text-sm font-medium">{{ userDisplay?.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">User ID:</span>
|
||||
<Badge variant="secondary" class="text-xs font-mono">{{ userDisplay?.shortId }}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wallet Information -->
|
||||
<div class="bg-muted/50 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Wallet class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">Wallet Status:</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div v-if="userWallets.length === 0" class="text-sm text-muted-foreground">
|
||||
No wallets found
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="wallet in userWallets" :key="wallet.id" class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{{ wallet.name }}</span>
|
||||
<Badge v-if="wallet.balance_msat > 0" variant="default" class="text-xs">
|
||||
{{ (wallet.balance_msat / 1000).toFixed(0) }} sats
|
||||
</Badge>
|
||||
<Badge v-else variant="secondary" class="text-xs">Empty</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasWalletWithBalance" class="flex items-center gap-2 text-sm text-green-600">
|
||||
<Zap class="w-4 h-4" />
|
||||
<span>Auto-payment available</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Manual payment required</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-muted/50 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<CreditCard class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">Payment Details:</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-muted-foreground">Event:</span>
|
||||
<span class="text-sm font-medium">{{ event.name }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-muted-foreground">Price:</span>
|
||||
<span class="text-sm font-medium">{{ event.price_per_ticket }} {{ event.currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="text-sm text-destructive bg-destructive/10 p-3 rounded-lg">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@click="handlePurchase"
|
||||
:disabled="isLoading || !canPurchase"
|
||||
class="w-full"
|
||||
>
|
||||
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
||||
<span v-else-if="hasWalletWithBalance" class="flex items-center gap-2">
|
||||
<Zap class="w-4 h-4" />
|
||||
Pay with Wallet
|
||||
</span>
|
||||
<span v-else>Generate Payment Request</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Payment QR Code and Status -->
|
||||
<div v-else class="py-4 flex flex-col items-center gap-4">
|
||||
<img :src="qrCode" alt="Lightning payment QR code" class="w-64 h-64" />
|
||||
<div class="text-center space-y-2">
|
||||
<p class="text-sm text-muted-foreground">Scan with your Lightning wallet to pay</p>
|
||||
<Button variant="outline" @click="handleOpenLightningWallet">
|
||||
<h3 class="text-lg font-semibold">Payment Required</h3>
|
||||
<p v-if="isPayingWithWallet" class="text-sm text-muted-foreground">
|
||||
Processing payment with your wallet...
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground">
|
||||
Scan the QR code with your Lightning wallet to complete the payment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!isPayingWithWallet" class="bg-muted/50 rounded-lg p-4">
|
||||
<img :src="qrCode" alt="Lightning payment QR code" class="w-64 h-64 mx-auto" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 w-full">
|
||||
<Button v-if="!isPayingWithWallet" variant="outline" @click="handleOpenLightningWallet" class="w-full">
|
||||
<Wallet class="w-4 h-4 mr-2" />
|
||||
Open in Lightning Wallet
|
||||
</Button>
|
||||
|
||||
<div v-if="isPaymentPending" class="text-center space-y-2">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ isPayingWithWallet ? 'Processing payment...' : 'Waiting for payment...' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Payment will be confirmed automatically once received
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!paymentHash" class="flex justify-end">
|
||||
<Button type="submit" :disabled="isLoading" @click="handleSubmit">
|
||||
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
||||
Continue to Payment
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue