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