Add FCM push notifications, queue alerts, caller ID fixes, and auto-revert agent status
All checks were successful
Create Release / build (push) Successful in 6s
All checks were successful
Create Release / build (push) Successful in 6s
Server-side: - Add push credential auto-creation for FCM incoming call notifications - Add queue alert FCM notifications (data-only for background delivery) - Add queue alert cancellation on call accept/disconnect - Fix caller ID to show caller's number instead of Twilio number - Fix FCM token storage when refresh_token is null - Add pre_call_status tracking to revert agent status 30s after call ends - Add SSE fallback polling for mobile app connectivity Mobile app: - Add Android telecom permissions and phone account registration - Add VoiceFirebaseMessagingService for incoming call push handling - Add insistent queue alert notifications with custom sound - Fix caller number display on active call screen - Add caller ID selection dropdown on dashboard - Add phone numbers endpoint and provider support - Add unit tests for CallInfo, QueueState, and CallProvider - Remove local.properties from tracking, add .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,20 @@ class TWP_Mobile_API {
|
||||
'callback' => array($this, 'get_phone_numbers'),
|
||||
'permission_callback' => array($this->auth, 'verify_token')
|
||||
));
|
||||
|
||||
// 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')
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -297,12 +311,9 @@ class TWP_Mobile_API {
|
||||
$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));
|
||||
}
|
||||
// Check for WebRTC client_identity parameter
|
||||
$body = $request->get_json_params();
|
||||
$client_identity = isset($body['client_identity']) ? sanitize_text_field($body['client_identity']) : null;
|
||||
|
||||
// Initialize Twilio API
|
||||
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
|
||||
@@ -327,46 +338,120 @@ class TWP_Mobile_API {
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
);
|
||||
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', '');
|
||||
}
|
||||
|
||||
// Update call record
|
||||
$wpdb->update(
|
||||
$calls_table,
|
||||
array(
|
||||
'status' => 'connecting',
|
||||
'agent_phone' => $agent_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')
|
||||
);
|
||||
|
||||
// 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')
|
||||
);
|
||||
|
||||
// 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
|
||||
)
|
||||
);
|
||||
|
||||
// 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
|
||||
),
|
||||
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);
|
||||
), 200);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
|
||||
@@ -690,11 +775,35 @@ class TWP_Mobile_API {
|
||||
return new WP_Error('token_error', 'Twilio credentials not configured', array('status' => 500));
|
||||
}
|
||||
|
||||
// AccessToken for mobile Voice SDK (not ClientToken which is browser-only)
|
||||
$token = new \Twilio\Jwt\AccessToken($account_sid, $account_sid, $auth_token, 3600, $identity);
|
||||
// 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);
|
||||
$voiceGrant = new \Twilio\Jwt\Grants\VoiceGrant();
|
||||
$voiceGrant->setOutgoingApplicationSid($twiml_app_sid);
|
||||
$voiceGrant->setIncomingAllow(true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
$token->addGrant($voiceGrant);
|
||||
|
||||
return new WP_REST_Response(array(
|
||||
@@ -766,6 +875,79 @@ class TWP_Mobile_API {
|
||||
return (bool)$is_assigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate wait time in seconds
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user