@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 +82 -0
- package/dist/cjs/client/components/call-button.js +38 -4
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/resources/calls.js +31 -2
- package/dist/esm/client/components/call-button.js +38 -4
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/resources/calls.js +31 -2
- package/dist/types/client/components/call-button.d.ts +6 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/calls.d.ts +26 -1
- package/dist/types/types.d.ts +80 -2
- package/package.json +1 -1
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
|
|
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),
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
|
64
|
-
*
|
|
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
|
|
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),
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
|
61
|
-
*
|
|
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 {
|
|
@@ -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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|