@webotme/react-native 0.1.1 → 0.1.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 +79 -0
- package/package.json +16 -5
- package/src/chat.ts +369 -0
- package/src/client.ts +4 -1
- package/src/executor.ts +156 -0
- package/src/index.ts +54 -0
- package/src/lazy-webview.tsx +59 -0
- package/src/navigationAdapter.ts +58 -0
- package/src/provider.tsx +72 -76
- package/src/safety.ts +72 -0
- package/src/scan.txt +1 -0
- package/src/screenHook.ts +82 -0
- package/src/screenScanner.tsx +376 -0
- package/src/types.ts +2 -0
- package/src/voice.ts +592 -0
- package/src/widget.tsx +3 -2
package/src/index.ts
CHANGED
|
@@ -10,7 +10,61 @@ export type { WebotMeLauncherProps, LauncherPosition } from "./launcher";
|
|
|
10
10
|
export { WebotMeClient } from "./client";
|
|
11
11
|
export type { SendResult } from "./client";
|
|
12
12
|
|
|
13
|
+
export {
|
|
14
|
+
chatWithUs,
|
|
15
|
+
configureChat,
|
|
16
|
+
resetChat,
|
|
17
|
+
updateChatScreenContext,
|
|
18
|
+
useChat,
|
|
19
|
+
subscribeChat,
|
|
20
|
+
getChatState,
|
|
21
|
+
getChatSessionId,
|
|
22
|
+
} from "./chat";
|
|
23
|
+
export type { ChatResult } from "./chat";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
configureVoice,
|
|
27
|
+
isVoiceSupported,
|
|
28
|
+
setVoiceReplies,
|
|
29
|
+
speakReplyIfEnabled,
|
|
30
|
+
speakWithUs,
|
|
31
|
+
stopVoice,
|
|
32
|
+
useVoiceChat,
|
|
33
|
+
voiceWithUs,
|
|
34
|
+
} from "./voice";
|
|
35
|
+
export type { VoiceOptions, VoiceResult, VoiceState } from "./voice";
|
|
36
|
+
|
|
13
37
|
export { WebotMeError } from "./types";
|
|
38
|
+
export {
|
|
39
|
+
executeAction,
|
|
40
|
+
executeActions,
|
|
41
|
+
resetExecutorState,
|
|
42
|
+
setupExecutor,
|
|
43
|
+
type ExecutorOptions,
|
|
44
|
+
} from "./executor";
|
|
45
|
+
export {
|
|
46
|
+
getCurrentRoute,
|
|
47
|
+
goBack,
|
|
48
|
+
navigateTo,
|
|
49
|
+
setCurrentRoute,
|
|
50
|
+
useNavigationAdapter,
|
|
51
|
+
} from "./navigationAdapter";
|
|
52
|
+
export {
|
|
53
|
+
isActionAllowed,
|
|
54
|
+
isBlocked,
|
|
55
|
+
needsConfirmation,
|
|
56
|
+
requireConfirmation,
|
|
57
|
+
setupSafetyLayer,
|
|
58
|
+
} from "./safety";
|
|
59
|
+
export {
|
|
60
|
+
registerScreenTree,
|
|
61
|
+
scanScreen,
|
|
62
|
+
setupFiberScanner,
|
|
63
|
+
stopFiberScanner,
|
|
64
|
+
useFiberScanner,
|
|
65
|
+
findClickHandler,
|
|
66
|
+
findFillHandler,
|
|
67
|
+
} from "./screenScanner";
|
|
14
68
|
export type {
|
|
15
69
|
WebotMeAction,
|
|
16
70
|
WebotMeButton,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `react-native-webview` is a native module, so importing it at the top of a
|
|
6
|
+
* file breaks any app that runs in Expo Go or a plain JS bundle. The web
|
|
7
|
+
* variant is the only thing that needs it, so it is resolved on first render
|
|
8
|
+
* instead — apps that only use `chatWithUs` never touch it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type AnyProps = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
function MissingWebView() {
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.wrap}>
|
|
16
|
+
<Text style={styles.title}>Web chat needs a development build</Text>
|
|
17
|
+
<Text style={styles.body}>
|
|
18
|
+
react-native-webview is a native module, so it is not available in Expo Go.
|
|
19
|
+
</Text>
|
|
20
|
+
<Text style={styles.body}>
|
|
21
|
+
Run "npx expo run:android" / "npx expo run:ios", or use an EAS build. If you
|
|
22
|
+
only need text chat, use variant="native" or the chatWithUs function.
|
|
23
|
+
</Text>
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let resolved: React.ComponentType<AnyProps> | null = null;
|
|
29
|
+
|
|
30
|
+
export function loadWebView(): React.ComponentType<AnyProps> {
|
|
31
|
+
if (resolved) return resolved;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const mod = require("react-native-webview");
|
|
36
|
+
const candidate = mod?.WebView ?? mod?.default ?? mod;
|
|
37
|
+
resolved =
|
|
38
|
+
candidate && (typeof candidate === "function" || typeof candidate === "object")
|
|
39
|
+
? (candidate as React.ComponentType<AnyProps>)
|
|
40
|
+
: MissingWebView;
|
|
41
|
+
} catch {
|
|
42
|
+
resolved = MissingWebView;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const styles = StyleSheet.create({
|
|
49
|
+
wrap: {
|
|
50
|
+
flex: 1,
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
justifyContent: "center",
|
|
53
|
+
gap: 8,
|
|
54
|
+
padding: 24,
|
|
55
|
+
backgroundColor: "#0f172a",
|
|
56
|
+
},
|
|
57
|
+
title: { color: "#f8fafc", fontSize: 15, fontWeight: "700", textAlign: "center" },
|
|
58
|
+
body: { color: "#94a3b8", fontSize: 13, textAlign: "center", lineHeight: 19 },
|
|
59
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
let lastRoute = "";
|
|
4
|
+
|
|
5
|
+
/** Last route seen by the app. */
|
|
6
|
+
export function getCurrentRoute(): string {
|
|
7
|
+
return lastRoute;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setCurrentRoute(route: string): void {
|
|
11
|
+
lastRoute = route;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Navigate imperatively using Expo Router's router. */
|
|
15
|
+
export function navigateTo(route: string, params?: Record<string, any>): void {
|
|
16
|
+
try {
|
|
17
|
+
const router =
|
|
18
|
+
require("expo-router").default ?? require("expo-router").router;
|
|
19
|
+
if (params) (router as any)?.push?.({ pathname: route as any, params } as any);
|
|
20
|
+
else (router as any)?.push?.(route as any);
|
|
21
|
+
} catch {
|
|
22
|
+
/* no router available */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function goBack(): void {
|
|
27
|
+
try {
|
|
28
|
+
const router =
|
|
29
|
+
require("expo-router").default ?? require("expo-router").router;
|
|
30
|
+
(router as any)?.back?.();
|
|
31
|
+
} catch {
|
|
32
|
+
/* noop */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bridge the live Expo Router pathname into the scanner's screen map. Call
|
|
38
|
+
* this once from a component that sits inside the router context.
|
|
39
|
+
*/
|
|
40
|
+
export function useNavigationAdapter(): { route: string | undefined; back: () => void } {
|
|
41
|
+
let pathname: string | undefined;
|
|
42
|
+
try {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const usePathname = require("expo-router").usePathname as () => string | undefined;
|
|
45
|
+
// Expo Router provides this hook inside the app root; if it is missing the
|
|
46
|
+
// scanner/scanner route just stays blank.
|
|
47
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
48
|
+
pathname = usePathname();
|
|
49
|
+
} catch {
|
|
50
|
+
pathname = undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (pathname) setCurrentRoute(pathname);
|
|
55
|
+
}, [pathname]);
|
|
56
|
+
|
|
57
|
+
return { route: pathname, back: goBack };
|
|
58
|
+
}
|
package/src/provider.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
createContext,
|
|
3
|
+
useCallback,
|
|
3
4
|
useContext,
|
|
4
5
|
useEffect,
|
|
5
6
|
useMemo,
|
|
@@ -7,6 +8,17 @@ import React, {
|
|
|
7
8
|
useState,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { WebotMeClient } from "./client";
|
|
11
|
+
import {
|
|
12
|
+
chatWithUs,
|
|
13
|
+
configureChat,
|
|
14
|
+
resetChat,
|
|
15
|
+
updateChatScreenContext,
|
|
16
|
+
useChat,
|
|
17
|
+
} from "./chat";
|
|
18
|
+
import { registerScreenTree, setupFiberScanner } from "./screenScanner";
|
|
19
|
+
import { setupExecutor } from "./executor";
|
|
20
|
+
import { setupSafetyLayer } from "./safety";
|
|
21
|
+
import { useNavigationAdapter } from "./navigationAdapter";
|
|
10
22
|
import type {
|
|
11
23
|
WebotMeConfig,
|
|
12
24
|
WebotMeContract,
|
|
@@ -85,16 +97,50 @@ export function WebotMeProvider({
|
|
|
85
97
|
[botId, apiBaseUrl, timeoutMs],
|
|
86
98
|
);
|
|
87
99
|
|
|
100
|
+
// Feed the same bot into the one-function chat engine so `chatWithUs` works
|
|
101
|
+
// in apps that only mounted the provider and nothing else.
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
configureChat({ botId, apiBaseUrl, userId, theme, welcome, screenContext, timeoutMs });
|
|
104
|
+
// Only re-run when the bot itself changes; message sends must not reset it.
|
|
105
|
+
}, [botId, apiBaseUrl, userId, welcome, timeoutMs]);
|
|
106
|
+
|
|
107
|
+
// Keep the element tree snapshot fresh for the screen scanner.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
registerScreenTree(children);
|
|
110
|
+
}, [children]);
|
|
111
|
+
|
|
112
|
+
// Auto-start the screen scanner so it can feed live screen context.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const stop = setupFiberScanner({ intervalMs: 2500 });
|
|
115
|
+
return stop;
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
// Arm the action executor so server-returned actions run on their own.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const unsubscribe = setupExecutor();
|
|
121
|
+
return unsubscribe;
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
// Default safety gate: confirm sensitive actions, block configured keywords.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const reset = setupSafetyLayer();
|
|
127
|
+
return reset;
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
updateChatScreenContext(screenContext);
|
|
132
|
+
}, [screenContext]);
|
|
133
|
+
|
|
88
134
|
// Stable visitor id for the lifetime of the app install.
|
|
89
135
|
const visitorRef = useRef<string>(userId || makeId("v"));
|
|
90
|
-
const [sessionId, setSessionId] = useState(() => makeId("s"));
|
|
91
|
-
const [messages, setMessages] = useState<WebotMeMessage[]>([]);
|
|
92
|
-
const [sending, setSending] = useState(false);
|
|
93
|
-
const [error, setError] = useState<string | null>(null);
|
|
94
136
|
const [contract, setContract] = useState<WebotMeContract | null>(null);
|
|
95
137
|
const [attempt, setAttempt] = useState(0);
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
|
|
139
|
+
// The engine owns the conversation so `chatWithUs` and `useWebotMeChat`
|
|
140
|
+
// always share one session and one transcript.
|
|
141
|
+
const engineState = useChat();
|
|
142
|
+
// Track the live route so the scanner + executor act on the current screen.
|
|
143
|
+
useNavigationAdapter();
|
|
98
144
|
|
|
99
145
|
const chatBaseUrl = useMemo(
|
|
100
146
|
() => (chatBaseUrlProp || apiBaseUrl).replace(/\/+$/, ""),
|
|
@@ -122,76 +168,27 @@ export function WebotMeProvider({
|
|
|
122
168
|
// New session when the caller bumps resetKey.
|
|
123
169
|
useEffect(() => {
|
|
124
170
|
if (resetKey === undefined) return;
|
|
125
|
-
|
|
126
|
-
setMessages(welcome ? [botMessage(welcome)] : []);
|
|
127
|
-
setError(null);
|
|
128
|
-
setPendingWorkflow(null);
|
|
129
|
-
setAutoTask(null);
|
|
171
|
+
resetChat();
|
|
130
172
|
}, [resetKey, welcome]);
|
|
131
173
|
|
|
132
|
-
const send =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
from: "user",
|
|
139
|
-
text: trimmed,
|
|
140
|
-
createdAt: Date.now(),
|
|
141
|
-
};
|
|
142
|
-
const pendingId = makeId("m");
|
|
143
|
-
setMessages((prev) => [
|
|
144
|
-
...prev,
|
|
145
|
-
userMsg,
|
|
146
|
-
{ id: pendingId, from: "bot", text: "", createdAt: Date.now(), pending: true },
|
|
147
|
-
]);
|
|
148
|
-
setSending(true);
|
|
149
|
-
setError(null);
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const result = await client.send({
|
|
153
|
-
message: trimmed,
|
|
154
|
-
sessionId,
|
|
155
|
-
userId: visitorRef.current,
|
|
156
|
-
screenContext,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Handle new response format with actions, pendingWorkflow, autoTask
|
|
160
|
-
const botMsg: WebotMeMessage = {
|
|
161
|
-
id: pendingId,
|
|
162
|
-
from: "bot",
|
|
163
|
-
text: result.text || result.reply || "",
|
|
164
|
-
createdAt: Date.now(),
|
|
165
|
-
action: result.action ? result.actions?.[0] : undefined,
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
setMessages((prev) =>
|
|
169
|
-
prev.map((m) =>
|
|
170
|
-
m.id === pendingId ? botMsg : m,
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Update workflow state
|
|
175
|
-
if (result.pendingWorkflow) {
|
|
176
|
-
setPendingWorkflow(result.pendingWorkflow);
|
|
177
|
-
} else if (result.autoTask) {
|
|
178
|
-
setAutoTask(result.autoTask);
|
|
179
|
-
} else if (!result.pendingWorkflow && !result.autoTask) {
|
|
180
|
-
// Only clear if server explicitly says no workflow
|
|
181
|
-
setPendingWorkflow(null);
|
|
182
|
-
setAutoTask(null);
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
const message = err instanceof Error ? err.message : "Something went wrong";
|
|
186
|
-
setError(message);
|
|
187
|
-
setMessages((prev) => prev.filter((m) => m.id !== pendingId));
|
|
188
|
-
} finally {
|
|
189
|
-
setSending(false);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
174
|
+
const send = useCallback(
|
|
175
|
+
async (text: string) => {
|
|
176
|
+
await chatWithUs(text);
|
|
177
|
+
},
|
|
178
|
+
[],
|
|
179
|
+
);
|
|
192
180
|
|
|
193
181
|
const retry = () => setAttempt((n) => n + 1);
|
|
194
|
-
const reset = () =>
|
|
182
|
+
const reset = () => resetChat();
|
|
183
|
+
|
|
184
|
+
const {
|
|
185
|
+
sessionId,
|
|
186
|
+
messages,
|
|
187
|
+
sending,
|
|
188
|
+
error,
|
|
189
|
+
pendingWorkflow,
|
|
190
|
+
autoTask,
|
|
191
|
+
} = engineState;
|
|
195
192
|
|
|
196
193
|
const value = useMemo<WebotMeContextValue>(
|
|
197
194
|
() => ({
|
|
@@ -221,6 +218,9 @@ export function WebotMeProvider({
|
|
|
221
218
|
sending,
|
|
222
219
|
error,
|
|
223
220
|
contract,
|
|
221
|
+
send,
|
|
222
|
+
retry,
|
|
223
|
+
reset,
|
|
224
224
|
pendingWorkflow,
|
|
225
225
|
autoTask,
|
|
226
226
|
],
|
|
@@ -231,8 +231,4 @@ export function WebotMeProvider({
|
|
|
231
231
|
);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
function botMessage(text: string): WebotMeMessage {
|
|
235
|
-
return { id: makeId("m"), from: "bot", text, createdAt: Date.now() };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
234
|
export type { WebotMeTheme } from "./types";
|
package/src/safety.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Alert } from "react-native";
|
|
2
|
+
import type { WebotMeAction } from "./types";
|
|
3
|
+
|
|
4
|
+
type SafetyOptions = {
|
|
5
|
+
/** Show a confirmation before running any action. Default false. */
|
|
6
|
+
alwaysConfirm?: boolean;
|
|
7
|
+
/** URL/label substrings that make an action require confirmation. */
|
|
8
|
+
confirmKeywords?: string[];
|
|
9
|
+
/** URL/label substrings whose actions are blocked entirely. */
|
|
10
|
+
blockKeywords?: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let options: SafetyOptions = {
|
|
14
|
+
alwaysConfirm: false,
|
|
15
|
+
confirmKeywords: ["delete", "remove", "cancel", "purchase", "checkout", "payment", "order", "logout"],
|
|
16
|
+
blockKeywords: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function setupSafetyLayer(next?: Partial<SafetyOptions>): () => void {
|
|
20
|
+
const previous = options;
|
|
21
|
+
options = { ...options, ...next };
|
|
22
|
+
return () => {
|
|
23
|
+
options = previous;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hazardText(action: WebotMeAction): string {
|
|
28
|
+
const parts = [
|
|
29
|
+
action.type,
|
|
30
|
+
(action as any).label ?? "",
|
|
31
|
+
(action as any).data?.url ?? "",
|
|
32
|
+
(action as any).data?.text ?? "",
|
|
33
|
+
(action as any).data?.label ?? "",
|
|
34
|
+
(action as any).data?.selector ?? "",
|
|
35
|
+
];
|
|
36
|
+
return parts.join(" ").toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isBlocked(action: WebotMeAction): boolean {
|
|
40
|
+
const hay = hazardText(action);
|
|
41
|
+
return (options.blockKeywords ?? []).some((k) => k && hay.includes(k.toLowerCase()));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function needsConfirmation(action: WebotMeAction): boolean {
|
|
45
|
+
if (options.alwaysConfirm) return true;
|
|
46
|
+
if (action.type === "checkout") return true;
|
|
47
|
+
const hay = hazardText(action);
|
|
48
|
+
return (options.confirmKeywords ?? []).some((k) => k && hay.includes(k.toLowerCase()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Ask the user before running an action. Returns true if approved. */
|
|
52
|
+
export function requireConfirmation(action: WebotMeAction): Promise<boolean> {
|
|
53
|
+
const label = (action as any).label ?? (action as any).data?.url ?? (action as any).data?.text ?? action.type;
|
|
54
|
+
return new Promise<boolean>((resolve) => {
|
|
55
|
+
Alert.alert(
|
|
56
|
+
"Allow this action?",
|
|
57
|
+
`${action.type}: ${label}`,
|
|
58
|
+
[
|
|
59
|
+
{ text: "Cancel", style: "cancel", onPress: () => resolve(false) },
|
|
60
|
+
{ text: "Allow", onPress: () => resolve(true) },
|
|
61
|
+
],
|
|
62
|
+
{ cancelable: true, onDismiss: () => resolve(false) },
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Full gate used by the executor before every action run. */
|
|
68
|
+
export async function isActionAllowed(action: WebotMeAction): Promise<boolean> {
|
|
69
|
+
if (isBlocked(action)) return false;
|
|
70
|
+
if (needsConfirmation(action)) return requireConfirmation(action);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
package/src/scan.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
��
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen hook: grab the live React tree by installing a minimal React
|
|
3
|
+
* DevTools global hook (the same seam the React DevTools overlay uses)
|
|
4
|
+
* *before* React / RN get to wire themselves in. An app that wants the
|
|
5
|
+
* full automatic screen scan should require this module as the very first
|
|
6
|
+
* import — typically via a tiny `entry.js` that replaces the package `main`.
|
|
7
|
+
*
|
|
8
|
+
* The package's own entry point can't do this alone: by the time
|
|
9
|
+
* `<WebotMeProvider>` renders, RN's ReactFabric module has already decided
|
|
10
|
+
* not to call into the hook. A file loaded *before* expo-router/entry can.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let capturedRoot: any = null;
|
|
14
|
+
|
|
15
|
+
function capture(root: any) {
|
|
16
|
+
try {
|
|
17
|
+
capturedRoot = root || null;
|
|
18
|
+
} catch {
|
|
19
|
+
/* noop */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function installScreenHook(): void {
|
|
24
|
+
const g = globalThis as any;
|
|
25
|
+
const existing = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
26
|
+
|
|
27
|
+
if (existing) {
|
|
28
|
+
// DevTools attached already: wrap commits so we capture the root too.
|
|
29
|
+
if (!existing.__weBotCapturedHook) {
|
|
30
|
+
const prev = existing.onCommitFiberRoot;
|
|
31
|
+
existing.onCommitFiberRoot = (id: any, root: any, ...rest: any[]) => {
|
|
32
|
+
capture(root);
|
|
33
|
+
try {
|
|
34
|
+
prev?.(id, root, ...rest);
|
|
35
|
+
} catch {
|
|
36
|
+
/* ignore */
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
existing.__weBotCapturedHook = true;
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hook: any = {
|
|
45
|
+
isDisabled: false,
|
|
46
|
+
supportsFiber: true,
|
|
47
|
+
checkDCE: false,
|
|
48
|
+
renderers: new Map(),
|
|
49
|
+
internalModuleStop: undefined,
|
|
50
|
+
getFiberRoots(_id: any) {
|
|
51
|
+
return capturedRoot ? new Set([capturedRoot]) : new Set();
|
|
52
|
+
},
|
|
53
|
+
inject(internals: any) {
|
|
54
|
+
hook.injectedInternals = internals;
|
|
55
|
+
return 1;
|
|
56
|
+
},
|
|
57
|
+
onScheduleFiberRoot() {},
|
|
58
|
+
onCommitFiberRoot(_id: any, root: any) {
|
|
59
|
+
capture(root);
|
|
60
|
+
},
|
|
61
|
+
onCommitFiberUnmount() {},
|
|
62
|
+
onPostCommitFiberRoot() {},
|
|
63
|
+
setStrictMode() {},
|
|
64
|
+
registerInternalModuleStop() {},
|
|
65
|
+
overrideHookState() {},
|
|
66
|
+
supportsChunkDevMode: false,
|
|
67
|
+
};
|
|
68
|
+
g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
installScreenHook();
|
|
72
|
+
|
|
73
|
+
/** The committed React fiber root (HostRoot) of the app, or null if not yet. */
|
|
74
|
+
export function getCapturedRootFiber(): any | null {
|
|
75
|
+
try {
|
|
76
|
+
const raw = capturedRoot;
|
|
77
|
+
if (!raw) return null;
|
|
78
|
+
return raw.current ? raw.current : raw;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|