@robylon/react-native-sdk 2.1.1-staging.3 → 2.1.3-staging.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.
@@ -1 +1 @@
1
- {"version":3,"file":"useChatbotEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useChatbotEvents.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACpB,MAAM,iBAAiB,CAAC;AAGzB,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,GAAG,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,eAAO,MAAM,gBAAgB,mEAM1B,cAAc;sBAwDN,gBAAgB,SAAS,GAAG;iCArDtB,aAAa,mBACR,OAAO,MAAM,EAAE,GAAG,CAAC;CAgExC,CAAC"}
1
+ {"version":3,"file":"useChatbotEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useChatbotEvents.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACpB,MAAM,iBAAiB,CAAC;AAGzB,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,GAAG,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,eAAO,MAAM,gBAAgB,mEAM1B,cAAc;sBA0EN,gBAAgB,SAAS,GAAG;iCAxDtB,aAAa,mBACR,OAAO,MAAM,EAAE,GAAG,CAAC;CAyExC,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Session id pass-through between the chatbot web app and the SDK client.
3
+ *
4
+ * The session id travels on its own message type so that it never reaches
5
+ * `onEvent`, `onMessage` or the internal `/users/sdk/record-logs/` endpoint.
6
+ * Its only exits from the SDK are the `onSession` prop and `getSessionId()`
7
+ * on the ref, both of which the client has to opt into explicitly.
8
+ */
9
+ /** Message the web app posts whenever the active session id changes. */
10
+ export declare const SESSION_UPDATED_MESSAGE_TYPE = "SESSION_UPDATED";
11
+ /** Key carrying the resume target inside the `registerUserId` payload. */
12
+ export declare const RESUME_SESSION_KEY = "sessionId";
13
+ /**
14
+ * Pulls the session id out of a `SESSION_UPDATED` payload.
15
+ *
16
+ * Accepts the snake_case, camelCase and nested spellings so a naming slip on
17
+ * the web side degrades to a no-op rather than a broken build. Anything that
18
+ * is not a non-empty string is dropped.
19
+ */
20
+ export declare const extractSessionId: (data: any) => string | undefined;
21
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/utils/session.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,wEAAwE;AACxE,eAAO,MAAM,4BAA4B,oBAAoB,CAAC;AAE9D,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,SAAU,GAAG,KAAG,MAAM,GAAG,SAGrD,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.1.1-staging.3";
1
+ export declare const SDK_VERSION = "2.1.3-staging.0";
2
2
  //# sourceMappingURL=version.staging.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robylon/react-native-sdk",
3
- "version": "2.1.1-staging.3",
3
+ "version": "2.1.3-staging.0",
4
4
  "description": "React Native SDK for Robylon",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -25,7 +25,7 @@
25
25
  "publish:staging": "npm version $(node scripts/get-next-version.js staging) && node scripts/update-version.js staging && npm run build:staging && npm publish --tag staging",
26
26
  "publish:production": "node scripts/validate-publish.js production && npm run build:production && npm publish",
27
27
  "publish:hotfix": "node scripts/validate-publish.js hotfix && npm version $(node scripts/get-next-version.js hotfix) && node scripts/update-version.js production && npm run build:production && npm publish --tag hotfix",
28
- "postinstall": "node scripts/setup-git-hooks.js",
28
+ "postinstall": "node scripts/setup-git-hooks.js || exit 0",
29
29
  "branch": "node scripts/create-branch.js",
30
30
  "tag": "node scripts/create-version-tag.js"
31
31
  },
@@ -2,17 +2,29 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
 
5
- // Create .git/hooks directory if it doesn't exist
6
- const hooksDir = path.join(__dirname, "../.git/hooks");
7
- if (!fs.existsSync(hooksDir)) {
8
- fs.mkdirSync(hooksDir, { recursive: true });
5
+ // This runs from postinstall, which also fires when the SDK is installed as a
6
+ // dependency. Bail out unless we are in the SDK's own checkout, so consumers
7
+ // do not get a stray .git/hooks directory inside node_modules.
8
+ const gitDir = path.join(__dirname, "../.git");
9
+ if (!fs.existsSync(gitDir)) {
10
+ process.exit(0);
9
11
  }
10
12
 
11
- // Copy the validate-branch-name script
12
- const sourcePath = path.join(__dirname, "validate-branch-name.sh");
13
- const targetPath = path.join(hooksDir, "commit-msg");
13
+ try {
14
+ const hooksDir = path.join(gitDir, "hooks");
15
+ if (!fs.existsSync(hooksDir)) {
16
+ fs.mkdirSync(hooksDir, { recursive: true });
17
+ }
14
18
 
15
- fs.copyFileSync(sourcePath, targetPath);
16
- fs.chmodSync(targetPath, "755");
19
+ // Copy the validate-branch-name script
20
+ const sourcePath = path.join(__dirname, "validate-branch-name.sh");
21
+ const targetPath = path.join(hooksDir, "commit-msg");
17
22
 
18
- console.log("Git hooks installed successfully!");
23
+ fs.copyFileSync(sourcePath, targetPath);
24
+ fs.chmodSync(targetPath, "755");
25
+
26
+ console.log("Git hooks installed successfully!");
27
+ } catch (error) {
28
+ // Never fail an install over developer tooling.
29
+ console.warn("Skipped git hook setup:", error.message);
30
+ }
@@ -49,6 +49,11 @@ import {
49
49
  } from "./utils/webViewStorage";
50
50
  import { animateWebViewOpen, animateWebViewClose } from "./utils/animations";
51
51
  import { handleDownloadRequest, DownloadRequest } from "./utils/fileDownload";
52
+ import {
53
+ SESSION_UPDATED_MESSAGE_TYPE,
54
+ RESUME_SESSION_KEY,
55
+ extractSessionId,
56
+ } from "./utils/session";
52
57
 
53
58
  export interface ChatbotProps {
54
59
  api_key: string;
@@ -61,10 +66,16 @@ export interface ChatbotProps {
61
66
  is_test_user?: boolean;
62
67
  [key: string | number]: any;
63
68
  };
69
+ session_id?: string;
70
+ session_context?: Record<string, any>;
64
71
  onEvent?: ChatbotEventHandler;
65
72
  onOpen?: () => void;
66
73
  onClose?: () => void;
67
74
  onReady?: () => void;
75
+ onSession?: (
76
+ session_id: string,
77
+ session_context?: Record<string, any>,
78
+ ) => void;
68
79
  }
69
80
 
70
81
  export interface ChatbotRef {
@@ -72,6 +83,7 @@ export interface ChatbotRef {
72
83
  close: () => void;
73
84
  toggle: () => void;
74
85
  isReady: () => boolean;
86
+ getSessionId: () => string | undefined;
75
87
  }
76
88
 
77
89
  interface InternalChatbotProps extends ChatbotProps {
@@ -151,6 +163,9 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
151
163
  onClose: externalOnClose,
152
164
  onReady,
153
165
  user_profile,
166
+ session_id,
167
+ session_context,
168
+ onSession,
154
169
  },
155
170
  ref,
156
171
  ) => {
@@ -171,6 +186,18 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
171
186
  const [systemInfo, setSystemInfo] = useState({ os: "", browser: "" });
172
187
  const systemInfoRef = useRef({ os: "", browser: "" });
173
188
 
189
+ // Session id pass-through. All four are refs so that client-supplied values
190
+ // never enter handleMessage's dependency array: adding them would rebuild
191
+ // the handler on every client re-render, omitting them without a ref would
192
+ // capture a stale closure.
193
+ const sessionIdRef = useRef<string | undefined>(undefined);
194
+ const onSessionRef = useRef(onSession);
195
+ const sessionContextRef = useRef(session_context);
196
+ const resumeSessionRef = useRef(session_id);
197
+ onSessionRef.current = onSession;
198
+ sessionContextRef.current = session_context;
199
+ resumeSessionRef.current = session_id;
200
+
174
201
  // Initialize animated values
175
202
  const animatedValues = useRef({
176
203
  scale: new Animated.Value(1),
@@ -221,10 +248,37 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
221
248
  }
222
249
  },
223
250
  isReady: () => isInitialized,
251
+ getSessionId: () => sessionIdRef.current,
224
252
  }),
225
253
  [isInitialized, isWebViewVisible],
226
254
  );
227
255
 
256
+ // Hands the session id to the client. Deduped so that a re-emission on
257
+ // reopen does not fire client side effects twice, and wrapped so that a
258
+ // throwing client handler cannot break message handling.
259
+ const emitSession = useCallback((data: any) => {
260
+ const sessionId = extractSessionId(data);
261
+ if (!sessionId || sessionId === sessionIdRef.current) return;
262
+ sessionIdRef.current = sessionId;
263
+
264
+ try {
265
+ onSessionRef.current?.(sessionId, sessionContextRef.current ?? {});
266
+ } catch (error) {
267
+ errorTracker.trackError(
268
+ error instanceof Error
269
+ ? error
270
+ : new Error("onSession handler threw an error"),
271
+ "ChatbotSDK",
272
+ {
273
+ // The session id itself is deliberately left out of the report so
274
+ // that it stays confined to the two opt-in client channels.
275
+ type: ErrorTypes.RUNTIME_ERROR,
276
+ context: { source: SESSION_UPDATED_MESSAGE_TYPE },
277
+ },
278
+ );
279
+ }
280
+ }, []);
281
+
228
282
  const triggerAppInit = () => {
229
283
  if (webViewRef.current) {
230
284
  webViewRef.current?.injectJavaScript(getBrowserAndOSInfoScript());
@@ -308,6 +362,15 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
308
362
  return;
309
363
  }
310
364
 
365
+ // Returns before the switch below so the session id never reaches
366
+ // emitEvent (and through it onEvent and /users/sdk/record-logs/) or
367
+ // the default branch's onMessage. Clients who registered neither
368
+ // onSession nor getSessionId cannot observe it.
369
+ if (parsedData?.type === SESSION_UPDATED_MESSAGE_TYPE) {
370
+ emitSession(parsedData?.data);
371
+ return;
372
+ }
373
+
311
374
  if (parsedData?.type === "APP_READY") {
312
375
  emitEvent(ChatbotEventType.CHATBOT_APP_READY);
313
376
  if (webViewRef.current && isWebViewVisible) {
@@ -318,6 +381,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
318
381
  action: "registerUserId",
319
382
  data: {
320
383
  userId: effectiveUserId,
384
+ [RESUME_SESSION_KEY]: resumeSessionRef.current || undefined,
321
385
  token: user_token ? `${user_token}` : undefined,
322
386
  userProfile: {
323
387
  ...user_profile,
@@ -328,7 +392,21 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
328
392
  },
329
393
  },
330
394
  };
331
- logger.debug("messageData====>", JSON.stringify(messageData));
395
+ // logger.debug is an unconditional console.log, so the session id
396
+ // is redacted here rather than printed into every client's
397
+ // production console.
398
+ logger.debug(
399
+ "messageData====>",
400
+ JSON.stringify({
401
+ ...messageData,
402
+ data: {
403
+ ...messageData.data,
404
+ [RESUME_SESSION_KEY]: resumeSessionRef.current
405
+ ? "[redacted]"
406
+ : undefined,
407
+ },
408
+ }),
409
+ );
332
410
  webViewRef.current?.injectJavaScript(`
333
411
  window.postMessage(${JSON.stringify({
334
412
  name: "openFrame",
@@ -411,6 +489,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
411
489
  },
412
490
  [
413
491
  emitEvent,
492
+ emitSession,
414
493
  isWebViewVisible,
415
494
  onMessage,
416
495
  systemInfo,
@@ -470,6 +549,11 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
470
549
  const endpointUrl = `${API_URL}/chat/chatbot/get/`;
471
550
  const payload = {
472
551
  client_user_id: effectiveUserId,
552
+ // Read from the ref rather than the prop so that a later change
553
+ // does not re-trigger the config fetch. Consistent with session_id
554
+ // being an initialization-time input. JSON.stringify drops the key
555
+ // when there is nothing to resume.
556
+ resumable_session_id: resumeSessionRef.current || undefined,
473
557
  org_id: api_key,
474
558
  token: user_token,
475
559
  extra_info: {},
@@ -728,7 +812,17 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
728
812
 
729
813
  return (
730
814
  <ErrorBoundary componentName="ChatbotSDK">
731
- <View style={styles?.mainContainer}>
815
+ {/*
816
+ This root view is flex: 1 and stays mounted while the chat is closed.
817
+ Left hit-testable, it sits invisibly over whatever the host renders
818
+ behind it and swallows every tap. "box-none" exempts the container
819
+ itself while leaving its children interactive, which is what the
820
+ floating button and the dev-only debug button need. The WebView
821
+ wrapper below manages its own pointerEvents.
822
+ */}
823
+ <View
824
+ style={styles?.mainContainer}
825
+ pointerEvents={isWebViewVisible ? "auto" : "box-none"}>
732
826
  {!loading && (
733
827
  <>
734
828
  <ErrorBoundary componentName="FloatingButton">
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useRef } from "react";
2
2
  import { getSystemInfo } from "../utils/systemInfo";
3
3
  import { logger } from "../utils/logger";
4
4
  import {
@@ -23,6 +23,21 @@ export const useChatbotEvents = ({
23
23
  onEvent,
24
24
  systemInfo,
25
25
  }: EventHookProps) => {
26
+ // Held in refs so that emitEvent keeps a stable identity across renders.
27
+ // Host apps routinely pass an inline onEvent and an inline user_profile
28
+ // object literal; with those in the dependency arrays, emitEvent changed
29
+ // identity on every render, which re-ran the effects keyed on it, which
30
+ // re-emitted events, which re-rendered the host. That loop also POSTed to
31
+ // /users/sdk/record-logs/ on every iteration.
32
+ const onEventRef = useRef(onEvent);
33
+ const chatbotConfigRef = useRef(chatbotConfig);
34
+ const userProfileRef = useRef(user_profile);
35
+ const systemInfoRef = useRef(systemInfo);
36
+ onEventRef.current = onEvent;
37
+ chatbotConfigRef.current = chatbotConfig;
38
+ userProfileRef.current = user_profile;
39
+ systemInfoRef.current = systemInfo;
40
+
26
41
  const onInternalEvent = useCallback(
27
42
  async (
28
43
  eventType: AllEventTypes,
@@ -30,9 +45,12 @@ export const useChatbotEvents = ({
30
45
  ) => {
31
46
  if (!api_key) return;
32
47
 
48
+ const chatbotConfig = chatbotConfigRef.current;
49
+ const user_profile = userProfileRef.current;
50
+
33
51
  try {
34
52
  const apiUrl = API_URL;
35
- const sysInfo = getSystemInfo(systemInfo);
53
+ const sysInfo = getSystemInfo(systemInfoRef.current);
36
54
 
37
55
  const eventData = {
38
56
  trigger_time: Date.now(),
@@ -74,16 +92,22 @@ export const useChatbotEvents = ({
74
92
  logger.error("Failed to send internal event:", error);
75
93
  }
76
94
  },
77
- [api_key, chatbotConfig, user_profile, systemInfo]
95
+ [api_key]
78
96
  );
79
97
 
80
98
  const emitEvent = useCallback(
81
99
  (type: ChatbotEventType, data?: any) => {
82
100
  const timestamp = Date.now();
83
- onEvent?.({ type, timestamp, data });
101
+ // Isolated so that a throwing host handler cannot prevent the internal
102
+ // event from being recorded, or break the caller.
103
+ try {
104
+ onEventRef.current?.({ type, timestamp, data });
105
+ } catch (error) {
106
+ logger.error("onEvent handler threw an error:", error);
107
+ }
84
108
  onInternalEvent(type, { ...data, timestamp });
85
109
  },
86
- [onEvent, onInternalEvent]
110
+ [onInternalEvent]
87
111
  );
88
112
 
89
113
  return {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Session id pass-through between the chatbot web app and the SDK client.
3
+ *
4
+ * The session id travels on its own message type so that it never reaches
5
+ * `onEvent`, `onMessage` or the internal `/users/sdk/record-logs/` endpoint.
6
+ * Its only exits from the SDK are the `onSession` prop and `getSessionId()`
7
+ * on the ref, both of which the client has to opt into explicitly.
8
+ */
9
+
10
+ /** Message the web app posts whenever the active session id changes. */
11
+ export const SESSION_UPDATED_MESSAGE_TYPE = "SESSION_UPDATED";
12
+
13
+ /** Key carrying the resume target inside the `registerUserId` payload. */
14
+ export const RESUME_SESSION_KEY = "sessionId";
15
+
16
+ /**
17
+ * Pulls the session id out of a `SESSION_UPDATED` payload.
18
+ *
19
+ * Accepts the snake_case, camelCase and nested spellings so a naming slip on
20
+ * the web side degrades to a no-op rather than a broken build. Anything that
21
+ * is not a non-empty string is dropped.
22
+ */
23
+ export const extractSessionId = (data: any): string | undefined => {
24
+ const raw = data?.session_id ?? data?.sessionId ?? data?.session?.id;
25
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
26
+ };
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not modify it manually.
2
- export const SDK_VERSION = '2.1.1-staging.3';
2
+ export const SDK_VERSION = '2.1.3-staging.0';