feat: Ozonetel SIP integration — agent login, SIP registration, inbound call flow working

Known issues to fix:
- Must refresh page after each call (SIP session not resetting to idle properly)
- Decline/reject call not working
- Caller ID shows DID instead of original caller (Ozonetel IVR config issue with call:cid)
- Socket.IO reconnect noise in console (sidecar not running)
This commit is contained in:
2026-03-17 21:20:32 +05:30
parent d54d54f5f3
commit 36b3a5d34d

View File

@@ -1,25 +1,106 @@
import { createContext, useContext, useEffect, useRef, type PropsWithChildren } from 'react'; import { createContext, useContext, useEffect, useRef, useState, type PropsWithChildren } from 'react';
import { useSipPhone } from '@/hooks/use-sip-phone'; import { useSipPhone } from '@/hooks/use-sip-phone';
type SipContextType = ReturnType<typeof useSipPhone>; const SIDECAR_URL = import.meta.env.VITE_SIDECAR_URL ?? 'http://localhost:4100';
type SipContextType = ReturnType<typeof useSipPhone> & {
ozonetelStatus: 'idle' | 'logging-in' | 'logged-in' | 'error';
ozonetelError: string | null;
};
const SipContext = createContext<SipContextType | null>(null); const SipContext = createContext<SipContextType | null>(null);
export const SipProvider = ({ children }: PropsWithChildren) => { export const SipProvider = ({ children }: PropsWithChildren) => {
const sipPhone = useSipPhone(); const sipPhone = useSipPhone();
const hasConnected = useRef(false); const hasConnected = useRef(false);
const [ozonetelStatus, setOzonetelStatus] = useState<'idle' | 'logging-in' | 'logged-in' | 'error'>('idle');
const [ozonetelError, setOzonetelError] = useState<string | null>(null);
// Auto-connect on mount — skip StrictMode double-fire // Auto-connect SIP + login to Ozonetel on mount
useEffect(() => { useEffect(() => {
if (!hasConnected.current) { if (!hasConnected.current) {
hasConnected.current = true; hasConnected.current = true;
// 1. Connect SIP (WebSocket → Ozonetel SIP server)
sipPhone.connect(); sipPhone.connect();
// 2. Login agent to Ozonetel's routing system
// Try sidecar first, fallback to direct Ozonetel API call
const sipId = (import.meta.env.VITE_SIP_URI ?? '').replace('sip:', '').split('@')[0];
const agentId = import.meta.env.VITE_OZONETEL_AGENT_ID ?? 'Agent3';
const agentPassword = import.meta.env.VITE_OZONETEL_AGENT_PASSWORD ?? 'Test123$';
const accountId = import.meta.env.VITE_OZONETEL_ACCOUNT_ID ?? 'global_demo';
const apiKey = import.meta.env.VITE_OZONETEL_API_KEY ?? '';
if (sipId) {
setOzonetelStatus('logging-in');
const loginViaOzonetelDirect = async () => {
const params = new URLSearchParams({
userName: accountId,
apiKey: apiKey,
phoneNumber: sipId,
action: 'login',
mode: 'blended',
state: 'Ready',
});
const response = await fetch(
`https://in1-ccaas-api.ozonetel.com/CAServices/AgentAuthenticationV2/index.php`,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(`${agentId}:${agentPassword}`),
},
body: params.toString(),
},
);
return response.json();
};
const loginViaSidecar = async () => {
const response = await fetch(`${SIDECAR_URL}/api/ozonetel/agent-login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId,
password: agentPassword,
phoneNumber: sipId,
mode: 'blended',
}),
});
return response.json();
};
// Try sidecar first, fallback to direct
loginViaSidecar()
.catch(() => loginViaOzonetelDirect())
.then(data => {
if (data.status === 'success' || data.message?.includes('logged in')) {
setOzonetelStatus('logged-in');
console.log('Ozonetel agent login:', data.message);
} else {
setOzonetelStatus('error');
setOzonetelError(data.message ?? 'Unknown error');
console.warn('Ozonetel agent login issue:', data);
}
})
.catch(err => {
setOzonetelStatus('error');
setOzonetelError(err.message);
console.error('Ozonetel agent login failed:', err);
});
}
} }
// Do NOT disconnect on cleanup — the SIP connection should persist
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return <SipContext.Provider value={sipPhone}>{children}</SipContext.Provider>; return (
<SipContext.Provider value={{ ...sipPhone, ozonetelStatus, ozonetelError }}>
{children}
</SipContext.Provider>
);
}; };
export const useSip = (): SipContextType => { export const useSip = (): SipContextType => {