@parlr/react-native 0.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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +918 -0
  3. package/lib/commonjs/components/AttachmentPicker.js +292 -0
  4. package/lib/commonjs/components/AttachmentPicker.js.map +1 -0
  5. package/lib/commonjs/components/AttachmentPreview.js +200 -0
  6. package/lib/commonjs/components/AttachmentPreview.js.map +1 -0
  7. package/lib/commonjs/components/ChatBubble.js +391 -0
  8. package/lib/commonjs/components/ChatBubble.js.map +1 -0
  9. package/lib/commonjs/components/EmptyState.js +115 -0
  10. package/lib/commonjs/components/EmptyState.js.map +1 -0
  11. package/lib/commonjs/components/ParlrChat.js +745 -0
  12. package/lib/commonjs/components/ParlrChat.js.map +1 -0
  13. package/lib/commonjs/components/ParlrConversationList.js +509 -0
  14. package/lib/commonjs/components/ParlrConversationList.js.map +1 -0
  15. package/lib/commonjs/components/PreChatForm.js +263 -0
  16. package/lib/commonjs/components/PreChatForm.js.map +1 -0
  17. package/lib/commonjs/components/RichMessage.js +284 -0
  18. package/lib/commonjs/components/RichMessage.js.map +1 -0
  19. package/lib/commonjs/components/SatisfactionSurvey.js +292 -0
  20. package/lib/commonjs/components/SatisfactionSurvey.js.map +1 -0
  21. package/lib/commonjs/components/TypingIndicator.js +86 -0
  22. package/lib/commonjs/components/TypingIndicator.js.map +1 -0
  23. package/lib/commonjs/core/api.js +310 -0
  24. package/lib/commonjs/core/api.js.map +1 -0
  25. package/lib/commonjs/core/config.js +40 -0
  26. package/lib/commonjs/core/config.js.map +1 -0
  27. package/lib/commonjs/core/errors.js +73 -0
  28. package/lib/commonjs/core/errors.js.map +1 -0
  29. package/lib/commonjs/core/offlineQueue.js +89 -0
  30. package/lib/commonjs/core/offlineQueue.js.map +1 -0
  31. package/lib/commonjs/core/pushNotifications.js +21 -0
  32. package/lib/commonjs/core/pushNotifications.js.map +1 -0
  33. package/lib/commonjs/core/session.js +130 -0
  34. package/lib/commonjs/core/session.js.map +1 -0
  35. package/lib/commonjs/core/theme.js +110 -0
  36. package/lib/commonjs/core/theme.js.map +1 -0
  37. package/lib/commonjs/core/types.js +6 -0
  38. package/lib/commonjs/core/types.js.map +1 -0
  39. package/lib/commonjs/core/websocket.js +245 -0
  40. package/lib/commonjs/core/websocket.js.map +1 -0
  41. package/lib/commonjs/hooks/useChat.js +462 -0
  42. package/lib/commonjs/hooks/useChat.js.map +1 -0
  43. package/lib/commonjs/hooks/useParlr.js +44 -0
  44. package/lib/commonjs/hooks/useParlr.js.map +1 -0
  45. package/lib/commonjs/index.js +185 -0
  46. package/lib/commonjs/index.js.map +1 -0
  47. package/lib/commonjs/package.json +1 -0
  48. package/lib/commonjs/provider/ParlrContext.js +38 -0
  49. package/lib/commonjs/provider/ParlrContext.js.map +1 -0
  50. package/lib/commonjs/provider/ParlrProvider.js +256 -0
  51. package/lib/commonjs/provider/ParlrProvider.js.map +1 -0
  52. package/lib/module/components/AttachmentPicker.js +287 -0
  53. package/lib/module/components/AttachmentPicker.js.map +1 -0
  54. package/lib/module/components/AttachmentPreview.js +195 -0
  55. package/lib/module/components/AttachmentPreview.js.map +1 -0
  56. package/lib/module/components/ChatBubble.js +386 -0
  57. package/lib/module/components/ChatBubble.js.map +1 -0
  58. package/lib/module/components/EmptyState.js +110 -0
  59. package/lib/module/components/EmptyState.js.map +1 -0
  60. package/lib/module/components/ParlrChat.js +740 -0
  61. package/lib/module/components/ParlrChat.js.map +1 -0
  62. package/lib/module/components/ParlrConversationList.js +504 -0
  63. package/lib/module/components/ParlrConversationList.js.map +1 -0
  64. package/lib/module/components/PreChatForm.js +258 -0
  65. package/lib/module/components/PreChatForm.js.map +1 -0
  66. package/lib/module/components/RichMessage.js +280 -0
  67. package/lib/module/components/RichMessage.js.map +1 -0
  68. package/lib/module/components/SatisfactionSurvey.js +287 -0
  69. package/lib/module/components/SatisfactionSurvey.js.map +1 -0
  70. package/lib/module/components/TypingIndicator.js +81 -0
  71. package/lib/module/components/TypingIndicator.js.map +1 -0
  72. package/lib/module/core/api.js +305 -0
  73. package/lib/module/core/api.js.map +1 -0
  74. package/lib/module/core/config.js +36 -0
  75. package/lib/module/core/config.js.map +1 -0
  76. package/lib/module/core/errors.js +64 -0
  77. package/lib/module/core/errors.js.map +1 -0
  78. package/lib/module/core/offlineQueue.js +82 -0
  79. package/lib/module/core/offlineQueue.js.map +1 -0
  80. package/lib/module/core/pushNotifications.js +16 -0
  81. package/lib/module/core/pushNotifications.js.map +1 -0
  82. package/lib/module/core/session.js +122 -0
  83. package/lib/module/core/session.js.map +1 -0
  84. package/lib/module/core/theme.js +105 -0
  85. package/lib/module/core/theme.js.map +1 -0
  86. package/lib/module/core/types.js +4 -0
  87. package/lib/module/core/types.js.map +1 -0
  88. package/lib/module/core/websocket.js +241 -0
  89. package/lib/module/core/websocket.js.map +1 -0
  90. package/lib/module/hooks/useChat.js +458 -0
  91. package/lib/module/hooks/useChat.js.map +1 -0
  92. package/lib/module/hooks/useParlr.js +40 -0
  93. package/lib/module/hooks/useParlr.js.map +1 -0
  94. package/lib/module/index.js +58 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/package.json +1 -0
  97. package/lib/module/provider/ParlrContext.js +35 -0
  98. package/lib/module/provider/ParlrContext.js.map +1 -0
  99. package/lib/module/provider/ParlrProvider.js +251 -0
  100. package/lib/module/provider/ParlrProvider.js.map +1 -0
  101. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts +23 -0
  102. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts +16 -0
  104. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/ChatBubble.d.ts +14 -0
  106. package/lib/typescript/commonjs/components/ChatBubble.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/EmptyState.d.ts +10 -0
  108. package/lib/typescript/commonjs/components/EmptyState.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/ParlrChat.d.ts +34 -0
  110. package/lib/typescript/commonjs/components/ParlrChat.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts +17 -0
  112. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/components/PreChatForm.d.ts +20 -0
  114. package/lib/typescript/commonjs/components/PreChatForm.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/components/RichMessage.d.ts +41 -0
  116. package/lib/typescript/commonjs/components/RichMessage.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts +17 -0
  118. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/components/TypingIndicator.d.ts +7 -0
  120. package/lib/typescript/commonjs/components/TypingIndicator.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/core/api.d.ts +37 -0
  122. package/lib/typescript/commonjs/core/api.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/core/config.d.ts +9 -0
  124. package/lib/typescript/commonjs/core/config.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/core/errors.d.ts +35 -0
  126. package/lib/typescript/commonjs/core/errors.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/core/offlineQueue.d.ts +16 -0
  128. package/lib/typescript/commonjs/core/offlineQueue.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/core/pushNotifications.d.ts +6 -0
  130. package/lib/typescript/commonjs/core/pushNotifications.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/core/session.d.ts +15 -0
  132. package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/core/theme.d.ts +43 -0
  134. package/lib/typescript/commonjs/core/theme.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/core/types.d.ts +185 -0
  136. package/lib/typescript/commonjs/core/types.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/core/websocket.d.ts +17 -0
  138. package/lib/typescript/commonjs/core/websocket.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/hooks/useChat.d.ts +35 -0
  140. package/lib/typescript/commonjs/hooks/useChat.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/hooks/useParlr.d.ts +11 -0
  142. package/lib/typescript/commonjs/hooks/useParlr.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/index.d.ts +30 -0
  144. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/package.json +1 -0
  146. package/lib/typescript/commonjs/provider/ParlrContext.d.ts +13 -0
  147. package/lib/typescript/commonjs/provider/ParlrContext.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts +5 -0
  149. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts.map +1 -0
  150. package/lib/typescript/module/components/AttachmentPicker.d.ts +23 -0
  151. package/lib/typescript/module/components/AttachmentPicker.d.ts.map +1 -0
  152. package/lib/typescript/module/components/AttachmentPreview.d.ts +16 -0
  153. package/lib/typescript/module/components/AttachmentPreview.d.ts.map +1 -0
  154. package/lib/typescript/module/components/ChatBubble.d.ts +14 -0
  155. package/lib/typescript/module/components/ChatBubble.d.ts.map +1 -0
  156. package/lib/typescript/module/components/EmptyState.d.ts +10 -0
  157. package/lib/typescript/module/components/EmptyState.d.ts.map +1 -0
  158. package/lib/typescript/module/components/ParlrChat.d.ts +34 -0
  159. package/lib/typescript/module/components/ParlrChat.d.ts.map +1 -0
  160. package/lib/typescript/module/components/ParlrConversationList.d.ts +17 -0
  161. package/lib/typescript/module/components/ParlrConversationList.d.ts.map +1 -0
  162. package/lib/typescript/module/components/PreChatForm.d.ts +20 -0
  163. package/lib/typescript/module/components/PreChatForm.d.ts.map +1 -0
  164. package/lib/typescript/module/components/RichMessage.d.ts +41 -0
  165. package/lib/typescript/module/components/RichMessage.d.ts.map +1 -0
  166. package/lib/typescript/module/components/SatisfactionSurvey.d.ts +17 -0
  167. package/lib/typescript/module/components/SatisfactionSurvey.d.ts.map +1 -0
  168. package/lib/typescript/module/components/TypingIndicator.d.ts +7 -0
  169. package/lib/typescript/module/components/TypingIndicator.d.ts.map +1 -0
  170. package/lib/typescript/module/core/api.d.ts +37 -0
  171. package/lib/typescript/module/core/api.d.ts.map +1 -0
  172. package/lib/typescript/module/core/config.d.ts +9 -0
  173. package/lib/typescript/module/core/config.d.ts.map +1 -0
  174. package/lib/typescript/module/core/errors.d.ts +35 -0
  175. package/lib/typescript/module/core/errors.d.ts.map +1 -0
  176. package/lib/typescript/module/core/offlineQueue.d.ts +16 -0
  177. package/lib/typescript/module/core/offlineQueue.d.ts.map +1 -0
  178. package/lib/typescript/module/core/pushNotifications.d.ts +6 -0
  179. package/lib/typescript/module/core/pushNotifications.d.ts.map +1 -0
  180. package/lib/typescript/module/core/session.d.ts +15 -0
  181. package/lib/typescript/module/core/session.d.ts.map +1 -0
  182. package/lib/typescript/module/core/theme.d.ts +43 -0
  183. package/lib/typescript/module/core/theme.d.ts.map +1 -0
  184. package/lib/typescript/module/core/types.d.ts +185 -0
  185. package/lib/typescript/module/core/types.d.ts.map +1 -0
  186. package/lib/typescript/module/core/websocket.d.ts +17 -0
  187. package/lib/typescript/module/core/websocket.d.ts.map +1 -0
  188. package/lib/typescript/module/hooks/useChat.d.ts +35 -0
  189. package/lib/typescript/module/hooks/useChat.d.ts.map +1 -0
  190. package/lib/typescript/module/hooks/useParlr.d.ts +11 -0
  191. package/lib/typescript/module/hooks/useParlr.d.ts.map +1 -0
  192. package/lib/typescript/module/index.d.ts +30 -0
  193. package/lib/typescript/module/index.d.ts.map +1 -0
  194. package/lib/typescript/module/package.json +1 -0
  195. package/lib/typescript/module/provider/ParlrContext.d.ts +13 -0
  196. package/lib/typescript/module/provider/ParlrContext.d.ts.map +1 -0
  197. package/lib/typescript/module/provider/ParlrProvider.d.ts +5 -0
  198. package/lib/typescript/module/provider/ParlrProvider.d.ts.map +1 -0
  199. package/package.json +120 -0
  200. package/src/components/AttachmentPicker.tsx +310 -0
  201. package/src/components/AttachmentPreview.tsx +209 -0
  202. package/src/components/ChatBubble.tsx +424 -0
  203. package/src/components/EmptyState.tsx +118 -0
  204. package/src/components/ParlrChat.tsx +863 -0
  205. package/src/components/ParlrConversationList.tsx +559 -0
  206. package/src/components/PreChatForm.tsx +313 -0
  207. package/src/components/RichMessage.tsx +353 -0
  208. package/src/components/SatisfactionSurvey.tsx +333 -0
  209. package/src/components/TypingIndicator.tsx +89 -0
  210. package/src/core/api.ts +406 -0
  211. package/src/core/config.ts +39 -0
  212. package/src/core/errors.ts +68 -0
  213. package/src/core/offlineQueue.ts +94 -0
  214. package/src/core/pushNotifications.ts +22 -0
  215. package/src/core/session.ts +156 -0
  216. package/src/core/theme.ts +133 -0
  217. package/src/core/types.ts +237 -0
  218. package/src/core/websocket.ts +270 -0
  219. package/src/hooks/useChat.ts +534 -0
  220. package/src/hooks/useParlr.ts +43 -0
  221. package/src/index.ts +98 -0
  222. package/src/provider/ParlrContext.ts +40 -0
  223. package/src/provider/ParlrProvider.tsx +338 -0
@@ -0,0 +1,40 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - React Context
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { createContext } from 'react';
6
+ import type { ParlrApiClient } from '../core/api';
7
+ import { defaultLightTheme } from '../core/theme';
8
+ import type { ParlrContextValue, ResolvedConfig } from '../core/types';
9
+ import type { ParlrWebSocket } from '../core/websocket';
10
+
11
+ /**
12
+ * Public context consumed by hooks (`useParlr`, `useChat`).
13
+ * Includes both user-facing state and internal plumbing (api, ws).
14
+ */
15
+ export interface ParlrContextInternal extends ParlrContextValue {
16
+ api: ParlrApiClient | null;
17
+ ws: ParlrWebSocket | null;
18
+ }
19
+
20
+ const NOOP = async () => {};
21
+
22
+ export const ParlrContext = createContext<ParlrContextInternal>({
23
+ config: {
24
+ workspaceId: '',
25
+ apiBaseUrl: '',
26
+ wsUrl: '',
27
+ locale: 'fr',
28
+ debug: false,
29
+ } satisfies ResolvedConfig,
30
+ session: null,
31
+ isReady: false,
32
+ isConnected: false,
33
+ conversations: [],
34
+ unreadCount: 0,
35
+ identify: NOOP,
36
+ refreshConversations: NOOP,
37
+ theme: defaultLightTheme,
38
+ api: null,
39
+ ws: null,
40
+ });
@@ -0,0 +1,338 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Provider
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Initializes the session, REST client and WebSocket on mount, then provides
6
+ // them to the rest of the tree through React context.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React, {
10
+ useCallback,
11
+ useEffect,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ type PropsWithChildren,
16
+ } from 'react';
17
+ import { useColorScheme } from 'react-native';
18
+ import { createApiClient, type ParlrApiClient } from '../core/api';
19
+ import { resolveConfig } from '../core/config';
20
+ import { ParlrError } from '../core/errors';
21
+ import {
22
+ clearSession,
23
+ isSessionValid,
24
+ loadSession,
25
+ saveSession,
26
+ } from '../core/session';
27
+ import {
28
+ defaultDarkTheme,
29
+ defaultLightTheme,
30
+ mergeTheme,
31
+ } from '../core/theme';
32
+ import type {
33
+ Conversation,
34
+ ParlrConfig,
35
+ ParlrUser,
36
+ ResolvedConfig,
37
+ Session,
38
+ } from '../core/types';
39
+ import { createWebSocket, type ParlrWebSocket } from '../core/websocket';
40
+ import { ParlrContext, type ParlrContextInternal } from './ParlrContext';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Props
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export type ParlrProviderProps = PropsWithChildren<ParlrConfig>;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Debounce helper
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function useDebouncedCallback<T extends (...args: never[]) => void>(
53
+ fn: T,
54
+ delayMs: number,
55
+ ): T {
56
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
57
+ const fnRef = useRef(fn);
58
+ fnRef.current = fn;
59
+
60
+ return useCallback(
61
+ ((...args: Parameters<T>) => {
62
+ if (timerRef.current) clearTimeout(timerRef.current);
63
+ timerRef.current = setTimeout(() => fnRef.current(...args), delayMs);
64
+ }) as T,
65
+ [delayMs],
66
+ );
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Component
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export function ParlrProvider({
74
+ children,
75
+ ...configProps
76
+ }: ParlrProviderProps) {
77
+ // --- Resolved config (stable across renders unless props change) ----------
78
+ const config: ResolvedConfig = useMemo(
79
+ () => resolveConfig(configProps),
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ [
82
+ configProps.workspaceId,
83
+ configProps.apiBaseUrl,
84
+ configProps.wsUrl,
85
+ configProps.locale,
86
+ configProps.debug,
87
+ configProps.onError,
88
+ ],
89
+ );
90
+
91
+ // --- Resolve theme based on system color scheme + user overrides ----------
92
+ const colorScheme = useColorScheme();
93
+ const theme = useMemo(() => {
94
+ const base = colorScheme === 'dark' ? defaultDarkTheme : defaultLightTheme;
95
+ return configProps.theme ? mergeTheme(base, configProps.theme) : base;
96
+ }, [colorScheme, configProps.theme]);
97
+
98
+ // --- State -----------------------------------------------------------------
99
+ const [session, setSession] = useState<Session | null>(null);
100
+ const [isReady, setIsReady] = useState(false);
101
+ const [isConnected, setIsConnected] = useState(false);
102
+ const [conversations, setConversations] = useState<Conversation[]>([]);
103
+
104
+ // --- Refs (singletons across the provider lifecycle) -----------------------
105
+ const sessionRef = useRef<Session | null>(null);
106
+ const apiRef = useRef<ParlrApiClient | null>(null);
107
+ const wsRef = useRef<ParlrWebSocket | null>(null);
108
+
109
+ // Keep ref in sync with state so the token accessor always returns latest.
110
+ sessionRef.current = session;
111
+
112
+ // --- Create API client (once per config) -----------------------------------
113
+ useEffect(() => {
114
+ const api = createApiClient(config, () => sessionRef.current?.token ?? null);
115
+ apiRef.current = api;
116
+ }, [config]);
117
+
118
+ // --- Fetch conversations (debounced) ---------------------------------------
119
+ const refreshConversationsImpl = useCallback(async () => {
120
+ const api = apiRef.current;
121
+ /* istanbul ignore next -- defensive: apiRef always set by preceding effect */
122
+ if (!api || !sessionRef.current) return;
123
+
124
+ try {
125
+ const res = await api.listConversations();
126
+ setConversations(res.data);
127
+ } catch (err) {
128
+ if (config.debug) {
129
+ console.warn(
130
+ '[@parlr/react-native] Failed to refresh conversations:',
131
+ err,
132
+ );
133
+ }
134
+ config.onError?.(err instanceof ParlrError ? err : new ParlrError(err instanceof Error ? err.message : String(err)));
135
+ }
136
+ }, [config]);
137
+
138
+ const refreshConversations = useDebouncedCallback(
139
+ refreshConversationsImpl as (...args: never[]) => void,
140
+ 500,
141
+ ) as unknown as () => Promise<void>;
142
+
143
+ // --- Create WS client (once per config) ------------------------------------
144
+ useEffect(() => {
145
+ const ws = createWebSocket(config);
146
+ wsRef.current = ws;
147
+
148
+ const offConnected = ws.on('auth_ok', () => setIsConnected(true));
149
+ const offAuthError = ws.on('auth_error', () => setIsConnected(false));
150
+ const offDisconnected = ws.on('disconnected', () => setIsConnected(false));
151
+
152
+ // When a new message arrives for any conversation, refresh list.
153
+ const offMessage = ws.on('new_message', () => {
154
+ refreshConversations();
155
+ });
156
+
157
+ const offConvUpdated = ws.on('conversation_updated', () => {
158
+ refreshConversations();
159
+ });
160
+
161
+ return () => {
162
+ offConnected();
163
+ offAuthError();
164
+ offDisconnected();
165
+ offMessage();
166
+ offConvUpdated();
167
+ ws.disconnect();
168
+ };
169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
170
+ }, [config]);
171
+
172
+ // --- Bootstrap session on mount --------------------------------------------
173
+ useEffect(() => {
174
+ let cancelled = false;
175
+
176
+ async function bootstrap() {
177
+ const api = apiRef.current;
178
+ if (!api) return;
179
+
180
+ try {
181
+ // 1. Try to restore a persisted session.
182
+ const existing = await loadSession(config.workspaceId);
183
+
184
+ if (existing && isSessionValid(existing)) {
185
+ if (!cancelled) {
186
+ setSession(existing);
187
+ sessionRef.current = existing;
188
+ }
189
+ } else {
190
+ // 2. Create a new session.
191
+ if (existing) {
192
+ await clearSession(config.workspaceId);
193
+ }
194
+
195
+ const newSession = await api.createSession();
196
+ await saveSession(config.workspaceId, newSession);
197
+
198
+ if (!cancelled) {
199
+ setSession(newSession);
200
+ sessionRef.current = newSession;
201
+ }
202
+ }
203
+
204
+ if (!cancelled) {
205
+ setIsReady(true);
206
+ }
207
+ } catch (err) {
208
+ console.error(
209
+ '[@parlr/react-native] Failed to bootstrap session:',
210
+ err,
211
+ );
212
+ config.onError?.(err instanceof ParlrError ? err : new ParlrError(err instanceof Error ? err.message : String(err)));
213
+ if (!cancelled) {
214
+ setIsReady(true); // still "ready" so UI can render an error state
215
+ }
216
+ }
217
+ }
218
+
219
+ bootstrap();
220
+
221
+ return () => {
222
+ cancelled = true;
223
+ };
224
+ }, [config]);
225
+
226
+ // --- Connect WebSocket once session is available ---------------------------
227
+ useEffect(() => {
228
+ if (session?.token && wsRef.current) {
229
+ wsRef.current.connect(session.token);
230
+ }
231
+ }, [session]);
232
+
233
+ // --- Auto-refresh session when expired -------------------------------------
234
+ useEffect(() => {
235
+ if (!session || !apiRef.current) return;
236
+
237
+ // Check session validity periodically.
238
+ const interval = setInterval(async () => {
239
+ if (session && !isSessionValid(session)) {
240
+ if (config.debug) {
241
+ console.log('[@parlr/react-native] Session expired, refreshing...');
242
+ }
243
+ try {
244
+ const api = apiRef.current;
245
+ /* istanbul ignore next -- defensive: apiRef always set by preceding effect */
246
+ if (!api) return;
247
+
248
+ const newSession = await api.createSession();
249
+ await saveSession(config.workspaceId, newSession);
250
+ setSession(newSession);
251
+ sessionRef.current = newSession;
252
+ } catch (err) {
253
+ console.error(
254
+ '[@parlr/react-native] Failed to refresh session:',
255
+ err,
256
+ );
257
+ config.onError?.(err instanceof ParlrError ? err : new ParlrError(err instanceof Error ? err.message : String(err)));
258
+ }
259
+ }
260
+ }, 60_000); // Check every minute.
261
+
262
+ return () => clearInterval(interval);
263
+ }, [session, config]);
264
+
265
+ // --- Fetch conversations once session is ready -----------------------------
266
+ useEffect(() => {
267
+ if (isReady && session) {
268
+ refreshConversationsImpl();
269
+ }
270
+ }, [isReady, session, refreshConversationsImpl]);
271
+
272
+ // --- Poll conversations when WebSocket is not connected --------------------
273
+ // Without WS, the conversation list (unread counts, new conversations from
274
+ // agent replies) would never update. Poll every 10 s as a fallback.
275
+ useEffect(() => {
276
+ if (!isReady || !session) return;
277
+ // Skip polling if WS is live — the new_message listener already refreshes.
278
+ if (isConnected) return;
279
+
280
+ const interval = setInterval(() => {
281
+ refreshConversationsImpl();
282
+ }, 10_000);
283
+
284
+ return () => clearInterval(interval);
285
+ }, [isReady, session, isConnected, refreshConversationsImpl]);
286
+
287
+ // --- Identify callback -----------------------------------------------------
288
+ const identify = useCallback(
289
+ async (user: ParlrUser) => {
290
+ const api = apiRef.current;
291
+ if (!api) {
292
+ throw new Error(
293
+ '[@parlr/react-native] Cannot identify before SDK is initialised.',
294
+ );
295
+ }
296
+ await api.identify(user);
297
+ },
298
+ [],
299
+ );
300
+
301
+ // --- Unread count (derived) ------------------------------------------------
302
+ const unreadCount = useMemo(
303
+ () => conversations.reduce((sum, c) => sum + c.unreadCount, 0),
304
+ [conversations],
305
+ );
306
+
307
+ // --- Context value ---------------------------------------------------------
308
+ const value: ParlrContextInternal = useMemo(
309
+ () => ({
310
+ config,
311
+ session,
312
+ isReady,
313
+ isConnected,
314
+ conversations,
315
+ unreadCount,
316
+ identify,
317
+ refreshConversations: refreshConversationsImpl,
318
+ theme,
319
+ api: apiRef.current,
320
+ ws: wsRef.current,
321
+ }),
322
+ [
323
+ config,
324
+ session,
325
+ isReady,
326
+ isConnected,
327
+ conversations,
328
+ unreadCount,
329
+ identify,
330
+ refreshConversationsImpl,
331
+ theme,
332
+ ],
333
+ );
334
+
335
+ return (
336
+ <ParlrContext.Provider value={value}>{children}</ParlrContext.Provider>
337
+ );
338
+ }