Add automatic token refresh for browser phone to prevent timeouts

- Implement proactive token refresh 5 minutes before expiry (1-hour tokens)
- Add call-safe refresh logic that postpones refresh during active calls
- Replace fixed-interval refresh with smart scheduling based on token expiry
- Add proper cleanup on page unload to prevent memory leaks
- Enhance error handling with retry mechanisms for network failures
- Apply to both admin browser phone page and frontend shortcode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-14 12:01:05 -07:00
parent 8c78e0cb11
commit 2cb9b9472d
2 changed files with 165 additions and 4 deletions

View File

@@ -5691,6 +5691,8 @@ class TWP_Admin {
var currentCall = null; var currentCall = null;
var callTimer = null; var callTimer = null;
var callStartTime = null; var callStartTime = null;
var tokenRefreshTimer = null;
var tokenExpiry = null;
// Wait for SDK to load // Wait for SDK to load
function waitForTwilioSDK(callback) { function waitForTwilioSDK(callback) {
@@ -5718,6 +5720,9 @@ class TWP_Admin {
if (response.success) { if (response.success) {
$('#browser-phone-error').hide(); $('#browser-phone-error').hide();
setupTwilioDevice(response.data.token); setupTwilioDevice(response.data.token);
// Set token expiry and schedule refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else { } else {
// WordPress wp_send_json_error sends the error message as response.data // WordPress wp_send_json_error sends the error message as response.data
var errorMsg = response.data || response.error || 'Unknown error'; var errorMsg = response.data || response.error || 'Unknown error';
@@ -5852,18 +5857,65 @@ class TWP_Admin {
} }
function refreshToken() { 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');
setTimeout(refreshToken, 60000); // Retry in 1 minute
return;
}
$.post(ajaxurl, { $.post(ajaxurl, {
action: 'twp_generate_capability_token', action: 'twp_generate_capability_token',
nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>' nonce: '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'
}, function(response) { }, function(response) {
if (response.success && device) { if (response.success && device) {
console.log('Token refreshed successfully');
device.updateToken(response.data.token); device.updateToken(response.data.token);
// Update token expiry and schedule next refresh
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else {
console.error('Failed to refresh token:', response.data);
showError('Failed to refresh connection. Please refresh the page.');
} }
}).fail(function() { }).fail(function() {
console.error('Failed to refresh token'); console.error('Failed to refresh token - network error');
// Retry in 30 seconds
setTimeout(refreshToken, 30000);
}); });
} }
/**
* Schedule token refresh
* Refreshes token 5 minutes before expiry
*/
function scheduleTokenRefresh() {
// Clear any existing timer
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (!tokenExpiry) {
console.error('Token expiry time not set');
return;
}
// Calculate time until refresh (5 minutes before expiry)
var refreshBuffer = 5 * 60 * 1000; // 5 minutes in milliseconds
var 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);
}
}
function showError(message) { function showError(message) {
$('#browser-phone-error').html('<p><strong>Error:</strong> ' + message + '</p>').show(); $('#browser-phone-error').html('<p><strong>Error:</strong> ' + message + '</p>').show();
$('#phone-status').text('Error').css('color', '#f44336'); $('#phone-status').text('Error').css('color', '#f44336');
@@ -6005,8 +6057,15 @@ class TWP_Admin {
}, 1000); }, 1000);
}); });
// Refresh token every 50 minutes (tokens expire in 1 hour) // Clean up on page unload
setInterval(initializeBrowserPhone, 50 * 60 * 1000); $(window).on('beforeunload', function() {
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (device) {
device.destroy();
}
});
// Mode switching functionality // Mode switching functionality
$('input[name="call_mode"]').on('change', function() { $('input[name="call_mode"]').on('change', function() {

View File

@@ -13,6 +13,8 @@
let availableNumbers = []; let availableNumbers = [];
let userQueues = []; let userQueues = [];
let selectedQueue = null; let selectedQueue = null;
let tokenRefreshTimer = null;
let tokenExpiry = null;
// Initialize when document is ready // Initialize when document is ready
$(document).ready(function() { $(document).ready(function() {
@@ -86,6 +88,9 @@
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
setupTwilioDevice(response.data.token); setupTwilioDevice(response.data.token);
// Set token expiry time (expires in 1 hour)
tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000;
scheduleTokenRefresh();
} else { } else {
updateStatus('offline', 'Failed to initialize'); updateStatus('offline', 'Failed to initialize');
showMessage('Failed to initialize browser phone: ' + (response.data || 'Unknown error'), 'error'); showMessage('Failed to initialize browser phone: ' + (response.data || 'Unknown error'), 'error');
@@ -112,6 +117,12 @@
} }
try { try {
// If device already exists, destroy it first to prevent multiple connections
if (twilioDevice) {
twilioDevice.destroy();
twilioDevice = null;
}
twilioDevice = new Twilio.Device(token, { twilioDevice = new Twilio.Device(token, {
logLevel: 1, logLevel: 1,
answerOnBridge: true answerOnBridge: true
@@ -120,13 +131,21 @@
twilioDevice.on('registered', function() { twilioDevice.on('registered', function() {
updateStatus('online', 'Ready'); updateStatus('online', 'Ready');
isConnected = true; isConnected = true;
showMessage('Browser phone ready!', 'success'); // Only show success message on initial connection
if (!tokenRefreshTimer) {
showMessage('Browser phone ready!', 'success');
}
}); });
twilioDevice.on('error', function(error) { twilioDevice.on('error', function(error) {
console.error('Twilio Device Error:', error); console.error('Twilio Device Error:', error);
updateStatus('offline', 'Error: ' + error.message); updateStatus('offline', 'Error: ' + error.message);
showMessage('Device error: ' + error.message, 'error'); 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) { twilioDevice.on('incoming', function(call) {
@@ -641,5 +660,88 @@
} }
}, 30000); // Every 30 seconds }, 30000); // Every 30 seconds
/**
* Schedule token refresh
* Refreshes token 5 minutes before expiry
*/
function scheduleTokenRefresh() {
// Clear any existing timer
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (!tokenExpiry) {
console.error('Token expiry time not set');
return;
}
// Calculate time until refresh (5 minutes before expiry)
const refreshBuffer = 5 * 60 * 1000; // 5 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);
}
});
}
// Clean up on page unload
$(window).on('beforeunload', function() {
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
if (twilioDevice) {
twilioDevice.destroy();
}
});
})(jQuery); })(jQuery);