@rehers/rehers-roleplay-sdk 2.5.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +22 -6
- package/index.d.ts +24 -4
- package/package.json +5 -3
- package/react.d.ts +10 -3
- package/react.js +78 -24
- package/roleplay-sdk.js +507 -96
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ Add this once, above all your routes. It initializes the SDK for the logged-in S
|
|
|
17
17
|
Production flow:
|
|
18
18
|
|
|
19
19
|
1. Your backend requests a short-lived `userToken` from `POST /api/seamless/auth/user-token`
|
|
20
|
-
2. Your frontend
|
|
21
|
-
3.
|
|
20
|
+
2. Your frontend provides a `getUserToken()` callback that calls your backend route
|
|
21
|
+
3. The SDK calls `getUserToken()` on startup and before the iframe session expires
|
|
22
22
|
|
|
23
23
|
The browser should not mint sessions from raw identity fields in production.
|
|
24
24
|
|
|
@@ -26,11 +26,18 @@ The browser should not mint sessions from raw identity fields in production.
|
|
|
26
26
|
import { SeamlessRoleplayProvider } from "@rehers/rehers-roleplay-sdk/react";
|
|
27
27
|
|
|
28
28
|
function App() {
|
|
29
|
-
|
|
29
|
+
async function getUserToken() {
|
|
30
|
+
const tokenRes = await fetch("/api/roleplay/user-token", {
|
|
31
|
+
method: "POST",
|
|
32
|
+
credentials: "include",
|
|
33
|
+
}).then((r) => r.json());
|
|
34
|
+
|
|
35
|
+
return tokenRes.userToken;
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
39
|
<SeamlessRoleplayProvider
|
|
33
|
-
|
|
40
|
+
getUserToken={getUserToken}
|
|
34
41
|
onReady={() => console.log("Roleplay SDK ready")}
|
|
35
42
|
onError={(err) => console.error("Roleplay SDK error", err)}
|
|
36
43
|
>
|
|
@@ -55,7 +62,9 @@ const tokenRes = await fetch("/api/roleplay/user-token", {
|
|
|
55
62
|
const userToken = tokenRes.userToken;
|
|
56
63
|
```
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
The SDK owns session refresh timing and will call `getUserToken()` again before
|
|
66
|
+
the embedded app session expires. That's the only setup. Everything below just
|
|
67
|
+
works.
|
|
59
68
|
|
|
60
69
|
---
|
|
61
70
|
|
|
@@ -221,7 +230,14 @@ import "@rehers/rehers-roleplay-sdk";
|
|
|
221
230
|
|
|
222
231
|
// Initialize once
|
|
223
232
|
SeamlessRoleplay.init({
|
|
224
|
-
|
|
233
|
+
getUserToken: async () => {
|
|
234
|
+
const res = await fetch("/api/roleplay/user-token", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
credentials: "include",
|
|
237
|
+
});
|
|
238
|
+
const data = await res.json();
|
|
239
|
+
return data.userToken;
|
|
240
|
+
},
|
|
225
241
|
onReady() { console.log("ready"); },
|
|
226
242
|
});
|
|
227
243
|
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
export type SeamlessRoleplayUserTokenProvider = () => string | Promise<string>;
|
|
2
|
+
|
|
1
3
|
interface SeamlessRoleplayInitBase {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Returns a fresh short-lived signed user JWT minted by your backend for the
|
|
6
|
+
* currently signed-in Seamless user. The SDK calls this on startup and before
|
|
7
|
+
* its iframe session expires.
|
|
8
|
+
*/
|
|
9
|
+
getUserToken: SeamlessRoleplayUserTokenProvider;
|
|
4
10
|
/** Override the app origin — where the iframe loads from (for dev/testing only) */
|
|
5
11
|
origin?: string;
|
|
6
12
|
/** Called when the SDK session is ready */
|
|
@@ -64,18 +70,32 @@ export interface AddToScenarioOptions {
|
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
export interface SeamlessRoleplaySDK {
|
|
67
|
-
/** Initialize the SDK with a
|
|
73
|
+
/** Initialize the SDK with a host-provided user token callback. */
|
|
68
74
|
init(options: SeamlessRoleplayInitOptions): void;
|
|
69
75
|
/** Open the roleplay modal for a contact (dialog mode). */
|
|
70
76
|
open(data: SeamlessRoleplayOpenData): void;
|
|
71
77
|
/** Mount the full Roleplay app into a container. */
|
|
72
|
-
mount(
|
|
78
|
+
mount(
|
|
79
|
+
container: HTMLElement,
|
|
80
|
+
options?: {
|
|
81
|
+
/** Called if the embedded app closes itself. */
|
|
82
|
+
onClose?: () => void;
|
|
83
|
+
/** Called if the embedded app reports an error. */
|
|
84
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
85
|
+
}
|
|
86
|
+
): void;
|
|
73
87
|
/** Open the add-to-scenario dialog for bulk contact import. */
|
|
74
88
|
addToScenario(options: AddToScenarioOptions): void;
|
|
75
89
|
/** Close the active dialog. Does not affect mount. */
|
|
76
90
|
close(): void;
|
|
77
91
|
/** Unmount the mounted embed. Does not affect dialogs. */
|
|
78
92
|
unmount(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Force a session re-resolution — e.g. after the user completes payment in
|
|
95
|
+
* another tab. Re-fetches and broadcasts the session to any mounted embed or
|
|
96
|
+
* open dialog. (The SDK also auto-recovers when the tab is refocused.)
|
|
97
|
+
*/
|
|
98
|
+
reauth(): void;
|
|
79
99
|
/** Destroy the SDK — clears state, timers, and DOM (both mount and dialogs). */
|
|
80
100
|
destroy(): void;
|
|
81
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehers/rehers-roleplay-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"sideEffects": true,
|
|
5
5
|
"description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
|
|
6
6
|
"main": "roleplay-sdk.js",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"roleplay-sdk.js",
|
|
20
20
|
"index.d.ts",
|
|
21
21
|
"react.js",
|
|
22
|
-
"react.d.ts"
|
|
22
|
+
"react.d.ts",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"NOTICE"
|
|
23
25
|
],
|
|
24
26
|
"peerDependencies": {
|
|
25
27
|
"react": ">=18.0.0",
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
"react-dom": { "optional": true }
|
|
31
33
|
},
|
|
32
34
|
"keywords": ["seamless", "roleplay", "sdk", "sales", "training", "react"],
|
|
33
|
-
"license": "
|
|
35
|
+
"license": "Apache-2.0",
|
|
34
36
|
"repository": {
|
|
35
37
|
"type": "git",
|
|
36
38
|
"url": "git+https://github.com/rehers/seamless-frontend-independent.git"
|
package/react.d.ts
CHANGED
|
@@ -8,12 +8,12 @@ interface SeamlessRoleplayContextValue {
|
|
|
8
8
|
code: string;
|
|
9
9
|
message: string;
|
|
10
10
|
} | null;
|
|
11
|
-
sdk: SeamlessRoleplaySDK;
|
|
11
|
+
sdk: SeamlessRoleplaySDK | null;
|
|
12
12
|
}
|
|
13
13
|
export type SeamlessRoleplayProviderProps = SeamlessRoleplayInitOptions & {
|
|
14
14
|
children: ReactNode;
|
|
15
15
|
};
|
|
16
|
-
export declare function SeamlessRoleplayProvider({
|
|
16
|
+
export declare function SeamlessRoleplayProvider({ getUserToken, origin, onReady, onError, children, }: SeamlessRoleplayProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
17
17
|
export declare function useSeamlessRoleplay(): SeamlessRoleplayContextValue;
|
|
18
18
|
export interface RoleplayDialogProps {
|
|
19
19
|
open: boolean;
|
|
@@ -33,8 +33,15 @@ export declare function RoleplayDialog({ open: isOpen, name, domain, company, ti
|
|
|
33
33
|
export interface RoleplayEmbedProps {
|
|
34
34
|
className?: string;
|
|
35
35
|
style?: React.CSSProperties;
|
|
36
|
+
/** Called if the embedded app reports an error */
|
|
37
|
+
onError?: (error: {
|
|
38
|
+
code: string;
|
|
39
|
+
message: string;
|
|
40
|
+
}) => void;
|
|
41
|
+
/** Called if the embedded app closes itself */
|
|
42
|
+
onClose?: () => void;
|
|
36
43
|
}
|
|
37
|
-
export declare function RoleplayEmbed({ className, style }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
export declare function RoleplayEmbed({ className, style, onError, onClose }: RoleplayEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
38
45
|
export interface AddToScenarioDialogProps {
|
|
39
46
|
open: boolean;
|
|
40
47
|
contacts: AddToScenarioContact[];
|
package/react.js
CHANGED
|
@@ -12,23 +12,56 @@ function useCallbackRef(cb) {
|
|
|
12
12
|
});
|
|
13
13
|
return ref;
|
|
14
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
15
|
const SeamlessRoleplayContext = createContext(null);
|
|
24
16
|
let providerMountCount = 0;
|
|
25
|
-
export function SeamlessRoleplayProvider({
|
|
17
|
+
export function SeamlessRoleplayProvider({ getUserToken, origin, onReady, onError, children, }) {
|
|
26
18
|
const [state, setState] = useState({ isReady: false, error: null });
|
|
27
19
|
const mountedRef = useRef(false);
|
|
28
20
|
const onReadyRef = useCallbackRef(onReady);
|
|
29
21
|
const onErrorRef = useCallbackRef(onError);
|
|
30
|
-
const
|
|
22
|
+
const getUserTokenRef = useCallbackRef(getUserToken);
|
|
23
|
+
const [sdk, setSdk] = useState(null);
|
|
24
|
+
// Resolve the SDK on the CLIENT only. `window` is absent during SSR, and an
|
|
25
|
+
// async <script> tag may not have executed at first render — so we never call
|
|
26
|
+
// getSDK() during render (that would throw and break SSR/hydration). Poll
|
|
27
|
+
// briefly for an async load, and surface a clear error if it never appears.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
const resolve = () => {
|
|
31
|
+
if (typeof window !== "undefined" && window.SeamlessRoleplay) {
|
|
32
|
+
if (!cancelled)
|
|
33
|
+
setSdk(window.SeamlessRoleplay);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
|
+
if (resolve())
|
|
39
|
+
return;
|
|
40
|
+
const poll = setInterval(() => {
|
|
41
|
+
if (resolve())
|
|
42
|
+
clearInterval(poll);
|
|
43
|
+
}, 50);
|
|
44
|
+
const giveUp = setTimeout(() => {
|
|
45
|
+
clearInterval(poll);
|
|
46
|
+
if (!cancelled && !(typeof window !== "undefined" && window.SeamlessRoleplay)) {
|
|
47
|
+
setState({
|
|
48
|
+
isReady: false,
|
|
49
|
+
error: {
|
|
50
|
+
code: "SDK_NOT_LOADED",
|
|
51
|
+
message: "@rehers/rehers-roleplay-sdk was not found on window. Make sure it is installed and loaded before the React wrapper.",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}, 10000);
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
clearInterval(poll);
|
|
59
|
+
clearTimeout(giveUp);
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
31
62
|
useEffect(() => {
|
|
63
|
+
if (!sdk)
|
|
64
|
+
return;
|
|
32
65
|
providerMountCount++;
|
|
33
66
|
if (providerMountCount > 1 && process.env.NODE_ENV !== "production") {
|
|
34
67
|
console.warn("[SeamlessRoleplay/React] Multiple SeamlessRoleplayProvider instances detected. " +
|
|
@@ -37,7 +70,15 @@ export function SeamlessRoleplayProvider({ userToken, origin, onReady, onError,
|
|
|
37
70
|
mountedRef.current = true;
|
|
38
71
|
setState({ isReady: false, error: null });
|
|
39
72
|
const initOptions = {
|
|
40
|
-
|
|
73
|
+
// Ref-guarded so an inline `getUserToken` prop (the common, default way to
|
|
74
|
+
// write it) does NOT re-init the SDK on every parent render. init runs
|
|
75
|
+
// once per SDK/origin, not once per render.
|
|
76
|
+
getUserToken: () => {
|
|
77
|
+
const fn = getUserTokenRef.current;
|
|
78
|
+
if (!fn)
|
|
79
|
+
return Promise.reject(new Error("getUserToken is not available"));
|
|
80
|
+
return fn();
|
|
81
|
+
},
|
|
41
82
|
origin,
|
|
42
83
|
onReady: () => {
|
|
43
84
|
var _a;
|
|
@@ -60,7 +101,7 @@ export function SeamlessRoleplayProvider({ userToken, origin, onReady, onError,
|
|
|
60
101
|
providerMountCount--;
|
|
61
102
|
sdk.destroy();
|
|
62
103
|
};
|
|
63
|
-
}, [sdk,
|
|
104
|
+
}, [sdk, origin]);
|
|
64
105
|
const contextValue = useMemo(() => ({ ...state, sdk }), [state, sdk]);
|
|
65
106
|
return (_jsx(SeamlessRoleplayContext.Provider, { value: contextValue, children: children }));
|
|
66
107
|
}
|
|
@@ -77,18 +118,26 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
|
|
|
77
118
|
const onCloseRef = useCallbackRef(onClose);
|
|
78
119
|
const onErrorRef = useCallbackRef(onError);
|
|
79
120
|
const isOpenRef = useRef(false);
|
|
121
|
+
// Keep the latest contact data in a ref so changing a field while the dialog
|
|
122
|
+
// is open does NOT re-run the effect and tear down / restart the live call.
|
|
123
|
+
// The dialog opens once per open-transition; data is read at open time.
|
|
124
|
+
const dataRef = useRef({ name, domain, company, title, companyDescription, liUrl });
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
dataRef.current = { name, domain, company, title, companyDescription, liUrl };
|
|
127
|
+
});
|
|
80
128
|
useEffect(() => {
|
|
81
|
-
if (!isReady)
|
|
129
|
+
if (!isReady || !sdk)
|
|
82
130
|
return;
|
|
83
131
|
if (isOpen) {
|
|
84
132
|
isOpenRef.current = true;
|
|
133
|
+
const d = dataRef.current;
|
|
85
134
|
sdk.open({
|
|
86
|
-
name,
|
|
87
|
-
domain,
|
|
88
|
-
company,
|
|
89
|
-
title,
|
|
90
|
-
companyDescription,
|
|
91
|
-
liUrl,
|
|
135
|
+
name: d.name,
|
|
136
|
+
domain: d.domain,
|
|
137
|
+
company: d.company,
|
|
138
|
+
title: d.title,
|
|
139
|
+
companyDescription: d.companyDescription,
|
|
140
|
+
liUrl: d.liUrl,
|
|
92
141
|
onClose: () => {
|
|
93
142
|
var _a;
|
|
94
143
|
isOpenRef.current = false;
|
|
@@ -107,16 +156,21 @@ export function RoleplayDialog({ open: isOpen, name, domain, company, title, com
|
|
|
107
156
|
sdk.close();
|
|
108
157
|
}
|
|
109
158
|
};
|
|
110
|
-
}, [isReady, isOpen,
|
|
159
|
+
}, [isReady, isOpen, sdk]);
|
|
111
160
|
return null;
|
|
112
161
|
}
|
|
113
|
-
export function RoleplayEmbed({ className, style }) {
|
|
162
|
+
export function RoleplayEmbed({ className, style, onError, onClose }) {
|
|
114
163
|
const { isReady, sdk } = useSeamlessRoleplay();
|
|
115
164
|
const containerRef = useRef(null);
|
|
165
|
+
const onErrorRef = useCallbackRef(onError);
|
|
166
|
+
const onCloseRef = useCallbackRef(onClose);
|
|
116
167
|
useEffect(() => {
|
|
117
|
-
if (!isReady || !containerRef.current)
|
|
168
|
+
if (!isReady || !sdk || !containerRef.current)
|
|
118
169
|
return;
|
|
119
|
-
sdk.mount(containerRef.current
|
|
170
|
+
sdk.mount(containerRef.current, {
|
|
171
|
+
onError: (e) => { var _a; return (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, e); },
|
|
172
|
+
onClose: () => { var _a; return (_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef); },
|
|
173
|
+
});
|
|
120
174
|
return () => {
|
|
121
175
|
sdk.unmount();
|
|
122
176
|
};
|
|
@@ -134,7 +188,7 @@ export function AddToScenarioDialog({ open: isOpen, contacts, onComplete, onClos
|
|
|
134
188
|
contactsRef.current = contacts;
|
|
135
189
|
});
|
|
136
190
|
useEffect(() => {
|
|
137
|
-
if (!isReady)
|
|
191
|
+
if (!isReady || !sdk)
|
|
138
192
|
return;
|
|
139
193
|
if (isOpen) {
|
|
140
194
|
isOpenRef.current = true;
|
package/roleplay-sdk.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SeamlessRoleplay SDK
|
|
2
|
+
* SeamlessRoleplay SDK v3
|
|
3
3
|
*
|
|
4
4
|
* User-token auth model. No build step required.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* SeamlessRoleplay.init({
|
|
7
|
+
* SeamlessRoleplay.init({ getUserToken: async () => 'jwt...' });
|
|
8
8
|
* SeamlessRoleplay.open({ name: '...', domain: '...', company: '...', title: '...' });
|
|
9
9
|
*/
|
|
10
10
|
(function () {
|
|
@@ -16,16 +16,22 @@
|
|
|
16
16
|
var SDK_LOG_PREFIX = "[SeamlessRoleplay]";
|
|
17
17
|
|
|
18
18
|
// ── Auth state ────────────────────────────────────────────────────
|
|
19
|
-
var
|
|
19
|
+
var getUserToken = null;
|
|
20
|
+
var latestUserToken = null;
|
|
21
|
+
var userTokenRequest = null;
|
|
20
22
|
var paymentLink = null;
|
|
21
23
|
var appOrigin = null;
|
|
22
24
|
|
|
23
25
|
var sessionToken = null;
|
|
24
26
|
var sessionExpiresAt = 0; // epoch ms
|
|
27
|
+
var sessionRefreshBufferMs = 30000;
|
|
25
28
|
var refreshTimer = null;
|
|
29
|
+
var refreshRetryDelayMs = 5000;
|
|
26
30
|
var fetchingSession = null; // single-flight Promise
|
|
27
31
|
var activeSessionXhr = null;
|
|
28
32
|
var activeInitVersion = 0;
|
|
33
|
+
var sessionRefreshExpiredNotified = false;
|
|
34
|
+
var visibilityListener = null;
|
|
29
35
|
|
|
30
36
|
var initCallbacks = { onReady: null, onError: null };
|
|
31
37
|
var initCalled = false;
|
|
@@ -47,6 +53,11 @@
|
|
|
47
53
|
var dialogListener = null;
|
|
48
54
|
var dialogCloseTeardownTimer = null;
|
|
49
55
|
|
|
56
|
+
// ── Dialog accessibility state ────────────────────────────────────
|
|
57
|
+
var dialogPreviousFocus = null; // element to restore focus to on close
|
|
58
|
+
var dialogKeydownListener = null; // Escape handler
|
|
59
|
+
var dialogInertedSiblings = null; // background nodes hidden while modal is open
|
|
60
|
+
|
|
50
61
|
// ── Safe logging ──────────────────────────────────────────────────
|
|
51
62
|
|
|
52
63
|
function logError(method, err) {
|
|
@@ -69,6 +80,31 @@
|
|
|
69
80
|
return DEFAULT_API_ORIGIN;
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
// The app origin is used BOTH as the iframe src and as the postMessage target
|
|
84
|
+
// for the session JWT, so an untrusted override would exfiltrate the token.
|
|
85
|
+
// Only allow the production app domain (apex/subdomains, https) or localhost.
|
|
86
|
+
function isAllowedAppOrigin(origin) {
|
|
87
|
+
try {
|
|
88
|
+
var u = new URL(origin);
|
|
89
|
+
if (
|
|
90
|
+
u.protocol === "https:" &&
|
|
91
|
+
(u.hostname === "roleplaywithseamless.ai" ||
|
|
92
|
+
u.hostname.endsWith(".roleplaywithseamless.ai"))
|
|
93
|
+
) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
(u.protocol === "http:" || u.protocol === "https:") &&
|
|
98
|
+
(u.hostname === "localhost" || u.hostname === "127.0.0.1")
|
|
99
|
+
) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
function buildIframeSrc(path) {
|
|
73
109
|
var targetPath = path || "/embed/roleplay-call";
|
|
74
110
|
var url = new URL(targetPath, getOrigin());
|
|
@@ -89,6 +125,16 @@
|
|
|
89
125
|
}
|
|
90
126
|
}
|
|
91
127
|
|
|
128
|
+
function normalizeToken(value) {
|
|
129
|
+
if (typeof value !== "string") return null;
|
|
130
|
+
var trimmed = value.trim();
|
|
131
|
+
return trimmed ? trimmed : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function makeError(code, message) {
|
|
135
|
+
return { code: code, message: message };
|
|
136
|
+
}
|
|
137
|
+
|
|
92
138
|
// ── Session management ────────────────────────────────────────────
|
|
93
139
|
|
|
94
140
|
function clearSessionRequest() {
|
|
@@ -101,88 +147,262 @@
|
|
|
101
147
|
fetchingSession = null;
|
|
102
148
|
}
|
|
103
149
|
|
|
104
|
-
function
|
|
150
|
+
function resolveUserToken() {
|
|
151
|
+
if (typeof getUserToken !== "function") {
|
|
152
|
+
return Promise.reject(
|
|
153
|
+
makeError("INVALID_INIT", "requires { getUserToken }")
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (userTokenRequest) return userTokenRequest;
|
|
158
|
+
|
|
159
|
+
userTokenRequest = Promise.resolve()
|
|
160
|
+
.then(function () {
|
|
161
|
+
return getUserToken();
|
|
162
|
+
})
|
|
163
|
+
.then(
|
|
164
|
+
function (value) {
|
|
165
|
+
userTokenRequest = null;
|
|
166
|
+
var token = normalizeToken(value);
|
|
167
|
+
if (!token) {
|
|
168
|
+
throw makeError(
|
|
169
|
+
"USER_TOKEN_ERROR",
|
|
170
|
+
"getUserToken() did not return a valid userToken"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
latestUserToken = token;
|
|
174
|
+
return token;
|
|
175
|
+
},
|
|
176
|
+
function (err) {
|
|
177
|
+
userTokenRequest = null;
|
|
178
|
+
throw makeError(
|
|
179
|
+
(err && err.code) || "USER_TOKEN_ERROR",
|
|
180
|
+
(err && err.message) || "Failed to get a fresh userToken"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return userTokenRequest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function dispatchRefreshToTarget(targetIframe) {
|
|
189
|
+
if (!sessionToken) return;
|
|
190
|
+
sendMsg(targetIframe, {
|
|
191
|
+
type: "seamless-session-refresh",
|
|
192
|
+
sessionToken: sessionToken,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function broadcastSessionRefresh() {
|
|
197
|
+
if (mountIframe) dispatchRefreshToTarget(mountIframe);
|
|
198
|
+
if (dialogIframe) dispatchRefreshToTarget(dialogIframe);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function notifySessionRefreshFailed(err) {
|
|
202
|
+
var error = {
|
|
203
|
+
code: "SESSION_REFRESH_FAILED",
|
|
204
|
+
message:
|
|
205
|
+
(err && err.message) ||
|
|
206
|
+
"Failed to refresh the SDK session before it expired",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
if (initCallbacks.onError) initCallbacks.onError(error);
|
|
211
|
+
} catch (_) {}
|
|
212
|
+
try {
|
|
213
|
+
if (mountCallbacks.onError) mountCallbacks.onError(error);
|
|
214
|
+
} catch (_) {}
|
|
215
|
+
try {
|
|
216
|
+
if (dialogCallbacks.onError) dialogCallbacks.onError(error);
|
|
217
|
+
} catch (_) {}
|
|
218
|
+
try {
|
|
219
|
+
if (dialogAddToScenarioCallbacks.onError) {
|
|
220
|
+
dialogAddToScenarioCallbacks.onError(error);
|
|
221
|
+
}
|
|
222
|
+
} catch (_) {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function computeRefreshBuffer(ttlMs) {
|
|
226
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return 30000;
|
|
227
|
+
return Math.min(30000, Math.max(1000, Math.floor(ttlMs * 0.1)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function computeRefreshDelay(ttlMs) {
|
|
231
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return 5000;
|
|
232
|
+
|
|
233
|
+
var eightyPercent = Math.floor(ttlMs * 0.8);
|
|
234
|
+
var oneMinuteBeforeExpiry = ttlMs - 60000;
|
|
235
|
+
var target =
|
|
236
|
+
oneMinuteBeforeExpiry > 0
|
|
237
|
+
? Math.min(eightyPercent, oneMinuteBeforeExpiry)
|
|
238
|
+
: Math.floor(ttlMs * 0.5);
|
|
239
|
+
|
|
240
|
+
return Math.max(target, 1000);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function scheduleRefreshRetry(err) {
|
|
244
|
+
if (!initCalled || !sessionExpiresAt) return;
|
|
245
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
246
|
+
|
|
247
|
+
var remainingMs = sessionExpiresAt - Date.now();
|
|
248
|
+
|
|
249
|
+
// The token has expired: a fresh one is now most needed, so DO NOT give up.
|
|
250
|
+
// Notify the host once that a refresh missed the window, then keep retrying
|
|
251
|
+
// with capped backoff until the session is restored (or the SDK is
|
|
252
|
+
// destroyed / re-initialized). Previously this returned here permanently,
|
|
253
|
+
// so one post-expiry failure killed refresh forever.
|
|
254
|
+
if (remainingMs <= 0 && !sessionRefreshExpiredNotified) {
|
|
255
|
+
sessionRefreshExpiredNotified = true;
|
|
256
|
+
notifySessionRefreshFailed(err);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var delay = Math.max(1000, refreshRetryDelayMs);
|
|
260
|
+
refreshRetryDelayMs = Math.min(refreshRetryDelayMs * 2, 60000);
|
|
261
|
+
|
|
262
|
+
refreshTimer = setTimeout(function () {
|
|
263
|
+
fetchSession({ broadcastRefresh: true }).catch(scheduleRefreshRetry);
|
|
264
|
+
}, delay);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Re-resolve the session when the tab becomes visible again. Background tabs
|
|
268
|
+
// throttle timers, so a long-lived session can lapse while hidden; and a user
|
|
269
|
+
// who was in trial mode may have completed payment in another tab. Either way,
|
|
270
|
+
// refreshing on foreground recovers a mounted embed without a page reload.
|
|
271
|
+
function handleVisibilityChange() {
|
|
272
|
+
try {
|
|
273
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!initCalled) return;
|
|
277
|
+
var needsRefresh =
|
|
278
|
+
!sessionToken || Date.now() >= sessionExpiresAt - sessionRefreshBufferMs;
|
|
279
|
+
if (needsRefresh) {
|
|
280
|
+
fetchSession({ broadcastRefresh: true }).catch(function () {});
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
logError("handleVisibilityChange", e);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function fetchSession(options) {
|
|
288
|
+
options = options || {};
|
|
105
289
|
var requestInitVersion = activeInitVersion;
|
|
106
290
|
|
|
107
291
|
if (fetchingSession && fetchingSession.initVersion === requestInitVersion) {
|
|
292
|
+
if (options.broadcastRefresh) {
|
|
293
|
+
return fetchingSession.promise.then(function (result) {
|
|
294
|
+
if (
|
|
295
|
+
requestInitVersion === activeInitVersion &&
|
|
296
|
+
result &&
|
|
297
|
+
result.sessionToken
|
|
298
|
+
) {
|
|
299
|
+
broadcastSessionRefresh();
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
108
304
|
return fetchingSession.promise;
|
|
109
305
|
}
|
|
110
306
|
|
|
111
|
-
var requestPromise =
|
|
112
|
-
|
|
113
|
-
|
|
307
|
+
var requestPromise = resolveUserToken().then(function (freshUserToken) {
|
|
308
|
+
if (requestInitVersion !== activeInitVersion) {
|
|
309
|
+
throw makeError("ABORTED", "Stale session request ignored");
|
|
310
|
+
}
|
|
114
311
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
xhr.setRequestHeader("Content-Type", "application/json");
|
|
119
|
-
xhr.withCredentials = false;
|
|
120
|
-
xhr.timeout = SESSION_TIMEOUT_MS;
|
|
312
|
+
return new Promise(function (resolve, reject) {
|
|
313
|
+
var url = getApiOrigin() + "/api/sdk/session";
|
|
314
|
+
var body = { userToken: freshUserToken };
|
|
121
315
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
316
|
+
var xhr = new XMLHttpRequest();
|
|
317
|
+
activeSessionXhr = xhr;
|
|
318
|
+
xhr.open("POST", url, true);
|
|
319
|
+
xhr.setRequestHeader("Content-Type", "application/json");
|
|
320
|
+
xhr.withCredentials = false;
|
|
321
|
+
xhr.timeout = SESSION_TIMEOUT_MS;
|
|
322
|
+
|
|
323
|
+
function cleanupRequest() {
|
|
324
|
+
if (activeSessionXhr === xhr) {
|
|
325
|
+
activeSessionXhr = null;
|
|
326
|
+
}
|
|
327
|
+
if (fetchingSession && fetchingSession.promise === requestPromise) {
|
|
328
|
+
fetchingSession = null;
|
|
329
|
+
}
|
|
128
330
|
}
|
|
129
|
-
}
|
|
130
331
|
|
|
131
|
-
|
|
132
|
-
|
|
332
|
+
xhr.onload = function () {
|
|
333
|
+
cleanupRequest();
|
|
133
334
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
335
|
+
if (requestInitVersion !== activeInitVersion) {
|
|
336
|
+
reject({ code: "ABORTED", message: "Stale session response ignored" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
138
339
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
340
|
+
var data;
|
|
341
|
+
try {
|
|
342
|
+
data = JSON.parse(xhr.responseText);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
reject({ code: "PARSE_ERROR", message: "Invalid response from session endpoint" });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
146
347
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
348
|
+
if (xhr.status === 200 && data.sessionToken) {
|
|
349
|
+
sessionToken = data.sessionToken;
|
|
350
|
+
var ttl = (data.expiresIn || 3600) * 1000;
|
|
351
|
+
sessionRefreshBufferMs = computeRefreshBuffer(ttl);
|
|
352
|
+
sessionExpiresAt = Date.now() + ttl;
|
|
353
|
+
refreshRetryDelayMs = 5000;
|
|
354
|
+
sessionRefreshExpiredNotified = false;
|
|
355
|
+
scheduleRefresh(ttl);
|
|
356
|
+
if (options.broadcastRefresh) broadcastSessionRefresh();
|
|
357
|
+
resolve({ sessionToken: sessionToken });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
155
360
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
361
|
+
if (data.error === "USER_NOT_FOUND") {
|
|
362
|
+
// Trial mode — not a fatal error
|
|
363
|
+
sessionToken = null;
|
|
364
|
+
sessionExpiresAt = 0;
|
|
365
|
+
refreshRetryDelayMs = 5000;
|
|
366
|
+
sessionRefreshExpiredNotified = false;
|
|
367
|
+
if (refreshTimer) {
|
|
368
|
+
clearTimeout(refreshTimer);
|
|
369
|
+
refreshTimer = null;
|
|
370
|
+
}
|
|
371
|
+
if (data.paymentLink) paymentLink = data.paymentLink;
|
|
372
|
+
resolve({ trialMode: true });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
163
375
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
376
|
+
reject({
|
|
377
|
+
code: data.error || "SESSION_ERROR",
|
|
378
|
+
message: data.message || "Failed to create session (HTTP " + xhr.status + ")",
|
|
379
|
+
});
|
|
380
|
+
};
|
|
169
381
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
382
|
+
xhr.onerror = function () {
|
|
383
|
+
cleanupRequest();
|
|
384
|
+
reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
|
|
385
|
+
};
|
|
174
386
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
387
|
+
xhr.ontimeout = function () {
|
|
388
|
+
cleanupRequest();
|
|
389
|
+
reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
|
|
390
|
+
};
|
|
179
391
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
392
|
+
xhr.onabort = function () {
|
|
393
|
+
cleanupRequest();
|
|
394
|
+
reject({ code: "ABORTED", message: "Session request was aborted" });
|
|
395
|
+
};
|
|
184
396
|
|
|
185
|
-
|
|
397
|
+
xhr.send(JSON.stringify(body));
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
requestPromise = requestPromise.catch(function (err) {
|
|
402
|
+
if (fetchingSession && fetchingSession.promise === requestPromise) {
|
|
403
|
+
fetchingSession = null;
|
|
404
|
+
}
|
|
405
|
+
throw err;
|
|
186
406
|
});
|
|
187
407
|
|
|
188
408
|
fetchingSession = {
|
|
@@ -194,7 +414,7 @@
|
|
|
194
414
|
}
|
|
195
415
|
|
|
196
416
|
function getSessionToken() {
|
|
197
|
-
if (sessionToken && Date.now() < sessionExpiresAt -
|
|
417
|
+
if (sessionToken && Date.now() < sessionExpiresAt - sessionRefreshBufferMs) {
|
|
198
418
|
return Promise.resolve(sessionToken);
|
|
199
419
|
}
|
|
200
420
|
return fetchSession().then(function (result) {
|
|
@@ -204,35 +424,41 @@
|
|
|
204
424
|
|
|
205
425
|
function scheduleRefresh(ttlMs) {
|
|
206
426
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
207
|
-
var delay =
|
|
427
|
+
var delay = computeRefreshDelay(ttlMs);
|
|
208
428
|
refreshTimer = setTimeout(function () {
|
|
209
|
-
fetchSession().catch(
|
|
210
|
-
// Silent — next open() will retry
|
|
211
|
-
});
|
|
429
|
+
fetchSession({ broadcastRefresh: true }).catch(scheduleRefreshRetry);
|
|
212
430
|
}, delay);
|
|
213
431
|
}
|
|
214
432
|
|
|
215
433
|
// ── Teardown (dialog only — mount is independent) ─────────────────
|
|
216
434
|
|
|
217
435
|
function teardownDialog() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
436
|
+
if (dialogCloseTeardownTimer) {
|
|
437
|
+
clearTimeout(dialogCloseTeardownTimer);
|
|
438
|
+
dialogCloseTeardownTimer = null;
|
|
439
|
+
}
|
|
223
440
|
|
|
441
|
+
// Remove the listener FIRST, in its own try, so a DOM exception below can
|
|
442
|
+
// never skip it (which would leak listeners across many open/close cycles).
|
|
443
|
+
if (dialogListener) {
|
|
444
|
+
try {
|
|
445
|
+
window.removeEventListener("message", dialogListener);
|
|
446
|
+
} catch (_) {}
|
|
447
|
+
dialogListener = null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
224
451
|
if (dialogOverlay && dialogOverlay.parentNode) {
|
|
225
452
|
dialogOverlay.parentNode.removeChild(dialogOverlay);
|
|
226
453
|
}
|
|
227
|
-
dialogOverlay = null;
|
|
228
|
-
|
|
229
|
-
if (dialogListener) {
|
|
230
|
-
window.removeEventListener("message", dialogListener);
|
|
231
|
-
dialogListener = null;
|
|
232
|
-
}
|
|
233
454
|
} catch (e) {
|
|
234
455
|
logError("teardownDialog", e);
|
|
235
456
|
}
|
|
457
|
+
dialogOverlay = null;
|
|
458
|
+
|
|
459
|
+
// Restore focus to where it was, un-hide/un-inert the background, drop the
|
|
460
|
+
// Escape handler. Safe no-op if accessibility was never set up.
|
|
461
|
+
teardownDialogAccessibility();
|
|
236
462
|
|
|
237
463
|
dialogIframe = null;
|
|
238
464
|
dialogContactData = null;
|
|
@@ -243,14 +469,18 @@
|
|
|
243
469
|
}
|
|
244
470
|
|
|
245
471
|
function teardownMount() {
|
|
472
|
+
// Remove the listener FIRST so a DOM exception below can't leak it.
|
|
473
|
+
if (mountListener) {
|
|
474
|
+
try {
|
|
475
|
+
window.removeEventListener("message", mountListener);
|
|
476
|
+
} catch (_) {}
|
|
477
|
+
mountListener = null;
|
|
478
|
+
}
|
|
479
|
+
|
|
246
480
|
try {
|
|
247
481
|
if (mountIframe && mountIframe.parentNode) {
|
|
248
482
|
mountIframe.parentNode.removeChild(mountIframe);
|
|
249
483
|
}
|
|
250
|
-
if (mountListener) {
|
|
251
|
-
window.removeEventListener("message", mountListener);
|
|
252
|
-
mountListener = null;
|
|
253
|
-
}
|
|
254
484
|
} catch (e) {
|
|
255
485
|
logError("teardownMount", e);
|
|
256
486
|
}
|
|
@@ -260,6 +490,128 @@
|
|
|
260
490
|
mountCallbacks = { onClose: null, onError: null };
|
|
261
491
|
}
|
|
262
492
|
|
|
493
|
+
// ── Dialog accessibility ──────────────────────────────────────────
|
|
494
|
+
// The dialog is a modal over the host page, so it follows the ARIA dialog
|
|
495
|
+
// pattern: labelled role="dialog" aria-modal, focus moved in on open and
|
|
496
|
+
// restored on close, background hidden from assistive tech + made inert, Tab
|
|
497
|
+
// focus trapped via edge sentinels, and Escape to close. NOTE: the content is
|
|
498
|
+
// a cross-origin iframe, so the parent cannot observe keystrokes while focus
|
|
499
|
+
// is INSIDE it — Escape works from the dialog chrome, and the embedded app
|
|
500
|
+
// forwards Escape from within the iframe for full coverage.
|
|
501
|
+
|
|
502
|
+
function hideBackgroundFromAssistiveTech(overlayEl) {
|
|
503
|
+
dialogInertedSiblings = [];
|
|
504
|
+
if (typeof document === "undefined" || !document.body) return;
|
|
505
|
+
var children = document.body.children;
|
|
506
|
+
for (var i = 0; i < children.length; i++) {
|
|
507
|
+
var el = children[i];
|
|
508
|
+
if (el === overlayEl) continue;
|
|
509
|
+
dialogInertedSiblings.push({
|
|
510
|
+
el: el,
|
|
511
|
+
ariaHidden: el.getAttribute("aria-hidden"),
|
|
512
|
+
wasInert: el.hasAttribute("inert"),
|
|
513
|
+
});
|
|
514
|
+
el.setAttribute("aria-hidden", "true");
|
|
515
|
+
try {
|
|
516
|
+
el.inert = true;
|
|
517
|
+
} catch (_) {}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function restoreBackgroundFromAssistiveTech() {
|
|
522
|
+
if (!dialogInertedSiblings) return;
|
|
523
|
+
for (var i = 0; i < dialogInertedSiblings.length; i++) {
|
|
524
|
+
var rec = dialogInertedSiblings[i];
|
|
525
|
+
try {
|
|
526
|
+
if (rec.ariaHidden === null) rec.el.removeAttribute("aria-hidden");
|
|
527
|
+
else rec.el.setAttribute("aria-hidden", rec.ariaHidden);
|
|
528
|
+
if (!rec.wasInert) rec.el.inert = false;
|
|
529
|
+
} catch (_) {}
|
|
530
|
+
}
|
|
531
|
+
dialogInertedSiblings = null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function makeFocusSentinel() {
|
|
535
|
+
var s = document.createElement("div");
|
|
536
|
+
s.tabIndex = 0;
|
|
537
|
+
s.setAttribute("aria-hidden", "true");
|
|
538
|
+
var st = s.style;
|
|
539
|
+
st.position = "absolute";
|
|
540
|
+
st.width = "1px";
|
|
541
|
+
st.height = "1px";
|
|
542
|
+
st.padding = "0";
|
|
543
|
+
st.margin = "-1px";
|
|
544
|
+
st.overflow = "hidden";
|
|
545
|
+
st.border = "0";
|
|
546
|
+
st.clip = "rect(0 0 0 0)";
|
|
547
|
+
return s;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function setupDialogAccessibility(overlayEl, closeBtn, labelText) {
|
|
551
|
+
try {
|
|
552
|
+
overlayEl.setAttribute("role", "dialog");
|
|
553
|
+
overlayEl.setAttribute("aria-modal", "true");
|
|
554
|
+
if (labelText) overlayEl.setAttribute("aria-label", labelText);
|
|
555
|
+
if (closeBtn) closeBtn.setAttribute("aria-label", "Close");
|
|
556
|
+
|
|
557
|
+
dialogPreviousFocus =
|
|
558
|
+
document.activeElement && document.activeElement.focus
|
|
559
|
+
? document.activeElement
|
|
560
|
+
: null;
|
|
561
|
+
|
|
562
|
+
hideBackgroundFromAssistiveTech(overlayEl);
|
|
563
|
+
|
|
564
|
+
// Edge sentinels wrap Tab focus back into the dialog. This works across
|
|
565
|
+
// the cross-origin iframe boundary (focus exiting the iframe lands on the
|
|
566
|
+
// trailing sentinel in the parent), where a keydown-based trap cannot.
|
|
567
|
+
var lead = makeFocusSentinel();
|
|
568
|
+
var trail = makeFocusSentinel();
|
|
569
|
+
lead.addEventListener("focus", function () {
|
|
570
|
+
var f = overlayEl.querySelector("iframe");
|
|
571
|
+
if (f) f.focus();
|
|
572
|
+
else if (closeBtn) closeBtn.focus();
|
|
573
|
+
});
|
|
574
|
+
trail.addEventListener("focus", function () {
|
|
575
|
+
if (closeBtn) closeBtn.focus();
|
|
576
|
+
});
|
|
577
|
+
overlayEl.insertBefore(lead, overlayEl.firstChild);
|
|
578
|
+
overlayEl.appendChild(trail);
|
|
579
|
+
|
|
580
|
+
if (closeBtn) {
|
|
581
|
+
try {
|
|
582
|
+
closeBtn.focus();
|
|
583
|
+
} catch (_) {}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
dialogKeydownListener = function (e) {
|
|
587
|
+
if (e.key === "Escape" || e.keyCode === 27) {
|
|
588
|
+
e.stopPropagation();
|
|
589
|
+
closeDialog();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
document.addEventListener("keydown", dialogKeydownListener, true);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
logError("setupDialogAccessibility", e);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function teardownDialogAccessibility() {
|
|
599
|
+
if (dialogKeydownListener) {
|
|
600
|
+
try {
|
|
601
|
+
document.removeEventListener("keydown", dialogKeydownListener, true);
|
|
602
|
+
} catch (_) {}
|
|
603
|
+
dialogKeydownListener = null;
|
|
604
|
+
}
|
|
605
|
+
restoreBackgroundFromAssistiveTech();
|
|
606
|
+
var toRestore = dialogPreviousFocus;
|
|
607
|
+
dialogPreviousFocus = null;
|
|
608
|
+
if (toRestore && toRestore.focus) {
|
|
609
|
+
try {
|
|
610
|
+
toRestore.focus();
|
|
611
|
+
} catch (_) {}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
263
615
|
// ── Message dispatch ──────────────────────────────────────────────
|
|
264
616
|
|
|
265
617
|
function dispatchInitToTarget(targetIframe, contactData, atsContacts) {
|
|
@@ -451,17 +803,17 @@
|
|
|
451
803
|
|
|
452
804
|
var SeamlessRoleplay = {
|
|
453
805
|
/**
|
|
454
|
-
* Initialize the SDK with a
|
|
806
|
+
* Initialize the SDK with a host-provided user token callback.
|
|
455
807
|
*/
|
|
456
808
|
init: function (opts) {
|
|
457
809
|
try {
|
|
458
810
|
var initVersion = activeInitVersion + 1;
|
|
459
|
-
var
|
|
811
|
+
var hasUserTokenProvider = !!(opts && typeof opts.getUserToken === "function");
|
|
460
812
|
|
|
461
|
-
if (!opts || !
|
|
813
|
+
if (!opts || !hasUserTokenProvider) {
|
|
462
814
|
var error = {
|
|
463
815
|
code: "INVALID_INIT",
|
|
464
|
-
message: "requires {
|
|
816
|
+
message: "requires { getUserToken }",
|
|
465
817
|
};
|
|
466
818
|
logError("init", error.message);
|
|
467
819
|
if (opts && typeof opts.onError === "function") {
|
|
@@ -479,14 +831,37 @@
|
|
|
479
831
|
clearSessionRequest();
|
|
480
832
|
sessionToken = null;
|
|
481
833
|
sessionExpiresAt = 0;
|
|
834
|
+
sessionRefreshBufferMs = 30000;
|
|
835
|
+
refreshRetryDelayMs = 5000;
|
|
482
836
|
paymentLink = null;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
837
|
+
latestUserToken = null;
|
|
838
|
+
userTokenRequest = null;
|
|
839
|
+
|
|
840
|
+
getUserToken = opts.getUserToken;
|
|
841
|
+
if (opts.origin) {
|
|
842
|
+
if (isAllowedAppOrigin(opts.origin)) {
|
|
843
|
+
appOrigin = opts.origin;
|
|
844
|
+
} else {
|
|
845
|
+
appOrigin = null;
|
|
846
|
+
logError("init", "Ignoring untrusted origin override: " + opts.origin);
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
appOrigin = null;
|
|
850
|
+
}
|
|
486
851
|
initCallbacks.onReady = opts.onReady || null;
|
|
487
852
|
initCallbacks.onError = opts.onError || null;
|
|
488
853
|
initCalled = true;
|
|
489
854
|
|
|
855
|
+
// Re-resolve the session when the tab is refocused (recovers throttled
|
|
856
|
+
// refresh timers and trial->paid transitions). Re-armed on every init().
|
|
857
|
+
if (typeof document !== "undefined" && document.addEventListener) {
|
|
858
|
+
if (visibilityListener) {
|
|
859
|
+
document.removeEventListener("visibilitychange", visibilityListener);
|
|
860
|
+
}
|
|
861
|
+
visibilityListener = handleVisibilityChange;
|
|
862
|
+
document.addEventListener("visibilitychange", visibilityListener);
|
|
863
|
+
}
|
|
864
|
+
|
|
490
865
|
// Fetch session immediately
|
|
491
866
|
fetchSession()
|
|
492
867
|
.then(function (result) {
|
|
@@ -601,6 +976,7 @@
|
|
|
601
976
|
dialogOverlay = el;
|
|
602
977
|
dialogIframe = iframeEl;
|
|
603
978
|
document.body.appendChild(dialogOverlay);
|
|
979
|
+
setupDialogAccessibility(el, closeBtn, "Roleplay call");
|
|
604
980
|
} catch (e) {
|
|
605
981
|
logError("open", e);
|
|
606
982
|
teardownDialog();
|
|
@@ -613,7 +989,7 @@
|
|
|
613
989
|
/**
|
|
614
990
|
* Mount the full Roleplay app into a container element.
|
|
615
991
|
*/
|
|
616
|
-
mount: function (container) {
|
|
992
|
+
mount: function (container, opts) {
|
|
617
993
|
try {
|
|
618
994
|
if (!initCalled) {
|
|
619
995
|
logError("mount", "init() must be called first");
|
|
@@ -627,7 +1003,10 @@
|
|
|
627
1003
|
// Tear down any existing mount (re-mount)
|
|
628
1004
|
if (mountIframe) teardownMount();
|
|
629
1005
|
|
|
630
|
-
mountCallbacks = {
|
|
1006
|
+
mountCallbacks = {
|
|
1007
|
+
onClose: (opts && opts.onClose) || null,
|
|
1008
|
+
onError: (opts && opts.onError) || null,
|
|
1009
|
+
};
|
|
631
1010
|
mountContainer = container;
|
|
632
1011
|
|
|
633
1012
|
// Listen for messages
|
|
@@ -764,6 +1143,7 @@
|
|
|
764
1143
|
dialogOverlay = el;
|
|
765
1144
|
dialogIframe = iframeEl;
|
|
766
1145
|
document.body.appendChild(dialogOverlay);
|
|
1146
|
+
setupDialogAccessibility(el, closeBtn, "Add contacts to scenario");
|
|
767
1147
|
} catch (e) {
|
|
768
1148
|
logError("addToScenario", e);
|
|
769
1149
|
teardownDialog();
|
|
@@ -796,6 +1176,28 @@
|
|
|
796
1176
|
}
|
|
797
1177
|
},
|
|
798
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* Force a session re-resolution — e.g. after the user completes payment in
|
|
1181
|
+
* another tab. Re-fetches the session and broadcasts it to any mounted embed
|
|
1182
|
+
* or open dialog, recovering trial->paid without a page reload.
|
|
1183
|
+
*/
|
|
1184
|
+
reauth: function () {
|
|
1185
|
+
try {
|
|
1186
|
+
if (!initCalled) {
|
|
1187
|
+
logError("reauth", "init() must be called first");
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
refreshRetryDelayMs = 5000;
|
|
1191
|
+
sessionRefreshExpiredNotified = false;
|
|
1192
|
+
fetchSession({ broadcastRefresh: true }).catch(function (err) {
|
|
1193
|
+
if (err && err.code === "ABORTED") return;
|
|
1194
|
+
scheduleRefreshRetry(err);
|
|
1195
|
+
});
|
|
1196
|
+
} catch (e) {
|
|
1197
|
+
logError("reauth", e);
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
|
|
799
1201
|
/**
|
|
800
1202
|
* Destroy the SDK — clears state, timers, and DOM (both mount and dialogs).
|
|
801
1203
|
*/
|
|
@@ -806,13 +1208,22 @@
|
|
|
806
1208
|
clearTimeout(refreshTimer);
|
|
807
1209
|
refreshTimer = null;
|
|
808
1210
|
}
|
|
1211
|
+
if (visibilityListener && typeof document !== "undefined") {
|
|
1212
|
+
document.removeEventListener("visibilitychange", visibilityListener);
|
|
1213
|
+
visibilityListener = null;
|
|
1214
|
+
}
|
|
1215
|
+
sessionRefreshExpiredNotified = false;
|
|
809
1216
|
clearSessionRequest();
|
|
810
1217
|
teardownDialog();
|
|
811
1218
|
teardownMount();
|
|
812
|
-
|
|
1219
|
+
getUserToken = null;
|
|
1220
|
+
latestUserToken = null;
|
|
1221
|
+
userTokenRequest = null;
|
|
813
1222
|
paymentLink = null;
|
|
814
1223
|
sessionToken = null;
|
|
815
1224
|
sessionExpiresAt = 0;
|
|
1225
|
+
sessionRefreshBufferMs = 30000;
|
|
1226
|
+
refreshRetryDelayMs = 5000;
|
|
816
1227
|
initCallbacks = { onReady: null, onError: null };
|
|
817
1228
|
initCalled = false;
|
|
818
1229
|
} catch (e) {
|