import { createContext, useContext, useState, useEffect, useRef, useCallback, ReactNode } from 'react'; type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error'; interface SettingsContextType { crtEnabled: boolean; setCrtEnabled: (enabled: boolean) => void; soundEnabled: boolean; setSoundEnabled: (enabled: boolean) => void; cryptoConsent: boolean | null; // null = never asked this session setCryptoConsent: (consent: boolean) => void; playSound: (type: SoundType) => void; hashrate: number; setHashrate: (rate: number) => void; totalHashes: number; setTotalHashes: (hashes: number) => void; acceptedHashes: number; setAcceptedHashes: (hashes: number) => void; audioBlocked: boolean; resetAudioContext: () => void; } const SettingsContext = createContext(undefined); export const SettingsProvider = ({ children }: { children: ReactNode }) => { const [crtEnabled, setCrtEnabled] = useState(() => { const saved = localStorage.getItem('crtEnabled'); return saved !== null ? JSON.parse(saved) : true; }); const [soundEnabled, setSoundEnabled] = useState(() => { const saved = localStorage.getItem('soundEnabled'); return saved !== null ? JSON.parse(saved) : true; }); // Crypto consent: null = needs prompt, true = accepted, false = declined // On session start, if user previously declined, we reset to null to re-prompt const [cryptoConsent, setCryptoConsentState] = useState(() => { const saved = localStorage.getItem('cryptoConsent'); if (saved === null) return null; // Never set const parsed = JSON.parse(saved); // If declined (false), return null to re-prompt on new session // We use sessionStorage to track if we've already shown the prompt this session const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted'); if (parsed === false && !sessionPrompted) { return null; // Re-prompt } return parsed; }); const setCryptoConsent = (consent: boolean) => { setCryptoConsentState(consent); localStorage.setItem('cryptoConsent', JSON.stringify(consent)); sessionStorage.setItem('cryptoConsentPrompted', 'true'); }; const [audioBlocked, setAudioBlocked] = useState(false); const [audioFailCount, setAudioFailCount] = useState(0); const [hashrate, setHashrate] = useState(0); const [totalHashes, setTotalHashes] = useState(0); const [acceptedHashes, setAcceptedHashes] = useState(0); // Single AudioContext instance, persisted across renders const audioContextRef = useRef(null); // Use ref to always have current soundEnabled value in callbacks const soundEnabledRef = useRef(soundEnabled); useEffect(() => { soundEnabledRef.current = soundEnabled; }, [soundEnabled]); // Get or create AudioContext const getAudioContext = useCallback(() => { if (!audioContextRef.current) { audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); } // Resume if suspended (browser autoplay policy) if (audioContextRef.current.state === 'suspended') { audioContextRef.current.resume(); } return audioContextRef.current; }, []); // Cleanup on unmount useEffect(() => { return () => { if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } }; }, []); useEffect(() => { localStorage.setItem('crtEnabled', JSON.stringify(crtEnabled)); }, [crtEnabled]); useEffect(() => { localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled)); }, [soundEnabled]); // Reset audio context to try again after user interaction const resetAudioContext = useCallback(() => { if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } setAudioBlocked(false); setAudioFailCount(0); }, []); const playSound = useCallback((type: SoundType) => { // Use ref to ensure we always have the latest value if (!soundEnabledRef.current) return; try { const audioContext = getAudioContext(); // Check if context is blocked if (audioContext.state === 'suspended') { audioContext.resume().catch(() => { setAudioFailCount(prev => { const newCount = prev + 1; if (newCount >= 3) { setAudioBlocked(true); } return newCount; }); }); return; } const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); const now = audioContext.currentTime; switch (type) { case 'click': oscillator.frequency.value = 800; gainNode.gain.value = 0.1; oscillator.start(now); oscillator.stop(now + 0.05); break; case 'beep': oscillator.frequency.value = 1200; gainNode.gain.value = 0.08; oscillator.start(now); oscillator.stop(now + 0.1); break; case 'hover': oscillator.frequency.value = 600; gainNode.gain.value = 0.05; oscillator.start(now); oscillator.stop(now + 0.03); break; case 'boot': oscillator.type = 'sawtooth'; oscillator.frequency.setValueAtTime(200, now); oscillator.frequency.exponentialRampToValueAtTime(800, now + 0.2); gainNode.gain.setValueAtTime(0.1, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3); oscillator.start(now); oscillator.stop(now + 0.3); break; case 'success': oscillator.frequency.setValueAtTime(800, now); oscillator.frequency.setValueAtTime(1200, now + 0.1); gainNode.gain.value = 0.1; oscillator.start(now); oscillator.stop(now + 0.2); break; case 'error': oscillator.type = 'square'; oscillator.frequency.value = 150; gainNode.gain.value = 0.08; oscillator.start(now); oscillator.stop(now + 0.15); break; } // Reset fail count on success if (audioFailCount > 0) { setAudioFailCount(0); setAudioBlocked(false); } } catch (e) { console.warn('Audio playback failed:', e); setAudioFailCount(prev => { const newCount = prev + 1; if (newCount >= 3) { setAudioBlocked(true); } return newCount; }); } }, [getAudioContext, audioFailCount]); return ( {children} ); }; export const useSettings = () => { const context = useContext(SettingsContext); if (context === undefined) { throw new Error('useSettings must be used within a SettingsProvider'); } return context; };