"use client"; import React, { createContext, useContext, useEffect, useState, ReactNode, useCallback, } from "react"; import { supabase } from "@/lib/supabase"; import { useAuth } from "@/contexts/AuthContext"; interface UserProfile { displayName: string | null; organisation: string | null; messageCreditsUsed: number; creditsResetDate: string; creditsRemaining: number; tier: string; tabularModel: string; claudeApiKey: string | null; geminiApiKey: string | null; } interface UserProfileContextType { profile: UserProfile | null; loading: boolean; updateDisplayName: (name: string) => Promise; updateOrganisation: (organisation: string) => Promise; updateModelPreference: ( field: "tabularModel", value: string, ) => Promise; updateApiKey: ( provider: "claude" | "gemini", value: string | null, ) => Promise; reloadProfile: () => Promise; incrementMessageCredits: () => Promise; } const UserProfileContext = createContext( undefined, ); export function UserProfileProvider({ children }: { children: ReactNode }) { const { user, isAuthenticated } = useAuth(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const loadProfile = useCallback(async (userId: string) => { try { const { data, error } = await supabase .from("user_profiles") .select("*") .eq("user_id", userId) .single(); // Define credit limit constant const MONTHLY_CREDIT_LIMIT = 999999; // temporarily unlimited // Calculate a default future reset date (30 days from now) const futureResetDate = new Date(); futureResetDate.setDate(futureResetDate.getDate() + 30); const defaultResetDateStr = futureResetDate.toISOString(); if (error) { // Set fallback profile data if profile doesn't exist setProfile({ displayName: null, organisation: null, messageCreditsUsed: 0, creditsResetDate: defaultResetDateStr, creditsRemaining: MONTHLY_CREDIT_LIMIT, tier: "Free", tabularModel: "gemini-3-flash-preview", claudeApiKey: null, geminiApiKey: null, }); return; } // Use fetched data to update profile state if (data) { let creditsUsed = data.message_credits_used; let resetDate = data.credits_reset_date; let creditsRemaining = MONTHLY_CREDIT_LIMIT - creditsUsed; let shouldUpdateDb = false; // Check if credits have expired and need reset if (resetDate && new Date() > new Date(resetDate)) { // Calculate new reset date const newResetDate = new Date(); newResetDate.setDate(newResetDate.getDate() + 30); resetDate = newResetDate.toISOString(); creditsUsed = 0; creditsRemaining = MONTHLY_CREDIT_LIMIT; shouldUpdateDb = true; } // 1. Update local state immediately setProfile({ displayName: data.display_name, organisation: data.organisation ?? null, messageCreditsUsed: creditsUsed, creditsResetDate: resetDate, creditsRemaining: creditsRemaining, tier: data.tier || "Free", tabularModel: data.tabular_model || "gemini-3-flash-preview", claudeApiKey: data.claude_api_key ?? null, geminiApiKey: data.gemini_api_key ?? null, }); // 2. Update database in background if needed if (shouldUpdateDb) { supabase .from("user_profiles") .update({ message_credits_used: 0, credits_reset_date: resetDate, updated_at: new Date().toISOString(), }) .eq("user_id", userId) .then(({ error }) => { if (error) console.error( "Failed to auto-reset credits", error, ); }); } } } catch (e) { // Calculate a default future reset date for fallback const futureResetDate = new Date(); futureResetDate.setDate(futureResetDate.getDate() + 30); // Set fallback profile data on exception setProfile({ displayName: null, organisation: null, messageCreditsUsed: 0, creditsResetDate: futureResetDate.toISOString(), creditsRemaining: 999999, // temporarily unlimited tier: "Free", tabularModel: "gemini-3-flash-preview", claudeApiKey: null, geminiApiKey: null, }); } finally { setLoading(false); } }, []); useEffect(() => { if (isAuthenticated && user) { setLoading(true); loadProfile(user.id); } else { setProfile(null); setLoading(false); } }, [isAuthenticated, user, loadProfile]); const updateDisplayName = useCallback( async (displayName: string): Promise => { if (!user) { return false; } try { const { error } = await supabase .from("user_profiles") .update({ display_name: displayName, updated_at: new Date().toISOString(), }) .eq("user_id", user.id); if (error) { throw error; } setProfile((prev) => (prev ? { ...prev, displayName } : null)); return true; } catch { return false; } }, [user], ); const updateOrganisation = useCallback( async (organisation: string): Promise => { if (!user) return false; try { const { error } = await supabase .from("user_profiles") .update({ organisation, updated_at: new Date().toISOString(), }) .eq("user_id", user.id); if (error) throw error; setProfile((prev) => prev ? { ...prev, organisation } : null, ); return true; } catch { return false; } }, [user], ); const updateModelPreference = useCallback( async ( field: "tabularModel", value: string, ): Promise => { if (!user) return false; const dbField = field === "tabularModel" ? "tabular_model" : ""; if (!dbField) return false; try { const { error } = await supabase .from("user_profiles") .update({ [dbField]: value, updated_at: new Date().toISOString(), }) .eq("user_id", user.id); if (error) throw error; setProfile((prev) => prev ? { ...prev, [field]: value } : null, ); return true; } catch { return false; } }, [user], ); const updateApiKey = useCallback( async ( provider: "claude" | "gemini", value: string | null, ): Promise => { if (!user) return false; const dbField = provider === "claude" ? "claude_api_key" : "gemini_api_key"; const stateField = provider === "claude" ? "claudeApiKey" : "geminiApiKey"; const normalized = value?.trim() ? value.trim() : null; try { const { error } = await supabase .from("user_profiles") .update({ [dbField]: normalized, updated_at: new Date().toISOString(), }) .eq("user_id", user.id); if (error) throw error; setProfile((prev) => prev ? { ...prev, [stateField]: normalized } : null, ); return true; } catch { return false; } }, [user], ); const reloadProfile = useCallback(async () => { if (user) { await loadProfile(user.id); } }, [user, loadProfile]); const incrementMessageCredits = useCallback(async (): Promise => { if (!user || !profile) { return false; } // Check if user has credits remaining if (profile.creditsRemaining <= 0) { return false; } try { const newCreditsUsed = profile.messageCreditsUsed + 1; const { error } = await supabase .from("user_profiles") .update({ message_credits_used: newCreditsUsed, updated_at: new Date().toISOString(), }) .eq("user_id", user.id); if (error) { throw error; } // Update local state setProfile((prev) => prev ? { ...prev, messageCreditsUsed: newCreditsUsed, creditsRemaining: 999999 - newCreditsUsed, // temporarily unlimited } : null, ); return true; } catch (err) { return false; } }, [user, profile]); return ( {children} ); } export function useUserProfile() { const context = useContext(UserProfileContext); if (context === undefined) { throw new Error( "useUserProfile must be used within a UserProfileProvider", ); } return context; }