@convirza/dialer-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1841 -0
- package/dist/api/CallHistoryAPI.d.ts +16 -0
- package/dist/api/DialerAPI.d.ts +52 -0
- package/dist/api/PhoneNumbersAPI.d.ts +21 -0
- package/dist/constants/api-config.d.ts +12 -0
- package/dist/constants/countries.d.ts +7 -0
- package/dist/constants/index.d.ts +30 -0
- package/dist/constants/sip-config.d.ts +14 -0
- package/dist/core/ConvirzaDialer.d.ts +40 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.esm.js +1 -0
- package/dist/index.umd.js +1 -0
- package/dist/init.d.ts +4 -0
- package/dist/media/AudioDeviceManager.d.ts +34 -0
- package/dist/media/CallQualityMonitor.d.ts +41 -0
- package/dist/services/CallHistoryService.d.ts +41 -0
- package/dist/sip/SipAdapter.d.ts +99 -0
- package/dist/storage/CallHistoryStore.d.ts +16 -0
- package/dist/types/call-history.d.ts +29 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/phone-numbers.d.ts +22 -0
- package/dist/ui/ConvirzaDialerElement.d.ts +250 -0
- package/dist/ui/ConvirzaDialerElement.styles.d.ts +2 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/validators.d.ts +4 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,1841 @@
|
|
|
1
|
+
# @convirza/dialer-sdk
|
|
2
|
+
|
|
3
|
+
Production-ready embeddable WebRTC SIP dialer widget. Auto-configures from OAuth session, handles token refresh, cross-tab logout detection. Includes call parking, call history persistence, audio device management, call quality monitoring.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Global Singleton API](#option-a--global-singleton-api-recommended)
|
|
12
|
+
- [Standalone Helpers](#option-b--standalone-helper-functions)
|
|
13
|
+
- [Web Component](#option-c--web-component-convirza-dialer)
|
|
14
|
+
- [Authentication Flow](#authentication-flow)
|
|
15
|
+
- [How It Works](#how-it-works)
|
|
16
|
+
- [Token Refresh](#token-refresh)
|
|
17
|
+
- [Logout Detection](#logout-detection)
|
|
18
|
+
- [API Reference](#api-reference)
|
|
19
|
+
- [convirzaDialer (Singleton)](#convirzadialer-singleton-dialerapi)
|
|
20
|
+
- [SipAdapter (Headless SIP)](#sipadapter)
|
|
21
|
+
- [AudioDeviceManager](#audiodevicemanager)
|
|
22
|
+
- [CallQualityMonitor](#callqualitymonitor)
|
|
23
|
+
- [PhoneNumbersAPI](#phonenumbersapi)
|
|
24
|
+
- [CallHistoryAPI](#callhistoryapi)
|
|
25
|
+
- [Web Component Reference](#web-component-reference)
|
|
26
|
+
- [HTML Attributes](#web-component-attributes)
|
|
27
|
+
- [Public Methods](#web-component-public-methods)
|
|
28
|
+
- [Events](#web-component-events)
|
|
29
|
+
- [Feature Guides](#feature-guides)
|
|
30
|
+
- [Call Parking](#call-parking)
|
|
31
|
+
- [Call History Persistence](#call-history-persistence)
|
|
32
|
+
- [Audio Device Management](#audio-device-management-1)
|
|
33
|
+
- [Call Quality Monitoring](#call-quality-monitoring-1)
|
|
34
|
+
- [Theming](#theming)
|
|
35
|
+
- [Types](#types)
|
|
36
|
+
- [Browser Support](#browser-support)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @convirza/dialer-sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Peer dependency** — SIP.js must be loaded separately (UMD global `SIP` or `sip`):
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script src="https://cdn.jsdelivr.net/npm/sip.js@0.13.8/dist/sip.js"></script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or install via npm and bundle it yourself:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install sip.js@^0.13.8
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
|
|
64
|
+
User must be logged into your app with `access_token` + `refresh_token` stored in `localStorage`.
|
|
65
|
+
|
|
66
|
+
### Option A — Global singleton API (recommended)
|
|
67
|
+
|
|
68
|
+
Best for single-page apps where the dialer is always present.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { convirzaDialer } from '@convirza/dialer-sdk';
|
|
72
|
+
|
|
73
|
+
// Get tokens from user session (set during login)
|
|
74
|
+
const accessToken = localStorage.getItem('access_token');
|
|
75
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
76
|
+
|
|
77
|
+
// Initialize widget (auto-fetches SIP config from OAuth session endpoint)
|
|
78
|
+
convirzaDialer.init({
|
|
79
|
+
access_token: accessToken,
|
|
80
|
+
refresh_token: refreshToken,
|
|
81
|
+
oauth_endpoint: 'https://stag-5-oauth.convirza.com/oauth/internal',
|
|
82
|
+
api_url: 'https://stag-5-dialer-apis.convirza.com',
|
|
83
|
+
dark_theme: true,
|
|
84
|
+
primaryColor: '#6366f1',
|
|
85
|
+
brandName: 'Acme Corp',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Listen for token refresh (only fired if access_token expires)
|
|
89
|
+
window.addEventListener('dialer-token-refreshed', (e) => {
|
|
90
|
+
localStorage.setItem('access_token', e.detail.access_token);
|
|
91
|
+
localStorage.setItem('refresh_token', e.detail.refresh_token);
|
|
92
|
+
console.log('Dialer tokens refreshed');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Handle auth failure (refresh_token expired)
|
|
96
|
+
window.addEventListener('dialer-auth-failed', (e) => {
|
|
97
|
+
console.error('Dialer auth failed:', e.detail.error);
|
|
98
|
+
localStorage.clear();
|
|
99
|
+
window.location.href = '/login';
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Cross-tab logout detection (auto-destroys widget)
|
|
103
|
+
// Storage event fires when another tab removes access_token
|
|
104
|
+
window.addEventListener('storage', (e) => {
|
|
105
|
+
if (e.key === 'access_token' && !e.newValue) {
|
|
106
|
+
convirzaDialer.destroy();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Place a call
|
|
111
|
+
convirzaDialer.call('+15551234567');
|
|
112
|
+
|
|
113
|
+
// Control active call
|
|
114
|
+
convirzaDialer.mute(true);
|
|
115
|
+
convirzaDialer.hold(true);
|
|
116
|
+
|
|
117
|
+
// End the active call
|
|
118
|
+
convirzaDialer.endCall();
|
|
119
|
+
|
|
120
|
+
// Clean up on logout
|
|
121
|
+
convirzaDialer.destroy();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Option B — Standalone helper functions
|
|
125
|
+
|
|
126
|
+
Same as singleton, but exposed as named exports for tree-shaking.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { initDialer, destroyDialer, isDialerInitialized } from '@convirza/dialer-sdk';
|
|
130
|
+
|
|
131
|
+
const accessToken = localStorage.getItem('access_token');
|
|
132
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
133
|
+
|
|
134
|
+
initDialer({
|
|
135
|
+
access_token: accessToken,
|
|
136
|
+
refresh_token: refreshToken,
|
|
137
|
+
api_url: 'https://stag-5-dialer-apis.convirza.com',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (isDialerInitialized()) {
|
|
141
|
+
destroyDialer();
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Option C — Web Component (`<convirza-dialer>`)
|
|
146
|
+
|
|
147
|
+
Embed the dialer widget as a custom HTML element.
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<script type="module">
|
|
151
|
+
import '@convirza/dialer-sdk';
|
|
152
|
+
|
|
153
|
+
const accessToken = localStorage.getItem('access_token');
|
|
154
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
155
|
+
|
|
156
|
+
const dialer = document.createElement('convirza-dialer');
|
|
157
|
+
dialer.setAttribute('access-token', accessToken);
|
|
158
|
+
dialer.setAttribute('refresh-token', refreshToken);
|
|
159
|
+
dialer.setAttribute('auto-configure', 'true');
|
|
160
|
+
dialer.setAttribute('oauth-endpoint', 'https://stag-5-oauth.convirza.com/oauth/internal');
|
|
161
|
+
dialer.setAttribute('theme', 'dark');
|
|
162
|
+
dialer.setAttribute('api-url', 'https://stag-5-dialer-apis.convirza.com');
|
|
163
|
+
|
|
164
|
+
// Listen for token refresh (only fires if access_token expires)
|
|
165
|
+
dialer.addEventListener('token-refreshed', (e) => {
|
|
166
|
+
localStorage.setItem('access_token', e.detail.access_token);
|
|
167
|
+
localStorage.setItem('refresh_token', e.detail.refresh_token);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Handle auth failure
|
|
171
|
+
dialer.addEventListener('auth-failed', (e) => {
|
|
172
|
+
console.error('Auth failed:', e.detail.error);
|
|
173
|
+
window.location.href = '/login';
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
document.body.appendChild(dialer);
|
|
177
|
+
</script>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Attributes:**
|
|
181
|
+
|
|
182
|
+
- `access-token` — Access token from user login (used first)
|
|
183
|
+
- `refresh-token` — Refresh token (fallback if access_token expires)
|
|
184
|
+
- `auto-configure="true"` — Enables auto-config flow
|
|
185
|
+
- `oauth-endpoint` — OAuth base URL (default: `https://stag-5-oauth.convirza.com/oauth/internal`)
|
|
186
|
+
- `api-url` — API base URL for park slots, call history
|
|
187
|
+
|
|
188
|
+
**Events:**
|
|
189
|
+
|
|
190
|
+
- `token-refreshed` — New tokens received (only if access_token expired)
|
|
191
|
+
- `auth-failed` — Both tokens failed (redirect to login)
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Authentication Flow
|
|
196
|
+
|
|
197
|
+
### How It Works
|
|
198
|
+
|
|
199
|
+
Widget auto-configures SIP credentials from OAuth session data. No manual SIP configuration needed.
|
|
200
|
+
|
|
201
|
+
**1. User logs into main app**
|
|
202
|
+
|
|
203
|
+
Your app authenticates user → receives `access_token` + `refresh_token`:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
// Your login handler
|
|
207
|
+
const response = await fetch('https://stag-5-oauth.convirza.com/oauth/internal/token', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ email, password }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const { access_token, refresh_token } = response.data;
|
|
214
|
+
|
|
215
|
+
localStorage.setItem('access_token', access_token);
|
|
216
|
+
localStorage.setItem('refresh_token', refresh_token);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**2. Initialize dialer with tokens**
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
convirzaDialer.init({
|
|
223
|
+
access_token: localStorage.getItem('access_token'),
|
|
224
|
+
refresh_token: localStorage.getItem('refresh_token'),
|
|
225
|
+
oauth_endpoint: 'https://stag-5-oauth.convirza.com/oauth/internal',
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**3. Widget fetches SIP config**
|
|
230
|
+
|
|
231
|
+
Widget calls `GET /oauth/internal/session` with `access_token` → gets user's dialer config:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"user": {
|
|
236
|
+
"user_id": 123,
|
|
237
|
+
"dialer_config": {
|
|
238
|
+
"domains": [
|
|
239
|
+
{
|
|
240
|
+
"sip_domain": "prod-registration",
|
|
241
|
+
"sip_proxy_1": "sip.example.com",
|
|
242
|
+
"extensions": [{ "extension": "1001" }]
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**4. Widget auto-configures SIP credentials**
|
|
251
|
+
|
|
252
|
+
Internally sets:
|
|
253
|
+
|
|
254
|
+
- SIP username: `1001` (from extension)
|
|
255
|
+
- SIP password: `123@prod-registration` (user_id @ sip_domain)
|
|
256
|
+
- SIP domain: `prod-registration`
|
|
257
|
+
- WebSocket server: `wss://sip.example.com:8443`
|
|
258
|
+
|
|
259
|
+
**5. Widget connects to SIP server**
|
|
260
|
+
|
|
261
|
+
SIP registration happens automatically. Widget ready to make calls.
|
|
262
|
+
|
|
263
|
+
### Token Refresh
|
|
264
|
+
|
|
265
|
+
**Automatic refresh on expiry:**
|
|
266
|
+
|
|
267
|
+
1. Widget tries `GET /session` with `access_token`
|
|
268
|
+
2. If 401 Unauthorized → `access_token` expired
|
|
269
|
+
3. Widget calls `POST /refresh-token` with `refresh_token`
|
|
270
|
+
4. Gets new `access_token` + `refresh_token`
|
|
271
|
+
5. Widget emits `dialer-token-refreshed` event
|
|
272
|
+
6. Your app updates localStorage
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
window.addEventListener('dialer-token-refreshed', (e) => {
|
|
276
|
+
localStorage.setItem('access_token', e.detail.access_token);
|
|
277
|
+
localStorage.setItem('refresh_token', e.detail.refresh_token);
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**If refresh_token also expired:**
|
|
282
|
+
|
|
283
|
+
Widget emits `dialer-auth-failed` → redirect user to login.
|
|
284
|
+
|
|
285
|
+
### Logout Detection
|
|
286
|
+
|
|
287
|
+
**Same-tab logout:**
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
// Your logout handler
|
|
291
|
+
function logout() {
|
|
292
|
+
localStorage.removeItem('access_token');
|
|
293
|
+
localStorage.removeItem('refresh_token');
|
|
294
|
+
convirzaDialer.destroy(); // Widget destroyed
|
|
295
|
+
window.location.href = '/login';
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Cross-tab logout:**
|
|
300
|
+
|
|
301
|
+
Widget automatically destroys when `access_token` removed in another tab:
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// Storage event listener (built into widget)
|
|
305
|
+
window.addEventListener('storage', (e) => {
|
|
306
|
+
if (e.key === 'access_token' && !e.newValue) {
|
|
307
|
+
convirzaDialer.destroy(); // Auto-destroyed
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
User logs out in Tab A → widget destroyed in Tab B automatically.
|
|
313
|
+
|
|
314
|
+
### Backend Requirements
|
|
315
|
+
|
|
316
|
+
Widget expects these OAuth endpoints:
|
|
317
|
+
|
|
318
|
+
**Session endpoint:**
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
GET /oauth/internal/session
|
|
322
|
+
Authorization: Bearer {access_token}
|
|
323
|
+
|
|
324
|
+
Returns: { user: { dialer_config: {...} } }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Refresh endpoint:**
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
POST /oauth/internal/refresh-token
|
|
331
|
+
Body: { refresh_token: "..." }
|
|
332
|
+
|
|
333
|
+
Returns: { access_token, refresh_token, user: { dialer_config: {...} } }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## API Reference
|
|
339
|
+
|
|
340
|
+
### `convirzaDialer` (singleton `DialerAPI`)
|
|
341
|
+
|
|
342
|
+
The default export is a singleton that manages the `<convirza-dialer>` web component and provides a simple imperative API.
|
|
343
|
+
|
|
344
|
+
#### `convirzaDialer.init(config)`
|
|
345
|
+
|
|
346
|
+
Initialize the dialer and inject the widget into the page. **Must be called before any other method.**
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
convirzaDialer.init(config: DialerInitConfig): void
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
| Field | Type | Required | Description |
|
|
353
|
+
| ---------------- | --------- | -------- | -------------------------------------------- |
|
|
354
|
+
| `access_token` | `string` | Yes | Access token from user login session |
|
|
355
|
+
| `refresh_token` | `string` | Yes | Refresh token (used if access_token expires) |
|
|
356
|
+
| `oauth_endpoint` | `string` | No | OAuth base URL (default: internal) |
|
|
357
|
+
| `api_url` | `string` | No | API base URL for park slots, call history |
|
|
358
|
+
| `brandName` | `string` | No | Brand name shown in widget header |
|
|
359
|
+
| `brandLogo` | `string` | No | URL of brand logo image |
|
|
360
|
+
| `dark_theme` | `boolean` | No | Use dark theme (default `false`) |
|
|
361
|
+
| `showPopup` | `boolean` | No | Start with widget expanded |
|
|
362
|
+
| `primaryColor` | `string` | No | CSS color for primary UI elements (hex/rgb) |
|
|
363
|
+
| `accentColor` | `string` | No | CSS color for accent elements |
|
|
364
|
+
|
|
365
|
+
OAuth response provides SIP credentials automatically. User never sees SIP details.
|
|
366
|
+
|
|
367
|
+
**Example:**
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
convirzaDialer.init({
|
|
371
|
+
oauth_endpoint: 'https://api.example.com/oauth/token',
|
|
372
|
+
username: 'user@example.com',
|
|
373
|
+
password: 'userpassword',
|
|
374
|
+
dark_theme: true,
|
|
375
|
+
primaryColor: '#6366f1',
|
|
376
|
+
brandName: 'Acme Corp',
|
|
377
|
+
brandLogo: 'https://example.com/logo.png',
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### `convirzaDialer.refresh()`
|
|
382
|
+
|
|
383
|
+
Re-initialize the widget using the same config. Useful after credential rotation.
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
convirzaDialer.refresh(): void
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `convirzaDialer.call(phoneNumber?)`
|
|
390
|
+
|
|
391
|
+
Place an outbound call. Opens the widget popup if collapsed.
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
convirzaDialer.call(phoneNumber?: string): void
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Parameters:**
|
|
398
|
+
|
|
399
|
+
- `phoneNumber` (optional): E.164 phone number (e.g., `+15551234567`) or SIP extension. If omitted, just opens the widget.
|
|
400
|
+
|
|
401
|
+
**Example:**
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
// Dial number immediately
|
|
405
|
+
convirzaDialer.call('+15551234567');
|
|
406
|
+
|
|
407
|
+
// Open widget without dialing
|
|
408
|
+
convirzaDialer.call();
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### `convirzaDialer.endCall()`
|
|
412
|
+
|
|
413
|
+
Hang up the active call. Throws if there is no active call.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
convirzaDialer.endCall(): void
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Example:**
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
if (convirzaDialer.getActiveCallID()) {
|
|
423
|
+
convirzaDialer.endCall();
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### `convirzaDialer.mute(enable?)`
|
|
428
|
+
|
|
429
|
+
Mute or unmute the microphone. Toggles if `enable` is omitted.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
convirzaDialer.mute(enable?: boolean): boolean // returns current mute state
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Parameters:**
|
|
436
|
+
|
|
437
|
+
- `enable` (optional): `true` to mute, `false` to unmute. Omit to toggle.
|
|
438
|
+
|
|
439
|
+
**Returns:** Current mute state (`true` = muted).
|
|
440
|
+
|
|
441
|
+
**Example:**
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
// Toggle mute
|
|
445
|
+
const isMuted = convirzaDialer.mute();
|
|
446
|
+
|
|
447
|
+
// Explicitly mute
|
|
448
|
+
convirzaDialer.mute(true);
|
|
449
|
+
|
|
450
|
+
// Explicitly unmute
|
|
451
|
+
convirzaDialer.mute(false);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
#### `convirzaDialer.hold(enable?)`
|
|
455
|
+
|
|
456
|
+
Place the call on hold or take it off hold.
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
convirzaDialer.hold(enable?: boolean): boolean
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Parameters:**
|
|
463
|
+
|
|
464
|
+
- `enable` (optional): `true` to hold, `false` to unhold. Omit to toggle.
|
|
465
|
+
|
|
466
|
+
**Returns:** Current hold state (`true` = on hold).
|
|
467
|
+
|
|
468
|
+
**Example:**
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
// Toggle hold
|
|
472
|
+
const isOnHold = convirzaDialer.hold();
|
|
473
|
+
|
|
474
|
+
// Explicitly hold
|
|
475
|
+
convirzaDialer.hold(true);
|
|
476
|
+
|
|
477
|
+
// Explicitly resume
|
|
478
|
+
convirzaDialer.hold(false);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### `convirzaDialer.record(enable?)`
|
|
482
|
+
|
|
483
|
+
Start or stop server-side recording. Sends SIP INFO message to backend.
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
convirzaDialer.record(enable?: boolean): boolean // returns current recording state
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Parameters:**
|
|
490
|
+
|
|
491
|
+
- `enable` (optional): `true` to start recording, `false` to stop. Omit to toggle.
|
|
492
|
+
|
|
493
|
+
**Returns:** Current recording state (`true` = recording).
|
|
494
|
+
|
|
495
|
+
**Note:** Server must support `application/x-recording-command` SIP INFO messages.
|
|
496
|
+
|
|
497
|
+
**Example:**
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
// Start recording
|
|
501
|
+
convirzaDialer.record(true);
|
|
502
|
+
|
|
503
|
+
// Stop recording
|
|
504
|
+
convirzaDialer.record(false);
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### `convirzaDialer.transfer(target)`
|
|
508
|
+
|
|
509
|
+
Blind-transfer the active call to another extension or phone number.
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
convirzaDialer.transfer(target: string): void
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Parameters:**
|
|
516
|
+
|
|
517
|
+
- `target`: SIP URI (e.g., `sip:1002@pbx.example.com`) or bare extension (e.g., `1002`).
|
|
518
|
+
|
|
519
|
+
**Example:**
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
// Transfer to extension
|
|
523
|
+
convirzaDialer.transfer('1002');
|
|
524
|
+
|
|
525
|
+
// Transfer to external number
|
|
526
|
+
convirzaDialer.transfer('+15559876543');
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### `convirzaDialer.showPopup()`
|
|
530
|
+
|
|
531
|
+
Expand the dialer widget.
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
convirzaDialer.showPopup(): void
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### `convirzaDialer.hidePopup()`
|
|
538
|
+
|
|
539
|
+
Collapse the dialer widget to floating action button (FAB).
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
convirzaDialer.hidePopup(): void
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
#### `convirzaDialer.getActiveCallID()`
|
|
546
|
+
|
|
547
|
+
Returns the ID of the active call, or `null` if idle.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
convirzaDialer.getActiveCallID(): string | null
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Example:**
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
const callId = convirzaDialer.getActiveCallID();
|
|
557
|
+
if (callId) {
|
|
558
|
+
console.log('Active call:', callId);
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### `convirzaDialer.callList(limit?)`
|
|
563
|
+
|
|
564
|
+
Returns the in-session call history (most recent first).
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
convirzaDialer.callList(limit?: number): CallListItem[]
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Parameters:**
|
|
571
|
+
|
|
572
|
+
- `limit` (optional): Max number of calls to return.
|
|
573
|
+
|
|
574
|
+
**Returns:** Array of `CallListItem`:
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
interface CallListItem {
|
|
578
|
+
id: string;
|
|
579
|
+
phoneNumber: string;
|
|
580
|
+
timestamp: number; // Unix timestamp (ms)
|
|
581
|
+
duration?: number; // Call duration in milliseconds
|
|
582
|
+
direction: 'inbound' | 'outbound';
|
|
583
|
+
status: 'answered' | 'missed' | 'voicemail';
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Example:**
|
|
588
|
+
|
|
589
|
+
```js
|
|
590
|
+
const recent = convirzaDialer.callList(10);
|
|
591
|
+
recent.forEach((call) => {
|
|
592
|
+
console.log(`${call.direction} call to ${call.phoneNumber}: ${call.status}`);
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### `convirzaDialer.destroy()`
|
|
597
|
+
|
|
598
|
+
Remove the widget from DOM and clean up all event listeners. Call on logout or page unload.
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
convirzaDialer.destroy(): void
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Example:**
|
|
605
|
+
|
|
606
|
+
```js
|
|
607
|
+
// On user logout
|
|
608
|
+
function logout() {
|
|
609
|
+
convirzaDialer.destroy();
|
|
610
|
+
// ... other cleanup
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
#### Read-only properties
|
|
615
|
+
|
|
616
|
+
| Property | Type | Description |
|
|
617
|
+
| ------------------ | --------- | ----------------------- |
|
|
618
|
+
| `isMutedState` | `boolean` | Current mute state |
|
|
619
|
+
| `isRecordingState` | `boolean` | Current recording state |
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Helper functions
|
|
624
|
+
|
|
625
|
+
#### `initDialer(config)`
|
|
626
|
+
|
|
627
|
+
Initialize the global dialer. Warns and no-ops if called a second time.
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
import { initDialer } from '@convirza/dialer-sdk';
|
|
631
|
+
initDialer(config: DialerInitConfig): void
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
#### `destroyDialer()`
|
|
635
|
+
|
|
636
|
+
Destroy the global dialer. Warns if not initialized.
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { destroyDialer } from '@convirza/dialer-sdk';
|
|
640
|
+
destroyDialer(): void
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### `isDialerInitialized()`
|
|
644
|
+
|
|
645
|
+
Returns `true` if the global dialer has been initialized.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { isDialerInitialized } from '@convirza/dialer-sdk';
|
|
649
|
+
isDialerInitialized(): boolean
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### `SipAdapter`
|
|
655
|
+
|
|
656
|
+
Low-level SIP client for headless / custom-UI integrations. Requires `sip.js` as a UMD global (`window.SIP` or `window.sip`).
|
|
657
|
+
|
|
658
|
+
#### Constructor
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
new SipAdapter(config: SipConfig, callbacks?: SipCallbacks)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
**`SipConfig`**
|
|
665
|
+
|
|
666
|
+
| Field | Type | Required | Description |
|
|
667
|
+
| -------------- | ---------------- | -------- | ------------------------------------------------- |
|
|
668
|
+
| `sipUsername` | `string` | Yes | SIP username |
|
|
669
|
+
| `sipPassword` | `string` | Yes | SIP password |
|
|
670
|
+
| `sipDomain` | `string` | Yes | SIP registrar domain |
|
|
671
|
+
| `wsServers` | `string[]` | Yes | WebSocket proxy URL(s) |
|
|
672
|
+
| `displayName` | `string` | No | Caller display name |
|
|
673
|
+
| `iceServers` | `RTCIceServer[]` | No | Override STUN/TURN servers (default: Google STUN) |
|
|
674
|
+
| `microphoneId` | `string` | No | Preferred microphone device ID |
|
|
675
|
+
| `speakerId` | `string` | No | Preferred speaker device ID |
|
|
676
|
+
| `apiUrl` | `string` | No | Backend API URL (required for call parking) |
|
|
677
|
+
| `authToken` | `string` | No | Bearer token (required for authenticated APIs) |
|
|
678
|
+
|
|
679
|
+
**`SipCallbacks`**
|
|
680
|
+
|
|
681
|
+
| Callback | Signature | When it fires |
|
|
682
|
+
| ------------------------ | ---------------------------------------------------- | --------------------------------------------------------- |
|
|
683
|
+
| `onRegistered` | `() => void` | SIP registration succeeded |
|
|
684
|
+
| `onUnregistered` | `() => void` | SIP unregistered (intentional or expired) |
|
|
685
|
+
| `onRegistrationFailed` | `(cause?: string) => void` | Registration failed (e.g., 403 wrong password) |
|
|
686
|
+
| `onTransportError` | `(error: Error) => void` | WebSocket transport error |
|
|
687
|
+
| `onConnecting` | `() => void` | Transport connecting (before registration) |
|
|
688
|
+
| `onCallRinging` | `() => void` | Outbound call is ringing (received 180 Ringing) |
|
|
689
|
+
| `onCallAnswered` | `() => void` | Call connected (received 200 OK) |
|
|
690
|
+
| `onCallEnded` | `(reason?: string) => void` | Call ended by any party (BYE, CANCEL, timeout) |
|
|
691
|
+
| `onCallFailed` | `(error: Error) => void` | Call setup or media failure (e.g., no WebRTC permissions) |
|
|
692
|
+
| `onIncomingCall` | `(phoneNumber: string, callerName?: string) => void` | Inbound INVITE received |
|
|
693
|
+
| `onRemoteHold` | `(isOnHold: boolean) => void` | Remote party placed call on hold |
|
|
694
|
+
| `onQualityChange` | `(metrics: CallQualityMetrics) => void` | Periodic RTC quality update (every 2s) |
|
|
695
|
+
| `onRecordingStateChange` | `(recording: boolean) => void` | Server recording started/stopped (via SIP INFO) |
|
|
696
|
+
| `onTransferSucceeded` | `() => void` | Blind transfer (REFER) accepted by server |
|
|
697
|
+
| `onTransferFailed` | `(reason: string) => void` | Blind transfer rejected by server |
|
|
698
|
+
| `onDevicesChanged` | `(devices: AudioDevice[]) => void` | Audio device list changed (mic/speaker plugged in/out) |
|
|
699
|
+
| `onError` | `(error: Error) => void` | General unhandled error |
|
|
700
|
+
|
|
701
|
+
#### Methods
|
|
702
|
+
|
|
703
|
+
##### `sip.connect()`
|
|
704
|
+
|
|
705
|
+
Connect to the WebSocket transport and register with the SIP server. **Must be called first.**
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
await sip.connect(): Promise<void>
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**Example:**
|
|
712
|
+
|
|
713
|
+
```js
|
|
714
|
+
await sip.connect();
|
|
715
|
+
console.log('Registered with SIP server');
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
**Errors:**
|
|
719
|
+
|
|
720
|
+
- Throws if SIP.js not loaded
|
|
721
|
+
- Throws if WebSocket connection fails
|
|
722
|
+
- Fires `onRegistrationFailed` on auth failure
|
|
723
|
+
|
|
724
|
+
##### `sip.disconnect()`
|
|
725
|
+
|
|
726
|
+
End any active call, unregister, and close the WebSocket transport.
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
await sip.disconnect(): Promise<void>
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Example:**
|
|
733
|
+
|
|
734
|
+
```js
|
|
735
|
+
await sip.disconnect();
|
|
736
|
+
console.log('Disconnected from SIP server');
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
##### `sip.call(phoneNumber, options?)`
|
|
740
|
+
|
|
741
|
+
Place an outbound call.
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
await sip.call(phoneNumber: string, options?: {
|
|
745
|
+
callerId?: string; // P-Asserted-Identity number (E.164)
|
|
746
|
+
callerIdName?: string; // P-Asserted-Identity display name
|
|
747
|
+
}): Promise<void>
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
**Parameters:**
|
|
751
|
+
|
|
752
|
+
- `phoneNumber`: E.164 phone number or SIP extension
|
|
753
|
+
- `options.callerId`: Override outbound caller ID (if supported by server)
|
|
754
|
+
- `options.callerIdName`: Caller display name
|
|
755
|
+
|
|
756
|
+
**Example:**
|
|
757
|
+
|
|
758
|
+
```js
|
|
759
|
+
// Simple call
|
|
760
|
+
await sip.call('+15551234567');
|
|
761
|
+
|
|
762
|
+
// With custom caller ID
|
|
763
|
+
await sip.call('+15551234567', {
|
|
764
|
+
callerId: '+15559876543',
|
|
765
|
+
callerIdName: 'Support Line',
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Fires:**
|
|
770
|
+
|
|
771
|
+
- `onCallRinging` when remote party rings
|
|
772
|
+
- `onCallAnswered` when call connects
|
|
773
|
+
- `onCallFailed` on error
|
|
774
|
+
|
|
775
|
+
##### `sip.endCall()`
|
|
776
|
+
|
|
777
|
+
Hang up the current call (sends BYE or CANCEL as appropriate).
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
await sip.endCall(): Promise<void>
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Example:**
|
|
784
|
+
|
|
785
|
+
```js
|
|
786
|
+
if (sip.hasActiveCall) {
|
|
787
|
+
await sip.endCall();
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
##### `sip.answerCall()`
|
|
792
|
+
|
|
793
|
+
Answer an incoming call. **Must be called after `onIncomingCall` fires.**
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
await sip.answerCall(): Promise<void>
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Example:**
|
|
800
|
+
|
|
801
|
+
```js
|
|
802
|
+
const sip = new SipAdapter(config, {
|
|
803
|
+
onIncomingCall: (number, name) => {
|
|
804
|
+
console.log(`Incoming call from ${name} <${number}>`);
|
|
805
|
+
sip.answerCall(); // Auto-answer
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
##### `sip.rejectCall()`
|
|
811
|
+
|
|
812
|
+
Reject an incoming call (sends 486 Busy Here).
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
await sip.rejectCall(): Promise<void>
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**Example:**
|
|
819
|
+
|
|
820
|
+
```js
|
|
821
|
+
const sip = new SipAdapter(config, {
|
|
822
|
+
onIncomingCall: (number) => {
|
|
823
|
+
if (number === '+15551111111') {
|
|
824
|
+
sip.rejectCall(); // Block this caller
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
##### `sip.mute(enable?)`
|
|
831
|
+
|
|
832
|
+
Mute/unmute the local microphone track. Toggles if `enable` is omitted.
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
sip.mute(enable?: boolean): boolean // returns new mute state
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Example:**
|
|
839
|
+
|
|
840
|
+
```js
|
|
841
|
+
// Toggle mute
|
|
842
|
+
const isMuted = sip.mute();
|
|
843
|
+
|
|
844
|
+
// Explicitly mute
|
|
845
|
+
sip.mute(true);
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
##### `sip.hold(enable?)`
|
|
849
|
+
|
|
850
|
+
Put the call on hold (`true`) or take it off hold (`false`). Defaults to `true`.
|
|
851
|
+
|
|
852
|
+
```ts
|
|
853
|
+
await sip.hold(enable?: boolean): Promise<void>
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
**Example:**
|
|
857
|
+
|
|
858
|
+
```js
|
|
859
|
+
// Put on hold
|
|
860
|
+
await sip.hold(true);
|
|
861
|
+
|
|
862
|
+
// Resume
|
|
863
|
+
await sip.hold(false);
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Note:** Uses SIP re-INVITE with `sendrecv`/`sendonly` SDP attributes.
|
|
867
|
+
|
|
868
|
+
##### `sip.sendDTMF(digit)`
|
|
869
|
+
|
|
870
|
+
Send a DTMF digit via SIP INFO (RFC 2833 alternative).
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
sip.sendDTMF(digit: string): void // digit: '0'-'9', '*', '#'
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Example:**
|
|
877
|
+
|
|
878
|
+
```js
|
|
879
|
+
// Send IVR menu choice
|
|
880
|
+
sip.sendDTMF('1');
|
|
881
|
+
|
|
882
|
+
// Send PIN
|
|
883
|
+
'1234'.split('').forEach((d) => sip.sendDTMF(d));
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Note:** Uses `application/dtmf-relay` content type.
|
|
887
|
+
|
|
888
|
+
##### `sip.startRecording()`
|
|
889
|
+
|
|
890
|
+
Signal the server to start recording (sends SIP INFO with `application/x-recording-command: start`).
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
sip.startRecording(): void
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
**Example:**
|
|
897
|
+
|
|
898
|
+
```js
|
|
899
|
+
sip.startRecording();
|
|
900
|
+
console.log('Recording started');
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
##### `sip.stopRecording()`
|
|
904
|
+
|
|
905
|
+
Signal the server to stop recording.
|
|
906
|
+
|
|
907
|
+
```ts
|
|
908
|
+
sip.stopRecording(): void
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
##### `sip.park(parkExtension)`
|
|
912
|
+
|
|
913
|
+
Park the call at a valet park slot via the Convirza REST API. Requires `apiUrl` and `authToken` in config.
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
await sip.park(parkExtension: string): Promise<void>
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Parameters:**
|
|
920
|
+
|
|
921
|
+
- `parkExtension`: Park slot extension (e.g., `'700'`)
|
|
922
|
+
|
|
923
|
+
**Example:**
|
|
924
|
+
|
|
925
|
+
```js
|
|
926
|
+
// Park call at slot 700
|
|
927
|
+
await sip.park('700');
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
**See:** [Call Parking Guide](#call-parking) for full details.
|
|
931
|
+
|
|
932
|
+
##### `sip.blindTransfer(target)`
|
|
933
|
+
|
|
934
|
+
Blind-transfer the call via SIP REFER. `target` can be a full SIP URI or a bare extension.
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
await sip.blindTransfer(target: string): Promise<void>
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
**Parameters:**
|
|
941
|
+
|
|
942
|
+
- `target`: SIP URI (e.g., `sip:1002@pbx.example.com`) or extension (e.g., `1002`)
|
|
943
|
+
|
|
944
|
+
**Example:**
|
|
945
|
+
|
|
946
|
+
```js
|
|
947
|
+
// Transfer to extension
|
|
948
|
+
await sip.blindTransfer('1002');
|
|
949
|
+
|
|
950
|
+
// Transfer to external number
|
|
951
|
+
await sip.blindTransfer('sip:+15559876543@pbx.example.com');
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Fires:**
|
|
955
|
+
|
|
956
|
+
- `onTransferSucceeded` on 202 Accepted
|
|
957
|
+
- `onTransferFailed` on rejection
|
|
958
|
+
|
|
959
|
+
##### `sip.setMicrophone(deviceId)`
|
|
960
|
+
|
|
961
|
+
Hot-swap the active microphone without interrupting the call.
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
await sip.setMicrophone(deviceId: string): Promise<void>
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
**Parameters:**
|
|
968
|
+
|
|
969
|
+
- `deviceId`: MediaDeviceInfo device ID
|
|
970
|
+
|
|
971
|
+
**Example:**
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
const devices = await sip.getAudioDevices();
|
|
975
|
+
const builtInMic = devices.microphones.find((d) => d.label.includes('Built-in'));
|
|
976
|
+
if (builtInMic) {
|
|
977
|
+
await sip.setMicrophone(builtInMic.deviceId);
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
**Note:** Uses `replaceTrack()` on the sender — no interruption to call.
|
|
982
|
+
|
|
983
|
+
##### `sip.setSpeaker(deviceId)`
|
|
984
|
+
|
|
985
|
+
Switch the audio output device.
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
await sip.setSpeaker(deviceId: string): Promise<void>
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
**Parameters:**
|
|
992
|
+
|
|
993
|
+
- `deviceId`: MediaDeviceInfo device ID
|
|
994
|
+
|
|
995
|
+
**Example:**
|
|
996
|
+
|
|
997
|
+
```js
|
|
998
|
+
const devices = await sip.getAudioDevices();
|
|
999
|
+
const headphones = devices.speakers.find((d) => d.label.includes('Headphones'));
|
|
1000
|
+
if (headphones) {
|
|
1001
|
+
await sip.setSpeaker(headphones.deviceId);
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
**Browser support:** Chrome/Edge only (`setSinkId` API). Silently no-ops in Firefox/Safari.
|
|
1006
|
+
|
|
1007
|
+
##### `sip.getAudioDevices()`
|
|
1008
|
+
|
|
1009
|
+
Enumerate available microphones and speakers.
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
await sip.getAudioDevices(): Promise<{
|
|
1013
|
+
microphones: Array<{ deviceId: string; label: string }>;
|
|
1014
|
+
speakers: Array<{ deviceId: string; label: string }>;
|
|
1015
|
+
}>
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
**Example:**
|
|
1019
|
+
|
|
1020
|
+
```js
|
|
1021
|
+
const { microphones, speakers } = await sip.getAudioDevices();
|
|
1022
|
+
console.log(
|
|
1023
|
+
'Mics:',
|
|
1024
|
+
microphones.map((m) => m.label)
|
|
1025
|
+
);
|
|
1026
|
+
console.log(
|
|
1027
|
+
'Speakers:',
|
|
1028
|
+
speakers.map((s) => s.label)
|
|
1029
|
+
);
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
**Note:** Requires microphone permission. Labels are generic until permission granted.
|
|
1033
|
+
|
|
1034
|
+
##### `sip.getCurrentDevices()`
|
|
1035
|
+
|
|
1036
|
+
Return the currently active microphone and speaker device IDs.
|
|
1037
|
+
|
|
1038
|
+
```ts
|
|
1039
|
+
sip.getCurrentDevices(): { microphoneId: string | null; speakerId: string | null }
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
**Example:**
|
|
1043
|
+
|
|
1044
|
+
```js
|
|
1045
|
+
const { microphoneId, speakerId } = sip.getCurrentDevices();
|
|
1046
|
+
console.log('Active mic:', microphoneId);
|
|
1047
|
+
console.log('Active speaker:', speakerId);
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
##### `sip.getCallQuality()`
|
|
1051
|
+
|
|
1052
|
+
Return the last WebRTC quality metrics sample, or `null` if no call is active.
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
sip.getCallQuality(): CallQualityMetrics | null
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
**Returns:**
|
|
1059
|
+
|
|
1060
|
+
```ts
|
|
1061
|
+
interface CallQualityMetrics {
|
|
1062
|
+
bitrate: number; // kbps
|
|
1063
|
+
packetLoss: number; // percentage (0-100)
|
|
1064
|
+
jitter: number; // ms
|
|
1065
|
+
latency: number; // ms (round-trip time)
|
|
1066
|
+
audioLevel: number; // 0–1 (remote audio volume)
|
|
1067
|
+
quality: 'excellent' | 'good' | 'fair' | 'poor';
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
**Example:**
|
|
1072
|
+
|
|
1073
|
+
```js
|
|
1074
|
+
const metrics = sip.getCallQuality();
|
|
1075
|
+
if (metrics && metrics.quality === 'poor') {
|
|
1076
|
+
console.warn('Poor call quality:', metrics.packetLoss + '% packet loss');
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
##### `sip.getCallUuid()`
|
|
1081
|
+
|
|
1082
|
+
Return the active call UUID (FreeSWITCH channel UUID or SIP Call-ID). Useful for server-side operations.
|
|
1083
|
+
|
|
1084
|
+
```ts
|
|
1085
|
+
sip.getCallUuid(): string | null
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Example:**
|
|
1089
|
+
|
|
1090
|
+
```js
|
|
1091
|
+
const uuid = sip.getCallUuid();
|
|
1092
|
+
console.log('Call UUID for backend lookup:', uuid);
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
##### `sip.reconnect()`
|
|
1096
|
+
|
|
1097
|
+
Trigger an immediate reconnection attempt (resets backoff counter). Useful on the browser `online` event.
|
|
1098
|
+
|
|
1099
|
+
```ts
|
|
1100
|
+
sip.reconnect(): void
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
**Example:**
|
|
1104
|
+
|
|
1105
|
+
```js
|
|
1106
|
+
window.addEventListener('online', () => {
|
|
1107
|
+
sip.reconnect(); // Force reconnect after network restored
|
|
1108
|
+
});
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
##### Read-only properties
|
|
1112
|
+
|
|
1113
|
+
| Property | Type | Description |
|
|
1114
|
+
| --------------- | --------- | ------------------------------------------------------------ |
|
|
1115
|
+
| `isConnected` | `boolean` | Whether the WebSocket transport is up |
|
|
1116
|
+
| `hasActiveCall` | `boolean` | Whether a SIP session is in progress (any state except idle) |
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
### `AudioDeviceManager`
|
|
1121
|
+
|
|
1122
|
+
Standalone audio device manager. Used internally by `SipAdapter` but also exportable for custom UIs.
|
|
1123
|
+
|
|
1124
|
+
```ts
|
|
1125
|
+
import { AudioDeviceManager } from '@convirza/dialer-sdk';
|
|
1126
|
+
|
|
1127
|
+
const adm = new AudioDeviceManager({
|
|
1128
|
+
onDevicesChanged: (devices) => console.log(devices),
|
|
1129
|
+
onMicrophoneChanged: (id) => console.log('Mic changed:', id),
|
|
1130
|
+
onSpeakerChanged: (id) => console.log('Speaker changed:', id),
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
const devices = await adm.enumerateDevices();
|
|
1134
|
+
const mics = adm.getMicrophones();
|
|
1135
|
+
const spk = adm.getSpeakers();
|
|
1136
|
+
|
|
1137
|
+
adm.savePreferences({ microphoneId: mics[0].deviceId, speakerId: spk[0].deviceId });
|
|
1138
|
+
const prefs = adm.loadPreferences();
|
|
1139
|
+
|
|
1140
|
+
await adm.applyAudioOutputDevice(audioElement, spk[0].deviceId);
|
|
1141
|
+
|
|
1142
|
+
adm.destroy();
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
| Method | Returns | Description |
|
|
1146
|
+
| -------------------------------------- | --------------------------------- | -------------------------------------------- |
|
|
1147
|
+
| `enumerateDevices()` | `Promise<AudioDevice[]>` | Refresh and return all audio devices |
|
|
1148
|
+
| `refreshDevices()` | `Promise<AudioDevice[]>` | Alias for `enumerateDevices()` |
|
|
1149
|
+
| `getMicrophones()` | `AudioDevice[]` | Cached microphone list |
|
|
1150
|
+
| `getSpeakers()` | `AudioDevice[]` | Cached speaker list |
|
|
1151
|
+
| `savePreferences(prefs)` | `void` | Persist device preferences to `localStorage` |
|
|
1152
|
+
| `loadPreferences()` | `DevicePreferences` | Load persisted preferences |
|
|
1153
|
+
| `applyAudioOutputDevice(el, deviceId)` | `Promise<void>` | Set `setSinkId` on an `<audio>` element |
|
|
1154
|
+
| `getAudioConstraints(microphoneId)` | `Promise<MediaStreamConstraints>` | Build `getUserMedia` constraints |
|
|
1155
|
+
| `destroy()` | `void` | Remove `devicechange` listener |
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
### `CallQualityMonitor`
|
|
1160
|
+
|
|
1161
|
+
Periodically samples WebRTC stats and emits quality metrics.
|
|
1162
|
+
|
|
1163
|
+
```ts
|
|
1164
|
+
import { CallQualityMonitor } from '@convirza/dialer-sdk';
|
|
1165
|
+
|
|
1166
|
+
const monitor = new CallQualityMonitor();
|
|
1167
|
+
monitor.setOnQualityChange((metrics) => {
|
|
1168
|
+
console.log(metrics.quality, metrics.packetLoss, metrics.latency);
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
monitor.start(peerConnection, 2000); // sample every 2 s
|
|
1172
|
+
// ...
|
|
1173
|
+
const last = monitor.getLastMetrics();
|
|
1174
|
+
monitor.stop();
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
| Method | Description |
|
|
1178
|
+
| ------------------------ | ------------------------------------------ |
|
|
1179
|
+
| `start(pc, interval?)` | Start sampling (default 2000 ms) |
|
|
1180
|
+
| `stop()` | Stop sampling and clear state |
|
|
1181
|
+
| `setOnQualityChange(cb)` | Register quality callback |
|
|
1182
|
+
| `getLastMetrics()` | Return last `CallQualityMetrics` or `null` |
|
|
1183
|
+
|
|
1184
|
+
---
|
|
1185
|
+
|
|
1186
|
+
### `PhoneNumbersAPI`
|
|
1187
|
+
|
|
1188
|
+
REST client for fetching provisioned phone numbers from the Convirza backend.
|
|
1189
|
+
|
|
1190
|
+
```ts
|
|
1191
|
+
import { PhoneNumbersAPI } from '@convirza/dialer-sdk';
|
|
1192
|
+
|
|
1193
|
+
const api = new PhoneNumbersAPI('https://api.convirza.com', 'Bearer-token');
|
|
1194
|
+
api.setAuthToken('new-token');
|
|
1195
|
+
|
|
1196
|
+
const result = await api.fetchOrderedNumbers({
|
|
1197
|
+
domainId: 123,
|
|
1198
|
+
limit: 50,
|
|
1199
|
+
status: 'provisioned',
|
|
1200
|
+
});
|
|
1201
|
+
// result: { items: OrderedPhoneNumber[], total: number }
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
| Method | Description |
|
|
1205
|
+
| ----------------------------- | -------------------------------------------- |
|
|
1206
|
+
| `setAuthToken(token)` | Update the bearer token |
|
|
1207
|
+
| `fetchOrderedNumbers(params)` | Fetch provisioned phone numbers for a domain |
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
### `CallHistoryAPI`
|
|
1212
|
+
|
|
1213
|
+
REST client for persisting and querying call history.
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
import { CallHistoryAPI } from '@convirza/dialer-sdk';
|
|
1217
|
+
|
|
1218
|
+
const api = new CallHistoryAPI('https://api.convirza.com', 'Bearer-token');
|
|
1219
|
+
|
|
1220
|
+
const history = await api.fetchHistory({
|
|
1221
|
+
userId: 'user-123',
|
|
1222
|
+
sipDomain: 'sip.example.com',
|
|
1223
|
+
limit: 50,
|
|
1224
|
+
offset: 0,
|
|
1225
|
+
});
|
|
1226
|
+
// history: { calls: CallRecord[], total: number, hasMore: boolean }
|
|
1227
|
+
|
|
1228
|
+
const saved = await api.saveCall({
|
|
1229
|
+
callId: 'abc-123',
|
|
1230
|
+
userId: 'user-123',
|
|
1231
|
+
sipDomain: 'sip.example.com',
|
|
1232
|
+
callerIdNumber: '+15551234567',
|
|
1233
|
+
phoneNumber: '+19876543210',
|
|
1234
|
+
direction: 'outbound',
|
|
1235
|
+
disposition: 'answered',
|
|
1236
|
+
startedAt: Date.now(),
|
|
1237
|
+
durationSeconds: 42,
|
|
1238
|
+
createdAt: Date.now(),
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const updated = await api.updateCall(saved.callId, { disposition: 'missed' });
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
| Method | Description |
|
|
1245
|
+
| ----------------------------- | -------------------------------- |
|
|
1246
|
+
| `setAuthToken(token)` | Update the bearer token |
|
|
1247
|
+
| `fetchHistory(params)` | Fetch paginated call history |
|
|
1248
|
+
| `saveCall(record)` | Create a new call history record |
|
|
1249
|
+
| `updateCall(callId, updates)` | Patch an existing call record |
|
|
1250
|
+
|
|
1251
|
+
---
|
|
1252
|
+
|
|
1253
|
+
## Web Component Reference
|
|
1254
|
+
|
|
1255
|
+
### Web Component Attributes
|
|
1256
|
+
|
|
1257
|
+
The `<convirza-dialer>` custom element accepts the following HTML attributes:
|
|
1258
|
+
|
|
1259
|
+
| Attribute | Type | Required | Description |
|
|
1260
|
+
| -------------------- | -------------------------------------------------------------- | -------- | ------------------------------------------------ |
|
|
1261
|
+
| `access-token` | `string` | Yes\* | Access token from user login |
|
|
1262
|
+
| `refresh-token` | `string` | Yes\* | Refresh token (fallback if access_token expires) |
|
|
1263
|
+
| `auto-configure` | `'true' \| 'false'` | Yes\* | Enable auto-config from OAuth session |
|
|
1264
|
+
| `oauth-endpoint` | `string` | No | OAuth base URL (default: stag-5 internal) |
|
|
1265
|
+
| `api-url` | `string` | No | API base URL for park/history |
|
|
1266
|
+
| `brand-name` | `string` | No | Brand name in header |
|
|
1267
|
+
| `brand-logo` | `string` | No | URL for brand logo |
|
|
1268
|
+
| `theme` | `'light' \| 'dark'` | No | UI theme (default: dark) |
|
|
1269
|
+
| `primary-color` | `string` | No | Primary CSS color (hex/rgb) |
|
|
1270
|
+
| `accent-color` | `string` | No | Accent CSS color |
|
|
1271
|
+
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | No | Widget corner position (default: bottom-right) |
|
|
1272
|
+
| `state` | `'collapsed' \| 'expanded'` | No | Initial state (default: collapsed) |
|
|
1273
|
+
| `z-index` | `number` | No | CSS z-index of the widget |
|
|
1274
|
+
| `park-poll-interval` | `number` | No | Park slots polling interval in ms (default 5000) |
|
|
1275
|
+
|
|
1276
|
+
**Required only for auto-configure mode.** Widget fetches SIP credentials from OAuth session endpoint automatically.
|
|
1277
|
+
|
|
1278
|
+
### Web Component Public Methods
|
|
1279
|
+
|
|
1280
|
+
All methods can be called on the element reference.
|
|
1281
|
+
|
|
1282
|
+
#### `element.placeCall(phoneNumber, options?)`
|
|
1283
|
+
|
|
1284
|
+
Place an outbound call and return a `CallSession` handle.
|
|
1285
|
+
|
|
1286
|
+
```ts
|
|
1287
|
+
placeCall(phoneNumber: string, options?: CallOptions): CallSession
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
**Parameters:**
|
|
1291
|
+
|
|
1292
|
+
- `phoneNumber`: E.164 phone number or SIP extension
|
|
1293
|
+
- `options.callerId`: Override caller ID
|
|
1294
|
+
- `options.displayName`: Override caller display name
|
|
1295
|
+
|
|
1296
|
+
**Returns:** `CallSession` object:
|
|
1297
|
+
|
|
1298
|
+
```ts
|
|
1299
|
+
interface CallSession {
|
|
1300
|
+
phoneNumber: string;
|
|
1301
|
+
status: CallStatus; // Current call status
|
|
1302
|
+
duration: number; // Current duration in ms
|
|
1303
|
+
onAnswered(callback: () => void): void;
|
|
1304
|
+
onEnded(callback: (duration: number) => void): void;
|
|
1305
|
+
end(): void;
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
**Example:**
|
|
1310
|
+
|
|
1311
|
+
```js
|
|
1312
|
+
const el = document.querySelector('convirza-dialer');
|
|
1313
|
+
const session = el.placeCall('+15551234567');
|
|
1314
|
+
|
|
1315
|
+
session.onAnswered(() => {
|
|
1316
|
+
console.log('Call connected');
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
session.onEnded((duration) => {
|
|
1320
|
+
console.log('Call ended after', duration, 'ms');
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
// Hang up programmatically
|
|
1324
|
+
setTimeout(() => session.end(), 30000); // Hang up after 30s
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
#### `element.endCall()`
|
|
1328
|
+
|
|
1329
|
+
Hang up the active call.
|
|
1330
|
+
|
|
1331
|
+
```ts
|
|
1332
|
+
endCall(): void
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
**Example:**
|
|
1336
|
+
|
|
1337
|
+
```js
|
|
1338
|
+
element.endCall();
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
#### `element.mute(enable?)`
|
|
1342
|
+
|
|
1343
|
+
Mute/unmute microphone. Toggles if `enable` omitted.
|
|
1344
|
+
|
|
1345
|
+
```ts
|
|
1346
|
+
mute(enable?: boolean): boolean // returns new mute state
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
#### `element.hold(enable?)`
|
|
1350
|
+
|
|
1351
|
+
Put call on hold. Toggles if `enable` omitted.
|
|
1352
|
+
|
|
1353
|
+
```ts
|
|
1354
|
+
hold(enable?: boolean): boolean // returns new hold state
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
#### `element.sendDTMF(digit)`
|
|
1358
|
+
|
|
1359
|
+
Send DTMF digit via SIP INFO.
|
|
1360
|
+
|
|
1361
|
+
```ts
|
|
1362
|
+
sendDTMF(digit: string): void // '0'-'9', '*', '#'
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
#### `element.startRecording()` / `element.stopRecording()`
|
|
1366
|
+
|
|
1367
|
+
Start/stop server-side recording.
|
|
1368
|
+
|
|
1369
|
+
```ts
|
|
1370
|
+
startRecording(): void
|
|
1371
|
+
stopRecording(): void
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
#### `element.open()`
|
|
1375
|
+
|
|
1376
|
+
Expand the widget.
|
|
1377
|
+
|
|
1378
|
+
```ts
|
|
1379
|
+
open(): void
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
#### `element.close()`
|
|
1383
|
+
|
|
1384
|
+
Collapse the widget to FAB.
|
|
1385
|
+
|
|
1386
|
+
```ts
|
|
1387
|
+
close(): void
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
#### `element.toggle()`
|
|
1391
|
+
|
|
1392
|
+
Toggle widget open/closed.
|
|
1393
|
+
|
|
1394
|
+
```ts
|
|
1395
|
+
toggle(): void
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
#### `element.setTheme(options)`
|
|
1399
|
+
|
|
1400
|
+
Dynamically update theme.
|
|
1401
|
+
|
|
1402
|
+
```ts
|
|
1403
|
+
setTheme(options: {
|
|
1404
|
+
theme?: 'dark' | 'light';
|
|
1405
|
+
primaryColor?: string;
|
|
1406
|
+
accentColor?: string;
|
|
1407
|
+
brandName?: string;
|
|
1408
|
+
}): void
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
**Example:**
|
|
1412
|
+
|
|
1413
|
+
```js
|
|
1414
|
+
element.setTheme({
|
|
1415
|
+
theme: 'dark',
|
|
1416
|
+
primaryColor: '#6366f1',
|
|
1417
|
+
brandName: 'Acme Corp',
|
|
1418
|
+
});
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
#### `element.clearCallHistoryCache()`
|
|
1422
|
+
|
|
1423
|
+
Clear IndexedDB call history cache and reload from backend.
|
|
1424
|
+
|
|
1425
|
+
```ts
|
|
1426
|
+
async clearCallHistoryCache(): Promise<void>
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
**Example:**
|
|
1430
|
+
|
|
1431
|
+
```js
|
|
1432
|
+
await element.clearCallHistoryCache();
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
### Web Component Events
|
|
1436
|
+
|
|
1437
|
+
The element dispatches the following `CustomEvent`s:
|
|
1438
|
+
|
|
1439
|
+
#### Authentication Events
|
|
1440
|
+
|
|
1441
|
+
| Event | `detail` | Description |
|
|
1442
|
+
| ------------------ | --------------------------------------------------------------- | ------------------------------------------ |
|
|
1443
|
+
| `token-refreshed` | `{ access_token: string, refresh_token: string, user: object }` | New tokens received (access_token expired) |
|
|
1444
|
+
| `auth-failed` | `{ error: string }` | Auth failed (both tokens expired/invalid) |
|
|
1445
|
+
| `sip-registered` | `{}` | SIP registration successful |
|
|
1446
|
+
| `sip-unregistered` | `{}` | SIP registration lost |
|
|
1447
|
+
|
|
1448
|
+
#### Call Events
|
|
1449
|
+
|
|
1450
|
+
| Event | `detail` | Description |
|
|
1451
|
+
| ------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
1452
|
+
| `call-started` | `{ phoneNumber: string, callerId?: string, direction?: 'inbound'\|'outbound' }` | Outbound call initiated or inbound call answered |
|
|
1453
|
+
| `call-ended` | `{ phoneNumber: string, duration: number, reason?: string }` | Call ended (duration in ms) |
|
|
1454
|
+
| `call-answered` | `{}` | Call connected (200 OK received) |
|
|
1455
|
+
| `state-change` | `{ oldState: CallStatus, newState: CallStatus, metadata?: CallMetadata }` | Call state transition |
|
|
1456
|
+
| `sip-call-mute` | `{ muted: boolean }` | Mute state changed |
|
|
1457
|
+
| `sip-call-hold` | `{ onHold: boolean }` | Hold state changed |
|
|
1458
|
+
| `dtmf-sent` | `{ digit: string }` | DTMF digit sent |
|
|
1459
|
+
| `call-transferred` | `{}` | Blind transfer succeeded |
|
|
1460
|
+
|
|
1461
|
+
#### Widget Events
|
|
1462
|
+
|
|
1463
|
+
| Event | `detail` | Description |
|
|
1464
|
+
| ----------------- | -------------------------------- | -------------------- |
|
|
1465
|
+
| `widget-opened` | `{}` | Widget expanded |
|
|
1466
|
+
| `widget-closed` | `{}` | Widget collapsed |
|
|
1467
|
+
| `dialer-error` | `{ error: string }` | Unhandled error |
|
|
1468
|
+
| `history-updated` | `{ history: CallHistoryItem[] }` | Call history updated |
|
|
1469
|
+
|
|
1470
|
+
**Example:**
|
|
1471
|
+
|
|
1472
|
+
```js
|
|
1473
|
+
// Auth events
|
|
1474
|
+
element.addEventListener('token-refreshed', (e) => {
|
|
1475
|
+
localStorage.setItem('access_token', e.detail.access_token);
|
|
1476
|
+
localStorage.setItem('refresh_token', e.detail.refresh_token);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
element.addEventListener('auth-failed', (e) => {
|
|
1480
|
+
console.error('Auth failed:', e.detail.error);
|
|
1481
|
+
window.location.href = '/login';
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// Call events
|
|
1485
|
+
element.addEventListener('call-started', (e) => {
|
|
1486
|
+
console.log('Call started:', e.detail.phoneNumber);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
element.addEventListener('call-ended', (e) => {
|
|
1490
|
+
console.log('Call ended after', e.detail.duration / 1000, 'seconds');
|
|
1491
|
+
});
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
---
|
|
1495
|
+
|
|
1496
|
+
## Feature Guides
|
|
1497
|
+
|
|
1498
|
+
### Call Parking
|
|
1499
|
+
|
|
1500
|
+
Call parking allows transferring an active call to a shared "parking lot" where any user in the domain can retrieve it.
|
|
1501
|
+
|
|
1502
|
+
#### How It Works
|
|
1503
|
+
|
|
1504
|
+
1. User clicks park slot (e.g., `700`)
|
|
1505
|
+
2. Widget sends valet park request to backend API
|
|
1506
|
+
3. Backend uses FreeSWITCH ESL to park call (no SIP REFER)
|
|
1507
|
+
4. Server responds with success
|
|
1508
|
+
5. Widget ends local session (call now owned by server)
|
|
1509
|
+
6. Widget polls backend API to sync park slot occupancy
|
|
1510
|
+
7. Other users see parked call and can retrieve by dialing park extension
|
|
1511
|
+
|
|
1512
|
+
#### Configuration
|
|
1513
|
+
|
|
1514
|
+
**HTML Attributes:**
|
|
1515
|
+
|
|
1516
|
+
```html
|
|
1517
|
+
<convirza-dialer
|
|
1518
|
+
api-url="https://stag-5-dialer-apis.convirza.com"
|
|
1519
|
+
auth-token="Bearer xyz"
|
|
1520
|
+
park-poll-interval="5000"
|
|
1521
|
+
></convirza-dialer>
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
- `api-url`: Base API URL — park endpoint is `{api-url}/v3/park-slots`
|
|
1525
|
+
- `auth-token`: JWT or API key for backend auth
|
|
1526
|
+
- `park-poll-interval`: Poll interval in ms (default: 5000)
|
|
1527
|
+
|
|
1528
|
+
#### Backend API
|
|
1529
|
+
|
|
1530
|
+
**Required endpoint:**
|
|
1531
|
+
|
|
1532
|
+
```
|
|
1533
|
+
GET /v3/park-slots?domain={sipDomain}
|
|
1534
|
+
Returns: [
|
|
1535
|
+
{
|
|
1536
|
+
extension: "700",
|
|
1537
|
+
label: "Park 1",
|
|
1538
|
+
occupied: true,
|
|
1539
|
+
parkedCall: {
|
|
1540
|
+
phoneNumber: "+12485794891",
|
|
1541
|
+
parkedBy: "1001",
|
|
1542
|
+
parkedAt: 1719234567890
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
...
|
|
1546
|
+
]
|
|
1547
|
+
|
|
1548
|
+
POST /v3/park-call
|
|
1549
|
+
Body: { extension: "700", callUuid: "abc-123", sipDomain: "pbx.example.com" }
|
|
1550
|
+
Returns: { success: true }
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
#### Events
|
|
1554
|
+
|
|
1555
|
+
**`call-parked`** — fired when park succeeds
|
|
1556
|
+
|
|
1557
|
+
```js
|
|
1558
|
+
element.addEventListener('call-parked', (e) => {
|
|
1559
|
+
// e.detail: { parkExtension, phoneNumber, parkedBy }
|
|
1560
|
+
});
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
**`park-slots-updated`** — fired on successful API fetch
|
|
1564
|
+
|
|
1565
|
+
```js
|
|
1566
|
+
element.addEventListener('park-slots-updated', (e) => {
|
|
1567
|
+
// e.detail: { slots: ParkAccount[] }
|
|
1568
|
+
});
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
**`call-park-failed`** — fired if park fails
|
|
1572
|
+
|
|
1573
|
+
```js
|
|
1574
|
+
element.addEventListener('call-park-failed', (e) => {
|
|
1575
|
+
// e.detail: { error: string }
|
|
1576
|
+
});
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
### Call History Persistence
|
|
1580
|
+
|
|
1581
|
+
Call history is saved to **two tiers**:
|
|
1582
|
+
|
|
1583
|
+
1. **IndexedDB** (local cache) — instant load, offline support
|
|
1584
|
+
2. **Backend API** (source of truth) — multi-device sync, compliance
|
|
1585
|
+
|
|
1586
|
+
Widget saves to IndexedDB immediately (non-blocking), then syncs to backend async.
|
|
1587
|
+
|
|
1588
|
+
#### Data Flow
|
|
1589
|
+
|
|
1590
|
+
**On call end:**
|
|
1591
|
+
|
|
1592
|
+
1. Widget generates UUID `callId`
|
|
1593
|
+
2. Saves to IndexedDB (instant)
|
|
1594
|
+
3. POSTs to `/api/call-history` (async, retries on failure)
|
|
1595
|
+
4. Reloads history from IndexedDB to update UI
|
|
1596
|
+
|
|
1597
|
+
**On SIP registration:**
|
|
1598
|
+
|
|
1599
|
+
1. Initialize `CallHistoryService`
|
|
1600
|
+
2. Load from IndexedDB (fast)
|
|
1601
|
+
3. Background-sync from backend (replaces IndexedDB cache)
|
|
1602
|
+
|
|
1603
|
+
**On page reload:**
|
|
1604
|
+
|
|
1605
|
+
- IndexedDB persists → history visible immediately
|
|
1606
|
+
- Backend sync refreshes on reconnect
|
|
1607
|
+
|
|
1608
|
+
#### Configuration
|
|
1609
|
+
|
|
1610
|
+
**HTML Attributes:**
|
|
1611
|
+
|
|
1612
|
+
```html
|
|
1613
|
+
<convirza-dialer api-url="https://api.example.com" auth-token="Bearer xyz"></convirza-dialer>
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
- `api-url`: Base API URL — history endpoint = `{api-url}/api/call-history`
|
|
1617
|
+
- `auth-token`: JWT or API key for backend auth
|
|
1618
|
+
|
|
1619
|
+
**If `api-url` not set:**
|
|
1620
|
+
|
|
1621
|
+
- IndexedDB-only mode (no backend sync)
|
|
1622
|
+
- History persists locally but not across devices
|
|
1623
|
+
|
|
1624
|
+
#### Backend API
|
|
1625
|
+
|
|
1626
|
+
**Required endpoints:**
|
|
1627
|
+
|
|
1628
|
+
```
|
|
1629
|
+
GET /api/call-history?userId={id}&sipDomain={domain}&limit=100&offset=0
|
|
1630
|
+
POST /api/call-history
|
|
1631
|
+
PATCH /api/call-history/{callId}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
**Database schema (PostgreSQL):**
|
|
1635
|
+
|
|
1636
|
+
```sql
|
|
1637
|
+
CREATE TABLE call_history (
|
|
1638
|
+
id BIGSERIAL PRIMARY KEY,
|
|
1639
|
+
call_id VARCHAR(100) UNIQUE NOT NULL,
|
|
1640
|
+
user_id VARCHAR(50) NOT NULL,
|
|
1641
|
+
sip_domain VARCHAR(100) NOT NULL,
|
|
1642
|
+
phone_number VARCHAR(50) NOT NULL,
|
|
1643
|
+
direction VARCHAR(10) NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
1644
|
+
status VARCHAR(20) NOT NULL CHECK (status IN ('answered', 'missed', 'rejected', 'failed')),
|
|
1645
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
1646
|
+
ended_at TIMESTAMPTZ,
|
|
1647
|
+
duration_seconds INTEGER DEFAULT 0,
|
|
1648
|
+
recording_url VARCHAR(500),
|
|
1649
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
1650
|
+
INDEX idx_user_domain_started (user_id, sip_domain, started_at DESC),
|
|
1651
|
+
INDEX idx_call_id (call_id)
|
|
1652
|
+
);
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
See `docs/CALL_HISTORY_API.md` for full spec, Node.js examples, security notes.
|
|
1656
|
+
|
|
1657
|
+
#### Events
|
|
1658
|
+
|
|
1659
|
+
**`history-updated`** — fired when history changes
|
|
1660
|
+
|
|
1661
|
+
```js
|
|
1662
|
+
element.addEventListener('history-updated', (e) => {
|
|
1663
|
+
// e.detail: { history: CallHistoryItem[] }
|
|
1664
|
+
});
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
### Audio Device Management
|
|
1668
|
+
|
|
1669
|
+
Microphone and speaker selection with hot-swapping during calls.
|
|
1670
|
+
|
|
1671
|
+
#### Usage
|
|
1672
|
+
|
|
1673
|
+
```js
|
|
1674
|
+
const devices = await sip.getAudioDevices();
|
|
1675
|
+
// { microphones: [...], speakers: [...] }
|
|
1676
|
+
|
|
1677
|
+
// Set device
|
|
1678
|
+
await sip.setMicrophone(deviceId);
|
|
1679
|
+
await sip.setSpeaker(deviceId);
|
|
1680
|
+
|
|
1681
|
+
// Preferences saved to localStorage: convirza_audio_preferences
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
**Browser support:**
|
|
1685
|
+
|
|
1686
|
+
- Microphone switching: Chrome, Firefox, Safari, Edge
|
|
1687
|
+
- Speaker switching (`setSinkId`): Chrome, Edge only (silently no-ops in Firefox/Safari)
|
|
1688
|
+
|
|
1689
|
+
### Call Quality Monitoring
|
|
1690
|
+
|
|
1691
|
+
Polls `RTCPeerConnection.getStats()` every 2s and emits quality metrics.
|
|
1692
|
+
|
|
1693
|
+
**Metrics:**
|
|
1694
|
+
|
|
1695
|
+
- **Bitrate** (kbps)
|
|
1696
|
+
- **Packet loss** (%)
|
|
1697
|
+
- **Jitter** (ms)
|
|
1698
|
+
- **Latency** (ms, round-trip)
|
|
1699
|
+
- **Audio level** (0–1)
|
|
1700
|
+
- **Quality** (`excellent` | `good` | `fair` | `poor`)
|
|
1701
|
+
|
|
1702
|
+
**Usage:**
|
|
1703
|
+
|
|
1704
|
+
```js
|
|
1705
|
+
const sip = new SipAdapter(config, {
|
|
1706
|
+
onQualityChange: (metrics) => {
|
|
1707
|
+
if (metrics.quality === 'poor') {
|
|
1708
|
+
console.warn('Poor call quality:', metrics);
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
// Or poll manually
|
|
1714
|
+
const metrics = sip.getCallQuality();
|
|
1715
|
+
console.log('Packet loss:', metrics.packetLoss + '%');
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
### Theming
|
|
1719
|
+
|
|
1720
|
+
#### HTML Attributes (static)
|
|
1721
|
+
|
|
1722
|
+
```html
|
|
1723
|
+
<convirza-dialer
|
|
1724
|
+
theme="dark"
|
|
1725
|
+
primary-color="#6366f1"
|
|
1726
|
+
accent-color="#22c55e"
|
|
1727
|
+
brand-name="Convirza"
|
|
1728
|
+
brand-logo="https://..."
|
|
1729
|
+
></convirza-dialer>
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
#### JavaScript `setTheme()` (dynamic)
|
|
1733
|
+
|
|
1734
|
+
```js
|
|
1735
|
+
element.setTheme({
|
|
1736
|
+
theme: 'dark',
|
|
1737
|
+
primaryColor: '#6366f1',
|
|
1738
|
+
accentColor: '#10b981',
|
|
1739
|
+
brandName: 'Acme Corp',
|
|
1740
|
+
});
|
|
1741
|
+
```
|
|
1742
|
+
|
|
1743
|
+
#### Direct `setAttribute()`
|
|
1744
|
+
|
|
1745
|
+
```js
|
|
1746
|
+
element.setAttribute('primary-color', '#6366f1');
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
CSS vars injected at runtime via `style.setProperty('--primary-color', ...)`.
|
|
1750
|
+
|
|
1751
|
+
---
|
|
1752
|
+
|
|
1753
|
+
## Types
|
|
1754
|
+
|
|
1755
|
+
```ts
|
|
1756
|
+
// Widget position
|
|
1757
|
+
type WidgetPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
1758
|
+
|
|
1759
|
+
// Call lifecycle state
|
|
1760
|
+
type CallStatus = 'idle' | 'dialing' | 'ringing' | 'connected' | 'ended' | 'error';
|
|
1761
|
+
|
|
1762
|
+
// Widget open/closed state
|
|
1763
|
+
type WidgetState = 'collapsed' | 'expanded';
|
|
1764
|
+
|
|
1765
|
+
// UI theme
|
|
1766
|
+
type ThemeMode = 'light' | 'dark';
|
|
1767
|
+
|
|
1768
|
+
// Call direction and outcome
|
|
1769
|
+
type CallDirection = 'inbound' | 'outbound';
|
|
1770
|
+
type CallDisposition = 'answered' | 'missed' | 'rejected' | 'failed';
|
|
1771
|
+
|
|
1772
|
+
// Events emitted by ConvirzaDialer class
|
|
1773
|
+
interface DialerEvents {
|
|
1774
|
+
onOpen?: () => void;
|
|
1775
|
+
onClose?: () => void;
|
|
1776
|
+
onCallStarted?: (phoneNumber: string) => void;
|
|
1777
|
+
onCallEnded?: (phoneNumber: string, duration: number) => void;
|
|
1778
|
+
onStateChange?: (oldState: CallStatus, newState: CallStatus, metadata?: CallMetadata) => void;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Returned by placeCall()
|
|
1782
|
+
interface CallSession {
|
|
1783
|
+
phoneNumber: string;
|
|
1784
|
+
status: CallStatus;
|
|
1785
|
+
duration: number;
|
|
1786
|
+
onAnswered(callback: () => void): void;
|
|
1787
|
+
onEnded(callback: (duration: number) => void): void;
|
|
1788
|
+
end(): void;
|
|
1789
|
+
}
|
|
1790
|
+
```
|
|
1791
|
+
|
|
1792
|
+
---
|
|
1793
|
+
|
|
1794
|
+
## Browser Support
|
|
1795
|
+
|
|
1796
|
+
Requires a browser with WebRTC (`RTCPeerConnection`) and WebSocket support.
|
|
1797
|
+
|
|
1798
|
+
- **Chrome** 74+
|
|
1799
|
+
- **Firefox** 69+
|
|
1800
|
+
- **Safari** 14.1+
|
|
1801
|
+
- **Edge** 79+
|
|
1802
|
+
|
|
1803
|
+
**Feature availability:**
|
|
1804
|
+
|
|
1805
|
+
- **Speaker selection** (`setSinkId`): Chrome/Edge only
|
|
1806
|
+
- **Microphone hot-swap**: All browsers
|
|
1807
|
+
- **WebRTC stats**: All browsers
|
|
1808
|
+
|
|
1809
|
+
---
|
|
1810
|
+
|
|
1811
|
+
## Project Structure
|
|
1812
|
+
|
|
1813
|
+
```
|
|
1814
|
+
convirza-dialer-1/
|
|
1815
|
+
├── packages/web-sdk/
|
|
1816
|
+
│ ├── src/ # Source code (TypeScript)
|
|
1817
|
+
│ │ ├── ui/ # Web component
|
|
1818
|
+
│ │ ├── sip/ # SIP adapter
|
|
1819
|
+
│ │ ├── api/ # REST clients
|
|
1820
|
+
│ │ ├── media/ # Audio device management
|
|
1821
|
+
│ │ ├── constants/ # Configuration
|
|
1822
|
+
│ │ └── types/ # TypeScript types
|
|
1823
|
+
│ ├── test/ # Unit tests
|
|
1824
|
+
│ ├── dist/ # Built output (published)
|
|
1825
|
+
│ │ ├── index.esm.js # ES modules
|
|
1826
|
+
│ │ ├── index.cjs.js # CommonJS
|
|
1827
|
+
│ │ ├── index.umd.js # UMD (browser)
|
|
1828
|
+
│ │ └── types/ # TypeScript declarations
|
|
1829
|
+
│ └── package.json # Package metadata
|
|
1830
|
+
└── demo/ # Demo files (root level, not published)
|
|
1831
|
+
├── demo.html # Interactive demo
|
|
1832
|
+
├── sip.bundle.js # Bundled SIP.js
|
|
1833
|
+
└── README.md # Demo instructions
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
**For development:** Work in `src/` directory.
|
|
1837
|
+
**For users:** Import from `dist/` (handled automatically by package.json).
|
|
1838
|
+
|
|
1839
|
+
## License
|
|
1840
|
+
|
|
1841
|
+
PROPRIETARY — Copyright Convirza. All rights reserved.
|