diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index f80e6c9..3bad851 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -6996,6 +6996,7 @@ class TWP_Admin {
Ready
+
Connecting...
@@ -7434,7 +7435,211 @@ class TWP_Admin { var callStartTime = null; var tokenRefreshTimer = null; var tokenExpiry = null; - + var audioContext = null; + var ringtoneAudio = null; + var isPageVisible = true; + var deviceConnectionState = 'disconnected'; // disconnected, connecting, connected + var serviceWorkerRegistration = null; + + // Initialize AudioContext for mobile audio playback + function initializeAudioContext() { + try { + if (!audioContext) { + // Create AudioContext with compatibility + var AudioContextClass = window.AudioContext || window.webkitAudioContext; + audioContext = new AudioContextClass(); + console.log('AudioContext created, state:', audioContext.state); + } + + // Resume AudioContext if suspended (required on mobile) + if (audioContext.state === 'suspended') { + audioContext.resume().then(function() { + console.log('AudioContext resumed successfully'); + }).catch(function(err) { + console.error('Failed to resume AudioContext:', err); + }); + } + + return true; + } catch (error) { + console.error('Failed to initialize AudioContext:', error); + return false; + } + } + + // Create and setup ringtone audio element + function setupRingtone() { + if (!ringtoneAudio) { + ringtoneAudio = new Audio(); + // Use a simple sine wave tone or default ringtone + // For now, we'll use a data URI for a simple beep tone + ringtoneAudio.loop = true; + ringtoneAudio.volume = 0.7; + + // Create a simple ringtone using Web Audio API + createRingtone(); + } + } + + // Create ringtone using Web Audio API for better mobile support + function createRingtone() { + // Use a simple base64-encoded beep tone (short MP3) + // This is a simple 1-second beep at 800Hz + // You can replace this with a custom ringtone file URL if you upload one + + // For now, use a simple approach: HTML5 Audio with error fallback + // Note: On mobile, audio playback may be restricted, so we rely heavily on vibration + var ringtoneUrl = ''; + + // Try to load the ringtone file + ringtoneAudio.src = ringtoneUrl; + + // Fallback: if ringtone file fails to load, we'll just use vibration + ringtoneAudio.addEventListener('error', function(e) { + console.log('Ringtone file not found (this is normal), using vibration only for mobile'); + // Don't show error - vibration is sufficient for mobile + }, { once: true }); + + // Try to preload + ringtoneAudio.load(); + } + + // Play ringtone for incoming call + function playRingtone() { + try { + // Initialize AudioContext on user interaction + initializeAudioContext(); + + if (ringtoneAudio) { + var playPromise = ringtoneAudio.play(); + + if (playPromise !== undefined) { + playPromise.then(function() { + console.log('Ringtone playing'); + }).catch(function(error) { + console.error('Ringtone play failed:', error); + // Fallback: vibrate on mobile + vibrateDevice([300, 200, 300, 200, 300]); + }); + } + } + + // Always vibrate on mobile for better notification + vibrateDevice([300, 200, 300, 200, 300]); + + } catch (error) { + console.error('Error playing ringtone:', error); + } + } + + // Stop ringtone + function stopRingtone() { + try { + if (ringtoneAudio) { + ringtoneAudio.pause(); + ringtoneAudio.currentTime = 0; + } + } catch (error) { + console.error('Error stopping ringtone:', error); + } + } + + // Vibrate device (mobile) + function vibrateDevice(pattern) { + if ('vibrate' in navigator) { + navigator.vibrate(pattern); + } + } + + // Register service worker for PWA notifications + function registerServiceWorker() { + if ('serviceWorker' in navigator) { + var swPath = ''; + + navigator.serviceWorker.register(swPath) + .then(function(registration) { + console.log('Service Worker registered:', registration); + serviceWorkerRegistration = registration; + + // Request notification permission + if ('Notification' in window && Notification.permission === 'default') { + Notification.requestPermission().then(function(permission) { + console.log('Notification permission:', permission); + }); + } + }) + .catch(function(error) { + console.error('Service Worker registration failed:', error); + }); + } + } + + // Send notification via service worker + function sendIncomingCallNotification(callerNumber) { + // Try browser notification first + 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: '', + tag: 'incoming-call', + requireInteraction: true + }); + } else { + // Fallback: show notification directly + new Notification('Incoming Call', { + body: 'Call from ' + (callerNumber || 'Unknown Number'), + icon: '', + tag: 'incoming-call', + requireInteraction: true + }); + } + } + } + + // Monitor page visibility for background call handling + function setupPageVisibility() { + document.addEventListener('visibilitychange', function() { + isPageVisible = !document.hidden; + console.log('Page visibility changed:', isPageVisible ? 'visible' : 'hidden'); + + // If page becomes visible, resume audio context + if (isPageVisible && audioContext) { + initializeAudioContext(); + } + }); + } + + // Update device connection status in UI + function updateConnectionStatus(state) { + deviceConnectionState = state; + var statusText = ''; + var statusColor = ''; + + switch(state) { + case 'connected': + statusText = 'Connected'; + statusColor = '#4CAF50'; + break; + case 'connecting': + statusText = 'Connecting...'; + statusColor = '#FF9800'; + break; + case 'disconnected': + statusText = 'Disconnected'; + statusColor = '#f44336'; + break; + default: + statusText = 'Unknown'; + statusColor = '#999'; + } + + // Update status indicator (we'll add this to the UI) + $('#device-connection-status').text(statusText).css('color', statusColor); + } + // Wait for SDK to load function waitForTwilioSDK(callback) { if (typeof Twilio !== 'undefined' && Twilio.Device) { @@ -7450,7 +7655,19 @@ class TWP_Admin { // Initialize the browser phone function initializeBrowserPhone() { $('#phone-status').text('Initializing...'); - + updateConnectionStatus('connecting'); + + // Initialize audio and PWA features + setupRingtone(); + registerServiceWorker(); + setupPageVisibility(); + + // Initialize AudioContext on first user interaction + $(document).one('click touchstart', function() { + console.log('User interaction detected, initializing AudioContext'); + initializeAudioContext(); + }); + // Wait for SDK before proceeding waitForTwilioSDK(function() { // Get capability token (access token for v2) @@ -7468,9 +7685,11 @@ class TWP_Admin { // WordPress wp_send_json_error sends the error message as response.data var errorMsg = response.data || response.error || 'Unknown error'; showError('Failed to initialize: ' + errorMsg); + updateConnectionStatus('disconnected'); } }).fail(function() { showError('Failed to connect to server'); + updateConnectionStatus('disconnected'); }); }); } @@ -7517,25 +7736,57 @@ class TWP_Admin { if (typeof Twilio === 'undefined' || !Twilio.Device) { throw new Error('Twilio Voice SDK not loaded'); } - + + console.log('Setting up Twilio Device...'); + updateConnectionStatus('connecting'); + // Request media permissions before setting up device const hasPermissions = await requestMediaPermissions(); if (!hasPermissions) { + updateConnectionStatus('disconnected'); return; // Stop setup if permissions denied } - + // Clean up existing device if any if (device) { + console.log('Destroying existing device'); await device.destroy(); } - + + // Detect if we're on Android/mobile for specific settings + var isAndroid = /Android/i.test(navigator.userAgent); + var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); + + console.log('Device detection - Android:', isAndroid, 'Mobile:', isMobile); + + // Android-specific audio constraints for better WebRTC performance + var audioConstraints = { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true + }; + + // Additional Android-specific settings + if (isAndroid) { + audioConstraints.googEchoCancellation = true; + audioConstraints.googNoiseSuppression = true; + audioConstraints.googAutoGainControl = true; + audioConstraints.googHighpassFilter = true; + } + // Setup Twilio Voice SDK v2 Device // Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device device = new Twilio.Device(token, { logLevel: 1, // 0 = TRACE, 1 = DEBUG codecPreferences: ['opus', 'pcmu'], - edge: 'sydney' // Or closest edge location + edge: 'sydney', // Or closest edge location + enableIceRestart: true, // Important for mobile network switching + audioConstraints: audioConstraints, + maxCallSignalingTimeoutMs: 30000, // 30 seconds timeout for mobile + closeProtection: true // Warn before closing during call }); + + console.log('Twilio Device created with audio constraints:', audioConstraints); // Set up event handlers BEFORE registering // Device registered and ready @@ -7543,14 +7794,23 @@ class TWP_Admin { console.log('Device registered successfully'); $('#phone-status').text('Ready').css('color', '#4CAF50'); $('#call-btn').prop('disabled', false); + updateConnectionStatus('connected'); }); - + + // Device unregistered + device.on('unregistered', function() { + console.log('Device unregistered'); + updateConnectionStatus('disconnected'); + }); + // Handle errors device.on('error', function(error) { console.error('Twilio Device Error:', error); - + console.error('Error code:', error.code, 'Message:', error.message); + updateConnectionStatus('disconnected'); + var errorMsg = error.message || error.toString(); - + // Provide specific help for common errors if (errorMsg.includes('valid callerId must be provided')) { errorMsg = 'Caller ID error: Make sure you select a verified Twilio phone number as Caller ID. The number must be purchased through your Twilio account.'; @@ -7560,23 +7820,48 @@ class TWP_Admin { errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.'; // Try to reinitialize after token error setTimeout(initializeBrowserPhone, 5000); + } else if (errorMsg.includes('31005') || errorMsg.includes('Connection error')) { + errorMsg = 'Connection error: Check your internet connection. If on mobile, try switching between WiFi and cellular data.'; + // Retry connection + setTimeout(function() { + if (device) { + console.log('Attempting to reconnect device...'); + device.register(); + } + }, 3000); } - + showError(errorMsg); }); - + // Handle incoming calls device.on('incoming', function(call) { + console.log('Incoming call from:', call.parameters.From); + console.log('Call SID:', call.parameters.CallSid); + console.log('Device connection state:', deviceConnectionState); + currentCall = call; + var callerNumber = call.parameters.From || 'Unknown Number'; + $('#phone-status').text('Incoming Call').css('color', '#FF9800'); - $('#phone-number-display').text(call.parameters.From || 'Unknown Number'); + $('#phone-number-display').text(callerNumber); $('#call-btn').hide(); $('#answer-btn').show(); - + + // Play ringtone and show notification + playRingtone(); + + // If page is in background, send notification + if (!isPageVisible) { + console.log('Page in background, sending notification'); + sendIncomingCallNotification(callerNumber); + } + // Setup call event handlers setupCallHandlers(call); - + if ($('#auto-answer').is(':checked')) { + console.log('Auto-answer enabled, accepting call'); call.accept(); } }); @@ -7599,6 +7884,8 @@ class TWP_Admin { function setupCallHandlers(call) { // Call accepted/connected call.on('accept', function() { + console.log('Call accepted and connected'); + stopRingtone(); $('#phone-status').text('Connected').css('color', '#2196F3'); $('#call-btn').hide(); $('#answer-btn').hide(); @@ -7607,9 +7894,11 @@ class TWP_Admin { $('#admin-call-controls-panel').show(); startCallTimer(); }); - + // Call disconnected call.on('disconnect', function() { + console.log('Call disconnected'); + stopRingtone(); currentCall = null; $('#phone-status').text('Ready').css('color', '#4CAF50'); $('#hangup-btn').hide(); @@ -7619,22 +7908,26 @@ class TWP_Admin { $('#admin-call-controls-panel').hide(); $('#call-timer').hide(); stopCallTimer(); - + // Reset button states $('#admin-hold-btn').text('Hold').removeClass('btn-active'); $('#admin-record-btn').text('Record').removeClass('btn-active'); }); - + // Call rejected call.on('reject', function() { + console.log('Call rejected'); + stopRingtone(); currentCall = null; $('#phone-status').text('Ready').css('color', '#4CAF50'); $('#answer-btn').hide(); $('#call-btn').show(); }); - + // Call cancelled (by caller before answer) call.on('cancel', function() { + console.log('Call cancelled by caller'); + stopRingtone(); currentCall = null; $('#phone-status').text('Missed Call').css('color', '#FF9800'); $('#answer-btn').hide(); @@ -7643,6 +7936,26 @@ class TWP_Admin { $('#phone-status').text('Ready').css('color', '#4CAF50'); }, 3000); }); + + // Call error + call.on('error', function(error) { + console.error('Call error:', error); + console.error('Error code:', error.code, 'Message:', error.message); + stopRingtone(); + + var errorMsg = error.message || error.toString(); + + // Specific error handling for Android/mobile + if (error.code === 31005) { + errorMsg = 'Connection failed: Check your network connection. Try switching between WiFi and cellular data.'; + } else if (error.code === 31201 || error.code === 31204) { + errorMsg = 'Call setup failed: Please try again. If the problem persists, refresh the page.'; + } else if (error.code === 31208) { + errorMsg = 'Media connection failed: Check microphone permissions and try again.'; + } + + showError('Call error: ' + errorMsg); + }); } function refreshToken() { @@ -7753,11 +8066,15 @@ class TWP_Admin { $('#caller-id-select').html(''); }); - // Dialpad functionality - $('.dialpad-btn').on('click', function() { + // Dialpad functionality (support both click and touch events) + $('.dialpad-btn').on('click touchend', function(e) { + e.preventDefault(); // Prevent duplicate events var digit = $(this).data('digit'); var currentVal = $('#phone-number-input').val(); $('#phone-number-input').val(currentVal + digit); + + // Initialize AudioContext on user interaction (mobile requirement) + initializeAudioContext(); }); // Call button @@ -7818,8 +8135,45 @@ class TWP_Admin { // Answer button $('#answer-btn').on('click', function() { - if (currentCall) { + console.log('Answer button clicked'); + console.log('Device connection state:', deviceConnectionState); + console.log('Current call:', currentCall); + + if (!currentCall) { + console.error('No current call to answer'); + showError('No incoming call to answer'); + return; + } + + // Check device connection state + if (deviceConnectionState !== 'connected') { + console.error('Device not connected, state:', deviceConnectionState); + showError('Phone not connected. Reconnecting...'); + + // Try to reconnect + if (device) { + device.register().then(function() { + console.log('Device reconnected, answering call'); + if (currentCall) { + currentCall.accept(); + } + }).catch(function(err) { + console.error('Failed to reconnect device:', err); + showError('Failed to reconnect. Please refresh the page.'); + }); + } + return; + } + + // Initialize AudioContext before accepting (important for mobile) + initializeAudioContext(); + + try { + console.log('Accepting call...'); currentCall.accept(); + } catch (error) { + console.error('Error accepting call:', error); + showError('Failed to answer call: ' + error.message); } }); diff --git a/assets/images/README.md b/assets/images/README.md new file mode 100644 index 0000000..6b831d2 --- /dev/null +++ b/assets/images/README.md @@ -0,0 +1,42 @@ +# Browser Phone Images + +## Required Images for PWA Notifications + +Place the following image files in this directory for browser push notifications: + +### phone-icon.png +- **Purpose**: Main notification icon +- **Size**: 192x192 pixels (recommended) +- **Format**: PNG with transparency +- **Usage**: Shown in browser notifications for incoming calls + +### phone-badge.png +- **Purpose**: Small badge icon for notifications +- **Size**: 96x96 pixels or 72x72 pixels +- **Format**: PNG with transparency +- **Usage**: Displayed in the notification badge area (Android) + +## Fallback Behavior + +If these images are not present, the browser will: +- Use a default browser notification icon +- Still display text-based notifications +- Functionality will not be affected + +## Image Creation Tips + +- Use a simple, recognizable phone icon +- Ensure good contrast for visibility +- Test on both light and dark backgrounds +- Transparent backgrounds work best +- Use a consistent color scheme with your brand + +## Example Resources + +- Google Material Icons: https://fonts.google.com/icons +- Font Awesome: https://fontawesome.com/ +- Flaticon: https://www.flaticon.com/ +- Or create custom icons using tools like: + - Figma + - Adobe Illustrator + - Inkscape (free) diff --git a/assets/js/twp-service-worker.js b/assets/js/twp-service-worker.js index 4daa4c6..e0acde5 100644 --- a/assets/js/twp-service-worker.js +++ b/assets/js/twp-service-worker.js @@ -72,7 +72,23 @@ self.addEventListener('push', function(event) { // Handle messages from the main script self.addEventListener('message', function(event) { + console.log('TWP Service Worker: Message received', event.data); + if (event.data && event.data.type === 'SHOW_NOTIFICATION') { - self.registration.showNotification(event.data.title, event.data.options); + const options = { + body: event.data.body || 'You have a new call waiting', + icon: event.data.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: [300, 200, 300, 200, 300], + tag: event.data.tag || 'incoming-call', + requireInteraction: event.data.requireInteraction !== undefined ? event.data.requireInteraction : true, + data: event.data.data || {} + }; + + console.log('TWP Service Worker: Showing notification', event.data.title, options); + + event.waitUntil( + self.registration.showNotification(event.data.title || 'Incoming Call', options) + ); } }); \ No newline at end of file diff --git a/assets/sounds/README.md b/assets/sounds/README.md new file mode 100644 index 0000000..a1f93af --- /dev/null +++ b/assets/sounds/README.md @@ -0,0 +1,36 @@ +# Ringtone Audio Files + +## Custom Ringtone + +To add a custom ringtone for incoming calls in the browser phone: + +1. Place your ringtone audio file in this directory +2. Name it: `ringtone.mp3` +3. Recommended format: MP3, under 5 seconds, loopable + +## Fallback Behavior + +If no ringtone file is present, the browser phone will: +- Use device vibration on mobile devices (Android, iOS) +- Rely on browser notifications to alert users +- Display visual indicators in the browser interface + +## Supported Audio Formats + +- **MP3** (recommended) - Best compatibility across browsers +- **OGG** - Good for Firefox, Chrome +- **WAV** - Larger file size but universal support + +## File Requirements + +- Maximum file size: 100KB recommended +- Duration: 1-5 seconds (will loop) +- Sample rate: 44.1kHz or 48kHz +- Bitrate: 128kbps or higher + +## Example Ringtone Sources + +You can find free ringtone files at: +- https://freesound.org/ +- https://incompetech.com/ +- Or create your own using Audacity or similar tools