From 772734840d03938f7bd0ec0f5fa37146c21b5002 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Sun, 3 May 2026 16:07:55 +0700 Subject: [PATCH] fix(voice): terminate ringing WhatsApp calls on page close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser-direct WebRTC has no rejoin path, so any call that outlives the agent's tab becomes permanently orphaned on Meta's side. The existing pagehide beacon only covered the active (accepted) call — extend it to every ringing inbound too, so a hard refresh during ringing releases the call instead of leaving Meta to time it out. - useWhatsappCallSession: factor sendWhatsappCallBeacon(callId) out of the active-call wrapper so any caller can post terminate for any callId without going through the activeCallId / intentionallyClosing guard. - useCallSession.handlePageHide: after the active-call beacon, iterate the calls store for any ringing WhatsApp call and beacon /terminate for each. terminate is the right endpoint because the backend records it as 'no_answer' when the call was still ringing — accurate UX shape. --- .../dashboard/composables/useCallSession.js | 10 ++++++++-- .../composables/useWhatsappCallSession.js | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) 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); +};