import type { D3roSupabaseClient, Subscription } from '@d3ro/api-client' import type { SupabaseClient } from '@supabase/supabase-js' export interface DashboardRecentEntry { id: string title: string | null text: string durationSeconds: number mode: string createdAt: string } export interface DashboardStats { totalSessions: number totalRecordingSeconds: number totalWordCount: number todaySessions: number todayRecordingSeconds: number todayWordCount: number streakDays: number recentHistory: DashboardRecentEntry[] generatedAt: string } export interface DashboardSubscription { tier: Subscription['tier'] status: string | null provider: Subscription['provider'] overageCredits: number currentPeriodEnd: string | null cancelAt: string | null autoRenewing: boolean | null } export interface DashboardSnapshot { stats: DashboardStats subscription: DashboardSubscription } export type DashboardClientErrorCode = | 'auth' | 'invalid-response' | 'network' | 'not-initialized' | 'server' export class DashboardClientError extends Error { constructor( public readonly code: DashboardClientErrorCode, message: string ) { super(message) this.name = 'DashboardClientError' } } const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const TIERS = new Set(['free', 'pro', 'pro_plus']) const PROVIDERS = new Set(['none', 'stripe', 'payple', 'google_play', 'app_store', 'admin']) function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function nonNegativeNumber(value: unknown): number | null { const numeric = typeof value === 'number' ? value : Number(value) return Number.isFinite(numeric) && numeric >= 0 ? numeric : null } function nonNegativeInteger(value: unknown): number | null { const numeric = nonNegativeNumber(value) return numeric !== null && Number.isSafeInteger(numeric) ? numeric : null } function normalizeDate(value: unknown, nullable = false): string | null { if (nullable && value === null) return null if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) return null return new Date(value).toISOString() } export function normalizeDashboardStats(value: unknown): DashboardStats { if (!isRecord(value) || !Array.isArray(value.recent_history)) { throw new DashboardClientError('invalid-response', 'Dashboard response shape is invalid') } const totalSessions = nonNegativeInteger(value.total_sessions) const totalRecordingSeconds = nonNegativeNumber(value.total_recording_seconds) const totalWordCount = nonNegativeInteger(value.total_word_count) const todaySessions = nonNegativeInteger(value.today_sessions) const todayRecordingSeconds = nonNegativeNumber(value.today_recording_seconds) const todayWordCount = nonNegativeInteger(value.today_word_count) const streakDays = nonNegativeInteger(value.streak_days) const generatedAt = normalizeDate(value.generated_at) if ( totalSessions === null || totalRecordingSeconds === null || totalWordCount === null || todaySessions === null || todayRecordingSeconds === null || todayWordCount === null || streakDays === null || generatedAt === null ) { throw new DashboardClientError('invalid-response', 'Dashboard metrics are invalid') } const recentHistory = value.recent_history.map((candidate): DashboardRecentEntry => { if (!isRecord(candidate)) { throw new DashboardClientError('invalid-response', 'Dashboard history row is invalid') } const durationSeconds = nonNegativeNumber(candidate.duration_seconds) const createdAt = normalizeDate(candidate.created_at) if ( typeof candidate.id !== 'string' || (candidate.title !== null && typeof candidate.title !== 'string') || typeof candidate.text !== 'string' || typeof candidate.mode !== 'string' || durationSeconds === null || createdAt === null ) { throw new DashboardClientError('invalid-response', 'Dashboard history row is invalid') } return { id: candidate.id, title: candidate.title, text: candidate.text, durationSeconds, mode: candidate.mode, createdAt } }) return { totalSessions, totalRecordingSeconds, totalWordCount, todaySessions, todayRecordingSeconds, todayWordCount, streakDays, recentHistory, generatedAt } } function normalizeSubscription(value: unknown): DashboardSubscription { if (!isRecord(value)) { throw new DashboardClientError('not-initialized', 'Subscription row is missing') } const overageCredits = nonNegativeInteger(value.overage_credits) const periodEnd = normalizeDate(value.current_period_end, true) const cancelAt = normalizeDate(value.cancel_at, true) if ( typeof value.tier !== 'string' || !TIERS.has(value.tier) || (value.status !== null && typeof value.status !== 'string') || typeof value.provider !== 'string' || !PROVIDERS.has(value.provider) || overageCredits === null || (value.current_period_end !== null && periodEnd === null) || (value.cancel_at !== null && cancelAt === null) || (value.auto_renewing !== null && typeof value.auto_renewing !== 'boolean') ) { throw new DashboardClientError('invalid-response', 'Subscription response shape is invalid') } return { tier: value.tier as Subscription['tier'], status: value.status, provider: value.provider as Subscription['provider'], overageCredits, currentPeriodEnd: periodEnd, cancelAt, autoRenewing: value.auto_renewing } } function mapDashboardError(error: unknown): DashboardClientError { if (error instanceof DashboardClientError) return error const candidate = error as { code?: unknown; message?: unknown } const databaseCode = typeof candidate?.code === 'string' ? candidate.code : '' const message = typeof candidate?.message === 'string' ? candidate.message : 'Dashboard request failed' const normalized = message.toLowerCase() if ( error instanceof TypeError || normalized.includes('failed to fetch') || normalized.includes('network request failed') || normalized.includes('networkerror') ) { return new DashboardClientError('network', message) } if (databaseCode === 'PGRST301' || databaseCode === '42501' || normalized.includes('jwt')) { return new DashboardClientError('auth', message) } return new DashboardClientError('server', message) } export async function loadDashboardSnapshot( client: D3roSupabaseClient, userId: string ): Promise { if (!UUID_PATTERN.test(userId)) { throw new DashboardClientError('auth', 'A valid authenticated user is required') } try { const untypedClient = client as unknown as SupabaseClient const [statsResult, subscriptionResult] = await Promise.all([ untypedClient.rpc('mobile_dashboard_stats'), client .from('subscriptions') .select('tier,status,provider,overage_credits,current_period_end,cancel_at,auto_renewing') .eq('user_id', userId) .maybeSingle() ]) if (statsResult.error) throw statsResult.error if (subscriptionResult.error) throw subscriptionResult.error if (!subscriptionResult.data) { throw new DashboardClientError('not-initialized', 'Subscription row is missing') } return { stats: normalizeDashboardStats(statsResult.data), subscription: normalizeSubscription(subscriptionResult.data) } } catch (error) { throw mapDashboardError(error) } }