/** * 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; // 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); } 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 */ 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 { 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(''); 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(); }); // 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); }); // 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'); } /** * 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('