feat(events): Add Lightning Network ticket purchase flow with QR code generation
- Integrate QRCode library for Lightning payment QR code generation - Create PurchaseTicketDialog component for ticket purchase workflow - Implement dynamic ticket purchase API integration with error handling - Add Lightning wallet payment QR code and deep linking support - Update events page to trigger ticket purchase dialog - Configure environment variables for ticket purchase API endpoint
This commit is contained in:
parent
933b2f3af1
commit
c3a8abb252
4 changed files with 410 additions and 13 deletions
162
src/components/events/PurchaseTicketDialog.vue
Normal file
162
src/components/events/PurchaseTicketDialog.vue
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script setup lang="ts">
|
||||
import { ref } 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'
|
||||
|
||||
interface Props {
|
||||
event: {
|
||||
id: string
|
||||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
}
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:isOpen', value: boolean): void
|
||||
}
|
||||
|
||||
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('')
|
||||
|
||||
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 = `${import.meta.env.VITE_API_BASE_URL}/events/api/v1/tickets/${props.event.id}/${encodeURIComponent(name.value)}/${encodeURIComponent(email.value)}`
|
||||
console.log('Calling API:', apiUrl)
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-API-KEY': import.meta.env.VITE_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)
|
||||
} 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}`
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:isOpen', false)
|
||||
// Reset form state
|
||||
name.value = ''
|
||||
email.value = ''
|
||||
paymentHash.value = ''
|
||||
paymentRequest.value = ''
|
||||
qrCode.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="isOpen" @update:open="handleClose">
|
||||
<DialogContent class="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Purchase Ticket</DialogTitle>
|
||||
<DialogDescription>
|
||||
Purchase a ticket for {{ event.name }} 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" />
|
||||
</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>
|
||||
<div v-if="error" class="text-sm text-destructive">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Open in Lightning Wallet
|
||||
</Button>
|
||||
</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>
|
||||
|
|
@ -1,17 +1,36 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useEvents } from '@/composables/useEvents'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { format } from 'date-fns'
|
||||
import PurchaseTicketDialog from '@/components/events/PurchaseTicketDialog.vue'
|
||||
|
||||
const { upcomingEvents, pastEvents, isLoading, error, refresh } = useEvents()
|
||||
const showPurchaseDialog = ref(false)
|
||||
const selectedEvent = ref<{
|
||||
id: string
|
||||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
} | null>(null)
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
return format(new Date(dateStr), 'PPP')
|
||||
}
|
||||
|
||||
function handlePurchaseClick(event: {
|
||||
id: string
|
||||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
}) {
|
||||
selectedEvent.value = event
|
||||
showPurchaseDialog.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -63,7 +82,12 @@ function formatDate(dateStr: string) {
|
|||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button class="w-full" variant="default" :disabled="event.amount_tickets <= event.sold">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="default"
|
||||
:disabled="event.amount_tickets <= event.sold"
|
||||
@click="handlePurchaseClick(event)"
|
||||
>
|
||||
Buy Ticket
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
|
@ -111,5 +135,11 @@ function formatDate(dateStr: string) {
|
|||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<PurchaseTicketDialog
|
||||
v-if="selectedEvent"
|
||||
:event="selectedEvent"
|
||||
v-model:is-open="showPurchaseDialog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue