@coventia/softphone 0.2.1 → 0.4.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.
Files changed (40) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-C6YpT6uR.d.cts} +107 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-C6YpT6uR.d.ts} +107 -1
  3. package/dist/chunk-5E5OEXS2.js +14 -0
  4. package/dist/chunk-5E5OEXS2.js.map +1 -0
  5. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  6. package/dist/chunk-UHNLEBBO.js.map +1 -0
  7. package/dist/element/index.d.ts +124 -9
  8. package/dist/element/index.js +1837 -413
  9. package/dist/element/index.js.map +1 -1
  10. package/dist/{engine-BlQBo2Ys.d.cts → engine-CDdaSojx.d.cts} +1 -1
  11. package/dist/{engine-C0BOkIcW.d.ts → engine-DPft99Wj.d.ts} +1 -1
  12. package/dist/index.cjs +213 -13
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +16 -6
  15. package/dist/index.d.ts +16 -6
  16. package/dist/index.js +75 -10
  17. package/dist/index.js.map +1 -1
  18. package/dist/react/index.cjs +2 -0
  19. package/dist/react/index.cjs.map +1 -1
  20. package/dist/react/index.d.cts +2 -2
  21. package/dist/react/index.d.ts +2 -2
  22. package/dist/react/index.js +1 -1
  23. package/dist/{softphone-DlGV12Xp.d.cts → softphone-CY5d2WWY.d.cts} +28 -10
  24. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DXgmTyrX.d.ts} +28 -10
  25. package/dist/softphone.css +364 -35
  26. package/dist/{sources-IRKNQPWW.js → sources-ONYQW733.js} +125 -6
  27. package/dist/sources-ONYQW733.js.map +1 -0
  28. package/dist/testing/index.d.cts +2 -2
  29. package/dist/testing/index.d.ts +2 -2
  30. package/dist/ui/index.cjs +1450 -33
  31. package/dist/ui/index.cjs.map +1 -1
  32. package/dist/ui/index.d.cts +25 -3
  33. package/dist/ui/index.d.ts +25 -3
  34. package/dist/ui/index.js +1445 -36
  35. package/dist/ui/index.js.map +1 -1
  36. package/dist/zadarma/index.d.cts +2 -2
  37. package/dist/zadarma/index.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/chunk-LMJXX4YM.js.map +0 -1
  40. package/dist/sources-IRKNQPWW.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-VKq1I8yP.cjs';
1
+ import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, w as SoftphoneState, s as SoftphoneError } from './call-C6YpT6uR.cjs';
2
2
 
3
3
  /**
4
4
  * The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
@@ -1,4 +1,4 @@
1
- import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-VKq1I8yP.js';
1
+ import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, w as SoftphoneState, s as SoftphoneError } from './call-C6YpT6uR.js';
2
2
 
3
3
  /**
4
4
  * The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
package/dist/index.cjs CHANGED
@@ -110,9 +110,9 @@ function createRequest(config, method) {
110
110
  });
111
111
  const body = await res.json().catch(() => null);
112
112
  const message = body?.message ?? `${path} \u2014 HTTP ${res.status}`;
113
- if (!res.ok) throw new ApiError(res.status, message);
113
+ if (!res.ok) throw new ApiError(res.status, message, body);
114
114
  if (body?.success === false) {
115
- throw new ApiError(res.status, message);
115
+ throw new ApiError(res.status, message, body);
116
116
  }
117
117
  return body;
118
118
  }
@@ -138,15 +138,32 @@ var init_api = __esm({
138
138
  init_token_cache();
139
139
  ApiError = class extends Error {
140
140
  status;
141
- constructor(status, message) {
141
+ body;
142
+ constructor(status, message, body = null) {
142
143
  super(message);
143
144
  this.name = "ApiError";
144
145
  this.status = status;
146
+ this.body = body;
145
147
  }
146
148
  };
147
149
  }
148
150
  });
149
151
 
152
+ // src/core/outbound-numbers.ts
153
+ function isOutboundNumberRefused(value) {
154
+ const refusal = value?.refusal;
155
+ return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
156
+ }
157
+ function outboundNumberRefused(refusal) {
158
+ const error = new Error(`Outbound number change refused (${refusal.reason}); retry in ${refusal.retryAfterSeconds}s`);
159
+ error.name = "OutboundNumberRefused";
160
+ return Object.assign(error, { refusal });
161
+ }
162
+ var init_outbound_numbers = __esm({
163
+ "src/core/outbound-numbers.ts"() {
164
+ }
165
+ });
166
+
150
167
  // src/coventia/sources.ts
151
168
  var sources_exports = {};
152
169
  __export(sources_exports, {
@@ -228,6 +245,8 @@ function coventiaSources(config) {
228
245
  // row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
229
246
  // would just repeat the row under a wrong label.
230
247
  ...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
248
+ // And the other way round: which of the org's numbers an outbound call showed.
249
+ ...toDirection(row.direction) === "outbound" && row.from_number ? { from: row.from_number } : {},
231
250
  recorded,
232
251
  handledBy,
233
252
  // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
@@ -331,6 +350,118 @@ function coventiaSources(config) {
331
350
  })();
332
351
  });
333
352
  };
353
+ const fileBase = config.baseUrl.replace(/\/+$/, "");
354
+ const toLoop = (l) => l && Number.isFinite(l.start) && Number.isFinite(l.end) && l.start >= 0 && l.end > l.start ? { start: l.start, end: l.end } : null;
355
+ const toRingtoneSettings = (body) => {
356
+ const catalogue = (body.catalogue ?? []).map((r) => ({
357
+ id: r.id,
358
+ name: r.name,
359
+ url: r.path ? `${fileBase}${r.path}` : null,
360
+ loop: r.path ? toLoop(r.loop) : null
361
+ }));
362
+ const builtIn = catalogue.find((r) => r.url === null)?.id ?? "clasico";
363
+ const effective = catalogue.some((r) => r.id === body.effective) ? body.effective : builtIn;
364
+ return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };
365
+ };
366
+ const CACHE_PREFIX = "coventia-softphone:ringtones:";
367
+ const cacheKey = async (prefix = CACHE_PREFIX) => {
368
+ try {
369
+ const { identity } = await config.tokenSource.getToken();
370
+ return `${prefix}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
371
+ } catch {
372
+ return null;
373
+ }
374
+ };
375
+ const remember = async (settings) => {
376
+ const key = await cacheKey();
377
+ try {
378
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
379
+ } catch {
380
+ }
381
+ return settings;
382
+ };
383
+ const recall = async () => {
384
+ const key = await cacheKey();
385
+ try {
386
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
387
+ const s = raw ? JSON.parse(raw) : null;
388
+ const ok = s && Array.isArray(s.catalogue) && s.catalogue.every((r) => typeof r?.id === "string" && typeof r.name === "string" && (r.url === null || typeof r.url === "string")) && s.catalogue.some((r) => r.id === s.effective);
389
+ return ok ? { ...s, catalogue: s.catalogue.map((r) => ({ ...r, loop: toLoop(r.loop) })) } : null;
390
+ } catch {
391
+ return null;
392
+ }
393
+ };
394
+ const ringtones = {
395
+ get: async (signal) => remember(toRingtoneSettings(await get(`${base}/ringtones`, org, signal))),
396
+ // `null` clears: the empty id is dropped from the query by the request helper, and the backend reads a
397
+ // missing `ringtone_id` as "clear".
398
+ setMine: async (id) => remember(
399
+ toRingtoneSettings(await post(`${base}/ringtones/user`, { ...org, ringtone_id: id ?? "" }))
400
+ ),
401
+ cached: recall
402
+ };
403
+ const KINDS = /* @__PURE__ */ new Set(["landline", "mobile", "commercial", "other"]);
404
+ const STATES = /* @__PURE__ */ new Set(["applied", "pending", "failed", "unassigned"]);
405
+ const toOutboundSettings = (body) => {
406
+ const numbers = (body.numbers ?? []).map((n) => ({
407
+ id: n.id,
408
+ number: n.number,
409
+ label: n.label ?? null,
410
+ kind: KINDS.has(n.kind ?? "") ? n.kind : "other"
411
+ }));
412
+ const known = (id) => id && numbers.some((n) => n.id === id) ? id : null;
413
+ return {
414
+ numbers,
415
+ orgDefault: body.orgDefault ?? null,
416
+ mine: known(body.mine),
417
+ // Defensive: a malformed answer naming a number that is not in the list reads as the org default.
418
+ effective: known(body.effective) ?? known(body.orgDefault),
419
+ extension: body.extension ?? null,
420
+ state: STATES.has(body.state ?? "") ? body.state : "applied",
421
+ changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0))
422
+ };
423
+ };
424
+ const OUTBOUND_PREFIX = "coventia-softphone:outbound-number:";
425
+ const rememberOutbound = async (settings) => {
426
+ const key = await cacheKey(OUTBOUND_PREFIX);
427
+ try {
428
+ if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
429
+ } catch {
430
+ }
431
+ return settings;
432
+ };
433
+ const recallOutbound = async () => {
434
+ const key = await cacheKey(OUTBOUND_PREFIX);
435
+ try {
436
+ const raw = key ? globalThis.localStorage?.getItem(key) : null;
437
+ const s = raw ? JSON.parse(raw) : null;
438
+ const ok = s && Array.isArray(s.numbers) && s.numbers.every((n) => typeof n?.id === "string" && typeof n.number === "string") && (s.effective === null || s.numbers.some((n) => n.id === s.effective));
439
+ return ok ? { ...s, changeAvailableInSeconds: 0 } : null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ };
444
+ const outboundNumbers = {
445
+ get: async (signal) => rememberOutbound(toOutboundSettings(await get(`${base}/outbound-numbers`, org, signal))),
446
+ // `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.
447
+ setMine: async (id) => {
448
+ try {
449
+ return rememberOutbound(
450
+ toOutboundSettings(await post(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? "" }))
451
+ );
452
+ } catch (error) {
453
+ const body = error instanceof ApiError ? error.body : null;
454
+ if (error instanceof ApiError && error.status === 429 && body?.code === "rate_limited") {
455
+ throw outboundNumberRefused({
456
+ reason: body.reason === "user" ? "user" : "platform",
457
+ retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1))
458
+ });
459
+ }
460
+ throw error;
461
+ }
462
+ },
463
+ cached: recallOutbound
464
+ };
334
465
  const contacts = config.contacts ?? coventiaContacts;
335
466
  const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
336
467
  const history = withNameResolution(rawHistory, resolve);
@@ -342,7 +473,9 @@ function coventiaSources(config) {
342
473
  resolveContact: resolve,
343
474
  listAgents: coventiaListAgents,
344
475
  transferOutcomes,
345
- presence
476
+ presence,
477
+ ringtones,
478
+ outboundNumbers
346
479
  };
347
480
  }
348
481
  var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toWebSocketUrl;
@@ -350,6 +483,7 @@ var init_sources2 = __esm({
350
483
  "src/coventia/sources.ts"() {
351
484
  init_sources();
352
485
  init_api();
486
+ init_outbound_numbers();
353
487
  toContact = (row) => ({
354
488
  id: row.wa_id,
355
489
  name: row.name,
@@ -2006,7 +2140,17 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
2006
2140
  resolveContact: async (number) => (await load()).resolveContact(number),
2007
2141
  listAgents: async () => (await load()).listAgents(),
2008
2142
  transferOutcomes: async (params) => (await load()).transferOutcomes(params),
2009
- presence: async (callbacks) => (await load()).presence(callbacks)
2143
+ presence: async (callbacks) => (await load()).presence(callbacks),
2144
+ ringtones: {
2145
+ get: async (signal) => (await load()).ringtones.get(signal),
2146
+ setMine: async (id) => (await load()).ringtones.setMine(id),
2147
+ cached: async () => (await load()).ringtones.cached?.() ?? null
2148
+ },
2149
+ outboundNumbers: {
2150
+ get: async (signal) => (await load()).outboundNumbers.get(signal),
2151
+ setMine: async (id) => (await load()).outboundNumbers.setMine(id),
2152
+ cached: async () => (await load()).outboundNumbers.cached?.() ?? null
2153
+ }
2010
2154
  };
2011
2155
  }
2012
2156
 
@@ -2229,13 +2373,30 @@ var BrowserAudioDevices = class {
2229
2373
  outputs = [];
2230
2374
  currentInputId = null;
2231
2375
  currentOutputId = null;
2376
+ currentRingtoneOutputId = null;
2232
2377
  canSelectOutput;
2378
+ canSelectRingtoneOutput;
2233
2379
  #engine;
2234
2380
  #onChange;
2381
+ /**
2382
+ * What the user CHOSE, remembered across reloads — distinct from the `current*` fields, which are
2383
+ * what is usable right now. Keeping them apart is what stops a fresh page load from wiping a saved
2384
+ * choice: until the microphone has been granted, Chrome enumerates every device with a blank id, so
2385
+ * "is the saved device in the list?" is no on every first enumeration. The preference survives
2386
+ * that; `current*` simply reads `null` until the device is really there.
2387
+ */
2388
+ #preferred;
2389
+ /** The input the engine was last told about — it starts on the system default (`null`). */
2390
+ #engineInputId = null;
2235
2391
  constructor(engine, onChange) {
2236
2392
  this.#engine = engine;
2237
2393
  this.#onChange = onChange;
2238
2394
  this.canSelectOutput = isBrowser() && typeof HTMLMediaElement !== "undefined" && "setSinkId" in HTMLMediaElement.prototype;
2395
+ this.canSelectRingtoneOutput = this.canSelectOutput;
2396
+ this.#preferred = readStoredDevices();
2397
+ this.currentInputId = this.#preferred.input;
2398
+ this.currentOutputId = this.#preferred.output;
2399
+ this.currentRingtoneOutputId = this.#preferred.ringtone;
2239
2400
  if (isBrowser() && navigator.mediaDevices) {
2240
2401
  void this.refresh();
2241
2402
  navigator.mediaDevices.addEventListener?.("devicechange", () => void this.refresh());
@@ -2251,26 +2412,63 @@ var BrowserAudioDevices = class {
2251
2412
  }
2252
2413
  this.inputs = all.filter((d) => d.kind === "audioinput");
2253
2414
  this.outputs = all.filter((d) => d.kind === "audiooutput");
2254
- if (this.currentInputId && !this.inputs.some((d) => d.deviceId === this.currentInputId)) {
2255
- this.currentInputId = null;
2256
- }
2257
- if (this.currentOutputId && !this.outputs.some((d) => d.deviceId === this.currentOutputId)) {
2258
- this.currentOutputId = null;
2259
- }
2415
+ this.currentInputId = usable(this.#preferred.input, this.inputs);
2416
+ this.currentOutputId = usable(this.#preferred.output, this.outputs);
2417
+ this.currentRingtoneOutputId = usable(this.#preferred.ringtone, this.outputs);
2260
2418
  this.#onChange();
2419
+ if (this.currentInputId && this.currentInputId !== this.#engineInputId) {
2420
+ await this.#tellEngine(this.currentInputId);
2421
+ }
2261
2422
  }
2262
2423
  async setInput(deviceId) {
2263
2424
  this.currentInputId = deviceId;
2425
+ this.#remember({ input: deviceId });
2264
2426
  this.#onChange();
2427
+ await this.#tellEngine(deviceId);
2428
+ }
2429
+ async #tellEngine(deviceId) {
2430
+ this.#engineInputId = deviceId;
2265
2431
  await this.#engine.setInputDevice?.(deviceId);
2266
2432
  }
2267
2433
  async setOutput(deviceId) {
2268
2434
  this.currentOutputId = deviceId;
2435
+ this.#remember({ output: deviceId });
2269
2436
  this.#onChange();
2270
2437
  }
2271
- async setRingtoneOutput() {
2438
+ async setRingtoneOutput(deviceId) {
2439
+ this.currentRingtoneOutputId = deviceId;
2440
+ this.#remember({ ringtone: deviceId });
2441
+ this.#onChange();
2442
+ }
2443
+ #remember(patch) {
2444
+ this.#preferred = { ...this.#preferred, ...patch };
2445
+ writeStoredDevices(this.#preferred);
2272
2446
  }
2273
2447
  };
2448
+ var DEVICES_KEY = "coventia-softphone:devices";
2449
+ var NO_DEVICES = { input: null, output: null, ringtone: null };
2450
+ function usable(id, list) {
2451
+ return id && list.some((d) => d.deviceId === id) ? id : null;
2452
+ }
2453
+ function readStoredDevices() {
2454
+ if (!isBrowser()) return NO_DEVICES;
2455
+ try {
2456
+ const raw = JSON.parse(globalThis.localStorage?.getItem(DEVICES_KEY) ?? "null");
2457
+ if (!raw || typeof raw !== "object") return NO_DEVICES;
2458
+ const pick = (v) => typeof v === "string" && v ? v : null;
2459
+ const r = raw;
2460
+ return { input: pick(r.input), output: pick(r.output), ringtone: pick(r.ringtone) };
2461
+ } catch {
2462
+ return NO_DEVICES;
2463
+ }
2464
+ }
2465
+ function writeStoredDevices(devices) {
2466
+ if (!isBrowser()) return;
2467
+ try {
2468
+ globalThis.localStorage?.setItem(DEVICES_KEY, JSON.stringify(devices));
2469
+ } catch {
2470
+ }
2471
+ }
2274
2472
  var SoftphoneImpl = class extends Emitter {
2275
2473
  #state = "stopped";
2276
2474
  #lastError = null;
@@ -2674,7 +2872,8 @@ function isCallEngine(value) {
2674
2872
 
2675
2873
  // src/index.ts
2676
2874
  init_errors();
2677
- var VERSION = "0.2.1";
2875
+ init_outbound_numbers();
2876
+ var VERSION = "0.4.0";
2678
2877
 
2679
2878
  exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
2680
2879
  exports.CallEvent = CallEvent;
@@ -2685,6 +2884,7 @@ exports.canTransition = canTransition;
2685
2884
  exports.createSoftphone = createSoftphone;
2686
2885
  exports.isBrowser = isBrowser;
2687
2886
  exports.isFatal = isFatal;
2887
+ exports.isOutboundNumberRefused = isOutboundNumberRefused;
2688
2888
  exports.isTerminal = isTerminal;
2689
2889
  exports.isWebRtcSupported = isWebRtcSupported;
2690
2890
  //# sourceMappingURL=index.cjs.map