diff --git a/src/components/events/PurchaseTicketDialog.vue b/src/components/events/PurchaseTicketDialog.vue index e5b2cf4..4005b38 100644 --- a/src/components/events/PurchaseTicketDialog.vue +++ b/src/components/events/PurchaseTicketDialog.vue @@ -40,7 +40,10 @@ const { purchaseTicketForEvent, handleOpenLightningWallet, resetPaymentState, - cleanup + cleanup, + ticketQRCode, + purchasedTicketId, + showTicketQR } = useTicketPurchase() async function handlePurchase() { @@ -179,7 +182,7 @@ onUnmounted(() => { -
+

Payment Required

@@ -190,7 +193,7 @@ onUnmounted(() => {

-
+
Lightning payment QR code
@@ -213,6 +216,32 @@ onUnmounted(() => {
+ + +
+
+

Ticket Purchased Successfully!

+

+ Your ticket has been purchased. Here's your ticket QR code: +

+
+ +
+
+ Ticket QR code +
+

Ticket ID

+

{{ purchasedTicketId }}

+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/src/composables/useTicketPurchase.ts b/src/composables/useTicketPurchase.ts index 76cdf97..e6bd52d 100644 --- a/src/composables/useTicketPurchase.ts +++ b/src/composables/useTicketPurchase.ts @@ -1,201 +1,218 @@ -import { ref, computed } from 'vue' +import { ref, computed, onUnmounted } 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() + + // State const isLoading = ref(false) const error = ref(null) - const paymentHash = ref('') - const paymentRequest = ref('') - const qrCode = ref('') + const paymentHash = ref(null) + const paymentRequest = ref(null) + const qrCode = ref(null) const isPaymentPending = ref(false) - const paymentCheckInterval = ref(null) const isPayingWithWallet = ref(false) + + // Ticket QR code state + const ticketQRCode = ref(null) + const purchasedTicketId = ref(null) + const showTicketQR = ref(false) - const canPurchase = computed(() => { - return isAuthenticated.value && !isLoading.value - }) - + // Computed properties + const canPurchase = computed(() => isAuthenticated.value && currentUser.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) + name: currentUser.value.username || currentUser.value.id, + shortId: currentUser.value.id.slice(0, 8) } }) + + const userWallets = computed(() => currentUser.value?.wallets || []) + const hasWalletWithBalance = computed(() => + userWallets.value.some((wallet: any) => wallet.balance_msat > 0) + ) - 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) - }) - + // Generate QR code for Lightning payment async function generateQRCode(bolt11: string) { try { - const QRCode = await import('qrcode') - qrCode.value = await QRCode.toDataURL(`lightning:${bolt11}`) + const qrcode = await import('qrcode') + const dataUrl = await qrcode.toDataURL(bolt11, { + width: 256, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }) + qrCode.value = dataUrl } catch (err) { - console.error('Failed to generate QR code:', err) + console.error('Error generating QR code:', err) + error.value = 'Failed to generate QR code' } } - async function payWithWallet(paymentRequest: string) { - if (!currentUser.value || !userWallets.value.length) { - throw new Error('No wallet available for payment') + // Generate QR code for ticket + async function generateTicketQRCode(ticketId: string) { + try { + const qrcode = await import('qrcode') + const ticketUrl = `ticket://${ticketId}` + const dataUrl = await qrcode.toDataURL(ticketUrl, { + width: 128, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }) + ticketQRCode.value = dataUrl + return dataUrl + } catch (error) { + console.error('Error generating ticket QR code:', error) + return null } + } - // Find the first wallet with sufficient balance - const wallet = userWallets.value.find((w: Wallet) => w.balance_msat > 0) - if (!wallet) { + // Pay with wallet + async function payWithWallet(paymentRequest: string) { + const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0) + + if (!walletWithBalance) { 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 + await payInvoiceWithWallet(paymentRequest, walletWithBalance.id, walletWithBalance.adminkey) + return true + } catch (error) { + console.error('Wallet payment failed:', error) + throw error } } + // Purchase ticket for event 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 + if (!canPurchase.value) { + throw new Error('User must be authenticated to purchase tickets') } - try { - isLoading.value = true - error.value = '' + isLoading.value = true + error.value = null + paymentHash.value = null + paymentRequest.value = null + qrCode.value = null + ticketQRCode.value = null + purchasedTicketId.value = null + showTicketQR.value = false - // 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 + try { + // Get the invoice + const invoice = await purchaseTicket(eventId) + paymentHash.value = invoice.payment_hash + paymentRequest.value = invoice.payment_request + + // Generate QR code for payment + await generateQRCode(invoice.payment_request) + + // Try to pay with wallet if available if (hasWalletWithBalance.value) { + isPayingWithWallet.value = true 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) + await payWithWallet(invoice.payment_request) + // If wallet payment succeeds, proceed to check payment status + await startPaymentStatusCheck(eventId, invoice.payment_hash) + } catch (walletError) { + // If wallet payment fails, fall back to manual payment + console.log('Wallet payment failed, falling back to manual payment:', walletError) + isPayingWithWallet.value = false + await startPaymentStatusCheck(eventId, invoice.payment_hash) } } else { - // No wallet balance, show QR code for manual payment - startPaymentStatusCheck(eventId, result.payment_hash) + // No wallet balance, proceed with manual payment + await startPaymentStatusCheck(eventId, invoice.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 + error.value = err instanceof Error ? err.message : 'Failed to purchase ticket' + console.error('Error purchasing ticket:', err) } finally { isLoading.value = false } } - function startPaymentStatusCheck(eventId: string, paymentHash: string) { + // Start payment status check + async function startPaymentStatusCheck(eventId: string, hash: string) { isPaymentPending.value = true - - // Clear any existing interval - if (paymentCheckInterval.value) { - clearInterval(paymentCheckInterval.value) - } + let checkInterval: NodeJS.Timeout | null = null - // Check payment status every 2 seconds - paymentCheckInterval.value = window.setInterval(async () => { + const checkPayment = async () => { try { - const status = await checkPaymentStatus(eventId, paymentHash) + const result = await checkPaymentStatus(eventId, hash) - if (status.paid) { - // Payment successful - clearInterval(paymentCheckInterval.value!) - paymentCheckInterval.value = null + if (result.paid) { isPaymentPending.value = false + if (checkInterval) { + clearInterval(checkInterval) + } - toast.success('Payment successful! Your ticket has been purchased.') + // Generate ticket QR code + if (result.ticket_id) { + purchasedTicketId.value = result.ticket_id + await generateTicketQRCode(result.ticket_id) + showTicketQR.value = true + } - // Reset payment state - resetPaymentState() - - return { success: true, ticketId: status.ticket_id } + toast.success('Ticket purchased successfully!') } } catch (err) { console.error('Error checking payment status:', err) - // Don't show error to user for status checks, just log it } - }, 2000) + } + + // Check immediately + await checkPayment() + + // Then check every 2 seconds + checkInterval = setInterval(checkPayment, 2000) } + // Stop payment status check function stopPaymentStatusCheck() { - if (paymentCheckInterval.value) { - clearInterval(paymentCheckInterval.value) - paymentCheckInterval.value = null - } isPaymentPending.value = false } + // Reset payment state function resetPaymentState() { - paymentHash.value = '' - paymentRequest.value = '' - qrCode.value = '' - error.value = '' - stopPaymentStatusCheck() + isLoading.value = false + error.value = null + paymentHash.value = null + paymentRequest.value = null + qrCode.value = null + isPaymentPending.value = false + isPayingWithWallet.value = false + ticketQRCode.value = null + purchasedTicketId.value = null + showTicketQR.value = false } + // Open Lightning wallet function handleOpenLightningWallet() { if (paymentRequest.value) { - window.location.href = `lightning:${paymentRequest.value}` + window.open(`lightning:${paymentRequest.value}`, '_blank') } } - // Cleanup on unmount + // Cleanup function function cleanup() { stopPaymentStatusCheck() } + // Lifecycle + onUnmounted(() => { + cleanup() + }) + return { // State isLoading, @@ -205,16 +222,21 @@ export function useTicketPurchase() { qrCode, isPaymentPending, isPayingWithWallet, + ticketQRCode, + purchasedTicketId, + showTicketQR, + + // Computed canPurchase, userDisplay, userWallets, hasWalletWithBalance, - + // Actions purchaseTicketForEvent, - payWithWallet, handleOpenLightningWallet, resetPaymentState, cleanup, + generateTicketQRCode } } \ No newline at end of file diff --git a/src/pages/MyTickets.vue b/src/pages/MyTickets.vue index b5c6539..7accfd9 100644 --- a/src/pages/MyTickets.vue +++ b/src/pages/MyTickets.vue @@ -1,6 +1,6 @@