@coventia/softphone 0.2.1 → 0.3.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 (38) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-CaPDWVk3.d.cts} +44 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-CaPDWVk3.d.ts} +44 -1
  3. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  4. package/dist/chunk-UHNLEBBO.js.map +1 -0
  5. package/dist/element/index.d.ts +68 -9
  6. package/dist/element/index.js +962 -114
  7. package/dist/element/index.js.map +1 -1
  8. package/dist/{engine-C0BOkIcW.d.ts → engine-BgBsIcEa.d.ts} +1 -1
  9. package/dist/{engine-BlQBo2Ys.d.cts → engine-CU2rEo3g.d.cts} +1 -1
  10. package/dist/index.cjs +120 -10
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +69 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/index.cjs +2 -0
  17. package/dist/react/index.cjs.map +1 -1
  18. package/dist/react/index.d.cts +2 -2
  19. package/dist/react/index.d.ts +2 -2
  20. package/dist/react/index.js +1 -1
  21. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
  22. package/dist/{softphone-DlGV12Xp.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
  23. package/dist/softphone.css +110 -12
  24. package/dist/{sources-IRKNQPWW.js → sources-M3GYMS6V.js} +54 -3
  25. package/dist/sources-M3GYMS6V.js.map +1 -0
  26. package/dist/testing/index.d.cts +2 -2
  27. package/dist/testing/index.d.ts +2 -2
  28. package/dist/ui/index.cjs +885 -28
  29. package/dist/ui/index.cjs.map +1 -1
  30. package/dist/ui/index.d.cts +25 -3
  31. package/dist/ui/index.d.ts +25 -3
  32. package/dist/ui/index.js +885 -31
  33. package/dist/ui/index.js.map +1 -1
  34. package/dist/zadarma/index.d.cts +2 -2
  35. package/dist/zadarma/index.d.ts +2 -2
  36. package/package.json +1 -1
  37. package/dist/chunk-LMJXX4YM.js.map +0 -1
  38. package/dist/sources-IRKNQPWW.js.map +0 -1
@@ -372,6 +372,49 @@ interface PresenceCallbacks {
372
372
  * long-lived — the returned promise resolves once, when the connection opens, not once per update.
373
373
  */
374
374
  type PresenceSource = (callbacks: PresenceCallbacks) => Promise<PresenceConnection>;
375
+ /** One ringtone a user can pick (custom-ringtone.md §4.1). */
376
+ interface RingtoneOption {
377
+ /** Stable id — what is stored as the user's or the org's choice. */
378
+ readonly id: string;
379
+ /** Display name, in Spanish, as both pickers show it. */
380
+ readonly name: string;
381
+ /**
382
+ * Absolute URL of the MP3, or `null` for the tone the SDK synthesises itself (`clasico`) — the one
383
+ * that is always available and plays whenever a file is not ready.
384
+ */
385
+ readonly url: string | null;
386
+ /**
387
+ * Where the music sits inside the decoded file, in seconds — loop between these, not over the whole file. An
388
+ * MP3 round trip adds a fixed delay at the start and padding at the end; looping the whole buffer would make a
389
+ * continuous ringtone stumble at every restart. `null` for the built-in tone, or when the backend gave none.
390
+ */
391
+ readonly loop: {
392
+ readonly start: number;
393
+ readonly end: number;
394
+ } | null;
395
+ }
396
+ /** The catalogue and the three levels of choice, as the backend resolves them for the caller. */
397
+ interface RingtoneSettings {
398
+ /** In display order. Always contains the built-in tone. */
399
+ readonly catalogue: readonly RingtoneOption[];
400
+ /** The org's default, or `null` when the org has not set one. */
401
+ readonly orgDefault: string | null;
402
+ /** This user's own choice, or `null` = "use the org default". */
403
+ readonly mine: string | null;
404
+ /** What actually rings: `mine`, else `orgDefault`, else the built-in tone — always in `catalogue`. */
405
+ readonly effective: string;
406
+ }
407
+ /** The ringtone settings of the user the softphone is signed in as, in its organization. */
408
+ interface RingtoneSource {
409
+ get(signal?: AbortSignal): Promise<RingtoneSettings>;
410
+ /** Saves this user's choice; `null` clears it (back to the org default). Resolves to the new settings. */
411
+ setMine(id: string | null): Promise<RingtoneSettings>;
412
+ /**
413
+ * The settings this user last had on this device, if remembered — instant, no network. Used only to start
414
+ * loading the likely ringtone early; `get()` is always the truth. Optional: a source may not remember.
415
+ */
416
+ cached?(): Promise<RingtoneSettings | null>;
417
+ }
375
418
 
376
419
  /**
377
420
  * The error taxonomy.
@@ -830,4 +873,4 @@ interface Call extends TypedEmitter<CallEventMap> {
830
873
  cancelConsult(): Promise<void>;
831
874
  }
832
875
 
833
- export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type TransferOutcomesPage as w, type PresenceSource as x, type CallHistoryEntry as y, type ContactsPage as z };
876
+ export { type AgentsDirectory as A, type PresenceSource as B, CALL_TRANSITIONS as C, type CallHistoryEntry as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, type ContactsPage as G, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type RingtoneOption as k, type RingtoneSettings as l, type RingtoneSource as m, type SoftphoneError as n, type SoftphoneErrorCode as o, SoftphoneEvent as p, type SoftphoneEventMap as q, type SoftphoneState as r, type TokenSource as s, type TypedEmitter as t, canTransition as u, isFatal as v, isTerminal as w, type ContactsSource as x, type HistorySource as y, type TransferOutcomesPage as z };
@@ -372,6 +372,49 @@ interface PresenceCallbacks {
372
372
  * long-lived — the returned promise resolves once, when the connection opens, not once per update.
373
373
  */
374
374
  type PresenceSource = (callbacks: PresenceCallbacks) => Promise<PresenceConnection>;
375
+ /** One ringtone a user can pick (custom-ringtone.md §4.1). */
376
+ interface RingtoneOption {
377
+ /** Stable id — what is stored as the user's or the org's choice. */
378
+ readonly id: string;
379
+ /** Display name, in Spanish, as both pickers show it. */
380
+ readonly name: string;
381
+ /**
382
+ * Absolute URL of the MP3, or `null` for the tone the SDK synthesises itself (`clasico`) — the one
383
+ * that is always available and plays whenever a file is not ready.
384
+ */
385
+ readonly url: string | null;
386
+ /**
387
+ * Where the music sits inside the decoded file, in seconds — loop between these, not over the whole file. An
388
+ * MP3 round trip adds a fixed delay at the start and padding at the end; looping the whole buffer would make a
389
+ * continuous ringtone stumble at every restart. `null` for the built-in tone, or when the backend gave none.
390
+ */
391
+ readonly loop: {
392
+ readonly start: number;
393
+ readonly end: number;
394
+ } | null;
395
+ }
396
+ /** The catalogue and the three levels of choice, as the backend resolves them for the caller. */
397
+ interface RingtoneSettings {
398
+ /** In display order. Always contains the built-in tone. */
399
+ readonly catalogue: readonly RingtoneOption[];
400
+ /** The org's default, or `null` when the org has not set one. */
401
+ readonly orgDefault: string | null;
402
+ /** This user's own choice, or `null` = "use the org default". */
403
+ readonly mine: string | null;
404
+ /** What actually rings: `mine`, else `orgDefault`, else the built-in tone — always in `catalogue`. */
405
+ readonly effective: string;
406
+ }
407
+ /** The ringtone settings of the user the softphone is signed in as, in its organization. */
408
+ interface RingtoneSource {
409
+ get(signal?: AbortSignal): Promise<RingtoneSettings>;
410
+ /** Saves this user's choice; `null` clears it (back to the org default). Resolves to the new settings. */
411
+ setMine(id: string | null): Promise<RingtoneSettings>;
412
+ /**
413
+ * The settings this user last had on this device, if remembered — instant, no network. Used only to start
414
+ * loading the likely ringtone early; `get()` is always the truth. Optional: a source may not remember.
415
+ */
416
+ cached?(): Promise<RingtoneSettings | null>;
417
+ }
375
418
 
376
419
  /**
377
420
  * The error taxonomy.
@@ -830,4 +873,4 @@ interface Call extends TypedEmitter<CallEventMap> {
830
873
  cancelConsult(): Promise<void>;
831
874
  }
832
875
 
833
- export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type TransferOutcomesPage as w, type PresenceSource as x, type CallHistoryEntry as y, type ContactsPage as z };
876
+ export { type AgentsDirectory as A, type PresenceSource as B, CALL_TRANSITIONS as C, type CallHistoryEntry as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, type ContactsPage as G, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type RingtoneOption as k, type RingtoneSettings as l, type RingtoneSource as m, type SoftphoneError as n, type SoftphoneErrorCode as o, SoftphoneEvent as p, type SoftphoneEventMap as q, type SoftphoneState as r, type TokenSource as s, type TypedEmitter as t, canTransition as u, isFatal as v, isTerminal as w, type ContactsSource as x, type HistorySource as y, type TransferOutcomesPage as z };
@@ -126,7 +126,9 @@ function useDevices() {
126
126
  outputs: audio.outputs,
127
127
  currentInputId: audio.currentInputId,
128
128
  currentOutputId: audio.currentOutputId,
129
+ currentRingtoneOutputId: audio.currentRingtoneOutputId,
129
130
  canSelectOutput: audio.canSelectOutput,
131
+ canSelectRingtoneOutput: audio.canSelectRingtoneOutput,
130
132
  setInput: (deviceId) => audio.setInput(deviceId),
131
133
  setOutput: (deviceId) => audio.setOutput(deviceId),
132
134
  setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
@@ -139,5 +141,5 @@ function useDevices() {
139
141
  }
140
142
 
141
143
  export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
142
- //# sourceMappingURL=chunk-LMJXX4YM.js.map
143
- //# sourceMappingURL=chunk-LMJXX4YM.js.map
144
+ //# sourceMappingURL=chunk-UHNLEBBO.js.map
145
+ //# sourceMappingURL=chunk-UHNLEBBO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/context.tsx","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"chunk-UHNLEBBO.js","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own\r\n * always-connected decision), so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
@@ -233,9 +233,52 @@ interface PresenceCallbacks {
233
233
  * long-lived — the returned promise resolves once, when the connection opens, not once per update.
234
234
  */
235
235
  type PresenceSource = (callbacks: PresenceCallbacks) => Promise<PresenceConnection>;
236
+ /** One ringtone a user can pick (custom-ringtone.md §4.1). */
237
+ interface RingtoneOption {
238
+ /** Stable id — what is stored as the user's or the org's choice. */
239
+ readonly id: string;
240
+ /** Display name, in Spanish, as both pickers show it. */
241
+ readonly name: string;
242
+ /**
243
+ * Absolute URL of the MP3, or `null` for the tone the SDK synthesises itself (`clasico`) — the one
244
+ * that is always available and plays whenever a file is not ready.
245
+ */
246
+ readonly url: string | null;
247
+ /**
248
+ * Where the music sits inside the decoded file, in seconds — loop between these, not over the whole file. An
249
+ * MP3 round trip adds a fixed delay at the start and padding at the end; looping the whole buffer would make a
250
+ * continuous ringtone stumble at every restart. `null` for the built-in tone, or when the backend gave none.
251
+ */
252
+ readonly loop: {
253
+ readonly start: number;
254
+ readonly end: number;
255
+ } | null;
256
+ }
257
+ /** The catalogue and the three levels of choice, as the backend resolves them for the caller. */
258
+ interface RingtoneSettings {
259
+ /** In display order. Always contains the built-in tone. */
260
+ readonly catalogue: readonly RingtoneOption[];
261
+ /** The org's default, or `null` when the org has not set one. */
262
+ readonly orgDefault: string | null;
263
+ /** This user's own choice, or `null` = "use the org default". */
264
+ readonly mine: string | null;
265
+ /** What actually rings: `mine`, else `orgDefault`, else the built-in tone — always in `catalogue`. */
266
+ readonly effective: string;
267
+ }
268
+ /** The ringtone settings of the user the softphone is signed in as, in its organization. */
269
+ interface RingtoneSource {
270
+ get(signal?: AbortSignal): Promise<RingtoneSettings>;
271
+ /** Saves this user's choice; `null` clears it (back to the org default). Resolves to the new settings. */
272
+ setMine(id: string | null): Promise<RingtoneSettings>;
273
+ /**
274
+ * The settings this user last had on this device, if remembered — instant, no network. Used only to start
275
+ * loading the likely ringtone early; `get()` is always the truth. Optional: a source may not remember.
276
+ */
277
+ cached?(): Promise<RingtoneSettings | null>;
278
+ }
236
279
 
237
- /** Contacts, history, caller ID, the agents directory, transfer outcomes and presence, all from one
238
- * config. */
280
+ /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
281
+ * settings, all from one config. */
239
282
  interface BackendSources {
240
283
  readonly contacts: ContactsSource;
241
284
  readonly history: HistorySource;
@@ -248,6 +291,8 @@ interface BackendSources {
248
291
  }) => Promise<TransferOutcomesPage>;
249
292
  /** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */
250
293
  readonly presence: PresenceSource;
294
+ /** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
295
+ readonly ringtones: RingtoneSource;
251
296
  }
252
297
 
253
298
  /**
@@ -690,12 +735,20 @@ interface AudioDevices {
690
735
  readonly inputs: readonly MediaDeviceInfo[];
691
736
  readonly outputs: readonly MediaDeviceInfo[];
692
737
  /**
693
- * The currently-selected input/output, or `null` — no explicit choice made, the browser's own
694
- * default is in use. Never a stale ID: if the selected device is unplugged, its selection is
695
- * cleared back to `null` rather than pointing at a device that no longer exists.
738
+ * The currently-selected input/output/ringtone output, or `null` — no explicit choice made, the
739
+ * browser's own default is in use. Never a stale ID: if the selected device is unplugged, its
740
+ * selection reads `null` rather than pointing at a device that no longer exists.
741
+ *
742
+ * **Choices are remembered** — in `localStorage` (`coventia-softphone:devices`), so they survive a
743
+ * reload. What is remembered is the user's *preference*, kept apart from these current values: an
744
+ * unplugged headset reads `null` here, and reads as selected again the moment it is plugged back in,
745
+ * without the user choosing it twice. Device ids are per browser and per site, which is exactly the
746
+ * scope of that storage (custom-ringtone.md §4.5). Where storage is blocked (private mode, a
747
+ * sandboxed iframe) choices simply last for the page, as they used to.
696
748
  */
697
749
  readonly currentInputId: string | null;
698
750
  readonly currentOutputId: string | null;
751
+ readonly currentRingtoneOutputId: string | null;
699
752
  /**
700
753
  * Re-enumerate. `inputs`/`outputs` already stay current on their own — a device physically
701
754
  * plugged or unplugged fires the browser's own `devicechange` and needs no help — but a
@@ -720,17 +773,23 @@ interface AudioDevices {
720
773
  */
721
774
  setOutput(deviceId: string | null): Promise<void>;
722
775
  /**
723
- * The RINGTONE output, separate from the call speaker.
776
+ * The RINGTONE output, separate from the call speaker, or `null` for the browser's default device.
724
777
  *
725
778
  * Of every SDK surveyed only Twilio ships this, and users want it: ring on the laptop speakers so
726
- * you hear it across the room, take the call in the headset.
779
+ * you hear it across the room, take the call in the headset. Only the inbound ring (and its preview
780
+ * in Ajustes) follows it; the call itself, the ringback and the short cues stay where they were.
727
781
  *
728
- * **Not yet implemented** — still Phase 2 groundwork, like `NoopAudioDevices` before it. Calling it
729
- * resolves without effect until a later pass wires it up.
782
+ * Like `setOutput`, this only records the choice and fires `SoftphoneEvent.DeviceChange` — the `/ui`
783
+ * layer is what routes the ring. A no-op for routing when `canSelectRingtoneOutput` is false.
730
784
  */
731
785
  setRingtoneOutput(deviceId: string | null): Promise<void>;
732
786
  /** False when the browser lacks `setSinkId` (Safari, most of Firefox) — `setOutput` is then a no-op. */
733
787
  readonly canSelectOutput: boolean;
788
+ /**
789
+ * Whether the ring can be sent to a chosen device. False on Safari (no `setSinkId` at all) — the
790
+ * ring then plays on the default output and a UI should not offer the picker.
791
+ */
792
+ readonly canSelectRingtoneOutput: boolean;
734
793
  }
735
794
  /**
736
795
  * The engine. Long-lived; owns registration, credentials, devices, and the provider.