@rehers/rehers-roleplay-sdk 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +274 -33
  2. package/package.json +23 -3
  3. package/react.d.ts +78 -0
  4. package/react.js +224 -0
package/README.md CHANGED
@@ -9,39 +9,274 @@ Use it in these two places:
9
9
 
10
10
  The SDK must be initialized with the currently logged-in Seamless user before either embed flow is used.
11
11
 
12
- ## Load the SDK
12
+ ## Install
13
13
 
14
- Seamless can load the SDK in any of these three ways.
14
+ ```bash
15
+ npm install @rehers/rehers-roleplay-sdk
16
+ ```
15
17
 
16
- ### Option 1: Downloaded SDK file
18
+ ---
17
19
 
18
- If Seamless is hosting the SDK file directly inside the dashboard:
20
+ ## React Integration (Recommended)
19
21
 
20
- ```html
21
- <script src="/path/to/roleplay-sdk.js"></script>
22
+ If the Seamless dashboard is built with React, use the React bindings. They handle SDK lifecycle, cleanup, and stale closures automatically.
23
+
24
+ ```tsx
25
+ import {
26
+ SeamlessRoleplayProvider,
27
+ RoleplayDialog,
28
+ RoleplayEmbed,
29
+ AddToScenarioDialog,
30
+ useSeamlessRoleplay,
31
+ } from "@rehers/rehers-roleplay-sdk/react";
22
32
  ```
23
33
 
24
- ### Option 2: npm package
34
+ ### Get the logged-in Seamless user
25
35
 
26
- If Seamless is bundling the SDK through the frontend codebase:
36
+ Use the existing Seamless dashboard session and call:
27
37
 
28
- ```bash
29
- npm install @rehers/rehers-roleplay-sdk
38
+ ```ts
39
+ const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
40
+ method: "GET",
41
+ credentials: "include",
42
+ headers: {
43
+ accept: "application/json, text/plain, */*",
44
+ },
45
+ }).then((res) => res.json());
46
+
47
+ const me = meResponse.data ?? meResponse;
48
+
49
+ const seamlessUserId = String(me.id);
50
+ const seamlessUserEmail = me.username;
51
+ const seamlessUserRole =
52
+ me.orgRole === "owner" ? "owner" :
53
+ me.isOrgAdmin ? "admin" :
54
+ "member";
55
+ ```
56
+
57
+ ### Wrap your app with the Provider
58
+
59
+ Add `SeamlessRoleplayProvider` once, near the root of the Seamless dashboard. It initializes the SDK and makes it available to all child components.
60
+
61
+ ```tsx
62
+ function App() {
63
+ return (
64
+ <SeamlessRoleplayProvider
65
+ publishableKey={ROLEPLAY_PUBLISHABLE_KEY}
66
+ userId={seamlessUserId}
67
+ userEmail={seamlessUserEmail}
68
+ userRole={seamlessUserRole}
69
+ onReady={() => console.log("Roleplay SDK ready")}
70
+ onError={(err) => console.error("Roleplay SDK error", err)}
71
+ >
72
+ <Dashboard />
73
+ </SeamlessRoleplayProvider>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### Embed the full Roleplay page
79
+
80
+ Use `RoleplayEmbed` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
81
+
82
+ ```tsx
83
+ function RoleplayPage() {
84
+ return (
85
+ <RoleplayEmbed
86
+ style={{ width: "100%", height: "100vh" }}
87
+ onCallStarted={(data) => console.log("Call started", data.callId)}
88
+ onCallEnded={(data) => console.log("Call ended", data.callId, data.duration)}
89
+ onError={(err) => console.error("Roleplay error", err)}
90
+ />
91
+ );
92
+ }
93
+ ```
94
+
95
+ The embed mounts when the component mounts and cleans up automatically when it unmounts. No manual teardown needed.
96
+
97
+ ### Open a dialog from Contact Search
98
+
99
+ Use `RoleplayDialog` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
100
+
101
+ ```tsx
102
+ function ContactRow({ contact }) {
103
+ const [showRoleplay, setShowRoleplay] = useState(false);
104
+
105
+ return (
106
+ <>
107
+ <button onClick={() => setShowRoleplay(true)}>Roleplay</button>
108
+
109
+ <RoleplayDialog
110
+ open={showRoleplay}
111
+ name={contact.name}
112
+ domain={contact.domain}
113
+ company={contact.company}
114
+ title={contact.title}
115
+ liUrl={contact.linkedinUrl}
116
+ companyDescription={contact.companyDescription}
117
+ onCallStarted={(data) => {
118
+ console.log("Roleplay call started", data.callId);
119
+ }}
120
+ onCallEnded={(data) => {
121
+ console.log("Roleplay call ended", data.callId, data.duration);
122
+ }}
123
+ onClose={() => setShowRoleplay(false)}
124
+ onError={(err) => {
125
+ console.error("Roleplay dialog error", err);
126
+ }}
127
+ />
128
+ </>
129
+ );
130
+ }
30
131
  ```
31
132
 
133
+ The dialog opens when `open` becomes `true` and closes when it becomes `false`. All callbacks always see the latest props and state — no stale closures.
134
+
135
+ ### Add contacts to a scenario
136
+
137
+ Use `AddToScenarioDialog` to send multiple contacts into a scenario picker dialog.
138
+
139
+ ```tsx
140
+ function BulkActions({ selectedContacts }) {
141
+ const [showATS, setShowATS] = useState(false);
142
+
143
+ return (
144
+ <>
145
+ <button onClick={() => setShowATS(true)}>Add to Scenario</button>
146
+
147
+ <AddToScenarioDialog
148
+ open={showATS}
149
+ contacts={selectedContacts.map((contact) => ({
150
+ name: contact.name,
151
+ company: contact.company,
152
+ title: contact.title,
153
+ domain: contact.domain,
154
+ liUrl: contact.linkedinUrl,
155
+ companyDescription: contact.companyDescription,
156
+ }))}
157
+ onComplete={(data) => {
158
+ console.log("Scenario import complete", data);
159
+ setShowATS(false);
160
+ }}
161
+ onClose={() => setShowATS(false)}
162
+ onError={(err) => {
163
+ console.error("Add to scenario error", err);
164
+ }}
165
+ />
166
+ </>
167
+ );
168
+ }
169
+ ```
170
+
171
+ Supports 1 to 25 contacts per call. Each contact requires `name`, `company`, `title`, and `domain`.
172
+
173
+ ### Use the hook for imperative access
174
+
175
+ If you need direct access to SDK methods instead of declarative components:
176
+
177
+ ```tsx
178
+ function CustomButton({ contact }) {
179
+ const { isReady, open, close } = useSeamlessRoleplay();
180
+
181
+ return (
182
+ <button
183
+ disabled={!isReady}
184
+ onClick={() =>
185
+ open({
186
+ ...contact,
187
+ onClose: () => console.log("closed"),
188
+ })
189
+ }
190
+ >
191
+ Start Roleplay
192
+ </button>
193
+ );
194
+ }
195
+ ```
196
+
197
+ ### React props reference
198
+
199
+ #### `SeamlessRoleplayProvider`
200
+
201
+ | Prop | Type | Required | Description |
202
+ |---|---|---|---|
203
+ | `publishableKey` | `string` | Yes | Publishable API key (`pk_live_...` or `pk_test_...`) |
204
+ | `userId` | `string` | Yes | `String(me.id)` from Seamless `/api/users/me` |
205
+ | `userEmail` | `string` | Yes | `me.username` from Seamless `/api/users/me` |
206
+ | `userRole` | `"owner" \| "admin" \| "member"` | No | User role for syncing permissions |
207
+ | `userToken` | `string` | No | Signed JWT for identity verification |
208
+ | `origin` | `string` | No | Override iframe origin (dev/testing only) |
209
+ | `onReady` | `() => void` | No | Called when SDK session is ready |
210
+ | `onError` | `(error) => void` | No | Called on initialization error |
211
+
212
+ #### `RoleplayEmbed`
213
+
214
+ | Prop | Type | Required | Description |
215
+ |---|---|---|---|
216
+ | `className` | `string` | No | CSS class for the container div |
217
+ | `style` | `CSSProperties` | No | Inline styles for the container div |
218
+ | `onCallStarted` | `(data) => void` | No | Called when a roleplay call starts |
219
+ | `onCallEnded` | `(data) => void` | No | Called when a roleplay call ends |
220
+ | `onClose` | `() => void` | No | Called when the embed is closed |
221
+ | `onError` | `(data) => void` | No | Called on error |
222
+
223
+ #### `RoleplayDialog`
224
+
225
+ | Prop | Type | Required | Description |
226
+ |---|---|---|---|
227
+ | `open` | `boolean` | Yes | Whether the dialog is open |
228
+ | `name` | `string` | Yes | Contact full name |
229
+ | `domain` | `string` | Yes | Company domain (e.g. `"stripe.com"`) |
230
+ | `company` | `string` | Yes | Company name |
231
+ | `title` | `string` | Yes | Contact job title |
232
+ | `companyDescription` | `string` | No | Company description |
233
+ | `liUrl` | `string` | No | LinkedIn profile URL |
234
+ | `onCallStarted` | `(data) => void` | No | Called when the roleplay call starts |
235
+ | `onCallEnded` | `(data) => void` | No | Called when the roleplay call ends |
236
+ | `onClose` | `() => void` | No | Called when the dialog is closed |
237
+ | `onError` | `(data) => void` | No | Called on error |
238
+
239
+ #### `AddToScenarioDialog`
240
+
241
+ | Prop | Type | Required | Description |
242
+ |---|---|---|---|
243
+ | `open` | `boolean` | Yes | Whether the dialog is open |
244
+ | `contacts` | `AddToScenarioContact[]` | Yes | Array of contacts (1–25) |
245
+ | `onComplete` | `(data) => void` | No | Called on successful import |
246
+ | `onClose` | `() => void` | No | Called when dialog is closed |
247
+ | `onError` | `(error) => void` | No | Called on error |
248
+
249
+ #### `useSeamlessRoleplay()`
250
+
251
+ Returns `{ isReady, error, sdk }`. Must be used inside a `SeamlessRoleplayProvider`.
252
+
253
+ ---
254
+
255
+ ## Raw JavaScript Integration
256
+
257
+ If the Seamless dashboard does not use React, or if you need direct imperative control, use the vanilla SDK directly.
258
+
259
+ ### Load the SDK
260
+
261
+ #### Option 1: npm import
262
+
32
263
  ```js
33
264
  import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
34
265
  ```
35
266
 
36
- ### Option 3: CDN script
267
+ #### Option 2: Script tag (hosted file)
268
+
269
+ ```html
270
+ <script src="/path/to/roleplay-sdk.js"></script>
271
+ ```
37
272
 
38
- If Seamless wants to load the SDK without bundling it:
273
+ #### Option 3: CDN script
39
274
 
40
275
  ```html
41
276
  <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
42
277
  ```
43
278
 
44
- ## Get the logged-in Seamless user
279
+ ### Get the logged-in Seamless user
45
280
 
46
281
  Use the existing Seamless dashboard session and call:
47
282
 
@@ -61,7 +296,7 @@ Normalize the response before reading fields:
61
296
  const me = meResponse.data ?? meResponse;
62
297
  ```
63
298
 
64
- ## Required mapping for `init()`
299
+ ### Required mapping for `init()`
65
300
 
66
301
  These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
67
302
 
@@ -82,14 +317,7 @@ const seamlessUserRole =
82
317
  "member";
83
318
  ```
84
319
 
85
- For the response shape currently returned by Seamless, this means:
86
-
87
- ```js
88
- const seamlessUserId = String(me.id);
89
- const seamlessUserEmail = me.username;
90
- ```
91
-
92
- ## Initialize the SDK once
320
+ ### Initialize the SDK once
93
321
 
94
322
  Initialize the SDK once per page load using the logged-in Seamless user. Reuse that same initialized SDK for both the full-page embed and the Contact Search dialog.
95
323
 
@@ -133,7 +361,7 @@ function ensureRoleplaySdkReady() {
133
361
  }
134
362
  ```
135
363
 
136
- ## Embed in the full Roleplay page
364
+ ### Embed in the full Roleplay page
137
365
 
138
366
  Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
139
367
 
@@ -156,7 +384,7 @@ If the Seamless page or tab is torn down, unmount the SDK:
156
384
  SeamlessRoleplay.unmount();
157
385
  ```
158
386
 
159
- ## Embed in Contact Search
387
+ ### Embed in Contact Search
160
388
 
161
389
  Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
162
390
 
@@ -198,7 +426,7 @@ The contact object passed to `open(...)` should map to:
198
426
  | `liUrl` | LinkedIn profile URL, if available |
199
427
  | `companyDescription` | Company description, if available |
200
428
 
201
- ## Optional: add contacts to a scenario
429
+ ### Add contacts to a scenario
202
430
 
203
431
  If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
204
432
 
@@ -228,9 +456,9 @@ async function addContactsToScenario(contacts) {
228
456
  }
229
457
  ```
230
458
 
231
- ## Minimal API surface
459
+ ### API reference
232
460
 
233
- ### `SeamlessRoleplay.init(options)`
461
+ #### `SeamlessRoleplay.init(options)`
234
462
 
235
463
  Initializes the SDK with the logged-in Seamless user.
236
464
 
@@ -245,30 +473,32 @@ SeamlessRoleplay.init({
245
473
  });
246
474
  ```
247
475
 
248
- ### `SeamlessRoleplay.mount(container)`
476
+ #### `SeamlessRoleplay.mount(container)`
249
477
 
250
478
  Mounts the full Roleplay app into a dashboard container.
251
479
 
252
- ### `SeamlessRoleplay.open(contactData)`
480
+ #### `SeamlessRoleplay.open(contactData)`
253
481
 
254
482
  Opens the Roleplay dialog for a single contact.
255
483
 
256
- ### `SeamlessRoleplay.addToScenario(options)`
484
+ #### `SeamlessRoleplay.addToScenario(options)`
257
485
 
258
486
  Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
259
487
 
260
- ### `SeamlessRoleplay.close()`
488
+ #### `SeamlessRoleplay.close()`
261
489
 
262
490
  Closes the active dialog.
263
491
 
264
- ### `SeamlessRoleplay.unmount()`
492
+ #### `SeamlessRoleplay.unmount()`
265
493
 
266
494
  Unmounts the full-page Roleplay embed.
267
495
 
268
- ### `SeamlessRoleplay.destroy()`
496
+ #### `SeamlessRoleplay.destroy()`
269
497
 
270
498
  Destroys the SDK state, timers, mount, and dialogs.
271
499
 
500
+ ---
501
+
272
502
  ## Trial and upgrade handling
273
503
 
274
504
  If the backend responds with `USER_NOT_FOUND` during SDK session creation, the SDK handles the upgrade or trial UI automatically. Seamless does not need separate client-side logic for that case.
@@ -278,10 +508,21 @@ If the backend responds with `USER_NOT_FOUND` during SDK session creation, the S
278
508
  Type declarations are included with the SDK package.
279
509
 
280
510
  ```ts
511
+ // Vanilla SDK types
281
512
  import type {
282
513
  SeamlessRoleplaySDK,
283
514
  SeamlessRoleplayInitOptions,
284
515
  SeamlessRoleplayOpenData,
285
516
  AddToScenarioOptions,
286
517
  } from "@rehers/rehers-roleplay-sdk";
518
+
519
+ // React bindings types
520
+ import type {
521
+ SeamlessRoleplayProviderProps,
522
+ RoleplayDialogProps,
523
+ RoleplayEmbedProps,
524
+ AddToScenarioDialogProps,
525
+ AddToScenarioContact,
526
+ AddToScenarioCompleteData,
527
+ } from "@rehers/rehers-roleplay-sdk/react";
287
528
  ```
package/package.json CHANGED
@@ -1,14 +1,34 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
5
5
  "main": "roleplay-sdk.js",
6
6
  "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./roleplay-sdk.js"
11
+ },
12
+ "./react": {
13
+ "types": "./react.d.ts",
14
+ "default": "./react.js"
15
+ }
16
+ },
7
17
  "files": [
8
18
  "roleplay-sdk.js",
9
- "index.d.ts"
19
+ "index.d.ts",
20
+ "react.js",
21
+ "react.d.ts"
10
22
  ],
11
- "keywords": ["seamless", "roleplay", "sdk", "sales", "training"],
23
+ "peerDependencies": {
24
+ "react": ">=18.0.0",
25
+ "react-dom": ">=18.0.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "react": { "optional": true },
29
+ "react-dom": { "optional": true }
30
+ },
31
+ "keywords": ["seamless", "roleplay", "sdk", "sales", "training", "react"],
12
32
  "license": "UNLICENSED",
13
33
  "repository": {
14
34
  "type": "git",
package/react.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { type ReactNode } from "react";
2
+ import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData } from "@rehers/rehers-roleplay-sdk";
3
+ import "@rehers/rehers-roleplay-sdk";
4
+ export type { AddToScenarioContact, AddToScenarioCompleteData };
5
+ interface SeamlessRoleplayContextValue {
6
+ isReady: boolean;
7
+ error: {
8
+ code: string;
9
+ message: string;
10
+ } | null;
11
+ sdk: SeamlessRoleplaySDK;
12
+ }
13
+ export interface SeamlessRoleplayProviderProps {
14
+ publishableKey: string;
15
+ userId: string;
16
+ userEmail: string;
17
+ userRole?: "owner" | "admin" | "member";
18
+ userToken?: string;
19
+ origin?: string;
20
+ onReady?: () => void;
21
+ onError?: (error: {
22
+ code: string;
23
+ message: string;
24
+ }) => void;
25
+ children: ReactNode;
26
+ }
27
+ export declare function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
29
+ export interface RoleplayDialogProps {
30
+ open: boolean;
31
+ name: string;
32
+ domain: string;
33
+ company: string;
34
+ title: string;
35
+ companyDescription?: string;
36
+ liUrl?: string;
37
+ onCallStarted?: (data: {
38
+ callId: string;
39
+ }) => void;
40
+ onCallEnded?: (data: {
41
+ callId: string;
42
+ duration?: number;
43
+ }) => void;
44
+ onClose?: () => void;
45
+ onError?: (data: {
46
+ code: string;
47
+ message: string;
48
+ }) => void;
49
+ }
50
+ export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }: RoleplayDialogProps): null;
51
+ export interface RoleplayEmbedProps {
52
+ className?: string;
53
+ style?: React.CSSProperties;
54
+ onCallStarted?: (data: {
55
+ callId: string;
56
+ }) => void;
57
+ onCallEnded?: (data: {
58
+ callId: string;
59
+ duration?: number;
60
+ }) => void;
61
+ onClose?: () => void;
62
+ onError?: (data: {
63
+ code: string;
64
+ message: string;
65
+ }) => void;
66
+ }
67
+ export declare function RoleplayEmbed({ className, style, onCallStarted, onCallEnded, onClose, onError, }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
68
+ export interface AddToScenarioDialogProps {
69
+ open: boolean;
70
+ contacts: AddToScenarioContact[];
71
+ onComplete?: (data: AddToScenarioCompleteData) => void;
72
+ onClose?: () => void;
73
+ onError?: (error: {
74
+ code: string;
75
+ message: string;
76
+ }) => void;
77
+ }
78
+ export declare function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }: AddToScenarioDialogProps): null;
package/react.js ADDED
@@ -0,0 +1,224 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
+ // Side-effect import: the SDK IIFE runs and sets window.SeamlessRoleplay.
4
+ // Works with bundlers (Vite, webpack) and script tags alike.
5
+ import "@rehers/rehers-roleplay-sdk";
6
+ // ── Callback ref helper ─────────────────────────────────────────────
7
+ // Keeps a ref in sync with the latest callback to avoid stale closures.
8
+ function useCallbackRef(cb) {
9
+ const ref = useRef(cb);
10
+ useLayoutEffect(() => {
11
+ ref.current = cb;
12
+ });
13
+ return ref;
14
+ }
15
+ // ── SDK singleton access ────────────────────────────────────────────
16
+ function getSDK() {
17
+ if (typeof window !== "undefined" && window.SeamlessRoleplay) {
18
+ return window.SeamlessRoleplay;
19
+ }
20
+ throw new Error("[SeamlessRoleplay/React] Could not find SeamlessRoleplay SDK. " +
21
+ 'Make sure "@rehers/rehers-roleplay-sdk" is installed and loaded before the React wrapper.');
22
+ }
23
+ const SeamlessRoleplayContext = createContext(null);
24
+ let providerMountCount = 0;
25
+ export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }) {
26
+ const [state, setState] = useState({ isReady: false, error: null });
27
+ const mountedRef = useRef(false);
28
+ const onReadyRef = useCallbackRef(onReady);
29
+ const onErrorRef = useCallbackRef(onError);
30
+ const sdk = useMemo(() => getSDK(), []);
31
+ useEffect(() => {
32
+ providerMountCount++;
33
+ if (providerMountCount > 1 && process.env.NODE_ENV !== "production") {
34
+ console.warn("[SeamlessRoleplay/React] Multiple SeamlessRoleplayProvider instances detected. " +
35
+ "The SDK is a singleton — only one Provider should be mounted at a time.");
36
+ }
37
+ mountedRef.current = true;
38
+ setState({ isReady: false, error: null });
39
+ sdk.init({
40
+ publishableKey,
41
+ userId,
42
+ userEmail,
43
+ userRole,
44
+ userToken,
45
+ origin,
46
+ onReady: () => {
47
+ var _a;
48
+ if (!mountedRef.current)
49
+ return;
50
+ setState({ isReady: true, error: null });
51
+ (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
52
+ },
53
+ onError: (err) => {
54
+ var _a;
55
+ if (!mountedRef.current)
56
+ return;
57
+ setState({ isReady: false, error: err });
58
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
59
+ },
60
+ });
61
+ return () => {
62
+ mountedRef.current = false;
63
+ providerMountCount--;
64
+ sdk.destroy();
65
+ };
66
+ }, [sdk, publishableKey, userId, userEmail, userRole, userToken, origin]);
67
+ const contextValue = useMemo(() => ({ ...state, sdk }), [state, sdk]);
68
+ return (_jsx(SeamlessRoleplayContext.Provider, { value: contextValue, children: children }));
69
+ }
70
+ // ── Hook ────────────────────────────────────────────────────────────
71
+ export function useSeamlessRoleplay() {
72
+ const ctx = useContext(SeamlessRoleplayContext);
73
+ if (!ctx) {
74
+ throw new Error("[SeamlessRoleplay/React] useSeamlessRoleplay() must be used inside a <SeamlessRoleplayProvider>.");
75
+ }
76
+ return ctx;
77
+ }
78
+ export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }) {
79
+ const { isReady, sdk } = useSeamlessRoleplay();
80
+ const onCallStartedRef = useCallbackRef(onCallStarted);
81
+ const onCallEndedRef = useCallbackRef(onCallEnded);
82
+ const onCloseRef = useCallbackRef(onClose);
83
+ const onErrorRef = useCallbackRef(onError);
84
+ const isOpenRef = useRef(false);
85
+ useEffect(() => {
86
+ if (!isReady)
87
+ return;
88
+ if (isOpen) {
89
+ isOpenRef.current = true;
90
+ sdk.open({
91
+ name,
92
+ domain,
93
+ company,
94
+ title,
95
+ companyDescription,
96
+ liUrl,
97
+ onCallStarted: (data) => { var _a; return (_a = onCallStartedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallStartedRef, data); },
98
+ onCallEnded: (data) => { var _a; return (_a = onCallEndedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallEndedRef, data); },
99
+ onClose: () => {
100
+ var _a;
101
+ isOpenRef.current = false;
102
+ (_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
103
+ },
104
+ onError: (data) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, data); },
105
+ });
106
+ }
107
+ else if (isOpenRef.current) {
108
+ isOpenRef.current = false;
109
+ sdk.close();
110
+ }
111
+ return () => {
112
+ if (isOpenRef.current) {
113
+ isOpenRef.current = false;
114
+ sdk.close();
115
+ }
116
+ };
117
+ }, [isReady, isOpen, name, domain, company, title, companyDescription, liUrl, sdk]);
118
+ return null;
119
+ }
120
+ // ── RoleplayEmbed ───────────────────────────────────────────────────
121
+ // The vanilla SDK's mount() doesn't accept callbacks, so RoleplayEmbed
122
+ // sets up its own postMessage listener after mount() appends the iframe.
123
+ const DEFAULT_APP_ORIGIN = "https://app.roleplaywithseamless.ai";
124
+ export function RoleplayEmbed({ className, style, onCallStarted, onCallEnded, onClose, onError, }) {
125
+ const { isReady, sdk } = useSeamlessRoleplay();
126
+ const containerRef = useRef(null);
127
+ const onCallStartedRef = useCallbackRef(onCallStarted);
128
+ const onCallEndedRef = useCallbackRef(onCallEnded);
129
+ const onCloseRef = useCallbackRef(onClose);
130
+ const onErrorRef = useCallbackRef(onError);
131
+ useEffect(() => {
132
+ if (!isReady || !containerRef.current)
133
+ return;
134
+ sdk.mount(containerRef.current);
135
+ // Grab the iframe that mount() just appended
136
+ const iframe = containerRef.current.querySelector("iframe");
137
+ if (!iframe)
138
+ return;
139
+ const hasCallbacks = onCallStarted || onCallEnded || onClose || onError;
140
+ let listener = null;
141
+ if (hasCallbacks) {
142
+ listener = (event) => {
143
+ var _a, _b, _c, _d;
144
+ if (event.origin !== DEFAULT_APP_ORIGIN)
145
+ return;
146
+ if (!iframe.contentWindow || event.source !== iframe.contentWindow)
147
+ return;
148
+ const data = event.data;
149
+ if (!data || typeof data.type !== "string")
150
+ return;
151
+ switch (data.type) {
152
+ case "ROLEPLAY_CALL_STARTED":
153
+ (_a = onCallStartedRef.current) === null || _a === void 0 ? void 0 : _a.call(onCallStartedRef, { callId: data.callId });
154
+ break;
155
+ case "ROLEPLAY_CALL_ENDED":
156
+ (_b = onCallEndedRef.current) === null || _b === void 0 ? void 0 : _b.call(onCallEndedRef, {
157
+ callId: data.callId,
158
+ duration: data.duration,
159
+ });
160
+ break;
161
+ case "ROLEPLAY_ERROR":
162
+ (_c = onErrorRef.current) === null || _c === void 0 ? void 0 : _c.call(onErrorRef, {
163
+ code: data.code,
164
+ message: data.message,
165
+ });
166
+ break;
167
+ case "ROLEPLAY_CLOSED":
168
+ (_d = onCloseRef.current) === null || _d === void 0 ? void 0 : _d.call(onCloseRef);
169
+ break;
170
+ }
171
+ };
172
+ window.addEventListener("message", listener);
173
+ }
174
+ return () => {
175
+ if (listener)
176
+ window.removeEventListener("message", listener);
177
+ sdk.unmount();
178
+ };
179
+ }, [isReady, sdk]);
180
+ return _jsx("div", { ref: containerRef, className: className, style: style });
181
+ }
182
+ export function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClose, onError, }) {
183
+ const { isReady, sdk } = useSeamlessRoleplay();
184
+ const onCompleteRef = useCallbackRef(onComplete);
185
+ const onCloseRef = useCallbackRef(onClose);
186
+ const onErrorRef = useCallbackRef(onError);
187
+ const isOpenRef = useRef(false);
188
+ const contactsRef = useRef(contacts);
189
+ useLayoutEffect(() => {
190
+ contactsRef.current = contacts;
191
+ });
192
+ useEffect(() => {
193
+ if (!isReady)
194
+ return;
195
+ if (isOpen) {
196
+ isOpenRef.current = true;
197
+ sdk.addToScenario({
198
+ contacts: contactsRef.current,
199
+ onComplete: (data) => {
200
+ var _a;
201
+ isOpenRef.current = false;
202
+ (_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef, data);
203
+ },
204
+ onClose: () => {
205
+ var _a;
206
+ isOpenRef.current = false;
207
+ (_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef);
208
+ },
209
+ onError: (err) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err); },
210
+ });
211
+ }
212
+ else if (isOpenRef.current) {
213
+ isOpenRef.current = false;
214
+ sdk.close();
215
+ }
216
+ return () => {
217
+ if (isOpenRef.current) {
218
+ isOpenRef.current = false;
219
+ sdk.close();
220
+ }
221
+ };
222
+ }, [isReady, isOpen, sdk]);
223
+ return null;
224
+ }