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:
@@ -32,7 +32,10 @@ export const useContentStore = defineStore('content', () => {
|
||||
const projects = await contentService.getProjects({ status: 'published' })
|
||||
|
||||
if (projects.length === 0) {
|
||||
throw new Error('No content available')
|
||||
// No published content yet — not an error, use placeholder content
|
||||
console.info('No published content from API, using placeholder content.')
|
||||
await fetchContentFromMock()
|
||||
return
|
||||
}
|
||||
|
||||
const allContent = mapApiProjectsToContents(projects)
|
||||
@@ -76,19 +79,25 @@ export const useContentStore = defineStore('content', () => {
|
||||
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
|
||||
|
||||
if (!Array.isArray(projects) || projects.length === 0) {
|
||||
throw new Error('No content available from IndeeHub API')
|
||||
// No published content yet — not an error, just use mock/placeholder data
|
||||
console.info('No published content in backend yet, using placeholder content.')
|
||||
await fetchContentFromMock()
|
||||
return
|
||||
}
|
||||
|
||||
// Map API projects to frontend Content format
|
||||
// Map API projects to frontend Content format.
|
||||
// The backend returns projects with a nested `film` object (the Content entity).
|
||||
// We need the content ID (film.id) for the rental/payment flow.
|
||||
const allContent: Content[] = projects.map((p: any) => ({
|
||||
id: p.id,
|
||||
contentId: p.film?.id,
|
||||
title: p.title,
|
||||
description: p.synopsis || '',
|
||||
thumbnail: p.poster || '',
|
||||
backdrop: p.poster || '',
|
||||
type: p.type || 'film',
|
||||
slug: p.slug,
|
||||
rentalPrice: p.rentalPrice,
|
||||
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
|
||||
status: p.status,
|
||||
categories: p.genre ? [p.genre.name] : [],
|
||||
streamingUrl: p.streamingUrl || p.partnerStreamUrl || undefined,
|
||||
@@ -168,15 +177,57 @@ export const useContentStore = defineStore('content', () => {
|
||||
/**
|
||||
* Convert published filmmaker projects to Content format and merge
|
||||
* them into the existing content rows so they appear on the browse page.
|
||||
*
|
||||
* If the filmmaker projects haven't been loaded yet (e.g. first visit
|
||||
* is the homepage, not backstage), attempt to fetch them first — but
|
||||
* only when the user has an active session.
|
||||
*/
|
||||
function mergePublishedFilmmakerProjects() {
|
||||
async function mergePublishedFilmmakerProjects() {
|
||||
try {
|
||||
const { projects } = useFilmmaker()
|
||||
const published = projects.value.filter(p => p.status === 'published')
|
||||
const filmmaker = useFilmmaker()
|
||||
|
||||
// If projects aren't loaded yet, check whether the user has
|
||||
// session tokens. We check sessionStorage directly because
|
||||
// authStore.initialize() may still be in-flight on first load.
|
||||
if (filmmaker.projects.value.length === 0) {
|
||||
const nostrToken = sessionStorage.getItem('nostr_token')
|
||||
const cognitoToken = sessionStorage.getItem('auth_token')
|
||||
const hasSession = !!nostrToken || !!cognitoToken
|
||||
|
||||
if (hasSession) {
|
||||
// Always sync nip98Service tokens on page load. The token
|
||||
// may exist in sessionStorage but its expiry record may be
|
||||
// stale, causing nip98Service.accessToken to return null.
|
||||
// Re-storing refreshes the expiry so the interceptor can
|
||||
// include the token; if it really expired the backend 401
|
||||
// interceptor will auto-refresh via the refresh token.
|
||||
if (nostrToken) {
|
||||
const { nip98Service } = await import('../services/nip98.service')
|
||||
nip98Service.storeTokens(
|
||||
nostrToken,
|
||||
sessionStorage.getItem('indeehub_api_refresh') ?? sessionStorage.getItem('refresh_token') ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await filmmaker.fetchProjects()
|
||||
} catch (err) {
|
||||
console.warn('[content-store] Failed to fetch filmmaker projects for merge:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const published = filmmaker.projects.value.filter(p => p.status === 'published')
|
||||
if (published.length === 0) return
|
||||
|
||||
console.debug(
|
||||
'[content-store] Merging published projects:',
|
||||
published.map(p => ({ id: p.id, title: p.title, filmId: p.film?.id, rentalPrice: p.film?.rentalPrice ?? p.rentalPrice })),
|
||||
)
|
||||
|
||||
const publishedContent: Content[] = published.map(p => ({
|
||||
id: p.id,
|
||||
contentId: p.film?.id,
|
||||
title: p.title || p.name,
|
||||
description: p.synopsis || '',
|
||||
thumbnail: p.poster || '/images/placeholder-poster.jpg',
|
||||
@@ -186,7 +237,7 @@ export const useContentStore = defineStore('content', () => {
|
||||
releaseYear: p.releaseDate ? new Date(p.releaseDate).getFullYear() : new Date().getFullYear(),
|
||||
categories: p.genres?.map(g => g.name) || [],
|
||||
slug: p.slug,
|
||||
rentalPrice: p.rentalPrice,
|
||||
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
|
||||
status: p.status,
|
||||
apiData: p,
|
||||
}))
|
||||
@@ -213,7 +264,7 @@ export const useContentStore = defineStore('content', () => {
|
||||
/**
|
||||
* Route to the correct loader based on the active content source
|
||||
*/
|
||||
function fetchContentFromMock() {
|
||||
async function fetchContentFromMock() {
|
||||
const sourceStore = useContentSourceStore()
|
||||
if (sourceStore.activeSource === 'topdocfilms') {
|
||||
fetchTopDocMock()
|
||||
@@ -221,30 +272,37 @@ export const useContentStore = defineStore('content', () => {
|
||||
fetchIndeeHubMock()
|
||||
}
|
||||
|
||||
// In mock mode, also include any projects published through the backstage
|
||||
mergePublishedFilmmakerProjects()
|
||||
// Also include any projects published through the backstage
|
||||
await mergePublishedFilmmakerProjects()
|
||||
}
|
||||
|
||||
/**
|
||||
* Main fetch content method
|
||||
* Main fetch content method.
|
||||
* When USE_MOCK is false and the self-hosted API URL is configured,
|
||||
* always try the self-hosted backend first (regardless of the
|
||||
* content-source toggle, which only affects mock catalogues).
|
||||
*/
|
||||
async function fetchContent() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const sourceStore = useContentSourceStore()
|
||||
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
|
||||
|
||||
if (sourceStore.activeSource === 'indeehub-api' && !USE_MOCK_DATA) {
|
||||
// Fetch from our self-hosted backend (only when backend is actually running)
|
||||
await fetchContentFromIndeehubApi()
|
||||
} else if (USE_MOCK_DATA) {
|
||||
if (USE_MOCK_DATA) {
|
||||
// Use mock data in development or when flag is set
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
fetchContentFromMock()
|
||||
await fetchContentFromMock()
|
||||
} else if (apiUrl) {
|
||||
// Self-hosted backend is configured — always prefer it
|
||||
await fetchContentFromIndeehubApi()
|
||||
// Also merge filmmaker's published projects that may not be in the
|
||||
// public results yet (e.g. content still transcoding)
|
||||
await mergePublishedFilmmakerProjects()
|
||||
} else {
|
||||
// Fetch from original API
|
||||
// No self-hosted backend — try external API
|
||||
await fetchContentFromApi()
|
||||
await mergePublishedFilmmakerProjects()
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Failed to load content'
|
||||
@@ -252,7 +310,7 @@ export const useContentStore = defineStore('content', () => {
|
||||
|
||||
// Fallback to mock data on error
|
||||
console.log('Falling back to mock data...')
|
||||
fetchContentFromMock()
|
||||
await fetchContentFromMock()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user