import { useState, useEffect, useCallback } from 'react'; import { useDubs } from '../provider'; import type { PromoCredit } from '../types'; export interface UseCreditsResult { credits: PromoCredit[]; totalLamports: number; totalSOL: number; loading: boolean; error: Error | null; refetch: () => Promise; } // Module-level pub/sub so every active useCredits instance can refetch // when ANY component spends or unlocks a credit. Without this each hook // keeps its own state, and a sponsored bet placed via useJoinGame leaves // the home-screen rewards widget + the streak sheet stuck on stale // counts until the next 60s poll. const creditListeners = new Set<() => void>(); /** * Notify every mounted useCredits hook to refetch from the server. * Called automatically by useJoinGame / useCreateGame after a sponsored * op succeeds; can also be called manually after any flow that mutates * credits server-side (e.g. shaking through a 1000-dub milestone). */ export function emitCreditsChanged(): void { for (const fn of creditListeners) fn(); } /** * Returns every active promo credit reserved to the authenticated user * (streak-milestone unlocks + any unused Twitter giveaway codes). * * Credits are kept on the server (`promo_codes` table); this hook just * mirrors them. Refetches automatically whenever any other useCredits * caller emits via emitCreditsChanged() — keeps every credit-aware UI * surface consistent in real time. */ export function useCredits(): UseCreditsResult { const { client } = useDubs(); const [credits, setCredits] = useState([]); const [totalLamports, setTotalLamports] = useState(0); const [totalSOL, setTotalSOL] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetch = useCallback(async () => { setLoading(true); setError(null); try { const result = await client.getCredits(); setCredits(result.credits); setTotalLamports(result.totalLamports); setTotalSOL(result.totalSOL); } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); } finally { setLoading(false); } }, [client]); useEffect(() => { fetch(); }, [fetch]); // Subscribe to global credit-change events so this instance refetches // whenever another component (or the join/create mutation) signals a // credit was spent or minted. useEffect(() => { creditListeners.add(fetch); return () => { creditListeners.delete(fetch); }; }, [fetch]); return { credits, totalLamports, totalSOL, loading, error, refetch: fetch }; }