@cosmoops/connect-sdk 0.42.0 → 0.44.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/CHANGELOG.md CHANGED
@@ -3,8 +3,90 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.44.0
7
+
8
+ ### `ConnectCall.dialledAt`: was the person's phone rung?
9
+
10
+ `unansweredReason: "cancelled"` covers two calls that need opposite handling:
11
+ your user hanging up while the person's phone rang, and your user giving up
12
+ before anybody was rung. The first left a missed call on the person's phone;
13
+ the second never happened. Nothing on the call told them apart, so an app
14
+ reading a call back after hang-up had to skip both and wait for the webhook.
15
+
16
+ - **`dialledAt`** on `ConnectCall` (from `calls.get()` and `calls.list()`) is
17
+ when Connect asked the carrier to ring the person, on a `member` call. Set
18
+ means a cancelled call was a missed one - file it as missed, exactly as
19
+ `call.missed` would. `null` means nobody was rung - file nothing. Connect's
20
+ webhooks already follow the same rule.
21
+ - It is on the record before any hang-up can be, so it never needs a retry.
22
+ `null` on agent calls and on member calls placed before this release.
23
+
24
+ Nothing to wire if you file calls from `call.completed` / `call.missed` only.
25
+
26
+ ### `calls.list({ placedByApplication: true })`: the calls your UI placed
27
+
28
+ A call placed with `<ConnectCallButton>` leaves from the workspace's own
29
+ number, which is assigned to no application - so `calls.list()` never
30
+ returned it, and an app whose webhook was delivered to another deployment had
31
+ no way to find out the call happened. `placedByApplication: true` (with an
32
+ optional `workspaceId`) lists exactly those calls, newest first: reconcile a
33
+ call log from it.
34
+
35
+ ### Call recording, from inside your app
36
+
37
+ `connect.calls.getCallSettings({ asUser })` and
38
+ `connect.calls.setCallRecording({ recordingEnabled, asUser })` read and switch
39
+ the workspace's call recording - the same switch as Connect's Calling screen.
40
+ `canManage` is the member's own `channel:manage`; draw the switch disabled
41
+ when it is false, because a save is refused `403`. The change applies to calls
42
+ started afterwards and is audited against the member. Needs `calls:place`,
43
+ which a key placing calls already holds.
44
+
45
+ ### The "Call ended" card shows the minutes left
46
+
47
+ After a call, `<ConnectCallProvider>` re-reads the calling balance and the
48
+ card now says how many minutes are left once that read lands, staying a few
49
+ seconds longer to show it. Nothing to wire.
50
+
51
+ ## 0.43.0
52
+
53
+ ### Recorded calls: transcript, summary, intent and action items
54
+
55
+ A workspace can now switch on **call recording** (Connect > Calling). Calls
56
+ placed with `<ConnectCallButton>` are then recorded, the person called hears
57
+ "This call may be recorded" as they answer, and a few minutes after the call
58
+ Connect has transcribed it and read it.
59
+
60
+ - **`call.analyzed`** - a new webhook event, sent after `call.completed` for a
61
+ recorded call. It carries `summary`, `intent` (a few words, e.g.
62
+ `"pricing enquiry"`), `sentiment`, `language`, `actionItems` (`title`,
63
+ `dueDate` as `YYYY-MM-DD` or `null`, and `owner`: `rep` or `customer`) and
64
+ `recordingFileId`. Subscribe to it on your webhook in Connect. Typed as
65
+ `CallAnalyzedEvent`.
66
+ - **`calls.get(callId)`** returns the same reading as `call.analysis`, and
67
+ the transcript as the call's `turns` - your user as `agent`, the person
68
+ called as `user`. `analysis` is `null` for a call that was not recorded.
69
+ - **The recording** is a Drive file: `connect.drive.getFileUrl(recordingFileId, { workspaceId })` with a
70
+ `drive:read` key plays it.
71
+
72
+ Recording is the workspace's choice, off by default, and adds ₹0.30 + GST per
73
+ minute to the call - nothing to wire on your side to offer it.
74
+
6
75
  ## 0.42.0
7
76
 
77
+ ### `connect.whatsapp` - your own approved templates, to a workspace's customers
78
+
79
+ - **`whatsapp.send({ workspaceId, template, to, values, consent })`** sends one
80
+ of your application's Meta-approved templates - named by slug, resolved by
81
+ Connect as `app_<yourApplicationKey>_<slug>` - through Connect's WhatsApp
82
+ sender. Needs the `whatsapp:send` scope.
83
+ - **`whatsapp.template(slug)`** answers `live` and the `variables` a send must
84
+ carry. Offer WhatsApp on a screen only when `live` is true.
85
+ - `consent.collectedVia` records an opt-in your application collected, only
86
+ when Connect has none for the number. A number that opted out stays opted
87
+ out: the send fails with `opted_out`.
88
+
89
+
8
90
  ### Ask AI explains failures instead of printing them
9
91
 
10
92
  Nothing to wire - update and the panel behaves better on a bad day.
@@ -57,10 +57,15 @@ function useConnectCall() {
57
57
  }
58
58
  /** How long the SDK may take to sign in before the attempt is abandoned. */
59
59
  const LOGIN_TIMEOUT_MS = 15_000;
60
- /** How long the "Call ended" card stays before the bar goes away. */
60
+ /** How long the "Call ended" card stays once it is showing the minutes left. */
61
61
  const ENDED_CARD_MS = 4_000;
62
62
  /** How long after a call to re-read the balance - the carrier's report settles it. */
63
63
  const AVAILABILITY_SETTLE_MS = 3_000;
64
+ /**
65
+ * The longest the card waits for that re-read before going anyway - a read
66
+ * that failed must not leave a finished call on screen.
67
+ */
68
+ const ENDED_CARD_MAX_MS = AVAILABILITY_SETTLE_MS + 6_000;
64
69
  function messageFromCause(cause) {
65
70
  if (typeof cause === "string" && cause.trim())
66
71
  return cause.trim();
@@ -86,6 +91,8 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
86
91
  const targetRef = (0, react_1.useRef)(null);
87
92
  const callIdRef = (0, react_1.useRef)(null);
88
93
  const endedRef = (0, react_1.useRef)(false);
94
+ /** Set when a call ends; the next balance read is the one the card shows. */
95
+ const awaitingBalanceRef = (0, react_1.useRef)(false);
89
96
  const dismissTimer = (0, react_1.useRef)(null);
90
97
  // Read inside SDK event handlers wired once, so they always see the latest.
91
98
  const onCallEndedRef = (0, react_1.useRef)(onCallEnded);
@@ -118,15 +125,32 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
118
125
  : current);
119
126
  if (dismissTimer.current)
120
127
  clearTimeout(dismissTimer.current);
121
- dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
128
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MAX_MS);
122
129
  // The charge lands a moment after the line drops, so the minutes shown
123
- // are re-read once it has had time to.
130
+ // are re-read once it has had time to - and the card waits for that read
131
+ // (see the effect below) rather than showing what was left before the call.
132
+ awaitingBalanceRef.current = true;
124
133
  setTimeout(() => refreshRef.current(), AVAILABILITY_SETTLE_MS);
125
134
  if (target) {
126
135
  target.onEnded?.({ callId });
127
136
  onCallEndedRef.current?.({ callId, phone: target.phone });
128
137
  }
129
138
  }, []);
139
+ /*
140
+ * The re-read after a call has landed: put what is left on the "Call ended"
141
+ * card, and give it its own few seconds from here, so the number is on
142
+ * screen long enough to read.
143
+ */
144
+ (0, react_1.useEffect)(() => {
145
+ if (!awaitingBalanceRef.current || !availability)
146
+ return;
147
+ awaitingBalanceRef.current = false;
148
+ const minutesLeft = availability.availableMinutes;
149
+ setCall((current) => (current?.phase === "ended" ? { ...current, minutesLeft } : current));
150
+ if (dismissTimer.current)
151
+ clearTimeout(dismissTimer.current);
152
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
153
+ }, [availability]);
130
154
  const wire = (0, react_1.useCallback)((client) => {
131
155
  const ringing = () => setCall((current) => (current ? { ...current, phase: "ringing" } : current));
132
156
  client.on("onCalling", ringing);
@@ -200,6 +224,7 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
200
224
  if (dismissTimer.current)
201
225
  clearTimeout(dismissTimer.current);
202
226
  targetRef.current = target;
227
+ awaitingBalanceRef.current = false;
203
228
  callIdRef.current = null;
204
229
  placedRef.current = false;
205
230
  endedRef.current = false;
@@ -212,6 +237,7 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
212
237
  endedAfterSeconds: null,
213
238
  muted: false,
214
239
  notice: null,
240
+ minutesLeft: null,
215
241
  });
216
242
  try {
217
243
  const client = await signIn();
@@ -383,7 +409,15 @@ function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className, }) {
383
409
  : call.phase === "ended" && call.endedAfterSeconds !== null
384
410
  ? `Call ended · ${formatSeconds(call.endedAfterSeconds)}`
385
411
  : PHASE_LABEL[call.phase];
386
- return ((0, jsx_runtime_1.jsx)("div", { role: "status", "aria-live": "polite", className: (0, cn_js_1.cn)("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-medium", children: call.label }), (0, jsx_runtime_1.jsxs)("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] })] }), live && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? (0, jsx_runtime_1.jsx)(lucide_react_1.MicOff, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Mic, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOff, { className: "size-4" }) })] }))] }) }));
412
+ const remaining = call.phase === "ended" ? minutesLeftLabel(call.minutesLeft) : null;
413
+ return ((0, jsx_runtime_1.jsx)("div", { role: "status", "aria-live": "polite", className: (0, cn_js_1.cn)("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-medium", children: call.label }), (0, jsx_runtime_1.jsxs)("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] }), remaining && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted tabular-nums", children: remaining })] }), live && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? (0, jsx_runtime_1.jsx)(lucide_react_1.MicOff, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Mic, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOff, { className: "size-4" }) })] }))] }) }));
414
+ }
415
+ /** "42 min of calling left", once the post-call balance read has landed. */
416
+ function minutesLeftLabel(minutes) {
417
+ if (minutes === null)
418
+ return null;
419
+ const whole = Math.max(0, Math.floor(minutes));
420
+ return whole === 0 ? "No calling minutes left" : `${whole} min of calling left`;
387
421
  }
388
422
  /** Seconds since `since`, ticking once a second. `0` while there is nothing to time. */
389
423
  function useElapsedSeconds(since) {
@@ -183,4 +183,4 @@ class ConnectClient {
183
183
  }
184
184
  exports.ConnectClient = ConnectClient;
185
185
  /** Kept in step with `package.json` by `sdk.test.ts`. */
186
- exports.SDK_VERSION = "0.42.0";
186
+ exports.SDK_VERSION = "0.44.0";
@@ -31,6 +31,8 @@ class CallsResource {
31
31
  path: "/calls",
32
32
  query: {
33
33
  channelId: input.channelId,
34
+ placedByApplication: input.placedByApplication ? "true" : undefined,
35
+ workspaceId: input.workspaceId,
34
36
  cursor: input.cursor,
35
37
  limit: input.limit === undefined ? undefined : String(input.limit),
36
38
  },
@@ -60,8 +62,11 @@ class CallsResource {
60
62
  * All three need the `calls:place` scope **and** a signed-in member
61
63
  * (`asUser`), whose own "Make calls" permission Connect checks. None works
62
64
  * server-to-server: a call is placed by a person, from the workspace's own
63
- * number, and is not recorded. Read how one went with `get(callId)`, which
64
- * needs `calls:read` - a call your application placed is readable by it.
65
+ * number. It is recorded only when the workspace has switched recording on
66
+ * (the person called hears a notice); `call.analyzed` then follows with the
67
+ * summary, intent and action items. Read how one went with `get(callId)`,
68
+ * which needs `calls:read` - a call your application placed is readable by
69
+ * it, transcript included.
65
70
  */
66
71
  /**
67
72
  * Whether the member can call, and if not why - what `<ConnectCallButton>`
@@ -75,6 +80,30 @@ class CallsResource {
75
80
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
76
81
  });
77
82
  }
83
+ /**
84
+ * Whether the workspace records its member calls, and whether this member
85
+ * may change that. The same switch as Connect's Calling screen.
86
+ */
87
+ getCallSettings(input) {
88
+ return (0, http_js_1.request)(this.transport, {
89
+ method: "GET",
90
+ path: "/calls/settings",
91
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
92
+ });
93
+ }
94
+ /**
95
+ * Switches recording on or off for calls started from now. Throws
96
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
97
+ * audited against them. Answers the settings as they now stand.
98
+ */
99
+ setCallRecording(input) {
100
+ return (0, http_js_1.request)(this.transport, {
101
+ method: "PUT",
102
+ path: "/calls/settings",
103
+ json: { recordingEnabled: input.recordingEnabled },
104
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
105
+ });
106
+ }
78
107
  /** A carrier token the member's browser signs in to the phone network with. */
79
108
  getBrowserCallingToken(input) {
80
109
  return (0, http_js_1.request)(this.transport, {
@@ -17,10 +17,15 @@ export function useConnectCall() {
17
17
  }
18
18
  /** How long the SDK may take to sign in before the attempt is abandoned. */
19
19
  const LOGIN_TIMEOUT_MS = 15_000;
20
- /** How long the "Call ended" card stays before the bar goes away. */
20
+ /** How long the "Call ended" card stays once it is showing the minutes left. */
21
21
  const ENDED_CARD_MS = 4_000;
22
22
  /** How long after a call to re-read the balance - the carrier's report settles it. */
23
23
  const AVAILABILITY_SETTLE_MS = 3_000;
24
+ /**
25
+ * The longest the card waits for that re-read before going anyway - a read
26
+ * that failed must not leave a finished call on screen.
27
+ */
28
+ const ENDED_CARD_MAX_MS = AVAILABILITY_SETTLE_MS + 6_000;
24
29
  function messageFromCause(cause) {
25
30
  if (typeof cause === "string" && cause.trim())
26
31
  return cause.trim();
@@ -46,6 +51,8 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
46
51
  const targetRef = useRef(null);
47
52
  const callIdRef = useRef(null);
48
53
  const endedRef = useRef(false);
54
+ /** Set when a call ends; the next balance read is the one the card shows. */
55
+ const awaitingBalanceRef = useRef(false);
49
56
  const dismissTimer = useRef(null);
50
57
  // Read inside SDK event handlers wired once, so they always see the latest.
51
58
  const onCallEndedRef = useRef(onCallEnded);
@@ -78,15 +85,32 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
78
85
  : current);
79
86
  if (dismissTimer.current)
80
87
  clearTimeout(dismissTimer.current);
81
- dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
88
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MAX_MS);
82
89
  // The charge lands a moment after the line drops, so the minutes shown
83
- // are re-read once it has had time to.
90
+ // are re-read once it has had time to - and the card waits for that read
91
+ // (see the effect below) rather than showing what was left before the call.
92
+ awaitingBalanceRef.current = true;
84
93
  setTimeout(() => refreshRef.current(), AVAILABILITY_SETTLE_MS);
85
94
  if (target) {
86
95
  target.onEnded?.({ callId });
87
96
  onCallEndedRef.current?.({ callId, phone: target.phone });
88
97
  }
89
98
  }, []);
99
+ /*
100
+ * The re-read after a call has landed: put what is left on the "Call ended"
101
+ * card, and give it its own few seconds from here, so the number is on
102
+ * screen long enough to read.
103
+ */
104
+ useEffect(() => {
105
+ if (!awaitingBalanceRef.current || !availability)
106
+ return;
107
+ awaitingBalanceRef.current = false;
108
+ const minutesLeft = availability.availableMinutes;
109
+ setCall((current) => (current?.phase === "ended" ? { ...current, minutesLeft } : current));
110
+ if (dismissTimer.current)
111
+ clearTimeout(dismissTimer.current);
112
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
113
+ }, [availability]);
90
114
  const wire = useCallback((client) => {
91
115
  const ringing = () => setCall((current) => (current ? { ...current, phase: "ringing" } : current));
92
116
  client.on("onCalling", ringing);
@@ -160,6 +184,7 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
160
184
  if (dismissTimer.current)
161
185
  clearTimeout(dismissTimer.current);
162
186
  targetRef.current = target;
187
+ awaitingBalanceRef.current = false;
163
188
  callIdRef.current = null;
164
189
  placedRef.current = false;
165
190
  endedRef.current = false;
@@ -172,6 +197,7 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
172
197
  endedAfterSeconds: null,
173
198
  muted: false,
174
199
  notice: null,
200
+ minutesLeft: null,
175
201
  });
176
202
  try {
177
203
  const client = await signIn();
@@ -343,7 +369,15 @@ export function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className,
343
369
  : call.phase === "ended" && call.endedAfterSeconds !== null
344
370
  ? `Call ended · ${formatSeconds(call.endedAfterSeconds)}`
345
371
  : PHASE_LABEL[call.phase];
346
- return (_jsx("div", { role: "status", "aria-live": "polite", className: cn("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: _jsxs("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: _jsx(PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: call.label }), _jsxs("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] })] }), live && (_jsxs(_Fragment, { children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? _jsx(MicOff, { className: "size-4" }) : _jsx(Mic, { className: "size-4" }) }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: _jsx(PhoneOff, { className: "size-4" }) })] }))] }) }));
372
+ const remaining = call.phase === "ended" ? minutesLeftLabel(call.minutesLeft) : null;
373
+ return (_jsx("div", { role: "status", "aria-live": "polite", className: cn("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: _jsxs("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: _jsx(PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: call.label }), _jsxs("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] }), remaining && _jsx("p", { className: "truncate text-xs text-muted tabular-nums", children: remaining })] }), live && (_jsxs(_Fragment, { children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? _jsx(MicOff, { className: "size-4" }) : _jsx(Mic, { className: "size-4" }) }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: _jsx(PhoneOff, { className: "size-4" }) })] }))] }) }));
374
+ }
375
+ /** "42 min of calling left", once the post-call balance read has landed. */
376
+ function minutesLeftLabel(minutes) {
377
+ if (minutes === null)
378
+ return null;
379
+ const whole = Math.max(0, Math.floor(minutes));
380
+ return whole === 0 ? "No calling minutes left" : `${whole} min of calling left`;
347
381
  }
348
382
  /** Seconds since `since`, ticking once a second. `0` while there is nothing to time. */
349
383
  function useElapsedSeconds(since) {
@@ -179,4 +179,4 @@ export class ConnectClient {
179
179
  }
180
180
  }
181
181
  /** Kept in step with `package.json` by `sdk.test.ts`. */
182
- export const SDK_VERSION = "0.42.0";
182
+ export const SDK_VERSION = "0.44.0";
@@ -28,6 +28,8 @@ export class CallsResource {
28
28
  path: "/calls",
29
29
  query: {
30
30
  channelId: input.channelId,
31
+ placedByApplication: input.placedByApplication ? "true" : undefined,
32
+ workspaceId: input.workspaceId,
31
33
  cursor: input.cursor,
32
34
  limit: input.limit === undefined ? undefined : String(input.limit),
33
35
  },
@@ -57,8 +59,11 @@ export class CallsResource {
57
59
  * All three need the `calls:place` scope **and** a signed-in member
58
60
  * (`asUser`), whose own "Make calls" permission Connect checks. None works
59
61
  * server-to-server: a call is placed by a person, from the workspace's own
60
- * number, and is not recorded. Read how one went with `get(callId)`, which
61
- * needs `calls:read` - a call your application placed is readable by it.
62
+ * number. It is recorded only when the workspace has switched recording on
63
+ * (the person called hears a notice); `call.analyzed` then follows with the
64
+ * summary, intent and action items. Read how one went with `get(callId)`,
65
+ * which needs `calls:read` - a call your application placed is readable by
66
+ * it, transcript included.
62
67
  */
63
68
  /**
64
69
  * Whether the member can call, and if not why - what `<ConnectCallButton>`
@@ -72,6 +77,30 @@ export class CallsResource {
72
77
  headers: actingAs(input.asUser),
73
78
  });
74
79
  }
80
+ /**
81
+ * Whether the workspace records its member calls, and whether this member
82
+ * may change that. The same switch as Connect's Calling screen.
83
+ */
84
+ getCallSettings(input) {
85
+ return request(this.transport, {
86
+ method: "GET",
87
+ path: "/calls/settings",
88
+ headers: actingAs(input.asUser),
89
+ });
90
+ }
91
+ /**
92
+ * Switches recording on or off for calls started from now. Throws
93
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
94
+ * audited against them. Answers the settings as they now stand.
95
+ */
96
+ setCallRecording(input) {
97
+ return request(this.transport, {
98
+ method: "PUT",
99
+ path: "/calls/settings",
100
+ json: { recordingEnabled: input.recordingEnabled },
101
+ headers: actingAs(input.asUser),
102
+ });
103
+ }
75
104
  /** A carrier token the member's browser signs in to the phone network with. */
76
105
  getBrowserCallingToken(input) {
77
106
  return request(this.transport, {
@@ -45,6 +45,12 @@ interface ActiveCall {
45
45
  endedAfterSeconds: number | null;
46
46
  muted: boolean;
47
47
  notice: string | null;
48
+ /**
49
+ * Calling minutes left once this call has been charged - filled on the
50
+ * "Call ended" card by the balance re-read that follows it, and `null`
51
+ * until that read lands, so the card never shows the pre-call figure.
52
+ */
53
+ minutesLeft: number | null;
48
54
  }
49
55
  /** The number to ring, and how to name the person on the in-call bar. */
50
56
  export interface ConnectCallTarget {
@@ -141,4 +141,4 @@ export declare class ConnectClient {
141
141
  constructor(options: ConnectClientOptions);
142
142
  }
143
143
  /** Kept in step with `package.json` by `sdk.test.ts`. */
144
- export declare const SDK_VERSION = "0.42.0";
144
+ export declare const SDK_VERSION = "0.44.0";
@@ -1,8 +1,17 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { BrowserCallingToken, CallingAvailability, CallDetail, CallsPage, StartedBrowserCall } from "../../types.js";
2
+ import type { BrowserCallingToken, CallingAvailability, CallSettings, CallDetail, CallsPage, StartedBrowserCall } from "../../types.js";
3
3
  export interface ListCallsInput {
4
4
  /** Narrow to one channel. A channel belonging to another application simply matches nothing. */
5
5
  channelId?: string;
6
+ /**
7
+ * `true` for the member calls your UI placed (`<ConnectCallButton>`) instead
8
+ * of the calls on your channels. Those leave from the workspace's own
9
+ * number, so the default list never contains them - this is the list to
10
+ * reconcile a timeline from when a webhook went to another deployment.
11
+ */
12
+ placedByApplication?: boolean;
13
+ /** With `placedByApplication`, one workspace's calls. */
14
+ workspaceId?: string;
6
15
  /** From a previous page's `nextCursor`. */
7
16
  cursor?: string;
8
17
  /** 1-100, default 50. */
@@ -50,6 +59,22 @@ export declare class CallsResource {
50
59
  getCallingAvailability(input: {
51
60
  asUser: string;
52
61
  }): Promise<CallingAvailability>;
62
+ /**
63
+ * Whether the workspace records its member calls, and whether this member
64
+ * may change that. The same switch as Connect's Calling screen.
65
+ */
66
+ getCallSettings(input: {
67
+ asUser: string;
68
+ }): Promise<CallSettings>;
69
+ /**
70
+ * Switches recording on or off for calls started from now. Throws
71
+ * `ConnectError` 403 when the member lacks `channel:manage`; the change is
72
+ * audited against them. Answers the settings as they now stand.
73
+ */
74
+ setCallRecording(input: {
75
+ recordingEnabled: boolean;
76
+ asUser: string;
77
+ }): Promise<CallSettings>;
53
78
  /** A carrier token the member's browser signs in to the phone network with. */
54
79
  getBrowserCallingToken(input: {
55
80
  asUser: string;
@@ -1114,7 +1114,7 @@ export interface EmitEventsResponse {
1114
1114
  * `createConnectWebhookHandler` simply ignores anything you have not
1115
1115
  * registered a handler for.
1116
1116
  */
1117
- export type ConnectWebhookEventType = "workspace.deletion_scheduled" | "workspace.deletion_cancelled" | "workspace.deleted" | "subscription.cancelled" | "call.completed" | "call.missed" | "booking.confirmed" | "booking.cancelled" | "booking.rescheduled" | "contact.created" | "contact.updated";
1117
+ export type ConnectWebhookEventType = "workspace.deletion_scheduled" | "workspace.deletion_cancelled" | "workspace.deleted" | "subscription.cancelled" | "call.completed" | "call.missed" | "call.analyzed" | "booking.confirmed" | "booking.cancelled" | "booking.rescheduled" | "contact.created" | "contact.updated";
1118
1118
  /** What becomes of one class of data when the workspace is purged. */
1119
1119
  export type ConnectDataDisposition = "erase" | "anonymize" | "retain";
1120
1120
  /** Who answers for your copy of the customer's data. See the deletion-policy guide. */
@@ -1321,6 +1321,48 @@ export interface CallMissedEvent {
1321
1321
  /** ISO-8601. */
1322
1322
  occurredAt: string;
1323
1323
  }
1324
+ /** How the person called came across, by the end of the call. */
1325
+ export type CallSentiment = "positive" | "neutral" | "negative";
1326
+ /**
1327
+ * Something somebody committed to on a recorded call. `owner` is who said
1328
+ * they would do it - `rep` for your user's side, `customer` for the person
1329
+ * called. Both are worth a follow-up: one to do, one to chase.
1330
+ */
1331
+ export interface CallActionItem {
1332
+ title: string;
1333
+ /** `YYYY-MM-DD` when a day was named or clearly implied, else `null`. */
1334
+ dueDate: string | null;
1335
+ owner: "rep" | "customer";
1336
+ }
1337
+ /**
1338
+ * A recorded call, transcribed and read. Sent minutes after `call.completed`,
1339
+ * and only for calls placed while the workspace records them - file the call
1340
+ * on `call.completed` and enrich that row here. The transcript itself is the
1341
+ * call's turns on `calls.get`.
1342
+ */
1343
+ export interface CallAnalyzedEvent {
1344
+ workspaceId: string;
1345
+ callId: string;
1346
+ channelId: string | null;
1347
+ /** E.164 - the person called on an outbound call, the caller on an inbound one. */
1348
+ contactPhone: string | null;
1349
+ direction: ConnectCallDirection;
1350
+ handler: CallHandler;
1351
+ placedByUserId: string | null;
1352
+ placedByName: string | null;
1353
+ /** Two or three sentences, in English whatever language was spoken. */
1354
+ summary: string | null;
1355
+ /** Why they were on the call, in a few lowercase words - "pricing enquiry". */
1356
+ intent: string | null;
1357
+ sentiment: CallSentiment | null;
1358
+ /** The main language spoken, BCP-47 - `hi-IN`, `en-IN`. */
1359
+ language: string | null;
1360
+ actionItems: CallActionItem[];
1361
+ /** The audio in the workspace's Drive - read it with a `drive:read` key. */
1362
+ recordingFileId: string | null;
1363
+ /** ISO-8601 - when the call started. */
1364
+ occurredAt: string;
1365
+ }
1324
1366
  /**
1325
1367
  * An appointment was booked and is holding its slot.
1326
1368
  *
@@ -1446,6 +1488,7 @@ export interface ConnectWebhookEventMap {
1446
1488
  "subscription.cancelled": SubscriptionCancelledEvent;
1447
1489
  "call.completed": CallCompletedEvent;
1448
1490
  "call.missed": CallMissedEvent;
1491
+ "call.analyzed": CallAnalyzedEvent;
1449
1492
  "booking.confirmed": BookingConfirmedEvent;
1450
1493
  "booking.cancelled": BookingCancelledEvent;
1451
1494
  "booking.rescheduled": BookingRescheduledEvent;
@@ -2804,7 +2847,7 @@ export interface ConnectCall {
2804
2847
  extraction: Record<string, unknown> | null;
2805
2848
  agentId: string | null;
2806
2849
  agentVersion: number | null;
2807
- /** `member` for a call a person placed with `<ConnectCallButton>`; such a call has no agent, transcript or recording. */
2850
+ /** `member` for a call a person placed with `<ConnectCallButton>`. Such a call has no agent; it has a recording, a transcript and an `analysis` only when the workspace records calls. */
2808
2851
  handler: CallHandler;
2809
2852
  /** The member who placed a `member` call - their Connect user id. */
2810
2853
  placedByUserId: string | null;
@@ -2815,8 +2858,31 @@ export interface ConnectCall {
2815
2858
  * nobody answered.
2816
2859
  */
2817
2860
  talkSeconds: number | null;
2861
+ /**
2862
+ * When Connect asked the carrier to ring the person called, on a `member`
2863
+ * call. Tells apart the two calls `unansweredReason: "cancelled"` covers:
2864
+ * set when the member hung up while it rang (the person sees a missed call -
2865
+ * Connect sends `call.missed`), `null` when they gave up before anyone was
2866
+ * rung (no call happened, and no event is sent).
2867
+ */
2868
+ dialledAt: string | null;
2818
2869
  /** Why nobody answered - set only when `status` is `unanswered`. */
2819
2870
  unansweredReason: CallUnansweredReason | (string & {}) | null;
2871
+ /**
2872
+ * The reading of a recorded member call - summary, intent, action items.
2873
+ * `null` for a call that was not recorded; `status: "pending"` while it is
2874
+ * being read. The transcript is `CallDetail.turns`, the member as `agent`
2875
+ * and the person called as `user`.
2876
+ */
2877
+ analysis: ConnectCallAnalysis | null;
2878
+ }
2879
+ export interface ConnectCallAnalysis {
2880
+ status: "pending" | "done" | "failed";
2881
+ summary: string | null;
2882
+ intent: string | null;
2883
+ sentiment: CallSentiment | null;
2884
+ actionItems: CallActionItem[];
2885
+ language: string | null;
2820
2886
  }
2821
2887
  /**
2822
2888
  * What the browser needs to place one approved call - see
@@ -2844,6 +2910,18 @@ export interface StartedBrowserCall {
2844
2910
  * - `available`.
2845
2911
  */
2846
2912
  export type CallingAvailabilityStatus = "available" | "no_permission" | "no_number" | "number_pending" | "no_balance";
2913
+ /**
2914
+ * The workspace's calling settings, as the signed-in member sees them -
2915
+ * `connect.calls.getCallSettings()`.
2916
+ */
2917
+ export interface CallSettings {
2918
+ /** Whether member calls are recorded, transcribed and read (`call.analyzed`). */
2919
+ recordingEnabled: boolean;
2920
+ /** Paise, pre-tax, recording adds to every billed minute - show it beside the switch. */
2921
+ recordingRatePaisePerMinute: number;
2922
+ /** Whether this member may change it - their `channel:manage`. Draw the switch disabled when not. */
2923
+ canManage: boolean;
2924
+ }
2847
2925
  export interface CallingAvailability {
2848
2926
  status: CallingAvailabilityStatus;
2849
2927
  /** The workspace's business number, E.164, when it holds one. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",