import { useEffect, useCallback, type PropsWithChildren } from 'react'; import { useAtom, useSetAtom } from 'jotai'; import { sipConnectionStatusAtom, sipCallStateAtom, sipCallerNumberAtom, sipIsMutedAtom, sipIsOnHoldAtom, sipCallDurationAtom, sipCallStartTimeAtom, } from '@/state/sip-state'; import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager'; import type { SIPConfig } from '@/types/sip'; const DEFAULT_CONFIG: SIPConfig = { displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent', uri: import.meta.env.VITE_SIP_URI ?? '', password: import.meta.env.VITE_SIP_PASSWORD ?? '', wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '', stunServers: 'stun:stun.l.google.com:19302', }; export const SipProvider = ({ children }: PropsWithChildren) => { const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom); const [callState, setCallState] = useAtom(sipCallStateAtom); const setCallerNumber = useSetAtom(sipCallerNumberAtom); const setCallDuration = useSetAtom(sipCallDurationAtom); const setCallStartTime = useSetAtom(sipCallStartTimeAtom); // Register Jotai setters so the singleton SIP manager can update atoms useEffect(() => { registerSipStateUpdater({ setConnectionStatus, setCallState, setCallerNumber, }); }, [setConnectionStatus, setCallState, setCallerNumber]); // Auto-connect SIP on mount useEffect(() => { connectSip(DEFAULT_CONFIG); }, []); // Call duration timer useEffect(() => { if (callState === 'active') { const start = new Date(); setCallStartTime(start); const interval = window.setInterval(() => { setCallDuration(Math.floor((Date.now() - start.getTime()) / 1000)); }, 1000); return () => clearInterval(interval); } setCallDuration(0); setCallStartTime(null); }, [callState, setCallDuration, setCallStartTime]); // Auto-reset to idle after ended/failed useEffect(() => { if (callState === 'ended' || callState === 'failed') { const timer = setTimeout(() => { setCallState('idle'); setCallerNumber(null); }, 2000); return () => clearTimeout(timer); } }, [callState, setCallState, setCallerNumber]); // Cleanup on page unload useEffect(() => { const handleUnload = () => disconnectSip(); window.addEventListener('beforeunload', handleUnload); return () => window.removeEventListener('beforeunload', handleUnload); }, []); return <>{children}; }; // Hook for components to access SIP actions + state export const useSip = () => { const [connectionStatus] = useAtom(sipConnectionStatusAtom); const [callState] = useAtom(sipCallStateAtom); const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom); const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom); const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom); const [callDuration] = useAtom(sipCallDurationAtom); const makeCall = useCallback((phoneNumber: string) => { getSipClient()?.call(phoneNumber); setCallerNumber(phoneNumber); }, [setCallerNumber]); const answer = useCallback(() => getSipClient()?.answer(), []); const reject = useCallback(() => getSipClient()?.reject(), []); const hangup = useCallback(() => getSipClient()?.hangup(), []); const toggleMute = useCallback(() => { if (isMuted) { getSipClient()?.unmute(); } else { getSipClient()?.mute(); } setIsMuted(!isMuted); }, [isMuted, setIsMuted]); const toggleHold = useCallback(() => { if (isOnHold) { getSipClient()?.unhold(); } else { getSipClient()?.hold(); } setIsOnHold(!isOnHold); }, [isOnHold, setIsOnHold]); return { connectionStatus, callState, callerNumber, isMuted, isOnHold, callDuration, isRegistered: connectionStatus === 'registered', isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState), ozonetelStatus: 'logged-in' as const, ozonetelError: null as string | null, connect: () => connectSip(DEFAULT_CONFIG), disconnect: disconnectSip, makeCall, answer, reject, hangup, toggleMute, toggleHold, }; };