Currently i have integrated agent(from browser) to client(phone) call integration using twilio js sdk but now i want to integrate hold first call, add new call and merge call feature i have tried everything but no resource or conclusions found.
Following my browser js
// Fetch Twilio token from PHP backend
async function fetchToken(agentId, conductedFrom) {
try {
const response = await fetch(`${crm_url}/twilio-generate-in-browser-call-token.php`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: "agentId=" + agentId + "&conductedFrom=" + conductedFrom
});
if (!response.ok) {
throw new Error('Failed to fetch token');
}
const data = await response.json();
return data.token;
} catch (error) {
console.error('Error fetching token:', error);
alert('Failed to fetch token. Please try again.');
return null;
}
}
// Global variables
let twilioDevice = null;
let activeConnection = null;
let callStartTime = null;
let timerInterval = null;
// Initialize Twilio Client
async function initializeTwilio(agentId, conductedFrom) {
const token = await fetchToken(agentId, conductedFrom);
if (!token) {
console.error("Token generation failed");
return;
}
try {
twilioDevice = new Twilio.Device(token, {
codecPreferences: ['opus', 'pcmu'],
enableRingingState: true,
debug: true, // Enable debugging logs
closeProtection: true // Prevent accidental page close during calls
});
// console.log('Twilio Device initialized:', twilioDevice.on);
twilioDevice.on('registered', () => {
console.log('Twilio Device is registered and ready to make calls');
});
twilioDevice.on('error', (error) => {
console.error('Twilio.Device Error:', error);
updateCallStatus('Error: ' + error.message);
});
twilioDevice.on('incoming', (connection) => {
console.log('Incoming call from:', connection.parameters.From);
updateCallStatus('Incoming call...');
showIncomingCallModal(connection);
jQuery("#newCallingDialerModal").modal('hide');
});
twilioDevice.on('disconnect', (connection) => {
console.log('Call disconnected');
updateCallStatus('Call ended');
stopTimer();
activeConnection = null; // Reset the active connection
jQuery('#newCallingStatusModal').modal('hide'); // Close the modal immediately
jQuery("#newCallingDialerModal").modal('hide');
jQuery('#newCallingIncomingCallModal').modal('hide');
});
twilioDevice.on('offline', () => {
console.log('Twilio device is offline');
jQuery('#newCallingIncomingCallModal').modal('hide');
});
// Listen for the 'tokenWillExpire' event there is 1 hr time limit but even for fallback
twilioDevice.on('tokenWillExpire', async () => {
refreshTwilioToken(agentId, conductedFrom);
});
// Check if registration is successful, and if not, try to explicitly call register()
await new Promise(resolve => {
twilioDevice.on('registered', resolve);
setTimeout(() => {
console.log("Device didn't register automatically, trying explicit register...");
twilioDevice.register();
}, 3000); // If not registered within 5 seconds, try explicit register()
});
} catch (error) {
console.error("Error initializing Twilio Device:", error);
}
}
async function updateParentCallStatus(callSid = null, directionFrom = "outgoing") {
if (callSid == null) {
console.error('CallSid Needed');
return;
}
try {
const response = await fetch(`${crm_url}/twilio-current-call-status-callback.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: "firstCallUpdateUpdateSid=" + callSid + "&directionFrom=" + directionFrom
});
const data = await response.text();
console.log('Parent call status updated:', data);
} catch (error) {
console.error('Error updating parent call status:', error);
}
}
// Initialize Twilio when the page loads
initializeTwilio(agentId, conductedFrom);
// Make an outgoing call
jQuery(document).on('click', '.phone_dialpad_call_btn', async function () {
const thisElement = jQuery(this);
const dialpadParent = thisElement.closest("div.phone_dialpad_parent");
const countryCode = dialpadParent.find('select.phone_dialpad_countrycode_select_val').val();
const phoneNumber = dialpadParent.find('input.phone_dialpad_number_val').val();
if (!countryCode) {
alert('Select country code');
return;
}
if (!phoneNumber || !phoneNumber.match(/^\+?[1-9]\d{1,14}$/)) {
alert('Please enter a valid phone number');
return;
}
let dialingNumberString = countryCode + phoneNumber;
const token = await fetchToken(agentId, conductedFrom);
if (!token) {
console.log('Failed to fetch token');
return;
}
// Ensure Twilio Device is initialized
if (!twilioDevice) {
await initializeTwilio(agentId, conductedFrom);
if (!twilioDevice) {
console.error('Twilio Device initialization failed');
return;
}
}
try {
// Initiate the call
activeConnection = await twilioDevice.connect({
params: { To: dialingNumberString }
});
updateCallStatus('Calling...');
jQuery('#newCallingStatusModal').modal('show');
jQuery("#newCallingDialerModal").modal('hide');
// Timer starts only when the call is accepted
activeConnection.on('accept', () => {
console.log('Call connected');
updateCallStatus('Call in progress...');
startTimer(); // Start timer only when the call is accepted
// console.log("Full Call Parameters:", JSON.stringify(activeConnection.parameters, null, 2));
if (activeConnection.parameters.CallSid) {
updateParentCallStatus(activeConnection.parameters.CallSid);
}
});
// Handle call disconnection
activeConnection.on('disconnect', () => {
console.log('Call disconnected');
updateCallStatus('Call ended');
stopTimer(); // Stop timer immediately
// console.log("Full Call Parameters:", JSON.stringify(activeConnection.parameters, null, 2));
if (activeConnection.parameters.CallSid) {
updateParentCallStatus(activeConnection.parameters.CallSid);
}
activeConnection = null; // Reset the active connection
jQuery('#newCallingStatusModal').modal('hide'); // Close the modal immediately
});
} catch (error) {
console.error('Twilio Call Error:', error);
updateCallStatus('Call failed: ' + error.message);
stopTimer();
}
});
async function refreshTwilioToken(agentId, conductedFrom) {
console.log('Manually refreshing Twilio token...');
const newToken = await fetchToken(agentId, conductedFrom);
if (newToken && twilioDevice) {
twilioDevice.updateToken(newToken);
console.log('Token refreshed successfully');
} else {
console.error('Failed to refresh Twilio token');
}
}
// Hang up the call
document.getElementById('hangupButton').addEventListener('click', () => {
if (activeConnection) {
activeConnection.disconnect(); // Disconnect the call immediately
activeConnection = null;
}
updateCallStatus('Call ended');
stopTimer();
jQuery('#newCallingStatusModal').modal('hide'); // Close the modal immediately
jQuery('#newCallingIncomingCallModal').modal('hide');
});
// Incoming call modal handling
function showIncomingCallModal(connection) {
activeConnection = connection; // Set active connection
// Show the incoming call modal
jQuery('#newCallingIncomingCallModal').modal('show');
document.getElementById('incomingCallNumber').textContent = activeConnection.parameters.From;
// Accept the call
document.getElementById('acceptCallButton').onclick = () => {
activeConnection.accept();
updateCallStatus('Call in progress...');
jQuery('#newCallingStatusModal').modal('show');
jQuery('#newCallingIncomingCallModal').modal('hide');
startTimer(); // Start timer only when the call is accepted
// console.log("Full Call Parameters:", JSON.stringify(activeConnection.parameters, null, 2));
if (activeConnection.parameters.CallSid) {
updateParentCallStatus(activeConnection.parameters.CallSid, "incoming");
}
};
// Reject the call
document.getElementById('rejectCallButton').onclick = () => {
activeConnection.reject();
jQuery('#newCallingIncomingCallModal').modal('hide');
updateCallStatus('Call rejected');
jQuery('#newCallingStatusModal').modal('hide');
// console.log("Full Call Parameters:", JSON.stringify(activeConnection.parameters, null, 2));
if (activeConnection.parameters.CallSid) {
updateParentCallStatus(activeConnection.parameters.CallSid, "incoming");
}
activeConnection = null; // Reset active connection after rejection
};
// Listen for disconnect on the active connection
activeConnection.on('disconnect', () => {
console.log('Call disconnected');
updateCallStatus('Call ended');
stopTimer();
// console.log("Full Call Parameters:", JSON.stringify(activeConnection.parameters, null, 2));
if (activeConnection.parameters.CallSid) {
updateParentCallStatus(activeConnection.parameters.CallSid, "incoming");
}
activeConnection = null; // Reset active connection
jQuery('#newCallingStatusModal').modal('hide'); // Close call status modal
jQuery('#newCallingIncomingCallModal').modal('hide'); // Close incoming call modal
});
}
// Update call status in the modal
function updateCallStatus(status) {
document.getElementById('callStatus').textContent = status;
}
// Start the call timer
function startTimer() {
callStartTime = Date.now();
timerInterval = setInterval(updateTimer, 1000);
}
// Stop the call timer
function stopTimer() {
clearInterval(timerInterval);
document.getElementById('callTimer').textContent = '00:00';
}
// Update the timer display
function updateTimer() {
const elapsedTime = Math.floor((Date.now() - callStartTime) / 1000);
const minutes = Math.floor(elapsedTime / 60).toString().padStart(2, '0');
const seconds = (elapsedTime % 60).toString().padStart(2, '0');
document.getElementById('callTimer').textContent = `${minutes}:${seconds}`;
}
Following is outgoing call handler which is handled by twiml
// Caller input details from twilio
$toph = $_REQUEST['To']; // 917797809829
$fromph = $_REQUEST['From']; // client:2x13612
$callsid = $_REQUEST['CallSid']; // CAfef253c1b7c1d15bcb1cd7240ce6556c
$callstat = $_REQUEST['CallStatus']; // ringing
$twilioMasterNo = getTwilioMasterInfo($con);
// Twilio call status callback url handling hook file
$statusCallbackUrl = $site_URL . "twilio-current-call-status-callback.php";
$qp = $_REQUEST;
$calldesc = "";
foreach($qp as $key => $val){
$out = $key.' == '.$val;
if ($calldesc == "") {
$calldesc = $out;
} else {
$calldesc = $calldesc . ' && ' . $out;
}
}
if ($twilioMasterNo != "" && $leadid != "") {
?>
<Response>
<Dial callerId="<?= htmlspecialchars($twilioMasterNo) ?>" record="true">
<Number statusCallback="<?=htmlspecialchars($statusCallbackUrl)?>" statusCallbackEvent="initiated ringing answered completed"><?= htmlspecialchars($toph) ?></Number>
</Dial>
</Response>
<?php
}
Following is my incoming file ml handler
// Caller input details from twilio
$toph = $_REQUEST['To'];
$fromph = $_REQUEST['From'];
$callsid = $_REQUEST['CallSid'];
$callstat = $_REQUEST['CallStatus'];
// Twilio call status callback url handling hook file
$statusCallbackUrl = $site_URL . "twilio-current-call-status-callback.php";
$qp = $_REQUEST;
$calldesc = "";
foreach($qp as $key => $val){
$out = $key.' == '.$val;
if ($calldesc == "") {
$calldesc = $out;
} else {
$calldesc = $calldesc . ' && ' . $out;
}
}
if ($selectedAgent != "" && $leadid!= "") {
// Create an inbound call record
$op = create_vtigercallrecord($vclient, $leadid, $agentid, $userid, $leadph, $agentph, $callsid, $calldesc, $callstat, $conduct, $con);
?>
<Response>
<Dial record="true"> <!-- Enable call recording -->
<Client statusCallback="<?=htmlspecialchars($statusCallbackUrl)?>" statusCallbackEvent="initiated ringing answered completed"><?=htmlspecialchars($selectedAgent)?></Client> <!-- Replace with your identity -->
</Dial>
</Response>
<?php
}