From e6a35193f3a98e929ac369381b238fab6af256e0 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Tue, 5 May 2026 14:15:40 +0700 Subject: [PATCH] fix(voice): align outbound WhatsApp call lifecycle with real pickup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pile of related fixes around the dashboard's WhatsApp call flow: - ConversationHeader / Contacts/VoiceCallButton: drop the immediate setCallActive at initiate time. The call sits in incomingCalls (callDirection: outbound) until the backend signals real pickup, so the duration timer never starts pre-pickup. Phone button is disabled whenever there is an active or incoming call. - FloatingCallWidget: * Loop a ringtone (bell.mp3) for inbound ringing only. * Hide the green Join button for outbound — the agent has nothing to "join", and clicking it routed through acceptIncomingCall → prepareInboundAnswer → cleanup() and tore down the live outbound session before the API 409 ("already accepted by another agent"). * Auto-join watcher skips whatsapp outbound (Twilio's joinConference flow only). - useCallSession: * joinCall short-circuits for outbound calls — defense-in-depth so no future surface can re-trigger the destroyed-session bug. * endCall + outbound rejectIncomingCall pass call.callId to endActiveCall, so terminate fires even if module state was wiped. - useWhatsappCallSession: * New recorderArmed flag, reset by cleanup. ontrack only calls setupRecorder when armed. * Inbound's acceptIncomingCall arms the recorder before the API round-trip (agent click = pickup). * armOutboundRecorder exported for the cable handler when ACCEPTED arrives. * endActiveCall accepts a callIdOverride to fall back when the module's activeCallId was nulled by an earlier cleanup. - actionCable: * Split the cable contract: outbound_connected only applies the SDP answer (tunnel-up signal); outbound_accepted (new) is the real pickup signal — flips active and arms the recorder. --- .../Contacts/VoiceCallButton.vue | 3 +- .../components/widgets/FloatingCallWidget.vue | 44 ++++++++- .../conversation/ConversationHeader.vue | 12 ++- .../dashboard/composables/useCallSession.js | 24 ++++- .../composables/useWhatsappCallSession.js | 93 ++++++++++++------- .../dashboard/helper/actionCable.js | 24 ++++- 6 files changed, 156 insertions(+), 44 deletions(-) diff --git a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue index 2ffbac7ff..5d13b1ee1 100644 --- a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue +++ b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue @@ -93,6 +93,8 @@ const startWhatsappCall = async inboxId => { } const callsStore = useCallsStore(); + // Stay non-active until the connect cable event arrives — flipping to active + // here would start the duration timer before the contact picks up. callsStore.addCall({ callSid: response.call_id, callId: response.id, @@ -101,7 +103,6 @@ const startWhatsappCall = async inboxId => { callDirection: 'outbound', provider: 'whatsapp', }); - callsStore.setCallActive(response.call_id); useAlert(t('CONTACT_PANEL.CALL_INITIATED')); navigateToConversation(conversationId); diff --git a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue index fe22eb1eb..d711c7fb4 100644 --- a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue +++ b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue @@ -1,5 +1,5 @@