From 2cb9b9472d6fa34629326d3d31c9a601b0823527 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 14 Aug 2025 12:01:05 -0700 Subject: [PATCH] Add automatic token refresh for browser phone to prevent timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- admin/class-twp-admin.php | 65 ++++++++++++++++- assets/js/browser-phone-frontend.js | 104 +++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index fd15dda..9833a2e 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -5691,6 +5691,8 @@ class TWP_Admin { var currentCall = null; var callTimer = null; var callStartTime = null; + var tokenRefreshTimer = null; + var tokenExpiry = null; // Wait for SDK to load function waitForTwilioSDK(callback) { @@ -5718,6 +5720,9 @@ class TWP_Admin { if (response.success) { $('#browser-phone-error').hide(); setupTwilioDevice(response.data.token); + // Set token expiry and schedule refresh + tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; + scheduleTokenRefresh(); } else { // WordPress wp_send_json_error sends the error message as response.data var errorMsg = response.data || response.error || 'Unknown error'; @@ -5852,18 +5857,65 @@ class TWP_Admin { } 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, { action: 'twp_generate_capability_token', nonce: '' }, function(response) { if (response.success && device) { + console.log('Token refreshed successfully'); 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() { - 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) { $('#browser-phone-error').html('

Error: ' + message + '

').show(); $('#phone-status').text('Error').css('color', '#f44336'); @@ -6005,8 +6057,15 @@ class TWP_Admin { }, 1000); }); - // Refresh token every 50 minutes (tokens expire in 1 hour) - setInterval(initializeBrowserPhone, 50 * 60 * 1000); + // Clean up on page unload + $(window).on('beforeunload', function() { + if (tokenRefreshTimer) { + clearTimeout(tokenRefreshTimer); + } + if (device) { + device.destroy(); + } + }); // Mode switching functionality $('input[name="call_mode"]').on('change', function() { diff --git a/assets/js/browser-phone-frontend.js b/assets/js/browser-phone-frontend.js index 59d22d7..83e66d2 100644 --- a/assets/js/browser-phone-frontend.js +++ b/assets/js/browser-phone-frontend.js @@ -13,6 +13,8 @@ let availableNumbers = []; let userQueues = []; let selectedQueue = null; + let tokenRefreshTimer = null; + let tokenExpiry = null; // Initialize when document is ready $(document).ready(function() { @@ -86,6 +88,9 @@ success: function(response) { if (response.success) { setupTwilioDevice(response.data.token); + // Set token expiry time (expires in 1 hour) + tokenExpiry = Date.now() + (response.data.expires_in || 3600) * 1000; + scheduleTokenRefresh(); } else { updateStatus('offline', 'Failed to initialize'); showMessage('Failed to initialize browser phone: ' + (response.data || 'Unknown error'), 'error'); @@ -112,6 +117,12 @@ } try { + // If device already exists, destroy it first to prevent multiple connections + if (twilioDevice) { + twilioDevice.destroy(); + twilioDevice = null; + } + twilioDevice = new Twilio.Device(token, { logLevel: 1, answerOnBridge: true @@ -120,13 +131,21 @@ twilioDevice.on('registered', function() { updateStatus('online', 'Ready'); 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) { console.error('Twilio Device Error:', error); updateStatus('offline', 'Error: ' + error.message); 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) { @@ -641,5 +660,88 @@ } }, 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); \ No newline at end of file