Enhance deployment script and update package dependencies
- Added detailed labels to the deployment script for IndeedHub, including title, version, description, license, icon, and repository URL. - Updated package dependencies in package.json and package-lock.json, including upgrading 'nostr-tools' to version 2.23.0 and adding 'axios' and '@tanstack/vue-query'. - Improved README with a modern description of the platform and updated project structure details. This commit enhances the clarity of the deployment process and ensures the project is using the latest dependencies for better performance and features.
This commit is contained in:
267
src/services/api.service.ts
Normal file
267
src/services/api.service.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'
|
||||
import { apiConfig } from '../config/api.config'
|
||||
import type { ApiError } from '../types/api'
|
||||
|
||||
/**
|
||||
* Base API Service
|
||||
* Handles HTTP requests, token management, and error handling
|
||||
*/
|
||||
class ApiService {
|
||||
private client: AxiosInstance
|
||||
private tokenRefreshPromise: Promise<string> | null = null
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
this.setupInterceptors()
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup request and response interceptors
|
||||
*/
|
||||
private setupInterceptors() {
|
||||
// Request interceptor - Add auth token
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
// Response interceptor - Handle errors and token refresh
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError<ApiError>) => {
|
||||
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }
|
||||
|
||||
// Handle 401 - Token expired
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
|
||||
try {
|
||||
const newToken = await this.refreshToken()
|
||||
if (newToken && originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
return this.client(originalRequest)
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Token refresh failed - clear auth and redirect to login
|
||||
this.clearAuth()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return Promise.reject(this.handleError(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication token
|
||||
*/
|
||||
private getToken(): string | null {
|
||||
// Check session storage first (Cognito JWT)
|
||||
const cognitoToken = sessionStorage.getItem('auth_token')
|
||||
if (cognitoToken) return cognitoToken
|
||||
|
||||
// Check for Nostr session token
|
||||
const nostrToken = sessionStorage.getItem('nostr_token')
|
||||
if (nostrToken) return nostrToken
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication token
|
||||
*/
|
||||
public setToken(token: string, type: 'cognito' | 'nostr' = 'cognito') {
|
||||
if (type === 'cognito') {
|
||||
sessionStorage.setItem('auth_token', token)
|
||||
} else {
|
||||
sessionStorage.setItem('nostr_token', token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication
|
||||
*/
|
||||
public clearAuth() {
|
||||
sessionStorage.removeItem('auth_token')
|
||||
sessionStorage.removeItem('nostr_token')
|
||||
sessionStorage.removeItem('refresh_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh authentication token
|
||||
*/
|
||||
private async refreshToken(): Promise<string> {
|
||||
// Prevent multiple simultaneous refresh requests
|
||||
if (this.tokenRefreshPromise) {
|
||||
return this.tokenRefreshPromise
|
||||
}
|
||||
|
||||
this.tokenRefreshPromise = (async () => {
|
||||
try {
|
||||
const refreshToken = sessionStorage.getItem('refresh_token')
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available')
|
||||
}
|
||||
|
||||
// Call refresh endpoint (implement based on backend)
|
||||
const response = await axios.post(`${apiConfig.baseURL}/auth/refresh`, {
|
||||
refreshToken,
|
||||
})
|
||||
|
||||
const newToken = response.data.accessToken
|
||||
this.setToken(newToken, 'cognito')
|
||||
|
||||
if (response.data.refreshToken) {
|
||||
sessionStorage.setItem('refresh_token', response.data.refreshToken)
|
||||
}
|
||||
|
||||
return newToken
|
||||
} finally {
|
||||
this.tokenRefreshPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return this.tokenRefreshPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle and normalize API errors
|
||||
*/
|
||||
private handleError(error: AxiosError<ApiError>): ApiError {
|
||||
if (error.response) {
|
||||
// Server responded with error
|
||||
return {
|
||||
message: error.response.data?.message || 'An error occurred',
|
||||
statusCode: error.response.status,
|
||||
error: error.response.data?.error,
|
||||
details: error.response.data?.details,
|
||||
}
|
||||
} else if (error.request) {
|
||||
// Request made but no response
|
||||
return {
|
||||
message: 'Unable to connect to server. Please check your internet connection.',
|
||||
statusCode: 0,
|
||||
error: 'NETWORK_ERROR',
|
||||
}
|
||||
} else {
|
||||
// Something else happened
|
||||
return {
|
||||
message: error.message || 'An unexpected error occurred',
|
||||
statusCode: 0,
|
||||
error: 'UNKNOWN_ERROR',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry logic for failed requests
|
||||
*/
|
||||
private async retryRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
retries: number = apiConfig.maxRetries
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (retries > 0 && this.shouldRetry(error as AxiosError)) {
|
||||
await this.delay(apiConfig.retryDelay)
|
||||
return this.retryRequest(fn, retries - 1)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if request should be retried
|
||||
*/
|
||||
private shouldRetry(error: AxiosError): boolean {
|
||||
if (!apiConfig.enableRetry) return false
|
||||
|
||||
// Retry on network errors or 5xx server errors
|
||||
return (
|
||||
!error.response ||
|
||||
(error.response.status >= 500 && error.response.status < 600)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay helper for retry logic
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
if (apiConfig.enableRetry) {
|
||||
return this.retryRequest(async () => {
|
||||
const response = await this.client.get<T>(url, config)
|
||||
return response.data
|
||||
})
|
||||
}
|
||||
const response = await this.client.get<T>(url, config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.post<T>(url, data, config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
public async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.put<T>(url, data, config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH request
|
||||
*/
|
||||
public async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.patch<T>(url, data, config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.delete<T>(url, config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CDN URL for media assets
|
||||
*/
|
||||
public getCdnUrl(path: string): string {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return `${apiConfig.cdnURL}${path}`
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiService = new ApiService()
|
||||
147
src/services/auth.service.ts
Normal file
147
src/services/auth.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { apiService } from './api.service'
|
||||
import type {
|
||||
LoginCredentials,
|
||||
RegisterData,
|
||||
AuthResponse,
|
||||
NostrSessionRequest,
|
||||
NostrSessionResponse,
|
||||
ApiUser,
|
||||
} from '../types/api'
|
||||
|
||||
/**
|
||||
* Authentication Service
|
||||
* Handles Cognito and Nostr authentication
|
||||
*/
|
||||
class AuthService {
|
||||
/**
|
||||
* Login with email and password (Cognito)
|
||||
*/
|
||||
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
||||
try {
|
||||
const response = await apiService.post<AuthResponse>('/auth/login', credentials)
|
||||
|
||||
// Store tokens
|
||||
if (response.accessToken) {
|
||||
apiService.setToken(response.accessToken, 'cognito')
|
||||
if (response.refreshToken) {
|
||||
sessionStorage.setItem('refresh_token', response.refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new user
|
||||
*/
|
||||
async register(data: RegisterData): Promise<AuthResponse> {
|
||||
return apiService.post<AuthResponse>('/auth/register', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authenticated user
|
||||
*/
|
||||
async getCurrentUser(): Promise<ApiUser> {
|
||||
return apiService.get<ApiUser>('/auth/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate current session
|
||||
*/
|
||||
async validateSession(): Promise<boolean> {
|
||||
try {
|
||||
await apiService.post('/auth/validate-session')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
apiService.clearAuth()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Nostr session
|
||||
*/
|
||||
async createNostrSession(request: NostrSessionRequest): Promise<NostrSessionResponse> {
|
||||
const response = await apiService.post<NostrSessionResponse>('/auth/nostr/session', request)
|
||||
|
||||
// Store Nostr token
|
||||
if (response.token) {
|
||||
apiService.setToken(response.token, 'nostr')
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Nostr session
|
||||
*/
|
||||
async refreshNostrSession(pubkey: string, signature: string): Promise<NostrSessionResponse> {
|
||||
return apiService.post<NostrSessionResponse>('/auth/nostr/refresh', {
|
||||
pubkey,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Nostr pubkey to existing account
|
||||
*/
|
||||
async linkNostrPubkey(pubkey: string, signature: string): Promise<ApiUser> {
|
||||
return apiService.post<ApiUser>('/auth/nostr/link', {
|
||||
pubkey,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink Nostr pubkey from account
|
||||
*/
|
||||
async unlinkNostrPubkey(): Promise<ApiUser> {
|
||||
return apiService.post<ApiUser>('/auth/nostr/unlink')
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize OTP flow
|
||||
*/
|
||||
async initOtp(email: string): Promise<void> {
|
||||
await apiService.post('/auth/otp/init', { email })
|
||||
}
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*/
|
||||
async forgotPassword(email: string): Promise<void> {
|
||||
await apiService.post('/auth/forgot-password', { email })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with code
|
||||
*/
|
||||
async resetPassword(email: string, code: string, newPassword: string): Promise<void> {
|
||||
await apiService.post('/auth/reset-password', {
|
||||
email,
|
||||
code,
|
||||
newPassword,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm email with verification code
|
||||
*/
|
||||
async confirmEmail(email: string, code: string): Promise<void> {
|
||||
await apiService.post('/auth/confirm-email', {
|
||||
email,
|
||||
code,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService()
|
||||
111
src/services/content.service.ts
Normal file
111
src/services/content.service.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { apiService } from './api.service'
|
||||
import type { ApiProject, ApiContent } from '../types/api'
|
||||
|
||||
/**
|
||||
* Content Service
|
||||
* Handles projects and content data
|
||||
*/
|
||||
class ContentService {
|
||||
/**
|
||||
* Get all published projects with optional filters
|
||||
*/
|
||||
async getProjects(filters?: {
|
||||
type?: 'film' | 'episodic' | 'music-video'
|
||||
status?: string
|
||||
genre?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
}): Promise<ApiProject[]> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (filters?.type) params.append('type', filters.type)
|
||||
if (filters?.status) params.append('status', filters.status)
|
||||
if (filters?.genre) params.append('genre', filters.genre)
|
||||
if (filters?.limit) params.append('limit', filters.limit.toString())
|
||||
if (filters?.page) params.append('page', filters.page.toString())
|
||||
|
||||
const url = `/projects${params.toString() ? `?${params.toString()}` : ''}`
|
||||
return apiService.get<ApiProject[]>(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project by ID
|
||||
*/
|
||||
async getProjectById(id: string): Promise<ApiProject> {
|
||||
return apiService.get<ApiProject>(`/projects/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project by slug
|
||||
*/
|
||||
async getProjectBySlug(slug: string): Promise<ApiProject> {
|
||||
return apiService.get<ApiProject>(`/projects/slug/${slug}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content by ID
|
||||
*/
|
||||
async getContentById(id: string): Promise<ApiContent> {
|
||||
return apiService.get<ApiContent>(`/contents/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contents for a project
|
||||
*/
|
||||
async getContentsByProject(projectId: string): Promise<ApiContent[]> {
|
||||
return apiService.get<ApiContent[]>(`/contents/project/${projectId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streaming URL for content (requires subscription or rental)
|
||||
*/
|
||||
async getStreamingUrl(contentId: string): Promise<{ url: string; drmToken?: string }> {
|
||||
return apiService.get<{ url: string; drmToken?: string }>(`/contents/${contentId}/stream`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search projects
|
||||
*/
|
||||
async searchProjects(query: string, filters?: {
|
||||
type?: string
|
||||
genre?: string
|
||||
}): Promise<ApiProject[]> {
|
||||
const params = new URLSearchParams()
|
||||
params.append('q', query)
|
||||
|
||||
if (filters?.type) params.append('type', filters.type)
|
||||
if (filters?.genre) params.append('genre', filters.genre)
|
||||
|
||||
return apiService.get<ApiProject[]>(`/projects/search?${params.toString()}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured content (top-rated, recent releases)
|
||||
*/
|
||||
async getFeaturedContent(): Promise<ApiProject[]> {
|
||||
return apiService.get<ApiProject[]>('/projects?status=published&featured=true')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get genres
|
||||
*/
|
||||
async getGenres(): Promise<Array<{ id: string; name: string; slug: string }>> {
|
||||
return apiService.get('/genres')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get festivals
|
||||
*/
|
||||
async getFestivals(): Promise<Array<{ id: string; name: string; slug: string }>> {
|
||||
return apiService.get('/festivals')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get awards
|
||||
*/
|
||||
async getAwards(): Promise<Array<{ id: string; name: string; slug: string }>> {
|
||||
return apiService.get('/awards')
|
||||
}
|
||||
}
|
||||
|
||||
export const contentService = new ContentService()
|
||||
145
src/services/library.service.ts
Normal file
145
src/services/library.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { apiService } from './api.service'
|
||||
import type { ApiRent, ApiContent } from '../types/api'
|
||||
|
||||
/**
|
||||
* Library Service
|
||||
* Handles user library and rentals
|
||||
*/
|
||||
class LibraryService {
|
||||
/**
|
||||
* Get user's library (subscribed + rented content)
|
||||
*/
|
||||
async getUserLibrary(): Promise<{
|
||||
subscribed: ApiContent[]
|
||||
rented: ApiRent[]
|
||||
continueWatching: Array<{ content: ApiContent; progress: number }>
|
||||
}> {
|
||||
// Check if we're in development mode
|
||||
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
|
||||
|
||||
if (useMockData) {
|
||||
// Mock library data for development
|
||||
console.log('🔧 Development mode: Using mock library data')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// Import mock film data
|
||||
const { indeeHubFilms, bitcoinFilms } = await import('../data/indeeHubFilms')
|
||||
const allFilms = [...indeeHubFilms, ...bitcoinFilms]
|
||||
|
||||
// Create mock API content from our film data
|
||||
const mockApiContent = allFilms.slice(0, 20).map((film) => ({
|
||||
id: film.id,
|
||||
projectId: film.id,
|
||||
title: film.title,
|
||||
synopsis: film.description,
|
||||
file: `/content/${film.id}/video.mp4`,
|
||||
status: 'ready' as const,
|
||||
rentalPrice: 4.99,
|
||||
poster: film.thumbnail,
|
||||
metadata: { duration: film.duration },
|
||||
isRssEnabled: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}))
|
||||
|
||||
// Mock continue watching (first 3 films with progress)
|
||||
const continueWatching = mockApiContent.slice(0, 3).map((content, index) => ({
|
||||
content,
|
||||
progress: [35, 67, 12][index], // Different progress percentages
|
||||
}))
|
||||
|
||||
// Mock rented content (2 films with expiry)
|
||||
const rented: ApiRent[] = mockApiContent.slice(3, 5).map((content) => ({
|
||||
id: 'rent-' + content.id,
|
||||
userId: 'mock-user',
|
||||
contentId: content.id,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
|
||||
createdAt: new Date().toISOString(),
|
||||
content,
|
||||
}))
|
||||
|
||||
// All subscribed content (full catalog access)
|
||||
const subscribed = mockApiContent
|
||||
|
||||
return {
|
||||
subscribed,
|
||||
rented,
|
||||
continueWatching,
|
||||
}
|
||||
}
|
||||
|
||||
// Real API call
|
||||
return apiService.get('/library')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rented content
|
||||
*/
|
||||
async getRentedContent(): Promise<ApiRent[]> {
|
||||
return apiService.get<ApiRent[]>('/rents')
|
||||
}
|
||||
|
||||
/**
|
||||
* Rent content
|
||||
*/
|
||||
async rentContent(contentId: string, paymentMethodId?: string): Promise<ApiRent> {
|
||||
return apiService.post<ApiRent>('/rents', {
|
||||
contentId,
|
||||
paymentMethodId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to content
|
||||
*/
|
||||
async checkContentAccess(contentId: string): Promise<{
|
||||
hasAccess: boolean
|
||||
method?: 'subscription' | 'rental'
|
||||
expiresAt?: string
|
||||
}> {
|
||||
try {
|
||||
return await apiService.get(`/contents/${contentId}/access`)
|
||||
} catch {
|
||||
return { hasAccess: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content to watch later list
|
||||
*/
|
||||
async addToWatchLater(contentId: string): Promise<void> {
|
||||
await apiService.post('/library/watch-later', { contentId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove content from watch later list
|
||||
*/
|
||||
async removeFromWatchLater(contentId: string): Promise<void> {
|
||||
await apiService.delete(`/library/watch-later/${contentId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update watch progress
|
||||
*/
|
||||
async updateWatchProgress(contentId: string, progress: number, duration: number): Promise<void> {
|
||||
await apiService.post('/library/progress', {
|
||||
contentId,
|
||||
progress,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get watch progress for content
|
||||
*/
|
||||
async getWatchProgress(contentId: string): Promise<{ progress: number; duration: number } | null> {
|
||||
try {
|
||||
return await apiService.get(`/library/progress/${contentId}`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const libraryService = new LibraryService()
|
||||
114
src/services/subscription.service.ts
Normal file
114
src/services/subscription.service.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { apiService } from './api.service'
|
||||
import type { ApiSubscription } from '../types/api'
|
||||
|
||||
/**
|
||||
* Subscription Service
|
||||
* Handles user subscriptions
|
||||
*/
|
||||
class SubscriptionService {
|
||||
/**
|
||||
* Get user's subscriptions
|
||||
*/
|
||||
async getSubscriptions(): Promise<ApiSubscription[]> {
|
||||
return apiService.get<ApiSubscription[]>('/subscriptions')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active subscription
|
||||
*/
|
||||
async getActiveSubscription(): Promise<ApiSubscription | null> {
|
||||
const subscriptions = await this.getSubscriptions()
|
||||
return subscriptions.find((sub) => sub.status === 'active') || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a tier
|
||||
*/
|
||||
async subscribe(data: {
|
||||
tier: 'enthusiast' | 'film-buff' | 'cinephile'
|
||||
period: 'monthly' | 'annual'
|
||||
paymentMethodId?: string
|
||||
}): Promise<ApiSubscription> {
|
||||
return apiService.post<ApiSubscription>('/subscriptions', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel subscription
|
||||
*/
|
||||
async cancelSubscription(subscriptionId: string): Promise<void> {
|
||||
await apiService.delete(`/subscriptions/${subscriptionId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume cancelled subscription
|
||||
*/
|
||||
async resumeSubscription(subscriptionId: string): Promise<ApiSubscription> {
|
||||
return apiService.post<ApiSubscription>(`/subscriptions/${subscriptionId}/resume`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment method
|
||||
*/
|
||||
async updatePaymentMethod(subscriptionId: string, paymentMethodId: string): Promise<void> {
|
||||
await apiService.patch(`/subscriptions/${subscriptionId}/payment-method`, {
|
||||
paymentMethodId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription tiers with pricing
|
||||
*/
|
||||
async getSubscriptionTiers(): Promise<Array<{
|
||||
tier: string
|
||||
name: string
|
||||
monthlyPrice: number
|
||||
annualPrice: number
|
||||
features: string[]
|
||||
}>> {
|
||||
// This might be a static endpoint or hardcoded
|
||||
// Adjust based on actual API
|
||||
return [
|
||||
{
|
||||
tier: 'enthusiast',
|
||||
name: 'Enthusiast',
|
||||
monthlyPrice: 9.99,
|
||||
annualPrice: 99.99,
|
||||
features: [
|
||||
'Access to all films and series',
|
||||
'HD streaming',
|
||||
'Watch on 2 devices',
|
||||
'Cancel anytime',
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'film-buff',
|
||||
name: 'Film Buff',
|
||||
monthlyPrice: 19.99,
|
||||
annualPrice: 199.99,
|
||||
features: [
|
||||
'Everything in Enthusiast',
|
||||
'4K streaming',
|
||||
'Watch on 4 devices',
|
||||
'Exclusive behind-the-scenes content',
|
||||
'Early access to new releases',
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'cinephile',
|
||||
name: 'Cinephile',
|
||||
monthlyPrice: 29.99,
|
||||
annualPrice: 299.99,
|
||||
features: [
|
||||
'Everything in Film Buff',
|
||||
'Watch on unlimited devices',
|
||||
'Offline downloads',
|
||||
'Director commentary tracks',
|
||||
'Virtual festival access',
|
||||
'Support independent filmmakers',
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const subscriptionService = new SubscriptionService()
|
||||
Reference in New Issue
Block a user