Implement backend API and database services in Docker setup
- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO. - Introduced PostgreSQL and Redis services with health checks and configurations for data persistence. - Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets. - Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage. - Enhanced the Dockerfile to support the new API environment variables and configurations. - Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,96 +4,198 @@
|
||||
<div class="modal-container">
|
||||
<div class="modal-content">
|
||||
<!-- Close Button -->
|
||||
<button @click="closeModal" class="absolute top-4 right-4 text-white/60 hover:text-white transition-colors">
|
||||
<button @click="closeModal" class="absolute top-4 right-4 text-white/60 hover:text-white transition-colors z-10">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Header with Content Info -->
|
||||
<div class="flex gap-6 mb-8">
|
||||
<img
|
||||
v-if="content?.thumbnail"
|
||||
:src="content.thumbnail"
|
||||
:alt="content.title"
|
||||
class="w-32 h-48 object-cover rounded-lg"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">{{ content?.title }}</h2>
|
||||
<p class="text-white/60 text-sm mb-4 line-clamp-3">{{ content?.description }}</p>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-white/80">
|
||||
<span v-if="content?.releaseYear">{{ content.releaseYear }}</span>
|
||||
<span v-if="content?.duration">{{ formatDuration(content.duration) }}</span>
|
||||
<span v-if="content?.rating">{{ content.rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rental Info -->
|
||||
<div class="bg-white/5 rounded-xl p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-white mb-1">Rental Details</h3>
|
||||
<p class="text-white/60 text-sm">48-hour viewing period</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-3xl font-bold text-white">${{ content?.rentalPrice || '4.99' }}</div>
|
||||
<div class="text-white/60 text-sm">One-time payment</div>
|
||||
<!-- ─── INITIAL STATE: Content Info + Rent Button ─── -->
|
||||
<template v-if="paymentState === 'initial'">
|
||||
<!-- Header with Content Info -->
|
||||
<div class="flex gap-6 mb-8">
|
||||
<img
|
||||
v-if="content?.thumbnail"
|
||||
:src="content.thumbnail"
|
||||
:alt="content.title"
|
||||
class="w-32 h-48 object-cover rounded-lg"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">{{ content?.title }}</h2>
|
||||
<p class="text-white/60 text-sm mb-4 line-clamp-3">{{ content?.description }}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-white/80">
|
||||
<span v-if="content?.releaseYear">{{ content.releaseYear }}</span>
|
||||
<span v-if="content?.duration">{{ formatDuration(content.duration) }}</span>
|
||||
<span v-if="content?.rating">{{ content.rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-2 text-sm text-white/80">
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Watch for 48 hours from purchase
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
HD streaming quality
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Stream on any device
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Rental Info -->
|
||||
<div class="bg-white/5 rounded-xl p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-white mb-1">Rental Details</h3>
|
||||
<p class="text-white/60 text-sm">48-hour viewing period</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-3xl font-bold text-white">${{ content?.rentalPrice || '4.99' }}</div>
|
||||
<div class="text-white/60 text-sm">One-time payment</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-white/80">
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Watch for 48 hours from purchase
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
HD streaming quality
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Stream on any device
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="errorMessage" class="mb-4 p-3 rounded-lg bg-red-500/20 border border-red-500/30 text-red-400 text-sm">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
<div v-if="errorMessage" class="mb-4 p-3 rounded-lg bg-red-500/20 border border-red-500/30 text-red-400 text-sm">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Rent Button -->
|
||||
<button
|
||||
@click="handleRent"
|
||||
:disabled="isLoading"
|
||||
class="hero-play-button w-full flex items-center justify-center mb-4"
|
||||
>
|
||||
<span v-if="!isLoading">Rent for ${{ content?.rentalPrice || '4.99' }}</span>
|
||||
<span v-else>Processing...</span>
|
||||
</button>
|
||||
|
||||
<!-- Subscribe Alternative -->
|
||||
<div class="text-center">
|
||||
<p class="text-white/60 text-sm mb-2">Or get unlimited access with a subscription</p>
|
||||
<!-- Rent Button -->
|
||||
<button
|
||||
@click="$emit('openSubscription')"
|
||||
class="text-orange-500 hover:text-orange-400 font-medium text-sm transition-colors"
|
||||
@click="handleRent"
|
||||
:disabled="isLoading"
|
||||
class="hero-play-button w-full flex items-center justify-center mb-4"
|
||||
>
|
||||
View subscription plans →
|
||||
<svg v-if="!isLoading" class="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 10h-3V7H7v3H4v3h3v3h3v-3h3v-3zm8-6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V4zm-2 0l.01 14H5V4h14z"/>
|
||||
</svg>
|
||||
<span v-if="!isLoading">Pay with Lightning</span>
|
||||
<span v-else>Creating invoice...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-white/40 mt-6">
|
||||
Rental starts immediately after payment. No refunds.
|
||||
</p>
|
||||
<!-- Subscribe Alternative -->
|
||||
<div class="text-center">
|
||||
<p class="text-white/60 text-sm mb-2">Or get unlimited access with a subscription</p>
|
||||
<button
|
||||
@click="$emit('openSubscription')"
|
||||
class="text-orange-500 hover:text-orange-400 font-medium text-sm transition-colors"
|
||||
>
|
||||
View subscription plans →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-white/40 mt-6">
|
||||
Rental starts immediately after payment. No refunds.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- ─── INVOICE STATE: QR Code + BOLT11 ─── -->
|
||||
<template v-if="paymentState === 'invoice'">
|
||||
<div class="text-center">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Pay with Lightning</h2>
|
||||
<p class="text-white/60 text-sm mb-6">
|
||||
Scan the QR code or copy the invoice to pay
|
||||
</p>
|
||||
|
||||
<!-- QR Code -->
|
||||
<div class="bg-white rounded-2xl p-4 inline-block mb-6">
|
||||
<img v-if="qrCodeDataUrl" :src="qrCodeDataUrl" alt="Lightning Invoice QR Code" class="w-64 h-64" />
|
||||
<div v-else class="w-64 h-64 flex items-center justify-center text-gray-400">
|
||||
Generating QR...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount in sats -->
|
||||
<div class="mb-4">
|
||||
<div class="text-lg font-bold text-white">
|
||||
{{ formatSats(invoiceData?.sourceAmount?.amount) }} sats
|
||||
</div>
|
||||
<div class="text-sm text-white/60">
|
||||
≈ ${{ content?.rentalPrice || '4.99' }} USD
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiration Countdown -->
|
||||
<div class="mb-6 flex items-center justify-center gap-2 text-sm" :class="countdownSeconds <= 60 ? 'text-red-400' : 'text-white/60'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Expires in {{ formatCountdown(countdownSeconds) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Copy BOLT11 Button -->
|
||||
<button
|
||||
@click="copyInvoice"
|
||||
class="w-full bg-white/10 hover:bg-white/15 text-white rounded-xl px-4 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2 mb-4"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
{{ copyButtonText }}
|
||||
</button>
|
||||
|
||||
<!-- Open in wallet link -->
|
||||
<a
|
||||
:href="'lightning:' + bolt11Invoice"
|
||||
class="block text-center text-orange-500 hover:text-orange-400 text-sm font-medium transition-colors mb-4"
|
||||
>
|
||||
Open in wallet app
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-white/40">
|
||||
Waiting for payment confirmation...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ─── SUCCESS STATE ─── -->
|
||||
<template v-if="paymentState === 'success'">
|
||||
<div class="text-center py-8">
|
||||
<div class="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Payment Confirmed!</h2>
|
||||
<p class="text-white/60 mb-6">Your rental is now active. Enjoy watching!</p>
|
||||
<button
|
||||
@click="handleSuccess"
|
||||
class="hero-play-button w-full flex items-center justify-center"
|
||||
>
|
||||
Start Watching
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ─── EXPIRED STATE ─── -->
|
||||
<template v-if="paymentState === 'expired'">
|
||||
<div class="text-center py-8">
|
||||
<div class="w-20 h-20 bg-yellow-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Invoice Expired</h2>
|
||||
<p class="text-white/60 mb-6">The payment window has closed. Please try again.</p>
|
||||
<button
|
||||
@click="resetAndRetry"
|
||||
class="hero-play-button w-full flex items-center justify-center"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,9 +203,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { libraryService } from '../services/library.service'
|
||||
import type { Content } from '../types/content'
|
||||
import { USE_MOCK } from '../utils/mock'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
@@ -119,20 +223,131 @@ interface Emits {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
type PaymentState = 'initial' | 'invoice' | 'success' | 'expired'
|
||||
|
||||
const paymentState = ref<PaymentState>('initial')
|
||||
const isLoading = ref(false)
|
||||
const errorMessage = ref<string | null>(null)
|
||||
|
||||
// Invoice data
|
||||
const invoiceData = ref<any>(null)
|
||||
const bolt11Invoice = ref('')
|
||||
const qrCodeDataUrl = ref('')
|
||||
const copyButtonText = ref('Copy Invoice')
|
||||
|
||||
// Countdown
|
||||
const countdownSeconds = ref(0)
|
||||
let countdownInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// Polling
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// USE_MOCK imported from utils/mock
|
||||
|
||||
function closeModal() {
|
||||
emit('close')
|
||||
cleanup()
|
||||
paymentState.value = 'initial'
|
||||
errorMessage.value = null
|
||||
invoiceData.value = null
|
||||
bolt11Invoice.value = ''
|
||||
qrCodeDataUrl.value = ''
|
||||
copyButtonText.value = 'Copy Invoice'
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval)
|
||||
countdownInterval = null
|
||||
}
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
pollInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(cleanup)
|
||||
|
||||
// Reset state when modal closes externally
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (!newVal) {
|
||||
cleanup()
|
||||
paymentState.value = 'initial'
|
||||
errorMessage.value = null
|
||||
}
|
||||
})
|
||||
|
||||
function formatDuration(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`
|
||||
}
|
||||
|
||||
function formatSats(amount: string | undefined): string {
|
||||
if (!amount) return '---'
|
||||
// Amount from BTCPay is in BTC, convert to sats
|
||||
const btcAmount = parseFloat(amount)
|
||||
if (isNaN(btcAmount)) return amount
|
||||
const sats = Math.round(btcAmount * 100_000_000)
|
||||
return sats.toLocaleString()
|
||||
}
|
||||
|
||||
function formatCountdown(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
async function generateQRCode(bolt11: string) {
|
||||
try {
|
||||
qrCodeDataUrl.value = await QRCode.toDataURL(bolt11.toUpperCase(), {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
errorCorrectionLevel: 'M',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('QR Code generation failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown(expirationDate: Date | string) {
|
||||
const expiry = new Date(expirationDate).getTime()
|
||||
|
||||
const updateCountdown = () => {
|
||||
const now = Date.now()
|
||||
const remaining = Math.max(0, Math.floor((expiry - now) / 1000))
|
||||
countdownSeconds.value = remaining
|
||||
|
||||
if (remaining <= 0) {
|
||||
cleanup()
|
||||
paymentState.value = 'expired'
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdown()
|
||||
countdownInterval = setInterval(updateCountdown, 1000)
|
||||
}
|
||||
|
||||
function startPolling(contentId: string) {
|
||||
pollInterval = setInterval(async () => {
|
||||
try {
|
||||
if (USE_MOCK) return // mock mode handles differently
|
||||
|
||||
const response = await libraryService.checkRentExists(contentId)
|
||||
if (response.exists) {
|
||||
cleanup()
|
||||
paymentState.value = 'success'
|
||||
}
|
||||
} catch {
|
||||
// Silently retry polling
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
async function handleRent() {
|
||||
if (!props.content) return
|
||||
|
||||
@@ -140,29 +355,74 @@ async function handleRent() {
|
||||
errorMessage.value = null
|
||||
|
||||
try {
|
||||
// Check if we're in development mode
|
||||
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
|
||||
if (useMockData) {
|
||||
// Mock rental for development
|
||||
console.log('🔧 Development mode: Mock rental successful')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
emit('success')
|
||||
closeModal()
|
||||
if (USE_MOCK) {
|
||||
// Simulate mock Lightning invoice
|
||||
console.log('Development mode: Simulating Lightning invoice')
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
const mockBolt11 = 'lnbc50u1pjxyz123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
bolt11Invoice.value = mockBolt11
|
||||
invoiceData.value = {
|
||||
sourceAmount: { amount: '0.00005000', currency: 'BTC' },
|
||||
expiration: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
|
||||
}
|
||||
|
||||
await generateQRCode(mockBolt11)
|
||||
paymentState.value = 'invoice'
|
||||
startCountdown(invoiceData.value.expiration)
|
||||
|
||||
// Simulate payment after 5 seconds in mock mode
|
||||
setTimeout(() => {
|
||||
if (paymentState.value === 'invoice') {
|
||||
cleanup()
|
||||
paymentState.value = 'success'
|
||||
}
|
||||
}, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
// Real API call
|
||||
await libraryService.rentContent(props.content.id)
|
||||
emit('success')
|
||||
closeModal()
|
||||
|
||||
// Real API call — create Lightning invoice
|
||||
const result = await libraryService.rentContent(props.content.id)
|
||||
|
||||
invoiceData.value = result
|
||||
bolt11Invoice.value = result.lnInvoice
|
||||
await generateQRCode(result.lnInvoice)
|
||||
|
||||
paymentState.value = 'invoice'
|
||||
startCountdown(result.expiration)
|
||||
startPolling(props.content.id)
|
||||
} catch (error: any) {
|
||||
errorMessage.value = error.message || 'Rental failed. Please try again.'
|
||||
errorMessage.value = error.message || 'Failed to create invoice. Please try again.'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyInvoice() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(bolt11Invoice.value)
|
||||
copyButtonText.value = 'Copied!'
|
||||
setTimeout(() => {
|
||||
copyButtonText.value = 'Copy Invoice'
|
||||
}, 2000)
|
||||
} catch {
|
||||
copyButtonText.value = 'Copy failed'
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
emit('success')
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function resetAndRetry() {
|
||||
cleanup()
|
||||
paymentState.value = 'initial'
|
||||
errorMessage.value = null
|
||||
invoiceData.value = null
|
||||
bolt11Invoice.value = ''
|
||||
qrCodeDataUrl.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user