@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.
@@ -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`
@@ -138,4 +138,4 @@ export declare class ConnectClient {
138
138
  constructor(options: ConnectClientOptions);
139
139
  }
140
140
  /** Kept in step with `package.json` by `sdk.test.ts`. */
141
- export declare const SDK_VERSION = "0.40.0";
141
+ export declare const SDK_VERSION = "0.41.0";
@@ -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
  }
@@ -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.40.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
  }