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:
padreug 2025-03-09 18:24:35 +01:00
parent 933b2f3af1
commit c3a8abb252
4 changed files with 410 additions and 13 deletions

View 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>

View file

@ -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>