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-14 12:01:05 -07:00
let tokenRefreshTimer = null ;
let tokenExpiry = null ;
2025-08-15 09:14:51 -07:00
let queuePollingTimer = null ;
let lastQueueUpdate = { } ;
let alertSound = null ;
let alertInterval = null ;
let alertEnabled = false ;
2025-08-15 16:51:47 -07:00
let notificationPermission = 'default' ;
let backgroundAlertInterval = null ;
let isPageVisible = true ;
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-15 09:56:04 -07:00
initVoicemailSection ( ) ;
2025-08-15 16:51:47 -07:00
initializeNotifications ( ) ;
initializePageVisibility ( ) ;
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 ) ;
2025-08-14 12:01:05 -07:00
// Set token expiry time (expires in 1 hour)
tokenExpiry = Date . now ( ) + ( response . data . expires _in || 3600 ) * 1000 ;
scheduleTokenRefresh ( ) ;
2025-08-13 14:09:36 -07:00
} 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
* /
2025-08-13 15:20:14 -07:00
async 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 {
2025-08-14 12:01:05 -07:00
// If device already exists, destroy it first to prevent multiple connections
if ( twilioDevice ) {
twilioDevice . destroy ( ) ;
twilioDevice = null ;
}
2025-08-13 13:45:25 -07:00
twilioDevice = new Twilio . Device ( token , {
logLevel : 1 ,
answerOnBridge : true
} ) ;
twilioDevice . on ( 'registered' , function ( ) {
updateStatus ( 'online' , 'Ready' ) ;
isConnected = true ;
2025-08-14 12:01:05 -07:00
// Only show success message on initial connection
if ( ! tokenRefreshTimer ) {
showMessage ( 'Browser phone ready!' , 'success' ) ;
}
2025-08-13 13:45:25 -07:00
} ) ;
twilioDevice . on ( 'error' , function ( error ) {
console . error ( 'Twilio Device Error:' , error ) ;
updateStatus ( 'offline' , 'Error: ' + error . message ) ;
showMessage ( 'Device error: ' + error . message , 'error' ) ;
2025-08-14 12:01:05 -07:00
// If token expired error, refresh immediately
if ( error . message && error . message . toLowerCase ( ) . includes ( 'token' ) ) {
refreshToken ( ) ;
}
2025-08-13 13:45:25 -07:00
} ) ;
twilioDevice . on ( 'incoming' , function ( call ) {
handleIncomingCall ( call ) ;
} ) ;
twilioDevice . on ( 'disconnect' , function ( ) {
updateStatus ( 'offline' , 'Disconnected' ) ;
isConnected = false ;
} ) ;
2025-08-13 15:20:14 -07:00
// Register device asynchronously
await twilioDevice . register ( ) ;
2025-08-13 13:45:25 -07:00
} 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-15 09:14:51 -07:00
stopAlert ( ) ; // Stop alert when accepting call
2025-08-13 13:45:25 -07:00
} ) ;
2025-08-13 13:58:24 -07:00
// Refresh queues button
$ ( '#twp-refresh-queues' ) . on ( 'click' , function ( ) {
loadUserQueues ( ) ;
} ) ;
2025-08-15 09:14:51 -07:00
// Alert toggle button
$ ( document ) . on ( 'click' , '#twp-alert-toggle' , function ( ) {
toggleAlert ( ) ;
} ) ;
2025-08-15 09:29:35 -07:00
// Voicemail refresh button
$ ( '#twp-refresh-voicemails' ) . on ( 'click' , function ( ) {
loadUserVoicemails ( ) ;
} ) ;
// View all voicemails button
$ ( '#twp-view-all-voicemails' ) . on ( 'click' , function ( ) {
// Open admin voicemails page in new tab
window . open ( twp _frontend _ajax . admin _url + 'admin.php?page=twilio-wp-voicemails' , '_blank' ) ;
} ) ;
2025-08-15 09:56:04 -07:00
// Voicemail toggle button
$ ( '#twp-voicemail-toggle' ) . on ( 'click' , function ( ) {
toggleVoicemailSection ( ) ;
} ) ;
// Voicemail header click (also toggles)
$ ( '#twp-voicemail-header' ) . on ( 'click' , function ( e ) {
// Don't toggle if clicking the toggle button itself
if ( ! $ ( e . target ) . closest ( '.voicemail-toggle' ) . length ) {
toggleVoicemailSection ( ) ;
}
} ) ;
2025-08-15 09:29:35 -07:00
// Voicemail item click handler
$ ( document ) . on ( 'click' , '.voicemail-item' , function ( ) {
const voicemailId = $ ( this ) . data ( 'voicemail-id' ) ;
playVoicemail ( voicemailId ) ;
} ) ;
2025-08-13 13:58:24 -07:00
// 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
* /
2025-08-13 15:20:14 -07:00
async function makeCall ( number , callerId ) {
2025-08-13 13:45:25 -07:00
if ( currentCall ) {
showMessage ( 'Already in a call' , 'error' ) ;
return ;
}
2025-08-15 09:14:51 -07:00
// Stop alerts when making a call
stopAlert ( ) ;
2025-08-13 13:45:25 -07:00
updateCallState ( 'connecting' ) ;
showCallInfo ( 'Connecting...' ) ;
const params = {
To : number ,
From : callerId
} ;
try {
2025-08-13 15:20:14 -07:00
console . log ( 'Making call with params:' , params ) ;
currentCall = await twilioDevice . connect ( { params : params } ) ;
2025-08-13 13:45:25 -07:00
2025-08-13 15:20:14 -07:00
// Setup call event handlers
2025-08-13 13:45:25 -07:00
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-15 09:14:51 -07:00
// Restart alerts if enabled and there are waiting calls
if ( alertEnabled ) {
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( hasWaitingCalls ) {
setTimeout ( startAlert , 1000 ) ; // Small delay to avoid immediate alert
}
}
2025-08-13 13:45:25 -07:00
}
/ * *
2025-08-13 13:58:24 -07:00
* Load user ' s assigned queues
* /
2025-08-15 09:14:51 -07:00
function loadUserQueues ( silent = false ) {
2025-08-13 13:58:24 -07:00
$ . 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 ) {
2025-08-15 09:14:51 -07:00
// Check for new calls in queues
checkForNewCalls ( response . data ) ;
2025-08-13 13:58:24 -07:00
userQueues = response . data ;
displayQueues ( ) ;
2025-08-15 09:14:51 -07:00
} else if ( ! silent ) {
2025-08-13 13:58:24 -07:00
showMessage ( 'Failed to load queues: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
2025-08-15 09:14:51 -07:00
if ( ! silent ) {
showMessage ( 'Failed to load queues' , 'error' ) ;
}
}
} ) ;
}
/ * *
* Check for new calls in queues and trigger alerts
* /
function checkForNewCalls ( newQueues ) {
2025-08-15 10:28:39 -07:00
let hasWaitingCalls = false ;
let newCallDetected = false ;
2025-08-15 09:14:51 -07:00
newQueues . forEach ( function ( queue ) {
const queueId = queue . id ;
const currentWaiting = parseInt ( queue . current _waiting ) || 0 ;
const previousWaiting = lastQueueUpdate [ queueId ] || 0 ;
2025-08-15 10:28:39 -07:00
// Track if any queue has waiting calls
if ( currentWaiting > 0 ) {
hasWaitingCalls = true ;
}
2025-08-15 09:14:51 -07:00
// If waiting count increased, we have new calls
if ( currentWaiting > previousWaiting ) {
console . log ( 'New call detected in queue:' , queue . queue _name ) ;
2025-08-15 10:28:39 -07:00
newCallDetected = true ;
2025-08-15 16:51:47 -07:00
// Show browser notification for new call
if ( notificationPermission === 'granted' ) {
showBrowserNotification ( '📞 New Call in Queue!' , {
body : ` ${ queue . queue _name } : ${ currentWaiting } call ${ currentWaiting > 1 ? 's' : '' } waiting ` ,
icon : '📞' ,
vibrate : [ 300 , 200 , 300 ] ,
requireInteraction : true ,
tag : ` queue- ${ queue . id } ` ,
data : {
queueId : queue . id
}
} ) ;
}
2025-08-13 13:58:24 -07:00
}
2025-08-15 09:14:51 -07:00
lastQueueUpdate [ queueId ] = currentWaiting ;
2025-08-13 13:58:24 -07:00
} ) ;
2025-08-15 10:28:39 -07:00
// Manage alerts based on queue state
if ( alertEnabled && ! currentCall ) {
if ( newCallDetected ) {
// Start alert for new calls
startAlert ( ) ;
} else if ( ! hasWaitingCalls ) {
// Stop alert if no calls are waiting in any queue
console . log ( 'No calls waiting in any queue, stopping alerts' ) ;
stopAlert ( ) ;
}
}
2025-08-13 13:58:24 -07:00
}
/ * *
* 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 ( ) ;
2025-08-15 09:56:04 -07:00
$ ( '#twp-queue-global-actions' ) . hide ( ) ;
2025-08-13 13:58:24 -07:00
return ;
}
$ ( '#twp-queue-section' ) . show ( ) ;
2025-08-15 09:56:04 -07:00
$ ( '#twp-queue-global-actions' ) . show ( ) ;
2025-08-13 13:58:24 -07:00
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 ) ;
}
}
2025-08-15 09:14:51 -07:00
// Start queue polling with faster interval
startQueuePolling ( ) ;
/ * *
* Start polling for queue updates
* /
function startQueuePolling ( ) {
// Clear any existing timer
if ( queuePollingTimer ) {
clearInterval ( queuePollingTimer ) ;
2025-08-13 13:45:25 -07:00
}
2025-08-15 09:14:51 -07:00
// Poll every 5 seconds for real-time updates
queuePollingTimer = setInterval ( function ( ) {
if ( isConnected ) {
loadUserQueues ( true ) ; // Silent update
}
} , 5000 ) ; // Every 5 seconds
}
2025-08-13 13:45:25 -07:00
2025-08-14 12:01:05 -07:00
/ * *
* Schedule token refresh
2025-08-15 09:14:51 -07:00
* Refreshes token 10 minutes before expiry for safety
2025-08-14 12:01:05 -07:00
* /
function scheduleTokenRefresh ( ) {
// Clear any existing timer
if ( tokenRefreshTimer ) {
clearTimeout ( tokenRefreshTimer ) ;
}
if ( ! tokenExpiry ) {
console . error ( 'Token expiry time not set' ) ;
2025-08-15 09:14:51 -07:00
// Retry in 30 seconds if token expiry not set
setTimeout ( function ( ) {
if ( tokenExpiry ) {
scheduleTokenRefresh ( ) ;
}
} , 30000 ) ;
2025-08-14 12:01:05 -07:00
return ;
}
2025-08-15 09:14:51 -07:00
// Calculate time until refresh (10 minutes before expiry for extra safety)
const refreshBuffer = 10 * 60 * 1000 ; // 10 minutes in milliseconds
2025-08-14 12:01:05 -07:00
const timeUntilRefresh = tokenExpiry - Date . now ( ) - refreshBuffer ;
if ( timeUntilRefresh <= 0 ) {
// Token needs refresh immediately
refreshToken ( ) ;
} else {
// Schedule refresh
console . log ( 'Scheduling token refresh in' , Math . round ( timeUntilRefresh / 1000 ) , 'seconds' ) ;
tokenRefreshTimer = setTimeout ( refreshToken , timeUntilRefresh ) ;
}
}
/ * *
* Refresh the capability token
* /
function refreshToken ( ) {
console . log ( 'Refreshing capability token...' ) ;
// Don't refresh if currently in a call
if ( currentCall ) {
console . log ( 'Currently in call, postponing token refresh' ) ;
// Retry in 1 minute
setTimeout ( refreshToken , 60000 ) ;
return ;
}
$ . 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 ) {
console . log ( 'Token refreshed successfully' ) ;
// Update token expiry
tokenExpiry = Date . now ( ) + ( response . data . expires _in || 3600 ) * 1000 ;
// Update device with new token
setupTwilioDevice ( response . data . token ) ;
// Schedule next refresh
scheduleTokenRefresh ( ) ;
} else {
console . error ( 'Failed to refresh token:' , response . data ) ;
updateStatus ( 'offline' , 'Token refresh failed' ) ;
showMessage ( 'Failed to refresh connection. Please refresh the page.' , 'error' ) ;
}
} ,
error : function ( ) {
console . error ( 'Failed to refresh token - network error' ) ;
updateStatus ( 'offline' , 'Connection lost' ) ;
// Retry in 30 seconds
setTimeout ( refreshToken , 30000 ) ;
}
} ) ;
}
2025-08-15 09:14:51 -07:00
/ * *
* Initialize alert sound
* /
function initAlertSound ( ) {
// Create audio element for alert sound
alertSound = new Audio ( ) ;
alertSound . src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' ; // Simple beep sound
// Use Web Audio API for better sound
const audioContext = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
// Create a simple beep sound
function playBeep ( ) {
const oscillator = audioContext . createOscillator ( ) ;
const gainNode = audioContext . createGain ( ) ;
oscillator . connect ( gainNode ) ;
gainNode . connect ( audioContext . destination ) ;
oscillator . frequency . value = 800 ; // Frequency in Hz
gainNode . gain . setValueAtTime ( 0.3 , audioContext . currentTime ) ;
gainNode . gain . exponentialRampToValueAtTime ( 0.01 , audioContext . currentTime + 0.5 ) ;
oscillator . start ( audioContext . currentTime ) ;
oscillator . stop ( audioContext . currentTime + 0.5 ) ;
}
return playBeep ;
}
const playAlertSound = initAlertSound ( ) ;
/ * *
* Start alert for new calls
* /
function startAlert ( ) {
if ( ! alertEnabled || alertInterval ) return ;
2025-08-15 10:28:39 -07:00
// Check if there are actually waiting calls before starting alert
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( ! hasWaitingCalls ) {
console . log ( 'No waiting calls found, not starting alert' ) ;
return ;
}
2025-08-15 09:14:51 -07:00
// Play initial alert
playAlertSound ( ) ;
// Repeat every 30 seconds
alertInterval = setInterval ( function ( ) {
if ( alertEnabled && ! currentCall ) {
2025-08-15 10:28:39 -07:00
// Check if there are still waiting calls
const stillHasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( stillHasWaitingCalls ) {
playAlertSound ( ) ;
} else {
console . log ( 'No more waiting calls, stopping alert' ) ;
stopAlert ( ) ;
}
2025-08-15 09:14:51 -07:00
} else {
stopAlert ( ) ;
}
} , 30000 ) ;
}
/ * *
* Stop alert
* /
function stopAlert ( ) {
if ( alertInterval ) {
clearInterval ( alertInterval ) ;
alertInterval = null ;
}
}
/ * *
* Toggle alert on / off
* /
function toggleAlert ( ) {
alertEnabled = ! alertEnabled ;
localStorage . setItem ( 'twp_alert_enabled' , alertEnabled ) ;
// Update button state
updateAlertButton ( ) ;
if ( ! alertEnabled ) {
stopAlert ( ) ;
showMessage ( 'Queue alerts disabled' , 'info' ) ;
} else {
showMessage ( 'Queue alerts enabled' , 'success' ) ;
// Check if there are waiting calls
const hasWaitingCalls = userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( hasWaitingCalls && ! currentCall ) {
startAlert ( ) ;
}
}
}
/ * *
* Update alert button UI
* /
function updateAlertButton ( ) {
const $btn = $ ( '#twp-alert-toggle' ) ;
if ( alertEnabled ) {
$btn . removeClass ( 'alert-off' ) . addClass ( 'alert-on' ) . html ( '🔔 Alerts ON' ) ;
} else {
$btn . removeClass ( 'alert-on' ) . addClass ( 'alert-off' ) . html ( '🔕 Alerts OFF' ) ;
}
}
/ * *
* Load alert preference from localStorage
* /
function loadAlertPreference ( ) {
const saved = localStorage . getItem ( 'twp_alert_enabled' ) ;
alertEnabled = saved === null ? true : saved === 'true' ;
updateAlertButton ( ) ;
}
2025-08-14 12:01:05 -07:00
// Clean up on page unload
$ ( window ) . on ( 'beforeunload' , function ( ) {
if ( tokenRefreshTimer ) {
clearTimeout ( tokenRefreshTimer ) ;
}
2025-08-15 09:14:51 -07:00
if ( queuePollingTimer ) {
clearInterval ( queuePollingTimer ) ;
}
if ( alertInterval ) {
clearInterval ( alertInterval ) ;
}
2025-08-15 16:51:47 -07:00
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
}
2025-08-14 12:01:05 -07:00
if ( twilioDevice ) {
twilioDevice . destroy ( ) ;
}
} ) ;
2025-08-13 13:45:25 -07:00
2025-08-15 09:29:35 -07:00
/ * *
* Load user ' s voicemails
* /
function loadUserVoicemails ( silent = false ) {
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_user_voicemails' ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success ) {
displayVoicemails ( response . data ) ;
} else if ( ! silent ) {
showMessage ( 'Failed to load voicemails: ' + ( response . data || 'Unknown error' ) , 'error' ) ;
}
} ,
error : function ( ) {
if ( ! silent ) {
showMessage ( 'Failed to load voicemails' , 'error' ) ;
}
}
} ) ;
}
2025-08-15 09:56:04 -07:00
/ * *
* Toggle voicemail section visibility
* /
function toggleVoicemailSection ( ) {
const $content = $ ( '#twp-voicemail-content' ) ;
const $toggle = $ ( '#twp-voicemail-toggle .toggle-icon' ) ;
const isVisible = $content . is ( ':visible' ) ;
if ( isVisible ) {
$content . slideUp ( 300 ) ;
$toggle . text ( '▼' ) ;
localStorage . setItem ( 'twp_voicemail_collapsed' , 'true' ) ;
} else {
$content . slideDown ( 300 ) ;
$toggle . text ( '▲' ) ;
localStorage . setItem ( 'twp_voicemail_collapsed' , 'false' ) ;
// Load voicemails when expanding if not already loaded
if ( $ ( '#twp-voicemail-list' ) . children ( '.voicemail-loading' ) . length > 0 ) {
loadUserVoicemails ( ) ;
}
}
}
/ * *
* Initialize voicemail section state
* /
function initVoicemailSection ( ) {
const isCollapsed = localStorage . getItem ( 'twp_voicemail_collapsed' ) === 'true' ;
const $content = $ ( '#twp-voicemail-content' ) ;
const $toggle = $ ( '#twp-voicemail-toggle .toggle-icon' ) ;
if ( isCollapsed ) {
$content . hide ( ) ;
$toggle . text ( '▼' ) ;
} else {
$content . show ( ) ;
$toggle . text ( '▲' ) ;
// Load voicemails immediately if expanded
loadUserVoicemails ( ) ;
}
}
2025-08-15 09:29:35 -07:00
/ * *
* Display voicemails in the UI
* /
function displayVoicemails ( data ) {
const $voicemailList = $ ( '#twp-voicemail-list' ) ;
// Update stats
$ ( '#twp-total-voicemails' ) . text ( data . total _count || 0 ) ;
$ ( '#twp-today-voicemails' ) . text ( data . today _count || 0 ) ;
if ( ! data . voicemails || data . voicemails . length === 0 ) {
$voicemailList . html ( '<div class="no-voicemails">No voicemails found.</div>' ) ;
return ;
}
let html = '' ;
data . voicemails . forEach ( function ( voicemail ) {
const hasTranscription = voicemail . transcription && voicemail . transcription !== 'No transcription' ;
const hasRecording = voicemail . has _recording ;
html += `
< div class = "voicemail-item ${hasRecording ? 'has-recording' : ''}" data - voicemail - id = "${voicemail.id}" >
< div class = "voicemail-header" >
< div class = "voicemail-from" >
< span class = "phone-icon" > 📞 < / s p a n >
< span class = "from-number" > $ { voicemail . from _number } < / s p a n >
< / d i v >
< div class = "voicemail-time" > $ { voicemail . time _ago } < / d i v >
< / d i v >
< div class = "voicemail-details" >
< div class = "voicemail-duration" >
< span class = "duration-icon" > ⏱ ️ < / s p a n >
< span > $ { formatDuration ( voicemail . duration ) } < / s p a n >
< / d i v >
$ { hasRecording ? '<span class="recording-indicator">🎵 Recording</span>' : '' }
< / d i v >
$ { hasTranscription ? ` <div class="voicemail-transcription"> ${ voicemail . transcription } </div> ` : '' }
< / d i v >
` ;
} ) ;
$voicemailList . html ( html ) ;
}
/ * *
* Format duration in seconds to mm : ss
* /
function formatDuration ( seconds ) {
if ( ! seconds || seconds === 0 ) return '0:00' ;
const minutes = Math . floor ( seconds / 60 ) ;
const remainingSeconds = seconds % 60 ;
return minutes + ':' + String ( remainingSeconds ) . padStart ( 2 , '0' ) ;
}
/ * *
* Play voicemail audio
* /
function playVoicemail ( voicemailId ) {
if ( ! voicemailId ) return ;
// Get voicemail audio URL and play it
$ . ajax ( {
url : twp _frontend _ajax . ajax _url ,
method : 'POST' ,
data : {
action : 'twp_get_voicemail_audio' ,
voicemail _id : voicemailId ,
nonce : twp _frontend _ajax . nonce
} ,
success : function ( response ) {
if ( response . success && response . data . audio _url ) {
// Create and play audio element
const audio = new Audio ( response . data . audio _url ) ;
audio . play ( ) . catch ( function ( error ) {
showMessage ( 'Failed to play voicemail: ' + error . message , 'error' ) ;
} ) ;
showMessage ( 'Playing voicemail...' , 'info' ) ;
} else {
showMessage ( 'No audio available for this voicemail' , 'error' ) ;
}
} ,
error : function ( ) {
showMessage ( 'Failed to load voicemail audio' , 'error' ) ;
}
} ) ;
}
2025-08-15 16:51:47 -07:00
/ * *
* Initialize browser notifications
* /
function initializeNotifications ( ) {
// Register service worker for background notifications
if ( 'serviceWorker' in navigator ) {
navigator . serviceWorker . register ( '/wp-content/plugins/twilio-wp-plugin/assets/js/twp-service-worker.js' )
. then ( function ( registration ) {
console . log ( 'Service Worker registered:' , registration ) ;
} )
. catch ( function ( error ) {
console . log ( 'Service Worker registration failed:' , error ) ;
} ) ;
}
// Check if browser supports notifications
if ( ! ( 'Notification' in window ) ) {
console . log ( 'This browser does not support notifications' ) ;
return ;
}
// Check current permission status
notificationPermission = Notification . permission ;
// Request permission if not already granted or denied
if ( notificationPermission === 'default' ) {
// Add a button to request permission
if ( $ ( '#twp-queue-global-actions' ) . length > 0 ) {
const $notificationBtn = $ ( '<button>' )
. attr ( 'id' , 'twp-enable-notifications' )
. addClass ( 'twp-btn twp-btn-info' )
2025-08-15 17:12:33 -07:00
. html ( '🔔 Enable Alerts' )
2025-08-15 16:51:47 -07:00
. on ( 'click' , requestNotificationPermission ) ;
$ ( '#twp-queue-global-actions .global-queue-actions' ) . append ( $notificationBtn ) ;
}
} else if ( notificationPermission === 'granted' ) {
console . log ( 'Notifications are enabled' ) ;
}
}
/ * *
* Request notification permission from user
* /
function requestNotificationPermission ( ) {
Notification . requestPermission ( ) . then ( function ( permission ) {
notificationPermission = permission ;
if ( permission === 'granted' ) {
showMessage ( 'Notifications enabled! You will be alerted even when the browser is in the background.' , 'success' ) ;
$ ( '#twp-enable-notifications' ) . hide ( ) ;
// Show test notification
showBrowserNotification ( 'Notifications Enabled' , {
body : 'You will now receive alerts for incoming calls' ,
icon : '📞' ,
tag : 'test-notification'
} ) ;
} else if ( permission === 'denied' ) {
showMessage ( 'Notifications blocked. Please enable them in your browser settings.' , 'error' ) ;
$ ( '#twp-enable-notifications' ) . text ( '❌ Notifications Blocked' ) ;
}
} ) ;
}
/ * *
* Show browser notification
* /
function showBrowserNotification ( title , options = { } ) {
if ( notificationPermission !== 'granted' ) {
return ;
}
const defaultOptions = {
body : '' ,
icon : '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-icon.png' ,
badge : '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-badge.png' ,
vibrate : [ 200 , 100 , 200 ] ,
requireInteraction : true , // Keep notification visible until clicked
tag : 'twp-call-notification' ,
data : options . data || { }
} ;
const notificationOptions = Object . assign ( defaultOptions , options ) ;
try {
const notification = new Notification ( title , notificationOptions ) ;
// Handle notification click
notification . onclick = function ( event ) {
event . preventDefault ( ) ;
window . focus ( ) ;
notification . close ( ) ;
// If there's queue data, select that queue
if ( event . target . data && event . target . data . queueId ) {
selectQueue ( event . target . data . queueId ) ;
}
} ;
// Auto-close after 30 seconds if not required interaction
if ( ! notificationOptions . requireInteraction ) {
setTimeout ( function ( ) {
notification . close ( ) ;
} , 30000 ) ;
}
return notification ;
} catch ( error ) {
console . error ( 'Failed to show notification:' , error ) ;
// Fallback to service worker notification if available
if ( 'serviceWorker' in navigator && navigator . serviceWorker . controller ) {
navigator . serviceWorker . ready . then ( function ( registration ) {
registration . showNotification ( title , notificationOptions ) ;
} ) ;
}
}
}
/ * *
* Initialize page visibility handling
* /
function initializePageVisibility ( ) {
// Set up visibility change detection
let hidden , visibilityChange ;
if ( typeof document . hidden !== 'undefined' ) {
hidden = 'hidden' ;
visibilityChange = 'visibilitychange' ;
} else if ( typeof document . msHidden !== 'undefined' ) {
hidden = 'msHidden' ;
visibilityChange = 'msvisibilitychange' ;
} else if ( typeof document . webkitHidden !== 'undefined' ) {
hidden = 'webkitHidden' ;
visibilityChange = 'webkitvisibilitychange' ;
}
// Handle visibility change
document . addEventListener ( visibilityChange , function ( ) {
isPageVisible = ! document [ hidden ] ;
if ( isPageVisible ) {
console . log ( 'Page is now visible' ) ;
// Resume normal operations
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} else {
console . log ( 'Page is now hidden/background' ) ;
// Start more aggressive notifications for background
if ( alertEnabled && userQueues . some ( q => parseInt ( q . current _waiting ) > 0 ) ) {
startBackgroundAlerts ( ) ;
}
}
} , false ) ;
// Also handle window focus/blur for better mobile support
$ ( window ) . on ( 'focus' , function ( ) {
isPageVisible = true ;
if ( backgroundAlertInterval ) {
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} ) ;
$ ( window ) . on ( 'blur' , function ( ) {
isPageVisible = false ;
} ) ;
}
/ * *
* Start background alerts with notifications
* /
function startBackgroundAlerts ( ) {
if ( backgroundAlertInterval ) return ;
// Check and notify every 10 seconds when in background
backgroundAlertInterval = setInterval ( function ( ) {
const waitingQueues = userQueues . filter ( q => parseInt ( q . current _waiting ) > 0 ) ;
if ( waitingQueues . length > 0 && ! currentCall ) {
// Count total waiting calls
const totalWaiting = waitingQueues . reduce ( ( sum , q ) => sum + parseInt ( q . current _waiting ) , 0 ) ;
// Show browser notification
showBrowserNotification ( ` ${ totalWaiting } Call ${ totalWaiting > 1 ? 's' : '' } Waiting! ` , {
body : waitingQueues . map ( q => ` ${ q . queue _name } : ${ q . current _waiting } waiting ` ) . join ( '\n' ) ,
icon : '📞' ,
vibrate : [ 300 , 200 , 300 , 200 , 300 ] ,
requireInteraction : true ,
tag : 'queue-alert' ,
data : {
queueId : waitingQueues [ 0 ] . id
}
} ) ;
// Also try to play sound if possible
try {
playAlertSound ( ) ;
} catch ( e ) {
// Sound might be blocked in background
}
} else if ( waitingQueues . length === 0 ) {
// No more calls, stop background alerts
clearInterval ( backgroundAlertInterval ) ;
backgroundAlertInterval = null ;
}
} , 10000 ) ; // Every 10 seconds in background
}
2025-08-15 09:14:51 -07:00
// Load alert preference on init
loadAlertPreference ( ) ;
2025-08-13 13:45:25 -07:00
} ) ( jQuery ) ;