Files
indee-demo/src/stores/content.ts
Dorian d1ac281ad9 feat: enhance webhook event handling and improve profile fetching logic
- Updated the WebhooksService to differentiate between 'InvoiceSettled' and 'InvoicePaymentSettled' events, ensuring accurate payment confirmation and logging.
- Enhanced the useAccounts composable to improve display name handling by skipping generic placeholder names and ensuring the most recent profile metadata is fetched from multiple relays.
- Modified the IndeehubApiService to handle JWT token refresh failures gracefully, allowing public endpoints to function without authentication.
- Updated content store logic to fetch all published projects from the public API, ensuring backstage content is visible to all users regardless of their active content source.

These changes improve the reliability of payment processing, enhance user profile representation, and ensure content visibility across the application.
2026-02-14 12:05:32 +00:00

335 lines
12 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Content } from '../types/content'
import { indeeHubFilms, bitcoinFilms, documentaries, dramas } from '../data/indeeHubFilms'
import { topDocFilms, topDocBitcoin, topDocCrypto, topDocMoney, topDocEconomics } from '../data/topDocFilms'
import { contentService } from '../services/content.service'
import { mapApiProjectsToContents } from '../utils/mappers'
import { useContentSourceStore } from './contentSource'
import { indeehubApiService } from '../services/indeehub-api.service'
// useFilmmaker import removed — mergePublishedFilmmakerProjects
// now fetches from the public API so all users see backstage content
import { USE_MOCK as USE_MOCK_DATA } from '../utils/mock'
export const useContentStore = defineStore('content', () => {
const featuredContent = ref<Content | null>(null)
const contentRows = ref<{ [key: string]: Content[] }>({
featured: [],
newReleases: [],
bitcoin: [],
documentaries: [],
dramas: [],
independent: []
})
const loading = ref(false)
const error = ref<string | null>(null)
/**
* Fetch content from the original API (external IndeeHub)
*/
async function fetchContentFromApi() {
try {
const projects = await contentService.getProjects({ status: 'published' })
if (projects.length === 0) {
// 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)
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
featuredContent.value = featuredFilm
const films = allContent.filter(c => c.type === 'film')
const bitcoinContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
)
const docs = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('documentary'))
)
const dramaContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('drama'))
)
contentRows.value = {
featured: allContent.slice(0, 10),
newReleases: films.slice(0, 8),
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
documentaries: docs.length > 0 ? docs : films.slice(0, 6),
dramas: dramaContent.length > 0 ? dramaContent : films.slice(0, 6),
independent: films.slice(0, 10)
}
} catch (err) {
console.error('API fetch failed:', err)
throw err
}
}
/**
* Fetch content from our self-hosted IndeeHub API
*/
async function fetchContentFromIndeehubApi() {
try {
const response = await indeehubApiService.getProjects({ fresh: true })
// Handle both array responses and wrapped responses like { data: [...] }
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
if (!Array.isArray(projects) || projects.length === 0) {
// 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.
// 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.film?.rentalPrice ?? p.rentalPrice,
status: p.status,
categories: p.genre ? [p.genre.name] : [],
streamingUrl: p.streamingUrl || p.partnerStreamUrl || undefined,
apiData: {
deliveryMode: p.deliveryMode,
partnerStreamUrl: p.partnerStreamUrl,
partnerDashUrl: p.partnerDashUrl,
partnerFairplayUrl: p.partnerFairplayUrl,
partnerDrmToken: p.partnerDrmToken,
},
}))
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
featuredContent.value = featuredFilm
const films = allContent.filter(c => c.type === 'film')
const bitcoinContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
)
const docs = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('documentary'))
)
const dramaContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('drama'))
)
contentRows.value = {
featured: allContent.slice(0, 10),
newReleases: films.slice(0, 8),
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
documentaries: docs.length > 0 ? docs : allContent.slice(0, 10),
dramas: dramaContent.length > 0 ? dramaContent : films.slice(0, 6),
independent: films.slice(0, 10)
}
} catch (err) {
console.error('IndeeHub API fetch failed:', err)
throw err
}
}
/**
* Fetch IndeeHub mock content (original catalog)
*/
function fetchIndeeHubMock() {
const godBlessBitcoin = bitcoinFilms.find(f => f.title === 'God Bless Bitcoin') || bitcoinFilms[0]
if (godBlessBitcoin) {
featuredContent.value = {
...godBlessBitcoin,
backdrop: '/images/god-bless-bitcoin-backdrop.jpg'
}
} else {
featuredContent.value = indeeHubFilms[0]
}
contentRows.value = {
featured: indeeHubFilms.slice(0, 10),
newReleases: indeeHubFilms.slice(0, 8).reverse(),
bitcoin: bitcoinFilms,
documentaries: documentaries.slice(0, 10),
dramas: dramas.slice(0, 10),
independent: indeeHubFilms.filter(f =>
!f.categories.includes('Bitcoin') && !f.categories.includes('Documentary')
).slice(0, 10)
}
}
/**
* Fetch TopDocumentaryFilms mock content
*/
function fetchTopDocMock() {
featuredContent.value = topDocFilms[0]
contentRows.value = {
featured: topDocFilms.slice(0, 10),
newReleases: [...topDocFilms].sort((a, b) => (b.releaseYear ?? 0) - (a.releaseYear ?? 0)).slice(0, 8),
bitcoin: topDocBitcoin,
documentaries: topDocEconomics.slice(0, 10),
dramas: topDocMoney,
independent: topDocCrypto.slice(0, 10)
}
}
/**
* Fetch ALL published projects from the public API and merge them
* into the existing content rows so backstage content appears for
* every user, regardless of which content source is active.
*
* Ownership is determined by comparing the project's filmmaker ID
* to the current user's filmmaker profile. Only the actual creator
* sees the "my movie" flag — not other logged-in users.
*/
async function mergePublishedFilmmakerProjects() {
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
if (!apiUrl) return // No backend configured
try {
// Fetch ALL published projects from the public API endpoint.
// This works for any user (authenticated or not) — no auth required.
// Use fresh: true to bypass the backend's 5-minute cache
// so newly published backstage content appears immediately.
const response = await indeehubApiService.getProjects({ fresh: true })
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
if (!Array.isArray(projects) || projects.length === 0) return
// Determine the current user's filmmaker ID (if any) for
// ownership tagging. Import lazily to avoid circular deps.
let currentFilmmakerId: string | null = null
try {
const { useAuthStore } = await import('./auth')
const authStore = useAuthStore()
currentFilmmakerId = authStore.user?.filmmaker?.id ?? null
} catch {
// Auth store not ready — ownership will be unknown
}
const publishedContent: Content[] = projects.map((p: any) => {
const projectFilmmakerId = p.filmmaker?.id ?? p.filmmakerId ?? null
return {
id: p.id,
contentId: p.film?.id,
title: p.title || p.name,
description: p.synopsis || '',
thumbnail: p.poster || '/images/placeholder-poster.jpg',
backdrop: p.poster || '/images/placeholder-poster.jpg',
type: p.type === 'episodic' ? 'series' as const : 'film' as const,
rating: p.format || undefined,
releaseYear: p.releaseDate ? new Date(p.releaseDate).getFullYear() : new Date().getFullYear(),
categories: p.genres?.map((g: any) => g.name) || [],
slug: p.slug,
rentalPrice: p.film?.rentalPrice ?? p.rentalPrice,
status: p.status,
apiData: p,
// Only mark as "own project" if the current user is the actual filmmaker
isOwnProject: !!(currentFilmmakerId && projectFilmmakerId && currentFilmmakerId === projectFilmmakerId),
}
})
console.debug(
'[content-store] Merging API published projects:',
publishedContent.map(c => ({ id: c.id, title: c.title, isOwn: c.isOwnProject })),
)
// Merge into each content row (prepend so they appear first)
for (const key of Object.keys(contentRows.value)) {
const existingIds = new Set(contentRows.value[key].map(c => c.id))
const newItems = publishedContent.filter(c => !existingIds.has(c.id))
if (newItems.length > 0) {
contentRows.value[key] = [...newItems, ...contentRows.value[key]]
}
}
// If no featured content yet, use the first published project
if (!featuredContent.value && publishedContent.length > 0) {
featuredContent.value = publishedContent[0]
}
} catch (err) {
console.warn('[content-store] Failed to merge API published projects:', err)
}
}
/**
* Route to the correct loader based on the active content source
*/
async function fetchContentFromMock() {
const sourceStore = useContentSourceStore()
if (sourceStore.activeSource === 'topdocfilms') {
fetchTopDocMock()
} else {
fetchIndeeHubMock()
}
// Also include any projects published through the backstage
await mergePublishedFilmmakerProjects()
}
/**
* Main fetch content method.
* Respects the content-source toggle:
* - 'indeehub-api' → self-hosted backend API
* - 'topdocfilms' → TopDoc mock catalog (YouTube documentaries)
* - 'indeehub' → IndeeHub mock catalog
*/
async function fetchContent() {
loading.value = true
error.value = null
try {
const sourceStore = useContentSourceStore()
const apiUrl = import.meta.env.VITE_INDEEHUB_API_URL || ''
if (USE_MOCK_DATA) {
await new Promise(resolve => setTimeout(resolve, 100))
await fetchContentFromMock()
} else if (sourceStore.activeSource === 'indeehub-api' && apiUrl) {
// Self-hosted backend API
await fetchContentFromIndeehubApi()
await mergePublishedFilmmakerProjects()
} else if (sourceStore.activeSource === 'topdocfilms') {
// TopDoc curated catalog (free YouTube documentaries)
fetchTopDocMock()
await mergePublishedFilmmakerProjects()
} else if (sourceStore.activeSource === 'indeehub') {
// IndeeHub mock catalog
fetchIndeeHubMock()
await mergePublishedFilmmakerProjects()
} else if (apiUrl) {
// Fallback to API if source is unknown but API is configured
await fetchContentFromIndeehubApi()
await mergePublishedFilmmakerProjects()
} else {
await fetchContentFromApi()
await mergePublishedFilmmakerProjects()
}
} catch (e: any) {
error.value = e.message || 'Failed to load content'
console.error('Content fetch error:', e)
// Fallback to mock data on error
console.log('Falling back to mock data...')
await fetchContentFromMock()
} finally {
loading.value = false
}
}
return {
featuredContent,
contentRows,
loading,
error,
fetchContent
}
})