From 345ef43740ba8d96d07cfb75a4f2874cb993e71c Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 2 Sep 2025 11:40:49 -0700 Subject: [PATCH] Security enhancement: Remove frontend browser phone interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated shortcode to redirect to admin browser phone page for enhanced security - Removed frontend browser phone assets (108KB total): - assets/js/browser-phone-frontend.js (85KB) - assets/css/browser-phone-frontend.css (23KB) - Modified shortcode to show secure redirect interface with authentication checks - Added new shortcode attributes: title, button_text, target - Enhanced documentation with security improvements and new behavior - Reduced frontend attack surface by eliminating JavaScript exposure - Improved performance with minimal asset loading for shortcode pages πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 58 +- README.md | 82 +- assets/css/browser-phone-frontend.css | 1340 -------------- assets/js/browser-phone-frontend.js | 2373 ------------------------- includes/class-twp-shortcodes.php | 255 +-- 5 files changed, 147 insertions(+), 3961 deletions(-) delete mode 100644 assets/css/browser-phone-frontend.css delete mode 100644 assets/js/browser-phone-frontend.js diff --git a/CLAUDE.md b/CLAUDE.md index e8c8128..d3f3ee0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,7 +154,7 @@ twilio-wp-plugin/ #### TWP_Shortcodes - **Purpose**: WordPress shortcodes for frontend features -- **Shortcode**: `[twp_browser_phone]` - Browser phone interface +- **Shortcode**: `[twp_browser_phone]` - Admin browser phone redirect interface ## πŸ’Ύ Database Schema @@ -240,7 +240,7 @@ twilio-wp-plugin/ - `/smart-routing-fallback` - Routing error handler - `/browser-fallback` - Browser phone fallback -## πŸŽ›οΈ AJAX Endpoints (Admin & Frontend) +## πŸŽ›οΈ AJAX Endpoints (Admin Only) ### Total: 68 AJAX Actions @@ -271,12 +271,31 @@ All call control functions (`twp_toggle_hold`, `twp_transfer_call`, `twp_requeue ## 🎨 Frontend Components +### Shortcode Implementation +The `[twp_browser_phone]` shortcode now provides a **redirect interface** instead of a full browser phone: + +#### Shortcode Attributes +- **`title`**: Display title (default: "Browser Phone") +- **`button_text`**: Button text (default: "Access Browser Phone") +- **`target`**: Link target (default: "_blank" - opens in new tab) + +#### Security Features +- **Login Required**: Users must be logged in to see the redirect +- **Permission Check**: Requires `twp_access_browser_phone` or `manage_options` capability +- **Error Messages**: Clear feedback for unauthorized access + +#### Styling +- **Inline CSS Only**: No external CSS files loaded +- **Minimal Assets**: Reduces frontend bloat +- **Responsive Design**: Works on all device sizes + ### JavaScript Files 1. **admin.js** (116KB) - Admin interface functionality -2. **browser-phone-frontend.js** (85KB) - Browser phone implementation -3. **twp-service-worker.js** (2.5KB) - Push notifications +2. **twp-service-worker.js** (2.5KB) - Push notifications -### Browser Phone Features +### Browser Phone Features (Admin Only) +- **Enhanced Security**: All browser phone functionality restricted to admin area +- **Admin URL**: `admin.php?page=twilio-wp-browser-phone` - Twilio Device SDK integration - Real-time call controls (hold, transfer, record) - Queue monitoring dashboard @@ -285,11 +304,36 @@ All call control functions (`twp_toggle_hold`, `twp_transfer_call`, `twp_requeue - Visual call state indicators ### CSS Files -- **admin.css** - Admin interface styling -- **browser-phone-frontend.css** - Browser phone UI +- **admin.css** - Admin interface styling (includes browser phone UI) ## πŸ”§ Recent Fixes & Improvements +### SECURITY ENHANCEMENT: Frontend Browser Phone Removal (September 2025) - PRODUCTION READY +Major security enhancement by removing frontend browser phone interface and implementing admin-only access. + +#### Browser Phone Security Enhancement +- **Frontend Interface Removed**: Eliminated full browser phone interface from frontend shortcode +- **Admin-Only Access**: All browser phone functionality moved to secure admin area +- **Asset Reduction**: Removed 108KB of frontend assets (browser-phone-frontend.js and browser-phone-frontend.css) +- **Redirect Interface**: Shortcode now provides secure redirect to admin browser phone page +- **Enhanced Permissions**: Strict capability checking with clear error messages for unauthorized users +- **Reduced Attack Surface**: Minimized frontend JavaScript exposure and potential security vectors +- **Performance Improvement**: Reduced frontend asset loading and improved page load times + +#### Shortcode Transformation +- **Security-First Design**: Login and permission validation before any functionality access +- **Minimal Asset Loading**: Only essential inline CSS for redirect interface styling +- **Responsive Redirect**: Professional redirect interface works on all devices +- **Customizable Attributes**: title, button_text, and target attributes for flexibility +- **Clear Error Messaging**: Informative error messages for authentication and authorization failures + +#### Technical Implementation +- **File Removal**: `assets/js/browser-phone-frontend.js` and `assets/css/browser-phone-frontend.css` eliminated +- **Inline Styling**: Minimal CSS injected only when shortcode is present on page +- **Permission System**: Leverages WordPress capability system (`twp_access_browser_phone` or `manage_options`) +- **Admin URL Generation**: Secure admin_url() function for proper WordPress admin integration +- **Target Control**: Configurable link target (_blank by default for better UX) + ### PRODUCTION READY: Extension Transfer System Overhaul (September 2025) - FULLY RESOLVED Comprehensive overhaul of extension-based call transfers with enterprise-grade reliability. diff --git a/README.md b/README.md index a50fffe..546493b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will - **Queue Notifications**: SMS alerts to designated numbers when calls enter queues ### 🌐 Browser Phone (WebRTC) -- **In-Browser Calling**: Make and receive calls directly from WordPress admin or frontend +- **Admin-Only Access**: Secure browser phone interface available only in WordPress admin area +- **Enhanced Security**: All browser phone functionality moved from frontend for improved security - **Twilio Voice SDK v2**: Uses latest SDK for WebRTC functionality - **Visual Dialpad**: Click-to-dial interface with DTMF support - **Call Controls**: Mute, hold indicators, call timer @@ -54,7 +55,7 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will - **Queue Integration**: Accept calls from specific queues - **Token Management**: Automatic token refresh for uninterrupted service - **Mobile-Friendly**: Responsive design optimized for smartphones and tablets -- **Shortcode Support**: Embed browser phone on any page with `[twp_browser_phone]` +- **Shortcode Redirect**: `[twp_browser_phone]` shortcode provides secure redirect to admin interface ### πŸ•’ Business Hours Management - **Schedule-based Routing**: Different call flows for business hours vs after-hours @@ -113,7 +114,15 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will ## Recent Updates ### MAJOR RELEASE v2.3.0 (September 2025) - Enterprise Ready -Comprehensive system overhaul with production-ready reliability improvements. +Comprehensive system overhaul with production-ready reliability improvements and enhanced security. + +#### πŸ”’ Security Enhancement: Frontend Browser Phone Removal +- **Security-First Approach**: Removed full browser phone interface from frontend shortcode +- **Admin-Only Access**: All browser phone functionality now restricted to secure WordPress admin area +- **Asset Reduction**: Eliminated 108KB of frontend assets (browser-phone-frontend.js and CSS) +- **Reduced Attack Surface**: Minimized frontend JavaScript exposure and potential security vectors +- **Performance Boost**: Improved page load times with reduced frontend asset loading +- **Professional Redirect**: Shortcode now provides clean redirect interface to admin browser phone #### πŸš€ Extension Transfer System - Complete Solution - **FIXED: Direct Voicemail Issue**: Extension transfers no longer go directly to voicemail for available agents @@ -326,11 +335,16 @@ Comprehensive redesign of hold, transfer, and requeue functionality with profess - Enter TwiML App SID - Save settings -3. **Access Browser Phone**: - - Navigate to **Twilio** β†’ **Browser Phone** +3. **Access Browser Phone** (Admin Only): + - Navigate to **WordPress Admin** β†’ **Twilio** β†’ **Browser Phone** - Select caller ID from available numbers - Start making/receiving calls +4. **Frontend Access** (Optional): + - Use `[twp_browser_phone]` shortcode on any page + - Shortcode provides secure redirect to admin interface + - Requires user login and appropriate permissions + ### Agent Configuration 1. **Create Agent Groups** (Twilio β†’ Agent Groups) 2. **Add Agents** to groups with priorities @@ -356,37 +370,41 @@ Comprehensive redesign of hold, transfer, and requeue functionality with profess - **Queue Timeouts**: Alert when calls wait too long (configurable threshold) - **Missed Calls**: Alert when calls are abandoned or timeout -## Frontend Browser Phone +## Browser Phone Access -### Shortcode Usage -The browser phone can be embedded on any WordPress page using the `[twp_browser_phone]` shortcode. This is perfect for creating dedicated agent pages or customer service portals. +### Shortcode Usage (Redirect Interface) +The `[twp_browser_phone]` shortcode now provides a **secure redirect interface** that directs users to the WordPress admin browser phone page for enhanced security. **Basic Usage:** ``` [twp_browser_phone] ``` -**With Options:** +**With Custom Options:** ``` -[twp_browser_phone title="Customer Service Phone" show_title="true" compact="false"] +[twp_browser_phone title="Customer Service Phone" button_text="Access Phone System" target="_blank"] ``` ### Shortcode Parameters -- **title**: Custom title for the phone widget (default: "Browser Phone") -- **show_title**: Display the title above the phone (default: "true") -- **compact**: Use compact layout for smaller spaces (default: "false") +- **title**: Custom title for the redirect interface (default: "Browser Phone") +- **button_text**: Text for the admin link button (default: "Access Browser Phone") +- **target**: Link target behavior (default: "_blank" - opens in new tab) -### Permission Requirements -- Users must be **logged in** to access the browser phone -- Users need the **`twp_access_browser_phone`** capability or **`manage_options`** -- Phone Agent role users have access by default +### Security Features +- **Authentication Required**: Users must be logged in to see the redirect interface +- **Permission Validation**: Requires `twp_access_browser_phone` capability or `manage_options` +- **Clear Error Messages**: Informative feedback for unauthorized access attempts +- **Admin-Only Interface**: All browser phone functionality restricted to WordPress admin area -### Mobile Optimization -- **Responsive Design**: Adapts to all screen sizes -- **Touch-Friendly**: Large buttons optimized for mobile devices -- **Haptic Feedback**: Vibration on button press (where supported) -- **Auto-Zoom Prevention**: Proper viewport handling for mobile browsers -- **Dark Mode Support**: Automatically adapts to user's system preference +### Browser Phone Interface (Admin Area) +Access the full browser phone interface at: **WordPress Admin β†’ Twilio β†’ Browser Phone** + +#### Features: +- **Enhanced Security**: Protected by WordPress admin authentication +- **Full Functionality**: Complete browser phone features available in secure environment +- **Responsive Design**: Adapts to all screen sizes within admin interface +- **Professional Interface**: Integrated with WordPress admin styling +- **No Frontend Assets**: Eliminates security risks from frontend JavaScript exposure ## Voice Configuration @@ -407,10 +425,12 @@ The browser phone can be embedded on any WordPress page using the `[twp_browser_ ### Common Issues -#### Browser Phone "Client version not supported" -- **Fixed in latest version**: Upgraded to Voice SDK v2 -- Clear browser cache and reload page -- Check TwiML App SID is configured +#### Browser Phone Access Issues +- **Access via Admin Only**: Browser phone moved to WordPress admin area for security +- **Shortcode Redirect**: Frontend shortcode now provides redirect to admin interface +- **Permission Required**: Ensure user has `twp_access_browser_phone` or `manage_options` capability +- **Login Required**: Users must be logged in to access browser phone functionality +- Check TwiML App SID is configured in WordPress admin settings #### "Twilio SDK classes not available" ```bash @@ -519,6 +539,10 @@ All webhooks are REST API endpoints under `/wp-json/twilio-webhook/v1/`: - **Mobile Browsers**: Limited WebRTC support ### Security Considerations +- **Admin-Only Browser Phone**: All browser phone functionality restricted to WordPress admin area +- **Frontend Asset Elimination**: Removed 108KB of frontend JavaScript and CSS to reduce attack surface +- **Authentication Required**: Shortcode requires user login and appropriate permissions +- **Secure Redirects**: Frontend shortcode provides authenticated redirect to admin interface - All webhooks use WordPress nonce verification - Phone numbers validated and sanitized - SQL queries use prepared statements @@ -528,6 +552,10 @@ All webhooks are REST API endpoints under `/wp-json/twilio-webhook/v1/`: ## Version History ### v2.3.0 (Current - September 2025) - ENTERPRISE READY +- **SECURITY ENHANCEMENT**: Removed frontend browser phone interface, moved to admin-only access for enhanced security +- **ASSET REDUCTION**: Eliminated 108KB of frontend assets (browser-phone-frontend.js and CSS files) +- **SHORTCODE SECURITY**: Browser phone shortcode now provides secure redirect with authentication checks +- **ATTACK SURFACE REDUCTION**: Minimized frontend JavaScript exposure and potential security vectors - **EXTENSION TRANSFER SYSTEM**: Complete overhaul with queue-based routing, 2-minute timeout, and automatic voicemail fallback - **BROWSER PHONE UNIVERSAL**: Firefox support, permission management, call stability, and cross-browser compatibility - **AGENT STATUS AUTOMATION**: 1-minute auto-revert system with WordPress cron job integration diff --git a/assets/css/browser-phone-frontend.css b/assets/css/browser-phone-frontend.css deleted file mode 100644 index bf06278..0000000 --- a/assets/css/browser-phone-frontend.css +++ /dev/null @@ -1,1340 +0,0 @@ -/* Twilio Browser Phone Frontend Styles - Mobile First */ - -/* Call Control Panel Styles */ -.twp-call-controls-panel { - margin: 15px 0; - padding: 15px; - background: #fff; - border-radius: 8px; - border: 1px solid #dee2e6; -} - -.call-control-buttons { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 10px; -} - -.twp-btn-control { - padding: 10px 15px; - background: #6c757d; - color: white; - border: none; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.twp-btn-control:hover { - background: #5a6268; - transform: translateY(-1px); -} - -.twp-btn-control.btn-active { - background: #007bff; -} - -.twp-btn-control.btn-recording { - background: #dc3545; - animation: recording-pulse 1.5s infinite; -} - -@keyframes recording-pulse { - 0% { opacity: 1; } - 50% { opacity: 0.7; } - 100% { opacity: 1; } -} - -/* Dialog Overlay Styles */ -.twp-dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; -} - -.twp-dialog { - background: white; - border-radius: 12px; - padding: 25px; - max-width: 400px; - width: 90%; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); -} - -.twp-dialog h3 { - margin: 0 0 15px 0; - font-size: 1.3rem; - color: #212529; -} - -.twp-dialog p { - margin: 0 0 20px 0; - color: #6c757d; -} - -.twp-dialog input[type="tel"], -.twp-dialog select { - width: 100%; - padding: 10px; - border: 2px solid #dee2e6; - border-radius: 6px; - font-size: 16px; - margin-bottom: 20px; -} - -.twp-dialog input[type="tel"]:focus, -.twp-dialog select:focus { - outline: none; - border-color: #007bff; -} - -.dialog-buttons { - display: flex; - gap: 10px; - justify-content: flex-end; -} - -.dialog-buttons .twp-btn { - padding: 10px 20px; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.dialog-buttons .twp-btn-primary { - background: #007bff; - color: white; - border: none; -} - -.dialog-buttons .twp-btn-primary:hover { - background: #0056b3; -} - -.dialog-buttons .twp-btn-secondary { - background: #6c757d; - color: white; - border: none; -} - -.dialog-buttons .twp-btn-secondary:hover { - background: #5a6268; -} - -/* Agent Transfer Dialog Styles */ -.twp-agent-transfer-dialog { - max-width: 500px; -} - -.agent-list { - max-height: 300px; - overflow-y: auto; - margin-bottom: 20px; - border: 1px solid #dee2e6; - border-radius: 6px; -} - -.agent-option { - padding: 12px; - border-bottom: 1px solid #e9ecef; - cursor: pointer; - transition: background 0.2s; - display: flex; - justify-content: space-between; - align-items: center; -} - -.agent-option:last-child { - border-bottom: none; -} - -.agent-option:hover:not(.offline) { - background: #f8f9fa; -} - -.agent-option.selected { - background: #e7f3ff; - border-left: 3px solid #007bff; -} - -.agent-option.offline { - opacity: 0.5; - cursor: not-allowed; -} - -.agent-info { - display: flex; - align-items: center; - gap: 10px; -} - -.agent-name { - font-weight: 500; - color: #212529; -} - -.agent-method { - font-size: 18px; -} - -.agent-status { - font-size: 14px; - white-space: nowrap; -} - -.manual-option { - margin-top: 20px; - padding-top: 20px; - border-top: 1px solid #dee2e6; -} - -.manual-option p { - margin-bottom: 10px; - font-size: 14px; - color: #6c757d; -} -.twp-browser-phone-container { - max-width: 400px; - margin: 0 auto; - padding: 20px; - background: #f8f9fa; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - box-sizing: border-box; -} - -.twp-browser-phone-container * { - box-sizing: border-box; -} - -.twp-browser-phone-container.compact { - padding: 15px; - max-width: 320px; -} - -.twp-browser-phone-title { - text-align: center; - margin: 0 0 20px 0; - color: #212529; - font-size: 1.5rem; - font-weight: 600; -} - -/* Connection Status */ -.twp-connection-status { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 20px; - padding: 12px; - background: #fff; - border-radius: 8px; - border: 2px solid #e9ecef; -} - -.status-indicator { - width: 12px; - height: 12px; - border-radius: 50%; - margin-right: 8px; - animation: pulse 2s infinite; -} - -.status-indicator.offline { - background-color: #dc3545; -} - -.status-indicator.connecting { - background-color: #ffc107; -} - -.status-indicator.online { - background-color: #28a745; - animation: none; -} - -.status-text { - font-weight: 600; - color: #212529; -} - -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } -} - -/* Phone Number Selection */ -.twp-phone-selection { - margin-bottom: 20px; -} - -.twp-phone-selection label { - display: block; - margin-bottom: 8px; - font-weight: 600; - color: #212529; -} - -.twp-select { - width: 100%; - padding: 12px 16px; - border: 2px solid #e9ecef; - border-radius: 8px; - font-size: 16px; - background: #fff; - color: #212529; - appearance: none; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 12px center; - background-repeat: no-repeat; - background-size: 16px; - padding-right: 40px; -} - -.twp-select:focus { - outline: none; - border-color: #007cba; - box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1); -} - -/* Dial Pad */ -.twp-dialpad { - margin-bottom: 20px; -} - -.twp-number-display { - position: relative; - margin-bottom: 20px; -} - -#twp-dial-number { - width: 100%; - padding: 16px 50px 16px 16px; - border: 2px solid #e9ecef; - border-radius: 8px; - font-size: 18px; - text-align: center; - background: #fff; - letter-spacing: 2px; -} - -#twp-dial-number:focus { - outline: none; - border-color: #007cba; - box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1); -} - -.twp-btn-clear { - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - background: #dc3545; - color: white; - border: none; - border-radius: 50%; - width: 32px; - height: 32px; - font-size: 18px; - font-weight: bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} - -.twp-btn-clear:hover { - background: #c82333; -} - -/* Dial Grid */ -.twp-dial-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; - max-width: 280px; - margin: 0 auto; -} - -.twp-dial-btn { - background: #fff; - border: 2px solid #e9ecef; - border-radius: 12px; - padding: 16px 8px; - font-size: 20px; - font-weight: 600; - color: #212529; - cursor: pointer; - transition: all 0.2s ease; - min-height: 60px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - user-select: none; - -webkit-user-select: none; - -webkit-tap-highlight-color: transparent; -} - -.twp-dial-btn:hover { - background: #f8f9fa; - border-color: #007cba; - transform: translateY(-1px); -} - -.twp-dial-btn:active { - transform: translateY(0); - background: #e9ecef; -} - -.twp-dial-btn span { - font-size: 11px; - color: #6c757d; - margin-top: 2px; - font-weight: 400; -} - -/* Call Controls */ -.twp-call-controls { - display: flex; - gap: 12px; - margin-bottom: 20px; -} - -.twp-btn { - flex: 1; - padding: 16px 24px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - min-height: 56px; -} - -.twp-btn-primary { - background: #28a745; - color: white; -} - -.twp-btn-primary:hover { - background: #218838; - transform: translateY(-1px); -} - -.twp-btn-primary:disabled { - background: #6c757d; - cursor: not-allowed; - transform: none; -} - -.twp-btn-danger { - background: #dc3545; - color: white; -} - -.twp-btn-danger:hover { - background: #c82333; - transform: translateY(-1px); -} - -.twp-btn-warning { - background: #ffc107; - color: #212529; -} - -.twp-btn-warning:hover { - background: #e0a800; - transform: translateY(-1px); -} - -.twp-btn-success { - background: #007cba; - color: white; -} - -.twp-btn-success:hover { - background: #005a87; - transform: translateY(-1px); -} - -.twp-btn-info { - background: #17a2b8; - color: white; - border: none; - padding: 10px 15px; - border-radius: 6px; - cursor: pointer; - font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 120px; - max-width: 200px; - display: inline-block; - text-align: center; - line-height: 1.4; -} - -.twp-btn-info:hover { - background: #138496; - transform: translateY(-1px); -} - -/* Call Info */ -.twp-call-info { - background: #fff; - padding: 16px; - border-radius: 8px; - border: 2px solid #e9ecef; - margin-bottom: 20px; -} - -.call-timer { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.timer-label { - color: #6c757d; - font-weight: 500; -} - -.timer-value { - font-size: 18px; - font-weight: 600; - color: #212529; - font-family: 'Courier New', monospace; -} - -.call-status { - text-align: center; - padding: 8px; - background: #f8f9fa; - border-radius: 4px; - color: #212529; - font-weight: 600; -} - -/* Queue Management Section */ -.twp-queue-section { - background: #fff; - padding: 16px; - border-radius: 8px; - border: 2px solid #e9ecef; - margin-bottom: 20px; -} - -.twp-queue-section h4 { - margin: 0 0 16px 0; - color: #212529; - font-size: 1.1rem; - text-align: center; -} - -/* Queue List */ -.twp-queue-list { - margin-bottom: 16px; -} - -.queue-loading, -.no-queues { - text-align: center; - color: #495057; - font-style: italic; - padding: 20px; - font-weight: 500; -} - -.no-queues { - background: #f8f9fa; - border: 2px dashed #e9ecef; - border-radius: 8px; -} - -.queue-item { - background: #f8f9fa; - border: 2px solid #e9ecef; - border-radius: 8px; - padding: 12px; - margin-bottom: 8px; - cursor: pointer; - transition: all 0.2s ease; - position: relative; -} - -.queue-item:hover { - border-color: #007cba; - background: #e3f2fd; -} - -.queue-item.selected { - border-color: #007cba; - background: #e3f2fd; - box-shadow: 0 0 0 1px #007cba; -} - -.queue-item.has-calls { - border-left: 4px solid #28a745; -} - -.queue-item.has-calls::after { - content: ''; - position: absolute; - top: 8px; - right: 8px; - width: 8px; - height: 8px; - background: #28a745; - border-radius: 50%; - animation: pulse 2s infinite; -} - -.queue-name { - font-weight: 600; - color: #212529; - margin-bottom: 4px; -} - -.queue-info { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.9rem; - color: #495057; -} - -.queue-waiting { - font-weight: 500; -} - -.queue-waiting.has-calls { - color: #28a745; - font-weight: 600; -} - -.queue-capacity { - font-size: 0.8rem; -} - -/* Queue Controls */ -.twp-queue-controls { - background: #f8f9fa; - padding: 16px; - border-radius: 8px; - border: 2px solid #e9ecef; - margin-top: 16px; -} - -.selected-queue-info { - margin-bottom: 16px; - text-align: center; -} - -.selected-queue-info h5 { - margin: 0 0 8px 0; - color: #212529; - font-size: 1rem; -} - -.queue-stats { - display: flex; - justify-content: center; - gap: 20px; - font-size: 0.9rem; - color: #495057; -} - -.queue-stats span { - font-weight: 500; -} - -.queue-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.queue-actions .twp-btn { - flex: 1; - min-width: 80px; -} - -/* Global Queue Actions */ -.queue-global-actions { - margin-top: 16px; - margin-bottom: 16px; -} - -.global-queue-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: center; -} - -.global-queue-actions .twp-btn { - flex: 1; - min-width: 100px; -} - -/* Alert Toggle Button */ -#twp-alert-toggle { - position: relative; -} - -#twp-alert-toggle.alert-on { - background-color: #28a745 !important; - border-color: #28a745; - color: white; -} - -#twp-alert-toggle.alert-off { - background-color: #6c757d !important; - border-color: #6c757d; - color: white; -} - -#twp-alert-toggle:hover { - opacity: 0.8; -} - -/* Voicemail Section */ -.twp-voicemail-section { - background: #fff; - border-radius: 8px; - border: 2px solid #e9ecef; - margin-bottom: 20px; - overflow: hidden; -} - -.voicemail-header { - padding: 16px; - cursor: pointer; - background: #f8f9fa; - border-bottom: 1px solid #e9ecef; - transition: background-color 0.2s ease; -} - -.voicemail-header:hover { - background: #e9ecef; -} - -.voicemail-header h4 { - margin: 0; - color: #212529; - font-size: 1.1rem; - display: inline-block; -} - -.voicemail-summary { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 8px; -} - -.summary-stats { - font-size: 0.9rem; - color: #6c757d; -} - -.voicemail-toggle { - background: none; - border: none; - cursor: pointer; - padding: 4px 8px; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -.voicemail-toggle:hover { - background: #dee2e6; -} - -.toggle-icon { - font-size: 1.2rem; - color: #495057; - transition: transform 0.2s ease; -} - -.voicemail-content { - padding: 16px; -} - -.twp-voicemail-list { - margin-bottom: 16px; - max-height: 200px; - overflow-y: auto; -} - -.voicemail-item { - padding: 12px; - border: 1px solid #dee2e6; - border-radius: 6px; - margin-bottom: 8px; - cursor: pointer; - transition: all 0.2s ease; -} - -.voicemail-item:hover { - background: #f8f9fa; - border-color: #007bff; -} - -.voicemail-item.has-recording { - border-left: 4px solid #28a745; -} - -.voicemail-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.voicemail-from { - display: flex; - align-items: center; - gap: 6px; -} - -.from-number { - font-weight: 600; - color: #495057; -} - -.voicemail-time { - font-size: 0.85rem; - color: #6c757d; -} - -.voicemail-details { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 8px; -} - -.voicemail-duration { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.9rem; - color: #6c757d; -} - -.recording-indicator { - background: #28a745; - color: white; - padding: 2px 8px; - border-radius: 12px; - font-size: 0.8rem; -} - -.voicemail-transcription { - font-size: 0.9rem; - color: #495057; - font-style: italic; - padding: 8px; - background: #f8f9fa; - border-radius: 4px; - border-left: 3px solid #007bff; -} - -.voicemail-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.voicemail-actions .twp-btn { - flex: 1; - min-width: 80px; -} - -.no-voicemails, -.voicemail-loading { - text-align: center; - padding: 20px; - color: #6c757d; - font-style: italic; -} - -.twp-btn-secondary { - background: #6c757d; - color: white; -} - -.twp-btn-secondary:hover { - background: #5a6268; - transform: translateY(-1px); -} - -/* Messages */ -.twp-messages { - margin-top: 16px; -} - -.twp-error { - background: #f8d7da; - color: #721c24; - padding: 12px 16px; - border-radius: 8px; - border: 1px solid #f5c6cb; - margin-bottom: 16px; -} - -.twp-success { - background: #d4edda; - color: #155724; - padding: 12px 16px; - border-radius: 8px; - border: 1px solid #c3e6cb; - margin-bottom: 16px; -} - -.twp-info { - background: #d1ecf1; - color: #0c5460; - padding: 12px 16px; - border-radius: 8px; - border: 1px solid #bee5eb; - margin-bottom: 16px; -} - -/* Tablet optimizations */ -@media (max-width: 768px) and (min-width: 481px) { - .twp-btn-info { - font-size: 12px; - padding: 8px 12px; - min-width: 140px; - max-width: 180px; - } - - .global-queue-actions { - flex-direction: column; - gap: 10px; - } - - .global-queue-actions .twp-btn { - min-width: 150px; - } -} - -/* Responsive Design */ -@media (max-width: 480px) { - .twp-browser-phone-container { - margin: 0; - padding: 16px; - border-radius: 0; - max-width: none; - } - - .twp-browser-phone-title { - font-size: 1.3rem; - margin-bottom: 16px; - } - - .twp-dial-grid { - gap: 8px; - max-width: 240px; - } - - .twp-dial-btn { - padding: 12px 6px; - min-height: 50px; - font-size: 18px; - } - - .twp-dial-btn span { - font-size: 10px; - } - - .twp-btn { - padding: 14px 20px; - font-size: 15px; - min-height: 50px; - } - - #twp-dial-number { - font-size: 16px; - padding: 14px 45px 14px 14px; - } -} - -@media (max-width: 320px) { - .twp-dial-grid { - max-width: 200px; - } - - .twp-dial-btn { - padding: 10px 4px; - min-height: 45px; - font-size: 16px; - } - - /* Queue section mobile adjustments */ - .queue-stats { - flex-direction: column; - gap: 8px; - } - - .queue-actions { - flex-direction: column; - } - - .queue-item { - padding: 10px; - } - - .queue-info { - flex-direction: column; - align-items: flex-start; - gap: 4px; - } -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .twp-browser-phone-container { - background: #1a202c; - color: #f7fafc; - } - - .twp-connection-status, - .twp-call-info, - .twp-queue-controls, - .twp-queue-section, - .twp-voicemail-section, - .queue-global-actions { - background: #2d3748; - border-color: #4a5568; - } - - .twp-select, - #twp-dial-number, - .twp-dial-btn { - background: #2d3748; - border-color: #4a5568; - color: #f7fafc; - } - - .call-status { - background: #1a202c; - color: #f7fafc; - } - - .queue-item { - background: #2d3748; - border-color: #4a5568; - color: #f7fafc; - } - - .queue-item:hover, - .queue-item.selected { - background: #4a5568; - border-color: #63b3ed; - } - - .queue-name, - .status-text, - .twp-phone-selection label, - .twp-browser-phone-title, - .twp-queue-section h4, - .twp-voicemail-section h4, - .selected-queue-info h5, - .timer-value, - .from-number, - .stat-value { - color: #f7fafc; - } - - .twp-dial-btn { - color: #f7fafc; - } - - .queue-info, - .queue-stats, - .queue-loading, - .no-queues, - .voicemail-stats, - .voicemail-time, - .voicemail-duration, - .stat-label, - .voicemail-loading, - .no-voicemails { - color: #cbd5e0; - } - - .no-queues, - .voicemail-header, - .voicemail-transcription { - background: #2d3748; - border-color: #4a5568; - } - - .voicemail-header:hover { - background: #4a5568; - } - - .summary-stats { - color: #cbd5e0; - } - - .voicemail-item { - background: #2d3748; - border-color: #4a5568; - color: #f7fafc; - } - - .voicemail-item:hover { - background: #4a5568; - border-color: #63b3ed; - } -} - -/* Animation for incoming calls */ -@keyframes incoming-call { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); } -} - -.twp-browser-phone-container.incoming-call { - animation: incoming-call 1s ease-in-out infinite; -} - -/* Loading state */ -.twp-loading { - opacity: 0.6; - pointer-events: none; -} - -.twp-loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - margin: -10px 0 0 -10px; - border: 2px solid #f3f3f3; - border-top: 2px solid #007cba; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Agent Transfer Dialog Styles */ -.twp-agent-transfer-dialog { - max-width: 500px; - width: 90vw; -} - -.agent-list { - max-height: 300px; - overflow-y: auto; - margin: 15px 0; - border: 1px solid #ddd; - border-radius: 6px; - background: #f9f9f9; -} - -.agent-option { - padding: 12px; - border-bottom: 1px solid #eee; - background: white; - margin-bottom: 1px; -} - -.agent-option:last-child { - border-bottom: none; -} - -.agent-option.offline { - opacity: 0.6; - background: #f5f5f5; -} - -.agent-info { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.agent-name { - font-weight: 600; - font-size: 0.95rem; -} - -.agent-status { - font-size: 0.85rem; - font-weight: 500; -} - -.transfer-methods { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.transfer-option { - background: #e9ecef; - border: 2px solid #e9ecef; - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 0.85rem; - transition: all 0.2s ease; - user-select: none; -} - -.transfer-option:hover { - background: #dee2e6; - border-color: #adb5bd; -} - -.transfer-option.selected { - background: #e3f2fd; - border-color: #2196f3; - color: #1976d2; -} - -.transfer-option:active { - transform: scale(0.98); -} - -.agent-option.offline .transfer-option { - opacity: 0.5; - cursor: not-allowed; -} - -.agent-option.offline .transfer-option:hover { - background: #e9ecef; - border-color: #e9ecef; -} - -.manual-option { - margin-top: 20px; - padding-top: 15px; - border-top: 1px solid #ddd; -} - -.manual-option h4 { - margin: 0 0 8px 0; - font-size: 1rem; - color: #495057; -} - -.manual-option p { - margin: 0 0 10px 0; - font-size: 0.9rem; - color: #6c757d; -} - -.no-agents { - text-align: center; - padding: 20px; - color: #6c757d; - font-style: italic; -} - -/* Hold button active state */ -.twp-btn.btn-active { - background-color: #ff6b35 !important; - color: white !important; - border-color: #e55100 !important; - font-weight: bold; -} - -.twp-btn.btn-active:hover { - background-color: #e55100 !important; - border-color: #bf360c !important; -} - -/* Enhanced Queue Display Styles */ -.user-extension-display { - background: #e8f4f8; - padding: 8px 12px; - border-radius: 4px; - margin-bottom: 10px; - font-size: 14px; - color: #2c5282; - text-align: center; -} - -.queue-header { - display: flex; - align-items: center; - gap: 8px; -} - -.queue-type-icon { - font-size: 16px; - flex-shrink: 0; -} - -.queue-type-personal { - border-left: 4px solid #28a745; -} - -.queue-type-hold { - border-left: 4px solid #ffc107; -} - -.queue-type-general { - border-left: 4px solid #007bff; -} - -.queue-item.queue-type-personal .queue-name { - color: #155724; - font-weight: 600; -} - -.queue-item.queue-type-hold .queue-name { - color: #856404; - font-weight: 500; -} - -.no-queues small { - color: #6c757d; - font-size: 12px; -} - -/* Responsive queue enhancements */ -@media (max-width: 480px) { - .user-extension-display { - font-size: 12px; - padding: 6px 10px; - } - - .queue-type-icon { - font-size: 14px; - } -} \ No newline at end of file diff --git a/assets/js/browser-phone-frontend.js b/assets/js/browser-phone-frontend.js deleted file mode 100644 index 771f811..0000000 --- a/assets/js/browser-phone-frontend.js +++ /dev/null @@ -1,2373 +0,0 @@ -/** - * Frontend Browser Phone for TWP Plugin - * Mobile-friendly implementation - */ -(function($) { - 'use strict'; - - let twilioDevice = null; - let currentCall = null; - let callTimer = null; - let callStartTime = null; - let isConnected = false; - let availableNumbers = []; - let userQueues = []; - let selectedQueue = null; - let tokenRefreshTimer = null; - let tokenExpiry = null; - let queuePollingTimer = null; - let lastQueueUpdate = {}; - let alertSound = null; - let alertInterval = null; - let alertEnabled = false; - let notificationPermission = 'default'; - let backgroundAlertInterval = null; - let isPageVisible = true; - let isOnHold = false; - let isRecording = false; - let recordingSid = null; - let personalQueueTimer = null; - let personalQueueName = null; - - // Initialize when document is ready - $(document).ready(function() { - if (!twp_frontend_ajax.is_logged_in) { - showMessage('You must be logged in to use the browser phone.', 'error'); - return; - } - - initializeBrowserPhone(); - bindEvents(); - loadPhoneNumbers(); - loadUserQueues(); - initVoicemailSection(); - initializeNotifications(); - initializePageVisibility(); - initializePersonalQueue(); - }); - - /** - * Wait for Twilio SDK to load - */ - function waitForTwilioSDK(callback) { - let attempts = 0; - const maxAttempts = 150; // 15 seconds max (100ms * 150) - - function checkTwilio() { - console.log('Checking Twilio SDK availability, attempt:', attempts + 1); - - if (typeof Twilio !== 'undefined' && Twilio.Device) { - console.log('Twilio SDK loaded successfully'); - callback(); - return; - } - - attempts++; - - // Update status message periodically - if (attempts === 30) { // 3 seconds - updateStatus('connecting', 'Loading Twilio SDK...'); - } else if (attempts === 60) { // 6 seconds - updateStatus('connecting', 'Still loading SDK...'); - } else if (attempts === 100) { // 10 seconds - updateStatus('connecting', 'SDK taking longer than expected...'); - } - - if (attempts >= maxAttempts) { - console.error('Twilio SDK failed to load. Window.Twilio:', typeof Twilio); - updateStatus('offline', 'SDK load timeout'); - showMessage('Twilio SDK failed to load within 15 seconds. This may be due to a slow connection or network restrictions. Please refresh the page and try again.', 'error'); - return; - } - - setTimeout(checkTwilio, 100); - } - - checkTwilio(); - } - - /** - * Initialize the browser phone - */ - function initializeBrowserPhone() { - updateStatus('connecting', 'Initializing...'); - - // Wait for Twilio SDK to load, then initialize - waitForTwilioSDK(function() { - // Generate capability token - $.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) { - 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'); - } - }, - error: function() { - updateStatus('offline', 'Connection failed'); - showMessage('Failed to connect to server', 'error'); - } - }); - }); - } - - /** - * Setup Twilio Device - */ - // Request microphone and speaker permissions - async function requestMediaPermissions() { - try { - console.log('Requesting media permissions...'); - - // Request microphone permission - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false - }); - - // Stop the stream immediately as we just needed permission - stream.getTracks().forEach(track => track.stop()); - - console.log('Media permissions granted'); - return true; - } catch (error) { - console.error('Media permission denied or not available:', error); - - // Show user-friendly error message - let errorMessage = 'Microphone access is required for browser phone functionality. '; - - if (error.name === 'NotAllowedError') { - errorMessage += 'Please allow microphone access in your browser settings and refresh the page.'; - } else if (error.name === 'NotFoundError') { - errorMessage += 'No microphone found. Please connect a microphone and try again.'; - } else { - errorMessage += 'Please check your browser settings and try again.'; - } - - showMessage(errorMessage, 'error'); - updateStatus('offline', 'Permission denied'); - return false; - } - } - - async function setupTwilioDevice(token) { - // Check if Twilio SDK is loaded - if (typeof Twilio === 'undefined' || !Twilio.Device) { - updateStatus('offline', 'Twilio SDK not loaded'); - showMessage('Twilio SDK failed to load. Please refresh the page.', 'error'); - console.error('Twilio SDK is not available'); - return; - } - - // Request media permissions before setting up device - const hasPermissions = await requestMediaPermissions(); - if (!hasPermissions) { - return; // Stop setup if permissions denied - } - - 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 - }); - - twilioDevice.on('registered', function() { - updateStatus('online', 'Ready'); - isConnected = true; - // 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) { - handleIncomingCall(call); - }); - - twilioDevice.on('disconnect', function() { - updateStatus('offline', 'Disconnected'); - isConnected = false; - }); - - // Register device asynchronously - await twilioDevice.register(); - - } catch (error) { - console.error('Failed to setup Twilio Device:', error); - updateStatus('offline', 'Setup failed'); - showMessage('Failed to setup device: ' + error.message, 'error'); - } - } - - /** - * Load available phone numbers - */ - function loadPhoneNumbers() { - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_phone_numbers', - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success && response.data) { - availableNumbers = response.data; - populateCallerIdSelect(); - } else { - showMessage('Failed to load phone numbers', 'error'); - } - }, - error: function() { - showMessage('Failed to load phone numbers', 'error'); - } - }); - } - - /** - * Populate caller ID select - */ - function populateCallerIdSelect() { - const $select = $('#twp-caller-id'); - $select.empty(); - - if (availableNumbers.length === 0) { - $select.append(''); - return; - } - - $select.append(''); - - availableNumbers.forEach(function(number) { - const friendlyName = number.friendly_name || number.phone_number; - $select.append(``); - }); - } - - /** - * Bind event handlers - */ - function bindEvents() { - // Dial pad buttons - $('.twp-dial-btn').on('click', function() { - const digit = $(this).data('digit'); - addDigit(digit); - - // Haptic feedback on mobile - if (navigator.vibrate) { - navigator.vibrate(50); - } - }); - - // Clear number button - $('#twp-clear-number').on('click', function() { - $('#twp-dial-number').val(''); - }); - - // Call button - $('#twp-call-btn').on('click', function() { - if (!isConnected) { - showMessage('Device not connected', 'error'); - return; - } - - const number = $('#twp-dial-number').val().trim(); - const callerId = $('#twp-caller-id').val(); - - if (!number) { - showMessage('Please enter a number to call', 'error'); - return; - } - - if (!callerId) { - showMessage('Please select a caller ID', 'error'); - return; - } - - makeCall(number, callerId); - }); - - // Hang up button - $('#twp-hangup-btn').on('click', function() { - hangupCall(); - }); - - $('#twp-resume-btn').on('click', function() { - toggleHold(); // Resume the call - }); - - // Call control buttons - $('#twp-hold-btn').on('click', function() { - toggleHold(); - }); - - $('#twp-transfer-btn').on('click', function() { - showTransferDialog(); - }); - - $('#twp-requeue-btn').on('click', function() { - showRequeueDialog(); - }); - - $('#twp-record-btn').on('click', function() { - toggleRecording(); - }); - - // Transfer dialog handlers - $(document).on('click', '#twp-confirm-transfer', function() { - const agentNumber = $('#twp-transfer-agent-number').val(); - if (agentNumber) { - transferCall(agentNumber); - } - }); - - $(document).on('click', '#twp-cancel-transfer', function() { - hideTransferDialog(); - }); - - // Requeue dialog handlers - $(document).on('click', '#twp-confirm-requeue', function() { - const queueId = $('#twp-requeue-select').val(); - if (queueId) { - requeueCall(queueId); - } - }); - - $(document).on('click', '#twp-cancel-requeue', function() { - hideRequeueDialog(); - }); - - // Accept queue call button - $('#twp-accept-queue-call').on('click', function() { - acceptQueueCall(); - stopAlert(); // Stop alert when accepting call - }); - - // Refresh queues button - $('#twp-refresh-queues').on('click', function() { - loadUserQueues(); - }); - - // Alert toggle button - $(document).on('click', '#twp-alert-toggle', function() { - toggleAlert(); - }); - - // Voicemail refresh button - $('#twp-refresh-voicemails').on('click', function() { - loadUserVoicemails(); - }); - - // View all voicemails button - $('#twp-view-all-voicemails').on('click', function() { - // Open admin voicemails page in new tab - window.open(twp_frontend_ajax.admin_url + 'admin.php?page=twilio-wp-voicemails', '_blank'); - }); - - // Voicemail toggle button - $('#twp-voicemail-toggle').on('click', function() { - toggleVoicemailSection(); - }); - - // Voicemail header click (also toggles) - $('#twp-voicemail-header').on('click', function(e) { - // Don't toggle if clicking the toggle button itself - if (!$(e.target).closest('.voicemail-toggle').length) { - toggleVoicemailSection(); - } - }); - - // Voicemail item click handler - $(document).on('click', '.voicemail-item', function() { - const voicemailId = $(this).data('voicemail-id'); - playVoicemail(voicemailId); - }); - - // Queue item selection - $(document).on('click', '.queue-item', function() { - const queueId = $(this).data('queue-id'); - selectQueue(queueId); - }); - - // Manual number input - $('#twp-dial-number').on('input', function() { - // Only allow valid phone number characters - let value = $(this).val().replace(/[^\d\+\-\(\)\s]/g, ''); - $(this).val(value); - }); - - // Handle enter key in dial number input - $('#twp-dial-number').on('keypress', function(e) { - if (e.which === 13) { // Enter key - $('#twp-call-btn').click(); - } - }); - } - - /** - * Add digit to dial pad - */ - function addDigit(digit) { - const $input = $('#twp-dial-number'); - $input.val($input.val() + digit); - - // Send DTMF if in a call - if (currentCall && currentCall.status() === 'open') { - currentCall.sendDigits(digit); - } - } - - /** - * Make outbound call - */ - async function makeCall(number, callerId) { - if (currentCall) { - showMessage('Already in a call', 'error'); - return; - } - - // Stop alerts when making a call - stopAlert(); - - updateCallState('connecting'); - showCallInfo('Connecting...'); - - const params = { - To: number, - From: callerId - }; - - try { - console.log('Making call with params:', params); - currentCall = await twilioDevice.connect({params: params}); - - // Setup call event handlers - currentCall.on('accept', function() { - updateCallState('connected'); - showCallInfo('Connected'); - startCallTimer(); - showMessage('Call connected!', 'success'); - }); - - currentCall.on('disconnect', function() { - endCall(); - showMessage('Call ended', 'info'); - }); - - currentCall.on('error', function(error) { - console.error('Call error:', error); - endCall(); - showMessage('Call failed: ' + error.message, 'error'); - }); - - } catch (error) { - console.error('Failed to make call:', error); - endCall(); - showMessage('Failed to make call: ' + error.message, 'error'); - } - } - - /** - * Handle incoming call - */ - function handleIncomingCall(call) { - currentCall = call; - - // Add visual indication - $('.twp-browser-phone-container').addClass('incoming-call'); - - updateCallState('ringing'); - showCallInfo('Incoming call from: ' + (call.parameters.From || 'Unknown')); - showMessage('Incoming call! Click Accept to answer.', 'info'); - - // Auto-answer after a delay (optional) - setTimeout(function() { - if (currentCall === call && call.status() === 'pending') { - acceptCall(); - } - }, 2000); - - call.on('accept', function() { - $('.twp-browser-phone-container').removeClass('incoming-call'); - updateCallState('connected'); - showCallInfo('Connected'); - startCallTimer(); - showMessage('Call answered!', 'success'); - }); - - call.on('disconnect', function() { - $('.twp-browser-phone-container').removeClass('incoming-call'); - endCall(); - showMessage('Call ended', 'info'); - }); - - call.on('error', function(error) { - $('.twp-browser-phone-container').removeClass('incoming-call'); - endCall(); - showMessage('Call error: ' + error.message, 'error'); - }); - } - - /** - * Accept incoming call - */ - function acceptCall() { - if (currentCall && currentCall.status() === 'pending') { - currentCall.accept(); - } - } - - /** - * Hang up current call - */ - function hangupCall() { - if (currentCall) { - currentCall.disconnect(); - } - endCall(); - } - - /** - * End call and cleanup - */ - function endCall() { - // Stop recording if active - if (isRecording) { - stopRecording(); - } - - currentCall = null; - isOnHold = false; - isRecording = false; - recordingSid = null; - stopCallTimer(); - updateCallState('idle'); - hideCallInfo(); - $('.twp-browser-phone-container').removeClass('incoming-call'); - - // Reset control buttons - console.log('endCall() called - resetting hold button from:', $('#twp-hold-btn').text()); - $('#twp-hold-btn').text('Hold').removeClass('btn-active'); - $('#twp-record-btn').text('Record').removeClass('btn-active'); - - // Reset resume button - const $resumeBtn = $('#twp-resume-btn'); - $resumeBtn.hide(); - - // Restart alerts if enabled and there are waiting calls - if (alertEnabled) { - const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); - if (hasWaitingCalls) { - setTimeout(startAlert, 1000); // Small delay to avoid immediate alert - } - } - } - - /** - * Load user's assigned queues - */ - function loadUserQueues(silent = false) { - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_agent_queues', - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success) { - // Check for new calls in queues - checkForNewCalls(response.data); - userQueues = response.data; - displayQueues(); - } else if (!silent) { - showMessage('Failed to load queues: ' + (response.data || 'Unknown error'), 'error'); - } - }, - error: function() { - if (!silent) { - showMessage('Failed to load queues', 'error'); - } - } - }); - } - - /** - * Check for new calls in queues and trigger alerts - */ - function checkForNewCalls(newQueues) { - let hasWaitingCalls = false; - let newCallDetected = false; - - newQueues.forEach(function(queue) { - const queueId = queue.id; - const currentWaiting = parseInt(queue.current_waiting) || 0; - const previousWaiting = lastQueueUpdate[queueId] || 0; - - // Track if any queue has waiting calls - if (currentWaiting > 0) { - hasWaitingCalls = true; - } - - // If waiting count increased, we have new calls - if (currentWaiting > previousWaiting) { - console.log('New call detected in queue:', queue.queue_name); - newCallDetected = true; - - // Show browser notification for new call - if (notificationPermission === 'granted') { - showBrowserNotification('πŸ“ž New Call in Queue!', { - body: `${queue.queue_name}: ${currentWaiting} call${currentWaiting > 1 ? 's' : ''} waiting`, - icon: 'πŸ“ž', - vibrate: [300, 200, 300], - requireInteraction: true, - tag: `queue-${queue.id}`, - data: { - queueId: queue.id - } - }); - } - } - - lastQueueUpdate[queueId] = currentWaiting; - }); - - // Manage alerts based on queue state - if (alertEnabled && !currentCall) { - if (newCallDetected) { - // Start alert for new calls - startAlert(); - } else if (!hasWaitingCalls) { - // Stop alert if no calls are waiting in any queue - console.log('No calls waiting in any queue, stopping alerts'); - stopAlert(); - } - } - } - - /** - * Display queues in the UI - */ - function displayQueues() { - const $queueList = $('#twp-queue-list'); - - if (userQueues.length === 0) { - $queueList.html('
No queues assigned to you.
Personal queues will be created automatically.
'); - $('#twp-queue-section').hide(); - $('#twp-queue-global-actions').hide(); - return; - } - - $('#twp-queue-section').show(); - $('#twp-queue-global-actions').show(); - - let html = ''; - let userExtension = null; - - userQueues.forEach(function(queue) { - const hasWaiting = parseInt(queue.current_waiting) > 0; - const waitingCount = queue.current_waiting || 0; - const queueType = queue.queue_type || 'general'; - - // Extract user extension from personal queues - if (queueType === 'personal' && queue.extension) { - userExtension = queue.extension; - } - - // Generate queue type indicator and description - let typeIndicator = ''; - let typeDescription = ''; - if (queueType === 'personal') { - typeIndicator = 'πŸ‘€'; - typeDescription = queue.extension ? ` (Ext: ${queue.extension})` : ''; - } else if (queueType === 'hold') { - typeIndicator = '⏸️'; - typeDescription = ' (Hold)'; - } else { - typeIndicator = 'πŸ“‹'; - typeDescription = ' (Team)'; - } - - html += ` -
-
-
- ${typeIndicator} - ${queue.queue_name}${typeDescription} -
-
-
- - ${waitingCount} waiting - - - Max: ${queue.max_size} - -
-
- `; - }); - - $queueList.html(html); - - // Show user extension in queue global actions if we found it - if (userExtension) { - const $globalActions = $('#twp-queue-global-actions .global-queue-actions'); - if ($globalActions.find('.user-extension-display').length === 0) { - $globalActions.prepend(`
πŸ“ž Your Extension: ${userExtension}
`); - } - } - - // Auto-select first queue with calls, or first personal queue, or first queue - const firstQueueWithCalls = userQueues.find(q => parseInt(q.current_waiting) > 0); - const firstPersonalQueue = userQueues.find(q => q.queue_type === 'personal'); - const queueToSelect = firstQueueWithCalls || firstPersonalQueue || userQueues[0]; - if (queueToSelect) { - selectQueue(queueToSelect.id); - } - } - - /** - * Select a queue - */ - function selectQueue(queueId) { - selectedQueue = userQueues.find(q => q.id == queueId); - - if (!selectedQueue) return; - - // Update UI selection - $('.queue-item').removeClass('selected'); - $(`.queue-item[data-queue-id="${queueId}"]`).addClass('selected'); - - // Update queue controls - $('#selected-queue-name').text(selectedQueue.queue_name); - $('#twp-waiting-count').text(selectedQueue.current_waiting || 0); - $('#twp-queue-max-size').text(selectedQueue.max_size); - - // Show queue controls if there are waiting calls - if (parseInt(selectedQueue.current_waiting) > 0) { - $('#twp-queue-controls').show(); - } else { - $('#twp-queue-controls').hide(); - } - } - - /** - * Accept next call from selected queue - */ - function acceptQueueCall() { - if (!selectedQueue) { - showMessage('Please select a queue first', 'error'); - return; - } - - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_accept_next_queue_call', - queue_id: selectedQueue.id, - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success) { - showMessage('Connecting to next caller...', 'info'); - // Refresh queue data after accepting call - setTimeout(loadUserQueues, 1000); - } else { - showMessage(response.data || 'No calls waiting in this queue', 'info'); - } - }, - error: function() { - showMessage('Failed to accept queue call', 'error'); - } - }); - } - - /** - * Update call state UI - */ - function updateCallState(state) { - const $callBtn = $('#twp-call-btn'); - const $hangupBtn = $('#twp-hangup-btn'); - const $resumeBtn = $('#twp-resume-btn'); - const $controlsPanel = $('#twp-call-controls-panel'); - - switch (state) { - case 'idle': - $callBtn.show().prop('disabled', false); - $hangupBtn.hide(); - $resumeBtn.hide(); - $controlsPanel.hide(); - break; - case 'connecting': - case 'ringing': - $callBtn.hide(); - $hangupBtn.show(); - $resumeBtn.hide(); - $controlsPanel.hide(); - break; - case 'connected': - $callBtn.hide(); - $hangupBtn.show(); - $resumeBtn.hide(); // Will be shown by hold logic when needed - $controlsPanel.show(); - break; - } - } - - /** - * Show call info panel - */ - function showCallInfo(status) { - $('#twp-call-info').show(); - $('#twp-call-status').text(status); - } - - /** - * Hide call info panel - */ - function hideCallInfo() { - $('#twp-call-info').hide(); - $('#twp-call-timer').text('00:00'); - $('#twp-call-status').text(''); - } - - /** - * Start call timer - */ - function startCallTimer() { - callStartTime = new Date(); - callTimer = setInterval(updateCallTimer, 1000); - updateCallTimer(); - } - - /** - * Stop call timer - */ - function stopCallTimer() { - if (callTimer) { - clearInterval(callTimer); - callTimer = null; - } - callStartTime = null; - } - - /** - * Update call timer display - */ - function updateCallTimer() { - if (!callStartTime) return; - - const elapsed = Math.floor((new Date() - callStartTime) / 1000); - const minutes = Math.floor(elapsed / 60); - const seconds = elapsed % 60; - - const timeString = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); - $('#twp-call-timer').text(timeString); - } - - /** - * Update connection status - */ - function updateStatus(status, text) { - const $indicator = $('#twp-status-indicator'); - const $text = $('#twp-status-text'); - - $indicator.removeClass('offline connecting online').addClass(status); - $text.text(text); - } - - /** - * Show message to user - */ - function showMessage(message, type) { - const $messages = $('#twp-messages'); - const $message = $('
').addClass('twp-' + type).text(message); - - $messages.empty().append($message); - - // Auto-hide success and info messages - if (type === 'success' || type === 'info') { - setTimeout(function() { - $message.fadeOut(function() { - $message.remove(); - }); - }, 5000); - } - } - - // Start queue polling with faster interval - startQueuePolling(); - - /** - * Start polling for queue updates - */ - function startQueuePolling() { - // Clear any existing timer - if (queuePollingTimer) { - clearInterval(queuePollingTimer); - } - - // Poll every 5 seconds for real-time updates - queuePollingTimer = setInterval(function() { - if (isConnected) { - loadUserQueues(true); // Silent update - } - }, 5000); // Every 5 seconds - } - - /** - * Schedule token refresh - * Refreshes token 10 minutes before expiry for safety - */ - function scheduleTokenRefresh() { - // Clear any existing timer - if (tokenRefreshTimer) { - clearTimeout(tokenRefreshTimer); - } - - if (!tokenExpiry) { - console.error('Token expiry time not set'); - // Retry in 30 seconds if token expiry not set - setTimeout(function() { - if (tokenExpiry) { - scheduleTokenRefresh(); - } - }, 30000); - return; - } - - // Calculate time until refresh (10 minutes before expiry for extra safety) - const refreshBuffer = 10 * 60 * 1000; // 10 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); - } - }); - } - - /** - * Initialize alert sound - */ - function initAlertSound() { - // Create audio element for alert sound - alertSound = new Audio(); - alertSound.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='; // Simple beep sound - // Use Web Audio API for better sound - const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - - // Create a simple beep sound - function playBeep() { - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); - - oscillator.frequency.value = 800; // Frequency in Hz - gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); - - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.5); - } - - return playBeep; - } - - const playAlertSound = initAlertSound(); - - /** - * Start alert for new calls - */ - function startAlert() { - if (!alertEnabled || alertInterval) return; - - // Check if there are actually waiting calls before starting alert - const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); - if (!hasWaitingCalls) { - console.log('No waiting calls found, not starting alert'); - return; - } - - // Play initial alert - playAlertSound(); - - // Repeat every 30 seconds - alertInterval = setInterval(function() { - if (alertEnabled && !currentCall) { - // Check if there are still waiting calls - const stillHasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); - if (stillHasWaitingCalls) { - playAlertSound(); - } else { - console.log('No more waiting calls, stopping alert'); - stopAlert(); - } - } else { - stopAlert(); - } - }, 30000); - } - - /** - * Stop alert - */ - function stopAlert() { - if (alertInterval) { - clearInterval(alertInterval); - alertInterval = null; - } - } - - /** - * Toggle alert on/off - */ - function toggleAlert() { - alertEnabled = !alertEnabled; - localStorage.setItem('twp_alert_enabled', alertEnabled); - - // Update button state - updateAlertButton(); - - if (!alertEnabled) { - stopAlert(); - showMessage('Queue alerts disabled', 'info'); - } else { - showMessage('Queue alerts enabled', 'success'); - // Check if there are waiting calls - const hasWaitingCalls = userQueues.some(q => parseInt(q.current_waiting) > 0); - if (hasWaitingCalls && !currentCall) { - startAlert(); - } - } - } - - /** - * Update alert button UI - */ - function updateAlertButton() { - const $btn = $('#twp-alert-toggle'); - if (alertEnabled) { - $btn.removeClass('alert-off').addClass('alert-on').html('πŸ”” Alerts ON'); - } else { - $btn.removeClass('alert-on').addClass('alert-off').html('πŸ”• Alerts OFF'); - } - } - - /** - * Load alert preference from localStorage - */ - function loadAlertPreference() { - const saved = localStorage.getItem('twp_alert_enabled'); - alertEnabled = saved === null ? true : saved === 'true'; - updateAlertButton(); - } - - // Clean up on page unload - $(window).on('beforeunload', function() { - if (tokenRefreshTimer) { - clearTimeout(tokenRefreshTimer); - } - if (queuePollingTimer) { - clearInterval(queuePollingTimer); - } - if (alertInterval) { - clearInterval(alertInterval); - } - if (backgroundAlertInterval) { - clearInterval(backgroundAlertInterval); - } - if (personalQueueTimer) { - clearInterval(personalQueueTimer); - } - if (twilioDevice) { - twilioDevice.destroy(); - } - }); - - /** - * Load user's voicemails - */ - function loadUserVoicemails(silent = false) { - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_user_voicemails', - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success) { - displayVoicemails(response.data); - } else if (!silent) { - showMessage('Failed to load voicemails: ' + (response.data || 'Unknown error'), 'error'); - } - }, - error: function() { - if (!silent) { - showMessage('Failed to load voicemails', 'error'); - } - } - }); - } - - /** - * Toggle voicemail section visibility - */ - function toggleVoicemailSection() { - const $content = $('#twp-voicemail-content'); - const $toggle = $('#twp-voicemail-toggle .toggle-icon'); - const isVisible = $content.is(':visible'); - - if (isVisible) { - $content.slideUp(300); - $toggle.text('β–Ό'); - localStorage.setItem('twp_voicemail_collapsed', 'true'); - } else { - $content.slideDown(300); - $toggle.text('β–²'); - localStorage.setItem('twp_voicemail_collapsed', 'false'); - // Load voicemails when expanding if not already loaded - if ($('#twp-voicemail-list').children('.voicemail-loading').length > 0) { - loadUserVoicemails(); - } - } - } - - /** - * Initialize voicemail section state - */ - function initVoicemailSection() { - const isCollapsed = localStorage.getItem('twp_voicemail_collapsed') === 'true'; - const $content = $('#twp-voicemail-content'); - const $toggle = $('#twp-voicemail-toggle .toggle-icon'); - - if (isCollapsed) { - $content.hide(); - $toggle.text('β–Ό'); - } else { - $content.show(); - $toggle.text('β–²'); - // Load voicemails immediately if expanded - loadUserVoicemails(); - } - } - - /** - * Display voicemails in the UI - */ - function displayVoicemails(data) { - const $voicemailList = $('#twp-voicemail-list'); - - // Update stats - $('#twp-total-voicemails').text(data.total_count || 0); - $('#twp-today-voicemails').text(data.today_count || 0); - - if (!data.voicemails || data.voicemails.length === 0) { - $voicemailList.html('
No voicemails found.
'); - return; - } - - let html = ''; - data.voicemails.forEach(function(voicemail) { - const hasTranscription = voicemail.transcription && voicemail.transcription !== 'No transcription'; - const hasRecording = voicemail.has_recording; - - html += ` -
-
-
- πŸ“ž - ${voicemail.from_number} -
-
${voicemail.time_ago}
-
-
-
- ⏱️ - ${formatDuration(voicemail.duration)} -
- ${hasRecording ? '🎡 Recording' : ''} -
- ${hasTranscription ? `
${voicemail.transcription}
` : ''} -
- `; - }); - - $voicemailList.html(html); - } - - /** - * Format duration in seconds to mm:ss - */ - function formatDuration(seconds) { - if (!seconds || seconds === 0) return '0:00'; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return minutes + ':' + String(remainingSeconds).padStart(2, '0'); - } - - /** - * Play voicemail audio - */ - function playVoicemail(voicemailId) { - if (!voicemailId) return; - - // Get voicemail audio URL and play it - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_voicemail_audio', - voicemail_id: voicemailId, - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success && response.data.audio_url) { - // Create and play audio element - const audio = new Audio(response.data.audio_url); - audio.play().catch(function(error) { - showMessage('Failed to play voicemail: ' + error.message, 'error'); - }); - showMessage('Playing voicemail...', 'info'); - } else { - showMessage('No audio available for this voicemail', 'error'); - } - }, - error: function() { - showMessage('Failed to load voicemail audio', 'error'); - } - }); - } - - /** - * Initialize browser notifications - */ - function initializeNotifications() { - // Register service worker for background notifications - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/wp-content/plugins/twilio-wp-plugin/assets/js/twp-service-worker.js') - .then(function(registration) { - console.log('Service Worker registered:', registration); - }) - .catch(function(error) { - console.log('Service Worker registration failed:', error); - }); - } - - // Check if browser supports notifications - if (!('Notification' in window)) { - console.log('This browser does not support notifications'); - return; - } - - // Check current permission status - notificationPermission = Notification.permission; - - // Request permission if not already granted or denied - if (notificationPermission === 'default') { - // Add a button to request permission - if ($('#twp-queue-global-actions').length > 0) { - const $notificationBtn = $(' - -
- - - `; - - $('body').append(dialog); - - // Handle transfer option selection - let selectedTransfer = null; - $('.transfer-option').on('click', function() { - const agentOption = $(this).closest('.agent-option'); - const queueOption = $(this).closest('.queue-option'); - - // Check if agent is available (only for agents, not queues) - if (agentOption.length && agentOption.hasClass('offline')) { - showMessage('Cannot transfer to offline agents', 'error'); - return; - } - - // Clear other selections - $('.transfer-option').removeClass('selected'); - $('#twp-transfer-manual-number').val(''); - - // Select this option - $(this).addClass('selected'); - - selectedTransfer = { - type: $(this).data('type'), - target: $(this).data('target'), - agentId: agentOption.length ? agentOption.data('agent-id') : null, - queueId: queueOption.length ? queueOption.data('queue-id') : null - }; - - $('#twp-confirm-agent-transfer').prop('disabled', false); - }); - - // Handle manual number entry - $('#twp-transfer-manual-number').on('input', function() { - const input = $(this).val().trim(); - if (input) { - $('.transfer-option').removeClass('selected'); - - // Determine if it's an extension or phone number - let transferType, transferTarget; - if (/^\d{3,4}$/.test(input)) { - // Extension - transferType = 'extension'; - transferTarget = input; - } else { - // Phone number - transferType = 'phone'; - transferTarget = input; - } - - selectedTransfer = { type: transferType, target: transferTarget }; - $('#twp-confirm-agent-transfer').prop('disabled', false); - } else { - $('#twp-confirm-agent-transfer').prop('disabled', !selectedTransfer); - } - }); - - // Handle transfer confirmation - $('#twp-confirm-agent-transfer').on('click', function() { - if (selectedTransfer) { - transferToTarget(selectedTransfer.type, selectedTransfer.target); - } - }); - } - - /** - * Build and display agent transfer dialog with loaded agents (legacy) - */ - function buildAgentTransferDialog(agents) { - let agentOptions = '
'; - - if (agents.length === 0) { - agentOptions += '

No other agents available for transfer

'; - } else { - agents.forEach(function(agent) { - const statusClass = agent.status === 'available' ? 'available' : - agent.status === 'busy' ? 'busy' : 'offline'; - const statusText = agent.status === 'available' ? '🟒 Available' : - agent.status === 'busy' ? 'πŸ”΄ Busy' : '⚫ Offline'; - - // Determine transfer options - let transferOptions = '
'; - - // Add browser phone queue option (always available) - transferOptions += ` -
- πŸ’» Browser Phone Queue -
- `; - - // Add phone option if available - if (agent.has_phone && agent.phone) { - transferOptions += ` -
- πŸ“± Phone (${agent.phone}) -
- `; - } - - transferOptions += '
'; - - agentOptions += ` -
-
- ${agent.name} - ${statusText} -
- ${transferOptions} -
- `; - }); - } - - agentOptions += '
'; - - const dialog = ` -
-
-

Transfer Call to Agent

-

Select an agent and transfer method:

- ${agentOptions} -
-

Manual Transfer

-

Or enter a phone number directly:

- -
-
- - -
-
-
- `; - - $('body').append(dialog); - - // Handle transfer option selection - let selectedTransfer = null; - $('.transfer-option').on('click', function() { - const agentOption = $(this).closest('.agent-option'); - - // Check if agent is available - if (agentOption.hasClass('offline')) { - showMessage('Cannot transfer to offline agents', 'error'); - return; - } - - // Clear other selections - $('.transfer-option').removeClass('selected'); - $('#twp-transfer-manual-number').val(''); - - // Select this option - $(this).addClass('selected'); - - selectedTransfer = { - type: $(this).data('type'), - target: $(this).data('target'), - agentId: agentOption.data('agent-id') - }; - - $('#twp-confirm-agent-transfer').prop('disabled', false); - }); - - // Handle manual number entry - $('#twp-transfer-manual-number').on('input', function() { - const number = $(this).val().trim(); - if (number) { - $('.transfer-option').removeClass('selected'); - selectedTransfer = { type: 'phone', target: number }; - $('#twp-confirm-agent-transfer').prop('disabled', false); - } else { - $('#twp-confirm-agent-transfer').prop('disabled', !selectedTransfer); - } - }); - - // Handle transfer confirmation - $('#twp-confirm-agent-transfer').on('click', function() { - if (selectedTransfer) { - transferToTarget(selectedTransfer.type, selectedTransfer.target); - } - }); - } - - /** - * Show manual transfer dialog (fallback) - */ - function showManualTransferDialog() { - const dialog = ` -
-
-

Transfer Call

-

Enter the phone number to transfer this call:

- -
- - -
-
-
- `; - $('body').append(dialog); - } - - - /** - * Hide transfer dialog - */ - function hideTransferDialog() { - $('#twp-transfer-dialog').remove(); - } - - /** - * Show requeue dialog - */ - function showRequeueDialog() { - // Load available queues first - $.ajax({ - url: twp_frontend_ajax.ajax_url, - method: 'POST', - data: { - action: 'twp_get_requeue_queues', - nonce: twp_frontend_ajax.nonce - }, - success: function(response) { - if (response.success && response.data.length > 0) { - let options = ''; - response.data.forEach(function(queue) { - options += ``; - }); - - const dialog = ` -
-
-

Requeue Call

-

Select a queue to transfer this call to:

- -
- - -
-
-
- `; - $('body').append(dialog); - } else { - showMessage('No queues available', 'error'); - } - }, - error: function() { - showMessage('Failed to load queues', 'error'); - } - }); - } - - /** - * Hide requeue dialog - */ - function hideRequeueDialog() { - $('#twp-requeue-dialog').remove(); - } - -})(jQuery); \ No newline at end of file diff --git a/includes/class-twp-shortcodes.php b/includes/class-twp-shortcodes.php index 8d24741..26b05ce 100644 --- a/includes/class-twp-shortcodes.php +++ b/includes/class-twp-shortcodes.php @@ -21,67 +21,44 @@ class TWP_Shortcodes { /** * Enqueue frontend assets when shortcode is present + * Since we now redirect to admin, only basic styling is needed */ public static function enqueue_frontend_assets() { global $post; if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'twp_browser_phone')) { - // Enqueue Twilio Voice SDK from unpkg CDN (same as backend) - wp_enqueue_script( - 'twilio-voice-sdk', - 'https://unpkg.com/@twilio/voice-sdk@2.11.0/dist/twilio.min.js', - array(), - '2.11.0', - false // Load in head to ensure it's available - ); - - // Add backup SDK loading - wp_add_inline_script('twilio-voice-sdk', " - window.twpLoadTwilioSDK = function() { - if (typeof Twilio === 'undefined') { - console.warn('Primary Twilio SDK failed, attempting fallback load'); - var script = document.createElement('script'); - script.src = 'https://unpkg.com/@twilio/voice-sdk@2.11.0/dist/twilio.min.js'; - script.onload = function() { console.log('Fallback Twilio SDK loaded'); }; - script.onerror = function() { console.error('Fallback Twilio SDK failed'); }; - document.head.appendChild(script); - } - }; - - // Try loading after a delay if not available - setTimeout(window.twpLoadTwilioSDK, 2000); - ", 'after'); - - // Enqueue our browser phone script - wp_enqueue_script( - 'twp-browser-phone-frontend', - TWP_PLUGIN_URL . 'assets/js/browser-phone-frontend.js', - array('jquery', 'twilio-voice-sdk'), - TWP_VERSION, - true - ); - - // Enqueue mobile-friendly styles - wp_enqueue_style( - 'twp-browser-phone-frontend', - TWP_PLUGIN_URL . 'assets/css/browser-phone-frontend.css', - array(), - TWP_VERSION - ); - - // Localize script with AJAX data - wp_localize_script('twp-browser-phone-frontend', 'twp_frontend_ajax', array( - 'ajax_url' => admin_url('admin-ajax.php'), - 'admin_url' => admin_url(), - 'nonce' => wp_create_nonce('twp_frontend_nonce'), - 'user_id' => get_current_user_id(), - 'is_logged_in' => is_user_logged_in() - )); + // Add basic styling for the redirect interface + wp_add_inline_style('wp-admin', ' + .twp-browser-phone-redirect { + padding: 20px; + border: 1px solid #ddd; + border-radius: 5px; + background: #f9f9f9; + text-align: center; + margin: 20px 0; + } + .twp-browser-phone-redirect h3 { + margin-top: 0; + color: #333; + } + .twp-browser-phone-redirect p { + margin-bottom: 15px; + } + .twp-error { + padding: 10px; + border: 1px solid #dc3232; + border-radius: 3px; + background: #fbeaea; + color: #dc3232; + margin: 10px 0; + } + '); } } /** * Browser phone shortcode handler + * Redirects users to the admin browser phone page */ public static function browser_phone_shortcode($atts) { // Check if user is logged in @@ -97,174 +74,24 @@ class TWP_Shortcodes { // Parse shortcode attributes $atts = shortcode_atts(array( 'title' => 'Browser Phone', - 'show_title' => 'true', - 'compact' => 'false' + 'button_text' => 'Access Browser Phone', + 'target' => '_blank' ), $atts, 'twp_browser_phone'); - $show_title = ($atts['show_title'] === 'true'); - $compact_mode = ($atts['compact'] === 'true'); + // Generate admin URL for browser phone page + $admin_url = admin_url('admin.php?page=twilio-wp-browser-phone'); + $target = ($atts['target'] === '_blank') ? 'target="_blank"' : ''; ob_start(); ?> -
- -

- - - -
- - Connecting... -
- - -
- - -
- - -
-
- - -
- -
- - - - - - - - - - - - -
-
- - -
- - - -
- - - - - - - - -
-

Your Queues

-
-
Loading queues...
-
- - - - - - -
- - -
-
-

Recent Voicemails

-
- - Total: 0 | - Today: 0 - - -
-
- - -
- - - - - -
+
+

+

The browser phone interface has been moved to the WordPress admin area for enhanced security and functionality.

+

+ class="button button-primary"> + + +