Compare commits
3 Commits
2026.03.06
...
2026.03.06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da794ed0c | ||
|
|
5adfa694c1 | ||
|
|
826fd3ae39 |
97
CLAUDE.md
97
CLAUDE.md
@@ -6,8 +6,26 @@
|
|||||||
- **URL**: `https://phone.cloud-hosting.io/`
|
- **URL**: `https://phone.cloud-hosting.io/`
|
||||||
- **Deployment**: rsync to Docker (remote server only, not local)
|
- **Deployment**: rsync to Docker (remote server only, not local)
|
||||||
- **SDK**: Twilio PHP SDK v8.7.0
|
- **SDK**: Twilio PHP SDK v8.7.0
|
||||||
|
- **PHP**: 8.0+ required
|
||||||
|
- **Optional**: AWS SDK (`aws/aws-sdk-php`) for SNS SMS provider
|
||||||
- **External SDK**: `wp-content/twilio-sdk/` (survives plugin updates)
|
- **External SDK**: `wp-content/twilio-sdk/` (survives plugin updates)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- **Install SDK (recommended)**: `./install-twilio-sdk-external.sh` (installs to `wp-content/twilio-sdk/`)
|
||||||
|
- **Install SDK (internal)**: `./install-twilio-sdk.sh` (installs to `vendor/`, lost on plugin update)
|
||||||
|
- **Test SDK**: `php test-sdk.php`
|
||||||
|
- **Composer install SDK**: `composer install-sdk`
|
||||||
|
- **Deploy**: rsync to Docker (remote server, see production path above)
|
||||||
|
- **CI/CD**: Gitea workflows in `.gitea/workflows/` — `release.yml`, `update-version.yml`
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- `twilio-wp-plugin.php` — Main plugin file, constants, SDK loading
|
||||||
|
- `includes/` — All backend classes (28 class files)
|
||||||
|
- `admin/` — Admin UI class (`TWP_Admin`), mobile app settings page
|
||||||
|
- `assets/js/` — Browser phone JS, service worker
|
||||||
|
- `assets/images/`, `assets/sounds/` — Static assets
|
||||||
|
- `.gitea/workflows/` — CI/CD (release, version update)
|
||||||
|
|
||||||
## Phone Variable Names
|
## Phone Variable Names
|
||||||
**Use**: `incoming_number`, `agent_number`, `customer_number`, `workflow_number`, `queue_number`, `default_number`
|
**Use**: `incoming_number`, `agent_number`, `customer_number`, `workflow_number`, `queue_number`, `default_number`
|
||||||
**Don't use**: `from_number`, `to_number`, `phone_number`, `$agent_phone`
|
**Don't use**: `from_number`, `to_number`, `phone_number`, `$agent_phone`
|
||||||
@@ -18,11 +36,20 @@
|
|||||||
- **TWP_Admin**: Has `find_customer_call_leg()` - CRITICAL for call control
|
- **TWP_Admin**: Has `find_customer_call_leg()` - CRITICAL for call control
|
||||||
- **TWP_TTS_Helper**: ElevenLabs/Alice fallback, 30-day cache
|
- **TWP_TTS_Helper**: ElevenLabs/Alice fallback, 30-day cache
|
||||||
- **TWP_User_Queue_Manager**: Auto-creates queues/extensions (100-9999)
|
- **TWP_User_Queue_Manager**: Auto-creates queues/extensions (100-9999)
|
||||||
- **TWP_Webhooks**: 26 endpoints at `twilio-webhook/v1`
|
- **TWP_Webhooks**: 35 endpoints at `twilio-webhook/v1`
|
||||||
- **TWP_Activator**: Creates 15 DB tables, run `ensure_tables_exist()` if missing
|
- **TWP_Activator**: Creates 16 DB tables, run `ensure_tables_exist()` if missing
|
||||||
|
- **TWP_Core**: Main plugin orchestrator, hooks all classes together
|
||||||
|
- **TWP_SMS_Manager**: SMS abstraction with provider interface
|
||||||
|
- **TWP_SMS_Provider_Twilio** / **TWP_SMS_Provider_SNS**: SMS providers (Twilio default, AWS SNS optional)
|
||||||
|
- **TWP_Mobile_API**: REST API for mobile app
|
||||||
|
- **TWP_Mobile_Auth** / **TWP_Mobile_SSE** / **TWP_FCM**: Mobile auth, server-sent events, push notifications
|
||||||
|
- **TWP_Call_Queue**: Queue operations and management
|
||||||
|
- **TWP_Callback_Manager**: Callback request handling
|
||||||
|
- **TWP_Workflow**: Workflow step execution engine
|
||||||
|
- **TWP_Auto_Updater**: Plugin auto-update from Gitea releases
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
15 tables with `twp_` prefix. Key notes:
|
16 tables with `twp_` prefix. Key notes:
|
||||||
- `twp_call_queues`: User queues (general/personal/hold)
|
- `twp_call_queues`: User queues (general/personal/hold)
|
||||||
- `twp_agent_status`: Has `auto_busy_at` for 1-min auto-revert
|
- `twp_agent_status`: Has `auto_busy_at` for 1-min auto-revert
|
||||||
- `twp_queued_calls`: Uses `enqueued_at` not `joined_at`
|
- `twp_queued_calls`: Uses `enqueued_at` not `joined_at`
|
||||||
@@ -41,32 +68,6 @@ $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
|||||||
- Queue: Pass `waitUrl` as option in `enqueue()`
|
- Queue: Pass `waitUrl` as option in `enqueue()`
|
||||||
- TwiML: Use SDK classes, not raw XML
|
- TwiML: Use SDK classes, not raw XML
|
||||||
|
|
||||||
## Recent Changes (v2.3.0)
|
|
||||||
- Browser phone moved to admin-only
|
|
||||||
- Call control uses `find_customer_call_leg()` to prevent disconnections
|
|
||||||
- Auto-creates user queues/extensions when needed
|
|
||||||
- Firefox support added
|
|
||||||
- 1-min agent status auto-revert
|
|
||||||
|
|
||||||
## SDK Installation
|
|
||||||
- **External SDK (Recommended)**: Use `install-twilio-sdk-external.sh` to install SDK to `wp-content/twilio-sdk/`
|
|
||||||
- Survives WordPress plugin updates
|
|
||||||
- SDK location defined by `TWP_EXTERNAL_SDK_DIR` constant
|
|
||||||
- Loading priority: External first, then internal `vendor/` fallback
|
|
||||||
- **Internal SDK (Alternative)**: Use `install-twilio-sdk.sh` to install to `vendor/`
|
|
||||||
- Will be deleted when WordPress updates the plugin
|
|
||||||
- Requires reinstallation after each plugin update
|
|
||||||
- **SDK Loading**: Plugin checks external location first via autoloader, falls back to internal
|
|
||||||
- **Post-Update Detection**: Hook on `upgrader_process_complete` checks SDK status and shows warning
|
|
||||||
|
|
||||||
## Browser Phone Configuration
|
|
||||||
- **Edge Location Setting**: Configurable via Settings → Twilio Edge Location
|
|
||||||
- Default: `roaming` (auto-select closest edge)
|
|
||||||
- Options: ashburn, umatilla, dublin, frankfurt, singapore, sydney, tokyo, sao-paulo
|
|
||||||
- Stored in: `twp_twilio_edge` option
|
|
||||||
- Used by: Browser phone JavaScript for WebRTC connection
|
|
||||||
- Critical: Wrong edge causes immediate call failures (e.g., US calls with Sydney edge)
|
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
- **API**: E.164 format (+1XXXXXXXXXX)
|
- **API**: E.164 format (+1XXXXXXXXXX)
|
||||||
- **Database**: Use `$wpdb`, prepared statements
|
- **Database**: Use `$wpdb`, prepared statements
|
||||||
@@ -79,30 +80,24 @@ $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
|||||||
- User-specific queues with extensions
|
- User-specific queues with extensions
|
||||||
- Browser phone at `admin.php?page=twilio-wp-browser-phone`
|
- Browser phone at `admin.php?page=twilio-wp-browser-phone`
|
||||||
- ElevenLabs TTS with Alice fallback
|
- ElevenLabs TTS with Alice fallback
|
||||||
- 68 AJAX actions, 26 REST endpoints
|
- 77 AJAX actions, 35 REST endpoints
|
||||||
|
- Browser phone moved to admin-only (v2.3.0)
|
||||||
|
- Firefox, Chrome, Safari, Edge support
|
||||||
|
- 1-min agent status auto-revert
|
||||||
|
|
||||||
## Recent Technical Changes (v2.8.9)
|
## SDK Loading
|
||||||
|
- **External SDK (Recommended)**: `wp-content/twilio-sdk/` — survives plugin updates
|
||||||
|
- **Internal SDK**: `vendor/` — deleted on plugin update, needs reinstall
|
||||||
|
- Loading priority: External first (`TWP_EXTERNAL_SDK_DIR`), then internal fallback
|
||||||
|
- Post-update hook (`upgrader_process_complete`) warns if SDK missing
|
||||||
|
|
||||||
### SDK Persistence Between Plugin Updates
|
## Browser Phone Configuration
|
||||||
- **Problem**: WordPress plugin updates delete entire plugin folder including `vendor/` SDK
|
- **Edge Location**: `twp_twilio_edge` option, default `roaming`
|
||||||
- **Solution**: External SDK installation at `wp-content/twilio-sdk/` survives updates
|
- Options: roaming, ashburn, umatilla, dublin, frankfurt, singapore, sydney, tokyo, sao-paulo
|
||||||
- **Implementation**:
|
- Wrong edge causes immediate call failures (e.g., US calls with Sydney edge)
|
||||||
- New constant: `TWP_EXTERNAL_SDK_DIR` points to `wp-content/twilio-sdk/`
|
|
||||||
- Loading priority in `twp_check_sdk_installation()`: External first, internal fallback
|
|
||||||
- Classes updated: `TWP_Twilio_API`, `TWP_Webhooks` constructors check external location first
|
|
||||||
- New script: `install-twilio-sdk-external.sh` automates external installation
|
|
||||||
- Post-update hook: `twp_check_sdk_after_update()` detects missing SDK after updates
|
|
||||||
- Admin notices: `twp_sdk_missing_notice()` shows both installation options
|
|
||||||
- Warning system: `twp_show_sdk_update_warning()` via transient after plugin updates
|
|
||||||
|
|
||||||
### US Calls Failing Fix (Browser Phone)
|
## Changelog
|
||||||
- **Problem**: Browser phone had hardcoded `edge: 'sydney'`, causing US calls to fail with immediate HANGUP
|
See `README.md` for detailed version history. Current version: v2.8.9.
|
||||||
- **Solution**: Configurable edge location via WordPress settings
|
|
||||||
- **Implementation**:
|
|
||||||
- New setting: `twp_twilio_edge` with default value `roaming`
|
|
||||||
- Settings UI: Dropdown in admin settings with 8 edge options
|
|
||||||
- Browser phone JS: Uses `get_option('twp_twilio_edge', 'roaming')` instead of hardcoded value
|
|
||||||
- Edge options: roaming, ashburn, umatilla, dublin, frankfurt, singapore, sydney, tokyo, sao-paulo
|
|
||||||
|
|
||||||
---
|
---
|
||||||
*Updated: Jan 2026*
|
*Updated: Mar 2026*
|
||||||
@@ -36,25 +36,37 @@ if (isset($_POST['twp_test_notification']) && check_admin_referer('twp_mobile_se
|
|||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
if (isset($_POST['twp_save_mobile_settings']) && check_admin_referer('twp_mobile_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_fcm_project_id', sanitize_text_field($_POST['twp_fcm_project_id']));
|
||||||
|
// Service account JSON — validate it parses as JSON before saving
|
||||||
|
$sa_json_raw = isset($_POST['twp_fcm_service_account_json']) ? wp_unslash($_POST['twp_fcm_service_account_json']) : '';
|
||||||
|
if (!empty($sa_json_raw)) {
|
||||||
|
$sa_parsed = json_decode($sa_json_raw, true);
|
||||||
|
if ($sa_parsed && isset($sa_parsed['client_email'], $sa_parsed['private_key'])) {
|
||||||
|
update_option('twp_fcm_service_account_json', $sa_json_raw);
|
||||||
|
} else {
|
||||||
|
$sa_json_error = 'Invalid service account JSON — must contain client_email and private_key fields.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
update_option('twp_fcm_service_account_json', '');
|
||||||
|
}
|
||||||
update_option('twp_auto_update_enabled', isset($_POST['twp_auto_update_enabled']) ? '1' : '0');
|
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_repo', sanitize_text_field($_POST['twp_gitea_repo']));
|
||||||
update_option('twp_gitea_token', sanitize_text_field($_POST['twp_gitea_token']));
|
update_option('twp_gitea_token', sanitize_text_field($_POST['twp_gitea_token']));
|
||||||
update_option('twp_twilio_api_key_sid', sanitize_text_field($_POST['twp_twilio_api_key_sid']));
|
update_option('twp_twilio_api_key_sid', sanitize_text_field($_POST['twp_twilio_api_key_sid']));
|
||||||
update_option('twp_twilio_api_key_secret', sanitize_text_field($_POST['twp_twilio_api_key_secret']));
|
update_option('twp_twilio_api_key_secret', sanitize_text_field($_POST['twp_twilio_api_key_secret']));
|
||||||
update_option('twp_fcm_push_credential_sid', sanitize_text_field($_POST['twp_fcm_push_credential_sid']));
|
|
||||||
|
|
||||||
$settings_saved = true;
|
$settings_saved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current settings
|
// Get current settings
|
||||||
$fcm_server_key = get_option('twp_fcm_server_key', '');
|
$fcm_project_id = get_option('twp_fcm_project_id', '');
|
||||||
|
$fcm_service_account_json = get_option('twp_fcm_service_account_json', '');
|
||||||
|
$fcm_sa_configured = !empty($fcm_service_account_json) && !empty($fcm_project_id);
|
||||||
$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
|
$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
|
||||||
$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
|
$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
|
||||||
$gitea_token = get_option('twp_gitea_token', '');
|
$gitea_token = get_option('twp_gitea_token', '');
|
||||||
$twilio_api_key_sid = get_option('twp_twilio_api_key_sid', '');
|
$twilio_api_key_sid = get_option('twp_twilio_api_key_sid', '');
|
||||||
$twilio_api_key_secret = get_option('twp_twilio_api_key_secret', '');
|
$twilio_api_key_secret = get_option('twp_twilio_api_key_secret', '');
|
||||||
$fcm_push_credential_sid = get_option('twp_fcm_push_credential_sid', '');
|
|
||||||
|
|
||||||
// Get update status
|
// Get update status
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
@@ -90,6 +102,12 @@ $total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($sa_json_error)): ?>
|
||||||
|
<div class="notice notice-error is-dismissible">
|
||||||
|
<p><strong><?php echo esc_html($sa_json_error); ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="twp-mobile-settings">
|
<div class="twp-mobile-settings">
|
||||||
<!-- Mobile App Overview -->
|
<!-- Mobile App Overview -->
|
||||||
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
@@ -118,26 +136,45 @@ $total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
|
|||||||
|
|
||||||
<!-- FCM Configuration -->
|
<!-- FCM Configuration -->
|
||||||
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
<h2>Firebase Cloud Messaging (FCM)</h2>
|
<h2>Firebase Cloud Messaging (FCM) — HTTP v2 API</h2>
|
||||||
<p>Configure FCM to enable push notifications for the mobile app.</p>
|
<p>Configure FCM using a service account for push notifications. The legacy server key API has been retired by Google.</p>
|
||||||
|
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="twp_fcm_server_key">FCM Server Key</label>
|
<label for="twp_fcm_project_id">Firebase Project ID</label>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="twp_fcm_server_key"
|
id="twp_fcm_project_id"
|
||||||
name="twp_fcm_server_key"
|
name="twp_fcm_project_id"
|
||||||
value="<?php echo esc_attr($fcm_server_key); ?>"
|
value="<?php echo esc_attr($fcm_project_id); ?>"
|
||||||
class="regular-text"
|
class="regular-text"
|
||||||
placeholder="AAAA...">
|
placeholder="my-project-12345">
|
||||||
<p class="description">
|
<p class="description">
|
||||||
Get your server key from Firebase Console > Project Settings > Cloud Messaging > Server Key
|
Found in Firebase Console > Project Settings > General > Project ID
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_fcm_service_account_json">Service Account JSON</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea id="twp_fcm_service_account_json"
|
||||||
|
name="twp_fcm_service_account_json"
|
||||||
|
rows="6"
|
||||||
|
class="large-text code"
|
||||||
|
placeholder='Paste the entire contents of your service account JSON file...'><?php echo esc_textarea($fcm_service_account_json); ?></textarea>
|
||||||
|
<p class="description">
|
||||||
|
Generate in Firebase Console > Project Settings > Service Accounts > Generate New Private Key.
|
||||||
|
Paste the entire JSON file contents here. Must contain <code>client_email</code> and <code>private_key</code> fields.
|
||||||
|
</p>
|
||||||
|
<?php if ($fcm_sa_configured): ?>
|
||||||
|
<p style="color: #00a32a; margin-top: 5px;">✓ Service account configured</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="twp_twilio_api_key_sid">Twilio API Key SID</label>
|
<label for="twp_twilio_api_key_sid">Twilio API Key SID</label>
|
||||||
@@ -169,25 +206,9 @@ $total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
|
|||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<label for="twp_fcm_push_credential_sid">Push Credential SID</label>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<input type="text"
|
|
||||||
id="twp_fcm_push_credential_sid"
|
|
||||||
name="twp_fcm_push_credential_sid"
|
|
||||||
value="<?php echo esc_attr($fcm_push_credential_sid); ?>"
|
|
||||||
class="regular-text"
|
|
||||||
placeholder="CR...">
|
|
||||||
<p class="description">
|
|
||||||
Twilio Push Credential SID. Create in Twilio Console > Messaging > Push Credentials using your FCM server key. Required for incoming call push notifications.
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php if (!empty($fcm_server_key)): ?>
|
<?php if ($fcm_sa_configured): ?>
|
||||||
<p>
|
<p>
|
||||||
<button type="submit" name="twp_test_notification" class="button">
|
<button type="submit" name="twp_test_notification" class="button">
|
||||||
Send Test Notification
|
Send Test Notification
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Firebase Cloud Messaging (FCM) Integration
|
* Firebase Cloud Messaging (FCM) Integration — HTTP v2 API
|
||||||
*
|
*
|
||||||
* Handles push notifications to mobile devices via FCM
|
* Handles push notifications to mobile devices via FCM using
|
||||||
|
* service account credentials and OAuth2 access tokens.
|
||||||
*/
|
*/
|
||||||
class TWP_FCM {
|
class TWP_FCM {
|
||||||
|
|
||||||
private $server_key;
|
private $project_id;
|
||||||
private $fcm_url = 'https://fcm.googleapis.com/fcm/send';
|
private $service_account;
|
||||||
|
private $fcm_url_template = 'https://fcm.googleapis.com/v1/projects/%s/messages:send';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->server_key = get_option('twp_fcm_server_key', '');
|
$this->project_id = get_option('twp_fcm_project_id', '');
|
||||||
|
$sa_json = get_option('twp_fcm_service_account_json', '');
|
||||||
|
if (!empty($sa_json)) {
|
||||||
|
$this->service_account = json_decode($sa_json, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send push notification to user's devices
|
* Send push notification to user's devices
|
||||||
*/
|
*/
|
||||||
public function send_notification($user_id, $title, $body, $data = array(), $data_only = false) {
|
public function send_notification($user_id, $title, $body, $data = array(), $data_only = false) {
|
||||||
if (empty($this->server_key)) {
|
if (empty($this->project_id) || empty($this->service_account)) {
|
||||||
error_log('TWP FCM: Server key not configured');
|
error_log('TWP FCM: Project ID or service account not configured');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,47 +63,54 @@ class TWP_FCM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send notification to specific token
|
* Send notification to specific token via FCM HTTP v2 API
|
||||||
*/
|
*/
|
||||||
private function send_to_token($token, $title, $body, $data = array(), $data_only = false) {
|
private function send_to_token($token, $title, $body, $data = array(), $data_only = false) {
|
||||||
if ($data_only) {
|
$access_token = $this->get_access_token();
|
||||||
$payload = array(
|
if (!$access_token) {
|
||||||
'to' => $token,
|
return array('success' => false, 'error' => 'auth_failed');
|
||||||
'data' => array_merge($data, array(
|
}
|
||||||
'title' => $title,
|
|
||||||
'body' => $body,
|
// FCM v2 requires all data values to be strings
|
||||||
'timestamp' => time()
|
$string_data = array();
|
||||||
)),
|
foreach ($data as $key => $value) {
|
||||||
'priority' => 'high'
|
$string_data[$key] = is_string($value) ? $value : (string)$value;
|
||||||
);
|
}
|
||||||
} else {
|
$string_data['title'] = $title;
|
||||||
$notification = array(
|
$string_data['body'] = $body;
|
||||||
|
$string_data['timestamp'] = (string)time();
|
||||||
|
|
||||||
|
// Build the v2 message payload
|
||||||
|
$message = array(
|
||||||
|
'token' => $token,
|
||||||
|
'data' => $string_data,
|
||||||
|
'android' => array(
|
||||||
|
'priority' => 'high',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$data_only) {
|
||||||
|
$message['notification'] = array(
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
'sound' => 'default',
|
|
||||||
'priority' => 'high',
|
|
||||||
'click_action' => 'FLUTTER_NOTIFICATION_CLICK'
|
|
||||||
);
|
);
|
||||||
|
$message['android']['notification'] = array(
|
||||||
$payload = array(
|
'sound' => 'default',
|
||||||
'to' => $token,
|
'click_action' => 'FLUTTER_NOTIFICATION_CLICK',
|
||||||
'notification' => $notification,
|
|
||||||
'data' => array_merge($data, array(
|
|
||||||
'title' => $title,
|
|
||||||
'body' => $body,
|
|
||||||
'timestamp' => time()
|
|
||||||
)),
|
|
||||||
'priority' => 'high'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$payload = array('message' => $message);
|
||||||
|
|
||||||
|
$url = sprintf($this->fcm_url_template, $this->project_id);
|
||||||
|
|
||||||
$headers = array(
|
$headers = array(
|
||||||
'Authorization: key=' . $this->server_key,
|
'Authorization: Bearer ' . $access_token,
|
||||||
'Content-Type: application/json'
|
'Content-Type: application/json'
|
||||||
);
|
);
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt($ch, CURLOPT_URL, $this->fcm_url);
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
@@ -111,10 +124,14 @@ class TWP_FCM {
|
|||||||
if ($http_code !== 200) {
|
if ($http_code !== 200) {
|
||||||
error_log("TWP FCM: Failed to send notification. HTTP $http_code: $response");
|
error_log("TWP FCM: Failed to send notification. HTTP $http_code: $response");
|
||||||
|
|
||||||
// Check if token is invalid
|
|
||||||
$response_data = json_decode($response, true);
|
$response_data = json_decode($response, true);
|
||||||
if (isset($response_data['results'][0]['error']) &&
|
$error_code = isset($response_data['error']['details'][0]['errorCode'])
|
||||||
in_array($response_data['results'][0]['error'], array('InvalidRegistration', 'NotRegistered'))) {
|
? $response_data['error']['details'][0]['errorCode'] : '';
|
||||||
|
$error_status = isset($response_data['error']['status'])
|
||||||
|
? $response_data['error']['status'] : '';
|
||||||
|
|
||||||
|
if (in_array($error_code, array('UNREGISTERED', 'INVALID_ARGUMENT')) ||
|
||||||
|
$error_status === 'NOT_FOUND') {
|
||||||
return array('success' => false, 'error' => 'invalid_token');
|
return array('success' => false, 'error' => 'invalid_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +141,107 @@ class TWP_FCM {
|
|||||||
return array('success' => true);
|
return array('success' => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OAuth2 access token from service account credentials.
|
||||||
|
* Caches the token in a transient until near expiry.
|
||||||
|
*/
|
||||||
|
private function get_access_token() {
|
||||||
|
$cached = get_transient('twp_fcm_access_token');
|
||||||
|
if ($cached) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->service_account)) {
|
||||||
|
error_log('TWP FCM: Service account not configured');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jwt = $this->create_jwt();
|
||||||
|
if (!$jwt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $this->service_account['token_uri']);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
|
||||||
|
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||||
|
'assertion' => $jwt,
|
||||||
|
)));
|
||||||
|
|
||||||
|
$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 get access token. HTTP $http_code: $response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token_data = json_decode($response, true);
|
||||||
|
$access_token = $token_data['access_token'];
|
||||||
|
$expires_in = isset($token_data['expires_in']) ? (int)$token_data['expires_in'] : 3600;
|
||||||
|
|
||||||
|
// Cache token for 5 minutes less than actual expiry
|
||||||
|
set_transient('twp_fcm_access_token', $access_token, max(60, $expires_in - 300));
|
||||||
|
|
||||||
|
return $access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a signed JWT for the service account OAuth2 flow
|
||||||
|
*/
|
||||||
|
private function create_jwt() {
|
||||||
|
$sa = $this->service_account;
|
||||||
|
|
||||||
|
if (empty($sa['client_email']) || empty($sa['private_key']) || empty($sa['token_uri'])) {
|
||||||
|
error_log('TWP FCM: Service account JSON missing required fields');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$header = array('alg' => 'RS256', 'typ' => 'JWT');
|
||||||
|
$claims = array(
|
||||||
|
'iss' => $sa['client_email'],
|
||||||
|
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
|
||||||
|
'aud' => $sa['token_uri'],
|
||||||
|
'iat' => $now,
|
||||||
|
'exp' => $now + 3600,
|
||||||
|
);
|
||||||
|
|
||||||
|
$segments = array(
|
||||||
|
$this->base64url_encode(json_encode($header)),
|
||||||
|
$this->base64url_encode(json_encode($claims)),
|
||||||
|
);
|
||||||
|
|
||||||
|
$signing_input = implode('.', $segments);
|
||||||
|
|
||||||
|
$private_key = openssl_pkey_get_private($sa['private_key']);
|
||||||
|
if (!$private_key) {
|
||||||
|
error_log('TWP FCM: Failed to parse service account private key');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = '';
|
||||||
|
if (!openssl_sign($signing_input, $signature, $private_key, OPENSSL_ALGO_SHA256)) {
|
||||||
|
error_log('TWP FCM: Failed to sign JWT');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$segments[] = $this->base64url_encode($signature);
|
||||||
|
|
||||||
|
return implode('.', $segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64url encode (RFC 4648)
|
||||||
|
*/
|
||||||
|
private function base64url_encode($data) {
|
||||||
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all active FCM tokens for a user
|
* Get all active FCM tokens for a user
|
||||||
*/
|
*/
|
||||||
@@ -218,7 +336,7 @@ class TWP_FCM {
|
|||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'type' => 'test',
|
'type' => 'test',
|
||||||
'test' => true
|
'test' => 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->send_notification($user_id, $title, $body, $data);
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
|||||||
@@ -696,7 +696,6 @@ class TWP_Mobile_API {
|
|||||||
$api_key_sid = get_option('twp_twilio_api_key_sid');
|
$api_key_sid = get_option('twp_twilio_api_key_sid');
|
||||||
$api_key_secret = get_option('twp_twilio_api_key_secret');
|
$api_key_secret = get_option('twp_twilio_api_key_secret');
|
||||||
$twiml_app_sid = get_option('twp_twiml_app_sid');
|
$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)) {
|
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));
|
return new WP_Error('missing_api_key', 'Twilio API Key SID and Secret must be configured', array('status' => 500));
|
||||||
@@ -715,10 +714,6 @@ class TWP_Mobile_API {
|
|||||||
$voiceGrant->setOutgoingApplicationSid($twiml_app_sid);
|
$voiceGrant->setOutgoingApplicationSid($twiml_app_sid);
|
||||||
$voiceGrant->setIncomingAllow(true);
|
$voiceGrant->setIncomingAllow(true);
|
||||||
|
|
||||||
if (!empty($push_credential_sid)) {
|
|
||||||
$voiceGrant->setPushCredentialSid($push_credential_sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token->addGrant($voiceGrant);
|
$token->addGrant($voiceGrant);
|
||||||
|
|
||||||
return new WP_REST_Response(array(
|
return new WP_REST_Response(array(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ android {
|
|||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
@@ -21,7 +22,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "io.cloudhosting.twp.twp_softphone"
|
applicationId = "io.cloudhosting.twp.twp_softphone"
|
||||||
minSdkVersion 24
|
minSdkVersion 26
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
@@ -42,6 +43,7 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
implementation platform('com.google.firebase:firebase-bom:33.0.0')
|
implementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||||
implementation 'com.google.firebase:firebase-messaging'
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
}
|
}
|
||||||
|
|||||||
29
mobile/android/app/google-services.json
Normal file
29
mobile/android/app/google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "187457540438",
|
||||||
|
"project_id": "twp-softphone",
|
||||||
|
"storage_bucket": "twp-softphone.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:187457540438:android:d6d777270c23f6660946f7",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "io.cloudhosting.twp.twp_softphone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyAdGJrWPy9b9arqHnlY5G_hsGiDcm6cchA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -18,8 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.1.0" apply false
|
id "com.android.application" version "8.7.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
id "com.google.gms.google-services" version "4.4.0" apply false
|
id "com.google.gms.google-services" version "4.4.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user