2025-12-01 15:43:14 -08:00
< ? php
/**
* Mobile App REST API Endpoints
*
* Provides REST API endpoints for mobile app functionality
*/
class TWP_Mobile_API {
private $auth ;
/**
* Constructor
*/
public function __construct () {
// Initialize auth handler
require_once plugin_dir_path ( __FILE__ ) . 'class-twp-mobile-auth.php' ;
$this -> 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<id>\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<call_sid>[^/]+)/accept' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'accept_call' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
register_rest_route ( 'twilio-mobile/v1' , '/calls/(?P<call_sid>[^/]+)/reject' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'reject_call' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
register_rest_route ( 'twilio-mobile/v1' , '/calls/(?P<call_sid>[^/]+)/hold' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'hold_call' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
register_rest_route ( 'twilio-mobile/v1' , '/calls/(?P<call_sid>[^/]+)/unhold' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'unhold_call' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
register_rest_route ( 'twilio-mobile/v1' , '/calls/(?P<call_sid>[^/]+)/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' )
));
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
// Voice token for VoIP
register_rest_route ( 'twilio-mobile/v1' , '/voice/token' , array (
'methods' => 'GET' ,
'callback' => array ( $this , 'get_voice_token' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
2026-03-06 18:05:54 -08:00
// Phone numbers for caller ID
register_rest_route ( 'twilio-mobile/v1' , '/phone-numbers' , array (
'methods' => 'GET' ,
'callback' => array ( $this , 'get_phone_numbers' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
2026-03-07 17:11:02 -08:00
// Outbound call (click-to-call via server)
register_rest_route ( 'twilio-mobile/v1' , '/calls/outbound' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'initiate_outbound_call' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
// FCM push credential setup (admin only)
register_rest_route ( 'twilio-mobile/v1' , '/admin/push-credential' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'setup_push_credential' ),
'permission_callback' => array ( $this -> auth , 'verify_token' )
));
2025-12-01 15:43:14 -08:00
});
}
/**
* 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 ));
}
2026-03-06 18:05:54 -08:00
require_once plugin_dir_path ( __FILE__ ) . 'class-twp-agent-manager.php' ;
require_once plugin_dir_path ( __FILE__ ) . 'class-twp-user-queue-manager.php' ;
2025-12-01 15:43:14 -08:00
2026-03-06 18:05:54 -08:00
// Handle login status change first (matches browser phone behavior)
2025-12-01 15:43:14 -08:00
if ( $is_logged_in !== null ) {
2026-03-06 18:05:54 -08:00
TWP_Agent_Manager :: set_agent_login_status ( $user_id , ( bool ) $is_logged_in );
2025-12-01 15:43:14 -08:00
}
2026-03-06 18:05:54 -08:00
// Set agent status (handles auto_busy_at and all status fields)
TWP_Agent_Manager :: set_agent_status ( $user_id , $new_status );
2025-12-01 15:43:14 -08:00
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' ;
2026-03-06 15:32:22 -08:00
$groups_table = $wpdb -> prefix . 'twp_group_members' ;
2025-12-01 15:43:14 -08:00
2026-03-06 15:32:22 -08:00
// Auto-create personal queues if they don't exist
$extensions_table = $wpdb -> prefix . 'twp_user_extensions' ;
$existing_extension = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT extension FROM $extensions_table WHERE user_id = %d " ,
2025-12-01 15:43:14 -08:00
$user_id
));
2026-03-06 15:32:22 -08:00
if ( ! $existing_extension ) {
2026-03-06 18:05:54 -08:00
require_once plugin_dir_path ( __FILE__ ) . 'class-twp-user-queue-manager.php' ;
2026-03-06 15:32:22 -08:00
TWP_User_Queue_Manager :: create_user_queues ( $user_id );
2025-12-01 15:43:14 -08:00
}
2026-03-06 15:32:22 -08:00
// Get queues where user is a member of the assigned agent group OR personal/hold queues
$queues = $wpdb -> get_results ( $wpdb -> prepare ( "
SELECT DISTINCT
2025-12-01 15:43:14 -08:00
q . id ,
q . queue_name ,
q . queue_type ,
q . extension ,
COUNT ( c . id ) as waiting_count
FROM $queues_table q
2026-03-06 15:32:22 -08:00
LEFT JOIN $groups_table gm ON gm . group_id = q . agent_group_id
2025-12-01 15:43:14 -08:00
LEFT JOIN $calls_table c ON q . id = c . queue_id AND c . status = 'waiting'
2026-03-06 15:32:22 -08:00
WHERE ( gm . user_id = % d AND gm . is_active = 1 )
OR ( q . user_id = % d AND q . queue_type IN ( 'personal' , 'hold' ))
2025-12-01 15:43:14 -08:00
GROUP BY q . id
2026-03-06 15:32:22 -08:00
ORDER BY
CASE
WHEN q . queue_type = 'personal' THEN 1
WHEN q . queue_type = 'hold' THEN 2
ELSE 3
END ,
q . queue_name ASC
" , $user_id , $user_id ));
2025-12-01 15:43:14 -08:00
$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' ];
2026-03-07 17:11:02 -08:00
// Check for WebRTC client_identity parameter
$body = $request -> get_json_params ();
$client_identity = isset ( $body [ 'client_identity' ]) ? sanitize_text_field ( $body [ 'client_identity' ]) : null ;
2025-12-01 15:43:14 -08:00
// 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 {
2026-03-07 17:11:02 -08:00
if ( ! empty ( $client_identity )) {
// WebRTC path: redirect the queued call to the Twilio Client device
// Use the original caller's number as caller ID so it shows on the agent's device
$caller_id = $call -> from_number ;
if ( empty ( $caller_id )) {
$caller_id = $call -> to_number ;
}
if ( empty ( $caller_id )) {
$caller_id = get_option ( 'twp_caller_id_number' , '' );
}
$twiml = '<Response><Dial callerId="' . htmlspecialchars ( $caller_id ) . '"><Client>' . htmlspecialchars ( $client_identity ) . '</Client></Dial></Response>' ;
error_log ( 'TWP accept_call: call_sid=' . $call_sid . ' client=' . $client_identity . ' twiml=' . $twiml );
$result = $twilio -> update_call ( $call_sid , array ( 'twiml' => $twiml ));
error_log ( 'TWP accept_call result: ' . json_encode ( $result ));
if ( ! $result [ 'success' ]) {
return new WP_Error ( 'twilio_error' , $result [ 'error' ] ? ? 'Failed to update call' , array ( 'status' => 500 ));
}
// Update call record
$wpdb -> update (
$calls_table ,
array (
'status' => 'connecting' ,
'agent_phone' => 'client:' . $client_identity ,
),
array ( 'call_sid' => $call_sid ),
array ( '%s' , '%s' ),
array ( '%s' )
);
2025-12-01 15:43:14 -08:00
2026-03-07 17:11:02 -08:00
// Save current status before setting busy, so we can revert after call ends
$status_table = $wpdb -> prefix . 'twp_agent_status' ;
$current = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT status FROM $status_table WHERE user_id = %d " , $user_id
));
$pre_call_status = ( $current && $current -> status !== 'busy' ) ? $current -> status : null ;
$wpdb -> update (
$status_table ,
array (
'status' => 'busy' ,
'current_call_sid' => $call_sid ,
'pre_call_status' => $pre_call_status ,
'auto_busy_at' => null ,
),
array ( 'user_id' => $user_id ),
array ( '%s' , '%s' , '%s' , '%s' ),
array ( '%d' )
);
2025-12-01 15:43:14 -08:00
2026-03-07 17:11:02 -08:00
// Cancel queue alert notifications on all agents' devices
require_once plugin_dir_path ( dirname ( __FILE__ )) . 'includes/class-twp-fcm.php' ;
$fcm = new TWP_FCM ();
$fcm -> cancel_queue_alert_for_queue ( $call -> queue_id , $call_sid );
return new WP_REST_Response ( array (
'success' => true ,
'message' => 'Call accepted via WebRTC client' ,
'call_sid' => $call_sid
), 200 );
} else {
// Phone-based path (original flow): dial the agent's 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 and no client_identity provided' , array ( 'status' => 400 ));
}
// 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
)
);
2025-12-01 15:43:14 -08:00
2026-03-07 17:11:02 -08:00
// 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 );
}
2025-12-01 15:43:14 -08:00
} 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 ) {
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
$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 ));
}
// Build identity for this agent
$user = get_userdata ( $user_id );
$clean_name = preg_replace ( '/[^a-zA-Z0-9]/' , '' , $user -> user_login );
if ( empty ( $clean_name )) {
$clean_name = 'user' ;
}
$identity = 'agent' . $user_id . $clean_name ;
// Redirect customer back to agent's client
$twiml = new \Twilio\TwiML\VoiceResponse ();
$dial = $twiml -> dial ();
$dial -> client ( $identity );
$twilio -> update_call ( $customer_call_sid , array ( 'twiml' => $twiml -> asXML ()));
return new WP_REST_Response ( array (
'success' => true ,
'message' => 'Call resumed from hold'
), 200 );
} catch ( Exception $e ) {
return new WP_Error ( 'unhold_error' , $e -> getMessage (), array ( 'status' => 500 ));
}
2025-12-01 15:43:14 -08:00
}
/**
* 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 );
}
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
/**
* Get Voice access token for VoIP
*/
public function get_voice_token ( $request ) {
$user_id = $this -> auth -> get_current_user_id ();
$user = get_userdata ( $user_id );
2026-03-06 14:50:53 -08:00
$clean_name = preg_replace ( '/[^a-zA-Z0-9]/' , '' , $user -> user_login );
if ( empty ( $clean_name )) {
$clean_name = 'user' ;
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
}
2026-03-06 14:50:53 -08:00
$identity = 'agent' . $user_id . $clean_name ;
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
2026-03-06 18:05:54 -08:00
try {
// Ensure Twilio SDK autoloader is loaded
require_once plugin_dir_path ( dirname ( __FILE__ )) . 'includes/class-twp-twilio-api.php' ;
new TWP_Twilio_API ();
$account_sid = get_option ( 'twp_twilio_account_sid' );
$auth_token = get_option ( 'twp_twilio_auth_token' );
$twiml_app_sid = get_option ( 'twp_twiml_app_sid' );
if ( empty ( $account_sid ) || empty ( $auth_token ) || empty ( $twiml_app_sid )) {
return new WP_Error ( 'token_error' , 'Twilio credentials not configured' , array ( 'status' => 500 ));
}
2026-03-07 17:11:02 -08:00
// AccessToken requires an API Key (not account credentials).
// Auto-create and cache one if it doesn't exist yet.
$api_key_sid = get_option ( 'twp_twilio_api_key_sid' );
$api_key_secret = get_option ( 'twp_twilio_api_key_secret' );
if ( empty ( $api_key_sid ) || empty ( $api_key_secret )) {
$client = new \Twilio\Rest\Client ( $account_sid , $auth_token );
$newKey = $client -> newKeys -> create ([ 'friendlyName' => 'TWP Mobile Voice' ]);
$api_key_sid = $newKey -> sid ;
$api_key_secret = $newKey -> secret ;
update_option ( 'twp_twilio_api_key_sid' , $api_key_sid );
update_option ( 'twp_twilio_api_key_secret' , $api_key_secret );
}
$token = new \Twilio\Jwt\AccessToken ( $account_sid , $api_key_sid , $api_key_secret , 3600 , $identity );
2026-03-06 18:05:54 -08:00
$voiceGrant = new \Twilio\Jwt\Grants\VoiceGrant ();
$voiceGrant -> setOutgoingApplicationSid ( $twiml_app_sid );
$voiceGrant -> setIncomingAllow ( true );
2026-03-07 17:11:02 -08:00
// Include FCM push credential for incoming call notifications.
// Auto-create from the stored Firebase service account JSON if not yet created.
$push_credential_sid = get_option ( 'twp_twilio_push_credential_sid' );
if ( empty ( $push_credential_sid )) {
$push_credential_sid = $this -> ensure_push_credential ( $account_sid , $auth_token );
}
if ( ! empty ( $push_credential_sid )) {
$voiceGrant -> setPushCredentialSid ( $push_credential_sid );
}
2026-03-06 18:05:54 -08:00
$token -> addGrant ( $voiceGrant );
return new WP_REST_Response ( array (
'token' => $token -> toJWT (),
'identity' => $identity ,
'expires_in' => 3600
), 200 );
} catch ( Exception $e ) {
return new WP_Error ( 'token_error' , $e -> getMessage (), array ( 'status' => 500 ));
}
}
/**
* Get available Twilio phone numbers for caller ID
*/
public function get_phone_numbers ( $request ) {
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
try {
2026-03-06 14:50:53 -08:00
require_once plugin_dir_path ( dirname ( __FILE__ )) . 'includes/class-twp-twilio-api.php' ;
$twilio = new TWP_Twilio_API ();
2026-03-06 18:05:54 -08:00
$result = $twilio -> get_phone_numbers ();
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
2026-03-06 14:50:53 -08:00
if ( ! $result [ 'success' ]) {
2026-03-06 18:05:54 -08:00
return new WP_Error ( 'twilio_error' , $result [ 'error' ], array ( 'status' => 500 ));
}
$phone_numbers = array ();
foreach ( $result [ 'data' ][ 'incoming_phone_numbers' ] as $number ) {
$phone_numbers [] = array (
'phone_number' => $number [ 'phone_number' ],
'friendly_name' => $number [ 'friendly_name' ],
);
2026-03-06 14:50:53 -08:00
}
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
return new WP_REST_Response ( array (
2026-03-06 18:05:54 -08:00
'success' => true ,
'phone_numbers' => $phone_numbers
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
), 200 );
} catch ( Exception $e ) {
2026-03-06 18:05:54 -08:00
return new WP_Error ( 'twilio_error' , $e -> getMessage (), array ( 'status' => 500 ));
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
}
}
2025-12-01 15:43:14 -08:00
/**
* 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 ;
}
2026-03-07 17:11:02 -08:00
/**
* Admin endpoint to force re - creation of the Twilio Push Credential .
*/
public function setup_push_credential ( $request ) {
$user_id = $this -> auth -> get_current_user_id ();
$user = get_userdata ( $user_id );
if ( ! user_can ( $user , 'manage_options' )) {
return new WP_Error ( 'forbidden' , 'Admin access required' , array ( 'status' => 403 ));
}
try {
require_once plugin_dir_path ( dirname ( __FILE__ )) . 'includes/class-twp-twilio-api.php' ;
new TWP_Twilio_API ();
$account_sid = get_option ( 'twp_twilio_account_sid' );
$auth_token = get_option ( 'twp_twilio_auth_token' );
// Force re-creation by clearing existing SID
delete_option ( 'twp_twilio_push_credential_sid' );
$sid = $this -> ensure_push_credential ( $account_sid , $auth_token );
if ( empty ( $sid )) {
return new WP_Error ( 'credential_error' , 'Failed to create push credential. Check that Firebase service account JSON is configured in Mobile App Settings.' , array ( 'status' => 500 ));
}
return new WP_REST_Response ( array (
'success' => true ,
'credential_sid' => $sid ,
), 200 );
} catch ( Exception $e ) {
error_log ( 'TWP setup_push_credential error: ' . $e -> getMessage ());
return new WP_Error ( 'credential_error' , $e -> getMessage (), array ( 'status' => 500 ));
}
}
/**
* Auto - create Twilio Push Credential from the stored Firebase service account JSON .
* Returns the credential SID or empty string on failure .
*/
private function ensure_push_credential ( $account_sid , $auth_token ) {
$sa_json = get_option ( 'twp_fcm_service_account_json' , '' );
if ( empty ( $sa_json )) {
return '' ;
}
$sa = json_decode ( $sa_json , true );
if ( ! $sa || empty ( $sa [ 'project_id' ]) || empty ( $sa [ 'private_key' ])) {
error_log ( 'TWP: Firebase service account JSON is invalid' );
return '' ;
}
try {
$client = new \Twilio\Rest\Client ( $account_sid , $auth_token );
$credential = $client -> notify -> v1 -> credentials -> create (
'fcm' ,
[
'friendlyName' => 'TWP Mobile FCM' ,
'secret' => $sa_json ,
]
);
update_option ( 'twp_twilio_push_credential_sid' , $credential -> sid );
error_log ( 'TWP: Created Twilio push credential: ' . $credential -> sid );
return $credential -> sid ;
} catch ( Exception $e ) {
error_log ( 'TWP ensure_push_credential error: ' . $e -> getMessage ());
return '' ;
}
}
2025-12-01 15:43:14 -08:00
/**
* 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 );
}
}