diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php
index ae7eead..f80e6c9 100644
--- a/admin/class-twp-admin.php
+++ b/admin/class-twp-admin.php
@@ -119,7 +119,16 @@ class TWP_Admin {
'twilio-wp-settings',
array($this, 'display_plugin_settings')
);
-
+
+ add_submenu_page(
+ 'twilio-wp-plugin',
+ 'Mobile App',
+ 'Mobile App',
+ 'manage_options',
+ 'twilio-wp-mobile-app',
+ array($this, 'display_mobile_app_settings')
+ );
+
add_submenu_page(
'twilio-wp-plugin',
'Phone Schedules',
@@ -10049,5 +10058,12 @@ class TWP_Admin {
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
}
}
-
+
+ /**
+ * Display mobile app settings page
+ */
+ public function display_mobile_app_settings() {
+ require_once TWP_PLUGIN_DIR . 'admin/mobile-app-settings.php';
+ }
+
}
\ No newline at end of file
diff --git a/admin/mobile-app-settings.php b/admin/mobile-app-settings.php
new file mode 100644
index 0000000..94c2d47
--- /dev/null
+++ b/admin/mobile-app-settings.php
@@ -0,0 +1,353 @@
+manual_check_for_updates();
+}
+
+// Handle test notification
+if (isset($_POST['twp_test_notification']) && check_admin_referer('twp_mobile_settings')) {
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
+ $fcm = new TWP_FCM();
+ $test_user_id = get_current_user_id();
+ $notification_sent = $fcm->send_test_notification($test_user_id);
+
+ if ($notification_sent) {
+ $notification_result = array('success' => true, 'message' => 'Test notification sent successfully!');
+ } else {
+ $notification_result = array('success' => false, 'message' => 'Failed to send test notification. Check FCM configuration.');
+ }
+}
+
+// Save settings
+if (isset($_POST['twp_save_mobile_settings']) && check_admin_referer('twp_mobile_settings')) {
+ update_option('twp_fcm_server_key', sanitize_text_field($_POST['twp_fcm_server_key']));
+ update_option('twp_auto_update_enabled', isset($_POST['twp_auto_update_enabled']) ? '1' : '0');
+ update_option('twp_gitea_repo', sanitize_text_field($_POST['twp_gitea_repo']));
+ update_option('twp_gitea_token', sanitize_text_field($_POST['twp_gitea_token']));
+
+ $settings_saved = true;
+}
+
+// Get current settings
+$fcm_server_key = get_option('twp_fcm_server_key', '');
+$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
+$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
+$gitea_token = get_option('twp_gitea_token', '');
+
+// Get update status
+require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
+$updater = new TWP_Auto_Updater();
+$update_status = $updater->get_update_status();
+
+// Get mobile app statistics
+global $wpdb;
+$sessions_table = $wpdb->prefix . 'twp_mobile_sessions';
+$active_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table WHERE is_active = 1 AND expires_at > NOW()");
+$total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
+
+?>
+
+
+
+
+
+
+
Settings saved successfully!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mobile App Overview
+
+
+
+ | API Endpoint: |
+ |
+
+
+ | Active Sessions: |
+ active / total |
+
+
+ | Plugin Version: |
+ |
+
+
+
+
+
+
+
+
+
+ 0): ?>
+
+
Active Mobile Sessions
+ get_results("
+ SELECT s.user_id, s.device_info, s.logged_in_at, s.last_used, u.user_login, u.display_name
+ FROM $sessions_table s
+ JOIN {$wpdb->users} u ON s.user_id = u.ID
+ WHERE s.is_active = 1 AND s.expires_at > NOW()
+ ORDER BY s.last_used DESC
+ LIMIT 20
+ ");
+ ?>
+
+
+
+ | User |
+ Device |
+ Last Activity |
+
+
+
+
+
+ | display_name ?: $session->user_login); ?> |
+ device_info ?: 'Unknown device'); ?> |
+ last_used), current_time('timestamp')) . ' ago'); ?> |
+
+
+
+
+
+
+
+
+
+
diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php
index 9158eb9..0152161 100644
--- a/includes/class-twp-activator.php
+++ b/includes/class-twp-activator.php
@@ -52,7 +52,8 @@ class TWP_Activator {
'twp_callbacks',
'twp_call_recordings',
'twp_user_extensions',
- 'twp_queue_assignments'
+ 'twp_queue_assignments',
+ 'twp_mobile_sessions'
);
$missing_tables = array();
@@ -361,7 +362,25 @@ class TWP_Activator {
KEY agent_id (agent_id),
KEY started_at (started_at)
) $charset_collate;";
-
+
+ // Mobile sessions table
+ $table_mobile_sessions = $wpdb->prefix . 'twp_mobile_sessions';
+ $sql_mobile_sessions = "CREATE TABLE $table_mobile_sessions (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ user_id bigint(20) NOT NULL,
+ refresh_token varchar(500) NOT NULL,
+ fcm_token text,
+ device_info text,
+ created_at datetime DEFAULT CURRENT_TIMESTAMP,
+ expires_at datetime NOT NULL,
+ last_used datetime DEFAULT CURRENT_TIMESTAMP,
+ is_active tinyint(1) DEFAULT 1,
+ PRIMARY KEY (id),
+ KEY user_id (user_id),
+ KEY is_active (is_active),
+ KEY expires_at (expires_at)
+ ) $charset_collate;";
+
dbDelta($sql_schedules);
dbDelta($sql_queues);
dbDelta($sql_queued_calls);
@@ -377,7 +396,8 @@ class TWP_Activator {
dbDelta($sql_recordings);
dbDelta($sql_user_extensions);
dbDelta($sql_queue_assignments);
-
+ dbDelta($sql_mobile_sessions);
+
// Add missing columns for existing installations
self::add_missing_columns();
}
diff --git a/includes/class-twp-auto-updater.php b/includes/class-twp-auto-updater.php
new file mode 100644
index 0000000..2d462d9
--- /dev/null
+++ b/includes/class-twp-auto-updater.php
@@ -0,0 +1,291 @@
+plugin_basename = plugin_basename(dirname(dirname(__FILE__)) . '/twilio-wp-plugin.php');
+ $this->current_version = defined('TWP_VERSION') ? TWP_VERSION : '0.0.0';
+ $this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
+ }
+
+ /**
+ * Initialize updater hooks
+ */
+ public function init() {
+ // Hook into WordPress update checks
+ add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update'));
+ add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
+
+ // Add settings page for manual check
+ add_action('admin_init', array($this, 'register_settings'));
+
+ // Add update check to admin notices
+ if (get_option('twp_auto_update_enabled', '1') === '1') {
+ add_action('admin_init', array($this, 'maybe_auto_check_updates'));
+ }
+ }
+
+ /**
+ * Register auto-update settings
+ */
+ public function register_settings() {
+ register_setting('twp_settings', 'twp_auto_update_enabled');
+ register_setting('twp_settings', 'twp_gitea_repo');
+ register_setting('twp_settings', 'twp_gitea_token'); // Optional for private repos
+ }
+
+ /**
+ * Check for updates periodically
+ */
+ public function maybe_auto_check_updates() {
+ $last_check = get_option('twp_last_update_check', 0);
+ $check_interval = 12 * HOUR_IN_SECONDS; // Check every 12 hours
+
+ if (time() - $last_check > $check_interval) {
+ update_option('twp_last_update_check', time());
+ // Force WordPress to check for updates
+ wp_clean_plugins_cache();
+ }
+ }
+
+ /**
+ * Check for plugin updates
+ */
+ public function check_for_update($transient) {
+ if (empty($transient->checked)) {
+ return $transient;
+ }
+
+ // Get Gitea repo from settings if available
+ $custom_repo = get_option('twp_gitea_repo', '');
+ if (!empty($custom_repo)) {
+ $this->gitea_repo = $custom_repo;
+ $this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
+ }
+
+ $update_info = $this->get_latest_release();
+
+ if ($update_info && version_compare($this->current_version, $update_info->version, '<')) {
+ $plugin_data = array(
+ 'id' => 'twilio-wp-plugin',
+ 'slug' => $this->plugin_slug,
+ 'plugin' => $this->plugin_basename,
+ 'new_version' => $update_info->version,
+ 'url' => $update_info->homepage,
+ 'package' => $update_info->download_url,
+ 'tested' => '6.8',
+ 'requires' => '5.8',
+ 'requires_php' => '7.4',
+ 'icons' => array(),
+ 'banners' => array(),
+ 'compatibility' => new stdClass(),
+ );
+
+ $transient->response[$this->plugin_basename] = (object) $plugin_data;
+
+ error_log("TWP Auto-Updater: New version {$update_info->version} available (current: {$this->current_version})");
+ } else {
+ error_log("TWP Auto-Updater: No updates available (current: {$this->current_version})");
+ }
+
+ return $transient;
+ }
+
+ /**
+ * Get plugin information for update screen
+ */
+ public function plugin_info($false, $action, $args) {
+ if ($action !== 'plugin_information') {
+ return $false;
+ }
+
+ if (!isset($args->slug) || $args->slug !== $this->plugin_slug) {
+ return $false;
+ }
+
+ $update_info = $this->get_latest_release();
+
+ if (!$update_info) {
+ return $false;
+ }
+
+ $plugin_info = new stdClass();
+ $plugin_info->name = 'Twilio WP Plugin';
+ $plugin_info->slug = $this->plugin_slug;
+ $plugin_info->version = $update_info->version;
+ $plugin_info->author = 'Joshua Knapp';
+ $plugin_info->homepage = $update_info->homepage;
+ $plugin_info->download_link = $update_info->download_url;
+ $plugin_info->requires = '5.8';
+ $plugin_info->tested = '6.8';
+ $plugin_info->requires_php = '7.4';
+ $plugin_info->last_updated = $update_info->release_date;
+ $plugin_info->downloaded = 10;
+
+ $plugin_info->sections = array(
+ 'description' => 'Twilio WordPress Plugin for call management and mobile app support.
',
+ 'changelog' => '' . esc_html($update_info->changelog) . '
',
+ 'installation' => 'Upload the plugin to your WordPress site and activate it.
'
+ );
+
+ return $plugin_info;
+ }
+
+ /**
+ * Get latest release information from Gitea
+ */
+ private function get_latest_release() {
+ // Check cache first (1 hour)
+ $cache_key = 'twp_latest_release';
+ $cached = get_transient($cache_key);
+
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ // Use cURL for Gitea API
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $this->gitea_api_url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
+ curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Twilio-WP-Plugin-Updater');
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+ // Add Gitea token if configured (for private repos)
+ $gitea_token = get_option('twp_gitea_token', '');
+ if (!empty($gitea_token)) {
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Accept: application/json',
+ 'Authorization: token ' . $gitea_token
+ ));
+ }
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if (!$response || $http_code !== 200) {
+ error_log("TWP Auto-Updater: Gitea API returned status $http_code");
+ return false;
+ }
+
+ $release = json_decode($response);
+
+ if (!$release || !isset($release->tag_name)) {
+ error_log('TWP Auto-Updater: Invalid release data from Gitea');
+ return false;
+ }
+
+ // Parse release information
+ $version = ltrim($release->tag_name, 'v'); // Remove 'v' prefix if present
+ $download_url = null;
+
+ // Find the zip asset
+ if (isset($release->assets) && is_array($release->assets)) {
+ foreach ($release->assets as $asset) {
+ if (strpos($asset->name, '.zip') !== false) {
+ $download_url = $asset->browser_download_url;
+ break;
+ }
+ }
+ }
+
+ // Fallback to zipball if no asset found
+ if (!$download_url) {
+ $download_url = $release->zipball_url;
+ }
+
+ // Format changelog
+ $changelog = !empty($release->body) ? $release->body : 'No changelog provided for this release.';
+
+ // Handle empty changelog
+ if (empty(trim($changelog))) {
+ $changelog = "Version " . $version . "\n\n" .
+ "Released on " . date('F j, Y', strtotime($release->published_at)) . "\n\n" .
+ "* Updated plugin files";
+ }
+
+ $update_info = (object) array(
+ 'version' => $version,
+ 'download_url' => $download_url,
+ 'homepage' => $this->gitea_base_url . '/' . $this->gitea_repo,
+ 'release_date' => $release->published_at,
+ 'description' => $changelog,
+ 'changelog' => $changelog
+ );
+
+ // Cache for 1 hour
+ set_transient($cache_key, $update_info, HOUR_IN_SECONDS);
+
+ return $update_info;
+ }
+
+ /**
+ * Manual update check (for admin page)
+ */
+ public function manual_check_for_updates() {
+ // Clear cache
+ delete_transient('twp_latest_release');
+ update_option('twp_last_update_check', 0);
+
+ // Force WordPress to check
+ wp_clean_plugins_cache();
+ delete_site_transient('update_plugins');
+
+ $update_info = $this->get_latest_release();
+
+ if (!$update_info) {
+ return array(
+ 'success' => false,
+ 'message' => 'Failed to check for updates. Please check your internet connection and Gitea repository settings.'
+ );
+ }
+
+ if (version_compare($this->current_version, $update_info->version, '<')) {
+ return array(
+ 'success' => true,
+ 'update_available' => true,
+ 'current_version' => $this->current_version,
+ 'latest_version' => $update_info->version,
+ 'message' => "Update available: Version {$update_info->version}. Go to Plugins page to update."
+ );
+ } else {
+ return array(
+ 'success' => true,
+ 'update_available' => false,
+ 'current_version' => $this->current_version,
+ 'message' => 'You are running the latest version.'
+ );
+ }
+ }
+
+ /**
+ * Get current update status
+ */
+ public function get_update_status() {
+ $update_info = $this->get_latest_release();
+
+ return array(
+ 'current_version' => $this->current_version,
+ 'latest_version' => $update_info ? $update_info->version : 'Unknown',
+ 'update_available' => $update_info && version_compare($this->current_version, $update_info->version, '<'),
+ 'last_check' => get_option('twp_last_update_check', 0),
+ 'auto_update_enabled' => get_option('twp_auto_update_enabled', '1') === '1'
+ );
+ }
+}
diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php
index e542e5f..2cc9b26 100644
--- a/includes/class-twp-core.php
+++ b/includes/class-twp-core.php
@@ -33,7 +33,14 @@ class TWP_Core {
// API classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
-
+
+ // Mobile app classes
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-auth.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-api.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-sse.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
+ require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
+
// Feature classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
@@ -318,6 +325,20 @@ class TWP_Core {
// Initialize webhooks
$webhooks = new TWP_Webhooks();
$webhooks->register_endpoints();
+
+ // Initialize mobile app endpoints
+ $mobile_auth = new TWP_Mobile_Auth();
+ $mobile_auth->register_endpoints();
+
+ $mobile_api = new TWP_Mobile_API();
+ $mobile_api->register_endpoints();
+
+ $mobile_sse = new TWP_Mobile_SSE();
+ $mobile_sse->register_endpoints();
+
+ // Initialize auto-updater
+ $updater = new TWP_Auto_Updater();
+ $updater->init();
// Add custom cron schedules
add_filter('cron_schedules', function($schedules) {
diff --git a/includes/class-twp-fcm.php b/includes/class-twp-fcm.php
new file mode 100644
index 0000000..69a1b2d
--- /dev/null
+++ b/includes/class-twp-fcm.php
@@ -0,0 +1,214 @@
+server_key = get_option('twp_fcm_server_key', '');
+ }
+
+ /**
+ * Send push notification to user's devices
+ */
+ public function send_notification($user_id, $title, $body, $data = array()) {
+ if (empty($this->server_key)) {
+ error_log('TWP FCM: Server key not configured');
+ return false;
+ }
+
+ // Get user's FCM tokens
+ $tokens = $this->get_user_tokens($user_id);
+
+ if (empty($tokens)) {
+ error_log("TWP FCM: No tokens found for user $user_id");
+ return false;
+ }
+
+ $success_count = 0;
+ $failed_tokens = array();
+
+ foreach ($tokens as $token) {
+ $result = $this->send_to_token($token, $title, $body, $data);
+
+ if ($result['success']) {
+ $success_count++;
+ } else {
+ $failed_tokens[] = $token;
+
+ // If token is invalid, remove it from database
+ if ($result['error'] === 'invalid_token') {
+ $this->remove_invalid_token($token);
+ }
+ }
+ }
+
+ error_log("TWP FCM: Sent notification to $success_count/" . count($tokens) . " devices for user $user_id");
+
+ return $success_count > 0;
+ }
+
+ /**
+ * Send notification to specific token
+ */
+ private function send_to_token($token, $title, $body, $data = array()) {
+ $notification = array(
+ 'title' => $title,
+ 'body' => $body,
+ 'sound' => 'default',
+ 'priority' => 'high',
+ 'click_action' => 'FLUTTER_NOTIFICATION_CLICK'
+ );
+
+ $payload = array(
+ 'to' => $token,
+ 'notification' => $notification,
+ 'data' => array_merge($data, array(
+ 'title' => $title,
+ 'body' => $body,
+ 'timestamp' => time()
+ )),
+ 'priority' => 'high'
+ );
+
+ $headers = array(
+ 'Authorization: key=' . $this->server_key,
+ 'Content-Type: application/json'
+ );
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $this->fcm_url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code !== 200) {
+ error_log("TWP FCM: Failed to send notification. HTTP $http_code: $response");
+
+ // Check if token is invalid
+ $response_data = json_decode($response, true);
+ if (isset($response_data['results'][0]['error']) &&
+ in_array($response_data['results'][0]['error'], array('InvalidRegistration', 'NotRegistered'))) {
+ return array('success' => false, 'error' => 'invalid_token');
+ }
+
+ return array('success' => false, 'error' => 'http_error');
+ }
+
+ return array('success' => true);
+ }
+
+ /**
+ * Get all active FCM tokens for a user
+ */
+ private function get_user_tokens($user_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ return $wpdb->get_col($wpdb->prepare(
+ "SELECT fcm_token FROM $table
+ WHERE user_id = %d
+ AND is_active = 1
+ AND fcm_token IS NOT NULL
+ AND fcm_token != ''
+ AND expires_at > NOW()",
+ $user_id
+ ));
+ }
+
+ /**
+ * Remove invalid FCM token from database
+ */
+ private function remove_invalid_token($token) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ $wpdb->update(
+ $table,
+ array('fcm_token' => null),
+ array('fcm_token' => $token),
+ array('%s'),
+ array('%s')
+ );
+
+ error_log("TWP FCM: Removed invalid token from database");
+ }
+
+ /**
+ * Send incoming call notification
+ */
+ public function notify_incoming_call($user_id, $from_number, $queue_name, $call_sid) {
+ $title = 'Incoming Call';
+ $body = "Call from $from_number in $queue_name queue";
+
+ $data = array(
+ 'type' => 'incoming_call',
+ 'call_sid' => $call_sid,
+ 'from_number' => $from_number,
+ 'queue_name' => $queue_name
+ );
+
+ return $this->send_notification($user_id, $title, $body, $data);
+ }
+
+ /**
+ * Send queue timeout notification
+ */
+ public function notify_queue_timeout($user_id, $queue_name, $waiting_count) {
+ $title = 'Queue Alert';
+ $body = "$queue_name has $waiting_count waiting call" . ($waiting_count > 1 ? 's' : '');
+
+ $data = array(
+ 'type' => 'queue_timeout',
+ 'queue_name' => $queue_name,
+ 'waiting_count' => $waiting_count
+ );
+
+ return $this->send_notification($user_id, $title, $body, $data);
+ }
+
+ /**
+ * Send agent status change notification
+ */
+ public function notify_status_change($user_id, $old_status, $new_status) {
+ $title = 'Status Changed';
+ $body = "Your status changed from $old_status to $new_status";
+
+ $data = array(
+ 'type' => 'status_change',
+ 'old_status' => $old_status,
+ 'new_status' => $new_status
+ );
+
+ return $this->send_notification($user_id, $title, $body, $data);
+ }
+
+ /**
+ * Test notification (for settings page)
+ */
+ public function send_test_notification($user_id) {
+ $title = 'Test Notification';
+ $body = 'This is a test notification from Twilio WordPress Plugin';
+
+ $data = array(
+ 'type' => 'test',
+ 'test' => true
+ );
+
+ return $this->send_notification($user_id, $title, $body, $data);
+ }
+}
diff --git a/includes/class-twp-mobile-api.php b/includes/class-twp-mobile-api.php
new file mode 100644
index 0000000..8c1f013
--- /dev/null
+++ b/includes/class-twp-mobile-api.php
@@ -0,0 +1,684 @@
+auth = new TWP_Mobile_Auth();
+ }
+
+ /**
+ * Register REST API endpoints
+ */
+ public function register_endpoints() {
+ add_action('rest_api_init', function() {
+ // Agent status endpoints
+ register_rest_route('twilio-mobile/v1', '/agent/status', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'get_agent_status'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/agent/status', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'update_agent_status'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ // Queue state endpoint
+ register_rest_route('twilio-mobile/v1', '/queues/state', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'get_queue_state'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ // Queue calls (specific queue)
+ register_rest_route('twilio-mobile/v1', '/queues/(?P\d+)/calls', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'get_queue_calls'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ // Call control endpoints
+ register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/accept', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'accept_call'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/reject', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'reject_call'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/hold', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'hold_call'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/unhold', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'unhold_call'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/transfer', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'transfer_call'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ // FCM token registration
+ register_rest_route('twilio-mobile/v1', '/fcm/register', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'register_fcm_token'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ // Agent phone number
+ register_rest_route('twilio-mobile/v1', '/agent/phone', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'get_agent_phone'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+
+ register_rest_route('twilio-mobile/v1', '/agent/phone', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'update_agent_phone'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+ });
+ }
+
+ /**
+ * Get agent status
+ */
+ public function get_agent_status($request) {
+ $user_id = $this->auth->get_current_user_id();
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_agent_status';
+
+ $status = $wpdb->get_row($wpdb->prepare(
+ "SELECT status, is_logged_in, current_call_sid, last_activity, available_for_queues FROM $table WHERE user_id = %d",
+ $user_id
+ ));
+
+ if (!$status) {
+ // Create default status
+ $wpdb->insert(
+ $table,
+ array('user_id' => $user_id, 'status' => 'offline', 'is_logged_in' => 0),
+ array('%d', '%s', '%d')
+ );
+
+ $status = (object) array(
+ 'status' => 'offline',
+ 'is_logged_in' => 0,
+ 'current_call_sid' => null,
+ 'last_activity' => current_time('mysql'),
+ 'available_for_queues' => 1
+ );
+ }
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'status' => $status->status,
+ 'is_logged_in' => (bool)$status->is_logged_in,
+ 'current_call_sid' => $status->current_call_sid,
+ 'last_activity' => $status->last_activity,
+ 'available_for_queues' => (bool)$status->available_for_queues
+ ), 200);
+ }
+
+ /**
+ * Update agent status
+ */
+ public function update_agent_status($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $new_status = $request->get_param('status');
+ $is_logged_in = $request->get_param('is_logged_in');
+
+ if (!in_array($new_status, array('available', 'busy', 'offline'))) {
+ return new WP_Error('invalid_status', 'Status must be available, busy, or offline', array('status' => 400));
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_agent_status';
+
+ // Check if status exists
+ $exists = $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $table WHERE user_id = %d",
+ $user_id
+ ));
+
+ $data = array(
+ 'status' => $new_status,
+ 'last_activity' => current_time('mysql')
+ );
+
+ if ($is_logged_in !== null) {
+ $data['is_logged_in'] = $is_logged_in ? 1 : 0;
+ if ($is_logged_in) {
+ $data['logged_in_at'] = current_time('mysql');
+ }
+ }
+
+ if ($exists) {
+ $wpdb->update(
+ $table,
+ $data,
+ array('user_id' => $user_id),
+ array('%s', '%s'),
+ array('%d')
+ );
+ } else {
+ $data['user_id'] = $user_id;
+ $wpdb->insert($table, $data);
+ }
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Status updated successfully'
+ ), 200);
+ }
+
+ /**
+ * Get queue state (all queues user has access to)
+ */
+ public function get_queue_state($request) {
+ $user_id = $this->auth->get_current_user_id();
+
+ global $wpdb;
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+ $assignments_table = $wpdb->prefix . 'twp_queue_assignments';
+
+ // Get queues assigned to this user
+ $queue_ids = $wpdb->get_col($wpdb->prepare(
+ "SELECT queue_id FROM $assignments_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ // Also include personal queues
+ $personal_queue_ids = $wpdb->get_col($wpdb->prepare(
+ "SELECT id FROM $queues_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ $all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
+
+ if (empty($all_queue_ids)) {
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'queues' => array()
+ ), 200);
+ }
+
+ $queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
+
+ // Get queue information with call counts
+ $queues = $wpdb->get_results("
+ SELECT
+ q.id,
+ q.queue_name,
+ q.queue_type,
+ q.extension,
+ COUNT(c.id) as waiting_count
+ FROM $queues_table q
+ LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
+ WHERE q.id IN ($queue_ids_str)
+ GROUP BY q.id
+ ");
+
+ $result = array();
+ foreach ($queues as $queue) {
+ $result[] = array(
+ 'id' => (int)$queue->id,
+ 'name' => $queue->queue_name,
+ 'type' => $queue->queue_type,
+ 'extension' => $queue->extension,
+ 'waiting_count' => (int)$queue->waiting_count
+ );
+ }
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'queues' => $result
+ ), 200);
+ }
+
+ /**
+ * Get calls in a specific queue
+ */
+ public function get_queue_calls($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $queue_id = (int)$request['id'];
+
+ // Verify user has access to this queue
+ if (!$this->user_has_queue_access($user_id, $queue_id)) {
+ return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_queued_calls';
+
+ $calls = $wpdb->get_results($wpdb->prepare(
+ "SELECT call_sid, from_number, to_number, position, status, joined_at, enqueued_at
+ FROM $table
+ WHERE queue_id = %d AND status = 'waiting'
+ ORDER BY position ASC",
+ $queue_id
+ ));
+
+ $result = array();
+ foreach ($calls as $call) {
+ $result[] = array(
+ 'call_sid' => $call->call_sid,
+ 'from_number' => $call->from_number,
+ 'to_number' => $call->to_number,
+ 'position' => (int)$call->position,
+ 'status' => $call->status,
+ 'wait_time' => $this->calculate_wait_time($call->enqueued_at ?: $call->joined_at)
+ );
+ }
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'calls' => $result
+ ), 200);
+ }
+
+ /**
+ * Accept a call (dequeue and connect to agent)
+ */
+ public function accept_call($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $call_sid = $request['call_sid'];
+
+ // Get agent phone number
+ $agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
+
+ if (empty($agent_number)) {
+ return new WP_Error('no_phone', 'No phone number configured for agent', array('status' => 400));
+ }
+
+ // Initialize Twilio API
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
+ $twilio = new TWP_Twilio_API();
+
+ // Get call info from queue
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
+ $call_sid
+ ));
+
+ if (!$call) {
+ return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
+ }
+
+ // Verify user has access to this queue
+ if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
+ return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
+ }
+
+ try {
+ // Connect agent to call
+ $agent_call = $twilio->create_call(
+ $agent_number,
+ $call->to_number,
+ array(
+ 'url' => site_url('/wp-json/twilio-webhook/v1/connect-agent'),
+ 'statusCallback' => site_url('/wp-json/twilio-webhook/v1/agent-call-status'),
+ 'statusCallbackEvent' => array('completed', 'no-answer', 'busy', 'failed'),
+ 'timeout' => 30
+ )
+ );
+
+ // Update call record
+ $wpdb->update(
+ $calls_table,
+ array(
+ 'status' => 'connecting',
+ 'agent_phone' => $agent_number,
+ 'agent_call_sid' => $agent_call->sid
+ ),
+ array('call_sid' => $call_sid),
+ array('%s', '%s', '%s'),
+ array('%s')
+ );
+
+ // Update agent status
+ $status_table = $wpdb->prefix . 'twp_agent_status';
+ $wpdb->update(
+ $status_table,
+ array('status' => 'busy', 'current_call_sid' => $call_sid),
+ array('user_id' => $user_id),
+ array('%s', '%s'),
+ array('%d')
+ );
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Call accepted, connecting to agent',
+ 'agent_call_sid' => $agent_call->sid
+ ), 200);
+
+ } catch (Exception $e) {
+ return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
+ }
+ }
+
+ /**
+ * Reject a call (send to voicemail)
+ */
+ public function reject_call($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $call_sid = $request['call_sid'];
+
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
+ $call_sid
+ ));
+
+ if (!$call) {
+ return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
+ }
+
+ // Verify user has access to this queue
+ if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
+ return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
+ }
+
+ try {
+ // Initialize Twilio API
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
+ $twilio = new TWP_Twilio_API();
+
+ // Redirect call to voicemail
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('The agent is unavailable. Please leave a message after the tone.');
+ $twiml->record(array(
+ 'action' => site_url('/wp-json/twilio-webhook/v1/voicemail-complete'),
+ 'maxLength' => 120,
+ 'transcribe' => true
+ ));
+ $twiml->say('We did not receive a recording. Goodbye.');
+
+ $twilio->update_call($call_sid, array('twiml' => $twiml->asXML()));
+
+ // Update call status
+ $wpdb->update(
+ $calls_table,
+ array('status' => 'voicemail', 'ended_at' => current_time('mysql')),
+ array('call_sid' => $call_sid),
+ array('%s', '%s'),
+ array('%s')
+ );
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Call sent to voicemail'
+ ), 200);
+
+ } catch (Exception $e) {
+ return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
+ }
+ }
+
+ /**
+ * Hold a call
+ */
+ public function hold_call($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $call_sid = $request['call_sid'];
+
+ try {
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
+
+ $admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
+ $twilio = new TWP_Twilio_API();
+
+ // Find customer call leg
+ $customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
+
+ if (!$customer_call_sid) {
+ return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
+ }
+
+ // Get user's hold queue
+ global $wpdb;
+ $ext_table = $wpdb->prefix . 'twp_user_extensions';
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+
+ $extension = $wpdb->get_row($wpdb->prepare(
+ "SELECT hold_queue_id FROM $ext_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ if (!$extension || !$extension->hold_queue_id) {
+ return new WP_Error('no_hold_queue', 'No hold queue configured', array('status' => 400));
+ }
+
+ $hold_queue = $wpdb->get_row($wpdb->prepare(
+ "SELECT queue_name, wait_music_url FROM $queues_table WHERE id = %d",
+ $extension->hold_queue_id
+ ));
+
+ // Put call on hold
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Please hold while we transfer your call.');
+ $enqueue = $twiml->enqueue($hold_queue->queue_name, array(
+ 'waitUrl' => $hold_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
+ ));
+
+ $twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Call placed on hold'
+ ), 200);
+
+ } catch (Exception $e) {
+ return new WP_Error('hold_error', $e->getMessage(), array('status' => 500));
+ }
+ }
+
+ /**
+ * Unhold a call (resume from hold queue)
+ */
+ public function unhold_call($request) {
+ // Implementation would retrieve from hold queue and reconnect
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Unhold functionality - to be implemented with queue retrieval'
+ ), 501);
+ }
+
+ /**
+ * Transfer a call to another extension/queue
+ */
+ public function transfer_call($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $call_sid = $request['call_sid'];
+ $target = $request->get_param('target'); // Extension number or queue ID
+
+ if (empty($target)) {
+ return new WP_Error('missing_target', 'Transfer target is required', array('status' => 400));
+ }
+
+ try {
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
+
+ $admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
+ $twilio = new TWP_Twilio_API();
+
+ // Find customer call leg
+ $customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
+
+ if (!$customer_call_sid) {
+ return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
+ }
+
+ // Look up target (extension or queue)
+ global $wpdb;
+ $ext_table = $wpdb->prefix . 'twp_user_extensions';
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+
+ // Try as extension first
+ $target_queue = $wpdb->get_row($wpdb->prepare(
+ "SELECT q.* FROM $queues_table q
+ JOIN $ext_table e ON q.id = e.personal_queue_id
+ WHERE e.extension = %s",
+ $target
+ ));
+
+ // If not extension, try as queue ID
+ if (!$target_queue && is_numeric($target)) {
+ $target_queue = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $queues_table WHERE id = %d",
+ $target
+ ));
+ }
+
+ if (!$target_queue) {
+ return new WP_Error('invalid_target', 'Transfer target not found', array('status' => 404));
+ }
+
+ // Transfer to queue
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Transferring your call.');
+ $twiml->enqueue($target_queue->queue_name, array(
+ 'waitUrl' => $target_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
+ ));
+
+ $twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Call transferred successfully'
+ ), 200);
+
+ } catch (Exception $e) {
+ return new WP_Error('transfer_error', $e->getMessage(), array('status' => 500));
+ }
+ }
+
+ /**
+ * Register FCM token for push notifications
+ */
+ public function register_fcm_token($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $fcm_token = $request->get_param('fcm_token');
+ $refresh_token = $request->get_param('refresh_token');
+
+ if (empty($fcm_token)) {
+ return new WP_Error('missing_token', 'FCM token is required', array('status' => 400));
+ }
+
+ $this->auth->update_fcm_token($user_id, $refresh_token, $fcm_token);
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'FCM token registered successfully'
+ ), 200);
+ }
+
+ /**
+ * Get agent phone number
+ */
+ public function get_agent_phone($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'phone_number' => $agent_number ?: null
+ ), 200);
+ }
+
+ /**
+ * Update agent phone number
+ */
+ public function update_agent_phone($request) {
+ $user_id = $this->auth->get_current_user_id();
+ $phone_number = $request->get_param('phone_number');
+
+ if (empty($phone_number)) {
+ return new WP_Error('missing_phone', 'Phone number is required', array('status' => 400));
+ }
+
+ // Validate E.164 format
+ if (!preg_match('/^\+[1-9]\d{1,14}$/', $phone_number)) {
+ return new WP_Error('invalid_phone', 'Phone number must be in E.164 format (+1XXXXXXXXXX)', array('status' => 400));
+ }
+
+ update_user_meta($user_id, 'twp_agent_phone', $phone_number);
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Phone number updated successfully'
+ ), 200);
+ }
+
+ /**
+ * Check if user has access to a queue
+ */
+ private function user_has_queue_access($user_id, $queue_id) {
+ global $wpdb;
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+ $assignments_table = $wpdb->prefix . 'twp_queue_assignments';
+
+ // Check if it's user's personal queue
+ $is_personal = $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $queues_table WHERE id = %d AND user_id = %d",
+ $queue_id, $user_id
+ ));
+
+ if ($is_personal) {
+ return true;
+ }
+
+ // Check if user is assigned to this queue
+ $is_assigned = $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM $assignments_table WHERE queue_id = %d AND user_id = %d",
+ $queue_id, $user_id
+ ));
+
+ return (bool)$is_assigned;
+ }
+
+ /**
+ * Calculate wait time in seconds
+ */
+ private function calculate_wait_time($start_time) {
+ if (!$start_time) {
+ return 0;
+ }
+
+ $start = strtotime($start_time);
+ $now = current_time('timestamp');
+
+ return max(0, $now - $start);
+ }
+}
diff --git a/includes/class-twp-mobile-auth.php b/includes/class-twp-mobile-auth.php
new file mode 100644
index 0000000..7a235c8
--- /dev/null
+++ b/includes/class-twp-mobile-auth.php
@@ -0,0 +1,457 @@
+secret_key = $this->get_secret_key();
+ }
+
+ /**
+ * Get or generate JWT secret key
+ */
+ private function get_secret_key() {
+ $key = get_option('twp_mobile_jwt_secret');
+
+ if (empty($key)) {
+ // Generate a secure random key
+ $key = bin2hex(random_bytes(32));
+ update_option('twp_mobile_jwt_secret', $key);
+ }
+
+ return $key;
+ }
+
+ /**
+ * Register REST API endpoints
+ */
+ public function register_endpoints() {
+ add_action('rest_api_init', function() {
+ // Login endpoint
+ register_rest_route('twilio-mobile/v1', '/auth/login', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_login'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Refresh token endpoint
+ register_rest_route('twilio-mobile/v1', '/auth/refresh', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_refresh'),
+ 'permission_callback' => '__return_true'
+ ));
+
+ // Logout endpoint
+ register_rest_route('twilio-mobile/v1', '/auth/logout', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'handle_logout'),
+ 'permission_callback' => array($this, 'verify_token')
+ ));
+
+ // Validate token endpoint (for debugging)
+ register_rest_route('twilio-mobile/v1', '/auth/validate', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'handle_validate'),
+ 'permission_callback' => array($this, 'verify_token')
+ ));
+ });
+ }
+
+ /**
+ * Handle login request
+ */
+ public function handle_login($request) {
+ $username = $request->get_param('username');
+ $password = $request->get_param('password');
+ $fcm_token = $request->get_param('fcm_token'); // Optional
+ $device_info = $request->get_param('device_info'); // Optional
+
+ if (empty($username) || empty($password)) {
+ return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400));
+ }
+
+ // Authenticate user
+ $user = wp_authenticate($username, $password);
+
+ if (is_wp_error($user)) {
+ return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401));
+ }
+
+ // Check if user has phone agent capabilities
+ if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
+ return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403));
+ }
+
+ // Generate tokens
+ $access_token = $this->generate_token($user->ID, 'access');
+ $refresh_token = $this->generate_token($user->ID, 'refresh');
+
+ // Store session in database
+ $this->store_session($user->ID, $refresh_token, $fcm_token, $device_info);
+
+ // Get user data
+ $user_data = $this->get_user_data($user->ID);
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'access_token' => $access_token,
+ 'refresh_token' => $refresh_token,
+ 'expires_in' => $this->token_expiry,
+ 'user' => $user_data
+ ), 200);
+ }
+
+ /**
+ * Handle token refresh request
+ */
+ public function handle_refresh($request) {
+ $refresh_token = $request->get_param('refresh_token');
+
+ if (empty($refresh_token)) {
+ return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400));
+ }
+
+ // Verify refresh token
+ $payload = $this->decode_token($refresh_token);
+
+ if (!$payload || $payload->type !== 'refresh') {
+ return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401));
+ }
+
+ // Check if session exists and is valid
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ $session = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()",
+ $payload->user_id,
+ $refresh_token
+ ));
+
+ if (!$session) {
+ return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401));
+ }
+
+ // Generate new access token
+ $access_token = $this->generate_token($payload->user_id, 'access');
+
+ // Update last_used timestamp
+ $wpdb->update(
+ $table,
+ array('last_used' => current_time('mysql')),
+ array('id' => $session->id),
+ array('%s'),
+ array('%d')
+ );
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'access_token' => $access_token,
+ 'expires_in' => $this->token_expiry
+ ), 200);
+ }
+
+ /**
+ * Handle logout request
+ */
+ public function handle_logout($request) {
+ $user_id = $this->get_current_user_id();
+
+ if (!$user_id) {
+ return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
+ }
+
+ // Get refresh token from request
+ $refresh_token = $request->get_param('refresh_token');
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ if ($refresh_token) {
+ // Invalidate specific session
+ $wpdb->update(
+ $table,
+ array('is_active' => 0),
+ array('user_id' => $user_id, 'refresh_token' => $refresh_token),
+ array('%d'),
+ array('%d', '%s')
+ );
+ } else {
+ // Invalidate all sessions for this user
+ $wpdb->update(
+ $table,
+ array('is_active' => 0),
+ array('user_id' => $user_id),
+ array('%d'),
+ array('%d')
+ );
+ }
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'message' => 'Logged out successfully'
+ ), 200);
+ }
+
+ /**
+ * Handle token validation request
+ */
+ public function handle_validate($request) {
+ $user_id = $this->get_current_user_id();
+
+ if (!$user_id) {
+ return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
+ }
+
+ $user_data = $this->get_user_data($user_id);
+
+ return new WP_REST_Response(array(
+ 'success' => true,
+ 'valid' => true,
+ 'user' => $user_data
+ ), 200);
+ }
+
+ /**
+ * Generate JWT token
+ */
+ private function generate_token($user_id, $type = 'access') {
+ $issued_at = time();
+ $expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry;
+
+ $payload = array(
+ 'iat' => $issued_at,
+ 'exp' => $issued_at + $expiry,
+ 'user_id' => $user_id,
+ 'type' => $type
+ );
+
+ return $this->encode_token($payload);
+ }
+
+ /**
+ * Simple JWT encoding (header.payload.signature)
+ */
+ private function encode_token($payload) {
+ $header = array('typ' => 'JWT', 'alg' => 'HS256');
+
+ $segments = array();
+ $segments[] = $this->base64url_encode(json_encode($header));
+ $segments[] = $this->base64url_encode(json_encode($payload));
+
+ $signing_input = implode('.', $segments);
+ $signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
+ $segments[] = $this->base64url_encode($signature);
+
+ return implode('.', $segments);
+ }
+
+ /**
+ * Simple JWT decoding
+ */
+ private function decode_token($token) {
+ $segments = explode('.', $token);
+
+ if (count($segments) !== 3) {
+ return false;
+ }
+
+ list($header_b64, $payload_b64, $signature_b64) = $segments;
+
+ // Verify signature
+ $signing_input = $header_b64 . '.' . $payload_b64;
+ $signature = $this->base64url_decode($signature_b64);
+ $expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
+
+ if (!hash_equals($signature, $expected_signature)) {
+ return false;
+ }
+
+ // Decode payload
+ $payload = json_decode($this->base64url_decode($payload_b64));
+
+ if (!$payload) {
+ return false;
+ }
+
+ // Check expiration
+ if (isset($payload->exp) && $payload->exp < time()) {
+ return false;
+ }
+
+ return $payload;
+ }
+
+ /**
+ * Base64 URL encode
+ */
+ private function base64url_encode($data) {
+ return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+ }
+
+ /**
+ * Base64 URL decode
+ */
+ private function base64url_decode($data) {
+ return base64_decode(strtr($data, '-_', '+/'));
+ }
+
+ /**
+ * Verify token (permission callback)
+ */
+ public function verify_token($request) {
+ $auth_header = $request->get_header('Authorization');
+
+ if (empty($auth_header)) {
+ return false;
+ }
+
+ // Extract token from "Bearer "
+ if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
+ $token = $matches[1];
+ } else {
+ return false;
+ }
+
+ $payload = $this->decode_token($token);
+
+ if (!$payload || $payload->type !== 'access') {
+ return false;
+ }
+
+ // Store user ID for later use
+ $request->set_param('_twp_user_id', $payload->user_id);
+
+ return true;
+ }
+
+ /**
+ * Get current user ID from token
+ */
+ public function get_current_user_id() {
+ $request = rest_get_server()->get_request();
+ return $request->get_param('_twp_user_id');
+ }
+
+ /**
+ * Store session in database
+ */
+ private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ $wpdb->insert(
+ $table,
+ array(
+ 'user_id' => $user_id,
+ 'refresh_token' => $refresh_token,
+ 'fcm_token' => $fcm_token,
+ 'device_info' => $device_info,
+ 'created_at' => current_time('mysql'),
+ 'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry),
+ 'last_used' => current_time('mysql'),
+ 'is_active' => 1
+ ),
+ array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
+ );
+ }
+
+ /**
+ * Get user data for response
+ */
+ private function get_user_data($user_id) {
+ $user = get_userdata($user_id);
+
+ if (!$user) {
+ return null;
+ }
+
+ // Get agent phone number
+ $agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
+
+ // Get agent status
+ global $wpdb;
+ $status_table = $wpdb->prefix . 'twp_agent_status';
+ $status = $wpdb->get_row($wpdb->prepare(
+ "SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ // Get user extension
+ $ext_table = $wpdb->prefix . 'twp_user_extensions';
+ $extension = $wpdb->get_row($wpdb->prepare(
+ "SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ return array(
+ 'id' => $user->ID,
+ 'username' => $user->user_login,
+ 'display_name' => $user->display_name,
+ 'email' => $user->user_email,
+ 'phone_number' => $agent_number,
+ 'extension' => $extension ? $extension->extension : null,
+ 'direct_dial' => $extension ? $extension->direct_dial_number : null,
+ 'status' => $status ? $status->status : 'offline',
+ 'is_logged_in' => $status ? (bool)$status->is_logged_in : false,
+ 'current_call_sid' => $status ? $status->current_call_sid : null,
+ 'capabilities' => array(
+ 'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'),
+ 'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'),
+ 'can_access_call_log' => user_can($user_id, 'twp_access_call_log'),
+ 'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'),
+ 'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'),
+ 'is_admin' => user_can($user_id, 'manage_options')
+ )
+ );
+ }
+
+ /**
+ * Update FCM token for existing session
+ */
+ public function update_fcm_token($user_id, $refresh_token, $fcm_token) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ $wpdb->update(
+ $table,
+ array('fcm_token' => $fcm_token),
+ array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1),
+ array('%s'),
+ array('%d', '%s', '%d')
+ );
+ }
+
+ /**
+ * Get all active FCM tokens for a user
+ */
+ public function get_user_fcm_tokens($user_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ return $wpdb->get_col($wpdb->prepare(
+ "SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()",
+ $user_id
+ ));
+ }
+
+ /**
+ * Clean up expired sessions
+ */
+ public static function cleanup_expired_sessions() {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_mobile_sessions';
+
+ $wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1");
+ }
+}
diff --git a/includes/class-twp-mobile-sse.php b/includes/class-twp-mobile-sse.php
new file mode 100644
index 0000000..33c5ddf
--- /dev/null
+++ b/includes/class-twp-mobile-sse.php
@@ -0,0 +1,308 @@
+auth = new TWP_Mobile_Auth();
+ }
+
+ /**
+ * Register SSE endpoint
+ */
+ public function register_endpoints() {
+ add_action('rest_api_init', function() {
+ register_rest_route('twilio-mobile/v1', '/stream/events', array(
+ 'methods' => 'GET',
+ 'callback' => array($this, 'stream_events'),
+ 'permission_callback' => array($this->auth, 'verify_token')
+ ));
+ });
+ }
+
+ /**
+ * Stream events to mobile app
+ */
+ public function stream_events($request) {
+ $user_id = $this->auth->get_current_user_id();
+
+ if (!$user_id) {
+ return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
+ }
+
+ // Set headers for SSE
+ header('Content-Type: text/event-stream');
+ header('Cache-Control: no-cache');
+ header('Connection: keep-alive');
+ header('X-Accel-Buffering: no'); // Disable nginx buffering
+
+ // Disable PHP output buffering
+ if (function_exists('apache_setenv')) {
+ @apache_setenv('no-gzip', '1');
+ }
+ @ini_set('zlib.output_compression', 0);
+ @ini_set('implicit_flush', 1);
+ ob_implicit_flush(1);
+ while (ob_get_level() > 0) {
+ ob_end_flush();
+ }
+
+ // Send initial connection event
+ $this->send_event('connected', array('user_id' => $user_id, 'timestamp' => time()));
+
+ // Get initial state
+ $last_check = time();
+ $previous_state = $this->get_current_state($user_id);
+
+ // Stream loop - check for changes every 2 seconds
+ $max_duration = 300; // 5 minutes max connection time
+ $start_time = time();
+
+ while (time() - $start_time < $max_duration) {
+ // Check if connection is still alive
+ if (connection_aborted()) {
+ break;
+ }
+
+ // Get current state
+ $current_state = $this->get_current_state($user_id);
+
+ // Compare and send updates
+ $this->check_and_send_updates($previous_state, $current_state);
+
+ // Update previous state
+ $previous_state = $current_state;
+
+ // Send heartbeat every 15 seconds
+ if (time() - $last_check >= 15) {
+ $this->send_event('heartbeat', array('timestamp' => time()));
+ $last_check = time();
+ }
+
+ // Sleep for 2 seconds
+ sleep(2);
+ }
+
+ // Connection closing
+ $this->send_event('disconnect', array('reason' => 'timeout', 'timestamp' => time()));
+ exit;
+ }
+
+ /**
+ * Get current state for agent
+ */
+ private function get_current_state($user_id) {
+ global $wpdb;
+
+ $state = array(
+ 'agent_status' => $this->get_agent_status($user_id),
+ 'queues' => $this->get_queues_state($user_id),
+ 'current_call' => $this->get_current_call($user_id)
+ );
+
+ return $state;
+ }
+
+ /**
+ * Get agent status
+ */
+ private function get_agent_status($user_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'twp_agent_status';
+
+ $status = $wpdb->get_row($wpdb->prepare(
+ "SELECT status, is_logged_in, current_call_sid FROM $table WHERE user_id = %d",
+ $user_id
+ ));
+
+ if (!$status) {
+ return array('status' => 'offline', 'is_logged_in' => false, 'current_call_sid' => null);
+ }
+
+ return array(
+ 'status' => $status->status,
+ 'is_logged_in' => (bool)$status->is_logged_in,
+ 'current_call_sid' => $status->current_call_sid
+ );
+ }
+
+ /**
+ * Get queues state
+ */
+ private function get_queues_state($user_id) {
+ global $wpdb;
+ $queues_table = $wpdb->prefix . 'twp_call_queues';
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+ $assignments_table = $wpdb->prefix . 'twp_queue_assignments';
+
+ // Get queue IDs
+ $queue_ids = $wpdb->get_col($wpdb->prepare(
+ "SELECT queue_id FROM $assignments_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ $personal_queue_ids = $wpdb->get_col($wpdb->prepare(
+ "SELECT id FROM $queues_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ $all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
+
+ if (empty($all_queue_ids)) {
+ return array();
+ }
+
+ $queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
+
+ $queues = $wpdb->get_results("
+ SELECT
+ q.id,
+ q.queue_name,
+ COUNT(c.id) as waiting_count,
+ MIN(c.enqueued_at) as oldest_call_time
+ FROM $queues_table q
+ LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
+ WHERE q.id IN ($queue_ids_str)
+ GROUP BY q.id
+ ");
+
+ $result = array();
+ foreach ($queues as $queue) {
+ $result[$queue->id] = array(
+ 'id' => (int)$queue->id,
+ 'name' => $queue->queue_name,
+ 'waiting_count' => (int)$queue->waiting_count,
+ 'oldest_call_time' => $queue->oldest_call_time
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get current call for agent
+ */
+ private function get_current_call($user_id) {
+ global $wpdb;
+ $calls_table = $wpdb->prefix . 'twp_queued_calls';
+
+ $agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
+
+ if (!$agent_number) {
+ return null;
+ }
+
+ $call = $wpdb->get_row($wpdb->prepare(
+ "SELECT call_sid, from_number, queue_id, status, joined_at
+ FROM $calls_table
+ WHERE agent_phone = %s AND status IN ('connecting', 'in_progress')
+ ORDER BY joined_at DESC
+ LIMIT 1",
+ $agent_number
+ ));
+
+ if (!$call) {
+ return null;
+ }
+
+ return array(
+ 'call_sid' => $call->call_sid,
+ 'from_number' => $call->from_number,
+ 'queue_id' => (int)$call->queue_id,
+ 'status' => $call->status,
+ 'duration' => time() - strtotime($call->joined_at)
+ );
+ }
+
+ /**
+ * Check state changes and send updates
+ */
+ private function check_and_send_updates($previous, $current) {
+ // Check agent status changes
+ if ($previous['agent_status'] !== $current['agent_status']) {
+ $this->send_event('agent_status_changed', $current['agent_status']);
+ }
+
+ // Check queue changes
+ $this->check_queue_changes($previous['queues'], $current['queues']);
+
+ // Check current call changes
+ if ($previous['current_call'] !== $current['current_call']) {
+ if ($current['current_call'] && !$previous['current_call']) {
+ // New call started
+ $this->send_event('call_started', $current['current_call']);
+ } elseif (!$current['current_call'] && $previous['current_call']) {
+ // Call ended
+ $this->send_event('call_ended', $previous['current_call']);
+ } elseif ($current['current_call'] && $previous['current_call']) {
+ // Call status changed
+ if ($current['current_call']['status'] !== $previous['current_call']['status']) {
+ $this->send_event('call_status_changed', $current['current_call']);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check for queue changes
+ */
+ private function check_queue_changes($previous_queues, $current_queues) {
+ foreach ($current_queues as $queue_id => $current_queue) {
+ $previous_queue = $previous_queues[$queue_id] ?? null;
+
+ if (!$previous_queue) {
+ // New queue added
+ $this->send_event('queue_added', $current_queue);
+ continue;
+ }
+
+ // Check for waiting count changes
+ if ($current_queue['waiting_count'] !== $previous_queue['waiting_count']) {
+ if ($current_queue['waiting_count'] > $previous_queue['waiting_count']) {
+ // New call in queue
+ $this->send_event('call_enqueued', array(
+ 'queue_id' => $queue_id,
+ 'queue_name' => $current_queue['name'],
+ 'waiting_count' => $current_queue['waiting_count']
+ ));
+ } else {
+ // Call removed from queue
+ $this->send_event('call_dequeued', array(
+ 'queue_id' => $queue_id,
+ 'queue_name' => $current_queue['name'],
+ 'waiting_count' => $current_queue['waiting_count']
+ ));
+ }
+ }
+ }
+
+ // Check for removed queues
+ foreach ($previous_queues as $queue_id => $previous_queue) {
+ if (!isset($current_queues[$queue_id])) {
+ $this->send_event('queue_removed', array('queue_id' => $queue_id));
+ }
+ }
+ }
+
+ /**
+ * Send SSE event
+ */
+ private function send_event($event_type, $data) {
+ echo "event: $event_type\n";
+ echo "data: " . json_encode($data) . "\n\n";
+
+ if (ob_get_level() > 0) {
+ ob_flush();
+ }
+ flush();
+ }
+}