Enhance payment processing and rental features
- Updated the BTCPay service to support internal Lightning invoices with private route hints, improving payment routing for users with private channels. - Added reconciliation methods for pending rents and subscriptions to ensure missed payments are processed on startup. - Enhanced the rental and subscription services to handle payments in satoshis, aligning with Lightning Network standards. - Improved the rental modal and content detail components to display rental status and pricing more clearly, including a countdown for rental expiration. - Refactored various components to streamline user experience and ensure accurate rental access checks.
This commit is contained in:
@@ -39,7 +39,11 @@
|
||||
<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-3xl font-bold text-white flex items-center gap-1 justify-end">
|
||||
<svg class="w-6 h-6 text-yellow-500" viewBox="0 0 24 24" fill="currentColor"><path d="M13 3l-2 7h5l-6 11 2-7H7l6-11z"/></svg>
|
||||
{{ (content?.rentalPrice || 5000).toLocaleString() }}
|
||||
<span class="text-lg font-normal text-white/60">sats</span>
|
||||
</div>
|
||||
<div class="text-white/60 text-sm">One-time payment</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +81,7 @@
|
||||
class="hero-play-button w-full flex items-center justify-center mb-4"
|
||||
>
|
||||
<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"/>
|
||||
<path d="M13 3l-2 7h5l-6 11 2-7H7l6-11z"/>
|
||||
</svg>
|
||||
<span v-if="!isLoading">Pay with Lightning</span>
|
||||
<span v-else>Creating invoice...</span>
|
||||
@@ -117,11 +121,9 @@
|
||||
|
||||
<!-- 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 class="text-lg font-bold text-white flex items-center justify-center gap-1">
|
||||
<svg class="w-5 h-5 text-yellow-500" viewBox="0 0 24 24" fill="currentColor"><path d="M13 3l-2 7h5l-6 11 2-7H7l6-11z"/></svg>
|
||||
{{ displaySats }} sats
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +205,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { libraryService } from '../services/library.service'
|
||||
import type { Content } from '../types/content'
|
||||
@@ -244,6 +246,19 @@ let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// USE_MOCK imported from utils/mock
|
||||
|
||||
/**
|
||||
* Display sats — prefer the invoice amount (from BTCPay), fall back to content rental price.
|
||||
*/
|
||||
const displaySats = computed(() => {
|
||||
// If we have an invoice with a source amount, use it
|
||||
const sourceAmount = invoiceData.value?.sourceAmount?.amount
|
||||
if (sourceAmount) {
|
||||
return formatSats(sourceAmount)
|
||||
}
|
||||
// Otherwise show the rental price directly (already in sats)
|
||||
return (props.content?.rentalPrice || 5000).toLocaleString()
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
cleanup()
|
||||
paymentState.value = 'initial'
|
||||
@@ -332,13 +347,18 @@ function startCountdown(expirationDate: Date | string) {
|
||||
countdownInterval = setInterval(updateCountdown, 1000)
|
||||
}
|
||||
|
||||
function startPolling(contentId: string) {
|
||||
/**
|
||||
* Poll the quote endpoint for the specific BTCPay invoice.
|
||||
* This also triggers server-side payment detection for route-hint invoices.
|
||||
* Only transitions to 'success' when the backend confirms THIS invoice is paid.
|
||||
*/
|
||||
function startPolling(invoiceId: string) {
|
||||
pollInterval = setInterval(async () => {
|
||||
try {
|
||||
if (USE_MOCK) return // mock mode handles differently
|
||||
if (USE_MOCK) return
|
||||
|
||||
const response = await libraryService.checkRentExists(contentId)
|
||||
if (response.exists) {
|
||||
const quote = await libraryService.pollQuoteStatus(invoiceId)
|
||||
if (quote.paid) {
|
||||
cleanup()
|
||||
paymentState.value = 'success'
|
||||
}
|
||||
@@ -381,8 +401,14 @@ async function handleRent() {
|
||||
return
|
||||
}
|
||||
|
||||
// Real API call — create Lightning invoice
|
||||
const result = await libraryService.rentContent(props.content.id)
|
||||
// Real API call — create Lightning invoice.
|
||||
// Prefer the content entity ID (film.id) for the rental flow.
|
||||
// Fall back to the project ID — the backend guard can resolve it.
|
||||
const contentId = props.content.contentId || props.content.id
|
||||
if (!contentId) {
|
||||
throw new Error('This content is not available for rental yet.')
|
||||
}
|
||||
const result = await libraryService.rentContent(contentId)
|
||||
|
||||
invoiceData.value = result
|
||||
bolt11Invoice.value = result.lnInvoice
|
||||
@@ -390,9 +416,15 @@ async function handleRent() {
|
||||
|
||||
paymentState.value = 'invoice'
|
||||
startCountdown(result.expiration)
|
||||
startPolling(props.content.id)
|
||||
startPolling(result.providerId)
|
||||
} catch (error: any) {
|
||||
errorMessage.value = error.message || 'Failed to create invoice. Please try again.'
|
||||
const status = error?.response?.status || error?.statusCode
|
||||
const serverMsg = error?.response?.data?.message || error?.message || ''
|
||||
if (status === 403 || serverMsg.includes('Forbidden')) {
|
||||
errorMessage.value = 'This content is not available for rental. The rental price may not be set yet.'
|
||||
} else {
|
||||
errorMessage.value = serverMsg || 'Failed to create invoice. Please try again.'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user