Compare commits
9 Commits
v2.8.9
...
2026.01.24
| Author | SHA1 | Date | |
|---|---|---|---|
| 03b6e5d70f | |||
| f8919af31a | |||
| 3e4dff5c4e | |||
| f0806d7e67 | |||
| 61beadcd06 | |||
| 026edde33b | |||
| a3345ed854 | |||
| 384ad5e265 | |||
| 86dd477d4f |
82
.gitea/workflows/release.yml
Normal file
82
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.CI_USER }}
|
||||||
|
password: ${{ secrets.CI_TOKEN }}
|
||||||
|
fetch-depth: 0 # Important: Fetch all history for commit messages
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
# Find the most recent tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "none")
|
||||||
|
|
||||||
|
if [ "$LATEST_TAG" = "none" ]; then
|
||||||
|
# If no previous tag exists, get all commits
|
||||||
|
COMMITS=$(git log --pretty=format:"* %s (%h)" --no-merges)
|
||||||
|
else
|
||||||
|
# Get commits since the last tag
|
||||||
|
COMMITS=$(git log --pretty=format:"* %s (%h)" ${LATEST_TAG}..HEAD --no-merges)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release notes with header (without encoding newlines)
|
||||||
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "## What's New in ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "" >> $GITHUB_OUTPUT
|
||||||
|
echo "$COMMITS" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Update plugin version
|
||||||
|
run: |
|
||||||
|
# Replace version placeholder with actual version
|
||||||
|
sed -i "s/Version: {auto_update_value_on_deploy}/Version: ${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
|
||||||
|
sed -i "s/TWP_VERSION', '{auto_update_value_on_deploy}/TWP_VERSION', '${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
# Verify the changes were made
|
||||||
|
grep "Version:" twilio-wp-plugin.php
|
||||||
|
grep "TWP_VERSION" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
- name: Create ZIP archive
|
||||||
|
run: |
|
||||||
|
# Create a temp directory with the correct plugin folder name
|
||||||
|
mkdir -p /tmp/twilio-wp-plugin
|
||||||
|
|
||||||
|
# Copy files to the temp directory (excluding git and other unnecessary files)
|
||||||
|
cp -r * /tmp/twilio-wp-plugin/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Exclude .git and .gitea directories
|
||||||
|
rm -rf /tmp/twilio-wp-plugin/.git /tmp/twilio-wp-plugin/.gitea 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create the ZIP file with the proper structure
|
||||||
|
cd /tmp
|
||||||
|
zip -r $GITHUB_WORKSPACE/twilio-wp-plugin.zip twilio-wp-plugin
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.REPO_TOKEN }}"
|
||||||
|
title: "Twilio WP Plugin Release ${{ steps.get_version.outputs.version }}"
|
||||||
|
tag_name: ${{ steps.get_version.outputs.version }}
|
||||||
|
body: "${{ steps.release_notes.outputs.notes }}"
|
||||||
|
files: |
|
||||||
|
twilio-wp-plugin.zip
|
||||||
53
.gitea/workflows/update-version.yml
Normal file
53
.gitea/workflows/update-version.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Update Plugin Version
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get release tag
|
||||||
|
id: get_tag
|
||||||
|
run: echo "TAG=${GITEA_REF#refs/tags/}" >> $GITEA_ENV
|
||||||
|
|
||||||
|
- name: Update version in plugin file
|
||||||
|
run: |
|
||||||
|
# Replace version in main plugin file (both header and constant)
|
||||||
|
sed -i "s/Version: {auto_update_value_on_deploy}/Version: ${{ env.TAG }}/" twilio-wp-plugin.php
|
||||||
|
sed -i "s/TWP_VERSION', '{auto_update_value_on_deploy}/TWP_VERSION', '${{ env.TAG }}/" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
# Verify changes
|
||||||
|
grep "Version:" twilio-wp-plugin.php
|
||||||
|
grep "TWP_VERSION" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@gitea.com"
|
||||||
|
git config --local user.name "Gitea Action"
|
||||||
|
git add twilio-wp-plugin.php
|
||||||
|
git commit -m "Update version to ${{ env.TAG }}" || echo "No changes to commit"
|
||||||
|
git push || echo "Nothing to push"
|
||||||
|
|
||||||
|
- name: Create plugin zip
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/twilio-wp-plugin
|
||||||
|
rsync -av --exclude=".git" --exclude=".gitea" --exclude="build" . /tmp/twilio-wp-plugin/
|
||||||
|
cd /tmp
|
||||||
|
zip -r $GITEA_WORK_DIR/twilio-wp-plugin.zip twilio-wp-plugin
|
||||||
|
|
||||||
|
- name: Upload zip to release
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ gitea.event.release.upload_url }}
|
||||||
|
asset_path: twilio-wp-plugin.zip
|
||||||
|
asset_name: twilio-wp-plugin.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
45
CLAUDE.md
45
CLAUDE.md
@@ -6,6 +6,7 @@
|
|||||||
- **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
|
||||||
|
- **External SDK**: `wp-content/twilio-sdk/` (survives plugin updates)
|
||||||
|
|
||||||
## 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`
|
||||||
@@ -47,6 +48,25 @@ $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
|||||||
- Firefox support added
|
- Firefox support added
|
||||||
- 1-min agent status auto-revert
|
- 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
|
||||||
@@ -61,5 +81,28 @@ $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
|||||||
- ElevenLabs TTS with Alice fallback
|
- ElevenLabs TTS with Alice fallback
|
||||||
- 68 AJAX actions, 26 REST endpoints
|
- 68 AJAX actions, 26 REST endpoints
|
||||||
|
|
||||||
|
## Recent Technical Changes (v2.8.9)
|
||||||
|
|
||||||
|
### SDK Persistence Between Plugin Updates
|
||||||
|
- **Problem**: WordPress plugin updates delete entire plugin folder including `vendor/` SDK
|
||||||
|
- **Solution**: External SDK installation at `wp-content/twilio-sdk/` survives updates
|
||||||
|
- **Implementation**:
|
||||||
|
- 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)
|
||||||
|
- **Problem**: Browser phone had hardcoded `edge: 'sydney'`, causing US calls to fail with immediate HANGUP
|
||||||
|
- **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: Sept 2025*
|
*Updated: Jan 2026*
|
||||||
427
DEBUGGING-TABLET.md
Normal file
427
DEBUGGING-TABLET.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
# Debugging Browser Phone on Android Tablet
|
||||||
|
|
||||||
|
This guide explains how to debug the Twilio WordPress Plugin browser phone on Android tablets (Samsung and other devices).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Android tablet with Chrome browser
|
||||||
|
- USB cable to connect tablet to computer
|
||||||
|
- Computer with Chrome browser installed
|
||||||
|
- USB debugging enabled on tablet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Enable USB Debugging on Android Tablet
|
||||||
|
|
||||||
|
### Step 1: Enable Developer Options
|
||||||
|
|
||||||
|
1. Open **Settings** on your Android tablet
|
||||||
|
2. Scroll down to **About tablet** (or **About device**)
|
||||||
|
3. Find **Build number** (may be under "Software information")
|
||||||
|
4. Tap **Build number** 7 times rapidly
|
||||||
|
5. You'll see a message: "You are now a developer!"
|
||||||
|
|
||||||
|
### Step 2: Enable USB Debugging
|
||||||
|
|
||||||
|
1. Go back to **Settings**
|
||||||
|
2. Scroll down to **Developer options** (newly appeared)
|
||||||
|
3. Toggle **Developer options** to ON
|
||||||
|
4. Find **USB debugging** in the list
|
||||||
|
5. Toggle **USB debugging** to ON
|
||||||
|
6. Confirm the prompt if asked
|
||||||
|
|
||||||
|
### Step 3: Trust Your Computer
|
||||||
|
|
||||||
|
1. Connect tablet to computer via USB cable
|
||||||
|
2. Unlock your tablet screen
|
||||||
|
3. A popup will appear: "Allow USB debugging?"
|
||||||
|
4. Check **Always allow from this computer**
|
||||||
|
5. Tap **OK** or **Allow**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Connect Chrome DevTools
|
||||||
|
|
||||||
|
### On Your Computer
|
||||||
|
|
||||||
|
1. Open **Chrome browser** on your computer
|
||||||
|
2. Navigate to: `chrome://inspect`
|
||||||
|
3. You should see your tablet device listed under "Remote Target"
|
||||||
|
4. Wait a few seconds for the device to appear
|
||||||
|
|
||||||
|
### Inspect the Browser Phone Page
|
||||||
|
|
||||||
|
1. On your tablet, open Chrome and navigate to:
|
||||||
|
```
|
||||||
|
https://phone.cloud-hosting.io/wp-admin/admin.php?page=twilio-wp-browser-phone
|
||||||
|
```
|
||||||
|
2. On your computer's Chrome DevTools (`chrome://inspect`), you'll see the page listed
|
||||||
|
3. Click **inspect** next to the browser phone page
|
||||||
|
4. A DevTools window will open showing your tablet's browser
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Debug Browser Phone Issues
|
||||||
|
|
||||||
|
### Check Console Logs
|
||||||
|
|
||||||
|
In the **Console** tab, look for key messages:
|
||||||
|
|
||||||
|
#### Successful Connection
|
||||||
|
```
|
||||||
|
Twilio SDK loaded successfully
|
||||||
|
Setting up Twilio Device...
|
||||||
|
Device detection - Android: true, Mobile: true
|
||||||
|
AudioContext created, state: running
|
||||||
|
Device registered successfully
|
||||||
|
Device connection state: connected
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Connection Issues
|
||||||
|
```
|
||||||
|
Device not connected, state: disconnected
|
||||||
|
Failed to register device
|
||||||
|
Connection error: 31005
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Call Issues
|
||||||
|
```
|
||||||
|
Answer button clicked
|
||||||
|
Device connection state: connected
|
||||||
|
Accepting call...
|
||||||
|
Call accepted and connected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Network Requests
|
||||||
|
|
||||||
|
1. Switch to **Network** tab in DevTools
|
||||||
|
2. Filter by: `twp_` or `twilio`
|
||||||
|
3. Check for failed requests (red status codes)
|
||||||
|
4. Look for:
|
||||||
|
- `twp_generate_capability_token` - Should return 200
|
||||||
|
- `twp_get_phone_numbers` - Should return 200
|
||||||
|
- WebSocket connections to Twilio
|
||||||
|
|
||||||
|
### Check Device Registration
|
||||||
|
|
||||||
|
In the **Console** tab, type:
|
||||||
|
```javascript
|
||||||
|
device
|
||||||
|
```
|
||||||
|
This shows the current Twilio Device object. Check:
|
||||||
|
- `device.state` - Should be "registered"
|
||||||
|
- `device.token` - Should exist (long string)
|
||||||
|
|
||||||
|
### Check AudioContext State
|
||||||
|
|
||||||
|
In the **Console** tab, type:
|
||||||
|
```javascript
|
||||||
|
audioContext
|
||||||
|
```
|
||||||
|
Check:
|
||||||
|
- Should exist (not null)
|
||||||
|
- `audioContext.state` - Should be "running" (not "suspended")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4: Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue 1: "Device not connected" Error
|
||||||
|
|
||||||
|
**Symptoms**: Status shows "Disconnected", calls hang up immediately
|
||||||
|
|
||||||
|
**Debugging**:
|
||||||
|
```javascript
|
||||||
|
// In console, check:
|
||||||
|
deviceConnectionState
|
||||||
|
// Should be: "connected"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check network connection (try WiFi vs cellular)
|
||||||
|
2. Refresh the page with cache cleared
|
||||||
|
3. Check console for token errors
|
||||||
|
4. Verify Twilio credentials in plugin settings
|
||||||
|
|
||||||
|
### Issue 2: Call Hangs Up Immediately When Answered
|
||||||
|
|
||||||
|
**Symptoms**: Click "Answer" button, call disconnects instantly
|
||||||
|
|
||||||
|
**Debugging**:
|
||||||
|
```javascript
|
||||||
|
// Check device state before answering:
|
||||||
|
deviceConnectionState
|
||||||
|
// Check for errors in call handler
|
||||||
|
```
|
||||||
|
|
||||||
|
**Look for console errors**:
|
||||||
|
- `31005` - Connection/network error
|
||||||
|
- `31201` - ICE connection failure
|
||||||
|
- `31208` - Media connection error
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Grant microphone permissions (Settings > Site settings > Microphone)
|
||||||
|
2. Check AudioContext is running: `audioContext.state`
|
||||||
|
3. Try switching between WiFi and cellular data
|
||||||
|
4. Clear Chrome cache and reload
|
||||||
|
|
||||||
|
### Issue 3: No Sound/Ringtone on Incoming Call
|
||||||
|
|
||||||
|
**Symptoms**: Call arrives but no audio plays, no vibration
|
||||||
|
|
||||||
|
**Debugging**:
|
||||||
|
```javascript
|
||||||
|
// Check audio setup:
|
||||||
|
ringtoneAudio
|
||||||
|
audioContext.state
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Tap screen to unlock AudioContext (mobile restriction)
|
||||||
|
2. Check if ringtone file exists (optional, vibration is fallback)
|
||||||
|
3. Verify device supports Vibration API: `'vibrate' in navigator`
|
||||||
|
4. Check browser volume settings
|
||||||
|
|
||||||
|
### Issue 4: No Browser Notifications
|
||||||
|
|
||||||
|
**Symptoms**: No notification when call arrives in background
|
||||||
|
|
||||||
|
**Debugging**:
|
||||||
|
```javascript
|
||||||
|
// Check notification permission:
|
||||||
|
Notification.permission
|
||||||
|
// Should be: "granted"
|
||||||
|
|
||||||
|
// Check service worker:
|
||||||
|
navigator.serviceWorker.controller
|
||||||
|
// Should exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Grant notification permission: Settings > Site settings > Notifications
|
||||||
|
2. Check service worker registration in **Application** tab of DevTools
|
||||||
|
3. Look for service worker logs in console
|
||||||
|
4. Reinstall PWA if installed
|
||||||
|
|
||||||
|
### Issue 5: Microphone Permission Denied
|
||||||
|
|
||||||
|
**Symptoms**: Error message about microphone access
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Chrome Settings > Site settings > Microphone
|
||||||
|
2. Find `phone.cloud-hosting.io`
|
||||||
|
3. Change permission to **Allow**
|
||||||
|
4. Refresh the browser phone page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 5: Tablet-Specific Checks
|
||||||
|
|
||||||
|
### Check User Agent
|
||||||
|
|
||||||
|
In console:
|
||||||
|
```javascript
|
||||||
|
navigator.userAgent
|
||||||
|
```
|
||||||
|
Should include "Android" and "Chrome"
|
||||||
|
|
||||||
|
### Check WebRTC Support
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check getUserMedia support:
|
||||||
|
navigator.mediaDevices.getUserMedia
|
||||||
|
// Should be: function
|
||||||
|
|
||||||
|
// Check Notification support:
|
||||||
|
'Notification' in window
|
||||||
|
// Should be: true
|
||||||
|
|
||||||
|
// Check Service Worker support:
|
||||||
|
'serviceWorker' in navigator
|
||||||
|
// Should be: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Audio Constraints
|
||||||
|
|
||||||
|
Look in console for:
|
||||||
|
```
|
||||||
|
Twilio Device created with audio constraints: {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true,
|
||||||
|
googEchoCancellation: true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Vibration
|
||||||
|
|
||||||
|
In console:
|
||||||
|
```javascript
|
||||||
|
navigator.vibrate([300, 200, 300])
|
||||||
|
```
|
||||||
|
Tablet should vibrate if supported.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 6: Advanced Debugging
|
||||||
|
|
||||||
|
### Monitor Twilio Device Events
|
||||||
|
|
||||||
|
In console, add event listeners:
|
||||||
|
```javascript
|
||||||
|
device.on('registered', () => console.log('Device registered!'));
|
||||||
|
device.on('error', (error) => console.error('Device error:', error));
|
||||||
|
device.on('incoming', (call) => console.log('Incoming call:', call));
|
||||||
|
device.on('unregistered', () => console.log('Device unregistered'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Call State During Active Call
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// When in a call:
|
||||||
|
currentCall
|
||||||
|
currentCall.status()
|
||||||
|
currentCall.parameters
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Audio Context Resume
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Try to resume manually:
|
||||||
|
audioContext.resume().then(() => {
|
||||||
|
console.log('AudioContext state:', audioContext.state);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Token Expiry
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// See when token expires:
|
||||||
|
new Date(tokenExpiry)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 7: Logging Important Information
|
||||||
|
|
||||||
|
### Collect Debugging Info
|
||||||
|
|
||||||
|
Run this in console to get a debug report:
|
||||||
|
```javascript
|
||||||
|
console.log('=== Browser Phone Debug Report ===');
|
||||||
|
console.log('User Agent:', navigator.userAgent);
|
||||||
|
console.log('Device State:', deviceConnectionState);
|
||||||
|
console.log('Device Registered:', device ? device.state : 'no device');
|
||||||
|
console.log('AudioContext:', audioContext ? audioContext.state : 'no context');
|
||||||
|
console.log('Notification Permission:', Notification.permission);
|
||||||
|
console.log('Service Worker:', navigator.serviceWorker.controller ? 'active' : 'inactive');
|
||||||
|
console.log('Current Call:', currentCall ? 'in call' : 'no call');
|
||||||
|
console.log('Token Expiry:', tokenExpiry ? new Date(tokenExpiry) : 'unknown');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check WordPress AJAX Responses
|
||||||
|
|
||||||
|
In **Network** tab:
|
||||||
|
1. Filter: `admin-ajax.php`
|
||||||
|
2. Click on request
|
||||||
|
3. Check **Preview** tab for response
|
||||||
|
4. Look for `success: true` or error messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 8: Testing Checklist
|
||||||
|
|
||||||
|
After fixing issues, test these scenarios:
|
||||||
|
|
||||||
|
- [ ] Device shows "Connected" status
|
||||||
|
- [ ] Can make outbound call successfully
|
||||||
|
- [ ] Can receive incoming call (see notification)
|
||||||
|
- [ ] Can answer incoming call without hang-up
|
||||||
|
- [ ] Ringtone plays or tablet vibrates
|
||||||
|
- [ ] Call audio is clear (echo cancellation working)
|
||||||
|
- [ ] Can switch between WiFi and cellular during call
|
||||||
|
- [ ] Browser notification appears when app in background
|
||||||
|
- [ ] Service worker logs appear in console
|
||||||
|
- [ ] No errors in console during call lifecycle
|
||||||
|
- [ ] Can put call on hold
|
||||||
|
- [ ] Can transfer call
|
||||||
|
- [ ] Call timer updates correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Specific Error Codes
|
||||||
|
|
||||||
|
### Twilio Error 31005
|
||||||
|
**Meaning**: WebSocket connection failed
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Network connectivity issues
|
||||||
|
- Firewall blocking WebSocket
|
||||||
|
- Mobile network switching (WiFi ↔ cellular)
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Check internet connection
|
||||||
|
- Try different network
|
||||||
|
- Wait for ICE restart (enabled in config)
|
||||||
|
|
||||||
|
### Twilio Error 31201
|
||||||
|
**Meaning**: ICE connection failed
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Restrictive NAT/firewall
|
||||||
|
- Mobile network issues
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Try different network
|
||||||
|
- Check WebRTC connectivity
|
||||||
|
- Enable mobile data if on WiFi only
|
||||||
|
|
||||||
|
### Twilio Error 31204
|
||||||
|
**Meaning**: Connection error
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Media connection setup failed
|
||||||
|
- Signaling timeout
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Refresh page
|
||||||
|
- Check microphone permissions
|
||||||
|
- Verify audio constraints applied
|
||||||
|
|
||||||
|
### Twilio Error 31208
|
||||||
|
**Meaning**: Media connection failed
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Microphone access denied
|
||||||
|
- Audio device issues
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Grant microphone permission
|
||||||
|
- Check device audio settings
|
||||||
|
- Restart browser
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- Twilio Voice SDK Errors: https://www.twilio.com/docs/voice/sdks/javascript/errors
|
||||||
|
- Chrome DevTools Remote Debugging: https://developer.chrome.com/docs/devtools/remote-debugging/
|
||||||
|
- WebRTC Troubleshooting: https://webrtc.github.io/samples/
|
||||||
|
- Service Worker Debugging: https://developer.chrome.com/docs/workbox/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact & Support
|
||||||
|
|
||||||
|
If issues persist after following this guide:
|
||||||
|
|
||||||
|
1. Collect debug report (see Part 7)
|
||||||
|
2. Take screenshots of console errors
|
||||||
|
3. Note tablet model and Android version
|
||||||
|
4. Report issue with collected information
|
||||||
|
|
||||||
|
**Important Files**:
|
||||||
|
- Browser phone implementation: `admin/class-twp-admin.php` (lines 7400-8300)
|
||||||
|
- Service worker: `assets/js/twp-service-worker.js`
|
||||||
|
- CLAUDE.md: Quick reference guide
|
||||||
62
README.md
62
README.md
@@ -8,11 +8,20 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will
|
|||||||
|
|
||||||
## Quick Installation
|
## Quick Installation
|
||||||
|
|
||||||
1. **Install the Twilio SDK** (Required):
|
1. **Install the Twilio SDK** (Required - Recommended Method):
|
||||||
|
```bash
|
||||||
|
chmod +x install-twilio-sdk-external.sh
|
||||||
|
./install-twilio-sdk-external.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the SDK to `wp-content/twilio-sdk/` which **survives WordPress plugin updates**. The plugin will automatically detect and use this external SDK.
|
||||||
|
|
||||||
|
**Alternative Method** (SDK will be deleted during plugin updates):
|
||||||
```bash
|
```bash
|
||||||
chmod +x install-twilio-sdk.sh
|
chmod +x install-twilio-sdk.sh
|
||||||
./install-twilio-sdk.sh
|
./install-twilio-sdk.sh
|
||||||
```
|
```
|
||||||
|
This installs the SDK inside the plugin folder. You'll need to reinstall the SDK after each plugin update.
|
||||||
|
|
||||||
2. **Test the SDK installation**:
|
2. **Test the SDK installation**:
|
||||||
```bash
|
```bash
|
||||||
@@ -23,6 +32,7 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will
|
|||||||
- Go to **Twilio** → **Settings**
|
- Go to **Twilio** → **Settings**
|
||||||
- Enter Account SID and Auth Token
|
- Enter Account SID and Auth Token
|
||||||
- Configure default phone numbers
|
- Configure default phone numbers
|
||||||
|
- Set Twilio Edge Location (for browser phone - see Browser Phone Setup below)
|
||||||
|
|
||||||
4. **Set up Phone Numbers** in Twilio Console:
|
4. **Set up Phone Numbers** in Twilio Console:
|
||||||
- Configure webhook URLs for voice and SMS
|
- Configure webhook URLs for voice and SMS
|
||||||
@@ -333,8 +343,20 @@ Comprehensive redesign of hold, transfer, and requeue functionality with profess
|
|||||||
2. **Configure in WordPress**:
|
2. **Configure in WordPress**:
|
||||||
- Go to **Twilio** → **Settings**
|
- Go to **Twilio** → **Settings**
|
||||||
- Enter TwiML App SID
|
- Enter TwiML App SID
|
||||||
|
- **Set Twilio Edge Location**: Select the edge location closest to your users (IMPORTANT)
|
||||||
|
- **Auto-select closest (Recommended)**: Automatically selects the best edge
|
||||||
|
- **US East (Ashburn)**: For East Coast USA users
|
||||||
|
- **US West (Umatilla)**: For West Coast USA users
|
||||||
|
- **Europe - Ireland (Dublin)**: For European users
|
||||||
|
- **Europe - Germany (Frankfurt)**: For Central European users
|
||||||
|
- **Singapore**: For Southeast Asian users
|
||||||
|
- **Sydney**: For Australian users
|
||||||
|
- **Tokyo**: For Japanese users
|
||||||
|
- **Sao Paulo**: For South American users
|
||||||
- Save settings
|
- Save settings
|
||||||
|
|
||||||
|
**Note**: Selecting the wrong edge location can cause calls to fail immediately. If you're experiencing browser phone connection issues, verify your edge location is appropriate for your region.
|
||||||
|
|
||||||
3. **Access Browser Phone** (Admin Only):
|
3. **Access Browser Phone** (Admin Only):
|
||||||
- Navigate to **WordPress Admin** → **Twilio** → **Browser Phone**
|
- Navigate to **WordPress Admin** → **Twilio** → **Browser Phone**
|
||||||
- Select caller ID from available numbers
|
- Select caller ID from available numbers
|
||||||
@@ -432,14 +454,34 @@ Access the full browser phone interface at: **WordPress Admin → Twilio → Bro
|
|||||||
- **Login Required**: Users must be logged in to access browser phone functionality
|
- **Login Required**: Users must be logged in to access browser phone functionality
|
||||||
- Check TwiML App SID is configured in WordPress admin settings
|
- Check TwiML App SID is configured in WordPress admin settings
|
||||||
|
|
||||||
|
#### Browser Phone Calls Failing Immediately
|
||||||
|
If browser phone calls disconnect immediately or show HANGUP errors:
|
||||||
|
- **Check Edge Location Setting**: Go to **Twilio** → **Settings** → **Twilio Edge Location**
|
||||||
|
- **US Users**: Should use "Auto-select closest" (roaming), "US East (Ashburn)", or "US West (Umatilla)"
|
||||||
|
- **International Users**: Select the edge location closest to your region
|
||||||
|
- **Problem**: Wrong edge location causes gateway to immediately reject calls
|
||||||
|
- **Solution**: Change edge location setting and try the call again (no restart needed)
|
||||||
|
|
||||||
#### "Twilio SDK classes not available"
|
#### "Twilio SDK classes not available"
|
||||||
|
|
||||||
|
**Recommended Solution** (SDK survives plugin updates):
|
||||||
```bash
|
```bash
|
||||||
# Reinstall SDK
|
# Install SDK to external location
|
||||||
|
./install-twilio-sdk-external.sh
|
||||||
|
# Test installation
|
||||||
|
php test-sdk.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative Solution** (will need reinstall after plugin updates):
|
||||||
|
```bash
|
||||||
|
# Install SDK inside plugin folder
|
||||||
./install-twilio-sdk.sh
|
./install-twilio-sdk.sh
|
||||||
# Test installation
|
# Test installation
|
||||||
php test-sdk.php
|
php test-sdk.php
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**After WordPress Plugin Update**: If you get this error after updating the plugin and used the internal SDK method, you'll need to reinstall the SDK. This won't happen if you use the external SDK method.
|
||||||
|
|
||||||
#### Calls Not Routing to Queues
|
#### Calls Not Routing to Queues
|
||||||
- Verify queue exists and is active
|
- Verify queue exists and is active
|
||||||
- Check agent group has members
|
- Check agent group has members
|
||||||
@@ -551,7 +593,19 @@ All webhooks are REST API endpoints under `/wp-json/twilio-webhook/v1/`:
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
### v2.3.0 (Current - September 2025) - ENTERPRISE READY
|
### v2.8.9 (Current - January 2026) - SDK PERSISTENCE & BROWSER PHONE FIXES
|
||||||
|
- **SDK PERSISTENCE**: External SDK installation option that survives WordPress plugin updates
|
||||||
|
- New installation script: `install-twilio-sdk-external.sh` installs SDK to `wp-content/twilio-sdk/`
|
||||||
|
- Automatic detection: Plugin checks external SDK location first, falls back to internal
|
||||||
|
- Post-update warnings: Notifies if SDK was deleted during plugin update
|
||||||
|
- Zero downtime: Phone system continues working through plugin updates
|
||||||
|
- **BROWSER PHONE FIX**: Resolved US calls failing immediately with HANGUP errors
|
||||||
|
- Made Twilio Edge Location configurable (was hardcoded to Sydney)
|
||||||
|
- New setting: Twilio Edge Location with 8 options (roaming/auto-select, ashburn, umatilla, dublin, frankfurt, singapore, sydney, tokyo, sao-paulo)
|
||||||
|
- Default: "roaming" (auto-select closest edge for optimal performance)
|
||||||
|
- Critical fix: US users can now make calls successfully (were failing with Sydney edge)
|
||||||
|
|
||||||
|
### v2.3.0 (September 2025) - ENTERPRISE READY
|
||||||
- **SECURITY ENHANCEMENT**: Removed frontend browser phone interface, moved to admin-only access for enhanced security
|
- **SECURITY ENHANCEMENT**: Removed frontend browser phone interface, moved to admin-only access for enhanced security
|
||||||
- **ASSET REDUCTION**: Eliminated 108KB of frontend assets (browser-phone-frontend.js and CSS files)
|
- **ASSET REDUCTION**: Eliminated 108KB of frontend assets (browser-phone-frontend.js and CSS files)
|
||||||
- **SHORTCODE SECURITY**: Browser phone shortcode now provides secure redirect with authentication checks
|
- **SHORTCODE SECURITY**: Browser phone shortcode now provides secure redirect with authentication checks
|
||||||
@@ -611,4 +665,4 @@ This plugin integrates with Twilio services and requires a Twilio account.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Enterprise Ready v2.3.0** - Extension transfers, browser phone compatibility, and automatic agent management now production-ready with comprehensive reliability improvements.
|
**Production Ready v2.8.9** - SDK persistence through plugin updates and configurable edge locations ensure zero-downtime phone operations.
|
||||||
@@ -119,7 +119,16 @@ class TWP_Admin {
|
|||||||
'twilio-wp-settings',
|
'twilio-wp-settings',
|
||||||
array($this, 'display_plugin_settings')
|
array($this, 'display_plugin_settings')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'twilio-wp-plugin',
|
||||||
|
'Mobile App',
|
||||||
|
'Mobile App',
|
||||||
|
'manage_options',
|
||||||
|
'twilio-wp-mobile-app',
|
||||||
|
array($this, 'display_mobile_app_settings')
|
||||||
|
);
|
||||||
|
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'twilio-wp-plugin',
|
'twilio-wp-plugin',
|
||||||
'Phone Schedules',
|
'Phone Schedules',
|
||||||
@@ -331,13 +340,32 @@ class TWP_Admin {
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">TwiML App SID</th>
|
<th scope="row">TwiML App SID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="twp_twiml_app_sid"
|
<input type="text" name="twp_twiml_app_sid"
|
||||||
value="<?php echo esc_attr(get_option('twp_twiml_app_sid')); ?>"
|
value="<?php echo esc_attr(get_option('twp_twiml_app_sid')); ?>"
|
||||||
class="regular-text" />
|
class="regular-text" />
|
||||||
<p class="description">TwiML Application SID for Browser Phone (optional). <a href="#twiml-app-instructions">See setup instructions below</a></p>
|
<p class="description">TwiML Application SID for Browser Phone (optional). <a href="#twiml-app-instructions">See setup instructions below</a></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Twilio Edge Location</th>
|
||||||
|
<td>
|
||||||
|
<?php $current_edge = get_option('twp_twilio_edge', 'roaming'); ?>
|
||||||
|
<select name="twp_twilio_edge" class="regular-text">
|
||||||
|
<option value="roaming" <?php selected($current_edge, 'roaming'); ?>>Auto-select closest (Recommended)</option>
|
||||||
|
<option value="ashburn" <?php selected($current_edge, 'ashburn'); ?>>US East (Ashburn)</option>
|
||||||
|
<option value="umatilla" <?php selected($current_edge, 'umatilla'); ?>>US West (Umatilla)</option>
|
||||||
|
<option value="dublin" <?php selected($current_edge, 'dublin'); ?>>Europe - Ireland (Dublin)</option>
|
||||||
|
<option value="frankfurt" <?php selected($current_edge, 'frankfurt'); ?>>Europe - Germany (Frankfurt)</option>
|
||||||
|
<option value="singapore" <?php selected($current_edge, 'singapore'); ?>>Asia Pacific (Singapore)</option>
|
||||||
|
<option value="sydney" <?php selected($current_edge, 'sydney'); ?>>Australia (Sydney)</option>
|
||||||
|
<option value="tokyo" <?php selected($current_edge, 'tokyo'); ?>>Japan (Tokyo)</option>
|
||||||
|
<option value="sao-paulo" <?php selected($current_edge, 'sao-paulo'); ?>>South America (Sao Paulo)</option>
|
||||||
|
</select>
|
||||||
|
<p class="description">Edge location for browser phone calls. Use "Auto-select closest" for best call quality, or select a specific region.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Eleven Labs API Settings</h2>
|
<h2>Eleven Labs API Settings</h2>
|
||||||
@@ -3809,6 +3837,7 @@ class TWP_Admin {
|
|||||||
register_setting('twilio-wp-settings-group', 'twp_twilio_account_sid');
|
register_setting('twilio-wp-settings-group', 'twp_twilio_account_sid');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_twilio_auth_token');
|
register_setting('twilio-wp-settings-group', 'twp_twilio_auth_token');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_twiml_app_sid');
|
register_setting('twilio-wp-settings-group', 'twp_twiml_app_sid');
|
||||||
|
register_setting('twilio-wp-settings-group', 'twp_twilio_edge');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_api_key');
|
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_api_key');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_voice_id');
|
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_voice_id');
|
||||||
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_model_id');
|
register_setting('twilio-wp-settings-group', 'twp_elevenlabs_model_id');
|
||||||
@@ -6987,6 +7016,8 @@ class TWP_Admin {
|
|||||||
<div class="phone-interface">
|
<div class="phone-interface">
|
||||||
<div class="phone-display">
|
<div class="phone-display">
|
||||||
<div id="phone-status">Ready</div>
|
<div id="phone-status">Ready</div>
|
||||||
|
<div id="device-connection-status" style="font-size: 12px; color: #999; margin-top: 5px;">Loading...</div>
|
||||||
|
<div id="twp-debug-info" style="font-size: 10px; color: #666; margin-top: 3px;"></div>
|
||||||
<div id="phone-number-display"></div>
|
<div id="phone-number-display"></div>
|
||||||
<div id="call-timer" style="display: none;">00:00</div>
|
<div id="call-timer" style="display: none;">00:00</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -7425,7 +7456,211 @@ class TWP_Admin {
|
|||||||
var callStartTime = null;
|
var callStartTime = null;
|
||||||
var tokenRefreshTimer = null;
|
var tokenRefreshTimer = null;
|
||||||
var tokenExpiry = null;
|
var tokenExpiry = null;
|
||||||
|
var audioContext = null;
|
||||||
|
var ringtoneAudio = null;
|
||||||
|
var isPageVisible = true;
|
||||||
|
var deviceConnectionState = 'disconnected'; // disconnected, connecting, connected
|
||||||
|
var serviceWorkerRegistration = null;
|
||||||
|
|
||||||
|
// Initialize AudioContext for mobile audio playback
|
||||||
|
function initializeAudioContext() {
|
||||||
|
try {
|
||||||
|
if (!audioContext) {
|
||||||
|
// Create AudioContext with compatibility
|
||||||
|
var AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
|
audioContext = new AudioContextClass();
|
||||||
|
console.log('AudioContext created, state:', audioContext.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume AudioContext if suspended (required on mobile)
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
audioContext.resume().then(function() {
|
||||||
|
console.log('AudioContext resumed successfully');
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to resume AudioContext:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize AudioContext:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and setup ringtone audio element
|
||||||
|
function setupRingtone() {
|
||||||
|
if (!ringtoneAudio) {
|
||||||
|
ringtoneAudio = new Audio();
|
||||||
|
// Use a simple sine wave tone or default ringtone
|
||||||
|
// For now, we'll use a data URI for a simple beep tone
|
||||||
|
ringtoneAudio.loop = true;
|
||||||
|
ringtoneAudio.volume = 0.7;
|
||||||
|
|
||||||
|
// Create a simple ringtone using Web Audio API
|
||||||
|
createRingtone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ringtone using Web Audio API for better mobile support
|
||||||
|
function createRingtone() {
|
||||||
|
// Use a simple base64-encoded beep tone (short MP3)
|
||||||
|
// This is a simple 1-second beep at 800Hz
|
||||||
|
// You can replace this with a custom ringtone file URL if you upload one
|
||||||
|
|
||||||
|
// For now, use a simple approach: HTML5 Audio with error fallback
|
||||||
|
// Note: On mobile, audio playback may be restricted, so we rely heavily on vibration
|
||||||
|
var ringtoneUrl = '<?php echo plugins_url('assets/sounds/ringtone.mp3', dirname(__FILE__)); ?>';
|
||||||
|
|
||||||
|
// Try to load the ringtone file
|
||||||
|
ringtoneAudio.src = ringtoneUrl;
|
||||||
|
|
||||||
|
// Fallback: if ringtone file fails to load, we'll just use vibration
|
||||||
|
ringtoneAudio.addEventListener('error', function(e) {
|
||||||
|
console.log('Ringtone file not found (this is normal), using vibration only for mobile');
|
||||||
|
// Don't show error - vibration is sufficient for mobile
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
// Try to preload
|
||||||
|
ringtoneAudio.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play ringtone for incoming call
|
||||||
|
function playRingtone() {
|
||||||
|
try {
|
||||||
|
// Initialize AudioContext on user interaction
|
||||||
|
initializeAudioContext();
|
||||||
|
|
||||||
|
if (ringtoneAudio) {
|
||||||
|
var playPromise = ringtoneAudio.play();
|
||||||
|
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise.then(function() {
|
||||||
|
console.log('Ringtone playing');
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.error('Ringtone play failed:', error);
|
||||||
|
// Fallback: vibrate on mobile
|
||||||
|
vibrateDevice([300, 200, 300, 200, 300]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always vibrate on mobile for better notification
|
||||||
|
vibrateDevice([300, 200, 300, 200, 300]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error playing ringtone:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop ringtone
|
||||||
|
function stopRingtone() {
|
||||||
|
try {
|
||||||
|
if (ringtoneAudio) {
|
||||||
|
ringtoneAudio.pause();
|
||||||
|
ringtoneAudio.currentTime = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping ringtone:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibrate device (mobile)
|
||||||
|
function vibrateDevice(pattern) {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register service worker for PWA notifications
|
||||||
|
function registerServiceWorker() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
var swPath = '<?php echo plugins_url('assets/js/twp-service-worker.js', dirname(__FILE__)); ?>';
|
||||||
|
|
||||||
|
navigator.serviceWorker.register(swPath)
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('Service Worker registered:', registration);
|
||||||
|
serviceWorkerRegistration = registration;
|
||||||
|
|
||||||
|
// Request notification permission
|
||||||
|
if ('Notification' in window && Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission().then(function(permission) {
|
||||||
|
console.log('Notification permission:', permission);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification via service worker
|
||||||
|
function sendIncomingCallNotification(callerNumber) {
|
||||||
|
// Try browser notification first
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
|
if (serviceWorkerRegistration && serviceWorkerRegistration.active) {
|
||||||
|
serviceWorkerRegistration.active.postMessage({
|
||||||
|
type: 'SHOW_NOTIFICATION',
|
||||||
|
title: 'Incoming Call',
|
||||||
|
body: 'Call from ' + (callerNumber || 'Unknown Number'),
|
||||||
|
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
|
||||||
|
tag: 'incoming-call',
|
||||||
|
requireInteraction: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: show notification directly
|
||||||
|
new Notification('Incoming Call', {
|
||||||
|
body: 'Call from ' + (callerNumber || 'Unknown Number'),
|
||||||
|
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
|
||||||
|
tag: 'incoming-call',
|
||||||
|
requireInteraction: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor page visibility for background call handling
|
||||||
|
function setupPageVisibility() {
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
isPageVisible = !document.hidden;
|
||||||
|
console.log('Page visibility changed:', isPageVisible ? 'visible' : 'hidden');
|
||||||
|
|
||||||
|
// If page becomes visible, resume audio context
|
||||||
|
if (isPageVisible && audioContext) {
|
||||||
|
initializeAudioContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update device connection status in UI
|
||||||
|
function updateConnectionStatus(state) {
|
||||||
|
deviceConnectionState = state;
|
||||||
|
var statusText = '';
|
||||||
|
var statusColor = '';
|
||||||
|
|
||||||
|
switch(state) {
|
||||||
|
case 'connected':
|
||||||
|
statusText = 'Connected';
|
||||||
|
statusColor = '#4CAF50';
|
||||||
|
break;
|
||||||
|
case 'connecting':
|
||||||
|
statusText = 'Connecting...';
|
||||||
|
statusColor = '#FF9800';
|
||||||
|
break;
|
||||||
|
case 'disconnected':
|
||||||
|
statusText = 'Disconnected';
|
||||||
|
statusColor = '#f44336';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusText = 'Unknown';
|
||||||
|
statusColor = '#999';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status indicator (we'll add this to the UI)
|
||||||
|
$('#device-connection-status').text(statusText).css('color', statusColor);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for SDK to load
|
// Wait for SDK to load
|
||||||
function waitForTwilioSDK(callback) {
|
function waitForTwilioSDK(callback) {
|
||||||
if (typeof Twilio !== 'undefined' && Twilio.Device) {
|
if (typeof Twilio !== 'undefined' && Twilio.Device) {
|
||||||
@@ -7440,8 +7675,21 @@ class TWP_Admin {
|
|||||||
|
|
||||||
// Initialize the browser phone
|
// Initialize the browser phone
|
||||||
function initializeBrowserPhone() {
|
function initializeBrowserPhone() {
|
||||||
|
debugLog('initializeBrowserPhone called');
|
||||||
$('#phone-status').text('Initializing...');
|
$('#phone-status').text('Initializing...');
|
||||||
|
updateConnectionStatus('connecting');
|
||||||
|
|
||||||
|
// Initialize audio and PWA features
|
||||||
|
setupRingtone();
|
||||||
|
registerServiceWorker();
|
||||||
|
setupPageVisibility();
|
||||||
|
|
||||||
|
// Initialize AudioContext on first user interaction
|
||||||
|
$(document).one('click touchstart', function() {
|
||||||
|
console.log('User interaction detected, initializing AudioContext');
|
||||||
|
initializeAudioContext();
|
||||||
|
});
|
||||||
|
|
||||||
// Wait for SDK before proceeding
|
// Wait for SDK before proceeding
|
||||||
waitForTwilioSDK(function() {
|
waitForTwilioSDK(function() {
|
||||||
// Get capability token (access token for v2)
|
// Get capability token (access token for v2)
|
||||||
@@ -7459,9 +7707,11 @@ class TWP_Admin {
|
|||||||
// WordPress wp_send_json_error sends the error message as response.data
|
// WordPress wp_send_json_error sends the error message as response.data
|
||||||
var errorMsg = response.data || response.error || 'Unknown error';
|
var errorMsg = response.data || response.error || 'Unknown error';
|
||||||
showError('Failed to initialize: ' + errorMsg);
|
showError('Failed to initialize: ' + errorMsg);
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
}
|
}
|
||||||
}).fail(function() {
|
}).fail(function() {
|
||||||
showError('Failed to connect to server');
|
showError('Failed to connect to server');
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -7503,45 +7753,91 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupTwilioDevice(token) {
|
async function setupTwilioDevice(token) {
|
||||||
|
debugLog('setupTwilioDevice called');
|
||||||
try {
|
try {
|
||||||
// Check if Twilio SDK is available
|
// Check if Twilio SDK is available
|
||||||
|
debugLog('Twilio check: ' + (typeof Twilio) + ', Device: ' + (typeof Twilio !== 'undefined' ? typeof Twilio.Device : 'N/A'));
|
||||||
if (typeof Twilio === 'undefined' || !Twilio.Device) {
|
if (typeof Twilio === 'undefined' || !Twilio.Device) {
|
||||||
throw new Error('Twilio Voice SDK not loaded');
|
throw new Error('Twilio Voice SDK not loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Setting up Twilio Device...');
|
||||||
|
debugLog('Creating Twilio.Device...');
|
||||||
|
updateConnectionStatus('connecting');
|
||||||
|
|
||||||
// Request media permissions before setting up device
|
// Request media permissions before setting up device
|
||||||
const hasPermissions = await requestMediaPermissions();
|
const hasPermissions = await requestMediaPermissions();
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
return; // Stop setup if permissions denied
|
return; // Stop setup if permissions denied
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up existing device if any
|
// Clean up existing device if any
|
||||||
if (device) {
|
if (device) {
|
||||||
|
console.log('Destroying existing device');
|
||||||
await device.destroy();
|
await device.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if we're on Android/mobile for specific settings
|
||||||
|
var isAndroid = /Android/i.test(navigator.userAgent);
|
||||||
|
var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
console.log('Device detection - Android:', isAndroid, 'Mobile:', isMobile);
|
||||||
|
|
||||||
|
// Android-specific audio constraints for better WebRTC performance
|
||||||
|
var audioConstraints = {
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true,
|
||||||
|
autoGainControl: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Additional Android-specific settings
|
||||||
|
if (isAndroid) {
|
||||||
|
audioConstraints.googEchoCancellation = true;
|
||||||
|
audioConstraints.googNoiseSuppression = true;
|
||||||
|
audioConstraints.googAutoGainControl = true;
|
||||||
|
audioConstraints.googHighpassFilter = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup Twilio Voice SDK v2 Device
|
// Setup Twilio Voice SDK v2 Device
|
||||||
// Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device
|
// Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device
|
||||||
device = new Twilio.Device(token, {
|
device = new Twilio.Device(token, {
|
||||||
logLevel: 1, // 0 = TRACE, 1 = DEBUG
|
logLevel: 1, // 0 = TRACE, 1 = DEBUG
|
||||||
codecPreferences: ['opus', 'pcmu'],
|
codecPreferences: ['opus', 'pcmu'],
|
||||||
edge: 'sydney' // Or closest edge location
|
edge: '<?php echo esc_js(get_option('twp_twilio_edge', 'roaming')); ?>',
|
||||||
|
enableIceRestart: true, // Important for mobile network switching
|
||||||
|
audioConstraints: audioConstraints,
|
||||||
|
maxCallSignalingTimeoutMs: 30000, // 30 seconds timeout for mobile
|
||||||
|
closeProtection: true // Warn before closing during call
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Twilio Device created with audio constraints:', audioConstraints);
|
||||||
|
debugLog('Device created, setting up handlers...');
|
||||||
|
|
||||||
// Set up event handlers BEFORE registering
|
// Set up event handlers BEFORE registering
|
||||||
// Device registered and ready
|
// Device registered and ready
|
||||||
device.on('registered', function() {
|
device.on('registered', function() {
|
||||||
console.log('Device registered successfully');
|
console.log('Device registered successfully');
|
||||||
|
debugLog('Device REGISTERED!');
|
||||||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||||||
$('#call-btn').prop('disabled', false);
|
$('#call-btn').prop('disabled', false);
|
||||||
|
updateConnectionStatus('connected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Device unregistered
|
||||||
|
device.on('unregistered', function() {
|
||||||
|
console.log('Device unregistered');
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
device.on('error', function(error) {
|
device.on('error', function(error) {
|
||||||
console.error('Twilio Device Error:', error);
|
console.error('Twilio Device Error:', error);
|
||||||
|
console.error('Error code:', error.code, 'Message:', error.message);
|
||||||
|
updateConnectionStatus('disconnected');
|
||||||
|
|
||||||
var errorMsg = error.message || error.toString();
|
var errorMsg = error.message || error.toString();
|
||||||
|
|
||||||
// Provide specific help for common errors
|
// Provide specific help for common errors
|
||||||
if (errorMsg.includes('valid callerId must be provided')) {
|
if (errorMsg.includes('valid callerId must be provided')) {
|
||||||
errorMsg = 'Caller ID error: Make sure you select a verified Twilio phone number as Caller ID. The number must be purchased through your Twilio account.';
|
errorMsg = 'Caller ID error: Make sure you select a verified Twilio phone number as Caller ID. The number must be purchased through your Twilio account.';
|
||||||
@@ -7551,23 +7847,48 @@ class TWP_Admin {
|
|||||||
errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.';
|
errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.';
|
||||||
// Try to reinitialize after token error
|
// Try to reinitialize after token error
|
||||||
setTimeout(initializeBrowserPhone, 5000);
|
setTimeout(initializeBrowserPhone, 5000);
|
||||||
|
} else if (errorMsg.includes('31005') || errorMsg.includes('Connection error')) {
|
||||||
|
errorMsg = 'Connection error: Check your internet connection. If on mobile, try switching between WiFi and cellular data.';
|
||||||
|
// Retry connection
|
||||||
|
setTimeout(function() {
|
||||||
|
if (device) {
|
||||||
|
console.log('Attempting to reconnect device...');
|
||||||
|
device.register();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
showError(errorMsg);
|
showError(errorMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle incoming calls
|
// Handle incoming calls
|
||||||
device.on('incoming', function(call) {
|
device.on('incoming', function(call) {
|
||||||
|
console.log('Incoming call from:', call.parameters.From);
|
||||||
|
console.log('Call SID:', call.parameters.CallSid);
|
||||||
|
console.log('Device connection state:', deviceConnectionState);
|
||||||
|
|
||||||
currentCall = call;
|
currentCall = call;
|
||||||
|
var callerNumber = call.parameters.From || 'Unknown Number';
|
||||||
|
|
||||||
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
|
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
|
||||||
$('#phone-number-display').text(call.parameters.From || 'Unknown Number');
|
$('#phone-number-display').text(callerNumber);
|
||||||
$('#call-btn').hide();
|
$('#call-btn').hide();
|
||||||
$('#answer-btn').show();
|
$('#answer-btn').show();
|
||||||
|
|
||||||
|
// Play ringtone and show notification
|
||||||
|
playRingtone();
|
||||||
|
|
||||||
|
// If page is in background, send notification
|
||||||
|
if (!isPageVisible) {
|
||||||
|
console.log('Page in background, sending notification');
|
||||||
|
sendIncomingCallNotification(callerNumber);
|
||||||
|
}
|
||||||
|
|
||||||
// Setup call event handlers
|
// Setup call event handlers
|
||||||
setupCallHandlers(call);
|
setupCallHandlers(call);
|
||||||
|
|
||||||
if ($('#auto-answer').is(':checked')) {
|
if ($('#auto-answer').is(':checked')) {
|
||||||
|
console.log('Auto-answer enabled, accepting call');
|
||||||
call.accept();
|
call.accept();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -7579,10 +7900,13 @@ class TWP_Admin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register device AFTER setting up event handlers
|
// Register device AFTER setting up event handlers
|
||||||
|
debugLog('Calling device.register()...');
|
||||||
await device.register();
|
await device.register();
|
||||||
|
debugLog('device.register() completed');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up Twilio Device:', error);
|
console.error('Error setting up Twilio Device:', error);
|
||||||
|
debugLog('ERROR: ' + error.message);
|
||||||
showError('Failed to setup device: ' + error.message);
|
showError('Failed to setup device: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7590,6 +7914,8 @@ class TWP_Admin {
|
|||||||
function setupCallHandlers(call) {
|
function setupCallHandlers(call) {
|
||||||
// Call accepted/connected
|
// Call accepted/connected
|
||||||
call.on('accept', function() {
|
call.on('accept', function() {
|
||||||
|
console.log('Call accepted and connected');
|
||||||
|
stopRingtone();
|
||||||
$('#phone-status').text('Connected').css('color', '#2196F3');
|
$('#phone-status').text('Connected').css('color', '#2196F3');
|
||||||
$('#call-btn').hide();
|
$('#call-btn').hide();
|
||||||
$('#answer-btn').hide();
|
$('#answer-btn').hide();
|
||||||
@@ -7598,9 +7924,11 @@ class TWP_Admin {
|
|||||||
$('#admin-call-controls-panel').show();
|
$('#admin-call-controls-panel').show();
|
||||||
startCallTimer();
|
startCallTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call disconnected
|
// Call disconnected
|
||||||
call.on('disconnect', function() {
|
call.on('disconnect', function() {
|
||||||
|
console.log('Call disconnected');
|
||||||
|
stopRingtone();
|
||||||
currentCall = null;
|
currentCall = null;
|
||||||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||||||
$('#hangup-btn').hide();
|
$('#hangup-btn').hide();
|
||||||
@@ -7610,22 +7938,26 @@ class TWP_Admin {
|
|||||||
$('#admin-call-controls-panel').hide();
|
$('#admin-call-controls-panel').hide();
|
||||||
$('#call-timer').hide();
|
$('#call-timer').hide();
|
||||||
stopCallTimer();
|
stopCallTimer();
|
||||||
|
|
||||||
// Reset button states
|
// Reset button states
|
||||||
$('#admin-hold-btn').text('Hold').removeClass('btn-active');
|
$('#admin-hold-btn').text('Hold').removeClass('btn-active');
|
||||||
$('#admin-record-btn').text('Record').removeClass('btn-active');
|
$('#admin-record-btn').text('Record').removeClass('btn-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call rejected
|
// Call rejected
|
||||||
call.on('reject', function() {
|
call.on('reject', function() {
|
||||||
|
console.log('Call rejected');
|
||||||
|
stopRingtone();
|
||||||
currentCall = null;
|
currentCall = null;
|
||||||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||||||
$('#answer-btn').hide();
|
$('#answer-btn').hide();
|
||||||
$('#call-btn').show();
|
$('#call-btn').show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call cancelled (by caller before answer)
|
// Call cancelled (by caller before answer)
|
||||||
call.on('cancel', function() {
|
call.on('cancel', function() {
|
||||||
|
console.log('Call cancelled by caller');
|
||||||
|
stopRingtone();
|
||||||
currentCall = null;
|
currentCall = null;
|
||||||
$('#phone-status').text('Missed Call').css('color', '#FF9800');
|
$('#phone-status').text('Missed Call').css('color', '#FF9800');
|
||||||
$('#answer-btn').hide();
|
$('#answer-btn').hide();
|
||||||
@@ -7634,6 +7966,26 @@ class TWP_Admin {
|
|||||||
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
$('#phone-status').text('Ready').css('color', '#4CAF50');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Call error
|
||||||
|
call.on('error', function(error) {
|
||||||
|
console.error('Call error:', error);
|
||||||
|
console.error('Error code:', error.code, 'Message:', error.message);
|
||||||
|
stopRingtone();
|
||||||
|
|
||||||
|
var errorMsg = error.message || error.toString();
|
||||||
|
|
||||||
|
// Specific error handling for Android/mobile
|
||||||
|
if (error.code === 31005) {
|
||||||
|
errorMsg = 'Connection failed: Check your network connection. Try switching between WiFi and cellular data.';
|
||||||
|
} else if (error.code === 31201 || error.code === 31204) {
|
||||||
|
errorMsg = 'Call setup failed: Please try again. If the problem persists, refresh the page.';
|
||||||
|
} else if (error.code === 31208) {
|
||||||
|
errorMsg = 'Media connection failed: Check microphone permissions and try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showError('Call error: ' + errorMsg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshToken() {
|
function refreshToken() {
|
||||||
@@ -7744,11 +8096,15 @@ class TWP_Admin {
|
|||||||
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
|
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dialpad functionality
|
// Dialpad functionality (support both click and touch events)
|
||||||
$('.dialpad-btn').on('click', function() {
|
$('.dialpad-btn').on('click touchend', function(e) {
|
||||||
|
e.preventDefault(); // Prevent duplicate events
|
||||||
var digit = $(this).data('digit');
|
var digit = $(this).data('digit');
|
||||||
var currentVal = $('#phone-number-input').val();
|
var currentVal = $('#phone-number-input').val();
|
||||||
$('#phone-number-input').val(currentVal + digit);
|
$('#phone-number-input').val(currentVal + digit);
|
||||||
|
|
||||||
|
// Initialize AudioContext on user interaction (mobile requirement)
|
||||||
|
initializeAudioContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call button
|
// Call button
|
||||||
@@ -7809,8 +8165,45 @@ class TWP_Admin {
|
|||||||
|
|
||||||
// Answer button
|
// Answer button
|
||||||
$('#answer-btn').on('click', function() {
|
$('#answer-btn').on('click', function() {
|
||||||
if (currentCall) {
|
console.log('Answer button clicked');
|
||||||
|
console.log('Device connection state:', deviceConnectionState);
|
||||||
|
console.log('Current call:', currentCall);
|
||||||
|
|
||||||
|
if (!currentCall) {
|
||||||
|
console.error('No current call to answer');
|
||||||
|
showError('No incoming call to answer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check device connection state
|
||||||
|
if (deviceConnectionState !== 'connected') {
|
||||||
|
console.error('Device not connected, state:', deviceConnectionState);
|
||||||
|
showError('Phone not connected. Reconnecting...');
|
||||||
|
|
||||||
|
// Try to reconnect
|
||||||
|
if (device) {
|
||||||
|
device.register().then(function() {
|
||||||
|
console.log('Device reconnected, answering call');
|
||||||
|
if (currentCall) {
|
||||||
|
currentCall.accept();
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to reconnect device:', err);
|
||||||
|
showError('Failed to reconnect. Please refresh the page.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize AudioContext before accepting (important for mobile)
|
||||||
|
initializeAudioContext();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Accepting call...');
|
||||||
currentCall.accept();
|
currentCall.accept();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error accepting call:', error);
|
||||||
|
showError('Failed to answer call: ' + error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7849,14 +8242,28 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Debug helper
|
||||||
|
function debugLog(msg) {
|
||||||
|
console.log('TWP Debug: ' + msg);
|
||||||
|
var debugEl = $('#twp-debug-info');
|
||||||
|
if (debugEl.length) {
|
||||||
|
debugEl.append(msg + '<br>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if SDK loaded and initialize
|
// Check if SDK loaded and initialize
|
||||||
|
debugLog('jQuery ready');
|
||||||
$(window).on('load', function() {
|
$(window).on('load', function() {
|
||||||
|
debugLog('Window loaded');
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
debugLog('Checking Twilio: ' + (typeof Twilio));
|
||||||
if (typeof Twilio === 'undefined') {
|
if (typeof Twilio === 'undefined') {
|
||||||
showError('Twilio Voice SDK failed to load. Please check your internet connection and try refreshing the page.');
|
showError('Twilio Voice SDK failed to load. Please check your internet connection and try refreshing the page.');
|
||||||
console.error('Twilio SDK not found. Script may be blocked or failed to load.');
|
console.error('Twilio SDK not found. Script may be blocked or failed to load.');
|
||||||
|
debugLog('SDK FAILED');
|
||||||
} else {
|
} else {
|
||||||
console.log('Twilio SDK loaded successfully');
|
console.log('Twilio SDK loaded successfully');
|
||||||
|
debugLog('SDK OK, initializing...');
|
||||||
initializeBrowserPhone();
|
initializeBrowserPhone();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -10049,5 +10456,12 @@ class TWP_Admin {
|
|||||||
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
|
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mobile app settings page
|
||||||
|
*/
|
||||||
|
public function display_mobile_app_settings() {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'admin/mobile-app-settings.php';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
353
admin/mobile-app-settings.php
Normal file
353
admin/mobile-app-settings.php
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile App Settings Page
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('You do not have sufficient permissions to access this page.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle manual update check
|
||||||
|
if (isset($_POST['twp_check_updates']) && check_admin_referer('twp_mobile_settings')) {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$update_result = $updater->manual_check_for_updates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle test notification
|
||||||
|
if (isset($_POST['twp_test_notification']) && check_admin_referer('twp_mobile_settings')) {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
|
||||||
|
$fcm = new TWP_FCM();
|
||||||
|
$test_user_id = get_current_user_id();
|
||||||
|
$notification_sent = $fcm->send_test_notification($test_user_id);
|
||||||
|
|
||||||
|
if ($notification_sent) {
|
||||||
|
$notification_result = array('success' => true, 'message' => 'Test notification sent successfully!');
|
||||||
|
} else {
|
||||||
|
$notification_result = array('success' => false, 'message' => 'Failed to send test notification. Check FCM configuration.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 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_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_token', sanitize_text_field($_POST['twp_gitea_token']));
|
||||||
|
|
||||||
|
$settings_saved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current settings
|
||||||
|
$fcm_server_key = get_option('twp_fcm_server_key', '');
|
||||||
|
$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
|
||||||
|
$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
|
||||||
|
$gitea_token = get_option('twp_gitea_token', '');
|
||||||
|
|
||||||
|
// Get update status
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$update_status = $updater->get_update_status();
|
||||||
|
|
||||||
|
// Get mobile app statistics
|
||||||
|
global $wpdb;
|
||||||
|
$sessions_table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
$active_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table WHERE is_active = 1 AND expires_at > NOW()");
|
||||||
|
$total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||||
|
|
||||||
|
<?php if (isset($settings_saved)): ?>
|
||||||
|
<div class="notice notice-success is-dismissible">
|
||||||
|
<p><strong>Settings saved successfully!</strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($update_result)): ?>
|
||||||
|
<div class="notice notice-<?php echo $update_result['update_available'] ? 'warning' : 'success'; ?> is-dismissible">
|
||||||
|
<p><strong><?php echo esc_html($update_result['message']); ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($notification_result)): ?>
|
||||||
|
<div class="notice notice-<?php echo $notification_result['success'] ? 'success' : 'error'; ?> is-dismissible">
|
||||||
|
<p><strong><?php echo esc_html($notification_result['message']); ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="twp-mobile-settings">
|
||||||
|
<!-- Mobile App Overview -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Mobile App Overview</h2>
|
||||||
|
<table class="widefat">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>API Endpoint:</strong></td>
|
||||||
|
<td><code><?php echo esc_html(site_url('/wp-json/twilio-mobile/v1')); ?></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Active Sessions:</strong></td>
|
||||||
|
<td><?php echo esc_html($active_sessions); ?> active / <?php echo esc_html($total_sessions); ?> total</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Plugin Version:</strong></td>
|
||||||
|
<td><?php echo esc_html(TWP_VERSION); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile App Settings Form -->
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('twp_mobile_settings'); ?>
|
||||||
|
|
||||||
|
<!-- FCM Configuration -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Firebase Cloud Messaging (FCM)</h2>
|
||||||
|
<p>Configure FCM to enable push notifications for the mobile app.</p>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_fcm_server_key">FCM Server Key</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
id="twp_fcm_server_key"
|
||||||
|
name="twp_fcm_server_key"
|
||||||
|
value="<?php echo esc_attr($fcm_server_key); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="AAAA...">
|
||||||
|
<p class="description">
|
||||||
|
Get your server key from Firebase Console > Project Settings > Cloud Messaging > Server Key
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if (!empty($fcm_server_key)): ?>
|
||||||
|
<p>
|
||||||
|
<button type="submit" name="twp_test_notification" class="button">
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
<span class="description">Send a test notification to your devices</span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-Update Settings -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Automatic Updates</h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Current Version</th>
|
||||||
|
<td>
|
||||||
|
<strong><?php echo esc_html($update_status['current_version']); ?></strong>
|
||||||
|
<?php if ($update_status['update_available']): ?>
|
||||||
|
<span style="color: #d63638; margin-left: 10px;">
|
||||||
|
⚠ Update available: <?php echo esc_html($update_status['latest_version']); ?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: #00a32a; margin-left: 10px;">✓ Up to date</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_auto_update_enabled">Enable Auto-Updates</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
id="twp_auto_update_enabled"
|
||||||
|
name="twp_auto_update_enabled"
|
||||||
|
value="1"
|
||||||
|
<?php checked($auto_update_enabled); ?>>
|
||||||
|
Automatically check for updates every 12 hours
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_gitea_repo">Gitea Repository</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
id="twp_gitea_repo"
|
||||||
|
name="twp_gitea_repo"
|
||||||
|
value="<?php echo esc_attr($gitea_repo); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="org/repo-name">
|
||||||
|
<p class="description">
|
||||||
|
Format: organization/repository (e.g., wp-plugins/twilio-wp-plugin)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_gitea_token">Gitea Access Token</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="password"
|
||||||
|
id="twp_gitea_token"
|
||||||
|
name="twp_gitea_token"
|
||||||
|
value="<?php echo esc_attr($gitea_token); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="">
|
||||||
|
<p class="description">
|
||||||
|
Optional. Required only for private repositories. Create token at:
|
||||||
|
<a href="https://repo.anhonesthost.net/user/settings/applications" target="_blank">Gitea Settings > Applications</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Last Update Check</th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$last_check = $update_status['last_check'];
|
||||||
|
if ($last_check > 0) {
|
||||||
|
echo esc_html(human_time_diff($last_check, current_time('timestamp')) . ' ago');
|
||||||
|
} else {
|
||||||
|
echo 'Never';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<button type="submit" name="twp_check_updates" class="button" style="margin-left: 15px;">
|
||||||
|
Check Now
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Documentation -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<p>Available REST API endpoints for mobile app development:</p>
|
||||||
|
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Endpoint</th>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/auth/login</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Authenticate and get JWT tokens</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/auth/refresh</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Refresh access token</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/agent/status</code></td>
|
||||||
|
<td>GET/POST</td>
|
||||||
|
<td>Get or update agent status</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/queues/state</code></td>
|
||||||
|
<td>GET</td>
|
||||||
|
<td>Get all queue states</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/calls/{call_sid}/accept</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Accept a queued call</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/stream/events</code></td>
|
||||||
|
<td>GET</td>
|
||||||
|
<td>Server-Sent Events stream for real-time updates</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px;">
|
||||||
|
<strong>Authentication:</strong> All endpoints (except login/refresh) require
|
||||||
|
<code>Authorization: Bearer <access_token></code> header.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<button type="submit" name="twp_save_mobile_settings" class="button button-primary">
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Active Sessions -->
|
||||||
|
<?php if ($active_sessions > 0): ?>
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Active Mobile Sessions</h2>
|
||||||
|
<?php
|
||||||
|
$sessions = $wpdb->get_results("
|
||||||
|
SELECT s.user_id, s.device_info, s.logged_in_at, s.last_used, u.user_login, u.display_name
|
||||||
|
FROM $sessions_table s
|
||||||
|
JOIN {$wpdb->users} u ON s.user_id = u.ID
|
||||||
|
WHERE s.is_active = 1 AND s.expires_at > NOW()
|
||||||
|
ORDER BY s.last_used DESC
|
||||||
|
LIMIT 20
|
||||||
|
");
|
||||||
|
?>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Last Activity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($sessions as $session): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html($session->display_name ?: $session->user_login); ?></td>
|
||||||
|
<td><?php echo esc_html($session->device_info ?: 'Unknown device'); ?></td>
|
||||||
|
<td><?php echo esc_html(human_time_diff(strtotime($session->last_used), current_time('timestamp')) . ' ago'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.twp-mobile-settings .card {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings .card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings code {
|
||||||
|
background: #f0f0f1;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings table.widefat td code {
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
assets/images/README.md
Normal file
42
assets/images/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Browser Phone Images
|
||||||
|
|
||||||
|
## Required Images for PWA Notifications
|
||||||
|
|
||||||
|
Place the following image files in this directory for browser push notifications:
|
||||||
|
|
||||||
|
### phone-icon.png
|
||||||
|
- **Purpose**: Main notification icon
|
||||||
|
- **Size**: 192x192 pixels (recommended)
|
||||||
|
- **Format**: PNG with transparency
|
||||||
|
- **Usage**: Shown in browser notifications for incoming calls
|
||||||
|
|
||||||
|
### phone-badge.png
|
||||||
|
- **Purpose**: Small badge icon for notifications
|
||||||
|
- **Size**: 96x96 pixels or 72x72 pixels
|
||||||
|
- **Format**: PNG with transparency
|
||||||
|
- **Usage**: Displayed in the notification badge area (Android)
|
||||||
|
|
||||||
|
## Fallback Behavior
|
||||||
|
|
||||||
|
If these images are not present, the browser will:
|
||||||
|
- Use a default browser notification icon
|
||||||
|
- Still display text-based notifications
|
||||||
|
- Functionality will not be affected
|
||||||
|
|
||||||
|
## Image Creation Tips
|
||||||
|
|
||||||
|
- Use a simple, recognizable phone icon
|
||||||
|
- Ensure good contrast for visibility
|
||||||
|
- Test on both light and dark backgrounds
|
||||||
|
- Transparent backgrounds work best
|
||||||
|
- Use a consistent color scheme with your brand
|
||||||
|
|
||||||
|
## Example Resources
|
||||||
|
|
||||||
|
- Google Material Icons: https://fonts.google.com/icons
|
||||||
|
- Font Awesome: https://fontawesome.com/
|
||||||
|
- Flaticon: https://www.flaticon.com/
|
||||||
|
- Or create custom icons using tools like:
|
||||||
|
- Figma
|
||||||
|
- Adobe Illustrator
|
||||||
|
- Inkscape (free)
|
||||||
@@ -72,7 +72,23 @@ self.addEventListener('push', function(event) {
|
|||||||
|
|
||||||
// Handle messages from the main script
|
// Handle messages from the main script
|
||||||
self.addEventListener('message', function(event) {
|
self.addEventListener('message', function(event) {
|
||||||
|
console.log('TWP Service Worker: Message received', event.data);
|
||||||
|
|
||||||
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
|
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
|
||||||
self.registration.showNotification(event.data.title, event.data.options);
|
const options = {
|
||||||
|
body: event.data.body || 'You have a new call waiting',
|
||||||
|
icon: event.data.icon || '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-icon.png',
|
||||||
|
badge: '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-badge.png',
|
||||||
|
vibrate: [300, 200, 300, 200, 300],
|
||||||
|
tag: event.data.tag || 'incoming-call',
|
||||||
|
requireInteraction: event.data.requireInteraction !== undefined ? event.data.requireInteraction : true,
|
||||||
|
data: event.data.data || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('TWP Service Worker: Showing notification', event.data.title, options);
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(event.data.title || 'Incoming Call', options)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
36
assets/sounds/README.md
Normal file
36
assets/sounds/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Ringtone Audio Files
|
||||||
|
|
||||||
|
## Custom Ringtone
|
||||||
|
|
||||||
|
To add a custom ringtone for incoming calls in the browser phone:
|
||||||
|
|
||||||
|
1. Place your ringtone audio file in this directory
|
||||||
|
2. Name it: `ringtone.mp3`
|
||||||
|
3. Recommended format: MP3, under 5 seconds, loopable
|
||||||
|
|
||||||
|
## Fallback Behavior
|
||||||
|
|
||||||
|
If no ringtone file is present, the browser phone will:
|
||||||
|
- Use device vibration on mobile devices (Android, iOS)
|
||||||
|
- Rely on browser notifications to alert users
|
||||||
|
- Display visual indicators in the browser interface
|
||||||
|
|
||||||
|
## Supported Audio Formats
|
||||||
|
|
||||||
|
- **MP3** (recommended) - Best compatibility across browsers
|
||||||
|
- **OGG** - Good for Firefox, Chrome
|
||||||
|
- **WAV** - Larger file size but universal support
|
||||||
|
|
||||||
|
## File Requirements
|
||||||
|
|
||||||
|
- Maximum file size: 100KB recommended
|
||||||
|
- Duration: 1-5 seconds (will loop)
|
||||||
|
- Sample rate: 44.1kHz or 48kHz
|
||||||
|
- Bitrate: 128kbps or higher
|
||||||
|
|
||||||
|
## Example Ringtone Sources
|
||||||
|
|
||||||
|
You can find free ringtone files at:
|
||||||
|
- https://freesound.org/
|
||||||
|
- https://incompetech.com/
|
||||||
|
- Or create your own using Audacity or similar tools
|
||||||
@@ -52,7 +52,8 @@ class TWP_Activator {
|
|||||||
'twp_callbacks',
|
'twp_callbacks',
|
||||||
'twp_call_recordings',
|
'twp_call_recordings',
|
||||||
'twp_user_extensions',
|
'twp_user_extensions',
|
||||||
'twp_queue_assignments'
|
'twp_queue_assignments',
|
||||||
|
'twp_mobile_sessions'
|
||||||
);
|
);
|
||||||
|
|
||||||
$missing_tables = array();
|
$missing_tables = array();
|
||||||
@@ -361,7 +362,25 @@ class TWP_Activator {
|
|||||||
KEY agent_id (agent_id),
|
KEY agent_id (agent_id),
|
||||||
KEY started_at (started_at)
|
KEY started_at (started_at)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Mobile sessions table
|
||||||
|
$table_mobile_sessions = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
$sql_mobile_sessions = "CREATE TABLE $table_mobile_sessions (
|
||||||
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id bigint(20) NOT NULL,
|
||||||
|
refresh_token varchar(500) NOT NULL,
|
||||||
|
fcm_token text,
|
||||||
|
device_info text,
|
||||||
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at datetime NOT NULL,
|
||||||
|
last_used datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_active tinyint(1) DEFAULT 1,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY user_id (user_id),
|
||||||
|
KEY is_active (is_active),
|
||||||
|
KEY expires_at (expires_at)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
dbDelta($sql_schedules);
|
dbDelta($sql_schedules);
|
||||||
dbDelta($sql_queues);
|
dbDelta($sql_queues);
|
||||||
dbDelta($sql_queued_calls);
|
dbDelta($sql_queued_calls);
|
||||||
@@ -377,7 +396,8 @@ class TWP_Activator {
|
|||||||
dbDelta($sql_recordings);
|
dbDelta($sql_recordings);
|
||||||
dbDelta($sql_user_extensions);
|
dbDelta($sql_user_extensions);
|
||||||
dbDelta($sql_queue_assignments);
|
dbDelta($sql_queue_assignments);
|
||||||
|
dbDelta($sql_mobile_sessions);
|
||||||
|
|
||||||
// Add missing columns for existing installations
|
// Add missing columns for existing installations
|
||||||
self::add_missing_columns();
|
self::add_missing_columns();
|
||||||
}
|
}
|
||||||
|
|||||||
291
includes/class-twp-auto-updater.php
Normal file
291
includes/class-twp-auto-updater.php
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Automatic Plugin Updater
|
||||||
|
*
|
||||||
|
* Checks for updates from Gitea repository and installs them automatically
|
||||||
|
*/
|
||||||
|
class TWP_Auto_Updater {
|
||||||
|
|
||||||
|
private $plugin_slug = 'twilio-wp-plugin';
|
||||||
|
private $plugin_basename;
|
||||||
|
private $gitea_repo = 'wp-plugins/twilio-wp-plugin';
|
||||||
|
private $gitea_api_url;
|
||||||
|
private $current_version;
|
||||||
|
private $gitea_base_url = 'https://repo.anhonesthost.net';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->plugin_basename = plugin_basename(dirname(dirname(__FILE__)) . '/twilio-wp-plugin.php');
|
||||||
|
$this->current_version = defined('TWP_VERSION') ? TWP_VERSION : '0.0.0';
|
||||||
|
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize updater hooks
|
||||||
|
*/
|
||||||
|
public function init() {
|
||||||
|
// Hook into WordPress update checks
|
||||||
|
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update'));
|
||||||
|
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
|
||||||
|
|
||||||
|
// Add settings page for manual check
|
||||||
|
add_action('admin_init', array($this, 'register_settings'));
|
||||||
|
|
||||||
|
// Add update check to admin notices
|
||||||
|
if (get_option('twp_auto_update_enabled', '1') === '1') {
|
||||||
|
add_action('admin_init', array($this, 'maybe_auto_check_updates'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register auto-update settings
|
||||||
|
*/
|
||||||
|
public function register_settings() {
|
||||||
|
register_setting('twp_settings', 'twp_auto_update_enabled');
|
||||||
|
register_setting('twp_settings', 'twp_gitea_repo');
|
||||||
|
register_setting('twp_settings', 'twp_gitea_token'); // Optional for private repos
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for updates periodically
|
||||||
|
*/
|
||||||
|
public function maybe_auto_check_updates() {
|
||||||
|
$last_check = get_option('twp_last_update_check', 0);
|
||||||
|
$check_interval = 12 * HOUR_IN_SECONDS; // Check every 12 hours
|
||||||
|
|
||||||
|
if (time() - $last_check > $check_interval) {
|
||||||
|
update_option('twp_last_update_check', time());
|
||||||
|
// Force WordPress to check for updates
|
||||||
|
wp_clean_plugins_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for plugin updates
|
||||||
|
*/
|
||||||
|
public function check_for_update($transient) {
|
||||||
|
if (empty($transient->checked)) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Gitea repo from settings if available
|
||||||
|
$custom_repo = get_option('twp_gitea_repo', '');
|
||||||
|
if (!empty($custom_repo)) {
|
||||||
|
$this->gitea_repo = $custom_repo;
|
||||||
|
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if ($update_info && version_compare($this->current_version, $update_info->version, '<')) {
|
||||||
|
$plugin_data = array(
|
||||||
|
'id' => 'twilio-wp-plugin',
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'plugin' => $this->plugin_basename,
|
||||||
|
'new_version' => $update_info->version,
|
||||||
|
'url' => $update_info->homepage,
|
||||||
|
'package' => $update_info->download_url,
|
||||||
|
'tested' => '6.8',
|
||||||
|
'requires' => '5.8',
|
||||||
|
'requires_php' => '7.4',
|
||||||
|
'icons' => array(),
|
||||||
|
'banners' => array(),
|
||||||
|
'compatibility' => new stdClass(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$transient->response[$this->plugin_basename] = (object) $plugin_data;
|
||||||
|
|
||||||
|
error_log("TWP Auto-Updater: New version {$update_info->version} available (current: {$this->current_version})");
|
||||||
|
} else {
|
||||||
|
error_log("TWP Auto-Updater: No updates available (current: {$this->current_version})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin information for update screen
|
||||||
|
*/
|
||||||
|
public function plugin_info($false, $action, $args) {
|
||||||
|
if ($action !== 'plugin_information') {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($args->slug) || $args->slug !== $this->plugin_slug) {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if (!$update_info) {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_info = new stdClass();
|
||||||
|
$plugin_info->name = 'Twilio WP Plugin';
|
||||||
|
$plugin_info->slug = $this->plugin_slug;
|
||||||
|
$plugin_info->version = $update_info->version;
|
||||||
|
$plugin_info->author = '<a href="https://cybercove.io/">Joshua Knapp</a>';
|
||||||
|
$plugin_info->homepage = $update_info->homepage;
|
||||||
|
$plugin_info->download_link = $update_info->download_url;
|
||||||
|
$plugin_info->requires = '5.8';
|
||||||
|
$plugin_info->tested = '6.8';
|
||||||
|
$plugin_info->requires_php = '7.4';
|
||||||
|
$plugin_info->last_updated = $update_info->release_date;
|
||||||
|
$plugin_info->downloaded = 10;
|
||||||
|
|
||||||
|
$plugin_info->sections = array(
|
||||||
|
'description' => '<p>Twilio WordPress Plugin for call management and mobile app support.</p>',
|
||||||
|
'changelog' => '<pre>' . esc_html($update_info->changelog) . '</pre>',
|
||||||
|
'installation' => '<p>Upload the plugin to your WordPress site and activate it.</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $plugin_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest release information from Gitea
|
||||||
|
*/
|
||||||
|
private function get_latest_release() {
|
||||||
|
// Check cache first (1 hour)
|
||||||
|
$cache_key = 'twp_latest_release';
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cURL for Gitea API
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $this->gitea_api_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
|
||||||
|
curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Twilio-WP-Plugin-Updater');
|
||||||
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||||
|
|
||||||
|
// Add Gitea token if configured (for private repos)
|
||||||
|
$gitea_token = get_option('twp_gitea_token', '');
|
||||||
|
if (!empty($gitea_token)) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||||
|
'Accept: application/json',
|
||||||
|
'Authorization: token ' . $gitea_token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if (!$response || $http_code !== 200) {
|
||||||
|
error_log("TWP Auto-Updater: Gitea API returned status $http_code");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = json_decode($response);
|
||||||
|
|
||||||
|
if (!$release || !isset($release->tag_name)) {
|
||||||
|
error_log('TWP Auto-Updater: Invalid release data from Gitea');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse release information
|
||||||
|
$version = ltrim($release->tag_name, 'v'); // Remove 'v' prefix if present
|
||||||
|
$download_url = null;
|
||||||
|
|
||||||
|
// Find the zip asset
|
||||||
|
if (isset($release->assets) && is_array($release->assets)) {
|
||||||
|
foreach ($release->assets as $asset) {
|
||||||
|
if (strpos($asset->name, '.zip') !== false) {
|
||||||
|
$download_url = $asset->browser_download_url;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to zipball if no asset found
|
||||||
|
if (!$download_url) {
|
||||||
|
$download_url = $release->zipball_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format changelog
|
||||||
|
$changelog = !empty($release->body) ? $release->body : 'No changelog provided for this release.';
|
||||||
|
|
||||||
|
// Handle empty changelog
|
||||||
|
if (empty(trim($changelog))) {
|
||||||
|
$changelog = "Version " . $version . "\n\n" .
|
||||||
|
"Released on " . date('F j, Y', strtotime($release->published_at)) . "\n\n" .
|
||||||
|
"* Updated plugin files";
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = (object) array(
|
||||||
|
'version' => $version,
|
||||||
|
'download_url' => $download_url,
|
||||||
|
'homepage' => $this->gitea_base_url . '/' . $this->gitea_repo,
|
||||||
|
'release_date' => $release->published_at,
|
||||||
|
'description' => $changelog,
|
||||||
|
'changelog' => $changelog
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache for 1 hour
|
||||||
|
set_transient($cache_key, $update_info, HOUR_IN_SECONDS);
|
||||||
|
|
||||||
|
return $update_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual update check (for admin page)
|
||||||
|
*/
|
||||||
|
public function manual_check_for_updates() {
|
||||||
|
// Clear cache
|
||||||
|
delete_transient('twp_latest_release');
|
||||||
|
update_option('twp_last_update_check', 0);
|
||||||
|
|
||||||
|
// Force WordPress to check
|
||||||
|
wp_clean_plugins_cache();
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if (!$update_info) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to check for updates. Please check your internet connection and Gitea repository settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare($this->current_version, $update_info->version, '<')) {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => true,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info->version,
|
||||||
|
'message' => "Update available: Version {$update_info->version}. Go to Plugins page to update."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => false,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'message' => 'You are running the latest version.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current update status
|
||||||
|
*/
|
||||||
|
public function get_update_status() {
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info ? $update_info->version : 'Unknown',
|
||||||
|
'update_available' => $update_info && version_compare($this->current_version, $update_info->version, '<'),
|
||||||
|
'last_check' => get_option('twp_last_update_check', 0),
|
||||||
|
'auto_update_enabled' => get_option('twp_auto_update_enabled', '1') === '1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,14 @@ class TWP_Core {
|
|||||||
// API classes
|
// API classes
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
|
||||||
|
|
||||||
|
// Mobile app classes
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-auth.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-api.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-sse.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
|
||||||
// Feature classes
|
// Feature classes
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
|
||||||
@@ -318,6 +325,20 @@ class TWP_Core {
|
|||||||
// Initialize webhooks
|
// Initialize webhooks
|
||||||
$webhooks = new TWP_Webhooks();
|
$webhooks = new TWP_Webhooks();
|
||||||
$webhooks->register_endpoints();
|
$webhooks->register_endpoints();
|
||||||
|
|
||||||
|
// Initialize mobile app endpoints
|
||||||
|
$mobile_auth = new TWP_Mobile_Auth();
|
||||||
|
$mobile_auth->register_endpoints();
|
||||||
|
|
||||||
|
$mobile_api = new TWP_Mobile_API();
|
||||||
|
$mobile_api->register_endpoints();
|
||||||
|
|
||||||
|
$mobile_sse = new TWP_Mobile_SSE();
|
||||||
|
$mobile_sse->register_endpoints();
|
||||||
|
|
||||||
|
// Initialize auto-updater
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$updater->init();
|
||||||
|
|
||||||
// Add custom cron schedules
|
// Add custom cron schedules
|
||||||
add_filter('cron_schedules', function($schedules) {
|
add_filter('cron_schedules', function($schedules) {
|
||||||
|
|||||||
214
includes/class-twp-fcm.php
Normal file
214
includes/class-twp-fcm.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Firebase Cloud Messaging (FCM) Integration
|
||||||
|
*
|
||||||
|
* Handles push notifications to mobile devices via FCM
|
||||||
|
*/
|
||||||
|
class TWP_FCM {
|
||||||
|
|
||||||
|
private $server_key;
|
||||||
|
private $fcm_url = 'https://fcm.googleapis.com/fcm/send';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->server_key = get_option('twp_fcm_server_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send push notification to user's devices
|
||||||
|
*/
|
||||||
|
public function send_notification($user_id, $title, $body, $data = array()) {
|
||||||
|
if (empty($this->server_key)) {
|
||||||
|
error_log('TWP FCM: Server key not configured');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's FCM tokens
|
||||||
|
$tokens = $this->get_user_tokens($user_id);
|
||||||
|
|
||||||
|
if (empty($tokens)) {
|
||||||
|
error_log("TWP FCM: No tokens found for user $user_id");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success_count = 0;
|
||||||
|
$failed_tokens = array();
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
$result = $this->send_to_token($token, $title, $body, $data);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$success_count++;
|
||||||
|
} else {
|
||||||
|
$failed_tokens[] = $token;
|
||||||
|
|
||||||
|
// If token is invalid, remove it from database
|
||||||
|
if ($result['error'] === 'invalid_token') {
|
||||||
|
$this->remove_invalid_token($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("TWP FCM: Sent notification to $success_count/" . count($tokens) . " devices for user $user_id");
|
||||||
|
|
||||||
|
return $success_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
'title' => $title,
|
||||||
|
'body' => $body,
|
||||||
|
'timestamp' => time()
|
||||||
|
)),
|
||||||
|
'priority' => 'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
'Authorization: key=' . $this->server_key,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
);
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $this->fcm_url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||||
|
|
||||||
|
$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 send notification. HTTP $http_code: $response");
|
||||||
|
|
||||||
|
// Check if token is invalid
|
||||||
|
$response_data = json_decode($response, true);
|
||||||
|
if (isset($response_data['results'][0]['error']) &&
|
||||||
|
in_array($response_data['results'][0]['error'], array('InvalidRegistration', 'NotRegistered'))) {
|
||||||
|
return array('success' => false, 'error' => 'invalid_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('success' => false, 'error' => 'http_error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('success' => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active FCM tokens for a user
|
||||||
|
*/
|
||||||
|
private function get_user_tokens($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
return $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT fcm_token FROM $table
|
||||||
|
WHERE user_id = %d
|
||||||
|
AND is_active = 1
|
||||||
|
AND fcm_token IS NOT NULL
|
||||||
|
AND fcm_token != ''
|
||||||
|
AND expires_at > NOW()",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove invalid FCM token from database
|
||||||
|
*/
|
||||||
|
private function remove_invalid_token($token) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('fcm_token' => null),
|
||||||
|
array('fcm_token' => $token),
|
||||||
|
array('%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log("TWP FCM: Removed invalid token from database");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send incoming call notification
|
||||||
|
*/
|
||||||
|
public function notify_incoming_call($user_id, $from_number, $queue_name, $call_sid) {
|
||||||
|
$title = 'Incoming Call';
|
||||||
|
$body = "Call from $from_number in $queue_name queue";
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'incoming_call',
|
||||||
|
'call_sid' => $call_sid,
|
||||||
|
'from_number' => $from_number,
|
||||||
|
'queue_name' => $queue_name
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send queue timeout notification
|
||||||
|
*/
|
||||||
|
public function notify_queue_timeout($user_id, $queue_name, $waiting_count) {
|
||||||
|
$title = 'Queue Alert';
|
||||||
|
$body = "$queue_name has $waiting_count waiting call" . ($waiting_count > 1 ? 's' : '');
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'queue_timeout',
|
||||||
|
'queue_name' => $queue_name,
|
||||||
|
'waiting_count' => $waiting_count
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send agent status change notification
|
||||||
|
*/
|
||||||
|
public function notify_status_change($user_id, $old_status, $new_status) {
|
||||||
|
$title = 'Status Changed';
|
||||||
|
$body = "Your status changed from $old_status to $new_status";
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'status_change',
|
||||||
|
'old_status' => $old_status,
|
||||||
|
'new_status' => $new_status
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test notification (for settings page)
|
||||||
|
*/
|
||||||
|
public function send_test_notification($user_id) {
|
||||||
|
$title = 'Test Notification';
|
||||||
|
$body = 'This is a test notification from Twilio WordPress Plugin';
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'test',
|
||||||
|
'test' => true
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
684
includes/class-twp-mobile-api.php
Normal file
684
includes/class-twp-mobile-api.php
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
<?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')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
|
||||||
|
// Check if status exists
|
||||||
|
$exists = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'status' => $new_status,
|
||||||
|
'last_activity' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($is_logged_in !== null) {
|
||||||
|
$data['is_logged_in'] = $is_logged_in ? 1 : 0;
|
||||||
|
if ($is_logged_in) {
|
||||||
|
$data['logged_in_at'] = current_time('mysql');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
$data,
|
||||||
|
array('user_id' => $user_id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$data['user_id'] = $user_id;
|
||||||
|
$wpdb->insert($table, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
|
||||||
|
|
||||||
|
// Get queues assigned to this user
|
||||||
|
$queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// Also include personal queues
|
||||||
|
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT id FROM $queues_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
|
||||||
|
|
||||||
|
if (empty($all_queue_ids)) {
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'queues' => array()
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
|
||||||
|
|
||||||
|
// Get queue information with call counts
|
||||||
|
$queues = $wpdb->get_results("
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
q.queue_name,
|
||||||
|
q.queue_type,
|
||||||
|
q.extension,
|
||||||
|
COUNT(c.id) as waiting_count
|
||||||
|
FROM $queues_table q
|
||||||
|
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
|
||||||
|
WHERE q.id IN ($queue_ids_str)
|
||||||
|
GROUP BY q.id
|
||||||
|
");
|
||||||
|
|
||||||
|
$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'];
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// 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
|
||||||
|
), 200);
|
||||||
|
|
||||||
|
} 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) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
457
includes/class-twp-mobile-auth.php
Normal file
457
includes/class-twp-mobile-auth.php
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile App JWT Authentication Handler
|
||||||
|
*
|
||||||
|
* Handles JWT token generation, validation, and refresh for Android/iOS apps
|
||||||
|
*/
|
||||||
|
class TWP_Mobile_Auth {
|
||||||
|
|
||||||
|
private $secret_key;
|
||||||
|
private $token_expiry = 86400; // 24 hours in seconds
|
||||||
|
private $refresh_expiry = 2592000; // 30 days in seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->secret_key = $this->get_secret_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or generate JWT secret key
|
||||||
|
*/
|
||||||
|
private function get_secret_key() {
|
||||||
|
$key = get_option('twp_mobile_jwt_secret');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
// Generate a secure random key
|
||||||
|
$key = bin2hex(random_bytes(32));
|
||||||
|
update_option('twp_mobile_jwt_secret', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API endpoints
|
||||||
|
*/
|
||||||
|
public function register_endpoints() {
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
// Login endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/login', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_login'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Refresh token endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/refresh', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_refresh'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Logout endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/logout', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_logout'),
|
||||||
|
'permission_callback' => array($this, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Validate token endpoint (for debugging)
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/validate', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'handle_validate'),
|
||||||
|
'permission_callback' => array($this, 'verify_token')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login request
|
||||||
|
*/
|
||||||
|
public function handle_login($request) {
|
||||||
|
$username = $request->get_param('username');
|
||||||
|
$password = $request->get_param('password');
|
||||||
|
$fcm_token = $request->get_param('fcm_token'); // Optional
|
||||||
|
$device_info = $request->get_param('device_info'); // Optional
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
$user = wp_authenticate($username, $password);
|
||||||
|
|
||||||
|
if (is_wp_error($user)) {
|
||||||
|
return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has phone agent capabilities
|
||||||
|
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
|
||||||
|
return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tokens
|
||||||
|
$access_token = $this->generate_token($user->ID, 'access');
|
||||||
|
$refresh_token = $this->generate_token($user->ID, 'refresh');
|
||||||
|
|
||||||
|
// Store session in database
|
||||||
|
$this->store_session($user->ID, $refresh_token, $fcm_token, $device_info);
|
||||||
|
|
||||||
|
// Get user data
|
||||||
|
$user_data = $this->get_user_data($user->ID);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
'refresh_token' => $refresh_token,
|
||||||
|
'expires_in' => $this->token_expiry,
|
||||||
|
'user' => $user_data
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle token refresh request
|
||||||
|
*/
|
||||||
|
public function handle_refresh($request) {
|
||||||
|
$refresh_token = $request->get_param('refresh_token');
|
||||||
|
|
||||||
|
if (empty($refresh_token)) {
|
||||||
|
return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify refresh token
|
||||||
|
$payload = $this->decode_token($refresh_token);
|
||||||
|
|
||||||
|
if (!$payload || $payload->type !== 'refresh') {
|
||||||
|
return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session exists and is valid
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$session = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()",
|
||||||
|
$payload->user_id,
|
||||||
|
$refresh_token
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
$access_token = $this->generate_token($payload->user_id, 'access');
|
||||||
|
|
||||||
|
// Update last_used timestamp
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('last_used' => current_time('mysql')),
|
||||||
|
array('id' => $session->id),
|
||||||
|
array('%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
'expires_in' => $this->token_expiry
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle logout request
|
||||||
|
*/
|
||||||
|
public function handle_logout($request) {
|
||||||
|
$user_id = $this->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get refresh token from request
|
||||||
|
$refresh_token = $request->get_param('refresh_token');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
if ($refresh_token) {
|
||||||
|
// Invalidate specific session
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('is_active' => 0),
|
||||||
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token),
|
||||||
|
array('%d'),
|
||||||
|
array('%d', '%s')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Invalidate all sessions for this user
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('is_active' => 0),
|
||||||
|
array('user_id' => $user_id),
|
||||||
|
array('%d'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Logged out successfully'
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle token validation request
|
||||||
|
*/
|
||||||
|
public function handle_validate($request) {
|
||||||
|
$user_id = $this->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_data = $this->get_user_data($user_id);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'valid' => true,
|
||||||
|
'user' => $user_data
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JWT token
|
||||||
|
*/
|
||||||
|
private function generate_token($user_id, $type = 'access') {
|
||||||
|
$issued_at = time();
|
||||||
|
$expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry;
|
||||||
|
|
||||||
|
$payload = array(
|
||||||
|
'iat' => $issued_at,
|
||||||
|
'exp' => $issued_at + $expiry,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->encode_token($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple JWT encoding (header.payload.signature)
|
||||||
|
*/
|
||||||
|
private function encode_token($payload) {
|
||||||
|
$header = array('typ' => 'JWT', 'alg' => 'HS256');
|
||||||
|
|
||||||
|
$segments = array();
|
||||||
|
$segments[] = $this->base64url_encode(json_encode($header));
|
||||||
|
$segments[] = $this->base64url_encode(json_encode($payload));
|
||||||
|
|
||||||
|
$signing_input = implode('.', $segments);
|
||||||
|
$signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
||||||
|
$segments[] = $this->base64url_encode($signature);
|
||||||
|
|
||||||
|
return implode('.', $segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple JWT decoding
|
||||||
|
*/
|
||||||
|
private function decode_token($token) {
|
||||||
|
$segments = explode('.', $token);
|
||||||
|
|
||||||
|
if (count($segments) !== 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($header_b64, $payload_b64, $signature_b64) = $segments;
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
$signing_input = $header_b64 . '.' . $payload_b64;
|
||||||
|
$signature = $this->base64url_decode($signature_b64);
|
||||||
|
$expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
||||||
|
|
||||||
|
if (!hash_equals($signature, $expected_signature)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode payload
|
||||||
|
$payload = json_decode($this->base64url_decode($payload_b64));
|
||||||
|
|
||||||
|
if (!$payload) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if (isset($payload->exp) && $payload->exp < time()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 URL encode
|
||||||
|
*/
|
||||||
|
private function base64url_encode($data) {
|
||||||
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 URL decode
|
||||||
|
*/
|
||||||
|
private function base64url_decode($data) {
|
||||||
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify token (permission callback)
|
||||||
|
*/
|
||||||
|
public function verify_token($request) {
|
||||||
|
$auth_header = $request->get_header('Authorization');
|
||||||
|
|
||||||
|
if (empty($auth_header)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from "Bearer <token>"
|
||||||
|
if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
|
||||||
|
$token = $matches[1];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->decode_token($token);
|
||||||
|
|
||||||
|
if (!$payload || $payload->type !== 'access') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID for later use
|
||||||
|
$request->set_param('_twp_user_id', $payload->user_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user ID from token
|
||||||
|
*/
|
||||||
|
public function get_current_user_id() {
|
||||||
|
$request = rest_get_server()->get_request();
|
||||||
|
return $request->get_param('_twp_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store session in database
|
||||||
|
*/
|
||||||
|
private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array(
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'refresh_token' => $refresh_token,
|
||||||
|
'fcm_token' => $fcm_token,
|
||||||
|
'device_info' => $device_info,
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry),
|
||||||
|
'last_used' => current_time('mysql'),
|
||||||
|
'is_active' => 1
|
||||||
|
),
|
||||||
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user data for response
|
||||||
|
*/
|
||||||
|
private function get_user_data($user_id) {
|
||||||
|
$user = get_userdata($user_id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get agent phone number
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
// Get agent status
|
||||||
|
global $wpdb;
|
||||||
|
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
$status = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// Get user extension
|
||||||
|
$ext_table = $wpdb->prefix . 'twp_user_extensions';
|
||||||
|
$extension = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $user->ID,
|
||||||
|
'username' => $user->user_login,
|
||||||
|
'display_name' => $user->display_name,
|
||||||
|
'email' => $user->user_email,
|
||||||
|
'phone_number' => $agent_number,
|
||||||
|
'extension' => $extension ? $extension->extension : null,
|
||||||
|
'direct_dial' => $extension ? $extension->direct_dial_number : null,
|
||||||
|
'status' => $status ? $status->status : 'offline',
|
||||||
|
'is_logged_in' => $status ? (bool)$status->is_logged_in : false,
|
||||||
|
'current_call_sid' => $status ? $status->current_call_sid : null,
|
||||||
|
'capabilities' => array(
|
||||||
|
'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'),
|
||||||
|
'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'),
|
||||||
|
'can_access_call_log' => user_can($user_id, 'twp_access_call_log'),
|
||||||
|
'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'),
|
||||||
|
'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'),
|
||||||
|
'is_admin' => user_can($user_id, 'manage_options')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update FCM token for existing session
|
||||||
|
*/
|
||||||
|
public function update_fcm_token($user_id, $refresh_token, $fcm_token) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('fcm_token' => $fcm_token),
|
||||||
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1),
|
||||||
|
array('%s'),
|
||||||
|
array('%d', '%s', '%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active FCM tokens for a user
|
||||||
|
*/
|
||||||
|
public function get_user_fcm_tokens($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
return $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired sessions
|
||||||
|
*/
|
||||||
|
public static function cleanup_expired_sessions() {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
308
includes/class-twp-mobile-sse.php
Normal file
308
includes/class-twp-mobile-sse.php
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Server-Sent Events (SSE) Handler for Mobile App
|
||||||
|
*
|
||||||
|
* Provides real-time updates for queue state, incoming calls, and agent status
|
||||||
|
*/
|
||||||
|
class TWP_Mobile_SSE {
|
||||||
|
|
||||||
|
private $auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'class-twp-mobile-auth.php';
|
||||||
|
$this->auth = new TWP_Mobile_Auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register SSE endpoint
|
||||||
|
*/
|
||||||
|
public function register_endpoints() {
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
register_rest_route('twilio-mobile/v1', '/stream/events', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'stream_events'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream events to mobile app
|
||||||
|
*/
|
||||||
|
public function stream_events($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers for SSE
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
header('Connection: keep-alive');
|
||||||
|
header('X-Accel-Buffering: no'); // Disable nginx buffering
|
||||||
|
|
||||||
|
// Disable PHP output buffering
|
||||||
|
if (function_exists('apache_setenv')) {
|
||||||
|
@apache_setenv('no-gzip', '1');
|
||||||
|
}
|
||||||
|
@ini_set('zlib.output_compression', 0);
|
||||||
|
@ini_set('implicit_flush', 1);
|
||||||
|
ob_implicit_flush(1);
|
||||||
|
while (ob_get_level() > 0) {
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial connection event
|
||||||
|
$this->send_event('connected', array('user_id' => $user_id, 'timestamp' => time()));
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
$last_check = time();
|
||||||
|
$previous_state = $this->get_current_state($user_id);
|
||||||
|
|
||||||
|
// Stream loop - check for changes every 2 seconds
|
||||||
|
$max_duration = 300; // 5 minutes max connection time
|
||||||
|
$start_time = time();
|
||||||
|
|
||||||
|
while (time() - $start_time < $max_duration) {
|
||||||
|
// Check if connection is still alive
|
||||||
|
if (connection_aborted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
$current_state = $this->get_current_state($user_id);
|
||||||
|
|
||||||
|
// Compare and send updates
|
||||||
|
$this->check_and_send_updates($previous_state, $current_state);
|
||||||
|
|
||||||
|
// Update previous state
|
||||||
|
$previous_state = $current_state;
|
||||||
|
|
||||||
|
// Send heartbeat every 15 seconds
|
||||||
|
if (time() - $last_check >= 15) {
|
||||||
|
$this->send_event('heartbeat', array('timestamp' => time()));
|
||||||
|
$last_check = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for 2 seconds
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection closing
|
||||||
|
$this->send_event('disconnect', array('reason' => 'timeout', 'timestamp' => time()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current state for agent
|
||||||
|
*/
|
||||||
|
private function get_current_state($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$state = array(
|
||||||
|
'agent_status' => $this->get_agent_status($user_id),
|
||||||
|
'queues' => $this->get_queues_state($user_id),
|
||||||
|
'current_call' => $this->get_current_call($user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent status
|
||||||
|
*/
|
||||||
|
private function get_agent_status($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
|
||||||
|
$status = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, is_logged_in, current_call_sid FROM $table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$status) {
|
||||||
|
return array('status' => 'offline', 'is_logged_in' => false, 'current_call_sid' => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'status' => $status->status,
|
||||||
|
'is_logged_in' => (bool)$status->is_logged_in,
|
||||||
|
'current_call_sid' => $status->current_call_sid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queues state
|
||||||
|
*/
|
||||||
|
private function get_queues_state($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
|
||||||
|
|
||||||
|
// Get queue IDs
|
||||||
|
$queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT id FROM $queues_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
|
||||||
|
|
||||||
|
if (empty($all_queue_ids)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
|
||||||
|
|
||||||
|
$queues = $wpdb->get_results("
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
q.queue_name,
|
||||||
|
COUNT(c.id) as waiting_count,
|
||||||
|
MIN(c.enqueued_at) as oldest_call_time
|
||||||
|
FROM $queues_table q
|
||||||
|
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
|
||||||
|
WHERE q.id IN ($queue_ids_str)
|
||||||
|
GROUP BY q.id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($queues as $queue) {
|
||||||
|
$result[$queue->id] = array(
|
||||||
|
'id' => (int)$queue->id,
|
||||||
|
'name' => $queue->queue_name,
|
||||||
|
'waiting_count' => (int)$queue->waiting_count,
|
||||||
|
'oldest_call_time' => $queue->oldest_call_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current call for agent
|
||||||
|
*/
|
||||||
|
private function get_current_call($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
if (!$agent_number) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$call = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT call_sid, from_number, queue_id, status, joined_at
|
||||||
|
FROM $calls_table
|
||||||
|
WHERE agent_phone = %s AND status IN ('connecting', 'in_progress')
|
||||||
|
ORDER BY joined_at DESC
|
||||||
|
LIMIT 1",
|
||||||
|
$agent_number
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$call) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'call_sid' => $call->call_sid,
|
||||||
|
'from_number' => $call->from_number,
|
||||||
|
'queue_id' => (int)$call->queue_id,
|
||||||
|
'status' => $call->status,
|
||||||
|
'duration' => time() - strtotime($call->joined_at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check state changes and send updates
|
||||||
|
*/
|
||||||
|
private function check_and_send_updates($previous, $current) {
|
||||||
|
// Check agent status changes
|
||||||
|
if ($previous['agent_status'] !== $current['agent_status']) {
|
||||||
|
$this->send_event('agent_status_changed', $current['agent_status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check queue changes
|
||||||
|
$this->check_queue_changes($previous['queues'], $current['queues']);
|
||||||
|
|
||||||
|
// Check current call changes
|
||||||
|
if ($previous['current_call'] !== $current['current_call']) {
|
||||||
|
if ($current['current_call'] && !$previous['current_call']) {
|
||||||
|
// New call started
|
||||||
|
$this->send_event('call_started', $current['current_call']);
|
||||||
|
} elseif (!$current['current_call'] && $previous['current_call']) {
|
||||||
|
// Call ended
|
||||||
|
$this->send_event('call_ended', $previous['current_call']);
|
||||||
|
} elseif ($current['current_call'] && $previous['current_call']) {
|
||||||
|
// Call status changed
|
||||||
|
if ($current['current_call']['status'] !== $previous['current_call']['status']) {
|
||||||
|
$this->send_event('call_status_changed', $current['current_call']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for queue changes
|
||||||
|
*/
|
||||||
|
private function check_queue_changes($previous_queues, $current_queues) {
|
||||||
|
foreach ($current_queues as $queue_id => $current_queue) {
|
||||||
|
$previous_queue = $previous_queues[$queue_id] ?? null;
|
||||||
|
|
||||||
|
if (!$previous_queue) {
|
||||||
|
// New queue added
|
||||||
|
$this->send_event('queue_added', $current_queue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for waiting count changes
|
||||||
|
if ($current_queue['waiting_count'] !== $previous_queue['waiting_count']) {
|
||||||
|
if ($current_queue['waiting_count'] > $previous_queue['waiting_count']) {
|
||||||
|
// New call in queue
|
||||||
|
$this->send_event('call_enqueued', array(
|
||||||
|
'queue_id' => $queue_id,
|
||||||
|
'queue_name' => $current_queue['name'],
|
||||||
|
'waiting_count' => $current_queue['waiting_count']
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Call removed from queue
|
||||||
|
$this->send_event('call_dequeued', array(
|
||||||
|
'queue_id' => $queue_id,
|
||||||
|
'queue_name' => $current_queue['name'],
|
||||||
|
'waiting_count' => $current_queue['waiting_count']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed queues
|
||||||
|
foreach ($previous_queues as $queue_id => $previous_queue) {
|
||||||
|
if (!isset($current_queues[$queue_id])) {
|
||||||
|
$this->send_event('queue_removed', array('queue_id' => $queue_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send SSE event
|
||||||
|
*/
|
||||||
|
private function send_event($event_type, $data) {
|
||||||
|
echo "event: $event_type\n";
|
||||||
|
echo "data: " . json_encode($data) . "\n\n";
|
||||||
|
|
||||||
|
if (ob_get_level() > 0) {
|
||||||
|
ob_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,13 +41,28 @@ class TWP_Twilio_API {
|
|||||||
* Initialize Twilio SDK client
|
* Initialize Twilio SDK client
|
||||||
*/
|
*/
|
||||||
private function init_sdk_client() {
|
private function init_sdk_client() {
|
||||||
// Check if autoloader exists
|
// Check for SDK autoloader - external location first (survives plugin updates)
|
||||||
$autoloader_path = TWP_PLUGIN_DIR . 'vendor/autoload.php';
|
$autoloader_path = null;
|
||||||
if (!file_exists($autoloader_path)) {
|
|
||||||
error_log('TWP Plugin: Autoloader not found at: ' . $autoloader_path);
|
// Priority 1: External SDK location (recommended)
|
||||||
throw new Exception('Twilio SDK not found. Please run: ./install-twilio-sdk.sh');
|
$external_autoloader = TWP_EXTERNAL_SDK_DIR . 'autoload.php';
|
||||||
|
if (file_exists($external_autoloader)) {
|
||||||
|
$autoloader_path = $external_autoloader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 2: Internal vendor directory (fallback)
|
||||||
|
if (!$autoloader_path) {
|
||||||
|
$internal_autoloader = TWP_PLUGIN_DIR . 'vendor/autoload.php';
|
||||||
|
if (file_exists($internal_autoloader)) {
|
||||||
|
$autoloader_path = $internal_autoloader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$autoloader_path) {
|
||||||
|
error_log('TWP Plugin: Autoloader not found. Checked: ' . $external_autoloader . ' and ' . TWP_PLUGIN_DIR . 'vendor/autoload.php');
|
||||||
|
throw new Exception('Twilio SDK not found. Please run: ./install-twilio-sdk-external.sh');
|
||||||
|
}
|
||||||
|
|
||||||
// Load the autoloader
|
// Load the autoloader
|
||||||
require_once $autoloader_path;
|
require_once $autoloader_path;
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,25 @@ class TWP_Webhooks {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
// Load Twilio SDK if not already loaded
|
// Load Twilio SDK if not already loaded
|
||||||
|
// Check external location first (survives plugin updates), then internal
|
||||||
if (!class_exists('\Twilio\Rest\Client')) {
|
if (!class_exists('\Twilio\Rest\Client')) {
|
||||||
$autoloader_path = plugin_dir_path(dirname(__FILE__)) . 'vendor/autoload.php';
|
$autoloader_path = null;
|
||||||
if (file_exists($autoloader_path)) {
|
|
||||||
|
// Priority 1: External SDK location
|
||||||
|
$external_autoloader = dirname(dirname(plugin_dir_path(dirname(__FILE__)))) . '/twilio-sdk/autoload.php';
|
||||||
|
if (file_exists($external_autoloader)) {
|
||||||
|
$autoloader_path = $external_autoloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Internal vendor directory
|
||||||
|
if (!$autoloader_path) {
|
||||||
|
$internal_autoloader = plugin_dir_path(dirname(__FILE__)) . 'vendor/autoload.php';
|
||||||
|
if (file_exists($internal_autoloader)) {
|
||||||
|
$autoloader_path = $internal_autoloader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($autoloader_path) {
|
||||||
require_once $autoloader_path;
|
require_once $autoloader_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
install-twilio-sdk-external.sh
Executable file
158
install-twilio-sdk-external.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to install Twilio PHP SDK to an external location
|
||||||
|
# This prevents SDK from being deleted when WordPress updates the plugin
|
||||||
|
#
|
||||||
|
# Location: wp-content/twilio-sdk/ (outside plugin folder)
|
||||||
|
|
||||||
|
echo "Installing Twilio PHP SDK v8.7.0 to external location..."
|
||||||
|
echo "This will install the SDK outside the plugin folder to survive plugin updates."
|
||||||
|
|
||||||
|
# Check if we can download files
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
echo "ERROR: curl is required to download the SDK"
|
||||||
|
echo "Please install curl and try again"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v tar &> /dev/null; then
|
||||||
|
echo "ERROR: tar is required to extract the SDK"
|
||||||
|
echo "Please install tar and try again"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the script directory (plugin directory)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Calculate wp-content directory (two levels up from plugin)
|
||||||
|
# Plugin is at: wp-content/plugins/twilio-wp-plugin/
|
||||||
|
# We want: wp-content/twilio-sdk/
|
||||||
|
WP_CONTENT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
SDK_DIR="$WP_CONTENT_DIR/twilio-sdk"
|
||||||
|
|
||||||
|
echo "Plugin directory: $SCRIPT_DIR"
|
||||||
|
echo "SDK will be installed to: $SDK_DIR"
|
||||||
|
|
||||||
|
# Create SDK directory
|
||||||
|
mkdir -p "$SDK_DIR/twilio/sdk"
|
||||||
|
|
||||||
|
# Download the latest release (8.7.0)
|
||||||
|
echo "Downloading Twilio SDK from GitHub..."
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
|
if ! curl -L https://github.com/twilio/twilio-php/archive/refs/tags/8.7.0.tar.gz -o twilio-sdk.tar.gz; then
|
||||||
|
echo "ERROR: Failed to download Twilio SDK"
|
||||||
|
echo "Please check your internet connection and try again"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the archive
|
||||||
|
echo "Extracting SDK files..."
|
||||||
|
if ! tar -xzf twilio-sdk.tar.gz; then
|
||||||
|
echo "ERROR: Failed to extract SDK files"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the extracted directory exists
|
||||||
|
if [ ! -d "twilio-php-8.7.0/src" ]; then
|
||||||
|
echo "ERROR: Extracted SDK directory structure is unexpected"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove existing SDK if it exists
|
||||||
|
if [ -d "$SDK_DIR/twilio/sdk" ]; then
|
||||||
|
echo "Removing existing SDK installation..."
|
||||||
|
rm -rf "$SDK_DIR/twilio/sdk"
|
||||||
|
mkdir -p "$SDK_DIR/twilio/sdk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Move the entire src directory to be the sdk
|
||||||
|
echo "Installing SDK files..."
|
||||||
|
if ! mv twilio-php-8.7.0/src/* "$SDK_DIR/twilio/sdk/"; then
|
||||||
|
echo "ERROR: Failed to move SDK files"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a comprehensive autoloader
|
||||||
|
cat > "$SDK_DIR/autoload.php" << 'EOF'
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Twilio SDK v8.7.0 Autoloader (External Installation)
|
||||||
|
* This file loads the Twilio PHP SDK classes
|
||||||
|
*
|
||||||
|
* Location: wp-content/twilio-sdk/autoload.php
|
||||||
|
* This location survives WordPress plugin updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent multiple registrations
|
||||||
|
if (!defined('TWILIO_AUTOLOADER_REGISTERED')) {
|
||||||
|
define('TWILIO_AUTOLOADER_REGISTERED', true);
|
||||||
|
|
||||||
|
// Register the autoloader
|
||||||
|
spl_autoload_register(function ($class) {
|
||||||
|
// Only handle Twilio classes
|
||||||
|
if (strpos($class, 'Twilio\\') !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert class name to file path
|
||||||
|
// The SDK structure is: twilio/sdk/Twilio/Rest/Client.php for Twilio\Rest\Client
|
||||||
|
$file = __DIR__ . '/twilio/sdk/' . str_replace('\\', '/', $class) . '.php';
|
||||||
|
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to load the SDK's own autoloader if it exists
|
||||||
|
$sdk_autoloader = __DIR__ . '/twilio/sdk/autoload.php';
|
||||||
|
if (file_exists($sdk_autoloader)) {
|
||||||
|
require_once $sdk_autoloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load essential Twilio classes manually to ensure they're available
|
||||||
|
$essential_classes = [
|
||||||
|
__DIR__ . '/twilio/sdk/Twilio/Rest/Client.php',
|
||||||
|
__DIR__ . '/twilio/sdk/Twilio/TwiML/VoiceResponse.php',
|
||||||
|
__DIR__ . '/twilio/sdk/Twilio/Exceptions/TwilioException.php',
|
||||||
|
__DIR__ . '/twilio/sdk/Twilio/Security/RequestValidator.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($essential_classes as $class_file) {
|
||||||
|
if (file_exists($class_file)) {
|
||||||
|
require_once $class_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Clean up temp directory
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo ""
|
||||||
|
echo "Verifying installation..."
|
||||||
|
if [ -f "$SDK_DIR/autoload.php" ] && [ -d "$SDK_DIR/twilio/sdk" ]; then
|
||||||
|
echo "=============================================="
|
||||||
|
echo "Twilio SDK v8.7.0 installed successfully!"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
echo "Installation location: $SDK_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "This SDK is installed OUTSIDE the plugin folder,"
|
||||||
|
echo "so it will NOT be deleted when WordPress updates the plugin."
|
||||||
|
echo ""
|
||||||
|
echo "The plugin will automatically detect this external SDK."
|
||||||
|
else
|
||||||
|
echo "Installation failed - files not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -77,16 +77,16 @@ if (!defined('TWILIO_AUTOLOADER_REGISTERED')) {
|
|||||||
if (strpos($class, 'Twilio\\') !== 0) {
|
if (strpos($class, 'Twilio\\') !== 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert class name to file path
|
// Convert class name to file path
|
||||||
$relative_class = substr($class, 7); // Remove 'Twilio\'
|
// The SDK structure is: twilio/sdk/Twilio/Rest/Client.php for Twilio\Rest\Client
|
||||||
$file = __DIR__ . '/twilio/sdk/' . str_replace('\\', '/', $relative_class) . '.php';
|
$file = __DIR__ . '/twilio/sdk/' . str_replace('\\', '/', $class) . '.php';
|
||||||
|
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
require_once $file;
|
require_once $file;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ if (!defined('TWILIO_AUTOLOADER_REGISTERED')) {
|
|||||||
|
|
||||||
// Load essential Twilio classes manually to ensure they're available
|
// Load essential Twilio classes manually to ensure they're available
|
||||||
$essential_classes = [
|
$essential_classes = [
|
||||||
__DIR__ . '/twilio/sdk/Rest/Client.php',
|
__DIR__ . '/twilio/sdk/Twilio/Rest/Client.php',
|
||||||
__DIR__ . '/twilio/sdk/TwiML/VoiceResponse.php',
|
__DIR__ . '/twilio/sdk/Twilio/TwiML/VoiceResponse.php',
|
||||||
__DIR__ . '/twilio/sdk/Exceptions/TwilioException.php',
|
__DIR__ . '/twilio/sdk/Twilio/Exceptions/TwilioException.php',
|
||||||
__DIR__ . '/twilio/sdk/Security/RequestValidator.php'
|
__DIR__ . '/twilio/sdk/Twilio/Security/RequestValidator.php'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($essential_classes as $class_file) {
|
foreach ($essential_classes as $class_file) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Twilio WP Plugin
|
* Plugin Name: Twilio WP Plugin
|
||||||
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
|
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
|
||||||
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
|
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
|
||||||
* Version: 2.8.9
|
* Version: {auto_update_value_on_deploy}
|
||||||
* Author: Josh Knapp
|
* Author: Josh Knapp
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
* Text Domain: twilio-wp-plugin
|
* Text Domain: twilio-wp-plugin
|
||||||
@@ -15,11 +15,13 @@ if (!defined('WPINC')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('TWP_VERSION', '2.8.9');
|
define('TWP_VERSION', '{auto_update_value_on_deploy}');
|
||||||
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
|
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
|
||||||
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
|
// External SDK location - survives plugin updates (wp-content/twilio-sdk/)
|
||||||
|
define('TWP_EXTERNAL_SDK_DIR', dirname(dirname(TWP_PLUGIN_DIR)) . '/twilio-sdk/');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin activation hook
|
* Plugin activation hook
|
||||||
@@ -31,17 +33,27 @@ function twp_activate() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Twilio SDK is installed and show admin notice if not
|
* Check if Twilio SDK is installed and show admin notice if not
|
||||||
|
* Checks external location first (survives plugin updates), then internal fallback
|
||||||
*/
|
*/
|
||||||
function twp_check_sdk_installation() {
|
function twp_check_sdk_installation() {
|
||||||
$autoloader_path = TWP_PLUGIN_DIR . 'vendor/autoload.php';
|
|
||||||
$sdk_installed = false;
|
$sdk_installed = false;
|
||||||
|
|
||||||
if (file_exists($autoloader_path)) {
|
// Priority 1: Check external SDK location (survives plugin updates)
|
||||||
// Try to load autoloader and check for classes
|
$external_autoloader = TWP_EXTERNAL_SDK_DIR . 'autoload.php';
|
||||||
require_once $autoloader_path;
|
if (file_exists($external_autoloader)) {
|
||||||
|
require_once $external_autoloader;
|
||||||
$sdk_installed = class_exists('Twilio\Rest\Client');
|
$sdk_installed = class_exists('Twilio\Rest\Client');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 2: Fall back to internal vendor directory
|
||||||
|
if (!$sdk_installed) {
|
||||||
|
$internal_autoloader = TWP_PLUGIN_DIR . 'vendor/autoload.php';
|
||||||
|
if (file_exists($internal_autoloader)) {
|
||||||
|
require_once $internal_autoloader;
|
||||||
|
$sdk_installed = class_exists('Twilio\Rest\Client');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$sdk_installed) {
|
if (!$sdk_installed) {
|
||||||
add_action('admin_notices', 'twp_sdk_missing_notice');
|
add_action('admin_notices', 'twp_sdk_missing_notice');
|
||||||
}
|
}
|
||||||
@@ -55,10 +67,12 @@ function twp_sdk_missing_notice() {
|
|||||||
<div class="notice notice-error is-dismissible">
|
<div class="notice notice-error is-dismissible">
|
||||||
<h3>Twilio WordPress Plugin - SDK Required</h3>
|
<h3>Twilio WordPress Plugin - SDK Required</h3>
|
||||||
<p><strong>The Twilio PHP SDK is required for this plugin to work.</strong></p>
|
<p><strong>The Twilio PHP SDK is required for this plugin to work.</strong></p>
|
||||||
<p>To install the SDK, run this command in your plugin directory:</p>
|
<p><strong>Recommended:</strong> Install SDK to external location (survives plugin updates):</p>
|
||||||
|
<code>chmod +x install-twilio-sdk-external.sh && ./install-twilio-sdk-external.sh</code>
|
||||||
|
<p style="margin-top: 10px;"><strong>Alternative:</strong> Install SDK inside plugin folder:</p>
|
||||||
<code>chmod +x install-twilio-sdk.sh && ./install-twilio-sdk.sh</code>
|
<code>chmod +x install-twilio-sdk.sh && ./install-twilio-sdk.sh</code>
|
||||||
<p>Or install via Composer: <code>composer install</code></p>
|
<p style="margin-top: 10px;"><em>Plugin path: <?php echo esc_html(TWP_PLUGIN_DIR); ?></em></p>
|
||||||
<p><em>Plugin path: <?php echo TWP_PLUGIN_DIR; ?></em></p>
|
<p><em>External SDK path: <?php echo esc_html(TWP_EXTERNAL_SDK_DIR); ?></em></p>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -126,6 +140,52 @@ function twp_deactivate() {
|
|||||||
register_activation_hook(__FILE__, 'twp_activate');
|
register_activation_hook(__FILE__, 'twp_activate');
|
||||||
register_deactivation_hook(__FILE__, 'twp_deactivate');
|
register_deactivation_hook(__FILE__, 'twp_deactivate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check SDK status after plugin updates
|
||||||
|
* Shows warning if SDK was deleted during update and external SDK not available
|
||||||
|
*/
|
||||||
|
function twp_check_sdk_after_update($upgrader_object, $options) {
|
||||||
|
// Only run for plugin updates
|
||||||
|
if ($options['action'] !== 'update' || $options['type'] !== 'plugin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this plugin was updated
|
||||||
|
$updated_plugins = isset($options['plugins']) ? $options['plugins'] : array();
|
||||||
|
if (!in_array(TWP_PLUGIN_BASENAME, $updated_plugins)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if SDK is available
|
||||||
|
$external_sdk = file_exists(TWP_EXTERNAL_SDK_DIR . 'autoload.php');
|
||||||
|
$internal_sdk = file_exists(TWP_PLUGIN_DIR . 'vendor/autoload.php');
|
||||||
|
|
||||||
|
if (!$external_sdk && !$internal_sdk) {
|
||||||
|
// Set a transient to show warning on next admin page load
|
||||||
|
set_transient('twp_sdk_update_warning', true, 60 * 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('upgrader_process_complete', 'twp_check_sdk_after_update', 10, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show SDK update warning
|
||||||
|
*/
|
||||||
|
function twp_show_sdk_update_warning() {
|
||||||
|
if (get_transient('twp_sdk_update_warning')) {
|
||||||
|
delete_transient('twp_sdk_update_warning');
|
||||||
|
?>
|
||||||
|
<div class="notice notice-warning is-dismissible">
|
||||||
|
<h3>Twilio WordPress Plugin - SDK Reinstall Required</h3>
|
||||||
|
<p><strong>The plugin was updated and the Twilio SDK needs to be reinstalled.</strong></p>
|
||||||
|
<p>To prevent this in the future, install the SDK to the external location:</p>
|
||||||
|
<code>cd <?php echo esc_html(TWP_PLUGIN_DIR); ?> && ./install-twilio-sdk-external.sh</code>
|
||||||
|
<p style="margin-top: 10px;">The external SDK at <code><?php echo esc_html(TWP_EXTERNAL_SDK_DIR); ?></code> survives plugin updates.</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('admin_notices', 'twp_show_sdk_update_warning');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core plugin class
|
* Core plugin class
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user