@rehers/rehers-roleplay-sdk 2.5.0 → 2.5.2

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/README.md CHANGED
@@ -1,13 +1,4 @@
1
- # Roleplay SDK for the Seamless.AI Dashboard
2
-
3
- This SDK is only for embedding Roleplay inside the Seamless.AI dashboard.
4
-
5
- Use it in these two places:
6
-
7
- - The dedicated Roleplay page or tab inside the Seamless dashboard
8
- - The Contact Search screen, where clicking `Roleplay` opens a dialog for a single contact
9
-
10
- The SDK must be initialized with the currently logged-in Seamless user before either embed flow is used.
1
+ # Roleplay SDK for Seamless.AI
11
2
 
12
3
  ## Install
13
4
 
@@ -15,514 +6,238 @@ The SDK must be initialized with the currently logged-in Seamless user before ei
15
6
  npm install @rehers/rehers-roleplay-sdk
16
7
  ```
17
8
 
18
- ---
19
-
20
- ## React Integration (Recommended)
9
+ For a working end-to-end example in this repo, use `sdk-demo/`.
21
10
 
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";
32
- ```
11
+ ---
33
12
 
34
- ### Get the logged-in Seamless user
13
+ ## 1. Wrap your app with the Provider
35
14
 
36
- Use the existing Seamless dashboard session and call:
15
+ Add this once, above all your routes. It initializes the SDK for the logged-in Seamless user.
37
16
 
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
- ```
17
+ Production flow:
56
18
 
57
- ### Wrap your app with the Provider
19
+ 1. Your backend requests a short-lived `userToken` from `POST /api/seamless/auth/user-token`
20
+ 2. Your frontend receives that `userToken`
21
+ 3. You pass `publishableKey` + `userToken` into the SDK
58
22
 
59
- Add `SeamlessRoleplayProvider` once, near the root of the Seamless dashboard. It initializes the SDK and makes it available to all child components.
23
+ The browser should not mint sessions from raw `userId`, `userEmail`, or `userRole` in production.
60
24
 
61
25
  ```tsx
26
+ import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
27
+
62
28
  function App() {
29
+ const userToken = useRoleplayUserToken(); // fetched from your backend
30
+
63
31
  return (
64
32
  <SeamlessRoleplayProvider
65
- publishableKey={ROLEPLAY_PUBLISHABLE_KEY}
66
- userId={seamlessUserId}
67
- userEmail={seamlessUserEmail}
68
- userRole={seamlessUserRole}
33
+ publishableKey="pk_live_..."
34
+ userToken={userToken}
69
35
  onReady={() => console.log("Roleplay SDK ready")}
70
36
  onError={(err) => console.error("Roleplay SDK error", err)}
71
37
  >
72
- <Dashboard />
38
+ <Routes>
39
+ <Route path="/roleplay" element={<RoleplayPage />} />
40
+ <Route path="/contacts" element={<ContactSearch />} />
41
+ {/* ...your other routes */}
42
+ </Routes>
73
43
  </SeamlessRoleplayProvider>
74
44
  );
75
45
  }
76
46
  ```
77
47
 
78
- ### Embed the full Roleplay page
48
+ If you need a backend shape, the secure flow looks like this:
49
+
50
+ ```ts
51
+ const tokenRes = await fetch("/api/roleplay/user-token", {
52
+ method: "POST",
53
+ credentials: "include",
54
+ }).then((r) => r.json());
55
+
56
+ const userToken = tokenRes.userToken;
57
+ ```
58
+
59
+ That's the only setup. Everything below just works.
60
+
61
+ ---
62
+
63
+ ## 2. Full Roleplay page
79
64
 
80
- Use `RoleplayEmbed` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
65
+ For the dedicated Roleplay tab/page, drop in `RoleplayEmbed`. It fills its container.
81
66
 
82
67
  ```tsx
68
+ import { RoleplayEmbed } from "@rehers/rehers-roleplay-sdk/react";
69
+
83
70
  function RoleplayPage() {
84
71
  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
- />
72
+ <RoleplayEmbed style={{ width: "100%", height: "100%" }} />
91
73
  );
92
74
  }
93
75
  ```
94
76
 
95
- The embed mounts when the component mounts and cleans up automatically when it unmounts. No manual teardown needed.
77
+ Make sure the parent has a height (`height: 100%`, `height: 100vh`, `flex: 1`, etc).
78
+
79
+ Mounts automatically. Cleans up automatically when the user navigates away.
80
+
81
+ ---
96
82
 
97
- ### Open a dialog from Contact Search
83
+ ## 3. Roleplay dialog on Contact Search
98
84
 
99
- Use `RoleplayDialog` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
85
+ When a user clicks "Roleplay" on a contact row, a dialog opens over the page.
100
86
 
101
87
  ```tsx
102
- function ContactRow({ contact }) {
103
- const [showRoleplay, setShowRoleplay] = useState(false);
88
+ import { useState } from "react";
89
+ import { RoleplayDialog } from "@rehers/rehers-roleplay-sdk/react";
90
+
91
+ function ContactSearch() {
92
+ const [activeContact, setActiveContact] = useState(null);
104
93
 
105
94
  return (
106
95
  <>
107
- <button onClick={() => setShowRoleplay(true)}>Roleplay</button>
108
-
96
+ {/* Your existing contact table — just add an onClick to each row's button */}
97
+ <table>
98
+ {contacts.map((contact) => (
99
+ <tr key={contact.id}>
100
+ <td>{contact.name}</td>
101
+ <td>{contact.company}</td>
102
+ <td>{contact.title}</td>
103
+ <td>
104
+ <button onClick={() => setActiveContact(contact)}>
105
+ Roleplay
106
+ </button>
107
+ </td>
108
+ </tr>
109
+ ))}
110
+ </table>
111
+
112
+ {/* This renders nothing visible — the SDK creates the overlay */}
109
113
  <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
- }}
114
+ open={activeContact !== null}
115
+ name={activeContact?.name ?? ""}
116
+ domain={activeContact?.domain ?? ""}
117
+ company={activeContact?.company ?? ""}
118
+ title={activeContact?.title ?? ""}
119
+ liUrl={activeContact?.linkedinUrl}
120
+ companyDescription={activeContact?.companyDescription}
121
+ onClose={() => setActiveContact(null)}
122
+ onError={(err) => console.error("Error", err)}
127
123
  />
128
124
  </>
129
125
  );
130
126
  }
131
127
  ```
132
128
 
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.
129
+ **How it works:** Set `activeContact` to a contact object to open the dialog. Set it back to `null` to close. The `onClose` callback fires when the user closes it themselves.
130
+
131
+ ---
134
132
 
135
- ### Add contacts to a scenario
133
+ ## 4. Add to Scenario (bulk)
136
134
 
137
- Use `AddToScenarioDialog` to send multiple contacts into a scenario picker dialog.
135
+ When users select multiple contacts and click "Add to Scenario", a scenario picker dialog opens.
138
136
 
139
137
  ```tsx
140
- function BulkActions({ selectedContacts }) {
141
- const [showATS, setShowATS] = useState(false);
138
+ import { useState } from "react";
139
+ import { AddToScenarioDialog } from "@rehers/rehers-roleplay-sdk/react";
140
+
141
+ function ContactSearch() {
142
+ const [selectedContacts, setSelectedContacts] = useState([]);
143
+ const [showScenario, setShowScenario] = useState(false);
142
144
 
143
145
  return (
144
146
  <>
145
- <button onClick={() => setShowATS(true)}>Add to Scenario</button>
147
+ {/* Your existing table with checkboxes that populate selectedContacts */}
148
+
149
+ <button
150
+ disabled={selectedContacts.length === 0}
151
+ onClick={() => setShowScenario(true)}
152
+ >
153
+ Add {selectedContacts.length} to Scenario
154
+ </button>
146
155
 
147
156
  <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,
157
+ open={showScenario}
158
+ contacts={selectedContacts.map((c) => ({
159
+ name: c.name,
160
+ company: c.company,
161
+ title: c.title,
162
+ domain: c.domain,
163
+ liUrl: c.linkedinUrl,
156
164
  }))}
157
165
  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);
166
+ console.log(`Added ${data.addedCount} to ${data.scenarioName}`);
167
+ setShowScenario(false);
168
+ setSelectedContacts([]);
164
169
  }}
170
+ onClose={() => setShowScenario(false)}
171
+ onError={(err) => console.error("Error", err)}
165
172
  />
166
173
  </>
167
174
  );
168
175
  }
169
176
  ```
170
177
 
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`.
178
+ Accepts 1 to 25 contacts. Each contact needs `name`, `company`, `title`, and `domain`.
252
179
 
253
180
  ---
254
181
 
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
182
+ ## Contact field mapping
262
183
 
263
- ```js
264
- import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
265
- ```
184
+ When passing contact data to `RoleplayDialog` or `AddToScenarioDialog`:
266
185
 
267
- #### Option 2: Script tag (hosted file)
268
-
269
- ```html
270
- <script src="/path/to/roleplay-sdk.js"></script>
271
- ```
272
-
273
- #### Option 3: CDN script
274
-
275
- ```html
276
- <script src="https://unpkg.com/@rehers/rehers-roleplay-sdk"></script>
277
- ```
278
-
279
- ### Get the logged-in Seamless user
280
-
281
- Use the existing Seamless dashboard session and call:
282
-
283
- ```js
284
- const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
285
- method: "GET",
286
- credentials: "include",
287
- headers: {
288
- accept: "application/json, text/plain, */*",
289
- },
290
- }).then((res) => res.json());
291
- ```
292
-
293
- Normalize the response before reading fields:
294
-
295
- ```js
296
- const me = meResponse.data ?? meResponse;
297
- ```
298
-
299
- ### Required mapping for `init()`
300
-
301
- These are the values Seamless must pass into `SeamlessRoleplay.init(...)`:
302
-
303
- | SDK field | Seamless `/api/users/me` field |
186
+ | SDK prop | Your contact field |
304
187
  |---|---|
305
- | `userId` | `String(me.id)` |
306
- | `userEmail` | `me.username` |
307
- | `userRole` | Optional. Suggested mapping from `me.orgRole` / `me.isOrgAdmin` |
308
-
309
- Example:
310
-
311
- ```js
312
- const seamlessUserId = String(me.id);
313
- const seamlessUserEmail = me.username;
314
- const seamlessUserRole =
315
- me.orgRole === "owner" ? "owner" :
316
- me.isOrgAdmin ? "admin" :
317
- "member";
318
- ```
319
-
320
- ### Initialize the SDK once
321
-
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.
323
-
324
- ```js
325
- let roleplayReadyPromise;
326
-
327
- function ensureRoleplaySdkReady() {
328
- if (roleplayReadyPromise) return roleplayReadyPromise;
329
-
330
- roleplayReadyPromise = (async () => {
331
- const meResponse = await fetch("https://api.seamless.ai/api/users/me", {
332
- method: "GET",
333
- credentials: "include",
334
- headers: {
335
- accept: "application/json, text/plain, */*",
336
- },
337
- }).then((res) => res.json());
338
-
339
- const me = meResponse.data ?? meResponse;
340
-
341
- const seamlessUserId = String(me.id);
342
- const seamlessUserEmail = me.username;
343
- const seamlessUserRole =
344
- me.orgRole === "owner" ? "owner" :
345
- me.isOrgAdmin ? "admin" :
346
- "member";
347
-
348
- await new Promise((resolve, reject) => {
349
- SeamlessRoleplay.init({
350
- publishableKey: ROLEPLAY_PUBLISHABLE_KEY,
351
- userId: seamlessUserId,
352
- userEmail: seamlessUserEmail,
353
- userRole: seamlessUserRole,
354
- onReady: resolve,
355
- onError: reject,
356
- });
357
- });
358
- })();
359
-
360
- return roleplayReadyPromise;
361
- }
362
- ```
363
-
364
- ### Embed in the full Roleplay page
365
-
366
- Use `mount(container)` when Seamless has a dedicated Roleplay page or tab and the full content area should be the Roleplay app.
367
-
368
- ```html
369
- <div id="roleplay-root" style="width: 100%; height: 100%;"></div>
370
- ```
188
+ | `name` | Full name |
189
+ | `domain` | Company domain (e.g. `"stripe.com"`) |
190
+ | `company` | Company name |
191
+ | `title` | Job title |
192
+ | `liUrl` | LinkedIn URL (optional) |
193
+ | `companyDescription` | Company description (optional) |
371
194
 
372
- ```js
373
- async function mountRoleplayPage() {
374
- await ensureRoleplaySdkReady();
195
+ ---
375
196
 
376
- const container = document.getElementById("roleplay-root");
377
- SeamlessRoleplay.mount(container);
378
- }
379
- ```
197
+ ## That's it
380
198
 
381
- If the Seamless page or tab is torn down, unmount the SDK:
199
+ - **Provider** wraps your app once handles auth, sessions, cleanup
200
+ - **RoleplayEmbed** is your full Roleplay page — one component
201
+ - **RoleplayDialog** opens per-contact from Contact Search — controlled by a boolean
202
+ - **AddToScenarioDialog** opens for bulk import — controlled by a boolean
382
203
 
383
- ```js
384
- SeamlessRoleplay.unmount();
385
- ```
204
+ All three clean up after themselves. No manual `destroy()`, no `useEffect`, no refs.
386
205
 
387
- ### Embed in Contact Search
206
+ TypeScript types are included — your editor will autocomplete all props.
388
207
 
389
- Use `open(contactData)` when the user clicks a `Roleplay` button from a single contact row on the Contact Search screen.
208
+ ---
390
209
 
391
- ```js
392
- async function openRoleplayForContact(contact) {
393
- await ensureRoleplaySdkReady();
394
-
395
- SeamlessRoleplay.open({
396
- name: contact.name,
397
- domain: contact.domain,
398
- company: contact.company,
399
- title: contact.title,
400
- liUrl: contact.linkedinUrl,
401
- companyDescription: contact.companyDescription,
402
- onCallStarted(data) {
403
- console.log("Roleplay call started", data.callId);
404
- },
405
- onCallEnded(data) {
406
- console.log("Roleplay call ended", data.callId, data.duration);
407
- },
408
- onClose() {
409
- console.log("Roleplay dialog closed");
410
- },
411
- onError(err) {
412
- console.error("Roleplay dialog error", err);
413
- },
414
- });
415
- }
416
- ```
210
+ ## Trial and upgrade handling
417
211
 
418
- The contact object passed to `open(...)` should map to:
212
+ If a user doesn't have a Roleplay subscription, the SDK shows the upgrade/trial UI automatically inside the iframe. No client-side logic needed.
419
213
 
420
- | SDK field | Contact Search value |
421
- |---|---|
422
- | `name` | Contact full name |
423
- | `domain` | Company domain |
424
- | `company` | Company name |
425
- | `title` | Contact title |
426
- | `liUrl` | LinkedIn profile URL, if available |
427
- | `companyDescription` | Company description, if available |
214
+ ---
428
215
 
429
- ### Add contacts to a scenario
216
+ ## Advanced: Vanilla JavaScript
430
217
 
431
- If Seamless wants to send multiple contacts into a scenario picker dialog, use `addToScenario(...)`.
218
+ If you need the vanilla SDK without React:
432
219
 
433
220
  ```js
434
- async function addContactsToScenario(contacts) {
435
- await ensureRoleplaySdkReady();
436
-
437
- SeamlessRoleplay.addToScenario({
438
- contacts: contacts.map((contact) => ({
439
- name: contact.name,
440
- company: contact.company,
441
- title: contact.title,
442
- domain: contact.domain,
443
- liUrl: contact.linkedinUrl,
444
- companyDescription: contact.companyDescription,
445
- })),
446
- onComplete(data) {
447
- console.log("Scenario import complete", data);
448
- },
449
- onClose() {
450
- console.log("Add to scenario dialog closed");
451
- },
452
- onError(err) {
453
- console.error("Add to scenario error", err);
454
- },
455
- });
456
- }
457
- ```
458
-
459
- ### API reference
460
-
461
- #### `SeamlessRoleplay.init(options)`
462
-
463
- Initializes the SDK with the logged-in Seamless user.
221
+ import "@rehers/rehers-roleplay-sdk";
464
222
 
465
- ```js
223
+ // Initialize once
466
224
  SeamlessRoleplay.init({
467
- publishableKey,
468
- userId,
469
- userEmail,
470
- userRole,
471
- onReady,
472
- onError,
225
+ publishableKey: "pk_live_...",
226
+ userToken: "...",
227
+ onReady() { console.log("ready"); },
473
228
  });
474
- ```
475
-
476
- #### `SeamlessRoleplay.mount(container)`
477
-
478
- Mounts the full Roleplay app into a dashboard container.
479
229
 
480
- #### `SeamlessRoleplay.open(contactData)`
230
+ // Full page embed
231
+ SeamlessRoleplay.mount(document.getElementById("container"));
481
232
 
482
- Opens the Roleplay dialog for a single contact.
233
+ // Contact dialog
234
+ SeamlessRoleplay.open({ name: "...", domain: "...", company: "...", title: "..." });
483
235
 
484
- #### `SeamlessRoleplay.addToScenario(options)`
236
+ // Bulk import
237
+ SeamlessRoleplay.addToScenario({ contacts: [...], onComplete(data) { } });
485
238
 
486
- Opens the bulk add-to-scenario dialog for 1 to 25 contacts.
487
-
488
- #### `SeamlessRoleplay.close()`
489
-
490
- Closes the active dialog.
491
-
492
- #### `SeamlessRoleplay.unmount()`
493
-
494
- Unmounts the full-page Roleplay embed.
495
-
496
- #### `SeamlessRoleplay.destroy()`
497
-
498
- Destroys the SDK state, timers, mount, and dialogs.
499
-
500
- ---
501
-
502
- ## Trial and upgrade handling
503
-
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.
505
-
506
- ## TypeScript
507
-
508
- Type declarations are included with the SDK package.
509
-
510
- ```ts
511
- // Vanilla SDK types
512
- import type {
513
- SeamlessRoleplaySDK,
514
- SeamlessRoleplayInitOptions,
515
- SeamlessRoleplayOpenData,
516
- AddToScenarioOptions,
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";
239
+ // Cleanup
240
+ SeamlessRoleplay.close(); // close dialog
241
+ SeamlessRoleplay.unmount(); // remove embed
242
+ SeamlessRoleplay.destroy(); // full cleanup
528
243
  ```
package/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
- export interface SeamlessRoleplayInitOptions {
1
+ interface SeamlessRoleplayInitBase {
2
2
  /** Publishable API key (starts with pk_live_ or pk_test_) */
3
3
  publishableKey: string;
4
- /** Logged-in Seamless user ID. Pass String(me.id) from GET /api/users/me */
5
- userId: string;
6
- /** Logged-in Seamless user email. Pass me.username from GET /api/users/me */
7
- userEmail: string;
8
4
  /** Optional user role for syncing permissions ("owner" | "admin" | "member") */
9
5
  userRole?: "owner" | "admin" | "member";
10
- /** Optional signed JWT for identity verification */
6
+ /** Optional short-lived signed JWT for identity verification */
11
7
  userToken?: string;
12
8
  /** Override the app origin — where the iframe loads from (for dev/testing only) */
13
9
  origin?: string;
@@ -17,6 +13,23 @@ export interface SeamlessRoleplayInitOptions {
17
13
  onError?: (error: { code: string; message: string }) => void;
18
14
  }
19
15
 
16
+ export type SeamlessRoleplayInitOptions =
17
+ | (SeamlessRoleplayInitBase & {
18
+ /** Preferred production auth path: signed Seamless bootstrap token */
19
+ userToken: string;
20
+ /** Optional fallback fields kept for local demos/internal tools */
21
+ userId?: string;
22
+ userEmail?: string;
23
+ })
24
+ | (SeamlessRoleplayInitBase & {
25
+ /** Legacy/demo fallback path when no signed token is available */
26
+ userToken?: string;
27
+ /** Logged-in Seamless user ID. Pass String(me.id) from GET /api/users/me */
28
+ userId: string;
29
+ /** Logged-in Seamless user email. Pass me.username from GET /api/users/me */
30
+ userEmail: string;
31
+ });
32
+
20
33
  export interface SeamlessRoleplayOpenData {
21
34
  /** Full name of the contact */
22
35
  name: string;
@@ -30,10 +43,6 @@ export interface SeamlessRoleplayOpenData {
30
43
  companyDescription?: string;
31
44
  /** Optional LinkedIn profile URL */
32
45
  liUrl?: string;
33
- /** Called when the roleplay call starts */
34
- onCallStarted?: (data: { callId: string }) => void;
35
- /** Called when the roleplay call ends */
36
- onCallEnded?: (data: { callId: string; duration?: number }) => void;
37
46
  /** Called when the dialog/mount is closed */
38
47
  onClose?: () => void;
39
48
  /** Called on error during the session */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
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",
package/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData } from "@rehers/rehers-roleplay-sdk";
2
+ import type { SeamlessRoleplaySDK, AddToScenarioContact, AddToScenarioCompleteData, SeamlessRoleplayInitOptions } from "@rehers/rehers-roleplay-sdk";
3
3
  import "@rehers/rehers-roleplay-sdk";
4
4
  export type { AddToScenarioContact, AddToScenarioCompleteData };
5
5
  interface SeamlessRoleplayContextValue {
@@ -10,20 +10,9 @@ interface SeamlessRoleplayContextValue {
10
10
  } | null;
11
11
  sdk: SeamlessRoleplaySDK;
12
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;
13
+ export type SeamlessRoleplayProviderProps = SeamlessRoleplayInitOptions & {
25
14
  children: ReactNode;
26
- }
15
+ };
27
16
  export declare function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, userRole, userToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
28
17
  export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
29
18
  export interface RoleplayDialogProps {
@@ -34,37 +23,18 @@ export interface RoleplayDialogProps {
34
23
  title: string;
35
24
  companyDescription?: string;
36
25
  liUrl?: string;
37
- onCallStarted?: (data: {
38
- callId: string;
39
- }) => void;
40
- onCallEnded?: (data: {
41
- callId: string;
42
- duration?: number;
43
- }) => void;
44
26
  onClose?: () => void;
45
27
  onError?: (data: {
46
28
  code: string;
47
29
  message: string;
48
30
  }) => void;
49
31
  }
50
- export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }: RoleplayDialogProps): null;
32
+ export declare function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onClose, onError, }: RoleplayDialogProps): null;
51
33
  export interface RoleplayEmbedProps {
52
34
  className?: string;
53
35
  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
36
  }
67
- export declare function RoleplayEmbed({ className, style, onCallStarted, onCallEnded, onClose, onError, }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
37
+ export declare function RoleplayEmbed({ className, style }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
68
38
  export interface AddToScenarioDialogProps {
69
39
  open: boolean;
70
40
  contacts: AddToScenarioContact[];
package/react.js CHANGED
@@ -36,28 +36,51 @@ export function SeamlessRoleplayProvider({ publishableKey, userId, userEmail, us
36
36
  }
37
37
  mountedRef.current = true;
38
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
- });
39
+ const initOptions = userToken
40
+ ? {
41
+ publishableKey,
42
+ userToken,
43
+ userId,
44
+ userEmail,
45
+ userRole,
46
+ origin,
47
+ onReady: () => {
48
+ var _a;
49
+ if (!mountedRef.current)
50
+ return;
51
+ setState({ isReady: true, error: null });
52
+ (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
53
+ },
54
+ onError: (err) => {
55
+ var _a;
56
+ if (!mountedRef.current)
57
+ return;
58
+ setState({ isReady: false, error: err });
59
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
60
+ },
61
+ }
62
+ : {
63
+ publishableKey,
64
+ userId: userId || "",
65
+ userEmail: userEmail || "",
66
+ userRole,
67
+ origin,
68
+ onReady: () => {
69
+ var _a;
70
+ if (!mountedRef.current)
71
+ return;
72
+ setState({ isReady: true, error: null });
73
+ (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
74
+ },
75
+ onError: (err) => {
76
+ var _a;
77
+ if (!mountedRef.current)
78
+ return;
79
+ setState({ isReady: false, error: err });
80
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, err);
81
+ },
82
+ };
83
+ sdk.init(initOptions);
61
84
  return () => {
62
85
  mountedRef.current = false;
63
86
  providerMountCount--;
@@ -75,10 +98,8 @@ export function useSeamlessRoleplay() {
75
98
  }
76
99
  return ctx;
77
100
  }
78
- export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onCallStarted, onCallEnded, onClose, onError, }) {
101
+ export function RoleplayDialog({ open: isOpen, name, domain, company, title, companyDescription, liUrl, onClose, onError, }) {
79
102
  const { isReady, sdk } = useSeamlessRoleplay();
80
- const onCallStartedRef = useCallbackRef(onCallStarted);
81
- const onCallEndedRef = useCallbackRef(onCallEnded);
82
103
  const onCloseRef = useCallbackRef(onClose);
83
104
  const onErrorRef = useCallbackRef(onError);
84
105
  const isOpenRef = useRef(false);
@@ -94,8 +115,6 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
94
115
  title,
95
116
  companyDescription,
96
117
  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
118
  onClose: () => {
100
119
  var _a;
101
120
  isOpenRef.current = false;
@@ -117,63 +136,14 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
117
136
  }, [isReady, isOpen, name, domain, company, title, companyDescription, liUrl, sdk]);
118
137
  return null;
119
138
  }
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, }) {
139
+ export function RoleplayEmbed({ className, style }) {
125
140
  const { isReady, sdk } = useSeamlessRoleplay();
126
141
  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
142
  useEffect(() => {
132
143
  if (!isReady || !containerRef.current)
133
144
  return;
134
145
  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
146
  return () => {
175
- if (listener)
176
- window.removeEventListener("message", listener);
177
147
  sdk.unmount();
178
148
  };
179
149
  }, [isReady, sdk]);
package/roleplay-sdk.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Publishable-key auth model. No build step required.
5
5
  *
6
6
  * Usage:
7
- * SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userId: 'user_789' });
7
+ * SeamlessRoleplay.init({ publishableKey: 'pk_live_...', userToken: 'jwt...' });
8
8
  * SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
9
9
  */
10
10
  (function () {
@@ -35,7 +35,7 @@
35
35
  // ── Mount state (persistent embed — survives dialog open/close) ───
36
36
  var mountIframe = null;
37
37
  var mountContainer = null;
38
- var mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
38
+ var mountCallbacks = { onClose: null, onError: null };
39
39
  var mountListener = null;
40
40
 
41
41
  // ── Dialog state (overlay — dialog or add-to-scenario) ────────────
@@ -43,7 +43,7 @@
43
43
  var dialogIframe = null;
44
44
  var dialogMode = null; // "dialog" | "add-to-scenario"
45
45
  var dialogContactData = null;
46
- var dialogCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
46
+ var dialogCallbacks = { onClose: null, onError: null };
47
47
  var dialogAddToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
48
48
  var dialogAddToScenarioPendingContacts = null;
49
49
  var dialogListener = null;
@@ -71,6 +71,16 @@
71
71
  return DEFAULT_API_ORIGIN;
72
72
  }
73
73
 
74
+ function buildIframeSrc(path) {
75
+ var targetPath = path || "/embed/roleplay-call";
76
+ var url = new URL(targetPath, getOrigin());
77
+ // Force the embedded app to drop any stale auth before it handles the
78
+ // SDK session-init message. Without this, hosted app sessions can leak
79
+ // through when switching users or entering trial mode in the demo.
80
+ url.searchParams.set("seamlessResetAuth", "1");
81
+ return url.toString();
82
+ }
83
+
74
84
  function sendMsg(iframeEl, msg) {
75
85
  try {
76
86
  if (iframeEl && iframeEl.contentWindow) {
@@ -88,10 +98,9 @@
88
98
 
89
99
  fetchingSession = new Promise(function (resolve, reject) {
90
100
  var url = getApiOrigin() + "/api/sdk/session";
91
- var body = { userId: userId };
92
- if (userEmail) body.userEmail = userEmail;
93
- if (userRole) body.userRole = userRole;
94
- if (userToken) body.userToken = userToken;
101
+ var body = userToken
102
+ ? { userToken: userToken }
103
+ : { userId: userId, userEmail: userEmail, userRole: userRole };
95
104
 
96
105
  var xhr = new XMLHttpRequest();
97
106
  xhr.open("POST", url, true);
@@ -199,7 +208,7 @@
199
208
  dialogContactData = null;
200
209
  dialogAddToScenarioPendingContacts = null;
201
210
  dialogMode = null;
202
- dialogCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
211
+ dialogCallbacks = { onClose: null, onError: null };
203
212
  dialogAddToScenarioCallbacks = { onComplete: null, onClose: null, onError: null };
204
213
  }
205
214
 
@@ -218,7 +227,7 @@
218
227
 
219
228
  mountIframe = null;
220
229
  mountContainer = null;
221
- mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
230
+ mountCallbacks = { onClose: null, onError: null };
222
231
  }
223
232
 
224
233
  // ── Message dispatch ──────────────────────────────────────────────
@@ -284,18 +293,6 @@
284
293
  });
285
294
  break;
286
295
 
287
- case "ROLEPLAY_CALL_STARTED":
288
- if (mountCallbacks.onCallStarted) {
289
- mountCallbacks.onCallStarted({ callId: data.callId });
290
- }
291
- break;
292
-
293
- case "ROLEPLAY_CALL_ENDED":
294
- if (mountCallbacks.onCallEnded) {
295
- mountCallbacks.onCallEnded({ callId: data.callId, duration: data.duration });
296
- }
297
- break;
298
-
299
296
  case "ROLEPLAY_ERROR":
300
297
  if (mountCallbacks.onError) {
301
298
  mountCallbacks.onError({ code: data.code, message: data.message });
@@ -334,18 +331,6 @@
334
331
  });
335
332
  break;
336
333
 
337
- case "ROLEPLAY_CALL_STARTED":
338
- if (dialogCallbacks.onCallStarted) {
339
- dialogCallbacks.onCallStarted({ callId: data.callId });
340
- }
341
- break;
342
-
343
- case "ROLEPLAY_CALL_ENDED":
344
- if (dialogCallbacks.onCallEnded) {
345
- dialogCallbacks.onCallEnded({ callId: data.callId, duration: data.duration });
346
- }
347
- break;
348
-
349
334
  case "ROLEPLAY_ERROR":
350
335
  if (dialogCallbacks.onError) {
351
336
  dialogCallbacks.onError({ code: data.code, message: data.message });
@@ -405,7 +390,7 @@
405
390
 
406
391
  function createIframe(path) {
407
392
  var iframeEl = document.createElement("iframe");
408
- iframeEl.src = getOrigin() + (path || "/embed/roleplay-call");
393
+ iframeEl.src = buildIframeSrc(path);
409
394
  iframeEl.allow = "camera; microphone; display-capture; autoplay";
410
395
  iframeEl.style.width = "100%";
411
396
  iframeEl.style.height = "100%";
@@ -422,8 +407,18 @@
422
407
  */
423
408
  init: function (opts) {
424
409
  try {
425
- if (!opts || !opts.publishableKey || !opts.userId || !opts.userEmail) {
426
- logError("init", "requires { publishableKey, userId, userEmail }");
410
+ var hasUserToken = !!(opts && opts.userToken && String(opts.userToken).trim());
411
+ var hasLegacyIdentity = !!(opts && opts.userId && opts.userEmail);
412
+
413
+ if (!opts || !opts.publishableKey || (!hasUserToken && !hasLegacyIdentity)) {
414
+ var error = {
415
+ code: "INVALID_INIT",
416
+ message: "requires { publishableKey, userToken } or legacy { publishableKey, userId, userEmail }",
417
+ };
418
+ logError("init", error.message);
419
+ if (opts && typeof opts.onError === "function") {
420
+ try { opts.onError(error); } catch (_) {}
421
+ }
427
422
  return;
428
423
  }
429
424
 
@@ -435,9 +430,10 @@
435
430
  fetchingSession = null;
436
431
  sessionToken = null;
437
432
  sessionExpiresAt = 0;
433
+ paymentLink = null;
438
434
 
439
435
  publishableKey = opts.publishableKey;
440
- userId = opts.userId;
436
+ userId = opts.userId || null;
441
437
  userEmail = opts.userEmail || null;
442
438
  userRole = opts.userRole || null;
443
439
  userToken = opts.userToken || null;
@@ -486,8 +482,6 @@
486
482
  if (dialogOverlay || dialogIframe) teardownDialog();
487
483
 
488
484
  dialogContactData = data;
489
- dialogCallbacks.onCallStarted = data.onCallStarted || null;
490
- dialogCallbacks.onCallEnded = data.onCallEnded || null;
491
485
  dialogCallbacks.onClose = data.onClose || null;
492
486
  dialogCallbacks.onError = data.onError || null;
493
487
  dialogMode = "dialog";
@@ -585,7 +579,7 @@
585
579
  // Tear down any existing mount (re-mount)
586
580
  if (mountIframe) teardownMount();
587
581
 
588
- mountCallbacks = { onCallStarted: null, onCallEnded: null, onClose: null, onError: null };
582
+ mountCallbacks = { onClose: null, onError: null };
589
583
  mountContainer = container;
590
584
 
591
585
  // Listen for messages
@@ -676,7 +670,7 @@
676
670
  cs.boxShadow = "0 25px 60px rgba(0,0,0,0.3)";
677
671
 
678
672
  var iframeEl = document.createElement("iframe");
679
- iframeEl.src = getOrigin() + "/embed/add-to-scenario";
673
+ iframeEl.src = buildIframeSrc("/embed/add-to-scenario");
680
674
  iframeEl.style.width = "100%";
681
675
  iframeEl.style.height = "100%";
682
676
  iframeEl.style.border = "none";