// Configuration injected by PHP template via twpConfig global var ajaxurl = window.twpConfig.ajaxUrl; var twpNonce = window.twpConfig.nonce; var twpRingtoneUrl = window.twpConfig.ringtoneUrl; var twpPhoneIconUrl = window.twpConfig.phoneIconUrl; var twpSwUrl = window.twpConfig.swUrl; var twpTwilioEdge = window.twpConfig.twilioEdge; (function($) { // ============================================================ // Flutter WebView Bridge // ============================================================ window.TwpMobile = window.TwpMobile || {}; /** Flutter injects FCM token via this method. */ window.TwpMobile.getFcmToken = function() { return window.TwpMobile._fcmToken || null; }; window.TwpMobile.setFcmToken = function(token) { window.TwpMobile._fcmToken = token; // Register FCM token with server via WP AJAX (uses cookie auth) $.post(ajaxurl, { action: 'twp_register_fcm_token', nonce: twpNonce, fcm_token: token }).fail(function() { console.warn('TWP: FCM token registration failed'); }); }; /** Flutter calls this when a notification is tapped. */ window.TwpMobile.onNotificationTap = function(data) { // Switch to phone tab and focus. switchTab('phone'); if (data && data.caller) { $('#phone-number-input').val(data.caller); } }; /** Notify Flutter that page is ready (via webview_flutter JavaScriptChannel). */ function notifyFlutterReady() { try { if (window.TwpMobile && window.TwpMobile.postMessage) { window.TwpMobile.postMessage('onPageReady'); } } catch (e) { /* not in WebView */ } } /** Notify Flutter that session has expired. */ function notifyFlutterSessionExpired() { try { if (window.TwpMobile && window.TwpMobile.postMessage) { window.TwpMobile.postMessage('onSessionExpired'); } } catch (e) { /* not in WebView */ } } /** * Wrapper around $.post that detects session expiration. */ function twpPost(data, successCb, failCb) { return $.post(ajaxurl, data, function(response) { if (successCb) successCb(response); }).fail(function(xhr) { // Detect login redirect / 403 if (xhr.status === 403 || (xhr.responseText && xhr.responseText.indexOf('wp-login') !== -1)) { notifyFlutterSessionExpired(); } if (failCb) failCb(xhr); }); } // ============================================================ // Tab Navigation // ============================================================ function switchTab(name) { $('.tab-btn').removeClass('active'); $('.tab-btn[data-tab="' + name + '"]').addClass('active'); $('.tab-pane').removeClass('active'); $('#tab-' + name).addClass('active'); } $('.tab-btn').on('click', function() { switchTab($(this).data('tab')); }); // ============================================================ // Notices // ============================================================ function showNotice(message, type) { var cls = 'twp-notice twp-notice-' + (type || 'info'); var $el = $('
Error: ' + message + '
').show(); $('#phone-status').text('Error').css('color', 'var(--danger)'); } // ============================================================ // Core phone state // ============================================================ var device = null; var currentCall = null; var callTimer = null; var callStartTime = null; var tokenRefreshTimer = null; var tokenExpiry = null; var audioContext = null; var ringtoneAudio = null; var isPageVisible = true; var deviceConnectionState = 'disconnected'; var serviceWorkerRegistration = null; var currentCallDirection = null; var callHistory = []; // ============================================================ // AudioContext & Ringtone // ============================================================ function initializeAudioContext() { try { if (!audioContext) { var AC = window.AudioContext || window.webkitAudioContext; audioContext = new AC(); } if (audioContext.state === 'suspended') { audioContext.resume().catch(function() {}); } return true; } catch (e) { return false; } } function setupRingtone() { if (!ringtoneAudio) { ringtoneAudio = new Audio(); ringtoneAudio.loop = true; ringtoneAudio.volume = 0.7; ringtoneAudio.src = twpRingtoneUrl; ringtoneAudio.addEventListener('error', function() {}, { once: true }); ringtoneAudio.load(); } } function playRingtone() { try { initializeAudioContext(); if (ringtoneAudio) { var p = ringtoneAudio.play(); if (p !== undefined) p.catch(function() { vibrateDevice([300,200,300,200,300]); }); } vibrateDevice([300,200,300,200,300]); } catch (e) {} } function stopRingtone() { try { if (ringtoneAudio) { ringtoneAudio.pause(); ringtoneAudio.currentTime = 0; } } catch (e) {} } function vibrateDevice(pattern) { if ('vibrate' in navigator) navigator.vibrate(pattern); } // ============================================================ // Service Worker & Notifications // ============================================================ function registerServiceWorker() { if ('serviceWorker' in navigator) { navigator.serviceWorker.register(twpSwUrl).then(function(reg) { serviceWorkerRegistration = reg; if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } }).catch(function() {}); } } function sendIncomingCallNotification(callerNumber) { if ('Notification' in window && Notification.permission === 'granted') { if (serviceWorkerRegistration && serviceWorkerRegistration.active) { serviceWorkerRegistration.active.postMessage({ type: 'SHOW_NOTIFICATION', title: 'Incoming Call', body: 'Call from ' + (callerNumber || 'Unknown Number'), icon: twpPhoneIconUrl, tag: 'incoming-call', requireInteraction: true }); } else { new Notification('Incoming Call', { body: 'Call from ' + (callerNumber || 'Unknown Number'), icon: twpPhoneIconUrl, tag: 'incoming-call', requireInteraction: true }); } } } // ============================================================ // Page Visibility // ============================================================ function setupPageVisibility() { document.addEventListener('visibilitychange', function() { isPageVisible = !document.hidden; if (isPageVisible && audioContext) initializeAudioContext(); }); } // ============================================================ // Connection Status // ============================================================ function updateConnectionStatus(state) { deviceConnectionState = state; var text = '', color = ''; switch (state) { case 'connected': text = 'Connected'; color = 'var(--success)'; break; case 'connecting': text = 'Connecting...'; color = 'var(--warning)'; break; case 'disconnected': text = 'Disconnected'; color = 'var(--danger)'; break; default: text = 'Unknown'; color = 'var(--text-secondary)'; } $('#device-connection-status').text(text).css('color', color); } // ============================================================ // Twilio Device Setup // ============================================================ function waitForTwilioSDK(cb) { if (typeof Twilio !== 'undefined' && Twilio.Device) { cb(); } else { setTimeout(function() { waitForTwilioSDK(cb); }, 100); } } function initializeBrowserPhone() { $('#phone-status').text('Initializing...'); updateConnectionStatus('connecting'); setupRingtone(); registerServiceWorker(); setupPageVisibility(); $(document).one('click touchstart', function() { initializeAudioContext(); }); waitForTwilioSDK(function() { twpPost({ action: 'twp_generate_capability_token', nonce: twpNonce }, function(response) { if (response.success) { $('#browser-phone-error').hide(); setupTwilioDevice(response.data.token); tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } else { var msg = response.data || 'Unknown error'; showError('Failed to initialize: ' + msg); updateConnectionStatus('disconnected'); } }, function() { showError('Failed to connect to server'); updateConnectionStatus('disconnected'); }); }); } async function requestMediaPermissions() { try { var stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); stream.getTracks().forEach(function(t) { t.stop(); }); return true; } catch (error) { var msg = 'Microphone access is required. '; if (error.name === 'NotAllowedError') msg += 'Please allow microphone access.'; else if (error.name === 'NotFoundError') msg += 'No microphone found.'; else msg += 'Check browser settings.'; showError(msg); return false; } } async function setupTwilioDevice(token) { try { if (typeof Twilio === 'undefined' || !Twilio.Device) throw new Error('Twilio Voice SDK not loaded'); updateConnectionStatus('connecting'); var hasPerms = await requestMediaPermissions(); if (!hasPerms) { updateConnectionStatus('disconnected'); return; } if (device) { await device.destroy(); } var isAndroid = /Android/i.test(navigator.userAgent); var audioConstraints = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }; if (isAndroid) { audioConstraints.googEchoCancellation = true; audioConstraints.googNoiseSuppression = true; audioConstraints.googAutoGainControl = true; audioConstraints.googHighpassFilter = true; } device = new Twilio.Device(token, { logLevel: 1, codecPreferences: ['opus', 'pcmu'], edge: twpTwilioEdge, enableIceRestart: true, audioConstraints: audioConstraints, maxCallSignalingTimeoutMs: 30000, closeProtection: true }); device.on('registered', function() { $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#call-btn').prop('disabled', false); updateConnectionStatus('connected'); }); device.on('unregistered', function() { updateConnectionStatus('disconnected'); }); device.on('error', function(error) { updateConnectionStatus('disconnected'); var msg = error.message || error.toString(); if (msg.includes('valid callerId')) { msg = 'Select a verified Twilio phone number as Caller ID.'; } else if (msg.includes('token') || msg.includes('Token')) { msg = 'Token error: ' + msg; setTimeout(initializeBrowserPhone, 5000); } else if (msg.includes('31005') || msg.includes('Connection error')) { msg = 'Connection error. Check internet connection.'; setTimeout(function() { if (device) device.register(); }, 3000); } showError(msg); }); device.on('incoming', function(call) { currentCall = call; currentCallDirection = 'inbound'; var caller = call.parameters.From || 'Unknown'; $('#phone-status').text('Incoming Call').css('color', 'var(--warning)'); $('#phone-number-display').text(caller); $('#call-btn').hide(); $('#answer-btn').show(); playRingtone(); if (!isPageVisible) sendIncomingCallNotification(caller); setupCallHandlers(call); // Switch to phone tab on incoming call switchTab('phone'); if ($('#auto-answer').is(':checked')) call.accept(); }); device.on('tokenWillExpire', function() { refreshToken(); }); await device.register(); } catch (error) { showError('Failed to setup device: ' + error.message); } } // ============================================================ // Call Handlers // ============================================================ function setupCallHandlers(call) { call.on('accept', function() { stopRingtone(); $('#phone-status').text('Connected').css('color', 'var(--accent)'); $('#call-btn').hide(); $('#answer-btn').hide(); $('#hangup-btn').show(); $('#admin-call-controls-panel').show(); startCallTimer(); }); call.on('disconnect', function() { stopRingtone(); // Capture call info for history before clearing var disconnectedNumber = $('#phone-number-display').text() || $('#phone-number-input').val(); var callDuration = $('#call-timer').text(); if (disconnectedNumber && callStartTime) { addToCallHistory(disconnectedNumber, currentCallDirection || 'outbound', callDuration); } currentCall = null; currentCallDirection = null; $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#hangup-btn').hide(); $('#answer-btn').hide(); $('#call-btn').show(); $('#admin-call-controls-panel').hide(); $('#call-timer').hide(); stopCallTimer(); $('#phone-number-input').val(''); $('#phone-number-display').text(''); $('#admin-hold-btn').html('⏸ Hold').removeClass('btn-active'); $('#admin-record-btn').html('⏺ Record').removeClass('btn-active'); adminIsOnHold = false; adminIsRecording = false; adminRecordingSid = null; }); call.on('reject', function() { stopRingtone(); currentCall = null; $('#phone-status').text('Ready').css('color', 'var(--success)'); $('#answer-btn').hide(); $('#call-btn').show(); }); call.on('cancel', function() { stopRingtone(); currentCall = null; $('#phone-status').text('Missed Call').css('color', 'var(--warning)'); $('#answer-btn').hide(); $('#call-btn').show(); setTimeout(function() { $('#phone-status').text('Ready').css('color', 'var(--success)'); }, 3000); }); call.on('error', function(error) { stopRingtone(); var msg = error.message || error.toString(); if (error.code === 31005) msg = 'Connection failed. Check network.'; else if (error.code === 31201 || error.code === 31204) msg = 'Call setup failed. Try again.'; else if (error.code === 31208) msg = 'Media failed. Check microphone.'; showError('Call error: ' + msg); }); } // ============================================================ // Token Refresh // ============================================================ function refreshToken() { if (currentCall) { setTimeout(refreshToken, 60000); return; } twpPost({ action: 'twp_generate_capability_token', nonce: twpNonce }, function(response) { if (response.success && device) { device.updateToken(response.data.token); tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; scheduleTokenRefresh(); } else { showError('Failed to refresh connection. Please reload.'); } }, function() { setTimeout(refreshToken, 30000); }); } function scheduleTokenRefresh() { if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer); if (!tokenExpiry) return; var ms = tokenExpiry - Date.now() - 5 * 60 * 1000; if (ms <= 0) refreshToken(); else tokenRefreshTimer = setTimeout(refreshToken, ms); } // ============================================================ // Timer // ============================================================ function startCallTimer() { callStartTime = new Date(); $('#call-timer').show(); callTimer = setInterval(function() { var s = Math.floor((new Date() - callStartTime) / 1000); var m = Math.floor(s / 60); s = s % 60; $('#call-timer').text((m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s); }, 1000); } function stopCallTimer() { if (callTimer) { clearInterval(callTimer); callTimer = null; } $('#call-timer').text('00:00'); } // ============================================================ // Caller ID loading // ============================================================ twpPost({ action: 'twp_get_phone_numbers', nonce: twpNonce }, function(response) { if (response.success) { var opts = ''; response.data.forEach(function(n) { opts += ''; }); $('#caller-id-select').html(opts); // Restore saved caller ID from localStorage var savedCallerId = localStorage.getItem('twp_caller_id'); if (savedCallerId) { $('#caller-id-select').val(savedCallerId); } } else { $('#caller-id-select').html(''); } }, function() { $('#caller-id-select').html(''); }); // Persist caller ID selection to localStorage $('#caller-id-select').on('change', function() { localStorage.setItem('twp_caller_id', $(this).val()); }); // ============================================================ // Dialpad // ============================================================ $('.dialpad-btn').on('click touchend', function(e) { e.preventDefault(); var digit = $(this).data('digit'); $('#phone-number-input').val($('#phone-number-input').val() + digit); initializeAudioContext(); // Send DTMF during active call if (currentCall) { currentCall.sendDigits(String(digit)); } }); // ============================================================ // Call / Hangup / Answer // ============================================================ $('#call-btn').on('click', async function() { var num = $('#phone-number-input').val().trim(); var cid = $('#caller-id-select').val(); if (!num) { showNotice('Enter a phone number', 'error'); return; } if (!cid) { showNotice('Select a caller ID', 'error'); return; } if (!device) { showNotice('Phone not initialized. Reload page.', 'error'); return; } num = num.replace(/\D/g, ''); if (num.length === 10) num = '+1' + num; else if (num.length === 11 && num.charAt(0) === '1') num = '+' + num; else if (!num.startsWith('+')) num = '+' + num; $('#phone-number-display').text(num); $('#phone-status').text('Calling...').css('color', 'var(--warning)'); currentCallDirection = 'outbound'; try { currentCall = await device.connect({ params: { To: num, From: cid } }); setupCallHandlers(currentCall); } catch (err) { showError('Failed to call: ' + err.message); $('#phone-status').text('Ready').css('color', 'var(--success)'); } }); $('#hangup-btn').on('click', function() { if (currentCall) currentCall.disconnect(); }); $('#answer-btn').on('click', function() { if (!currentCall) { showError('No incoming call'); return; } if (deviceConnectionState !== 'connected') { showError('Phone not connected. Reconnecting...'); if (device) device.register().then(function() { if (currentCall) currentCall.accept(); }).catch(function() { showError('Reconnect failed. Reload page.'); }); return; } initializeAudioContext(); try { currentCall.accept(); } catch (e) { showError('Failed to answer: ' + e.message); } }); // ============================================================ // Call Controls: Hold / Transfer / Requeue / Record // ============================================================ var adminIsOnHold = false; var adminIsRecording = false; var adminRecordingSid = null; function getCallSid() { if (!currentCall) return null; return currentCall.parameters.CallSid || (currentCall.customParameters && currentCall.customParameters.CallSid) || currentCall.outgoingConnectionId || currentCall.sid; } $('#admin-hold-btn').on('click', function() { var sid = getCallSid(); if (!sid) return; var $btn = $(this); twpPost({ action: 'twp_toggle_hold', call_sid: sid, hold: !adminIsOnHold, nonce: twpNonce }, function(r) { if (r.success) { adminIsOnHold = !adminIsOnHold; $btn.html(adminIsOnHold ? '▶ Unhold' : '⏸ Hold').toggleClass('btn-active', adminIsOnHold); showNotice(adminIsOnHold ? 'Call on hold' : 'Call resumed', 'info'); } else { showNotice('Hold failed: ' + (r.data || ''), 'error'); } }); }); $('#admin-transfer-btn').on('click', function() { if (!currentCall) return; twpPost({ action: 'twp_get_transfer_targets', nonce: twpNonce }, function(r) { if (r.success && r.data && (r.data.users || r.data.queues)) { showEnhancedTransferDialog(r.data); } else { twpPost({ action: 'twp_get_online_agents', nonce: twpNonce }, function(lr) { if (lr.success && lr.data.length > 0) showAgentTransferDialog(lr.data); else showManualTransferDialog(); }, function() { showManualTransferDialog(); }); } }, function() { showManualTransferDialog(); }); }); $('#admin-requeue-btn').on('click', function() { if (!currentCall) return; twpPost({ action: 'twp_get_all_queues', nonce: twpNonce }, function(r) { if (r.success && r.data.length > 0) showRequeueDialog(r.data); else showNotice('No queues available', 'error'); }, function() { showNotice('Failed to load queues', 'error'); }); }); $('#admin-record-btn').on('click', function() { if (!currentCall) return; if (adminIsRecording) stopRecording(); else startRecording(); }); function startRecording() { var sid = getCallSid(); if (!sid) { showNotice('Cannot determine call SID', 'error'); return; } twpPost({ action: 'twp_start_recording', call_sid: sid, nonce: twpNonce }, function(r) { if (r.success) { adminIsRecording = true; adminRecordingSid = r.data.recording_sid; $('#admin-record-btn').html('⏹ Stop Rec').addClass('btn-active'); showNotice('Recording started', 'success'); } else { showNotice('Recording failed: ' + (r.data || ''), 'error'); } }); } function stopRecording() { if (!adminRecordingSid) return; var sid = getCallSid() || ''; twpPost({ action: 'twp_stop_recording', call_sid: sid, recording_sid: adminRecordingSid, nonce: twpNonce }, function(r) { if (r.success) { adminIsRecording = false; adminRecordingSid = null; $('#admin-record-btn').html('⏺ Record').removeClass('btn-active'); showNotice('Recording stopped', 'info'); } else { showNotice('Stop recording failed: ' + (r.data || ''), 'error'); } }); } // ============================================================ // Transfer Dialogs // ============================================================ function closeDialog() { $('.twp-overlay, .twp-dialog').remove(); } function showEnhancedTransferDialog(data) { var html = 'Agents
'; data.users.forEach(function(u) { var status = u.is_logged_in ? '🟢 Online' : '🔴 Offline'; html += 'Queues
'; data.queues.forEach(function(q) { html += 'Manual
'; html += ''; html += 'Or enter number:
'; html += ''; html += 'Enter phone number:
'; html += ''; html += 'Select a queue:
'; queues.forEach(function(q) { html += '