@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 +149 -434
- package/index.d.ts +19 -10
- package/package.json +1 -1
- package/react.d.ts +5 -35
- package/react.js +47 -77
- package/roleplay-sdk.js +35 -41
package/README.md
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
# Roleplay SDK for
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
## 1. Wrap your app with the Provider
|
|
35
14
|
|
|
36
|
-
|
|
15
|
+
Add this once, above all your routes. It initializes the SDK for the logged-in Seamless user.
|
|
37
16
|
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
66
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
## 3. Roleplay dialog on Contact Search
|
|
98
84
|
|
|
99
|
-
|
|
85
|
+
When a user clicks "Roleplay" on a contact row, a dialog opens over the page.
|
|
100
86
|
|
|
101
87
|
```tsx
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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={
|
|
111
|
-
name={
|
|
112
|
-
domain={
|
|
113
|
-
company={
|
|
114
|
-
title={
|
|
115
|
-
liUrl={
|
|
116
|
-
companyDescription={
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
## 4. Add to Scenario (bulk)
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
When users select multiple contacts and click "Add to Scenario", a scenario picker dialog opens.
|
|
138
136
|
|
|
139
137
|
```tsx
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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={
|
|
149
|
-
contacts={selectedContacts.map((
|
|
150
|
-
name:
|
|
151
|
-
company:
|
|
152
|
-
title:
|
|
153
|
-
domain:
|
|
154
|
-
liUrl:
|
|
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(
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
264
|
-
import SeamlessRoleplay from "@rehers/rehers-roleplay-sdk";
|
|
265
|
-
```
|
|
184
|
+
When passing contact data to `RoleplayDialog` or `AddToScenarioDialog`:
|
|
266
185
|
|
|
267
|
-
|
|
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
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
373
|
-
async function mountRoleplayPage() {
|
|
374
|
-
await ensureRoleplaySdkReady();
|
|
195
|
+
---
|
|
375
196
|
|
|
376
|
-
|
|
377
|
-
SeamlessRoleplay.mount(container);
|
|
378
|
-
}
|
|
379
|
-
```
|
|
197
|
+
## That's it
|
|
380
198
|
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
SeamlessRoleplay.unmount();
|
|
385
|
-
```
|
|
204
|
+
All three clean up after themselves. No manual `destroy()`, no `useEffect`, no refs.
|
|
386
205
|
|
|
387
|
-
|
|
206
|
+
TypeScript types are included — your editor will autocomplete all props.
|
|
388
207
|
|
|
389
|
-
|
|
208
|
+
---
|
|
390
209
|
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
## Advanced: Vanilla JavaScript
|
|
430
217
|
|
|
431
|
-
If
|
|
218
|
+
If you need the vanilla SDK without React:
|
|
432
219
|
|
|
433
220
|
```js
|
|
434
|
-
|
|
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
|
-
|
|
223
|
+
// Initialize once
|
|
466
224
|
SeamlessRoleplay.init({
|
|
467
|
-
publishableKey,
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
230
|
+
// Full page embed
|
|
231
|
+
SeamlessRoleplay.mount(document.getElementById("container"));
|
|
481
232
|
|
|
482
|
-
|
|
233
|
+
// Contact dialog
|
|
234
|
+
SeamlessRoleplay.open({ name: "...", domain: "...", company: "...", title: "..." });
|
|
483
235
|
|
|
484
|
-
|
|
236
|
+
// Bulk import
|
|
237
|
+
SeamlessRoleplay.addToScenario({ contacts: [...], onComplete(data) { } });
|
|
485
238
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
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
|
-
|
|
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_...',
|
|
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 = {
|
|
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 = {
|
|
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 =
|
|
92
|
-
|
|
93
|
-
|
|
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 = {
|
|
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 = {
|
|
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 =
|
|
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
|
-
|
|
426
|
-
|
|
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 = {
|
|
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 =
|
|
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";
|