- Introduced asynchronous methods in PaymentService for retrieving user wallets and checking wallet balances, allowing for dual authentication detection. - Updated getUserWalletsAsync, hasWalletWithBalanceAsync, and getWalletWithBalanceAsync methods to streamline wallet access and balance checks. - Refactored useTicketPurchase composable to load user wallets asynchronously on component mount, improving user experience during ticket purchases. - Enhanced error handling and logging for wallet loading and payment processes. These changes improve the reliability and responsiveness of wallet interactions within the payment flow.
263 lines
No EOL
9.4 KiB
Vue
263 lines
No EOL
9.4 KiB
Vue
<!-- eslint-disable vue/multi-word-component-names -->
|
|
<script setup lang="ts">
|
|
import { onUnmounted, watch } from 'vue'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { useTicketPurchase } from '../composables/useTicketPurchase'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
|
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
|
|
|
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 { isAuthenticated, userDisplay } = useAuth()
|
|
const {
|
|
isLoading,
|
|
error,
|
|
paymentHash,
|
|
|
|
qrCode,
|
|
isPaymentPending,
|
|
isPayingWithWallet,
|
|
canPurchase,
|
|
userWallets,
|
|
hasWalletWithBalance,
|
|
purchaseTicketForEvent,
|
|
handleOpenLightningWallet,
|
|
resetPaymentState,
|
|
cleanup,
|
|
ticketQRCode,
|
|
purchasedTicketId,
|
|
showTicketQR,
|
|
loadWallets
|
|
} = useTicketPurchase()
|
|
|
|
async function handlePurchase() {
|
|
if (!canPurchase.value) return
|
|
|
|
try {
|
|
await purchaseTicketForEvent(props.event.id)
|
|
} catch (err) {
|
|
console.error('Error purchasing ticket:', err)
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:isOpen', false)
|
|
resetPaymentState()
|
|
}
|
|
|
|
// Reload wallets when dialog opens
|
|
watch(() => props.isOpen, async (newVal) => {
|
|
if (newVal && isAuthenticated.value) {
|
|
await loadWallets()
|
|
}
|
|
})
|
|
|
|
// Cleanup on unmount
|
|
onUnmounted(() => {
|
|
cleanup()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog :open="isOpen" @update:open="handleClose">
|
|
<DialogContent class="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle class="flex items-center gap-2">
|
|
<CreditCard class="w-5 h-5" />
|
|
Purchase Ticket
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Purchase a ticket for <strong>{{ event.name }}</strong> for {{ formatEventPrice(event.price_per_ticket, event.currency) }}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<!-- 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="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>
|
|
<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">
|
|
{{ formatWalletBalance(wallet.balance_msat) }}
|
|
</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>No funds available, fill your wallet or pay with an external one</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">{{ formatEventPrice(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-if="paymentHash && !showTicketQR" class="py-4 flex flex-col items-center gap-4">
|
|
<div class="text-center space-y-2">
|
|
<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 && qrCode" 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>
|
|
|
|
<!-- Ticket QR Code (After Successful Purchase) -->
|
|
<div v-else-if="showTicketQR && ticketQRCode" class="py-4 flex flex-col items-center gap-4">
|
|
<div class="text-center space-y-2">
|
|
<h3 class="text-lg font-semibold text-green-600">Ticket Purchased Successfully!</h3>
|
|
<p class="text-sm text-muted-foreground">
|
|
Your ticket has been purchased and is now available in your tickets area.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="bg-muted/50 rounded-lg p-4 w-full">
|
|
<div class="text-center space-y-3">
|
|
<div class="flex justify-center">
|
|
<Ticket class="w-12 h-12 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium">Ticket ID</p>
|
|
<div class="bg-background border rounded px-2 py-1 max-w-full mt-1">
|
|
<p class="text-xs font-mono break-all">{{ purchasedTicketId }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 w-full">
|
|
<Button @click="() => $router.push('/my-tickets')" class="w-full">
|
|
View My Tickets
|
|
</Button>
|
|
<Button variant="outline" @click="handleClose" class="w-full">
|
|
Close
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template> |