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...
00:00
@@ -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