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:
Dorian
2026-02-12 23:24:25 +00:00
parent cdd24a5def
commit 0da83f461c
39 changed files with 1182 additions and 270 deletions

View File

@@ -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
}