diff --git a/app/javascript/dashboard/composables/useCallSession.js b/app/javascript/dashboard/composables/useCallSession.js index 39312eb4d..b2a4b4e6d 100644 --- a/app/javascript/dashboard/composables/useCallSession.js +++ b/app/javascript/dashboard/composables/useCallSession.js @@ -6,6 +6,7 @@ import { useCallsStore } from 'dashboard/stores/calls'; import { useWhatsappCallSession, sendWhatsappTerminateBeacon, + sendWhatsappCallBeacon, cleanupWhatsappSession, } from 'dashboard/composables/useWhatsappCallSession'; import { handleVoiceCallCreated } from 'dashboard/helper/voice'; @@ -69,10 +70,15 @@ export function useCallSession() { }); }; - // pagehide fires after the user confirms the prompt. Let the WhatsApp session - // best-effort sendBeacon a terminate so the server doesn't keep the call open. + // pagehide fires after the user confirms the prompt. Beacon a terminate for + // every WhatsApp call in the store — active OR ringing — since browser-direct + // WebRTC has no rejoin path, so any call that survives the close becomes + // permanently orphaned on Meta's side. const handlePageHide = () => { sendWhatsappTerminateBeacon(); + callsStore.calls + .filter(c => c.provider === 'whatsapp' && !c.isActive && c.callId) + .forEach(c => sendWhatsappCallBeacon(c.callId)); }; const handleTwilioDisconnected = () => callsStore.clearActiveCall(); diff --git a/app/javascript/dashboard/composables/useWhatsappCallSession.js b/app/javascript/dashboard/composables/useWhatsappCallSession.js index dab4cbc0f..c52dba499 100644 --- a/app/javascript/dashboard/composables/useWhatsappCallSession.js +++ b/app/javascript/dashboard/composables/useWhatsappCallSession.js @@ -329,15 +329,26 @@ export const isWhatsappCallMuted = () => { return !tracks[0].enabled; }; -// Best-effort terminate when the tab actually closes after the beforeunload prompt. -export const sendWhatsappTerminateBeacon = () => { - if (!activeCallId || intentionallyClosing) return; +// Best-effort terminate beacon for any WhatsApp call — the backend's terminate +// endpoint handles both ringing and in_progress states (rejecting via terminate +// records 'no_answer' which is the right shape for "agent left the page"). +// Browser-direct WebRTC has no rejoin path, so the only sensible thing on +// page close is to release the call on Meta's side. +export const sendWhatsappCallBeacon = callId => { + if (!callId) return; const accountId = window.location.pathname.split('/')[3]; if (!accountId) return; - const url = `/api/v1/accounts/${accountId}/whatsapp_calls/${activeCallId}/terminate`; + const url = `/api/v1/accounts/${accountId}/whatsapp_calls/${callId}/terminate`; try { navigator.sendBeacon(url); } catch (_) { /* noop */ } }; + +// Backward-compat wrapper for the active-call case — guarded by +// intentionallyClosing so we don't double-terminate after an explicit hangup. +export const sendWhatsappTerminateBeacon = () => { + if (!activeCallId || intentionallyClosing) return; + sendWhatsappCallBeacon(activeCallId); +};