+
+
-
-
-
+
+
Login Required
+
+ Please log in to your account to purchase tickets using your wallet.
+
-
+
+
+
+
+
+
+
+
+ Purchasing as:
+
+
+
+ Name:
+ {{ userDisplay?.name }}
+
+
+ User ID:
+ {{ userDisplay?.shortId }}
+
+
+
+
+
+
+
+
+ Wallet Status:
+
+
+
+ No wallets found
+
+
+
+
+ {{ wallet.name }}
+
+ {{ (wallet.balance_msat / 1000).toFixed(0) }} sats
+
+ Empty
+
+
+
+
+ Auto-payment available
+
+
+ Manual payment required
+
+
+
+
+
+
+
+
+ Payment Details:
+
+
+
+ Event:
+ {{ event.name }}
+
+
+ Price:
+ {{ event.price_per_ticket }} {{ event.currency }}
+
+
+
+
+
{{ error }}
+
+
+
-
-
Scan with your Lightning wallet to pay
-
+
+
+
![Lightning payment QR code]()
+
+
+
+
+
+
+
+
+
+ {{ isPayingWithWallet ? 'Processing payment...' : 'Waiting for payment...' }}
+
+
+
+ Payment will be confirmed automatically once received
+
+
-
-
-
-
\ No newline at end of file
diff --git a/src/composables/useTicketPurchase.ts b/src/composables/useTicketPurchase.ts
new file mode 100644
index 0000000..76cdf97
--- /dev/null
+++ b/src/composables/useTicketPurchase.ts
@@ -0,0 +1,220 @@
+import { ref, computed } from 'vue'
+import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events'
+import { useAuth } from './useAuth'
+import { toast } from 'vue-sonner'
+
+interface Wallet {
+ id: string
+ user: string
+ name: string
+ adminkey: string
+ inkey: string
+ deleted: boolean
+ created_at: string
+ updated_at: string
+ currency?: string
+ balance_msat: number
+ extra?: {
+ icon: string
+ color: string
+ pinned: boolean
+ }
+}
+
+export function useTicketPurchase() {
+ const { isAuthenticated, currentUser } = useAuth()
+ const isLoading = ref(false)
+ const error = ref
(null)
+ const paymentHash = ref('')
+ const paymentRequest = ref('')
+ const qrCode = ref('')
+ const isPaymentPending = ref(false)
+ const paymentCheckInterval = ref(null)
+ const isPayingWithWallet = ref(false)
+
+ const canPurchase = computed(() => {
+ return isAuthenticated.value && !isLoading.value
+ })
+
+ const userDisplay = computed(() => {
+ if (!currentUser.value) return null
+
+ return {
+ name: currentUser.value.username || currentUser.value.email || 'Anonymous',
+ id: currentUser.value.id,
+ shortId: currentUser.value.id.slice(0, 8) + '...' + currentUser.value.id.slice(-8)
+ }
+ })
+
+ const userWallets = computed(() => {
+ if (!currentUser.value) return [] as Wallet[]
+ return currentUser.value.wallets || []
+ })
+
+ const hasWalletWithBalance = computed(() => {
+ return userWallets.value.some((wallet: Wallet) => wallet.balance_msat > 0)
+ })
+
+ async function generateQRCode(bolt11: string) {
+ try {
+ const QRCode = await import('qrcode')
+ qrCode.value = await QRCode.toDataURL(`lightning:${bolt11}`)
+ } catch (err) {
+ console.error('Failed to generate QR code:', err)
+ }
+ }
+
+ async function payWithWallet(paymentRequest: string) {
+ if (!currentUser.value || !userWallets.value.length) {
+ throw new Error('No wallet available for payment')
+ }
+
+ // Find the first wallet with sufficient balance
+ const wallet = userWallets.value.find((w: Wallet) => w.balance_msat > 0)
+ if (!wallet) {
+ throw new Error('No wallet with sufficient balance found')
+ }
+
+ try {
+ isPayingWithWallet.value = true
+ const result = await payInvoiceWithWallet(paymentRequest, wallet.id, wallet.adminkey)
+
+ toast.success(`Payment successful! Fee: ${result.fee_msat} msat`)
+ return result
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Failed to pay with wallet'
+ toast.error(message)
+ throw err
+ } finally {
+ isPayingWithWallet.value = false
+ }
+ }
+
+ async function purchaseTicketForEvent(eventId: string) {
+ if (!isAuthenticated.value) {
+ error.value = 'Please log in to purchase tickets'
+ toast.error('Please log in to purchase tickets')
+ return
+ }
+
+ try {
+ isLoading.value = true
+ error.value = ''
+
+ // Step 1: Get the invoice
+ const result = await purchaseTicket(eventId)
+
+ paymentHash.value = result.payment_hash
+ paymentRequest.value = result.payment_request
+ await generateQRCode(result.payment_request)
+
+ // Step 2: Try to pay with wallet if user has balance
+ if (hasWalletWithBalance.value) {
+ try {
+ await payWithWallet(result.payment_request)
+ // Payment successful, start monitoring for ticket confirmation
+ startPaymentStatusCheck(eventId, result.payment_hash)
+ return
+ } catch (walletPaymentError) {
+ console.log('Wallet payment failed, showing QR code for manual payment:', walletPaymentError)
+ // Fall back to QR code payment
+ startPaymentStatusCheck(eventId, result.payment_hash)
+ }
+ } else {
+ // No wallet balance, show QR code for manual payment
+ startPaymentStatusCheck(eventId, result.payment_hash)
+ }
+
+ toast.success('Payment request generated successfully')
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Failed to generate payment request'
+ error.value = message
+ toast.error(message)
+ throw err
+ } finally {
+ isLoading.value = false
+ }
+ }
+
+ function startPaymentStatusCheck(eventId: string, paymentHash: string) {
+ isPaymentPending.value = true
+
+ // Clear any existing interval
+ if (paymentCheckInterval.value) {
+ clearInterval(paymentCheckInterval.value)
+ }
+
+ // Check payment status every 2 seconds
+ paymentCheckInterval.value = window.setInterval(async () => {
+ try {
+ const status = await checkPaymentStatus(eventId, paymentHash)
+
+ if (status.paid) {
+ // Payment successful
+ clearInterval(paymentCheckInterval.value!)
+ paymentCheckInterval.value = null
+ isPaymentPending.value = false
+
+ toast.success('Payment successful! Your ticket has been purchased.')
+
+ // Reset payment state
+ resetPaymentState()
+
+ return { success: true, ticketId: status.ticket_id }
+ }
+ } catch (err) {
+ console.error('Error checking payment status:', err)
+ // Don't show error to user for status checks, just log it
+ }
+ }, 2000)
+ }
+
+ function stopPaymentStatusCheck() {
+ if (paymentCheckInterval.value) {
+ clearInterval(paymentCheckInterval.value)
+ paymentCheckInterval.value = null
+ }
+ isPaymentPending.value = false
+ }
+
+ function resetPaymentState() {
+ paymentHash.value = ''
+ paymentRequest.value = ''
+ qrCode.value = ''
+ error.value = ''
+ stopPaymentStatusCheck()
+ }
+
+ function handleOpenLightningWallet() {
+ if (paymentRequest.value) {
+ window.location.href = `lightning:${paymentRequest.value}`
+ }
+ }
+
+ // Cleanup on unmount
+ function cleanup() {
+ stopPaymentStatusCheck()
+ }
+
+ return {
+ // State
+ isLoading,
+ error,
+ paymentHash,
+ paymentRequest,
+ qrCode,
+ isPaymentPending,
+ isPayingWithWallet,
+ canPurchase,
+ userDisplay,
+ userWallets,
+ hasWalletWithBalance,
+
+ // Actions
+ purchaseTicketForEvent,
+ payWithWallet,
+ handleOpenLightningWallet,
+ resetPaymentState,
+ cleanup,
+ }
+}
\ No newline at end of file
diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts
index eb1b4b5..68eb6cf 100644
--- a/src/lib/api/events.ts
+++ b/src/lib/api/events.ts
@@ -1,9 +1,15 @@
import type { Event, EventsApiError } from '../types/event'
import { config } from '@/lib/config'
+import { lnbitsAPI } from './lnbits'
const API_BASE_URL = config.api.baseUrl || 'http://lnbits'
const API_KEY = config.api.key
+// Generic error type for API responses
+interface ApiError {
+ detail: string | Array<{ loc: [string, number]; msg: string; type: string }>
+}
+
export async function fetchEvents(allWallets = true): Promise {
try {
const response = await fetch(
@@ -17,8 +23,11 @@ export async function fetchEvents(allWallets = true): Promise {
)
if (!response.ok) {
- const error: EventsApiError = await response.json()
- throw new Error(error.detail[0]?.msg || 'Failed to fetch events')
+ const error: ApiError = await response.json()
+ const errorMessage = typeof error.detail === 'string'
+ ? error.detail
+ : error.detail[0]?.msg || 'Failed to fetch events'
+ throw new Error(errorMessage)
}
return await response.json() as Event[]
@@ -26,4 +35,100 @@ export async function fetchEvents(allWallets = true): Promise {
console.error('Error fetching events:', error)
throw error
}
+}
+
+export async function purchaseTicket(eventId: string): Promise<{ payment_hash: string; payment_request: string }> {
+ try {
+ // Get current user to ensure authentication
+ const user = await lnbitsAPI.getCurrentUser()
+ if (!user) {
+ throw new Error('User not authenticated')
+ }
+
+ const response = await fetch(
+ `${API_BASE_URL}/events/api/v1/tickets/${eventId}/user/${user.id}`,
+ {
+ method: 'GET',
+ headers: {
+ 'accept': 'application/json',
+ 'X-API-KEY': API_KEY,
+ 'Authorization': `Bearer ${lnbitsAPI.getAccessToken()}`,
+ },
+ }
+ )
+
+ if (!response.ok) {
+ const error: ApiError = await response.json()
+ const errorMessage = typeof error.detail === 'string'
+ ? error.detail
+ : error.detail[0]?.msg || 'Failed to purchase ticket'
+ throw new Error(errorMessage)
+ }
+
+ return await response.json()
+ } catch (error) {
+ console.error('Error purchasing ticket:', error)
+ throw error
+ }
+}
+
+export async function payInvoiceWithWallet(paymentRequest: string, walletId: string, adminKey: string): Promise<{ payment_hash: string; fee_msat: number; preimage: string }> {
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/api/v1/payments`,
+ {
+ method: 'POST',
+ headers: {
+ 'accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': adminKey,
+ },
+ body: JSON.stringify({
+ out: true,
+ bolt11: paymentRequest,
+ }),
+ }
+ )
+
+ if (!response.ok) {
+ const error: ApiError = await response.json()
+ const errorMessage = typeof error.detail === 'string'
+ ? error.detail
+ : error.detail[0]?.msg || 'Failed to pay invoice'
+ throw new Error(errorMessage)
+ }
+
+ return await response.json()
+ } catch (error) {
+ console.error('Error paying invoice:', error)
+ throw error
+ }
+}
+
+export async function checkPaymentStatus(eventId: string, paymentHash: string): Promise<{ paid: boolean; ticket_id?: string }> {
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/events/api/v1/tickets/${eventId}/${paymentHash}`,
+ {
+ method: 'POST',
+ headers: {
+ 'accept': 'application/json',
+ 'X-API-KEY': API_KEY,
+ },
+ }
+ )
+
+ if (!response.ok) {
+ const error: ApiError = await response.json()
+ const errorMessage = typeof error.detail === 'string'
+ ? error.detail
+ : error.detail[0]?.msg || 'Failed to check payment status'
+ throw new Error(errorMessage)
+ }
+
+ return await response.json()
+ } catch (error) {
+ console.error('Error checking payment status:', error)
+ throw error
+ }
}
\ No newline at end of file
diff --git a/src/lib/api/lnbits.ts b/src/lib/api/lnbits.ts
index e13a64c..91b9592 100644
--- a/src/lib/api/lnbits.ts
+++ b/src/lib/api/lnbits.ts
@@ -17,13 +17,47 @@ interface AuthResponse {
email?: string
}
+interface Wallet {
+ id: string
+ user: string
+ name: string
+ adminkey: string
+ inkey: string
+ deleted: boolean
+ created_at: string
+ updated_at: string
+ currency?: string
+ balance_msat: number
+ extra?: {
+ icon: string
+ color: string
+ pinned: boolean
+ }
+}
+
interface User {
id: string
username?: string
email?: string
pubkey?: string
+ external_id?: string
+ extensions: string[]
+ wallets: Wallet[]
+ admin: boolean
+ super_user: boolean
+ fiat_providers: string[]
+ has_password: boolean
created_at: string
updated_at: string
+ extra?: {
+ email_verified?: boolean
+ first_name?: string
+ last_name?: string
+ display_name?: string
+ picture?: string
+ provider?: string
+ visible_wallet_count?: number
+ }
}
import { getApiUrl, getAuthToken, setAuthToken, removeAuthToken } from '@/lib/config/lnbits'
diff --git a/src/pages/events.vue b/src/pages/events.vue
index 845571c..e3f1182 100644
--- a/src/pages/events.vue
+++ b/src/pages/events.vue
@@ -2,15 +2,18 @@