voxnix 1.0.24 → 2.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 CHANGED
@@ -1,20 +1,30 @@
1
1
  # Voxnix
2
2
 
3
- Universal Voice Widget SDK — SIP-based floating phone widget. Works in React, Vue, Angular, plain HTML, or any stack.
3
+ Headless voice SDK for the Omnix CPaaS platform. Voxnix connects the browser to your PBX over
4
+ SIP/WebSocket, manages the agent status (Ready / Not Ready / AUX)
5
+ through the CPaaS API, and gives you **functions + events** to build your own calling UI in any stack:
6
+ React, Vue, Angular, or plain HTML.
4
7
 
5
- **Current version: `1.0.12`**
8
+ **Current version: `2.0.0`**. Voxnix ships no UI. Upgrading from 1.x? See [Migrating from 1.x](#migrating-from-1x).
6
9
 
7
10
  ---
8
11
 
9
- ## What it does
10
-
11
- - Floating phone button (fixed position, configurable placement)
12
- - Auto-connects to your SIP/PABX server on init
13
- - Shows dialer numpad immediately after registration
14
- - Full call UI: incoming call, outgoing dialing, in-call controls (mute, hold, DTMF keypad)
15
- - **Agent status bar** — Ready / Not Ready / AUX with optional custom AUX codes
16
- - Fires callbacks for all call and agent-status events so your app can react
17
- - Keyboard shortcuts: digits to dial, `Enter` to call, `Backspace` to delete, `0–9`/`*`/`#` for DTMF during a call
12
+ ## Contents
13
+
14
+ - [Installation](#installation)
15
+ - [Quick start](#quick-start)
16
+ - [How it works](#how-it-works)
17
+ - [Config reference](#config-reference)
18
+ - [Instance API](#instance-api)
19
+ - [State](#state)
20
+ - [Events & callbacks](#events--callbacks)
21
+ - [Recipes](#recipes)
22
+ - [Browser requirements](#browser-requirements)
23
+ - [Security](#security)
24
+ - [Sample page](#sample-page)
25
+ - [Build](#build)
26
+ - [Migrating from 1.x](#migrating-from-1x)
27
+ - [Changelog](#changelog)
18
28
 
19
29
  ---
20
30
 
@@ -26,341 +36,428 @@ npm install voxnix
26
36
  yarn add voxnix
27
37
  ```
28
38
 
39
+ Or use a script tag (no bundler):
40
+
41
+ ```html
42
+ <script src="https://unpkg.com/voxnix/dist/voxnix.iife.js"></script>
43
+ <script>
44
+ const voice = OmnixWidget.init({ /* config */ });
45
+ </script>
46
+ ```
47
+
48
+ | File | Format | Use |
49
+ |-------------------|--------|------------------------------|
50
+ | `voxnix.js` | ESM | `import` in bundlers |
51
+ | `voxnix.umd.cjs` | UMD | `require()` / legacy loaders |
52
+ | `voxnix.iife.js` | IIFE | `<script>` tag → `window.OmnixWidget` |
53
+
29
54
  ---
30
55
 
31
- ## Quick start (any framework)
56
+ ## Quick start
32
57
 
33
58
  ```js
34
59
  import { OmnixWidget } from 'voxnix';
35
60
 
36
- const widget = OmnixWidget.init({
61
+ const voice = OmnixWidget.init({
37
62
  auth: {
38
- username: '1001',
39
- pwd_pbx: 'secret',
40
- pabx_host: 'sip.example.com',
41
- port: 8089, // optional, default 8089
63
+ username: '1001', // SIP extension
64
+ pwd_pbx: 'secret', // SIP password
65
+ pabx_host: 'sip.example.com', // PBX host
66
+ port: 8089, // WSS port (default 8089)
42
67
  },
68
+ 'X-CPAAS-Key-Fingerprint': 'sfp_v1_...', // CPaaS API key (agent status)
69
+ cpaas_base_url: 'https://api-cpaas.omnix.co.id',
43
70
 
44
- // Required for agent status API (Ready / AUX / Not Ready)
45
- 'X-CPAAS-Key-Fingerprint': 'your-cpaas-key-fingerprint-here',
71
+ onRegisterStatus: (s) => console.log('register:', s),
72
+ onCallStatus: (s) => console.log('call:', s),
73
+ onIncomingCall: (call) => console.log('incoming from', call.number),
74
+ });
46
75
 
47
- placement: 'bottom-left', // 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'
76
+ voice.dial('1002'); // outgoing call
77
+ voice.hangup(); // end it
78
+ ```
48
79
 
49
- // Agent status options (optional)
50
- autoReady: true, // auto-set agent to Ready after SIP register (default: true)
51
- showAuxButton: false, // show AUX button in status bar (default: false)
52
- auxOptions: [], // array of { code, label } — custom AUX codes
80
+ ---
53
81
 
54
- // Callbacks
55
- onRegisterStatus: (status) => {
56
- // status: 'REGISTERED' | { status: 'FAILED', reason: string }
57
- console.log('SIP status:', status);
58
- },
59
- onAgentStatus: (status) => console.log('Agent:', status), // 'READY' | 'NOT_READY' | 'AUX'
60
- onIncomingCall: (data) => console.log('Incoming:', data),
61
- onCallAnswered: (data) => console.log('Answered:', data),
62
- onCallEnded: (data) => console.log('Ended:', data),
63
- onCallUnanswered: () => console.log('Missed call'),
64
- });
82
+ ## How it works
83
+
84
+ ### Registration flow
65
85
 
66
- // Imperative controls (optional — widget has its own UI)
67
- widget.answer();
68
- widget.hangup();
69
- widget.reject();
70
- widget.mute(true); // mute
71
- widget.mute(false); // unmute
72
- widget.hold(true); // hold
73
- widget.hold(false); // resume
74
- widget.dial('1002'); // programmatic dial
75
- widget.destroy(); // unmount widget
86
+ ```
87
+ init()
88
+ └─ WebSocket connect wss://<sip_url | pabx_host>:<port>
89
+ └─ SIP REGISTER sip:<username>@<pbx_url | pabx_host>
90
+ ├─ autoReady: true (default)
91
+ │ └─ POST Ready to CPaaS ──ok──► register_status 'REGISTERED' + agent_status 'READY'
92
+ │ └─fail─► register_status { status: 'FAILED', reason }
93
+ └─ autoReady: false
94
+ └─ GET extension status from CPaaS
95
+ ├─ already Ready ──► 'REGISTERED' + 'READY'
96
+ └─ otherwise ──► 'SIP_CONNECTED' + 'NOT_READY' | 'AUX'
97
+ (call setReady() to go to 'REGISTERED')
76
98
  ```
77
99
 
78
- ---
100
+ Before every agent-status change, Voxnix checks the extension on CPaaS. If CPaaS reports
101
+ `is_register = 0`, Voxnix sends a SIP re-REGISTER and polls CPaaS (3 × 1s) until it reports
102
+ the extension as registered. If it still isn't registered, the status call returns `false`.
79
103
 
80
- ## Config reference
104
+ ### `register_status` values
81
105
 
82
- ### Top-level options
83
-
84
- | Option | Type | Default | Description |
85
- |-----------------------------|--------------------------------------------------------------------|-----------------|-----------------------------------------------------------------|
86
- | `auth` | `object` | — | SIP credentials (see [auth object](#auth-object)) |
87
- | `pabxConfig` | `{ auth: object }` | — | Alternative config shape — use `pabxConfig.auth` instead of `auth` |
88
- | `X-CPAAS-Key-Fingerprint` | `string` | — | **Required** for agent status API calls. See [section below](#x-cpaas-key-fingerprint) |
89
- | `fingerprint` | `string` | — | Alias for `X-CPAAS-Key-Fingerprint` |
90
- | `cpaasKeyFingerprint` | `string` | — | Alias for `X-CPAAS-Key-Fingerprint` |
91
- | `placement` | `'bottom-left'` \| `'bottom-right'` \| `'top-left'` \| `'top-right'` | `'bottom-left'` | Fixed position of the floating widget |
92
- | `offset` | `{ bottom?, top?, left?, right? }` | — | CSS pixel coords — overrides `placement` |
93
- | `container` | `string` \| `HTMLElement` | — | Mount target. Auto-creates `<div id="omnix-widget-root">` if omitted |
94
- | `autoReady` | `boolean` | `true` | Auto-call the Ready API right after SIP registration |
95
- | `showAuxButton` | `boolean` | `false` | Show the AUX menu in the agent status bar |
96
- | `auxOptions` | `Array<{ code: string, label: string }>` | `[]` | Custom AUX reason codes shown in the status dropdown |
97
- | `onRegisterStatus` | `(status) => void` | — | `'REGISTERED'` or `{ status: 'FAILED', reason: string }` |
98
- | `onAgentStatus` | `(status: string) => void` | — | `'READY'` \| `'NOT_READY'` \| `'AUX'` |
99
- | `onIncomingCall` | `(data) => void` | — | Fired on incoming SIP INVITE |
100
- | `onCallAnswered` | `(data) => void` | — | Fired when call is established |
101
- | `onCallEnded` | `(data) => void` | — | Fired on BYE / hangup |
102
- | `onCallUnanswered` | `() => void` | — | Fired when incoming call is not answered (missed) |
103
-
104
- ### `auth` object
105
-
106
- | Field | Type | Description |
107
- |-------------|----------|------------------------------------------|
108
- | `username` | `string` | SIP username / extension number |
109
- | `user_pbx` | `string` | Alias for `username` |
110
- | `pwd_pbx` | `string` | SIP password |
111
- | `secret` | `string` | Alias for `pwd_pbx` |
112
- | `pabx_host` | `string` | SIP domain / WSS host (strips `http(s)://`) |
113
- | `pbxurl` | `string` | Alias for `pabx_host` |
114
- | `port` | `number` | WSS port (default `8089`) |
106
+ | Value | Meaning |
107
+ |-------------------------------|---------|
108
+ | `'CONNECTING'` | Initial state (only visible via `getState()`) |
109
+ | `'SIP_CONNECTED'` | SIP registered, agent not Ready yet (`autoReady: false`, or after `setNotReady()`) |
110
+ | `'REGISTERED'` | SIP registered **and** agent Ready. Calls can be made and received |
111
+ | `{ status: 'FAILED', reason }`| See reasons below |
115
112
 
116
- ---
113
+ Typical `reason` values:
117
114
 
118
- ## `X-CPAAS-Key-Fingerprint`
115
+ - `Missing SIP credentials (username, password, host)`
116
+ - `WebSocket disconnected, retrying...`: Voxnix keeps reconnecting on its own. When the socket comes back, the registration flow runs again.
117
+ - `SIP registration failed: <cause>`: for example, a wrong password.
118
+ - `SIP Unregistered`
119
+ - `Failed to hit extension ready API`: the fingerprint is missing or wrong, or the CPaaS API rejected the request.
120
+ - `Extension not registered after SIP re-register`
119
121
 
120
- This key is the **API authentication credential** used when the widget calls the CPaaS extension status endpoint:
122
+ ### Call status flow
121
123
 
122
124
  ```
123
- POST https://api-cpaas.omnix.co.id/voice/extensions/status
124
- Header: X-CPAAS-Key-Fingerprint: <your-fingerprint>
125
+ outgoing: Idle ─dial()─► Calling... ─answered─► InCall ─hangup / remote BYE─► Idle
126
+ └─rejected / no answer─► Idle (+ call_failed)
127
+ incoming: Idle ─INVITE─► Ringing ─answer()─► InCall ─► Idle
128
+ └─reject() / caller cancels─► Idle (+ callUnanswered)
125
129
  ```
126
130
 
127
- The endpoint is hit automatically whenever the agent status changes (Ready / AUX / Not Ready). Without this key, status updates will silently fail and — if `autoReady: true` (default) — the widget will report `FAILED` registration even if the SIP connection itself succeeded.
131
+ Only one call at a time. A second incoming call while one is active is automatically rejected
132
+ with `486 Busy Here`.
133
+
134
+ ---
135
+
136
+ ## Config reference
137
+
138
+ `OmnixWidget.init(config)`. There is one engine per page. Calling `init()` again destroys the previous instance first.
139
+
140
+ ### SIP credentials: `auth`
141
+
142
+ | Field | Type | Required | Description |
143
+ |-------------|----------|----------|-------------|
144
+ | `username` | `string` | ✔ | SIP extension (alias: `user_pbx`) |
145
+ | `pwd_pbx` | `string` | ✔ | SIP password (alias: `secret`) |
146
+ | `pabx_host` | `string` | ✔* | PBX host (aliases: `pbxurl`, `host`). `http(s)://` is stripped |
147
+ | `port` | `number` | | WSS port, default `8089` |
128
148
 
129
- ### Accepted config locations
149
+ \* Not required when `pbx_url` is given.
130
150
 
131
- The widget looks for the fingerprint in the following order, using the first match:
151
+ `pabxConfig: { auth: {...} }` is accepted as an alternative to `auth`.
132
152
 
133
- | Priority | Config key | Example |
134
- |----------|------------|---------|
135
- | 1 | `'X-CPAAS-Key-Fingerprint'` (top-level) | `{ 'X-CPAAS-Key-Fingerprint': 'abc123', auth: { ... } }` |
136
- | 2 | `fingerprint` (top-level) | `{ fingerprint: 'abc123', auth: { ... } }` |
137
- | 3 | `cpaasKeyFingerprint` (top-level) | `{ cpaasKeyFingerprint: 'abc123', auth: { ... } }` |
138
- | 4 | `env.fingerprint` | `{ env: { fingerprint: 'abc123' }, auth: { ... } }` |
139
- | 5 | `env.X_CPAAS_Key_Fingerprint` | `{ env: { X_CPAAS_Key_Fingerprint: 'abc123' }, auth: { ... } }` |
140
- | 6 | `pabxConfig['X-CPAAS-Key-Fingerprint']` | `{ pabxConfig: { 'X-CPAAS-Key-Fingerprint': 'abc123', auth: { ... } } }` |
153
+ ### Hosts & endpoints
141
154
 
142
- ### Recommended usage
155
+ | Option | Default | Description |
156
+ |------------------|----------------------------------|-------------|
157
+ | `pbx_url` | `auth.pabx_host` | SIP domain used in the SIP URI `sip:<username>@<pbx_url>` (alias: `pabxConfig.pbx_url`) |
158
+ | `sip_url` | SIP domain | WebSocket host: `wss://<sip_url>:<port>`. `ws(s)://` and a trailing `/` are stripped (alias: `pabxConfig.sip_url`) |
159
+ | `cpaas_base_url` | `https://api-cpaas.omnix.co.id` | CPaaS API base URL, must be `https://`. A trailing `/` is stripped (aliases: `cpaasBaseUrl`, `env.CPAAS_BASE_URL`, `pabxConfig.cpaas_base_url`) |
160
+
161
+ Use these when the SIP domain, the WebSocket host and the API live on different hosts:
143
162
 
144
163
  ```js
145
- // Option A — top-level key (simplest)
146
164
  OmnixWidget.init({
147
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
148
- auth: { username: '1001', pwd_pbx: 'secret', pabx_host: 'sip.example.com' },
149
- autoReady: true,
165
+ auth: { username: '1001', pwd_pbx: '***', pabx_host: 'sip.example.com' },
166
+ 'X-CPAAS-Key-Fingerprint': 'sfp_v1_...',
167
+ cpaas_base_url: 'https://api.example.com/',
168
+ pbx_url: 'pbx.example.com', // SIP domain
169
+ sip_url: 'wss.example.com', // WSS host
150
170
  });
171
+ ```
151
172
 
152
- // Option B — inside pabxConfig (if your backend returns this shape)
153
- OmnixWidget.init({
154
- pabxConfig: {
155
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
156
- auth: { username: '1001', pwd_pbx: 'secret', pabx_host: 'sip.example.com' },
157
- },
158
- });
173
+ ### `X-CPAAS-Key-Fingerprint`
159
174
 
160
- // Option C — via env object
161
- OmnixWidget.init({
162
- env: { fingerprint: 'your-fingerprint-here' },
163
- auth: { username: '1001', pwd_pbx: 'secret', pabx_host: 'sip.example.com' },
164
- });
175
+ The API key Voxnix sends as a header to the CPaaS endpoints:
176
+
177
+ ```
178
+ GET {cpaas_base_url}/voice/portal/extension?extension=<username> → read status / is_register
179
+ POST {cpaas_base_url}/voice/extensions/status → { extension, status, reason }
165
180
  ```
166
181
 
167
- > **Important:** If the fingerprint is missing and `autoReady: true` (default), the widget will emit `{ status: 'FAILED', reason: 'Failed to hit extension ready API' }` via `onRegisterStatus` even though the SIP connection itself was successful. Set `autoReady: false` if you do not have a fingerprint yet.
182
+ Voxnix uses the first of these keys that is present:
183
+
184
+ | Priority | Key |
185
+ |----------|-----|
186
+ | 1 | `'X-CPAAS-Key-Fingerprint'` |
187
+ | 2 | `fingerprint` |
188
+ | 3 | `cpaasKeyFingerprint` |
189
+ | 4 | `env.fingerprint` |
190
+ | 5 | `env.X_CPAAS_Key_Fingerprint` |
191
+ | 6 | `pabxConfig['X-CPAAS-Key-Fingerprint']` |
192
+
193
+ > Without a fingerprint, every agent-status call returns `false`. With `autoReady: true` the
194
+ > engine then reports `FAILED` ("Failed to hit extension ready API") even though SIP registered fine.
195
+
196
+ ### Behaviour
197
+
198
+ | Option | Type | Default | Description |
199
+ |-------------|--------------------------|---------|-------------|
200
+ | `autoReady` | `boolean` | `true` | Set the agent Ready right after SIP registration. If `false`, the engine restores the current CPaaS status and waits for `setReady()` |
201
+ | `ringtone` | `boolean` | `true` | Play the built-in ringtone on an incoming call. Set `false` to play your own |
202
+ | `contacts` | `{ [number]: string }` | — | Name lookup used for `name` when the caller sends no display name |
203
+
204
+ ### Callbacks
205
+
206
+ All callbacks are optional. Payloads are described in [Events & callbacks](#events--callbacks).
207
+
208
+ | Option | Called when |
209
+ |--------------------|-------------|
210
+ | `onRegisterStatus` | Registration becomes `'REGISTERED'` or `FAILED`. `SIP_CONNECTED` is **not** reported here; use `on('register_status')` to see it |
211
+ | `onAgentStatus` | Agent status changes |
212
+ | `onCallStatus` | Call status changes |
213
+ | `onCallFailed` | An outgoing call fails (rejected, busy, unreachable, …) |
214
+ | `onIncomingCall` | An incoming call starts ringing |
215
+ | `onCallAnswered` | A call (either direction) is connected |
216
+ | `onCallEnded` | A connected call ends |
217
+ | `onCallUnanswered` | An incoming call ends without being answered (missed, rejected, caller cancelled) |
218
+
219
+ ---
220
+
221
+ ## Instance API
168
222
 
169
- ### Custom container / offset
223
+ `init()` returns:
224
+
225
+ | Method | Returns | Description |
226
+ |--------|---------|-------------|
227
+ | `dial(number)` | — | Start an outgoing call to `sip:<number>@<SIP domain>`. Ignored if another call is active. Invalid characters → `call_failed` `'Invalid number'` |
228
+ | `answer()` | — | Answer the ringing incoming call |
229
+ | `reject()` | — | Reject the ringing incoming call (same as `hangup()`) |
230
+ | `hangup()` | — | End the current call. Sends CANCEL while dialing, rejects while ringing, BYE when connected |
231
+ | `mute(muted = true)` | — | `true` mutes the microphone, `false` unmutes it. Only during a connected call |
232
+ | `hold(onHold = true)` | — | `true` holds the call, `false` resumes it. Only during a connected call |
233
+ | `sendDtmf(tones)` | — | Send DTMF during a connected call. Accepts one tone or a string: `'5'`, `'123#'` |
234
+ | `setReady()` | `Promise<boolean>` | Agent → Ready. Emits `'REGISTERED'` + `'READY'` on success |
235
+ | `setNotReady()` | `Promise<boolean>` | Agent → Not Ready (CPaaS `Logout`). Emits `'SIP_CONNECTED'` + `'NOT_READY'` |
236
+ | `setAux(code = 'AUX', label = code)` | `Promise<boolean>` | Agent → AUX. `code` is sent to CPaaS as the reason; `label` is for your UI |
237
+ | `getState()` | `object` | Snapshot of the current state, see [State](#state) |
238
+ | `on(event, cb)` | `() => void` | Subscribe to an event. Returns an unsubscribe function |
239
+ | `destroy()` | — | End any call, unregister, close the WebSocket, and drop all listeners |
240
+
241
+ The call controls are safe to call at any time. Outside a valid state they do nothing.
242
+
243
+ ---
244
+
245
+ ## State
246
+
247
+ `getState()` returns a fresh copy each time:
170
248
 
171
249
  ```js
172
- OmnixWidget.init({
173
- container: '#my-widget-slot', // CSS selector or DOM element
174
- // — or —
175
- offset: { bottom: 80, left: 10 }, // exact pixel position (bypasses placement)
176
- // ... rest of config
177
- });
250
+ {
251
+ registerStatus: 'REGISTERED', // 'CONNECTING' | 'SIP_CONNECTED' | 'REGISTERED' | 'FAILED'
252
+ agentStatus: 'READY', // 'NOT_READY' | 'READY' | 'AUX'
253
+ currentAux: null, // { code, label } while AUX
254
+ callStatus: 'InCall', // 'Idle' | 'Calling...' | 'Ringing' | 'InCall'
255
+ callDuration: '01:23', // 'MM:SS' or 'HH:MM:SS'
256
+ isMuted: false,
257
+ isOnHold: false,
258
+ remote: { number: '1002', name: 'Jane', direction: 'outgoing' }, // null when Idle
259
+ }
178
260
  ```
179
261
 
262
+ Use it to draw your UI when it first mounts, then keep it in sync with events.
263
+
180
264
  ---
181
265
 
182
- ## Instance API
266
+ ## Events & callbacks
183
267
 
184
- `OmnixWidget.init()` returns an instance with these methods:
268
+ Subscribe with `voice.on(event, cb)`. Events that have a callback option fire that callback too.
185
269
 
186
- | Method | Description |
187
- |----------------|------------------------------------------------------------|
188
- | `answer()` | Answer the current incoming call |
189
- | `hangup()` | End the active / outgoing call |
190
- | `reject()` | Reject the current incoming call |
191
- | `mute(bool)` | `true` = mute microphone, `false` = unmute |
192
- | `hold(bool)` | `true` = put call on hold, `false` = resume |
193
- | `dial(number)` | Programmatically dial a number string |
194
- | `destroy()` | Unmount the widget and clean up the DOM container |
270
+ | Event | Payload | Callback |
271
+ |------------------------|---------|----------|
272
+ | `register_status` | `'SIP_CONNECTED'` \| `'REGISTERED'` \| `{ status: 'FAILED', reason }` | `onRegisterStatus` (skips `SIP_CONNECTED`) |
273
+ | `agent_status` | `'READY'` \| `'NOT_READY'` \| `'AUX'` (`getState().currentAux` holds the AUX code) | `onAgentStatus` |
274
+ | `call_status` | `'Idle'` \| `'Calling...'` \| `'Ringing'` \| `'InCall'` | `onCallStatus` |
275
+ | `call_duration` | `'00:42'`, every second while connected, `'00:00'` on end | — |
276
+ | `call_failed` | `cause` string, e.g. `'Busy'`, `'Rejected'`, `'Unavailable'` | `onCallFailed` |
277
+ | `incoming_call` | `{ number, name, direction: 'incoming' }` | `onIncomingCall` |
278
+ | `sync_call_answered` | `{ number, name, direction }` | `onCallAnswered` |
279
+ | `sync_call_ended` | `{ number, name, direction, cause, originator }` | `onCallEnded` |
280
+ | `sync_call_unanswered` | `{ number, name, direction, cause, originator }` | `onCallUnanswered` |
281
+ | `mute_status` | `boolean` | — |
282
+ | `hold_status` | `boolean` | — |
283
+
284
+ - `direction`: `'incoming'` or `'outgoing'`.
285
+ - `originator`: who ended the call. `'local'` means you, `'remote'` means the other side, `'system'` means a network or SIP error.
286
+ - `cause`: the end reason, such as `'Terminated'`, `'Canceled'`, `'Rejected'`, `'Busy'` or `'No Answer'`.
195
287
 
196
288
  ---
197
289
 
198
- ## Agent status bar
290
+ ## Recipes
199
291
 
200
- When `autoReady: false` or `showAuxButton: true` a status bar appears at the bottom of the dialer.
292
+ ### Outgoing call
201
293
 
202
- > **Note:** The agent status bar calls the CPaaS extension status API. You **must** provide `X-CPAAS-Key-Fingerprint` (or one of its aliases) for these calls to succeed.
294
+ ```js
295
+ voice.on('call_status', (s) => render(s));
296
+ voice.on('call_duration', (d) => (timer.textContent = d));
297
+ voice.on('call_failed', (cause) => toast(`Call failed: ${cause}`));
298
+
299
+ callBtn.onclick = () => voice.dial(input.value);
300
+ hangupBtn.onclick = () => voice.hangup();
301
+ muteBtn.onclick = () => voice.mute(!voice.getState().isMuted);
302
+ holdBtn.onclick = () => voice.hold(!voice.getState().isOnHold);
303
+ keypad.onclick = (e) => voice.sendDtmf(e.target.dataset.key);
304
+ ```
305
+
306
+ ### Incoming call
203
307
 
204
308
  ```js
205
- OmnixWidget.init({
206
- auth: { ... },
207
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
208
- autoReady: false, // agent starts as NOT_READY; must go Ready manually
209
- showAuxButton: true, // show AUX dropdown
210
- auxOptions: [
211
- { code: 'BRK', label: 'Break' },
212
- { code: 'LCH', label: 'Lunch' },
213
- { code: 'TRN', label: 'Training' },
214
- ],
215
- onAgentStatus: (status) => console.log('Agent status changed:', status),
309
+ const voice = OmnixWidget.init({
310
+ ...config,
311
+ onIncomingCall: ({ number, name }) => showRinging(name || number),
312
+ onCallAnswered: () => showInCall(),
313
+ onCallEnded: ({ cause }) => showIdle(),
314
+ onCallUnanswered: ({ number }) => addMissedCall(number),
216
315
  });
316
+
317
+ answerBtn.onclick = () => voice.answer();
318
+ rejectBtn.onclick = () => voice.reject();
217
319
  ```
218
320
 
219
- | Status | Badge colour | Meaning |
220
- |-------------|--------------|--------------------------------------|
221
- | `READY` | 🟢 Green | Agent is available for calls |
222
- | `NOT_READY` | ⚪ Gray | Agent is unavailable |
223
- | `AUX` | 🟡 Amber | Agent is on a break / custom reason |
321
+ ### Agent status (manual Ready)
224
322
 
225
- ---
323
+ ```js
324
+ const voice = OmnixWidget.init({ ...config, autoReady: false });
226
325
 
227
- ## React integration
326
+ voice.on('register_status', (s) => {
327
+ if (s === 'SIP_CONNECTED') showSetReadyButton(); // connected, agent not Ready
328
+ if (s === 'REGISTERED') showDialer();
329
+ });
330
+
331
+ readyBtn.onclick = async () => { if (!(await voice.setReady())) toast('Failed to set Ready'); };
332
+ notReadyBtn.onclick = () => voice.setNotReady();
333
+ breakBtn.onclick = () => voice.setAux('BRK', 'Break');
334
+ ```
335
+
336
+ ### React hook
228
337
 
229
338
  ```jsx
230
- import { useEffect, useRef } from 'react';
339
+ import { useEffect, useRef, useState } from 'react';
231
340
  import { OmnixWidget } from 'voxnix';
232
341
 
233
- function App() {
234
- const widgetRef = useRef(null);
342
+ export function useVoice(config) {
343
+ const ref = useRef(null);
344
+ const [state, setState] = useState(null);
235
345
 
236
346
  useEffect(() => {
237
- widgetRef.current = OmnixWidget.init({
238
- auth: {
239
- username: '1001',
240
- pwd_pbx: 'secret',
241
- pabx_host: 'sip.example.com',
242
- },
243
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
244
- placement: 'bottom-right',
245
- autoReady: true,
246
- showAuxButton: true,
247
- auxOptions: [{ code: 'BRK', label: 'Break' }],
248
-
249
- onIncomingCall: (data) => { /* navigate, show notification, etc. */ },
250
- onCallAnswered: (data) => { /* routing logic */ },
251
- onCallEnded: () => { /* cleanup */ },
252
- onRegisterStatus: (s) => { /* update UI */ },
253
- onAgentStatus: (s) => { /* s: 'READY' | 'NOT_READY' | 'AUX' */ },
254
- });
255
-
256
- return () => widgetRef.current?.destroy();
257
- }, []);
258
-
259
- return <div id="app">...</div>;
347
+ const voice = OmnixWidget.init(config);
348
+ ref.current = voice;
349
+ const sync = () => setState(voice.getState());
350
+ const events = ['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status'];
351
+ events.forEach((e) => voice.on(e, sync));
352
+ sync();
353
+ return () => voice.destroy();
354
+ }, []); // init once
355
+
356
+ return [state, ref];
260
357
  }
261
- ```
262
358
 
263
- ---
359
+ // const [state, voice] = useVoice(config);
360
+ // <button onClick={() => voice.current.dial('1002')}>Call</button>
361
+ ```
264
362
 
265
- ## Vue integration
363
+ ### Vue 3
266
364
 
267
365
  ```js
366
+ import { onMounted, onUnmounted, ref } from 'vue';
268
367
  import { OmnixWidget } from 'voxnix';
269
368
 
270
- // inside setup() or options API
271
- onMounted(() => {
272
- const widget = OmnixWidget.init({
273
- auth: { username: '1001', pwd_pbx: 'secret', pabx_host: 'sip.example.com' },
274
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
275
- autoReady: false,
276
- showAuxButton: true,
277
- auxOptions: [{ code: 'BRK', label: 'Break' }],
369
+ export function useVoice(config) {
370
+ const state = ref(null);
371
+ let voice;
372
+ onMounted(() => {
373
+ voice = OmnixWidget.init(config);
374
+ const sync = () => (state.value = voice.getState());
375
+ ['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status']
376
+ .forEach((e) => voice.on(e, sync));
377
+ sync();
278
378
  });
279
- onUnmounted(() => widget.destroy());
280
- });
379
+ onUnmounted(() => voice?.destroy());
380
+ return { state, dial: (n) => voice.dial(n), hangup: () => voice.hangup() };
381
+ }
281
382
  ```
282
383
 
283
384
  ---
284
385
 
285
- ## Via CDN (plain HTML, no bundler)
386
+ ## Browser requirements
286
387
 
287
- ```html
288
- <!-- Self-contained IIFE build (bundles React) -->
289
- <script src="https://unpkg.com/voxnix/dist/voxnix.iife.js"></script>
290
- <script>
291
- OmnixWidget.init({
292
- auth: {
293
- username: '1001',
294
- pwd_pbx: 'secret',
295
- pabx_host: 'sip.example.com',
296
- },
297
- 'X-CPAAS-Key-Fingerprint': 'your-fingerprint-here',
298
- placement: 'bottom-right',
299
- autoReady: true,
300
- showAuxButton: true,
301
- auxOptions: [
302
- { code: 'BRK', label: 'Break' },
303
- { code: 'LCH', label: 'Lunch' },
304
- ],
305
- onRegisterStatus: (s) => console.log('SIP:', s),
306
- onIncomingCall: (d) => console.log('Incoming:', d),
307
- onAgentStatus: (s) => console.log('Agent:', s),
308
- });
309
- </script>
310
- ```
311
-
312
- > If you need to use your own React instance, include `react@18` and `react-dom@18` UMD builds **before** the script tag and use `dist/voxnix.js` (ESM) or `dist/voxnix.umd.cjs` (CJS) instead.
388
+ - **HTTPS (or `localhost`)**: browsers only give microphone access on secure origins.
389
+ - **Microphone permission**: the browser asks for it on the first `dial()` or `answer()`. If the user denies it, the call fails (`call_failed`, or `onCallUnanswered` for an incoming call).
390
+ - **Remote audio**: Voxnix creates a hidden `<audio id="voxnix-remote-audio">` in `document.body`. Autoplay rules may block it until the user has clicked something on the page. Answering or dialing from a click handler counts as that interaction.
391
+ - **Ringtone**: generated with WebAudio, which has the same user-interaction rule. Set `ringtone: false` to use your own sound.
313
392
 
314
393
  ---
315
394
 
316
- ## Keyboard shortcuts
317
-
318
- These shortcuts are active whenever the dialer widget is open:
395
+ ## Security
319
396
 
320
- | Key | State | Action |
321
- |------------------|---------|-------------------------------|
322
- | `0–9`, `*`, `#` | Idle | Append digit to dial input |
323
- | `Backspace` | Idle | Delete last digit |
324
- | `Enter` | Idle | Dial the current number |
325
- | `0–9`, `*`, `#` | In-call | Send DTMF tone |
397
+ - **Treat caller data as untrusted.** `name` comes from the caller's SIP display name and `number`
398
+ from their SIP URI. Render them with `textContent` (or your framework's default escaping),
399
+ never `innerHTML`.
400
+ - **Credentials live in the browser.** The SIP password and `X-CPAAS-Key-Fingerprint` are readable
401
+ by anyone with devtools on that page. Fetch them from your backend after the agent logs in,
402
+ use per-agent SIP credentials, and never hardcode them in frontend code.
403
+ - **HTTPS only.** `cpaas_base_url` must be `https://` (`http://localhost` is allowed for development).
404
+ Any other URL disables CPaaS API calls, and agent-status methods return `false`.
405
+ - **Input validation.** `dial()` only accepts `0-9 A-Z a-z + * # . _ -`. Anything else emits
406
+ `call_failed` with `'Invalid number'`.
407
+ - Voxnix never logs credentials or API responses to the console.
326
408
 
327
409
  ---
328
410
 
329
- ## Build
330
-
331
- ```bash
332
- # ESM + UMD (peer React — for npm consumers)
333
- yarn build:lib
411
+ ## Sample page
334
412
 
335
- # Self-contained IIFE (bundles React — for CDN / script tag)
336
- yarn build:iife
413
+ [`examples/index.html`](examples/index.html) is a plain-HTML page that loads the script-tag
414
+ build. It calls `OmnixWidget.init()` with a config object, has buttons for every API method, and
415
+ shows a live `getState()` view and an event log.
337
416
 
338
- # Both outputs
339
- yarn build
417
+ ```bash
418
+ npm run build
419
+ python3 -m http.server 5599 # from the repo root
420
+ # open http://localhost:5599/examples/
340
421
  ```
341
422
 
342
- Output files in `dist/`:
343
-
344
- | File | Format | React | Use case |
345
- |--------------------|--------|----------|-----------------------|
346
- | `voxnix.js` | ESM | peer dep | `import` / bundlers |
347
- | `voxnix.umd.cjs` | UMD | peer dep | CommonJS / `require` |
348
- | `voxnix.iife.js` | IIFE | bundled | `<script>` / CDN |
423
+ Edit the `CONFIG` object at the top of the script to point it at your PBX.
349
424
 
350
425
  ---
351
426
 
352
- ## Development
427
+ ## Build
353
428
 
354
429
  ```bash
355
- yarn install
356
- yarn dev # Vite dev server with hot reload
357
- yarn watch # Vite library watch mode
430
+ npm install
431
+ npm run build # ESM + UMD + IIFE into dist/
432
+ npm run build:lib # ESM + UMD only
433
+ npm run build:iife # IIFE only
434
+ npm run watch # rebuild ESM + UMD on change
358
435
  ```
359
436
 
360
437
  ---
361
438
 
439
+ ## Migrating from 1.x
440
+
441
+ - **The UI is gone.** There is no floating button or dialer panel. The options `placement`, `offset`, `container`, `showAuxButton` and `auxOptions` are ignored. Build the UI with the [Instance API](#instance-api) and [events](#events--callbacks).
442
+ - **New SIP engine.** Call callbacks now receive `{ number, name, direction, ... }` instead of a raw SIP session object.
443
+ - `hold(true)` now holds and `hold(false)` resumes (1.x had them reversed).
444
+ - `onCallEnded` fires once per call. Before, it could fire twice for incoming calls.
445
+ - New in 2.0: `sendDtmf`, `setReady`, `setNotReady`, `setAux`, `getState`, `on`, plus the options `onCallStatus`, `onCallFailed`, `ringtone` and `contacts`.
446
+ - `contacts` has no built-in demo entries any more. Pass your own map.
447
+
448
+ ---
449
+
362
450
  ## Changelog
363
451
 
452
+ ### v2.0.0
453
+ - Removed the built-in React UI; Voxnix is now a headless SDK (no React / Tailwind dependencies)
454
+ - New internal SIP engine
455
+ - New API: `sendDtmf`, `setReady`, `setNotReady`, `setAux`, `getState`, `on`; options `onCallStatus`, `onCallFailed`, `ringtone`, `contacts`
456
+ - Configurable `cpaas_base_url`, `pbx_url` (SIP domain), `sip_url` (WSS host)
457
+ - Re-REGISTER + CPaaS poll when the extension reports `is_register = 0`
458
+ - Security: credentials and API responses are no longer logged (1.0.23–1.0.24 logged the full config); HTTPS-only CPaaS URL; `dial()` input validation; `prepublishOnly` build
459
+ - Fixes: cancel while dialing, double `onCallEnded`, inverted `hold()`, outgoing call duration and callbacks, busy second incoming call, `destroy()` not stopping SIP, IIFE global `OmnixWidget.init`
460
+
364
461
  ### v1.0.12
365
462
  - Rollback DevToast / console interceptor feature
366
463