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

@@ -19,7 +19,7 @@ class TWP_FCM {
/**
* Send push notification to user's devices
*/
public function send_notification($user_id, $title, $body, $data = array()) {
public function send_notification($user_id, $title, $body, $data = array(), $data_only = false) {
if (empty($this->server_key)) {
error_log('TWP FCM: Server key not configured');
return false;
@@ -37,7 +37,7 @@ class TWP_FCM {
$failed_tokens = array();
foreach ($tokens as $token) {
$result = $this->send_to_token($token, $title, $body, $data);
$result = $this->send_to_token($token, $title, $body, $data, $data_only);
if ($result['success']) {
$success_count++;
@@ -59,25 +59,37 @@ class TWP_FCM {
/**
* 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(
private function send_to_token($token, $title, $body, $data = array(), $data_only = false) {
if ($data_only) {
$payload = array(
'to' => $token,
'data' => array_merge($data, array(
'title' => $title,
'body' => $body,
'timestamp' => time()
)),
'priority' => 'high'
);
} else {
$notification = array(
'title' => $title,
'body' => $body,
'timestamp' => time()
)),
'priority' => 'high'
);
'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,
@@ -162,7 +174,7 @@ class TWP_FCM {
'queue_name' => $queue_name
);
return $this->send_notification($user_id, $title, $body, $data);
return $this->send_notification($user_id, $title, $body, $data, true);
}
/**