/** * 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 = []; let userQueues = []; let selectedQueue = null; let tokenRefreshTimer = null; let tokenExpiry = null; let queuePollingTimer = null; let lastQueueUpdate = {}; let alertSound = null; let alertInterval = null; let alertEnabled = false; // 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(); loadUserQueues(); }); /** * Wait for Twilio SDK to load */ function waitForTwilioSDK(callback) { let attempts = 0; const maxAttempts = 150; // 15 seconds max (100ms * 150) function checkTwilio() { console.log('Checking Twilio SDK availability, attempt:', attempts + 1); if (typeof Twilio !== 'undefined' && Twilio.Device) { console.log('Twilio SDK loaded successfully'); callback(); return; } attempts++; // 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...'); } if (attempts >= maxAttempts) { console.error('Twilio SDK failed to load. Window.Twilio:', typeof Twilio); updateStatus('offline', 'SDK load timeout'); 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'); return; } setTimeout(checkTwilio, 100); } checkTwilio(); } /** * Initialize the browser phone */ function initializeBrowserPhone() { updateStatus('connecting', 'Initializing...'); // 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); // Set token expiry time (expires in 1 hour) tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } 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'); } }); }); } /** * Setup Twilio Device */ async function setupTwilioDevice(token) { // 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; } try { // If device already exists, destroy it first to prevent multiple connections if (twilioDevice) { twilioDevice.destroy(); twilioDevice = null; } twilioDevice = new Twilio.Device(token, { logLevel: 1, answerOnBridge: true }); twilioDevice.on('registered', function() { updateStatus('online', 'Ready'); isConnected = true; // Only show success message on initial connection if (!tokenRefreshTimer) { 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'); // If token expired error, refresh immediately if (error.message && error.message.toLowerCase().includes('token')) { refreshToken(); } }); twilioDevice.on('incoming', function(call) { handleIncomingCall(call); }); twilioDevice.on('disconnect', function() { updateStatus('offline', 'Disconnected'); isConnected = false; }); // Register device asynchronously await 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(''); return; } $select.append(''); availableNumbers.forEach(function(number) { const friendlyName = number.friendly_name || number.phone_number; $select.append(``); }); } /** * 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(); stopAlert(); // Stop alert when accepting call }); // Refresh queues button $('#twp-refresh-queues').on('click', function() { loadUserQueues(); }); // Alert toggle button $(document).on('click', '#twp-alert-toggle', function() { toggleAlert(); }); // Queue item selection $(document).on('click', '.queue-item', function() { const queueId = $(this).data('queue-id'); selectQueue(queueId); }); // 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 */ async function makeCall(number, callerId) { if (currentCall) { showMessage('Already in a call', 'error'); return; } // Stop alerts when making a call stopAlert(); updateCallState('connecting'); showCallInfo('Connecting...'); const params = { To: number, From: callerId }; try { console.log('Making call with params:', params); currentCall = await twilioDevice.connect({params: params}); // Setup call event handlers 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'); // 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 } } } /** * Load user's assigned queues */ function loadUserQueues(silent = false) { $.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) { // Check for new calls in queues checkForNewCalls(response.data); userQueues = response.data; displayQueues(); } else if (!silent) { showMessage('Failed to load queues: ' + (response.data || 'Unknown error'), 'error'); } }, error: function() { if (!silent) { showMessage('Failed to load queues', 'error'); } } }); } /** * Check for new calls in queues and trigger alerts */ function checkForNewCalls(newQueues) { newQueues.forEach(function(queue) { const queueId = queue.id; const currentWaiting = parseInt(queue.current_waiting) || 0; const previousWaiting = lastQueueUpdate[queueId] || 0; // If waiting count increased, we have new calls if (currentWaiting > previousWaiting) { console.log('New call detected in queue:', queue.queue_name); // Trigger alert if enabled if (alertEnabled && !currentCall) { startAlert(); } } lastQueueUpdate[queueId] = currentWaiting; }); } /** * Display queues in the UI */ function displayQueues() { const $queueList = $('#twp-queue-list'); if (userQueues.length === 0) { $queueList.html('
No queues assigned to you.
'); $('#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 += `
${queue.queue_name}
${waitingCount} waiting Max: ${queue.max_size}
`; }); $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 */ function acceptQueueCall() { if (!selectedQueue) { showMessage('Please select a queue first', 'error'); return; } $.ajax({ url: twp_frontend_ajax.ajax_url, method: 'POST', data: { action: 'twp_accept_next_queue_call', queue_id: selectedQueue.id, nonce: twp_frontend_ajax.nonce }, success: function(response) { if (response.success) { showMessage('Connecting to next caller...', 'info'); // Refresh queue data after accepting call setTimeout(loadUserQueues, 1000); } else { showMessage(response.data || 'No calls waiting in this queue', 'info'); } }, 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 = $('
').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); } } // Start queue polling with faster interval startQueuePolling(); /** * Start polling for queue updates */ function startQueuePolling() { // Clear any existing timer if (queuePollingTimer) { clearInterval(queuePollingTimer); } // Poll every 5 seconds for real-time updates queuePollingTimer = setInterval(function() { if (isConnected) { loadUserQueues(true); // Silent update } }, 5000); // Every 5 seconds } /** * Schedule token refresh * Refreshes token 10 minutes before expiry for safety */ function scheduleTokenRefresh() { // Clear any existing timer if (tokenRefreshTimer) { clearTimeout(tokenRefreshTimer); } if (!tokenExpiry) { console.error('Token expiry time not set'); // Retry in 30 seconds if token expiry not set setTimeout(function() { if (tokenExpiry) { scheduleTokenRefresh(); } }, 30000); return; } // Calculate time until refresh (10 minutes before expiry for extra safety) const refreshBuffer = 10 * 60 * 1000; // 10 minutes in milliseconds 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); } }); } /** * 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; // Play initial alert playAlertSound(); // Repeat every 30 seconds alertInterval = setInterval(function() { if (alertEnabled && !currentCall) { playAlertSound(); } 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(); } // Clean up on page unload $(window).on('beforeunload', function() { if (tokenRefreshTimer) { clearTimeout(tokenRefreshTimer); } if (queuePollingTimer) { clearInterval(queuePollingTimer); } if (alertInterval) { clearInterval(alertInterval); } if (twilioDevice) { twilioDevice.destroy(); } }); // Load alert preference on init loadAlertPreference(); })(jQuery);