turbodesk-livechat-react-native 0.1.0-alpha.24 → 0.1.0-alpha.25

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,397 +1,392 @@
1
- import React, {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from "react";
8
- import { useColorScheme, unstable_batchedUpdates, Platform } from "react-native";
9
- import { LiveChatContext } from "./LiveChatContext";
10
- import type { EmbedConfigLoadState, LiveChatProviderProps } from "./types";
11
- import {
12
- setApiBaseUrl,
13
- setTokenGetter,
14
- registerAuthRefresh,
15
- unregisterAuthRefresh,
16
- } from "../axios/axios";
17
- import {
18
- getOrCreateDeviceUserId,
19
- clearStoredDeviceUserId,
20
- } from "../core/identity";
21
- import { buildVisitorQueryParams } from "../core/visitor-params";
22
- import { widgetApi } from "../api/widget-api";
23
- import { conversationApi, deviceTokenApi } from "../api/conversation-api";
24
- import { WsClient } from "../realtime/ws-client";
25
- import type { WsConnectionState } from "../realtime/ws-client";
26
- import {
27
- buildThemeFromBrand,
28
- defaultTheme,
29
- } from "../ui/theme";
30
-
31
- function getWidgetName(config: any): string {
32
- return config?.widgetName?.trim() || "Chat with us";
33
- }
34
-
35
- function getWidgetGreeting(config: any): string {
36
- const s = config?.widgetSettings;
37
- const g = (s?.greetings ?? s?.greeting ?? "").toString().trim();
38
- return g || "Hi! How can we help you today?";
39
- }
40
-
41
- function getWidgetBrandColor(config: any): string | null {
42
- const color = config?.widgetSettings?.brandColor?.trim();
43
- return color || null;
44
- }
45
-
46
- function pickUnread(raw: any): number | null {
47
- const body = raw?.data ?? raw;
48
- const inner = body?.data ?? body;
49
- const v = inner?.totalUnread ?? inner?.unread;
50
- const n = typeof v === "string" ? parseInt(v, 10) : Number(v);
51
- return Number.isFinite(n) ? n : null;
52
- }
53
-
54
- export function LiveChatProvider({
55
- widgetId: widgetIdProp,
56
- apiBaseUrl: apiBaseUrlProp,
57
- userId: userIdProp,
58
- userToken: userTokenProp,
59
- authCallback,
60
- visitorProfile,
61
- appearance: appearanceProp,
62
- useBrandThemingForChat: useBrandThemingProp = false,
63
- theme: themeProp,
64
- fcmToken,
65
- fcmTokenPlatform,
66
- children,
67
- }: LiveChatProviderProps) {
68
- const systemColorScheme = useColorScheme();
69
-
70
- const cfg = useMemo(
71
- () => ({
72
- widgetId: widgetIdProp?.trim() ?? "",
73
- apiBaseUrl: apiBaseUrlProp?.replace(/\/+$/, "") ?? "",
74
- }),
75
- [widgetIdProp, apiBaseUrlProp]
76
- );
77
-
78
- const [userId, setUserId] = useState<string>(userIdProp ?? "");
79
- const [userIdReady, setUserIdReady] = useState<boolean>(
80
- !!(userIdProp?.trim() || userTokenProp?.trim())
81
- );
82
- const [userToken, setUserTokenState] = useState<string | undefined>(
83
- userTokenProp
84
- );
85
- const [widgetConfig, setWidgetConfig] = useState<any>(null);
86
- const [embedLoadState, setEmbedLoadState] =
87
- useState<EmbedConfigLoadState>("loading");
88
- const [embedLoadError, setEmbedLoadError] = useState<string | null>(null);
89
- const [totalUnread, setTotalUnread] = useState(0);
90
- const [isOpen, setIsOpen] = useState(false);
91
- const [isVisible, setIsVisible] = useState(true);
92
- const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
93
- const [connectionState, setConnectionState] = useState<WsConnectionState>({
94
- isConnected: false,
95
- isConnecting: false,
96
- isAwaitingRetry: false,
97
- lastError: null,
98
- });
99
-
100
- const wsClientRef = useRef<WsClient | null>(null);
101
- const userTokenRef = useRef(userToken);
102
- userTokenRef.current = userToken;
103
-
104
- // ── HTTP client bootstrap ──────────────────────────────────────────────
105
- useEffect(() => {
106
- setApiBaseUrl(cfg.apiBaseUrl);
107
- setTokenGetter(() => userTokenRef.current ?? null);
108
- }, [cfg.apiBaseUrl]);
109
-
110
- // ── auth refresh registration ──────────────────────────────────────────
111
- useEffect(() => {
112
- if (!authCallback) return;
113
- registerAuthRefresh(authCallback, (newToken) => {
114
- setToken(typeof newToken === "string" ? newToken : null);
115
- });
116
- return () => unregisterAuthRefresh();
117
- // eslint-disable-next-line react-hooks/exhaustive-deps
118
- }, [authCallback]);
119
-
120
- // ── resolve userId ─────────────────────────────────────────────────────
121
- useEffect(() => {
122
- if (userTokenProp) {
123
- setUserId(userIdProp ?? "");
124
- setUserTokenState(userTokenProp.trim());
125
- setUserIdReady(true);
126
- return;
127
- }
128
- const uid = userIdProp?.trim();
129
- if (uid) {
130
- setUserId(uid);
131
- setUserIdReady(true);
132
- return;
133
- }
134
- let cancelled = false;
135
- getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
136
- if (!cancelled) {
137
- setUserId(id);
138
- setUserIdReady(true);
139
- }
140
- });
141
- return () => {
142
- cancelled = true;
143
- };
144
- }, [cfg.widgetId, userIdProp, userTokenProp]);
145
-
146
- // ── propagate token changes from parent ───────────────────────────────
147
- useEffect(() => {
148
- setUserTokenState(userTokenProp?.trim() || undefined);
149
- }, [userTokenProp]);
150
-
151
- // ── widget config fetch ────────────────────────────────────────────────
152
- useEffect(() => {
153
- if (!cfg.widgetId) return;
154
- setEmbedLoadState("loading");
155
- setEmbedLoadError(null);
156
- let cancelled = false;
157
- (async () => {
158
- try {
159
- const raw = await widgetApi.getWidget(cfg.widgetId);
160
- if (cancelled) return;
161
- const config = raw?.data ?? raw;
162
- unstable_batchedUpdates(() => {
163
- setWidgetConfig(config);
164
- setEmbedLoadError(null);
165
- setEmbedLoadState("ready");
166
- });
167
- } catch (e) {
168
- if (cancelled) return;
169
- unstable_batchedUpdates(() => {
170
- setEmbedLoadError(
171
- e instanceof Error ? e.message : "Failed to load widget config"
172
- );
173
- setEmbedLoadState("error");
174
- });
175
- }
176
- })();
177
- return () => {
178
- cancelled = true;
179
- };
180
- }, [cfg.widgetId]);
181
-
182
- // ── load initial unread count ──────────────────────────────────────────
183
- const visitorQueryParams = useMemo(
184
- () =>
185
- buildVisitorQueryParams({
186
- widgetId: cfg.widgetId,
187
- userId,
188
- userToken,
189
- visitorProfile,
190
- }),
191
- [cfg.widgetId, userId, userToken, visitorProfile]
192
- );
193
-
194
- useEffect(() => {
195
- if (!visitorQueryParams) return;
196
- let cancelled = false;
197
- (async () => {
198
- try {
199
- const raw = await conversationApi.getTotalUnread({
200
- params: visitorQueryParams,
201
- });
202
- if (cancelled) return;
203
- const n = pickUnread(raw);
204
- if (n !== null) setTotalUnread(Math.max(0, n));
205
- } catch {
206
- /* best-effort */
207
- }
208
- })();
209
- return () => {
210
- cancelled = true;
211
- };
212
- }, [visitorQueryParams]);
213
-
214
- // ── FCM token registration ─────────────────────────────────────────────
215
- useEffect(() => {
216
- if (!fcmToken || !visitorQueryParams) return;
217
- const platform = fcmTokenPlatform ?? (Platform.OS === "ios" ? "ios" : "android");
218
- deviceTokenApi.register({ token: fcmToken, platform }, { params: visitorQueryParams }).catch(() => {});
219
- }, [fcmToken, fcmTokenPlatform, visitorQueryParams]);
220
-
221
- // ── WsClient lifecycle ─────────────────────────────────────────────────
222
- useEffect(() => {
223
- if (!cfg.widgetId || !userId || !userIdReady) return;
224
-
225
- const client = new WsClient({
226
- widgetId: cfg.widgetId,
227
- userId,
228
- userToken,
229
- });
230
-
231
- const unsub = client.onStateChange((s) => setConnectionState({ ...s }));
232
-
233
- const unsubMsg = client.subscribe("new_livechat_message", (payload: any) => {
234
- const data = payload?.data ?? payload;
235
- const msg = data?.message;
236
- if (msg?.senderType === "contact") return;
237
- setTotalUnread((n) => n + 1);
238
- });
239
-
240
- const unsubRead = client.subscribe(
241
- "visitor_conversation_messages_marked_as_read",
242
- (payload: any) => {
243
- const data = payload?.data ?? payload;
244
- const raw = data?.visitorUnreadCount;
245
- const parsed =
246
- typeof raw === "string" ? parseInt(raw, 10) : Number(raw);
247
- if (Number.isFinite(parsed)) {
248
- setTotalUnread(Math.max(0, parsed));
249
- return;
250
- }
251
- const deltaRaw = data?.messageCount;
252
- const delta =
253
- typeof deltaRaw === "string"
254
- ? parseInt(deltaRaw, 10)
255
- : Number(deltaRaw);
256
- if (Number.isFinite(delta) && delta > 0)
257
- setTotalUnread((n) => Math.max(0, n - delta));
258
- }
259
- );
260
-
261
- client.start();
262
- wsClientRef.current = client;
263
-
264
- return () => {
265
- unsub();
266
- unsubMsg();
267
- unsubRead();
268
- client.destroy();
269
- wsClientRef.current = null;
270
- };
271
- // eslint-disable-next-line react-hooks/exhaustive-deps
272
- }, [cfg.widgetId, cfg.apiBaseUrl, userId, userIdReady]);
273
-
274
- useEffect(() => {
275
- wsClientRef.current?.updateIdentity({ userId, userToken });
276
- }, [userId, userToken]);
277
-
278
- // ── public actions ─────────────────────────────────────────────────────
279
- const setToken = useCallback(
280
- (token: string | null | undefined) => {
281
- const trimmed = typeof token === "string" ? token.trim() : "";
282
- if (trimmed) {
283
- setUserTokenState(trimmed);
284
- setUserId("");
285
- setUserIdReady(true);
286
- return;
287
- }
288
- setUserTokenState(undefined);
289
- getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
290
- setUserId(id);
291
- setUserIdReady(true);
292
- });
293
- },
294
- [cfg.widgetId]
295
- );
296
-
297
- const logout = useCallback(() => {
298
- // Unregister FCM token before clearing identity
299
- if (fcmToken && visitorQueryParams) {
300
- deviceTokenApi.unregister({ token: fcmToken }, { params: visitorQueryParams }).catch(() => {});
301
- }
302
- setUserTokenState(undefined);
303
- clearStoredDeviceUserId(cfg.widgetId).then(() =>
304
- getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
305
- setUserId(id);
306
- setUserIdReady(true);
307
- })
308
- );
309
- }, [cfg.widgetId, fcmToken, visitorQueryParams]);
310
-
311
- const open = useCallback(() => setIsOpen(true), []);
312
- const close = useCallback(() => setIsOpen(false), []);
313
- const show = useCallback(() => setIsVisible(true), []);
314
- const hide = useCallback(() => {
315
- setIsOpen(false);
316
- setIsVisible(false);
317
- }, []);
318
-
319
- // ── theme resolution ───────────────────────────────────────────────────
320
- const theme = useMemo(() => {
321
- const brandColor =
322
- getWidgetBrandColor(widgetConfig) ?? defaultTheme.colors.brand;
323
-
324
- const resolvedAppearance =
325
- appearanceProp === "system"
326
- ? systemColorScheme === "dark"
327
- ? "dark"
328
- : "light"
329
- : appearanceProp ?? "light";
330
-
331
- return buildThemeFromBrand(
332
- brandColor,
333
- resolvedAppearance,
334
- useBrandThemingProp,
335
- themeProp
336
- );
337
- }, [widgetConfig, appearanceProp, systemColorScheme, useBrandThemingProp, themeProp]);
338
-
339
- const contextValue = useMemo(
340
- () => ({
341
- widgetId: cfg.widgetId,
342
- userId,
343
- userToken,
344
- setToken,
345
- logout,
346
- visitorProfile,
347
- visitorQueryParams,
348
- widgetConfig,
349
- widgetName: getWidgetName(widgetConfig),
350
- widgetGreeting: getWidgetGreeting(widgetConfig),
351
- embedLoadState,
352
- embedLoadError,
353
- totalUnread,
354
- setTotalUnread,
355
- isOpen,
356
- open,
357
- close,
358
- isVisible,
359
- show,
360
- hide,
361
- wsClient: wsClientRef.current,
362
- connectionState,
363
- activeConversationId,
364
- setActiveConversationId,
365
- theme,
366
- }),
367
- [
368
- cfg.widgetId,
369
- userId,
370
- userToken,
371
- setToken,
372
- logout,
373
- visitorProfile,
374
- visitorQueryParams,
375
- widgetConfig,
376
- embedLoadState,
377
- embedLoadError,
378
- totalUnread,
379
- isOpen,
380
- open,
381
- close,
382
- isVisible,
383
- show,
384
- hide,
385
- connectionState,
386
- activeConversationId,
387
- setActiveConversationId,
388
- theme,
389
- ]
390
- );
391
-
392
- return (
393
- <LiveChatContext.Provider value={contextValue}>
394
- {children}
395
- </LiveChatContext.Provider>
396
- );
397
- }
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { useColorScheme, unstable_batchedUpdates, Platform } from "react-native";
9
+ import { LiveChatContext } from "./LiveChatContext";
10
+ import type { EmbedConfigLoadState, LiveChatProviderProps } from "./types";
11
+ import {
12
+ setApiBaseUrl,
13
+ setTokenGetter,
14
+ registerAuthRefresh,
15
+ unregisterAuthRefresh,
16
+ } from "../axios/axios";
17
+ import {
18
+ getOrCreateDeviceUserId,
19
+ clearStoredDeviceUserId,
20
+ } from "../core/identity";
21
+ import { buildVisitorQueryParams } from "../core/visitor-params";
22
+ import { widgetApi } from "../api/widget-api";
23
+ import { conversationApi, deviceTokenApi } from "../api/conversation-api";
24
+ import { WsClient } from "../realtime/ws-client";
25
+ import type { WsConnectionState } from "../realtime/ws-client";
26
+ import {
27
+ buildThemeFromBrand,
28
+ defaultTheme,
29
+ } from "../ui/theme";
30
+
31
+ function getWidgetName(config: any): string {
32
+ return config?.widgetName?.trim() || "Chat with us";
33
+ }
34
+
35
+ function getWidgetGreeting(config: any): string {
36
+ const s = config?.widgetSettings;
37
+ const g = (s?.greetings ?? s?.greeting ?? "").toString().trim();
38
+ return g || "Hi! How can we help you today?";
39
+ }
40
+
41
+ function getWidgetBrandColor(config: any): string | null {
42
+ const color = config?.widgetSettings?.brandColor?.trim();
43
+ return color || null;
44
+ }
45
+
46
+ function pickUnread(raw: any): number | null {
47
+ const body = raw?.data ?? raw;
48
+ const inner = body?.data ?? body;
49
+ const v = inner?.totalUnread ?? inner?.unread;
50
+ const n = typeof v === "string" ? parseInt(v, 10) : Number(v);
51
+ return Number.isFinite(n) ? n : null;
52
+ }
53
+
54
+ export function LiveChatProvider({
55
+ widgetId: widgetIdProp,
56
+ apiBaseUrl: apiBaseUrlProp,
57
+ userId: userIdProp,
58
+ userToken: userTokenProp,
59
+ authCallback,
60
+ visitorProfile,
61
+ appearance: appearanceProp,
62
+ useBrandThemingForChat: useBrandThemingProp = false,
63
+ theme: themeProp,
64
+ fcmToken,
65
+ fcmTokenPlatform,
66
+ children,
67
+ }: LiveChatProviderProps) {
68
+ const systemColorScheme = useColorScheme();
69
+
70
+ const cfg = useMemo(
71
+ () => ({
72
+ widgetId: widgetIdProp?.trim() ?? "",
73
+ apiBaseUrl: apiBaseUrlProp?.replace(/\/+$/, "") ?? "",
74
+ }),
75
+ [widgetIdProp, apiBaseUrlProp]
76
+ );
77
+
78
+ const [userId, setUserId] = useState<string>(userIdProp ?? "");
79
+ const [userIdReady, setUserIdReady] = useState<boolean>(
80
+ !!(userIdProp?.trim() || userTokenProp?.trim())
81
+ );
82
+ const [userToken, setUserTokenState] = useState<string | undefined>(
83
+ userTokenProp
84
+ );
85
+ const [widgetConfig, setWidgetConfig] = useState<any>(null);
86
+ const [embedLoadState, setEmbedLoadState] =
87
+ useState<EmbedConfigLoadState>("loading");
88
+ const [embedLoadError, setEmbedLoadError] = useState<string | null>(null);
89
+ const [totalUnread, setTotalUnread] = useState(0);
90
+ const [isOpen, setIsOpen] = useState(false);
91
+ const [isVisible, setIsVisible] = useState(true);
92
+ const [connectionState, setConnectionState] = useState<WsConnectionState>({
93
+ isConnected: false,
94
+ isConnecting: false,
95
+ isAwaitingRetry: false,
96
+ lastError: null,
97
+ });
98
+
99
+ const wsClientRef = useRef<WsClient | null>(null);
100
+ const userTokenRef = useRef(userToken);
101
+ userTokenRef.current = userToken;
102
+
103
+ // ── HTTP client bootstrap ──────────────────────────────────────────────
104
+ useEffect(() => {
105
+ setApiBaseUrl(cfg.apiBaseUrl);
106
+ setTokenGetter(() => userTokenRef.current ?? null);
107
+ }, [cfg.apiBaseUrl]);
108
+
109
+ // ── auth refresh registration ──────────────────────────────────────────
110
+ useEffect(() => {
111
+ if (!authCallback) return;
112
+ registerAuthRefresh(authCallback, (newToken) => {
113
+ setToken(typeof newToken === "string" ? newToken : null);
114
+ });
115
+ return () => unregisterAuthRefresh();
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
117
+ }, [authCallback]);
118
+
119
+ // ── resolve userId ─────────────────────────────────────────────────────
120
+ useEffect(() => {
121
+ if (userTokenProp) {
122
+ setUserId(userIdProp ?? "");
123
+ setUserTokenState(userTokenProp.trim());
124
+ setUserIdReady(true);
125
+ return;
126
+ }
127
+ const uid = userIdProp?.trim();
128
+ if (uid) {
129
+ setUserId(uid);
130
+ setUserIdReady(true);
131
+ return;
132
+ }
133
+ let cancelled = false;
134
+ getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
135
+ if (!cancelled) {
136
+ setUserId(id);
137
+ setUserIdReady(true);
138
+ }
139
+ });
140
+ return () => {
141
+ cancelled = true;
142
+ };
143
+ }, [cfg.widgetId, userIdProp, userTokenProp]);
144
+
145
+ // ── propagate token changes from parent ───────────────────────────────
146
+ useEffect(() => {
147
+ setUserTokenState(userTokenProp?.trim() || undefined);
148
+ }, [userTokenProp]);
149
+
150
+ // ── widget config fetch ────────────────────────────────────────────────
151
+ useEffect(() => {
152
+ if (!cfg.widgetId) return;
153
+ setEmbedLoadState("loading");
154
+ setEmbedLoadError(null);
155
+ let cancelled = false;
156
+ (async () => {
157
+ try {
158
+ const raw = await widgetApi.getWidget(cfg.widgetId);
159
+ if (cancelled) return;
160
+ const config = raw?.data ?? raw;
161
+ unstable_batchedUpdates(() => {
162
+ setWidgetConfig(config);
163
+ setEmbedLoadError(null);
164
+ setEmbedLoadState("ready");
165
+ });
166
+ } catch (e) {
167
+ if (cancelled) return;
168
+ unstable_batchedUpdates(() => {
169
+ setEmbedLoadError(
170
+ e instanceof Error ? e.message : "Failed to load widget config"
171
+ );
172
+ setEmbedLoadState("error");
173
+ });
174
+ }
175
+ })();
176
+ return () => {
177
+ cancelled = true;
178
+ };
179
+ }, [cfg.widgetId]);
180
+
181
+ // ── load initial unread count ──────────────────────────────────────────
182
+ const visitorQueryParams = useMemo(
183
+ () =>
184
+ buildVisitorQueryParams({
185
+ widgetId: cfg.widgetId,
186
+ userId,
187
+ userToken,
188
+ visitorProfile,
189
+ }),
190
+ [cfg.widgetId, userId, userToken, visitorProfile]
191
+ );
192
+
193
+ useEffect(() => {
194
+ if (!visitorQueryParams) return;
195
+ let cancelled = false;
196
+ (async () => {
197
+ try {
198
+ const raw = await conversationApi.getTotalUnread({
199
+ params: visitorQueryParams,
200
+ });
201
+ if (cancelled) return;
202
+ const n = pickUnread(raw);
203
+ if (n !== null) setTotalUnread(Math.max(0, n));
204
+ } catch {
205
+ /* best-effort */
206
+ }
207
+ })();
208
+ return () => {
209
+ cancelled = true;
210
+ };
211
+ }, [visitorQueryParams]);
212
+
213
+ // ── FCM token registration ─────────────────────────────────────────────
214
+ useEffect(() => {
215
+ if (!fcmToken || !visitorQueryParams) return;
216
+ const platform = fcmTokenPlatform ?? (Platform.OS === "ios" ? "ios" : "android");
217
+ deviceTokenApi.register({ token: fcmToken, platform }, { params: visitorQueryParams }).catch(() => {});
218
+ }, [fcmToken, fcmTokenPlatform, visitorQueryParams]);
219
+
220
+ // ── WsClient lifecycle ─────────────────────────────────────────────────
221
+ useEffect(() => {
222
+ if (!cfg.widgetId || !userId || !userIdReady) return;
223
+
224
+ const client = new WsClient({
225
+ widgetId: cfg.widgetId,
226
+ userId,
227
+ userToken,
228
+ });
229
+
230
+ const unsub = client.onStateChange((s) => setConnectionState({ ...s }));
231
+
232
+ const unsubMsg = client.subscribe("new_livechat_message", (payload: any) => {
233
+ const data = payload?.data ?? payload;
234
+ const msg = data?.message;
235
+ if (msg?.senderType === "contact") return;
236
+ setTotalUnread((n) => n + 1);
237
+ });
238
+
239
+ const unsubRead = client.subscribe(
240
+ "visitor_conversation_messages_marked_as_read",
241
+ (payload: any) => {
242
+ const data = payload?.data ?? payload;
243
+ const raw = data?.visitorUnreadCount;
244
+ const parsed =
245
+ typeof raw === "string" ? parseInt(raw, 10) : Number(raw);
246
+ if (Number.isFinite(parsed)) {
247
+ setTotalUnread(Math.max(0, parsed));
248
+ return;
249
+ }
250
+ const deltaRaw = data?.messageCount;
251
+ const delta =
252
+ typeof deltaRaw === "string"
253
+ ? parseInt(deltaRaw, 10)
254
+ : Number(deltaRaw);
255
+ if (Number.isFinite(delta) && delta > 0)
256
+ setTotalUnread((n) => Math.max(0, n - delta));
257
+ }
258
+ );
259
+
260
+ client.start();
261
+ wsClientRef.current = client;
262
+
263
+ return () => {
264
+ unsub();
265
+ unsubMsg();
266
+ unsubRead();
267
+ client.destroy();
268
+ wsClientRef.current = null;
269
+ };
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps
271
+ }, [cfg.widgetId, cfg.apiBaseUrl, userId, userIdReady]);
272
+
273
+ useEffect(() => {
274
+ wsClientRef.current?.updateIdentity({ userId, userToken });
275
+ }, [userId, userToken]);
276
+
277
+ // ── public actions ─────────────────────────────────────────────────────
278
+ const setToken = useCallback(
279
+ (token: string | null | undefined) => {
280
+ const trimmed = typeof token === "string" ? token.trim() : "";
281
+ if (trimmed) {
282
+ setUserTokenState(trimmed);
283
+ setUserId("");
284
+ setUserIdReady(true);
285
+ return;
286
+ }
287
+ setUserTokenState(undefined);
288
+ getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
289
+ setUserId(id);
290
+ setUserIdReady(true);
291
+ });
292
+ },
293
+ [cfg.widgetId]
294
+ );
295
+
296
+ const logout = useCallback(() => {
297
+ // Unregister FCM token before clearing identity
298
+ if (fcmToken && visitorQueryParams) {
299
+ deviceTokenApi.unregister({ token: fcmToken }, { params: visitorQueryParams }).catch(() => {});
300
+ }
301
+ setUserTokenState(undefined);
302
+ clearStoredDeviceUserId(cfg.widgetId).then(() =>
303
+ getOrCreateDeviceUserId(cfg.widgetId).then((id) => {
304
+ setUserId(id);
305
+ setUserIdReady(true);
306
+ })
307
+ );
308
+ }, [cfg.widgetId, fcmToken, visitorQueryParams]);
309
+
310
+ const open = useCallback(() => setIsOpen(true), []);
311
+ const close = useCallback(() => setIsOpen(false), []);
312
+ const show = useCallback(() => setIsVisible(true), []);
313
+ const hide = useCallback(() => {
314
+ setIsOpen(false);
315
+ setIsVisible(false);
316
+ }, []);
317
+
318
+ // ── theme resolution ───────────────────────────────────────────────────
319
+ const theme = useMemo(() => {
320
+ const brandColor =
321
+ getWidgetBrandColor(widgetConfig) ?? defaultTheme.colors.brand;
322
+
323
+ const resolvedAppearance =
324
+ appearanceProp === "system"
325
+ ? systemColorScheme === "dark"
326
+ ? "dark"
327
+ : "light"
328
+ : appearanceProp ?? "light";
329
+
330
+ return buildThemeFromBrand(
331
+ brandColor,
332
+ resolvedAppearance,
333
+ useBrandThemingProp,
334
+ themeProp
335
+ );
336
+ }, [widgetConfig, appearanceProp, systemColorScheme, useBrandThemingProp, themeProp]);
337
+
338
+ const contextValue = useMemo(
339
+ () => ({
340
+ widgetId: cfg.widgetId,
341
+ userId,
342
+ userToken,
343
+ setToken,
344
+ logout,
345
+ visitorProfile,
346
+ visitorQueryParams,
347
+ widgetConfig,
348
+ widgetName: getWidgetName(widgetConfig),
349
+ widgetGreeting: getWidgetGreeting(widgetConfig),
350
+ embedLoadState,
351
+ embedLoadError,
352
+ totalUnread,
353
+ setTotalUnread,
354
+ isOpen,
355
+ open,
356
+ close,
357
+ isVisible,
358
+ show,
359
+ hide,
360
+ wsClient: wsClientRef.current,
361
+ connectionState,
362
+ theme,
363
+ }),
364
+ [
365
+ cfg.widgetId,
366
+ userId,
367
+ userToken,
368
+ setToken,
369
+ logout,
370
+ visitorProfile,
371
+ visitorQueryParams,
372
+ widgetConfig,
373
+ embedLoadState,
374
+ embedLoadError,
375
+ totalUnread,
376
+ isOpen,
377
+ open,
378
+ close,
379
+ isVisible,
380
+ show,
381
+ hide,
382
+ connectionState,
383
+ theme,
384
+ ]
385
+ );
386
+
387
+ return (
388
+ <LiveChatContext.Provider value={contextValue}>
389
+ {children}
390
+ </LiveChatContext.Provider>
391
+ );
392
+ }