ajaxter-chat 3.0.17 → 3.2.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 (146) hide show
  1. package/dist/components/BlockList/index.d.ts +1 -0
  2. package/dist/components/BlockList/index.d.ts.map +1 -0
  3. package/dist/components/BlockList/index.js +55 -28
  4. package/dist/components/BlockList/index.js.map +1 -0
  5. package/dist/components/CallScreen/index.d.ts +1 -0
  6. package/dist/components/CallScreen/index.d.ts.map +1 -0
  7. package/dist/components/CallScreen/index.js +107 -39
  8. package/dist/components/CallScreen/index.js.map +1 -0
  9. package/dist/components/ChatScreen/index.d.ts +1 -0
  10. package/dist/components/ChatScreen/index.d.ts.map +1 -0
  11. package/dist/components/ChatScreen/index.js +493 -294
  12. package/dist/components/ChatScreen/index.js.map +1 -0
  13. package/dist/components/ChatWidget.d.ts +1 -0
  14. package/dist/components/ChatWidget.d.ts.map +1 -0
  15. package/dist/components/ChatWidget.js +499 -367
  16. package/dist/components/ChatWidget.js.map +1 -0
  17. package/dist/components/EmojiPicker/index.d.ts +1 -0
  18. package/dist/components/EmojiPicker/index.d.ts.map +1 -0
  19. package/dist/components/EmojiPicker/index.js +19 -7
  20. package/dist/components/EmojiPicker/index.js.map +1 -0
  21. package/dist/components/ErrorBoundary/index.d.ts +20 -0
  22. package/dist/components/ErrorBoundary/index.d.ts.map +1 -0
  23. package/dist/components/ErrorBoundary/index.js +76 -0
  24. package/dist/components/ErrorBoundary/index.js.map +1 -0
  25. package/dist/components/HomeScreen/index.d.ts +1 -0
  26. package/dist/components/HomeScreen/index.d.ts.map +1 -0
  27. package/dist/components/HomeScreen/index.js +236 -158
  28. package/dist/components/HomeScreen/index.js.map +1 -0
  29. package/dist/components/MaintenanceView/index.d.ts +1 -0
  30. package/dist/components/MaintenanceView/index.d.ts.map +1 -0
  31. package/dist/components/MaintenanceView/index.js +28 -12
  32. package/dist/components/MaintenanceView/index.js.map +1 -0
  33. package/dist/components/MiniCallBar/index.d.ts +1 -0
  34. package/dist/components/MiniCallBar/index.d.ts.map +1 -0
  35. package/dist/components/MiniCallBar/index.js +85 -37
  36. package/dist/components/MiniCallBar/index.js.map +1 -0
  37. package/dist/components/PermissionsGateScreen/index.d.ts +1 -0
  38. package/dist/components/PermissionsGateScreen/index.d.ts.map +1 -0
  39. package/dist/components/PermissionsGateScreen/index.js +82 -28
  40. package/dist/components/PermissionsGateScreen/index.js.map +1 -0
  41. package/dist/components/RecentChatsScreen/index.d.ts +1 -0
  42. package/dist/components/RecentChatsScreen/index.d.ts.map +1 -0
  43. package/dist/components/RecentChatsScreen/index.js +79 -19
  44. package/dist/components/RecentChatsScreen/index.js.map +1 -0
  45. package/dist/components/SlideNavMenu.d.ts +1 -0
  46. package/dist/components/SlideNavMenu.d.ts.map +1 -0
  47. package/dist/components/SlideNavMenu.js +82 -63
  48. package/dist/components/SlideNavMenu.js.map +1 -0
  49. package/dist/components/Tabs/BottomTabs.d.ts +1 -0
  50. package/dist/components/Tabs/BottomTabs.d.ts.map +1 -0
  51. package/dist/components/Tabs/BottomTabs.js +34 -19
  52. package/dist/components/Tabs/BottomTabs.js.map +1 -0
  53. package/dist/components/TicketDetailScreen/index.d.ts +1 -0
  54. package/dist/components/TicketDetailScreen/index.d.ts.map +1 -0
  55. package/dist/components/TicketDetailScreen/index.js +66 -27
  56. package/dist/components/TicketDetailScreen/index.js.map +1 -0
  57. package/dist/components/TicketFormScreen/index.d.ts +1 -0
  58. package/dist/components/TicketFormScreen/index.d.ts.map +1 -0
  59. package/dist/components/TicketFormScreen/index.js +99 -49
  60. package/dist/components/TicketFormScreen/index.js.map +1 -0
  61. package/dist/components/TicketScreen/index.d.ts +1 -0
  62. package/dist/components/TicketScreen/index.d.ts.map +1 -0
  63. package/dist/components/TicketScreen/index.js +95 -26
  64. package/dist/components/TicketScreen/index.js.map +1 -0
  65. package/dist/components/UserListScreen/index.d.ts +1 -0
  66. package/dist/components/UserListScreen/index.d.ts.map +1 -0
  67. package/dist/components/UserListScreen/index.js +127 -53
  68. package/dist/components/UserListScreen/index.js.map +1 -0
  69. package/dist/components/ViewerBlockedScreen/index.d.ts +1 -0
  70. package/dist/components/ViewerBlockedScreen/index.d.ts.map +1 -0
  71. package/dist/components/ViewerBlockedScreen/index.js +113 -61
  72. package/dist/components/ViewerBlockedScreen/index.js.map +1 -0
  73. package/dist/config/index.d.ts +7 -3
  74. package/dist/config/index.d.ts.map +1 -0
  75. package/dist/config/index.js +73 -22
  76. package/dist/config/index.js.map +1 -0
  77. package/dist/hooks/useChat.d.ts +9 -1
  78. package/dist/hooks/useChat.d.ts.map +1 -0
  79. package/dist/hooks/useChat.js +60 -18
  80. package/dist/hooks/useChat.js.map +1 -0
  81. package/dist/hooks/useRemoteConfig.d.ts +1 -0
  82. package/dist/hooks/useRemoteConfig.d.ts.map +1 -0
  83. package/dist/hooks/useRemoteConfig.js +22 -15
  84. package/dist/hooks/useRemoteConfig.js.map +1 -0
  85. package/dist/hooks/useSocket.d.ts +59 -0
  86. package/dist/hooks/useSocket.d.ts.map +1 -0
  87. package/dist/hooks/useSocket.js +203 -0
  88. package/dist/hooks/useSocket.js.map +1 -0
  89. package/dist/hooks/useWebRTC.d.ts +10 -2
  90. package/dist/hooks/useWebRTC.d.ts.map +1 -0
  91. package/dist/hooks/useWebRTC.js +101 -69
  92. package/dist/hooks/useWebRTC.js.map +1 -0
  93. package/dist/index.d.ts +7 -1
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +67 -21
  96. package/dist/index.js.map +1 -0
  97. package/dist/types/index.d.ts +129 -48
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +4 -1
  100. package/dist/types/index.js.map +1 -0
  101. package/dist/utils/chat.d.ts +1 -0
  102. package/dist/utils/chat.d.ts.map +1 -0
  103. package/dist/utils/chat.js +17 -7
  104. package/dist/utils/chat.js.map +1 -0
  105. package/dist/utils/fileName.d.ts +1 -0
  106. package/dist/utils/fileName.d.ts.map +1 -0
  107. package/dist/utils/fileName.js +5 -1
  108. package/dist/utils/fileName.js.map +1 -0
  109. package/dist/utils/messageSound.d.ts +1 -0
  110. package/dist/utils/messageSound.d.ts.map +1 -0
  111. package/dist/utils/messageSound.js +9 -3
  112. package/dist/utils/messageSound.js.map +1 -0
  113. package/dist/utils/presenceStatus.d.ts +1 -0
  114. package/dist/utils/presenceStatus.d.ts.map +1 -0
  115. package/dist/utils/presenceStatus.js +11 -4
  116. package/dist/utils/presenceStatus.js.map +1 -0
  117. package/dist/utils/privacyConsent.d.ts +1 -0
  118. package/dist/utils/privacyConsent.d.ts.map +1 -0
  119. package/dist/utils/privacyConsent.js +9 -3
  120. package/dist/utils/privacyConsent.js.map +1 -0
  121. package/dist/utils/reenableRequest.d.ts +1 -0
  122. package/dist/utils/reenableRequest.d.ts.map +1 -0
  123. package/dist/utils/reenableRequest.js +5 -1
  124. package/dist/utils/reenableRequest.js.map +1 -0
  125. package/dist/utils/theme.d.ts +1 -0
  126. package/dist/utils/theme.d.ts.map +1 -0
  127. package/dist/utils/theme.js +10 -4
  128. package/dist/utils/theme.js.map +1 -0
  129. package/dist/utils/widgetPermissions.d.ts +1 -0
  130. package/dist/utils/widgetPermissions.d.ts.map +1 -0
  131. package/dist/utils/widgetPermissions.js +13 -5
  132. package/dist/utils/widgetPermissions.js.map +1 -0
  133. package/dist/utils/widgetSession.d.ts +1 -0
  134. package/dist/utils/widgetSession.d.ts.map +1 -0
  135. package/dist/utils/widgetSession.js +9 -3
  136. package/dist/utils/widgetSession.js.map +1 -0
  137. package/package.json +8 -4
  138. package/src/components/ChatWidget.tsx +643 -622
  139. package/src/components/ErrorBoundary/index.tsx +62 -0
  140. package/src/config/index.ts +87 -26
  141. package/src/hooks/useChat.ts +59 -12
  142. package/src/hooks/useRemoteConfig.ts +8 -3
  143. package/src/hooks/useSocket.ts +249 -0
  144. package/src/hooks/useWebRTC.ts +99 -64
  145. package/src/index.ts +14 -3
  146. package/src/types/index.ts +177 -143
@@ -0,0 +1,62 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+
3
+ interface Props {
4
+ children: ReactNode;
5
+ fallback?: ReactNode;
6
+ primaryColor?: string;
7
+ onError?: (error: Error, info: ErrorInfo) => void;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ }
14
+
15
+ export class ErrorBoundary extends Component<Props, State> {
16
+ constructor(props: Props) {
17
+ super(props);
18
+ this.state = { hasError: false, error: null };
19
+ }
20
+
21
+ static getDerivedStateFromError(error: Error): State {
22
+ return { hasError: true, error };
23
+ }
24
+
25
+ componentDidCatch(error: Error, info: ErrorInfo) {
26
+ console.error('[ChatWidget] Uncaught error:', error, info.componentStack);
27
+ this.props.onError?.(error, info);
28
+ }
29
+
30
+ handleReset = () => {
31
+ this.setState({ hasError: false, error: null });
32
+ };
33
+
34
+ render() {
35
+ if (this.state.hasError) {
36
+ if (this.props.fallback) return this.props.fallback;
37
+ const color = this.props.primaryColor ?? '#2563EB';
38
+ return (
39
+ <div style={{
40
+ display: 'flex', flexDirection: 'column', alignItems: 'center',
41
+ justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12,
42
+ }}>
43
+ <div style={{ fontSize: 40 }}>⚠️</div>
44
+ <p style={{ fontWeight: 700, color: '#1a2332', margin: 0 }}>Something went wrong</p>
45
+ <p style={{ fontSize: 13, color: '#7b8fa1', lineHeight: 1.6, margin: 0 }}>
46
+ {this.state.error?.message ?? 'An unexpected error occurred'}
47
+ </p>
48
+ <button
49
+ onClick={this.handleReset}
50
+ style={{
51
+ padding: '9px 20px', borderRadius: 10, border: 'none',
52
+ background: color, color: '#fff', cursor: 'pointer', fontWeight: 700, fontSize: 14,
53
+ }}
54
+ >
55
+ Try Again
56
+ </button>
57
+ </div>
58
+ );
59
+ }
60
+ return this.props.children;
61
+ }
62
+ }
@@ -1,56 +1,117 @@
1
- import { LocalEnvConfig, RemoteChatData } from '../types';
1
+ import { LocalEnvConfig, RemoteChatData, AjaxterConfigResponse, WidgetConfig } from '../types';
2
2
 
3
- /**
4
- * Default JSON endpoint. Override with `REACT_APP_CHAT_CONFIG_URL` / `NEXT_PUBLIC_CHAT_CONFIG_URL`.
5
- * If the remote host does not send CORS headers, set this to a same-origin path (e.g. `/api/chat-config`)
6
- * and proxy the JSON from your server — see `examples/next-app-router-chat-proxy.ts`.
7
- */
8
- const DEFAULT_CHAT_DATA_BASE = 'https://window.mscorpres.com/TEST/chatData.json';
9
- const DEMO_API_KEY = 'demo1234';
10
- const DEMO_WIDGET_ID = 'demo';
3
+ const DEFAULT_SOCKET_URL = 'http://localhost:3005';
4
+ const DEFAULT_CONFIG_URL = 'http://localhost:5000/api/chat-config';
5
+ const DEMO_API_KEY = 'demo1234';
6
+ const DEMO_WIDGET_ID = 'demo';
11
7
 
12
8
  function getEnv(key: string): string | undefined {
13
9
  if (typeof process !== 'undefined' && process.env) {
14
10
  return (
15
11
  process.env[`NEXT_PUBLIC_${key}`] ??
16
- process.env[`REACT_APP_${key}`] ??
17
- process.env[key] ??
12
+ process.env[`REACT_APP_${key}`] ??
13
+ process.env[key] ??
18
14
  undefined
19
15
  );
20
16
  }
21
17
  return undefined;
22
18
  }
23
19
 
24
- function getChatDataBaseUrl(): string {
25
- return getEnv('CHAT_CONFIG_URL')?.trim() || DEFAULT_CHAT_DATA_BASE;
20
+ export function loadLocalConfig(): LocalEnvConfig {
21
+ return {
22
+ apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
23
+ widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
24
+ socketUrl: getEnv('SOCKET_URL') ?? DEFAULT_SOCKET_URL,
25
+ };
26
+ }
27
+
28
+ function getConfigUrl(): string {
29
+ return getEnv('CHAT_CONFIG_URL')?.trim() ?? DEFAULT_CONFIG_URL;
26
30
  }
27
31
 
28
32
  /**
29
- * Loads remote widget config once via GET. Uses query params `key` and `widget` so the
30
- * server can validate the request without custom headers (avoids CORS preflight failures).
33
+ * Fetches widget config ONCE from the server.
34
+ * URL: GET /api/chat-config?key=<apiKey>&widget=<widgetId>
35
+ *
36
+ * Returns RemoteChatData (the shape the widget components expect),
37
+ * translated from the Ajaxter server response format.
31
38
  */
32
39
  export async function fetchRemoteChatData(
33
40
  apiKey: string,
34
41
  widgetId: string
35
42
  ): Promise<RemoteChatData> {
36
- const base = getChatDataBaseUrl();
37
- const url = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
38
- url.searchParams.set('key', apiKey);
43
+ const base = getConfigUrl();
44
+ const url = new URL(
45
+ base,
46
+ typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
47
+ );
48
+ url.searchParams.set('key', apiKey);
39
49
  url.searchParams.set('widget', widgetId);
40
50
 
41
51
  const res = await fetch(url.toString(), {
42
- method: 'GET',
52
+ method: 'GET',
53
+ headers: { Accept: 'application/json' },
43
54
  credentials: 'omit',
44
55
  mode: 'cors',
45
- headers: { Accept: 'application/json' },
46
56
  });
47
- if (!res.ok) throw new Error(`Failed to load chat config: ${res.status}`);
48
- return res.json() as Promise<RemoteChatData>;
49
- }
50
57
 
51
- export function loadLocalConfig(): LocalEnvConfig {
58
+ if (!res.ok) throw new Error(`Chat config fetch failed: ${res.status}`);
59
+
60
+ const json = await res.json() as AjaxterConfigResponse;
61
+
62
+ if (!json.ok || !json.data) {
63
+ throw new Error('Invalid config response from server');
64
+ }
65
+
66
+ const { data } = json;
67
+ const internal = data._internal;
68
+ const ws = data.widget;
69
+
70
+ // Map Ajaxter server response → WidgetConfig used by components
71
+ const widgetConfig: WidgetConfig = {
72
+ id: internal.id,
73
+ apiKey: internal.apiKey,
74
+ status: internal.status,
75
+ chatType: internal.chatType,
76
+ displayMode: internal.displayMode,
77
+ primaryColor: internal.primaryColor,
78
+ buttonLabel: internal.buttonLabel,
79
+
80
+ // Button position from visibility config
81
+ buttonPosition: (ws.visibility?.mobile?.position === 'bottom-left'
82
+ ? 'bottom-left'
83
+ : 'bottom-right') as 'bottom-right' | 'bottom-left',
84
+
85
+ // Welcome text from states.online header
86
+ welcomeTitle: data.branding?.name ?? internal.brandName ?? 'Hello there 👋',
87
+ welcomeSubtitle: (ws.states as Record<string, { header?: Array<{content?: {value?: string}}> }> | undefined)
88
+ ?.online?.header?.[0]?.content?.value
89
+ ?? 'How can we help you today?',
90
+
91
+ brandName: internal.brandName ?? undefined,
92
+ supportPhone: internal.supportPhone ?? undefined,
93
+ privacyPolicyUrl: internal.privacyPolicyUrl ?? undefined,
94
+ showPrivacyNotice: internal.showPrivacyNotice,
95
+
96
+ allowVoiceMessage: internal.allowVoiceMessage,
97
+ allowAttachment: internal.allowAttachment,
98
+ allowEmoji: internal.allowEmoji,
99
+ allowWebCall: internal.allowWebCall,
100
+ maxEmojiCount: internal.maxEmojiCount,
101
+ allowTranscriptDownload: internal.allowTranscript,
102
+ allowReport: internal.allowReport,
103
+ allowBlock: internal.allowBlock,
104
+
105
+ // Full Ajaxter settings stored for display-mode rendering
106
+ ajaxterSettings: ws,
107
+ };
108
+
52
109
  return {
53
- apiKey: getEnv('CHAT_API_KEY') ?? DEMO_API_KEY,
54
- widgetId: getEnv('CHAT_WIDGET_ID') ?? DEMO_WIDGET_ID,
110
+ widget: widgetConfig,
111
+ developers: data.developers ?? [],
112
+ users: data.users ?? [],
113
+ sampleChats: data.sampleChats ?? {},
114
+ sampleTickets: data.sampleTickets ?? [],
115
+ blockedUsers: data.blockedUsers ?? [],
55
116
  };
56
117
  }
@@ -1,19 +1,29 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, useCallback, useRef } from 'react';
2
2
  import { ChatMessage, ChatUser } from '../types';
3
3
 
4
- export function useChat(initialMessages: ChatMessage[] = []) {
4
+ export interface UseChatOptions {
5
+ onEmitMessage?: (msg: ChatMessage) => void;
6
+ onEmitPause?: (roomId: string, targetUid: string, paused: boolean) => void;
7
+ onEmitReport?: (roomId: string) => void;
8
+ }
9
+
10
+ export function useChat(initialMessages: ChatMessage[] = [], opts: UseChatOptions = {}) {
5
11
  const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
6
12
  const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
7
13
  const [isPaused, setIsPaused] = useState(false);
8
14
  const [isReported, setIsReported] = useState(false);
15
+ const activeUserRef = useRef<ChatUser | null>(null);
16
+ const isPausedRef = useRef(false);
17
+
18
+ const { onEmitMessage, onEmitPause, onEmitReport } = opts;
9
19
 
10
20
  const selectUser = useCallback((user: ChatUser, history: ChatMessage[] = []) => {
11
21
  setActiveUser(user);
22
+ activeUserRef.current = user;
12
23
  setMessages(history);
13
24
  setIsPaused(false);
25
+ isPausedRef.current = false;
14
26
  setIsReported(false);
15
- // TODO: socket.emit('join', { roomId: user.uid });
16
- // TODO: socket.on('message', msg => setMessages(prev => [...prev, msg]));
17
27
  }, []);
18
28
 
19
29
  const sendMessage = useCallback((
@@ -21,11 +31,12 @@ export function useChat(initialMessages: ChatMessage[] = []) {
21
31
  type: ChatMessage['type'] = 'text',
22
32
  extra: Partial<ChatMessage> = {}
23
33
  ) => {
24
- if (!activeUser || isPaused) return;
34
+ const user = activeUserRef.current;
35
+ if (!user || isPausedRef.current) return;
25
36
  const msg: ChatMessage = {
26
37
  id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
27
38
  senderId: 'me',
28
- receiverId: activeUser.uid,
39
+ receiverId: user.uid,
29
40
  text,
30
41
  timestamp: new Date().toISOString(),
31
42
  type,
@@ -33,16 +44,52 @@ export function useChat(initialMessages: ChatMessage[] = []) {
33
44
  ...extra,
34
45
  };
35
46
  setMessages(prev => [...prev, msg]);
36
- // TODO: socket.emit('message', msg);
37
- }, [activeUser, isPaused]);
47
+ onEmitMessage?.(msg);
48
+ }, [onEmitMessage]);
49
+
50
+ const receiveMessage = useCallback((msg: ChatMessage) => {
51
+ setMessages(prev => {
52
+ if (prev.some(m => m.id === msg.id)) return prev;
53
+ return [...prev, msg];
54
+ });
55
+ }, []);
56
+
57
+ const updateMessageStatus = useCallback((messageId: string, status: ChatMessage['status']) => {
58
+ setMessages(prev => prev.map(m => m.id === messageId ? { ...m, status } : m));
59
+ }, []);
60
+
61
+ const togglePause = useCallback(() => {
62
+ const user = activeUserRef.current;
63
+ if (!user) return;
64
+ const next = !isPausedRef.current;
65
+ setIsPaused(next);
66
+ isPausedRef.current = next;
67
+ const roomId = [user.uid, 'me'].sort().join('_');
68
+ onEmitPause?.(roomId, user.uid, next);
69
+ }, [onEmitPause]);
70
+
71
+ const reportChat = useCallback(() => {
72
+ const user = activeUserRef.current;
73
+ setIsReported(true);
74
+ if (user) {
75
+ const roomId = [user.uid, 'me'].sort().join('_');
76
+ onEmitReport?.(roomId);
77
+ }
78
+ }, [onEmitReport]);
38
79
 
39
- const togglePause = useCallback(() => setIsPaused(p => !p), []);
40
- const reportChat = useCallback(() => { setIsReported(true); /* TODO: API call */ }, []);
41
- const clearChat = useCallback(() => { setMessages([]); setActiveUser(null); }, []);
80
+ const clearChat = useCallback(() => {
81
+ setMessages([]);
82
+ setActiveUser(null);
83
+ activeUserRef.current = null;
84
+ setIsPaused(false);
85
+ isPausedRef.current = false;
86
+ setIsReported(false);
87
+ }, []);
42
88
 
43
89
  return {
44
90
  messages, activeUser, isPaused, isReported,
45
- selectUser, sendMessage, togglePause, reportChat, clearChat,
91
+ selectUser, sendMessage, receiveMessage, updateMessageStatus,
92
+ togglePause, reportChat, clearChat,
46
93
  setMessages,
47
94
  };
48
95
  }
@@ -8,17 +8,22 @@ export function useRemoteConfig(apiKey: string, widgetId: string) {
8
8
  const [error, setError] = useState<string | null>(null);
9
9
 
10
10
  useEffect(() => {
11
+ if (!apiKey || !widgetId) return;
11
12
  let cancelled = false;
12
13
  setLoading(true);
14
+ setError(null);
15
+
13
16
  fetchRemoteChatData(apiKey, widgetId)
14
- .then(d => { if (!cancelled) { setData(d); setError(null); setLoading(false); } })
17
+ .then(d => {
18
+ if (!cancelled) { setData(d); setLoading(false); }
19
+ })
15
20
  .catch(e => {
16
21
  if (!cancelled) {
17
- const msg = e instanceof Error ? e.message : String(e);
18
- setError(msg || 'Network error while loading configuration');
22
+ setError(e instanceof Error ? e.message : 'Failed to load config');
19
23
  setLoading(false);
20
24
  }
21
25
  });
26
+
22
27
  return () => { cancelled = true; };
23
28
  }, [apiKey, widgetId]);
24
29
 
@@ -0,0 +1,249 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useSocket — wraps socket.io-client for the ajaxter-chat npm package.
5
+ *
6
+ * The npm package fetches widget config ONCE via HTTP (GET /api/chat-config),
7
+ * then all real-time events (messages, calls, typing, presence…) flow through
8
+ * this Socket.IO connection.
9
+ *
10
+ * Install peer dep in consumer app: npm install socket.io-client
11
+ */
12
+
13
+ import { useEffect, useRef, useCallback, useState } from 'react';
14
+ import { ChatMessage } from '../types';
15
+
16
+ export type SocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
17
+
18
+ export interface SocketMessageAck {
19
+ messageId: string;
20
+ status: 'delivered' | 'read';
21
+ }
22
+
23
+ export interface UseSocketOptions {
24
+ widgetId: string;
25
+ viewerUid: string;
26
+ /** Socket.IO server URL — defaults to http://localhost:3005 */
27
+ serverUrl?: string;
28
+ onMessage?: (msg: ChatMessage) => void;
29
+ onMessageAck?: (ack: SocketMessageAck) => void;
30
+ onChatPaused?: (roomId: string, paused: boolean) => void;
31
+ onCallOffer?: (offer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
32
+ onCallAnswer?: (answer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
33
+ onIceCandidate?:(candidate: RTCIceCandidateInit, fromUid: string) => void;
34
+ onCallEnd?: (callId: string) => void;
35
+ onTypingStart?: (roomId: string, userId: string) => void;
36
+ onTypingStop?: (roomId: string, userId: string) => void;
37
+ onUserStatus?: (uid: string, status: 'online' | 'away' | 'offline') => void;
38
+ onTicketCreated?: (ticket: unknown) => void;
39
+ onTicketUpdated?: (ticket: unknown) => void;
40
+ onTransferAssigned?: (payload: unknown) => void;
41
+ onError?: (code: string, message: string) => void;
42
+ }
43
+
44
+ // Lazy-load socket.io-client so the package stays light when not connected
45
+ type SocketIO = {
46
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
47
+ off: (event: string, cb?: (...args: unknown[]) => void) => void;
48
+ emit: (event: string, data?: unknown) => void;
49
+ disconnect: () => void;
50
+ connected: boolean;
51
+ };
52
+
53
+ async function createSocket(url: string, query: Record<string, string>): Promise<SocketIO> {
54
+ // Dynamic import so bundle is not broken when socket.io-client is absent
55
+ // Consumer app must have socket.io-client installed
56
+ let io: (url: string, opts: unknown) => SocketIO;
57
+ try {
58
+ const mod = await import('socket.io-client' as string);
59
+ io = (mod.default || mod) as (url: string, opts: unknown) => SocketIO;
60
+ } catch {
61
+ throw new Error(
62
+ '[ajaxter-chat] socket.io-client not found. ' +
63
+ 'Run: npm install socket.io-client'
64
+ );
65
+ }
66
+ return io(url, {
67
+ query,
68
+ transports: ['websocket', 'polling'],
69
+ reconnection: true,
70
+ reconnectionDelay: 2000,
71
+ reconnectionAttempts: 10,
72
+ autoConnect: true,
73
+ });
74
+ }
75
+
76
+ const DEFAULT_SOCKET_URL = 'http://localhost:3005';
77
+
78
+ export function useSocket(opts: UseSocketOptions) {
79
+ const {
80
+ widgetId, viewerUid,
81
+ serverUrl = DEFAULT_SOCKET_URL,
82
+ onMessage, onMessageAck, onChatPaused,
83
+ onCallOffer, onCallAnswer, onIceCandidate, onCallEnd,
84
+ onTypingStart, onTypingStop, onUserStatus,
85
+ onTicketCreated, onTicketUpdated, onTransferAssigned, onError,
86
+ } = opts;
87
+
88
+ const [status, setStatus] = useState<SocketStatus>('disconnected');
89
+ const socketRef = useRef<SocketIO | null>(null);
90
+ const mountedRef = useRef(true);
91
+ const currentRoom = useRef<string | null>(null);
92
+
93
+ // Stable emit helper
94
+ const emit = useCallback((event: string, data?: unknown) => {
95
+ if (socketRef.current?.connected) {
96
+ socketRef.current.emit(event, data);
97
+ }
98
+ }, []);
99
+
100
+ useEffect(() => {
101
+ if (!widgetId || !viewerUid) return;
102
+ mountedRef.current = true;
103
+ setStatus('connecting');
104
+
105
+ createSocket(serverUrl, { widgetId, uid: viewerUid }).then(socket => {
106
+ if (!mountedRef.current) { socket.disconnect(); return; }
107
+ socketRef.current = socket;
108
+
109
+ socket.on('connect', () => {
110
+ if (!mountedRef.current) return;
111
+ setStatus('connected');
112
+ // Rejoin current room after reconnect
113
+ if (currentRoom.current) {
114
+ socket.emit('join', { roomId: currentRoom.current, widgetId });
115
+ }
116
+ });
117
+
118
+ socket.on('disconnect', () => {
119
+ if (mountedRef.current) setStatus('disconnected');
120
+ });
121
+
122
+ socket.on('connect_error', () => {
123
+ if (mountedRef.current) setStatus('error');
124
+ });
125
+
126
+ socket.on('message', (msg: unknown) => onMessage?.(msg as ChatMessage));
127
+ socket.on('message:ack', (ack: unknown) => onMessageAck?.(ack as SocketMessageAck));
128
+
129
+ socket.on('chat:paused', (p: unknown) => {
130
+ const { roomId, paused } = p as { roomId: string; paused: boolean };
131
+ onChatPaused?.(roomId, paused);
132
+ });
133
+
134
+ socket.on('call-offer', (p: unknown) => {
135
+ const { offer, from, callId } = p as { offer: RTCSessionDescriptionInit; from: string; callId: string };
136
+ onCallOffer?.(offer, from, callId);
137
+ });
138
+ socket.on('call-answer', (p: unknown) => {
139
+ const { answer, from, callId } = p as { answer: RTCSessionDescriptionInit; from: string; callId: string };
140
+ onCallAnswer?.(answer, from, callId);
141
+ });
142
+ socket.on('ice-candidate', (p: unknown) => {
143
+ const { candidate, from } = p as { candidate: RTCIceCandidateInit; from: string };
144
+ onIceCandidate?.(candidate, from);
145
+ });
146
+ socket.on('call-end', (p: unknown) => {
147
+ const { callId } = p as { callId: string };
148
+ onCallEnd?.(callId);
149
+ });
150
+
151
+ socket.on('typing_start', (p: unknown) => {
152
+ const { roomId, userId } = p as { roomId: string; userId: string };
153
+ onTypingStart?.(roomId, userId);
154
+ });
155
+ socket.on('typing_stop', (p: unknown) => {
156
+ const { roomId, userId } = p as { roomId: string; userId: string };
157
+ onTypingStop?.(roomId, userId);
158
+ });
159
+
160
+ socket.on('user:status', (p: unknown) => {
161
+ const { uid, status: s } = p as { uid: string; status: 'online'|'away'|'offline' };
162
+ onUserStatus?.(uid, s);
163
+ });
164
+
165
+ socket.on('ticket:created', onTicketCreated as (...args: unknown[]) => void);
166
+ socket.on('ticket:updated', onTicketUpdated as (...args: unknown[]) => void);
167
+ socket.on('transfer:assigned', onTransferAssigned as (...args: unknown[]) => void);
168
+
169
+ socket.on('error', (p: unknown) => {
170
+ const { code, message } = p as { code: string; message: string };
171
+ onError?.(code, message);
172
+ });
173
+ }).catch(err => {
174
+ console.error('[ajaxter-chat] Socket init error:', err.message);
175
+ if (mountedRef.current) setStatus('error');
176
+ });
177
+
178
+ return () => {
179
+ mountedRef.current = false;
180
+ socketRef.current?.disconnect();
181
+ socketRef.current = null;
182
+ };
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ }, [widgetId, viewerUid, serverUrl]);
185
+
186
+ // ── Public API ────────────────────────────────────────────────────────────
187
+
188
+ const joinRoom = useCallback((roomId: string) => {
189
+ currentRoom.current = roomId;
190
+ emit('join', { roomId, widgetId });
191
+ }, [emit, widgetId]);
192
+
193
+ const leaveRoom = useCallback((roomId: string) => {
194
+ if (currentRoom.current === roomId) currentRoom.current = null;
195
+ emit('leave', { roomId });
196
+ }, [emit]);
197
+
198
+ const emitMessage = useCallback((msg: ChatMessage) => emit('message', msg), [emit]);
199
+ const emitTypingStart = useCallback((roomId: string) => emit('typing_start', { roomId }), [emit]);
200
+ const emitTypingStop = useCallback((roomId: string) => emit('typing_stop', { roomId }), [emit]);
201
+ const emitMessageRead = useCallback((roomId: string, messageId: string) => emit('message:read', { roomId, messageId }), [emit]);
202
+
203
+ const emitPauseToggle = useCallback((roomId: string, targetUserId: string, paused: boolean) =>
204
+ emit('chat:pause', { roomId, targetUserId, paused }), [emit]);
205
+
206
+ const emitReport = useCallback((roomId: string, reason?: string) =>
207
+ emit('chat:report', { roomId, reason }), [emit]);
208
+
209
+ const emitBlock = useCallback((blockedUid: string) => emit('user:block', { blockedUid }), [emit]);
210
+ const emitUnblock = useCallback((blockedUid: string) => emit('user:unblock', { blockedUid }), [emit]);
211
+
212
+ const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) =>
213
+ emit('transfer', { fromRoomId, toUserId, note }), [emit]);
214
+
215
+ const emitAddParticipant = useCallback((roomId: string, uid: string) =>
216
+ emit('room:add-participant', { roomId, uid }), [emit]);
217
+
218
+ const emitCallOffer = useCallback((offer: RTCSessionDescriptionInit, toUid: string, callId: string, hasVideo?: boolean) =>
219
+ emit('call-offer', { offer, to: toUid, callId, hasVideo }), [emit]);
220
+
221
+ const emitCallAnswer = useCallback((answer: RTCSessionDescriptionInit, toUid: string, callId: string) =>
222
+ emit('call-answer', { answer, to: toUid, callId }), [emit]);
223
+
224
+ const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) =>
225
+ emit('ice-candidate', { candidate, to: toUid }), [emit]);
226
+
227
+ const emitCallEnd = useCallback((callId: string, toUid?: string) =>
228
+ emit('call-end', { callId, to: toUid }), [emit]);
229
+
230
+ const emitPresence = useCallback((presenceStatus: 'ACTIVE'|'AWAY'|'DND') =>
231
+ emit('presence:update', { status: presenceStatus }), [emit]);
232
+
233
+ const emitTicketCreate = useCallback((payload: { title: string; description: string; priority: string; createdBy?: string }) =>
234
+ emit('ticket:create', payload), [emit]);
235
+
236
+ const emitTicketUpdate = useCallback((payload: { ticketId: string; status?: string; priority?: string; assignedTo?: string }) =>
237
+ emit('ticket:update', payload), [emit]);
238
+
239
+ return {
240
+ status,
241
+ joinRoom, leaveRoom,
242
+ emitMessage, emitTypingStart, emitTypingStop, emitMessageRead,
243
+ emitPauseToggle, emitReport, emitBlock, emitUnblock, emitTransfer,
244
+ emitAddParticipant,
245
+ emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
246
+ emitPresence,
247
+ emitTicketCreate, emitTicketUpdate,
248
+ };
249
+ }