@cosmoops/connect-sdk 0.40.0 → 0.41.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 +49 -0
- package/dist/cjs/client/components/call-button.js +398 -0
- package/dist/cjs/client/index.js +7 -2
- package/dist/cjs/client/portal-links.js +1 -1
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +73 -0
- package/dist/cjs/server/resources/calls.js +58 -0
- package/dist/esm/client/components/call-button.js +358 -0
- package/dist/esm/client/index.js +2 -1
- package/dist/esm/client/portal-links.js +1 -1
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +73 -0
- package/dist/esm/server/resources/calls.js +58 -0
- package/dist/types/client/components/call-button.d.ts +132 -0
- package/dist/types/client/index.d.ts +2 -0
- package/dist/types/client/portal-links.d.ts +1 -1
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/calls.d.ts +32 -1
- package/dist/types/types.d.ts +110 -1
- package/package.json +4 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CallingAvailability } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Calling from the browser, through the workspace's own number.
|
|
5
|
+
*
|
|
6
|
+
* Three pieces: `<ConnectCallProvider>` once, high in your tree;
|
|
7
|
+
* `<ConnectCallButton phone="+91...">` wherever a person can be rung; and
|
|
8
|
+
* `useConnectCall()` if you want your own button.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a provider, and why high up
|
|
11
|
+
*
|
|
12
|
+
* A call has to outlive the page it was started from - someone who rings a
|
|
13
|
+
* customer and then opens their invoices must not hang up on them by doing so.
|
|
14
|
+
* So the call lives in the provider, which also draws the in-call bar, and a
|
|
15
|
+
* button only asks it to start one. Mount it inside `<ConnectProvider>`,
|
|
16
|
+
* beside your app shell rather than inside a page.
|
|
17
|
+
*
|
|
18
|
+
* ## What happens when someone presses Call
|
|
19
|
+
*
|
|
20
|
+
* 1. The browser signs in to the phone network (`POST /calls/browser-token`
|
|
21
|
+
* on your bridge), once per session - Plivo's SDK keeps its registration.
|
|
22
|
+
* Plivo's SDK is loaded at this moment, not before, so a page that never
|
|
23
|
+
* places a call never downloads it.
|
|
24
|
+
* 2. Connect approves the call (`POST /calls/start`): the workspace must hold
|
|
25
|
+
* a number and the member must have "Make calls". What comes back lets this
|
|
26
|
+
* browser place exactly this call and no other.
|
|
27
|
+
* 3. The browser places it. Connect dials the number from the workspace's own
|
|
28
|
+
* number; nothing is recorded.
|
|
29
|
+
*
|
|
30
|
+
* ## Logging a call
|
|
31
|
+
*
|
|
32
|
+
* `onEnded` hands you the call id - deliberately not an outcome. A browser is
|
|
33
|
+
* not a witness your call log should trust, and the carrier's report reaches
|
|
34
|
+
* Connect a moment after the line drops anyway. Read the outcome on your
|
|
35
|
+
* **server** with `connect.calls.get(callId)` (`calls:read`): `status`,
|
|
36
|
+
* `talkSeconds`, `unansweredReason` and `placedByUserId` are what to log.
|
|
37
|
+
*/
|
|
38
|
+
type CallPhase = "connecting" | "ringing" | "in-call" | "ended";
|
|
39
|
+
interface ActiveCall {
|
|
40
|
+
phase: CallPhase;
|
|
41
|
+
label: string;
|
|
42
|
+
number: string;
|
|
43
|
+
callId: string | null;
|
|
44
|
+
connectedAt: number | null;
|
|
45
|
+
endedAfterSeconds: number | null;
|
|
46
|
+
muted: boolean;
|
|
47
|
+
notice: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The number to ring, and how to name the person on the in-call bar. */
|
|
50
|
+
export interface ConnectCallTarget {
|
|
51
|
+
/** E.164, e.g. `+919876543210`. */
|
|
52
|
+
phone: string;
|
|
53
|
+
/** Shown on the in-call bar. Defaults to the number. */
|
|
54
|
+
name?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Called once the call is over, with Connect's id for it - or `null` when it
|
|
57
|
+
* never got as far as being approved. Read the outcome on your server with
|
|
58
|
+
* `connect.calls.get(callId)`; see the module comment for why not here.
|
|
59
|
+
*/
|
|
60
|
+
onEnded?: (result: {
|
|
61
|
+
callId: string | null;
|
|
62
|
+
}) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface ConnectCallContextValue {
|
|
65
|
+
/** A call is in progress - every Call button disables on it. */
|
|
66
|
+
isBusy: boolean;
|
|
67
|
+
startCall(target: ConnectCallTarget): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Whether this member can call and, if not, why - `null` while it loads.
|
|
70
|
+
* Read once by the provider and shared, so every button and status panel
|
|
71
|
+
* agrees, and refreshed after each call so the minutes shown keep up.
|
|
72
|
+
*/
|
|
73
|
+
availability: CallingAvailability | null;
|
|
74
|
+
/** Re-read `availability` - after a top-up in another tab, say. */
|
|
75
|
+
refreshAvailability(): void;
|
|
76
|
+
}
|
|
77
|
+
/** Start a call yourself. Must be inside `<ConnectCallProvider>`. */
|
|
78
|
+
export declare function useConnectCall(): ConnectCallContextValue;
|
|
79
|
+
export interface ConnectCallProviderProps {
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
/**
|
|
82
|
+
* Called for every call that ends, whichever button started it - the place
|
|
83
|
+
* to wire "log this call" once for the whole app. A button's own `onEnded`
|
|
84
|
+
* runs as well.
|
|
85
|
+
*/
|
|
86
|
+
onCallEnded?: (result: {
|
|
87
|
+
callId: string | null;
|
|
88
|
+
phone: string;
|
|
89
|
+
}) => void;
|
|
90
|
+
/** Called when a call could not be placed, with the reason to show. Defaults to the in-call bar saying so. */
|
|
91
|
+
onError?: (message: string) => void;
|
|
92
|
+
/** Extra classes for the in-call bar's outer element - merged with `cn`. */
|
|
93
|
+
className?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Holds the call in progress and draws the in-call bar. See the module comment.
|
|
97
|
+
*/
|
|
98
|
+
export declare function ConnectCallProvider({ children, onCallEnded, onError, className, }: ConnectCallProviderProps): import("react").JSX.Element;
|
|
99
|
+
export interface ConnectCallButtonProps extends ConnectCallTarget {
|
|
100
|
+
/** `compact` is an icon-only button for a row beside a number. */
|
|
101
|
+
variant?: "default" | "compact";
|
|
102
|
+
/** Button label. Defaults to "Call". */
|
|
103
|
+
children?: ReactNode;
|
|
104
|
+
isDisabled?: boolean;
|
|
105
|
+
/** Merged with `cn`, so an override replaces the default it conflicts with. */
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Rings `phone` from the workspace's number. Needs `<ConnectCallProvider>`
|
|
110
|
+
* above it, and a member with "Make calls" - render it only for them, since a
|
|
111
|
+
* button the server refuses for everyone else helps nobody.
|
|
112
|
+
*/
|
|
113
|
+
export declare function ConnectCallButton({ phone, name, onEnded, variant, children, isDisabled, className, }: ConnectCallButtonProps): import("react").JSX.Element | null;
|
|
114
|
+
export interface ConnectCallingStatusProps {
|
|
115
|
+
/** Merged with `cn`. */
|
|
116
|
+
className?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The workspace's calling at a glance - its business number, the minutes
|
|
120
|
+
* left, and the next step - for a settings page. Reads the same availability
|
|
121
|
+
* the Call buttons do, so the two never disagree. Buying minutes and adding a
|
|
122
|
+
* number happen in Connect, and come back here when done.
|
|
123
|
+
*/
|
|
124
|
+
export declare function ConnectCallingStatus({ className }: ConnectCallingStatusProps): import("react").JSX.Element;
|
|
125
|
+
/** The floating in-call card. Exported for Storybook only - the provider draws it. */
|
|
126
|
+
export declare function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className, }: {
|
|
127
|
+
call: ActiveCall;
|
|
128
|
+
onHangUp: () => void;
|
|
129
|
+
onToggleMute: () => void;
|
|
130
|
+
className?: string;
|
|
131
|
+
}): import("react").JSX.Element;
|
|
132
|
+
export type { ActiveCall as ConnectActiveCall };
|
|
@@ -106,6 +106,8 @@ export { ConnectCreateWorkspaceModal } from "./components/create-workspace-form.
|
|
|
106
106
|
export type { ConnectCreateWorkspaceModalProps } from "./components/create-workspace-form.js";
|
|
107
107
|
export { useCreateWorkspace } from "./use-workspaces.js";
|
|
108
108
|
export type { WorkspaceActionsState } from "./use-workspaces.js";
|
|
109
|
+
export { ConnectCallProvider, ConnectCallButton, ConnectCallingStatus, useConnectCall, } from "./components/call-button.js";
|
|
110
|
+
export type { ConnectCallProviderProps, ConnectCallButtonProps, ConnectCallTarget, ConnectCallContextValue, ConnectCallingStatusProps, } from "./components/call-button.js";
|
|
109
111
|
export { ConnectBookingCalendar } from "./components/booking-calendar.js";
|
|
110
112
|
export type { ConnectBookingCalendarProps } from "./components/booking-calendar.js";
|
|
111
113
|
export { ConnectBookingDialog, ConnectBookingDetail } from "./components/booking-dialogs.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `connectPortalUrl(appUrl, clientId, "/calendar/settings")`, and so on.
|
|
5
5
|
*
|
|
6
6
|
* Connect answers any of these paths for a signed-in user on the bare URL -
|
|
7
|
-
* these two params only add the "Back to <your app>" banner
|
|
7
|
+
* these two params only add the `"Back to <your app>"` banner
|
|
8
8
|
* (`ApplicationReturnBar` on Connect's side). It validates `return_to`
|
|
9
9
|
* against this `client_id`'s registered redirect origins and silently drops
|
|
10
10
|
* the banner if they don't match, so a wrong or stale `appUrl`/`clientId`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TransportOptions } from "../http.js";
|
|
2
|
-
import type { CallDetail, CallsPage } from "../../types.js";
|
|
2
|
+
import type { BrowserCallingToken, CallingAvailability, 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;
|
|
@@ -42,4 +42,35 @@ export declare class CallsResource {
|
|
|
42
42
|
* exist *or* belongs to another application.
|
|
43
43
|
*/
|
|
44
44
|
get(callId: string): Promise<CallDetail>;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the member can call, and if not why - what `<ConnectCallButton>`
|
|
47
|
+
* renders from. Throws `ConnectError` 403 for a member without "Make
|
|
48
|
+
* calls"; the bridge turns that into `status: "no_permission"`.
|
|
49
|
+
*/
|
|
50
|
+
getCallingAvailability(input: {
|
|
51
|
+
asUser: string;
|
|
52
|
+
}): Promise<CallingAvailability>;
|
|
53
|
+
/** A carrier token the member's browser signs in to the phone network with. */
|
|
54
|
+
getBrowserCallingToken(input: {
|
|
55
|
+
asUser: string;
|
|
56
|
+
}): Promise<BrowserCallingToken>;
|
|
57
|
+
/**
|
|
58
|
+
* Approves one call from the member to `phone` (E.164) and records it.
|
|
59
|
+
*
|
|
60
|
+
* Throws `ConnectError` with status `412` when the workspace has no active
|
|
61
|
+
* number, is not allowed to place calls, or the member is already on one -
|
|
62
|
+
* the message is written to be shown to them.
|
|
63
|
+
*/
|
|
64
|
+
startBrowserCall(input: {
|
|
65
|
+
phone: string;
|
|
66
|
+
asUser: string;
|
|
67
|
+
}): Promise<StartedBrowserCall>;
|
|
68
|
+
/**
|
|
69
|
+
* Closes a call the browser never managed to place - recorded as
|
|
70
|
+
* `unanswered`/`cancelled`. A no-op once the carrier has it.
|
|
71
|
+
*/
|
|
72
|
+
cancelBrowserCall(input: {
|
|
73
|
+
callId: string;
|
|
74
|
+
asUser: string;
|
|
75
|
+
}): Promise<void>;
|
|
45
76
|
}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1269,12 +1269,29 @@ export interface CallCompletedEvent {
|
|
|
1269
1269
|
* for presence.
|
|
1270
1270
|
*/
|
|
1271
1271
|
extraction: Record<string, unknown>;
|
|
1272
|
+
/**
|
|
1273
|
+
* `member` when a person placed this call from a browser - `<ConnectCallButton>`
|
|
1274
|
+
* or Connect's own portal - rather than an agent handling it. Absent on
|
|
1275
|
+
* events sent before member calling existed; treat absent as `agent`.
|
|
1276
|
+
*/
|
|
1277
|
+
handler?: CallHandler;
|
|
1278
|
+
/**
|
|
1279
|
+
* The member who placed a `member` call - their Connect user id, the same id
|
|
1280
|
+
* a workspace roster carries. The one to attribute the call to. `null` on an
|
|
1281
|
+
* agent's call.
|
|
1282
|
+
*/
|
|
1283
|
+
placedByUserId?: string | null;
|
|
1284
|
+
/** That member's display name, else their email - so the call can be attributed without a roster read. */
|
|
1285
|
+
placedByName?: string | null;
|
|
1272
1286
|
/** ISO-8601. */
|
|
1273
1287
|
occurredAt: string;
|
|
1274
1288
|
}
|
|
1275
1289
|
/**
|
|
1276
1290
|
* A call reached a channel and nothing answered it.
|
|
1277
1291
|
*
|
|
1292
|
+
* Also raised for a call a member placed that the person rang did not pick
|
|
1293
|
+
* up (`handler: "member"`, `direction: "outbound"`) - somebody to try again.
|
|
1294
|
+
*
|
|
1278
1295
|
* Its own event rather than an `outcome` on the one above, because the action
|
|
1279
1296
|
* differs in kind: a completed call is something to file, a missed one is
|
|
1280
1297
|
* somebody to ring back. Treating this as a lead is the right thing to do
|
|
@@ -1287,6 +1304,20 @@ export interface CallMissedEvent {
|
|
|
1287
1304
|
/** E.164, and the only identity this carries - see `CallCompletedEvent.contactPhone`. */
|
|
1288
1305
|
contactPhone: string | null;
|
|
1289
1306
|
direction: ConnectCallDirection;
|
|
1307
|
+
/**
|
|
1308
|
+
* `member` when a person placed this call from a browser - `<ConnectCallButton>`
|
|
1309
|
+
* or Connect's own portal - rather than an agent handling it. Absent on
|
|
1310
|
+
* events sent before member calling existed; treat absent as `agent`.
|
|
1311
|
+
*/
|
|
1312
|
+
handler?: CallHandler;
|
|
1313
|
+
/**
|
|
1314
|
+
* The member who placed a `member` call - their Connect user id, the same id
|
|
1315
|
+
* a workspace roster carries. The one to attribute the call to. `null` on an
|
|
1316
|
+
* agent's call.
|
|
1317
|
+
*/
|
|
1318
|
+
placedByUserId?: string | null;
|
|
1319
|
+
/** That member's display name, else their email - so the call can be attributed without a roster read. */
|
|
1320
|
+
placedByName?: string | null;
|
|
1290
1321
|
/** ISO-8601. */
|
|
1291
1322
|
occurredAt: string;
|
|
1292
1323
|
}
|
|
@@ -2696,8 +2727,16 @@ export type CallDirection = "inbound" | "outbound";
|
|
|
2696
2727
|
* `rejected` means Connect never connected it - see `rejectReason`.
|
|
2697
2728
|
* `answered` is a call still in progress; `completed` is one that ran and
|
|
2698
2729
|
* ended.
|
|
2730
|
+
*
|
|
2731
|
+
* `dialing` and `unanswered` are an outbound call's: ringing, and nobody
|
|
2732
|
+
* picked up - see `unansweredReason`. A call your `<ConnectCallButton>`
|
|
2733
|
+
* placed passes through `dialing` on its way to one of the other two.
|
|
2699
2734
|
*/
|
|
2700
|
-
export type CallStatus = "answered" | "rejected" | "completed";
|
|
2735
|
+
export type CallStatus = "dialing" | "answered" | "rejected" | "unanswered" | "completed";
|
|
2736
|
+
/** Who was on Connect's end of a call: one of the workspace's agents, or a member calling from a browser. */
|
|
2737
|
+
export type CallHandler = "agent" | "member";
|
|
2738
|
+
/** Why an outbound call reached nobody. Widened at its use site like `CallRejectReason`. */
|
|
2739
|
+
export type CallUnansweredReason = "no_answer" | "busy" | "failed" | "cancelled";
|
|
2701
2740
|
/**
|
|
2702
2741
|
* Why a call was refused. Widened at the point of use with `(string & {})`,
|
|
2703
2742
|
* the same way `ReadinessBlocker.code` is: Connect adds reasons as it learns
|
|
@@ -2765,6 +2804,76 @@ export interface ConnectCall {
|
|
|
2765
2804
|
extraction: Record<string, unknown> | null;
|
|
2766
2805
|
agentId: string | null;
|
|
2767
2806
|
agentVersion: number | null;
|
|
2807
|
+
/** `member` for a call a person placed with `<ConnectCallButton>`; such a call has no agent, transcript or recording. */
|
|
2808
|
+
handler: CallHandler;
|
|
2809
|
+
/** The member who placed a `member` call - their Connect user id. */
|
|
2810
|
+
placedByUserId: string | null;
|
|
2811
|
+
/**
|
|
2812
|
+
* How long the person called was on the line, for a `member` call. Read this
|
|
2813
|
+
* rather than `durationSeconds`, which there measures the member's own leg,
|
|
2814
|
+
* ringing included. `null` until the carrier reports it, and for a call
|
|
2815
|
+
* nobody answered.
|
|
2816
|
+
*/
|
|
2817
|
+
talkSeconds: number | null;
|
|
2818
|
+
/** Why nobody answered - set only when `status` is `unanswered`. */
|
|
2819
|
+
unansweredReason: CallUnansweredReason | (string & {}) | null;
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* What the browser needs to place one approved call - see
|
|
2823
|
+
* `calls.startBrowserCall`. `headers` are SIP headers to send with it; they
|
|
2824
|
+
* carry a one-time key, valid for two minutes, that lets the call be placed
|
|
2825
|
+
* once and by nobody else.
|
|
2826
|
+
*/
|
|
2827
|
+
export interface StartedBrowserCall {
|
|
2828
|
+
callId: string;
|
|
2829
|
+
/** The number being rung, E.164. */
|
|
2830
|
+
toNumber: string;
|
|
2831
|
+
/** The workspace's own number, which the person called will see. */
|
|
2832
|
+
fromNumber: string;
|
|
2833
|
+
headers: Record<string, string>;
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Why a member can or cannot call - see `calls.getCallingAvailability`.
|
|
2837
|
+
*
|
|
2838
|
+
* - `no_permission` - they lack "Make calls". Don't offer calling.
|
|
2839
|
+
* - `no_number` - the workspace has no business number. `offer` says where to
|
|
2840
|
+
* buy one (a "Business number" add-on on one of its subscriptions).
|
|
2841
|
+
* - `number_pending` - one was bought but Connect's pool was empty; an admin
|
|
2842
|
+
* can retry from Connect's Calling page.
|
|
2843
|
+
* - `no_balance` - a number, but not a minute's worth of prepaid minutes.
|
|
2844
|
+
* - `available`.
|
|
2845
|
+
*/
|
|
2846
|
+
export type CallingAvailabilityStatus = "available" | "no_permission" | "no_number" | "number_pending" | "no_balance";
|
|
2847
|
+
export interface CallingAvailability {
|
|
2848
|
+
status: CallingAvailabilityStatus;
|
|
2849
|
+
/** The workspace's business number, E.164, when it holds one. */
|
|
2850
|
+
number: string | null;
|
|
2851
|
+
/** Prepaid balance a new call may spend, in paise, pre-tax. */
|
|
2852
|
+
available: number;
|
|
2853
|
+
/** About how many minutes that buys. */
|
|
2854
|
+
availableMinutes: number;
|
|
2855
|
+
ratePaisePerMinute: number;
|
|
2856
|
+
/** Under about 55 minutes - worth a warning. */
|
|
2857
|
+
lowBalance: boolean;
|
|
2858
|
+
/** Whether this member may buy minutes. */
|
|
2859
|
+
canTopUp: boolean;
|
|
2860
|
+
/** Whether this member may add the number add-on or retry a pending number. */
|
|
2861
|
+
canManageNumber: boolean;
|
|
2862
|
+
/** Where a business number can be bought, when the workspace has none. */
|
|
2863
|
+
offer: {
|
|
2864
|
+
subscriptionId: string;
|
|
2865
|
+
applicationId: string;
|
|
2866
|
+
addonId: string;
|
|
2867
|
+
addonName: string;
|
|
2868
|
+
/** Pre-tax, paise, per billing cycle. */
|
|
2869
|
+
price: number;
|
|
2870
|
+
} | null;
|
|
2871
|
+
}
|
|
2872
|
+
/** A carrier access token a member's browser signs in to the phone network with. Valid for an hour. */
|
|
2873
|
+
export interface BrowserCallingToken {
|
|
2874
|
+
token: string;
|
|
2875
|
+
/** ISO-8601. */
|
|
2876
|
+
expiresAt: string;
|
|
2768
2877
|
}
|
|
2769
2878
|
/** Who spoke. */
|
|
2770
2879
|
export type CallTurnRole = "user" | "agent";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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",
|
|
@@ -136,5 +136,8 @@
|
|
|
136
136
|
"streamdown": {
|
|
137
137
|
"optional": true
|
|
138
138
|
}
|
|
139
|
+
},
|
|
140
|
+
"dependencies": {
|
|
141
|
+
"plivo-browser-sdk": "2.2.21"
|
|
139
142
|
}
|
|
140
143
|
}
|