@solveo-ai/react-native 0.1.0 → 0.1.2
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.
- package/README.md +116 -0
- package/dist/index.d.mts +199 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +840 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +822 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +19 -10
- package/src/SolveoProvider.tsx +1 -1
- package/src/hooks/useChat.ts +10 -7
- package/src/hooks/usePushNotifications.ts +29 -12
package/dist/index.js
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var sdkCore = require('@solveo-ai/sdk-core');
|
|
5
|
+
var AsyncStorage = require('@react-native-async-storage/async-storage');
|
|
6
|
+
var reactNative = require('react-native');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
11
|
+
var AsyncStorage__default = /*#__PURE__*/_interopDefault(AsyncStorage);
|
|
12
|
+
|
|
13
|
+
// src/SolveoProvider.tsx
|
|
14
|
+
var SolveoContext = React.createContext(null);
|
|
15
|
+
function SolveoProvider({ children, config }) {
|
|
16
|
+
const sdk = React.useMemo(
|
|
17
|
+
() => new sdkCore.SolveoSDK({
|
|
18
|
+
...config,
|
|
19
|
+
platform: "react-native"
|
|
20
|
+
}),
|
|
21
|
+
[config.apiUrl, config.apiKey, config.token, config.widgetId]
|
|
22
|
+
);
|
|
23
|
+
const [customerSocket, setCustomerSocket] = React.useState(null);
|
|
24
|
+
const [customerSocketManager, setCustomerSocketManager] = React.useState(null);
|
|
25
|
+
const [agentSocket, setAgentSocket] = React.useState(null);
|
|
26
|
+
const [agentSocketManager, setAgentSocketManager] = React.useState(null);
|
|
27
|
+
const [streamingSocket, setStreamingSocket] = React.useState(null);
|
|
28
|
+
const connectCustomerSocket = (conversationId) => {
|
|
29
|
+
if (customerSocket) {
|
|
30
|
+
customerSocket.disconnect();
|
|
31
|
+
}
|
|
32
|
+
const socket = sdkCore.createCustomerSocket(config.apiUrl, conversationId);
|
|
33
|
+
const manager = new sdkCore.CustomerSocketManager(socket);
|
|
34
|
+
setCustomerSocket(socket);
|
|
35
|
+
setCustomerSocketManager(manager);
|
|
36
|
+
socket.connect();
|
|
37
|
+
};
|
|
38
|
+
const disconnectCustomerSocket = () => {
|
|
39
|
+
if (customerSocket) {
|
|
40
|
+
customerSocket.disconnect();
|
|
41
|
+
setCustomerSocket(null);
|
|
42
|
+
setCustomerSocketManager(null);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const connectAgentSocket = (token) => {
|
|
46
|
+
if (agentSocket) {
|
|
47
|
+
agentSocket.disconnect();
|
|
48
|
+
}
|
|
49
|
+
const socket = sdkCore.createAgentSocket(config.apiUrl, token);
|
|
50
|
+
const manager = new sdkCore.AgentSocketManager(socket);
|
|
51
|
+
setAgentSocket(socket);
|
|
52
|
+
setAgentSocketManager(manager);
|
|
53
|
+
socket.connect();
|
|
54
|
+
};
|
|
55
|
+
const disconnectAgentSocket = () => {
|
|
56
|
+
if (agentSocket) {
|
|
57
|
+
agentSocket.disconnect();
|
|
58
|
+
setAgentSocket(null);
|
|
59
|
+
setAgentSocketManager(null);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const connectStreamingSocket = (conversationId) => {
|
|
63
|
+
if (streamingSocket) {
|
|
64
|
+
streamingSocket.disconnect();
|
|
65
|
+
}
|
|
66
|
+
const socket = sdkCore.createStreamingSocket(config.apiUrl, conversationId);
|
|
67
|
+
setStreamingSocket(socket);
|
|
68
|
+
socket.connect();
|
|
69
|
+
};
|
|
70
|
+
const disconnectStreamingSocket = () => {
|
|
71
|
+
if (streamingSocket) {
|
|
72
|
+
streamingSocket.disconnect();
|
|
73
|
+
setStreamingSocket(null);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (config.autoConnectCustomer && config.conversationId) {
|
|
78
|
+
connectCustomerSocket(config.conversationId);
|
|
79
|
+
}
|
|
80
|
+
if (config.autoConnectAgent && config.token) {
|
|
81
|
+
connectAgentSocket(config.token);
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
disconnectCustomerSocket();
|
|
85
|
+
disconnectAgentSocket();
|
|
86
|
+
disconnectStreamingSocket();
|
|
87
|
+
};
|
|
88
|
+
}, [config.autoConnectCustomer, config.autoConnectAgent, config.conversationId, config.token]);
|
|
89
|
+
const value = {
|
|
90
|
+
sdk,
|
|
91
|
+
config,
|
|
92
|
+
customerSocket,
|
|
93
|
+
customerSocketManager,
|
|
94
|
+
agentSocket,
|
|
95
|
+
agentSocketManager,
|
|
96
|
+
streamingSocket,
|
|
97
|
+
connectCustomerSocket,
|
|
98
|
+
disconnectCustomerSocket,
|
|
99
|
+
connectAgentSocket,
|
|
100
|
+
disconnectAgentSocket,
|
|
101
|
+
connectStreamingSocket,
|
|
102
|
+
disconnectStreamingSocket
|
|
103
|
+
};
|
|
104
|
+
return /* @__PURE__ */ React__default.default.createElement(SolveoContext.Provider, { value }, children);
|
|
105
|
+
}
|
|
106
|
+
function useSolveo() {
|
|
107
|
+
const context = React.useContext(SolveoContext);
|
|
108
|
+
if (!context) {
|
|
109
|
+
throw new Error("useSolveo must be used within a SolveoProvider");
|
|
110
|
+
}
|
|
111
|
+
return context;
|
|
112
|
+
}
|
|
113
|
+
var STORAGE_KEYS = {
|
|
114
|
+
CONVERSATION_ID: "@solveo/conversation_id",
|
|
115
|
+
MESSAGES: "@solveo/messages",
|
|
116
|
+
USER_IDENTITY: "@solveo/user_identity",
|
|
117
|
+
WIDGET_CONFIG: "@solveo/widget_config"
|
|
118
|
+
};
|
|
119
|
+
var storage = {
|
|
120
|
+
async getConversationId() {
|
|
121
|
+
return AsyncStorage__default.default.getItem(STORAGE_KEYS.CONVERSATION_ID);
|
|
122
|
+
},
|
|
123
|
+
async setConversationId(id) {
|
|
124
|
+
await AsyncStorage__default.default.setItem(STORAGE_KEYS.CONVERSATION_ID, id);
|
|
125
|
+
},
|
|
126
|
+
async clearConversationId() {
|
|
127
|
+
await AsyncStorage__default.default.removeItem(STORAGE_KEYS.CONVERSATION_ID);
|
|
128
|
+
},
|
|
129
|
+
async getMessages(conversationId) {
|
|
130
|
+
const data = await AsyncStorage__default.default.getItem(`${STORAGE_KEYS.MESSAGES}_${conversationId}`);
|
|
131
|
+
return data ? JSON.parse(data) : [];
|
|
132
|
+
},
|
|
133
|
+
async setMessages(conversationId, messages) {
|
|
134
|
+
await AsyncStorage__default.default.setItem(`${STORAGE_KEYS.MESSAGES}_${conversationId}`, JSON.stringify(messages));
|
|
135
|
+
},
|
|
136
|
+
async getUserIdentity() {
|
|
137
|
+
const data = await AsyncStorage__default.default.getItem(STORAGE_KEYS.USER_IDENTITY);
|
|
138
|
+
return data ? JSON.parse(data) : null;
|
|
139
|
+
},
|
|
140
|
+
async setUserIdentity(identity) {
|
|
141
|
+
await AsyncStorage__default.default.setItem(STORAGE_KEYS.USER_IDENTITY, JSON.stringify(identity));
|
|
142
|
+
},
|
|
143
|
+
async getWidgetConfig(widgetId) {
|
|
144
|
+
const data = await AsyncStorage__default.default.getItem(`${STORAGE_KEYS.WIDGET_CONFIG}_${widgetId}`);
|
|
145
|
+
return data ? JSON.parse(data) : null;
|
|
146
|
+
},
|
|
147
|
+
async setWidgetConfig(widgetId, config) {
|
|
148
|
+
await AsyncStorage__default.default.setItem(`${STORAGE_KEYS.WIDGET_CONFIG}_${widgetId}`, JSON.stringify(config));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/hooks/useChat.ts
|
|
153
|
+
function useChat(options = {}) {
|
|
154
|
+
const { sdk, config, streamingSocket, connectStreamingSocket } = useSolveo();
|
|
155
|
+
const [conversation, setConversation] = React.useState(null);
|
|
156
|
+
const [messages, setMessages] = React.useState([]);
|
|
157
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
158
|
+
const [isStreaming, setIsStreaming] = React.useState(false);
|
|
159
|
+
const [streamingContent, setStreamingContent] = React.useState("");
|
|
160
|
+
const [error, setError] = React.useState(null);
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
if (options.persistConversation) {
|
|
163
|
+
loadPersistedConversation();
|
|
164
|
+
}
|
|
165
|
+
}, [options.persistConversation]);
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
if (!streamingSocket || !conversation) return;
|
|
168
|
+
const handleMessage = (msg) => {
|
|
169
|
+
if (msg.type === "agent_typing") {
|
|
170
|
+
setIsStreaming(true);
|
|
171
|
+
setStreamingContent("");
|
|
172
|
+
} else if (msg.type === "agent_message_chunk") {
|
|
173
|
+
setStreamingContent((prev) => prev + (msg.content || ""));
|
|
174
|
+
} else if (msg.type === "agent_message_complete") {
|
|
175
|
+
setIsStreaming(false);
|
|
176
|
+
if (msg.message_id) {
|
|
177
|
+
loadMessages(conversation.id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const handleError = (err) => {
|
|
182
|
+
setError(new Error(err.message || "Streaming error"));
|
|
183
|
+
setIsStreaming(false);
|
|
184
|
+
};
|
|
185
|
+
streamingSocket.on("message", handleMessage);
|
|
186
|
+
streamingSocket.on("error", handleError);
|
|
187
|
+
return () => {
|
|
188
|
+
streamingSocket.off("message", handleMessage);
|
|
189
|
+
streamingSocket.off("error", handleError);
|
|
190
|
+
};
|
|
191
|
+
}, [streamingSocket, conversation]);
|
|
192
|
+
const loadPersistedConversation = async () => {
|
|
193
|
+
try {
|
|
194
|
+
const convId = await storage.getConversationId();
|
|
195
|
+
if (convId) {
|
|
196
|
+
const conv = await sdk.widgetConversations.get(convId);
|
|
197
|
+
setConversation(conv);
|
|
198
|
+
await loadMessages(convId);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error("Failed to load persisted conversation:", err);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const createConversation = React.useCallback(async () => {
|
|
205
|
+
setIsLoading(true);
|
|
206
|
+
setError(null);
|
|
207
|
+
try {
|
|
208
|
+
const identity = await storage.getUserIdentity();
|
|
209
|
+
const conv = await sdk.widgetConversations.create({
|
|
210
|
+
widget_id: options.widgetId || config.widgetId,
|
|
211
|
+
agent_id: options.agentId,
|
|
212
|
+
customer_email: identity?.email || config.user?.email,
|
|
213
|
+
customer_name: identity?.name || config.user?.name
|
|
214
|
+
});
|
|
215
|
+
setConversation(conv);
|
|
216
|
+
if (options.persistConversation) {
|
|
217
|
+
await storage.setConversationId(conv.id);
|
|
218
|
+
}
|
|
219
|
+
connectStreamingSocket(conv.id);
|
|
220
|
+
return conv;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
setError(err);
|
|
223
|
+
throw err;
|
|
224
|
+
} finally {
|
|
225
|
+
setIsLoading(false);
|
|
226
|
+
}
|
|
227
|
+
}, [sdk, options, config]);
|
|
228
|
+
const loadMessages = React.useCallback(async (conversationId) => {
|
|
229
|
+
try {
|
|
230
|
+
const msgs = await sdk.widgetMessages.list(conversationId);
|
|
231
|
+
setMessages(msgs);
|
|
232
|
+
if (options.persistConversation) {
|
|
233
|
+
await storage.setMessages(conversationId, msgs);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
setError(err);
|
|
237
|
+
}
|
|
238
|
+
}, [sdk, options]);
|
|
239
|
+
const sendMessage = React.useCallback(async (content) => {
|
|
240
|
+
if (!conversation) {
|
|
241
|
+
throw new Error("No active conversation");
|
|
242
|
+
}
|
|
243
|
+
setIsLoading(true);
|
|
244
|
+
setError(null);
|
|
245
|
+
try {
|
|
246
|
+
if (streamingSocket?.isConnected()) {
|
|
247
|
+
streamingSocket.sendMessage(content);
|
|
248
|
+
} else {
|
|
249
|
+
const response = await sdk.widgetMessages.send(conversation.id, { content });
|
|
250
|
+
setMessages((prev) => [...prev, response.message, response.ai_response].filter(Boolean));
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
setError(err);
|
|
254
|
+
throw err;
|
|
255
|
+
} finally {
|
|
256
|
+
setIsLoading(false);
|
|
257
|
+
}
|
|
258
|
+
}, [conversation, streamingSocket, sdk]);
|
|
259
|
+
const submitFeedback = React.useCallback(async (messageId, rating) => {
|
|
260
|
+
if (!conversation) return;
|
|
261
|
+
try {
|
|
262
|
+
await sdk.widgetMessages.submitFeedback(conversation.id, messageId, rating);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
setError(err);
|
|
265
|
+
}
|
|
266
|
+
}, [conversation, sdk]);
|
|
267
|
+
return {
|
|
268
|
+
conversation,
|
|
269
|
+
messages,
|
|
270
|
+
isLoading,
|
|
271
|
+
isStreaming,
|
|
272
|
+
streamingContent,
|
|
273
|
+
error,
|
|
274
|
+
createConversation,
|
|
275
|
+
sendMessage,
|
|
276
|
+
loadMessages,
|
|
277
|
+
submitFeedback
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function useRealtime() {
|
|
281
|
+
const { customerSocketManager } = useSolveo();
|
|
282
|
+
const [isConnected, setIsConnected] = React.useState(false);
|
|
283
|
+
const [agentTyping, setAgentTyping] = React.useState(false);
|
|
284
|
+
const [queuePosition, setQueuePosition] = React.useState(null);
|
|
285
|
+
const [estimatedWait, setEstimatedWait] = React.useState(null);
|
|
286
|
+
React.useEffect(() => {
|
|
287
|
+
if (!customerSocketManager) return;
|
|
288
|
+
const socket = customerSocketManager.getSocket();
|
|
289
|
+
socket.on("connect", () => setIsConnected(true));
|
|
290
|
+
socket.on("disconnect", () => setIsConnected(false));
|
|
291
|
+
customerSocketManager.onAgentTyping(() => {
|
|
292
|
+
setAgentTyping(true);
|
|
293
|
+
setTimeout(() => setAgentTyping(false), 3e3);
|
|
294
|
+
});
|
|
295
|
+
customerSocketManager.onQueueUpdate((data) => {
|
|
296
|
+
setQueuePosition(data.queue_position);
|
|
297
|
+
setEstimatedWait(data.estimated_wait);
|
|
298
|
+
});
|
|
299
|
+
return () => {
|
|
300
|
+
socket.off("connect");
|
|
301
|
+
socket.off("disconnect");
|
|
302
|
+
};
|
|
303
|
+
}, [customerSocketManager]);
|
|
304
|
+
return {
|
|
305
|
+
isConnected,
|
|
306
|
+
agentTyping,
|
|
307
|
+
queuePosition,
|
|
308
|
+
estimatedWait
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function useEscalation() {
|
|
312
|
+
const { customerSocketManager, connectCustomerSocket, disconnectCustomerSocket } = useSolveo();
|
|
313
|
+
const [escalationId, setEscalationId] = React.useState(null);
|
|
314
|
+
const [assignedAgent, setAssignedAgent] = React.useState(null);
|
|
315
|
+
const [status, setStatus] = React.useState("idle");
|
|
316
|
+
React.useEffect(() => {
|
|
317
|
+
if (!customerSocketManager) return;
|
|
318
|
+
customerSocketManager.onAgentConnected((data) => {
|
|
319
|
+
setEscalationId(data.escalation_id);
|
|
320
|
+
setAssignedAgent({
|
|
321
|
+
name: data.agent_name,
|
|
322
|
+
avatar_url: data.agent_avatar
|
|
323
|
+
});
|
|
324
|
+
setStatus("active");
|
|
325
|
+
});
|
|
326
|
+
customerSocketManager.onQueueUpdate((data) => {
|
|
327
|
+
setStatus("queued");
|
|
328
|
+
});
|
|
329
|
+
customerSocketManager.onEscalationResolved(() => {
|
|
330
|
+
setStatus("resolved");
|
|
331
|
+
});
|
|
332
|
+
customerSocketManager.onHandedBack(() => {
|
|
333
|
+
setStatus("idle");
|
|
334
|
+
setEscalationId(null);
|
|
335
|
+
setAssignedAgent(null);
|
|
336
|
+
});
|
|
337
|
+
}, [customerSocketManager]);
|
|
338
|
+
const requestHuman = React.useCallback((conversationId) => {
|
|
339
|
+
connectCustomerSocket(conversationId);
|
|
340
|
+
if (customerSocketManager) {
|
|
341
|
+
customerSocketManager.requestHuman();
|
|
342
|
+
setStatus("requesting");
|
|
343
|
+
}
|
|
344
|
+
}, [customerSocketManager, connectCustomerSocket]);
|
|
345
|
+
const endChat = React.useCallback(() => {
|
|
346
|
+
if (escalationId && customerSocketManager) {
|
|
347
|
+
customerSocketManager.endChat(escalationId);
|
|
348
|
+
disconnectCustomerSocket();
|
|
349
|
+
setStatus("idle");
|
|
350
|
+
setEscalationId(null);
|
|
351
|
+
setAssignedAgent(null);
|
|
352
|
+
}
|
|
353
|
+
}, [escalationId, customerSocketManager, disconnectCustomerSocket]);
|
|
354
|
+
const submitCSAT = React.useCallback((rating) => {
|
|
355
|
+
if (escalationId && customerSocketManager) {
|
|
356
|
+
customerSocketManager.submitCSAT(escalationId, rating);
|
|
357
|
+
}
|
|
358
|
+
}, [escalationId, customerSocketManager]);
|
|
359
|
+
return {
|
|
360
|
+
escalationId,
|
|
361
|
+
assignedAgent,
|
|
362
|
+
status,
|
|
363
|
+
requestHuman,
|
|
364
|
+
endChat,
|
|
365
|
+
submitCSAT
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function useWidgetConfig(widgetId) {
|
|
369
|
+
const { sdk, config } = useSolveo();
|
|
370
|
+
const effectiveWidgetId = widgetId || config.widgetId;
|
|
371
|
+
const [widget, setWidget] = React.useState(null);
|
|
372
|
+
const [widgetConfig, setWidgetConfig] = React.useState(null);
|
|
373
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
374
|
+
const [error, setError] = React.useState(null);
|
|
375
|
+
React.useEffect(() => {
|
|
376
|
+
if (effectiveWidgetId) {
|
|
377
|
+
loadConfig();
|
|
378
|
+
}
|
|
379
|
+
}, [effectiveWidgetId]);
|
|
380
|
+
const loadConfig = async () => {
|
|
381
|
+
if (!effectiveWidgetId) return;
|
|
382
|
+
setIsLoading(true);
|
|
383
|
+
setError(null);
|
|
384
|
+
try {
|
|
385
|
+
const cached = await storage.getWidgetConfig(effectiveWidgetId);
|
|
386
|
+
if (cached) {
|
|
387
|
+
setWidget(cached);
|
|
388
|
+
setWidgetConfig(cached.config);
|
|
389
|
+
}
|
|
390
|
+
const w = await sdk.widgetConfig.getConfig(effectiveWidgetId);
|
|
391
|
+
setWidget(w);
|
|
392
|
+
setWidgetConfig(w.config);
|
|
393
|
+
await storage.setWidgetConfig(effectiveWidgetId, w);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
setError(err);
|
|
396
|
+
} finally {
|
|
397
|
+
setIsLoading(false);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
widget,
|
|
402
|
+
widgetConfig,
|
|
403
|
+
isLoading,
|
|
404
|
+
error,
|
|
405
|
+
reload: loadConfig
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function usePushNotifications(options = {}) {
|
|
409
|
+
const { sdk } = useSolveo();
|
|
410
|
+
const {
|
|
411
|
+
mode = "widget",
|
|
412
|
+
conversationId,
|
|
413
|
+
autoRegister = false,
|
|
414
|
+
handlers
|
|
415
|
+
} = options;
|
|
416
|
+
const [state, setState] = React.useState({
|
|
417
|
+
device: null,
|
|
418
|
+
isRegistered: false,
|
|
419
|
+
isInitialized: false,
|
|
420
|
+
permissionStatus: "undetermined"
|
|
421
|
+
});
|
|
422
|
+
const requestPermission = React.useCallback(async () => {
|
|
423
|
+
try {
|
|
424
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
425
|
+
const authStatus = await messaging().requestPermission();
|
|
426
|
+
const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL;
|
|
427
|
+
setState((prev) => ({
|
|
428
|
+
...prev,
|
|
429
|
+
permissionStatus: enabled ? "granted" : "denied"
|
|
430
|
+
}));
|
|
431
|
+
return enabled;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error("Failed to request permission (Firebase Messaging not installed?):", error);
|
|
434
|
+
try {
|
|
435
|
+
const Notifications = await import('expo-notifications');
|
|
436
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
437
|
+
const granted = status === "granted";
|
|
438
|
+
setState((prev) => ({
|
|
439
|
+
...prev,
|
|
440
|
+
permissionStatus: granted ? "granted" : "denied"
|
|
441
|
+
}));
|
|
442
|
+
return granted;
|
|
443
|
+
} catch (expoError) {
|
|
444
|
+
console.error("Failed to request permission (Expo Notifications not installed?):", expoError);
|
|
445
|
+
setState((prev) => ({ ...prev, permissionStatus: "denied" }));
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}, []);
|
|
450
|
+
const getToken = React.useCallback(async () => {
|
|
451
|
+
try {
|
|
452
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
453
|
+
const token = await messaging().getToken();
|
|
454
|
+
return token;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error("Failed to get FCM token:", error);
|
|
457
|
+
try {
|
|
458
|
+
const Notifications = await import('expo-notifications');
|
|
459
|
+
const token = await Notifications.getExpoPushTokenAsync();
|
|
460
|
+
return token.data;
|
|
461
|
+
} catch (expoError) {
|
|
462
|
+
console.error("Failed to get Expo push token:", expoError);
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}, []);
|
|
467
|
+
const registerDevice = React.useCallback(
|
|
468
|
+
async (convId, pushToken, bundleId) => {
|
|
469
|
+
if (mode !== "widget") {
|
|
470
|
+
console.warn("registerDevice is only for widget mode. Use registerAgentDevice for agent mode.");
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const targetConversationId = convId || conversationId;
|
|
474
|
+
if (!targetConversationId) {
|
|
475
|
+
console.error("conversationId is required for widget mode registration");
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const token = pushToken || await getToken();
|
|
480
|
+
if (!token) {
|
|
481
|
+
throw new Error("Failed to get push token");
|
|
482
|
+
}
|
|
483
|
+
const platform = reactNative.Platform.OS === "ios" ? "IOS" : "ANDROID";
|
|
484
|
+
const device = await sdk.widgetDevices.register({
|
|
485
|
+
conversation_id: targetConversationId,
|
|
486
|
+
platform,
|
|
487
|
+
push_token: token,
|
|
488
|
+
bundle_id: bundleId
|
|
489
|
+
});
|
|
490
|
+
setState((prev) => ({ ...prev, device, isRegistered: true }));
|
|
491
|
+
return device;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error("Failed to register device:", error);
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
[sdk, conversationId, mode, getToken]
|
|
498
|
+
);
|
|
499
|
+
const registerAgentDevice = React.useCallback(
|
|
500
|
+
async (pushToken, deviceName) => {
|
|
501
|
+
if (mode !== "agent") {
|
|
502
|
+
console.warn("registerAgentDevice is only for agent mode. Use registerDevice for widget mode.");
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const token = pushToken || await getToken();
|
|
507
|
+
if (!token) {
|
|
508
|
+
throw new Error("Failed to get push token");
|
|
509
|
+
}
|
|
510
|
+
const platform = reactNative.Platform.OS === "ios" ? "IOS" : "ANDROID";
|
|
511
|
+
const response = await sdk.client.post("/user-devices", {
|
|
512
|
+
platform,
|
|
513
|
+
push_token: token,
|
|
514
|
+
device_name: deviceName || `${reactNative.Platform.OS} Device`
|
|
515
|
+
});
|
|
516
|
+
setState((prev) => ({
|
|
517
|
+
...prev,
|
|
518
|
+
device: response.data,
|
|
519
|
+
isRegistered: true
|
|
520
|
+
}));
|
|
521
|
+
return response.data;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error("Failed to register agent device:", error);
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
[sdk, mode, getToken]
|
|
528
|
+
);
|
|
529
|
+
const unregisterDevice = React.useCallback(async () => {
|
|
530
|
+
if (!state.device) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
if (mode === "widget") {
|
|
535
|
+
await sdk.widgetDevices.unregister(state.device.id);
|
|
536
|
+
} else {
|
|
537
|
+
await sdk.client.delete(`/user-devices/${state.device.id}`);
|
|
538
|
+
}
|
|
539
|
+
setState((prev) => ({
|
|
540
|
+
...prev,
|
|
541
|
+
device: null,
|
|
542
|
+
isRegistered: false
|
|
543
|
+
}));
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error("Failed to unregister device:", error);
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}, [sdk, state.device, mode]);
|
|
549
|
+
React.useEffect(() => {
|
|
550
|
+
if (!handlers?.onNotificationReceived) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
let unsubscribe;
|
|
554
|
+
const setupForegroundHandler = async () => {
|
|
555
|
+
try {
|
|
556
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
557
|
+
unsubscribe = messaging().onMessage(async (remoteMessage) => {
|
|
558
|
+
console.log("Foreground notification received:", remoteMessage);
|
|
559
|
+
handlers.onNotificationReceived?.(remoteMessage);
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
try {
|
|
563
|
+
const Notifications = await import('expo-notifications');
|
|
564
|
+
const subscription = Notifications.addNotificationReceivedListener((notification) => {
|
|
565
|
+
console.log("Foreground notification received (Expo):", notification);
|
|
566
|
+
handlers.onNotificationReceived?.(notification);
|
|
567
|
+
});
|
|
568
|
+
unsubscribe = () => subscription.remove();
|
|
569
|
+
} catch (expoError) {
|
|
570
|
+
console.error("Failed to setup foreground handler:", expoError);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
setupForegroundHandler();
|
|
575
|
+
return () => {
|
|
576
|
+
unsubscribe?.();
|
|
577
|
+
};
|
|
578
|
+
}, [handlers?.onNotificationReceived]);
|
|
579
|
+
React.useEffect(() => {
|
|
580
|
+
if (!handlers?.onNotificationTapped) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
let unsubscribe;
|
|
584
|
+
const setupTapHandler = async () => {
|
|
585
|
+
try {
|
|
586
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
587
|
+
messaging().getInitialNotification().then((remoteMessage) => {
|
|
588
|
+
if (remoteMessage) {
|
|
589
|
+
console.log("Notification opened app from quit state:", remoteMessage);
|
|
590
|
+
handlers.onNotificationTapped?.(remoteMessage);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
unsubscribe = messaging().onNotificationOpenedApp((remoteMessage) => {
|
|
594
|
+
console.log("Notification opened app from background:", remoteMessage);
|
|
595
|
+
handlers.onNotificationTapped?.(remoteMessage);
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
try {
|
|
599
|
+
const Notifications = await import('expo-notifications');
|
|
600
|
+
const subscription = Notifications.addNotificationResponseReceivedListener((response) => {
|
|
601
|
+
console.log("Notification tapped (Expo):", response);
|
|
602
|
+
handlers.onNotificationTapped?.(response.notification);
|
|
603
|
+
});
|
|
604
|
+
unsubscribe = () => subscription.remove();
|
|
605
|
+
} catch (expoError) {
|
|
606
|
+
console.error("Failed to setup tap handler:", expoError);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
setupTapHandler();
|
|
611
|
+
return () => {
|
|
612
|
+
unsubscribe?.();
|
|
613
|
+
};
|
|
614
|
+
}, [handlers?.onNotificationTapped]);
|
|
615
|
+
React.useEffect(() => {
|
|
616
|
+
let unsubscribe;
|
|
617
|
+
const setupTokenRefreshHandler = async () => {
|
|
618
|
+
try {
|
|
619
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
620
|
+
unsubscribe = messaging().onTokenRefresh(async (newToken) => {
|
|
621
|
+
console.log("FCM token refreshed:", newToken);
|
|
622
|
+
if (state.isRegistered) {
|
|
623
|
+
if (mode === "widget" && conversationId) {
|
|
624
|
+
await registerDevice(conversationId, newToken);
|
|
625
|
+
} else if (mode === "agent") {
|
|
626
|
+
await registerAgentDevice(newToken);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
} catch (error) {
|
|
631
|
+
console.error("Failed to setup token refresh handler:", error);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
setupTokenRefreshHandler();
|
|
635
|
+
return () => {
|
|
636
|
+
unsubscribe?.();
|
|
637
|
+
};
|
|
638
|
+
}, [state.isRegistered, mode, conversationId, registerDevice, registerAgentDevice]);
|
|
639
|
+
React.useEffect(() => {
|
|
640
|
+
const checkPermission = async () => {
|
|
641
|
+
try {
|
|
642
|
+
const messaging = await import('@react-native-firebase/messaging').then((m) => m.default);
|
|
643
|
+
const authStatus = await messaging().hasPermission();
|
|
644
|
+
const granted = authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL;
|
|
645
|
+
setState((prev) => ({
|
|
646
|
+
...prev,
|
|
647
|
+
permissionStatus: granted ? "granted" : authStatus === messaging.AuthorizationStatus.NOT_DETERMINED ? "undetermined" : "denied",
|
|
648
|
+
isInitialized: true
|
|
649
|
+
}));
|
|
650
|
+
if (granted && autoRegister && !state.isRegistered) {
|
|
651
|
+
if (mode === "widget" && conversationId) {
|
|
652
|
+
await registerDevice();
|
|
653
|
+
} else if (mode === "agent") {
|
|
654
|
+
await registerAgentDevice();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("Failed to check permission:", error);
|
|
659
|
+
setState((prev) => ({ ...prev, isInitialized: true }));
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
checkPermission();
|
|
663
|
+
}, [autoRegister, mode, conversationId, registerDevice, registerAgentDevice]);
|
|
664
|
+
return {
|
|
665
|
+
...state,
|
|
666
|
+
requestPermission,
|
|
667
|
+
registerDevice,
|
|
668
|
+
registerAgentDevice,
|
|
669
|
+
unregisterDevice
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function useAttachments(conversationId) {
|
|
673
|
+
const { sdk } = useSolveo();
|
|
674
|
+
const [uploading, setUploading] = React.useState(false);
|
|
675
|
+
const [uploadProgress, setUploadProgress] = React.useState(0);
|
|
676
|
+
const uploadAttachment = React.useCallback(async (file, attachmentType) => {
|
|
677
|
+
if (!conversationId) {
|
|
678
|
+
throw new Error("No conversation ID provided");
|
|
679
|
+
}
|
|
680
|
+
setUploading(true);
|
|
681
|
+
setUploadProgress(0);
|
|
682
|
+
try {
|
|
683
|
+
const response = await fetch(file.uri);
|
|
684
|
+
const blob = await response.blob();
|
|
685
|
+
const attachment = await sdk.widgetAttachments.upload(conversationId, blob, attachmentType);
|
|
686
|
+
setUploadProgress(100);
|
|
687
|
+
return attachment;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error("Failed to upload attachment:", error);
|
|
690
|
+
throw error;
|
|
691
|
+
} finally {
|
|
692
|
+
setUploading(false);
|
|
693
|
+
setUploadProgress(0);
|
|
694
|
+
}
|
|
695
|
+
}, [sdk, conversationId]);
|
|
696
|
+
return {
|
|
697
|
+
uploading,
|
|
698
|
+
uploadProgress,
|
|
699
|
+
uploadAttachment
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function useAgents(accountId) {
|
|
703
|
+
const { sdk } = useSolveo();
|
|
704
|
+
const [agents, setAgents] = React.useState([]);
|
|
705
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
706
|
+
const [error, setError] = React.useState(null);
|
|
707
|
+
const loadAgents = React.useCallback(async () => {
|
|
708
|
+
if (!accountId) return;
|
|
709
|
+
setIsLoading(true);
|
|
710
|
+
try {
|
|
711
|
+
const data = await sdk.agents.list(accountId);
|
|
712
|
+
setAgents(data);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
setError(err);
|
|
715
|
+
} finally {
|
|
716
|
+
setIsLoading(false);
|
|
717
|
+
}
|
|
718
|
+
}, [sdk, accountId]);
|
|
719
|
+
React.useEffect(() => {
|
|
720
|
+
if (accountId) {
|
|
721
|
+
loadAgents();
|
|
722
|
+
}
|
|
723
|
+
}, [accountId]);
|
|
724
|
+
const createAgent = React.useCallback(async (data) => {
|
|
725
|
+
if (!accountId) throw new Error("No account ID");
|
|
726
|
+
const agent = await sdk.agents.create(accountId, data);
|
|
727
|
+
setAgents((prev) => [...prev, agent]);
|
|
728
|
+
return agent;
|
|
729
|
+
}, [sdk, accountId]);
|
|
730
|
+
const updateAgent = React.useCallback(async (id, data) => {
|
|
731
|
+
const agent = await sdk.agents.update(id, data);
|
|
732
|
+
setAgents((prev) => prev.map((a) => a.id === id ? agent : a));
|
|
733
|
+
return agent;
|
|
734
|
+
}, [sdk]);
|
|
735
|
+
const deleteAgent = React.useCallback(async (id) => {
|
|
736
|
+
await sdk.agents.delete(id);
|
|
737
|
+
setAgents((prev) => prev.filter((a) => a.id !== id));
|
|
738
|
+
}, [sdk]);
|
|
739
|
+
return {
|
|
740
|
+
agents,
|
|
741
|
+
isLoading,
|
|
742
|
+
error,
|
|
743
|
+
createAgent,
|
|
744
|
+
updateAgent,
|
|
745
|
+
deleteAgent,
|
|
746
|
+
reload: loadAgents
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function useConversations(accountId) {
|
|
750
|
+
const { sdk } = useSolveo();
|
|
751
|
+
const [conversations, setConversations] = React.useState([]);
|
|
752
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
753
|
+
const [error, setError] = React.useState(null);
|
|
754
|
+
const loadConversations = React.useCallback(async () => {
|
|
755
|
+
if (!accountId) return;
|
|
756
|
+
setIsLoading(true);
|
|
757
|
+
try {
|
|
758
|
+
const data = await sdk.conversations.list(accountId);
|
|
759
|
+
setConversations(data);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
setError(err);
|
|
762
|
+
} finally {
|
|
763
|
+
setIsLoading(false);
|
|
764
|
+
}
|
|
765
|
+
}, [sdk, accountId]);
|
|
766
|
+
React.useEffect(() => {
|
|
767
|
+
if (accountId) {
|
|
768
|
+
loadConversations();
|
|
769
|
+
}
|
|
770
|
+
}, [accountId]);
|
|
771
|
+
const resolveConversation = React.useCallback(async (id) => {
|
|
772
|
+
await sdk.conversations.resolve(id);
|
|
773
|
+
setConversations((prev) => prev.map((c) => c.id === id ? { ...c, status: "RESOLVED" } : c));
|
|
774
|
+
}, [sdk]);
|
|
775
|
+
return {
|
|
776
|
+
conversations,
|
|
777
|
+
isLoading,
|
|
778
|
+
error,
|
|
779
|
+
resolveConversation,
|
|
780
|
+
reload: loadConversations
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function useAnalytics(accountId, days = 30) {
|
|
784
|
+
const { sdk } = useSolveo();
|
|
785
|
+
const [metrics, setMetrics] = React.useState(null);
|
|
786
|
+
const [sentiment, setSentiment] = React.useState(null);
|
|
787
|
+
const [feedback, setFeedback] = React.useState(null);
|
|
788
|
+
const [tokenUsage, setTokenUsage] = React.useState(null);
|
|
789
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
790
|
+
const [error, setError] = React.useState(null);
|
|
791
|
+
const loadAnalytics = React.useCallback(async () => {
|
|
792
|
+
if (!accountId) return;
|
|
793
|
+
setIsLoading(true);
|
|
794
|
+
try {
|
|
795
|
+
const [m, s, f, t] = await Promise.all([
|
|
796
|
+
sdk.analytics.getMetrics(accountId, days),
|
|
797
|
+
sdk.analytics.getSentiment(accountId, days),
|
|
798
|
+
sdk.analytics.getFeedback(accountId, days),
|
|
799
|
+
sdk.analytics.getTokenUsage(accountId, days)
|
|
800
|
+
]);
|
|
801
|
+
setMetrics(m);
|
|
802
|
+
setSentiment(s);
|
|
803
|
+
setFeedback(f);
|
|
804
|
+
setTokenUsage(t);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
setError(err);
|
|
807
|
+
} finally {
|
|
808
|
+
setIsLoading(false);
|
|
809
|
+
}
|
|
810
|
+
}, [sdk, accountId, days]);
|
|
811
|
+
React.useEffect(() => {
|
|
812
|
+
if (accountId) {
|
|
813
|
+
loadAnalytics();
|
|
814
|
+
}
|
|
815
|
+
}, [accountId, days]);
|
|
816
|
+
return {
|
|
817
|
+
metrics,
|
|
818
|
+
sentiment,
|
|
819
|
+
feedback,
|
|
820
|
+
tokenUsage,
|
|
821
|
+
isLoading,
|
|
822
|
+
error,
|
|
823
|
+
reload: loadAnalytics
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
exports.SolveoProvider = SolveoProvider;
|
|
828
|
+
exports.storage = storage;
|
|
829
|
+
exports.useAgents = useAgents;
|
|
830
|
+
exports.useAnalytics = useAnalytics;
|
|
831
|
+
exports.useAttachments = useAttachments;
|
|
832
|
+
exports.useChat = useChat;
|
|
833
|
+
exports.useConversations = useConversations;
|
|
834
|
+
exports.useEscalation = useEscalation;
|
|
835
|
+
exports.usePushNotifications = usePushNotifications;
|
|
836
|
+
exports.useRealtime = useRealtime;
|
|
837
|
+
exports.useSolveo = useSolveo;
|
|
838
|
+
exports.useWidgetConfig = useWidgetConfig;
|
|
839
|
+
//# sourceMappingURL=index.js.map
|
|
840
|
+
//# sourceMappingURL=index.js.map
|