2025-08-13 13:45:25 -07:00
/ * *
* Frontend Browser Phone for TWP Plugin
* Mobile - friendly implementation
* /
( function ( $ ) {
'use strict' ;
let twilioDevice = null ;
let currentCall = null ;
let callTimer = null ;
let callStartTime = null ;
let isConnected = false ;
let availableNumbers = [ ] ;
2025-08-13 13:58:24 -07:00
let userQueues = [ ] ;
let selectedQueue = null ;
2025-08-13 13:45:25 -07:00
// Initialize when document is ready
$ ( document ) . ready ( function ( ) {
if ( ! twp _frontend _ajax . is _logged _in ) {
showMessage ( 'You must be logged in to use the browser phone.' , 'error' ) ;
return ;
}
initializeBrowserPhone ( ) ;
bindEvents ( ) ;
loadPhoneNumbers ( ) ;
2025-08-13 13:58:24 -07:00
loadUserQueues ( ) ;
2025-08-13 13:45:25 -07:00
} ) ;
2025-08-13 14:09:36 -07:00
/ * *
* Wait for Twilio SDK to load
* /
function waitForTwilioSDK ( callback ) {
let attempts = 0 ;
2025-08-13 14:22:37 -07:00
const maxAttempts = 150 ; // 15 seconds max (100ms * 150)
2025-08-13 14:09:36 -07:00
function checkTwilio ( ) {
2025-08-13 14:22:37 -07:00
console . log ( 'Checking Twilio SDK availability, attempt:' , attempts + 1 ) ;
2025-08-13 14:09:36 -07:00
if ( typeof Twilio !== 'undefined' && Twilio . Device ) {
2025-08-13 14:22:37 -07:00
console . log ( 'Twilio SDK loaded successfully' ) ;
2025-08-13 14:09:36 -07:00
callback ( ) ;
return ;
}
attempts ++ ;
2025-08-13 14:22:37 -07:00
// Update status message periodically
if ( attempts === 30 ) { // 3 seconds
updateStatus ( 'connecting' , 'Loading Twilio SDK...' ) ;
} else if ( attempts === 60 ) { // 6 seconds
updateStatus ( 'connecting' , 'Still loading SDK...' ) ;
} else if ( attempts === 100 ) { // 10 seconds
updateStatus ( 'connecting' , 'SDK taking longer than expected...' ) ;
}
2025-08-13 14:09:36 -07:00
if ( attempts >= maxAttempts ) {
2025-08-13 14:22:37 -07:00
console . error ( 'Twilio SDK failed to load. Window.Twilio:' , typeof Twilio ) ;
2025-08-13 14:09:36 -07:00
updateStatus ( 'offline' , 'SDK load timeout' ) ;
2025-08-13 14:22:37 -07:00
showMessage ( 'Twilio SDK failed to load within 15 seconds. This may be due to a slow connection or network restrictions. Please refresh the page and try again.' , 'error' ) ;
2025-08-13 14:09:36 -07:00
return ;
}
setTimeout ( checkTwilio , 100 ) ;
}
checkTwilio ( ) ;
}
2025-08-13 13:45:25 -07:00
/ * *
* Initialize the browser phone
* /
function initializeBrowserPhone ( ) {
updateStatus ( 'connecting' , 'Initializing...' ) ;
2025-08-13 14:09:36 -07:00
// Wait for Twilio SDK to load, then initialize
waitForTwilioSDK ( function ( ) {
// Generate capability token
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_generate_capability_token' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
setupTwilioDevice ( response . data . token ) ;
} else {
updateStatus ( 'offline' , 'Failed to initialize' ) ;
showMessage ( 'Failed to initialize browser phone: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
updateStatus ( 'offline' , 'Connection failed' ) ;
showMessage ( 'Failed to connect to server' , 'error' ) ;
2025-08-13 13:45:25 -07:00
}
2025-08-13 14:09:36 -07:00
} ) ;
2025-08-13 13:45:25 -07:00
} ) ;
}
/ * *
* Setup Twilio Device
* /
function setupTwilioDevice ( token ) {
2025-08-13 14:09:36 -07:00
// Check if Twilio SDK is loaded
if ( typeof Twilio === 'undefined' || ! Twilio . Device ) {
updateStatus ( 'offline' , 'Twilio SDK not loaded' ) ;
showMessage ( 'Twilio SDK failed to load. Please refresh the page.' , 'error' ) ;
console . error ( 'Twilio SDK is not available' ) ;
return ;
}
2025-08-13 13:45:25 -07:00
try {
twilioDevice = new Twilio . Device ( token , {
logLevel : 1 ,
answerOnBridge : true
} ) ;
twilioDevice . on ( 'registered' , function ( ) {
updateStatus ( 'online' , 'Ready' ) ;
isConnected = true ;
showMessage ( 'Browser phone ready!' , 'success' ) ;
} ) ;
twilioDevice . on ( 'error' , function ( error ) {
console . error ( 'Twilio Device Error:' , error ) ;
updateStatus ( 'offline' , 'Error: ' + error . message ) ;
showMessage ( 'Device error: ' + error . message , 'error' ) ;
} ) ;
twilioDevice . on ( 'incoming' , function ( call ) {
handleIncomingCall ( call ) ;
} ) ;
twilioDevice . on ( 'disconnect' , function ( ) {
updateStatus ( 'offline' , 'Disconnected' ) ;
isConnected = false ;
} ) ;
twilioDevice . register ( ) ;
} catch ( error ) {
console . error ( 'Failed to setup Twilio Device:' , error ) ;
updateStatus ( 'offline' , 'Setup failed' ) ;
showMessage ( 'Failed to setup device: ' + error . message , 'error' ) ;
}
}
/ * *
* Load available phone numbers
* /
function loadPhoneNumbers ( ) {
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_phone_numbers' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success && response . data ) {
availableNumbers = response . data ;
populateCallerIdSelect ( ) ;
} else {
showMessage ( 'Failed to load phone numbers' , 'error' ) ;
}
} ,
error : function ( ) {
showMessage ( 'Failed to load phone numbers' , 'error' ) ;
}
} ) ;
}
/ * *
* Populate caller ID select
* /
function populateCallerIdSelect ( ) {
const $select = $ ( '#twp-caller-id' ) ;
$select . empty ( ) ;
if ( availableNumbers . length === 0 ) {
$select . append ( '<option value="">No numbers available</option>' ) ;
return ;
}
$select . append ( '<option value="">Select caller ID...</option>' ) ;
availableNumbers . forEach ( function ( number ) {
const friendlyName = number . friendly _name || number . phone _number ;
$select . append ( ` <option value=" ${ number . phone _number } "> ${ friendlyName } ( ${ number . phone _number } )</option> ` ) ;
} ) ;
}
/ * *
* Bind event handlers
* /
function bindEvents ( ) {
// Dial pad buttons
$ ( '.twp-dial-btn' ) . on ( 'click' , function ( ) {
const digit = $ ( this ) . data ( 'digit' ) ;
addDigit ( digit ) ;
// Haptic feedback on mobile
if ( navigator . vibrate ) {
navigator . vibrate ( 50 ) ;
}
} ) ;
// Clear number button
$ ( '#twp-clear-number' ) . on ( 'click' , function ( ) {
$ ( '#twp-dial-number' ) . val ( '' ) ;
} ) ;
// Call button
$ ( '#twp-call-btn' ) . on ( 'click' , function ( ) {
if ( ! isConnected ) {
showMessage ( 'Device not connected' , 'error' ) ;
return ;
}
const number = $ ( '#twp-dial-number' ) . val ( ) . trim ( ) ;
const callerId = $ ( '#twp-caller-id' ) . val ( ) ;
if ( ! number ) {
showMessage ( 'Please enter a number to call' , 'error' ) ;
return ;
}
if ( ! callerId ) {
showMessage ( 'Please select a caller ID' , 'error' ) ;
return ;
}
makeCall ( number , callerId ) ;
} ) ;
// Hang up button
$ ( '#twp-hangup-btn' ) . on ( 'click' , function ( ) {
hangupCall ( ) ;
} ) ;
// Accept queue call button
$ ( '#twp-accept-queue-call' ) . on ( 'click' , function ( ) {
acceptQueueCall ( ) ;
} ) ;
2025-08-13 13:58:24 -07:00
// Refresh queues button
$ ( '#twp-refresh-queues' ) . on ( 'click' , function ( ) {
loadUserQueues ( ) ;
} ) ;
// Queue item selection
$ ( document ) . on ( 'click' , '.queue-item' , function ( ) {
const queueId = $ ( this ) . data ( 'queue-id' ) ;
selectQueue ( queueId ) ;
} ) ;
2025-08-13 13:45:25 -07:00
// Manual number input
$ ( '#twp-dial-number' ) . on ( 'input' , function ( ) {
// Only allow valid phone number characters
let value = $ ( this ) . val ( ) . replace ( /[^\d\+\-\(\)\s]/g , '' ) ;
$ ( this ) . val ( value ) ;
} ) ;
// Handle enter key in dial number input
$ ( '#twp-dial-number' ) . on ( 'keypress' , function ( e ) {
if ( e . which === 13 ) { // Enter key
$ ( '#twp-call-btn' ) . click ( ) ;
}
} ) ;
}
/ * *
* Add digit to dial pad
* /
function addDigit ( digit ) {
const $input = $ ( '#twp-dial-number' ) ;
$input . val ( $input . val ( ) + digit ) ;
// Send DTMF if in a call
if ( currentCall && currentCall . status ( ) === 'open' ) {
currentCall . sendDigits ( digit ) ;
}
}
/ * *
* Make outbound call
* /
function makeCall ( number , callerId ) {
if ( currentCall ) {
showMessage ( 'Already in a call' , 'error' ) ;
return ;
}
updateCallState ( 'connecting' ) ;
showCallInfo ( 'Connecting...' ) ;
const params = {
To : number ,
From : callerId
} ;
try {
currentCall = twilioDevice . connect ( params ) ;
currentCall . on ( 'accept' , function ( ) {
updateCallState ( 'connected' ) ;
showCallInfo ( 'Connected' ) ;
startCallTimer ( ) ;
showMessage ( 'Call connected!' , 'success' ) ;
} ) ;
currentCall . on ( 'disconnect' , function ( ) {
endCall ( ) ;
showMessage ( 'Call ended' , 'info' ) ;
} ) ;
currentCall . on ( 'error' , function ( error ) {
console . error ( 'Call error:' , error ) ;
endCall ( ) ;
showMessage ( 'Call failed: ' + error . message , 'error' ) ;
} ) ;
} catch ( error ) {
console . error ( 'Failed to make call:' , error ) ;
endCall ( ) ;
showMessage ( 'Failed to make call: ' + error . message , 'error' ) ;
}
}
/ * *
* Handle incoming call
* /
function handleIncomingCall ( call ) {
currentCall = call ;
// Add visual indication
$ ( '.twp-browser-phone-container' ) . addClass ( 'incoming-call' ) ;
updateCallState ( 'ringing' ) ;
showCallInfo ( 'Incoming call from: ' + ( call . parameters . From || 'Unknown' ) ) ;
showMessage ( 'Incoming call! Click Accept to answer.' , 'info' ) ;
// Auto-answer after a delay (optional)
setTimeout ( function ( ) {
if ( currentCall === call && call . status ( ) === 'pending' ) {
acceptCall ( ) ;
}
} , 2000 ) ;
call . on ( 'accept' , function ( ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
updateCallState ( 'connected' ) ;
showCallInfo ( 'Connected' ) ;
startCallTimer ( ) ;
showMessage ( 'Call answered!' , 'success' ) ;
} ) ;
call . on ( 'disconnect' , function ( ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
endCall ( ) ;
showMessage ( 'Call ended' , 'info' ) ;
} ) ;
call . on ( 'error' , function ( error ) {
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
endCall ( ) ;
showMessage ( 'Call error: ' + error . message , 'error' ) ;
} ) ;
}
/ * *
* Accept incoming call
* /
function acceptCall ( ) {
if ( currentCall && currentCall . status ( ) === 'pending' ) {
currentCall . accept ( ) ;
}
}
/ * *
* Hang up current call
* /
function hangupCall ( ) {
if ( currentCall ) {
currentCall . disconnect ( ) ;
}
endCall ( ) ;
}
/ * *
* End call and cleanup
* /
function endCall ( ) {
currentCall = null ;
stopCallTimer ( ) ;
updateCallState ( 'idle' ) ;
hideCallInfo ( ) ;
$ ( '.twp-browser-phone-container' ) . removeClass ( 'incoming-call' ) ;
}
/ * *
2025-08-13 13:58:24 -07:00
* Load user ' s assigned queues
* /
function loadUserQueues ( ) {
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_agent_queues' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
userQueues = response . data ;
displayQueues ( ) ;
} else {
showMessage ( 'Failed to load queues: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
showMessage ( 'Failed to load queues' , 'error' ) ;
}
} ) ;
}
/ * *
* Display queues in the UI
* /
function displayQueues ( ) {
const $queueList = $ ( '#twp-queue-list' ) ;
if ( userQueues . length === 0 ) {
$queueList . html ( '<div class="no-queues">No queues assigned to you.</div>' ) ;
$ ( '#twp-queue-section' ) . hide ( ) ;
return ;
}
$ ( '#twp-queue-section' ) . show ( ) ;
let html = '' ;
userQueues . forEach ( function ( queue ) {
const hasWaiting = parseInt ( queue . current _waiting ) > 0 ;
const waitingCount = queue . current _waiting || 0 ;
html += `
< div class = "queue-item ${hasWaiting ? 'has-calls' : ''}" data - queue - id = "${queue.id}" >
< div class = "queue-name" > $ { queue . queue _name } < / d i v >
< div class = "queue-info" >
< span class = "queue-waiting ${hasWaiting ? 'has-calls' : ''}" >
$ { waitingCount } waiting
< / s p a n >
< span class = "queue-capacity" >
Max : $ { queue . max _size }
< / s p a n >
< / d i v >
< / d i v >
` ;
} ) ;
$queueList . html ( html ) ;
// Auto-select first queue with calls, or first queue if none have calls
const firstQueueWithCalls = userQueues . find ( q => parseInt ( q . current _waiting ) > 0 ) ;
const queueToSelect = firstQueueWithCalls || userQueues [ 0 ] ;
if ( queueToSelect ) {
selectQueue ( queueToSelect . id ) ;
}
}
/ * *
* Select a queue
* /
function selectQueue ( queueId ) {
selectedQueue = userQueues . find ( q => q . id == queueId ) ;
if ( ! selectedQueue ) return ;
// Update UI selection
$ ( '.queue-item' ) . removeClass ( 'selected' ) ;
$ ( ` .queue-item[data-queue-id=" ${ queueId } "] ` ) . addClass ( 'selected' ) ;
// Update queue controls
$ ( '#selected-queue-name' ) . text ( selectedQueue . queue _name ) ;
$ ( '#twp-waiting-count' ) . text ( selectedQueue . current _waiting || 0 ) ;
$ ( '#twp-queue-max-size' ) . text ( selectedQueue . max _size ) ;
// Show queue controls if there are waiting calls
if ( parseInt ( selectedQueue . current _waiting ) > 0 ) {
$ ( '#twp-queue-controls' ) . show ( ) ;
} else {
$ ( '#twp-queue-controls' ) . hide ( ) ;
}
}
/ * *
* Accept next call from selected queue
2025-08-13 13:45:25 -07:00
* /
function acceptQueueCall ( ) {
2025-08-13 13:58:24 -07:00
if ( ! selectedQueue ) {
showMessage ( 'Please select a queue first' , 'error' ) ;
return ;
}
2025-08-13 13:45:25 -07:00
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_accept_next_queue_call' ,
2025-08-13 13:58:24 -07:00
queue _id : selectedQueue . id ,
2025-08-13 13:45:25 -07:00
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
showMessage ( 'Connecting to next caller...' , 'info' ) ;
2025-08-13 13:58:24 -07:00
// Refresh queue data after accepting call
setTimeout ( loadUserQueues , 1000 ) ;
2025-08-13 13:45:25 -07:00
} else {
2025-08-13 13:58:24 -07:00
showMessage ( response . data || 'No calls waiting in this queue' , 'info' ) ;
2025-08-13 13:45:25 -07:00
}
} ,
error : function ( ) {
showMessage ( 'Failed to accept queue call' , 'error' ) ;
}
} ) ;
}
/ * *
* Update call state UI
* /
function updateCallState ( state ) {
const $callBtn = $ ( '#twp-call-btn' ) ;
const $hangupBtn = $ ( '#twp-hangup-btn' ) ;
switch ( state ) {
case 'idle' :
$callBtn . show ( ) . prop ( 'disabled' , false ) ;
$hangupBtn . hide ( ) ;
break ;
case 'connecting' :
case 'ringing' :
$callBtn . hide ( ) ;
$hangupBtn . show ( ) ;
break ;
case 'connected' :
$callBtn . hide ( ) ;
$hangupBtn . show ( ) ;
break ;
}
}
/ * *
* Show call info panel
* /
function showCallInfo ( status ) {
$ ( '#twp-call-info' ) . show ( ) ;
$ ( '#twp-call-status' ) . text ( status ) ;
}
/ * *
* Hide call info panel
* /
function hideCallInfo ( ) {
$ ( '#twp-call-info' ) . hide ( ) ;
$ ( '#twp-call-timer' ) . text ( '00:00' ) ;
$ ( '#twp-call-status' ) . text ( '' ) ;
}
/ * *
* Start call timer
* /
function startCallTimer ( ) {
callStartTime = new Date ( ) ;
callTimer = setInterval ( updateCallTimer , 1000 ) ;
updateCallTimer ( ) ;
}
/ * *
* Stop call timer
* /
function stopCallTimer ( ) {
if ( callTimer ) {
clearInterval ( callTimer ) ;
callTimer = null ;
}
callStartTime = null ;
}
/ * *
* Update call timer display
* /
function updateCallTimer ( ) {
if ( ! callStartTime ) return ;
const elapsed = Math . floor ( ( new Date ( ) - callStartTime ) / 1000 ) ;
const minutes = Math . floor ( elapsed / 60 ) ;
const seconds = elapsed % 60 ;
const timeString = String ( minutes ) . padStart ( 2 , '0' ) + ':' + String ( seconds ) . padStart ( 2 , '0' ) ;
$ ( '#twp-call-timer' ) . text ( timeString ) ;
}
/ * *
* Update connection status
* /
function updateStatus ( status , text ) {
const $indicator = $ ( '#twp-status-indicator' ) ;
const $text = $ ( '#twp-status-text' ) ;
$indicator . removeClass ( 'offline connecting online' ) . addClass ( status ) ;
$text . text ( text ) ;
}
/ * *
* Show message to user
* /
function showMessage ( message , type ) {
const $messages = $ ( '#twp-messages' ) ;
const $message = $ ( '<div>' ) . addClass ( 'twp-' + type ) . text ( message ) ;
$messages . empty ( ) . append ( $message ) ;
// Auto-hide success and info messages
if ( type === 'success' || type === 'info' ) {
setTimeout ( function ( ) {
$message . fadeOut ( function ( ) {
$message . remove ( ) ;
} ) ;
} , 5000 ) ;
}
}
// Periodic status updates
setInterval ( function ( ) {
if ( isConnected ) {
2025-08-13 13:58:24 -07:00
loadUserQueues ( ) ; // This will refresh all queue data including waiting counts
2025-08-13 13:45:25 -07:00
}
} , 30000 ) ; // Every 30 seconds
} ) ( jQuery ) ;