Add TWP Softphone Flutter app and complete mobile backend API
All checks were successful
Create Release / build (push) Successful in 4s

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>
This commit is contained in:
Claude
2026-03-06 13:01:23 -08:00
parent 03692608cc
commit 5c6932f1d1
49 changed files with 3243 additions and 28 deletions

View File

@@ -99,6 +99,13 @@ class TWP_Mobile_API {
'callback' => array($this, 'update_agent_phone'),
'permission_callback' => array($this->auth, 'verify_token')
));
// 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')
));
});
}
@@ -507,11 +514,46 @@ class TWP_Mobile_API {
* 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);
$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));
}
}
/**
@@ -641,6 +683,55 @@ class TWP_Mobile_API {
), 200);
}
/**
* 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);
$identity = 'agent' . $user_id . preg_replace('/[^a-zA-Z0-9]/', '', $user->user_login);
$account_sid = get_option('twp_twilio_account_sid');
$auth_token = get_option('twp_twilio_auth_token');
$api_key_sid = get_option('twp_twilio_api_key_sid');
$api_key_secret = get_option('twp_twilio_api_key_secret');
$twiml_app_sid = get_option('twp_twiml_app_sid');
$push_credential_sid = get_option('twp_fcm_push_credential_sid');
if (empty($api_key_sid) || empty($api_key_secret)) {
return new WP_Error('missing_api_key', 'Twilio API Key SID and Secret must be configured', array('status' => 500));
}
try {
$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);
if (!empty($push_credential_sid)) {
$voiceGrant->setPushCredentialSid($push_credential_sid);
}
$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));
}
}
/**
* Check if user has access to a queue
*/