@solveo-ai/react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/package.json +59 -0
- package/src/SolveoProvider.tsx +200 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useAgents.ts +57 -0
- package/src/hooks/useAnalytics.ts +55 -0
- package/src/hooks/useAttachments.ts +43 -0
- package/src/hooks/useChat.ts +149 -0
- package/src/hooks/useConversations.ts +42 -0
- package/src/hooks/useEscalation.ts +70 -0
- package/src/hooks/usePushNotifications.ts +414 -0
- package/src/hooks/useRealtime.ts +41 -0
- package/src/hooks/useWidgetConfig.ts +52 -0
- package/src/index.ts +20 -0
- package/src/utils/storage.ts +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @solveo-ai/react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK for the Solveo AI platform. Build AI-powered chat experiences in your mobile apps.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @solveo-ai/react-native @solveo-ai/sdk-core
|
|
9
|
+
# or
|
|
10
|
+
yarn add @solveo-ai/react-native @solveo-ai/sdk-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { SolveoProvider, useChat } from '@solveo-ai/react-native';
|
|
17
|
+
|
|
18
|
+
// Wrap your app
|
|
19
|
+
function App() {
|
|
20
|
+
return (
|
|
21
|
+
<SolveoProvider
|
|
22
|
+
config={{
|
|
23
|
+
apiUrl: 'https://api.solveoai.io',
|
|
24
|
+
widgetId: 'your-widget-id',
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<ChatScreen />
|
|
28
|
+
</SolveoProvider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use in any component
|
|
33
|
+
function ChatScreen() {
|
|
34
|
+
const { conversation, messages, sendMessage, createConversation } = useChat();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
createConversation();
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View>
|
|
42
|
+
{messages.map((msg) => (
|
|
43
|
+
<Text key={msg.id}>{msg.content}</Text>
|
|
44
|
+
))}
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
See [full documentation](https://docs.solveoai.io/sdk/react-native).
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@solveo-ai/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native SDK for Solveo AI platform",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"type-check": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"solveo",
|
|
26
|
+
"ai",
|
|
27
|
+
"chat",
|
|
28
|
+
"react-native",
|
|
29
|
+
"mobile",
|
|
30
|
+
"sdk"
|
|
31
|
+
],
|
|
32
|
+
"author": "Solveo AI",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=18.0.0",
|
|
39
|
+
"react-native": ">=0.72.0",
|
|
40
|
+
"@solveo-ai/sdk-core": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@react-native-async-storage/async-storage": "^2.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "^19.0.0",
|
|
47
|
+
"@types/react-native": "^0.73.0",
|
|
48
|
+
"react": "^19.2.3",
|
|
49
|
+
"react-native": "^0.76.0",
|
|
50
|
+
"tsup": "^8.0.0",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"expo-image-picker": "^16.0.0",
|
|
55
|
+
"expo-av": "^15.0.0",
|
|
56
|
+
"expo-file-system": "^18.0.0",
|
|
57
|
+
"react-native-image-picker": "^7.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SolveoSDK,
|
|
4
|
+
type SolveoClientConfig,
|
|
5
|
+
createCustomerSocket,
|
|
6
|
+
createAgentSocket,
|
|
7
|
+
createStreamingSocket,
|
|
8
|
+
CustomerSocketManager,
|
|
9
|
+
AgentSocketManager,
|
|
10
|
+
StreamingSocket,
|
|
11
|
+
} from '@solveo-ai/sdk-core';
|
|
12
|
+
import type { Socket } from 'socket.io-client';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Context Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface SolveoContextValue {
|
|
19
|
+
sdk: SolveoSDK;
|
|
20
|
+
config: SolveoProviderConfig;
|
|
21
|
+
// Customer socket (for widget mode)
|
|
22
|
+
customerSocket: Socket | null;
|
|
23
|
+
customerSocketManager: CustomerSocketManager | null;
|
|
24
|
+
// Agent socket (for management mode)
|
|
25
|
+
agentSocket: Socket | null;
|
|
26
|
+
agentSocketManager: AgentSocketManager | null;
|
|
27
|
+
// Streaming socket (for AI responses)
|
|
28
|
+
streamingSocket: StreamingSocket | null;
|
|
29
|
+
// Connection helpers
|
|
30
|
+
connectCustomerSocket: (conversationId: string) => void;
|
|
31
|
+
disconnectCustomerSocket: () => void;
|
|
32
|
+
connectAgentSocket: (token: string) => void;
|
|
33
|
+
disconnectAgentSocket: () => void;
|
|
34
|
+
connectStreamingSocket: (conversationId: string) => void;
|
|
35
|
+
disconnectStreamingSocket: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SolveoContext = createContext<SolveoContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Provider Props
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export interface SolveoProviderConfig extends Omit<SolveoClientConfig, 'platform'> {
|
|
45
|
+
// User identity (optional)
|
|
46
|
+
user?: {
|
|
47
|
+
email?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
};
|
|
51
|
+
// Auto-connect options
|
|
52
|
+
autoConnectCustomer?: boolean;
|
|
53
|
+
autoConnectAgent?: boolean;
|
|
54
|
+
conversationId?: string; // For auto-connecting customer socket
|
|
55
|
+
// Error handler
|
|
56
|
+
onError?: (error: Error) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SolveoProviderProps {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
config: SolveoProviderConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Provider Component
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
export function SolveoProvider({ children, config }: SolveoProviderProps) {
|
|
69
|
+
const sdk = useMemo(
|
|
70
|
+
() =>
|
|
71
|
+
new SolveoSDK({
|
|
72
|
+
...config,
|
|
73
|
+
platform: 'react-native',
|
|
74
|
+
}),
|
|
75
|
+
[config.apiUrl, config.apiKey, config.token, config.widgetId]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Socket states
|
|
79
|
+
const [customerSocket, setCustomerSocket] = useState<Socket | null>(null);
|
|
80
|
+
const [customerSocketManager, setCustomerSocketManager] = useState<CustomerSocketManager | null>(null);
|
|
81
|
+
const [agentSocket, setAgentSocket] = useState<Socket | null>(null);
|
|
82
|
+
const [agentSocketManager, setAgentSocketManager] = useState<AgentSocketManager | null>(null);
|
|
83
|
+
const [streamingSocket, setStreamingSocket] = useState<StreamingSocket | null>(null);
|
|
84
|
+
|
|
85
|
+
// Connect customer socket
|
|
86
|
+
const connectCustomerSocket = (conversationId: string) => {
|
|
87
|
+
if (customerSocket) {
|
|
88
|
+
customerSocket.disconnect();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const socket = createCustomerSocket(config.apiUrl, conversationId);
|
|
92
|
+
const manager = new CustomerSocketManager(socket);
|
|
93
|
+
|
|
94
|
+
setCustomerSocket(socket);
|
|
95
|
+
setCustomerSocketManager(manager);
|
|
96
|
+
|
|
97
|
+
socket.connect();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Disconnect customer socket
|
|
101
|
+
const disconnectCustomerSocket = () => {
|
|
102
|
+
if (customerSocket) {
|
|
103
|
+
customerSocket.disconnect();
|
|
104
|
+
setCustomerSocket(null);
|
|
105
|
+
setCustomerSocketManager(null);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Connect agent socket
|
|
110
|
+
const connectAgentSocket = (token: string) => {
|
|
111
|
+
if (agentSocket) {
|
|
112
|
+
agentSocket.disconnect();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const socket = createAgentSocket(config.apiUrl, token);
|
|
116
|
+
const manager = new AgentSocketManager(socket);
|
|
117
|
+
|
|
118
|
+
setAgentSocket(socket);
|
|
119
|
+
setAgentSocketManager(manager);
|
|
120
|
+
|
|
121
|
+
socket.connect();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Disconnect agent socket
|
|
125
|
+
const disconnectAgentSocket = () => {
|
|
126
|
+
if (agentSocket) {
|
|
127
|
+
agentSocket.disconnect();
|
|
128
|
+
setAgentSocket(null);
|
|
129
|
+
setAgentSocketManager(null);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Connect streaming socket
|
|
134
|
+
const connectStreamingSocket = (conversationId: string) => {
|
|
135
|
+
if (streamingSocket) {
|
|
136
|
+
streamingSocket.disconnect();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const socket = createStreamingSocket(config.apiUrl, conversationId);
|
|
140
|
+
setStreamingSocket(socket);
|
|
141
|
+
|
|
142
|
+
socket.connect();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Disconnect streaming socket
|
|
146
|
+
const disconnectStreamingSocket = () => {
|
|
147
|
+
if (streamingSocket) {
|
|
148
|
+
streamingSocket.disconnect();
|
|
149
|
+
setStreamingSocket(null);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Auto-connect on mount
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (config.autoConnectCustomer && config.conversationId) {
|
|
156
|
+
connectCustomerSocket(config.conversationId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (config.autoConnectAgent && config.token) {
|
|
160
|
+
connectAgentSocket(config.token);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Cleanup on unmount
|
|
164
|
+
return () => {
|
|
165
|
+
disconnectCustomerSocket();
|
|
166
|
+
disconnectAgentSocket();
|
|
167
|
+
disconnectStreamingSocket();
|
|
168
|
+
};
|
|
169
|
+
}, [config.autoConnectCustomer, config.autoConnectAgent, config.conversationId, config.token]);
|
|
170
|
+
|
|
171
|
+
const value: SolveoContextValue = {
|
|
172
|
+
sdk,
|
|
173
|
+
config,
|
|
174
|
+
customerSocket,
|
|
175
|
+
customerSocketManager,
|
|
176
|
+
agentSocket,
|
|
177
|
+
agentSocketManager,
|
|
178
|
+
streamingSocket,
|
|
179
|
+
connectCustomerSocket,
|
|
180
|
+
disconnectCustomerSocket,
|
|
181
|
+
connectAgentSocket,
|
|
182
|
+
disconnectAgentSocket,
|
|
183
|
+
connectStreamingSocket,
|
|
184
|
+
disconnectStreamingSocket,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return <SolveoContext.Provider value={value}>{children}</SolveoContext.Provider>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Hook to use Solveo context
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
export function useSolveo(): SolveoContextValue {
|
|
195
|
+
const context = useContext(SolveoContext);
|
|
196
|
+
if (!context) {
|
|
197
|
+
throw new Error('useSolveo must be used within a SolveoProvider');
|
|
198
|
+
}
|
|
199
|
+
return context;
|
|
200
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { useChat } from './useChat';
|
|
2
|
+
export { useRealtime } from './useRealtime';
|
|
3
|
+
export { useEscalation } from './useEscalation';
|
|
4
|
+
export { useWidgetConfig } from './useWidgetConfig';
|
|
5
|
+
|
|
6
|
+
// Simplified exports for remaining hooks (implementations can be expanded as needed)
|
|
7
|
+
export { usePushNotifications } from './usePushNotifications';
|
|
8
|
+
export { useAttachments } from './useAttachments';
|
|
9
|
+
export { useAgents } from './useAgents';
|
|
10
|
+
export { useConversations } from './useConversations';
|
|
11
|
+
export { useAnalytics } from './useAnalytics';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import type { Agent, AgentCreateRequest, AgentUpdateRequest } from '@solveo-ai/sdk-core';
|
|
4
|
+
|
|
5
|
+
export function useAgents(accountId?: string) {
|
|
6
|
+
const { sdk } = useSolveo();
|
|
7
|
+
const [agents, setAgents] = useState<Agent[]>([]);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState<Error | null>(null);
|
|
10
|
+
|
|
11
|
+
const loadAgents = useCallback(async () => {
|
|
12
|
+
if (!accountId) return;
|
|
13
|
+
setIsLoading(true);
|
|
14
|
+
try {
|
|
15
|
+
const data = await sdk.agents.list(accountId);
|
|
16
|
+
setAgents(data);
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
setError(err);
|
|
19
|
+
} finally {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, [sdk, accountId]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (accountId) {
|
|
26
|
+
loadAgents();
|
|
27
|
+
}
|
|
28
|
+
}, [accountId]);
|
|
29
|
+
|
|
30
|
+
const createAgent = useCallback(async (data: AgentCreateRequest) => {
|
|
31
|
+
if (!accountId) throw new Error('No account ID');
|
|
32
|
+
const agent = await sdk.agents.create(accountId, data);
|
|
33
|
+
setAgents((prev) => [...prev, agent]);
|
|
34
|
+
return agent;
|
|
35
|
+
}, [sdk, accountId]);
|
|
36
|
+
|
|
37
|
+
const updateAgent = useCallback(async (id: string, data: AgentUpdateRequest) => {
|
|
38
|
+
const agent = await sdk.agents.update(id, data);
|
|
39
|
+
setAgents((prev) => prev.map((a) => (a.id === id ? agent : a)));
|
|
40
|
+
return agent;
|
|
41
|
+
}, [sdk]);
|
|
42
|
+
|
|
43
|
+
const deleteAgent = useCallback(async (id: string) => {
|
|
44
|
+
await sdk.agents.delete(id);
|
|
45
|
+
setAgents((prev) => prev.filter((a) => a.id !== id));
|
|
46
|
+
}, [sdk]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
agents,
|
|
50
|
+
isLoading,
|
|
51
|
+
error,
|
|
52
|
+
createAgent,
|
|
53
|
+
updateAgent,
|
|
54
|
+
deleteAgent,
|
|
55
|
+
reload: loadAgents,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import type {
|
|
4
|
+
ConversationMetrics,
|
|
5
|
+
SentimentDistribution,
|
|
6
|
+
FeedbackStats,
|
|
7
|
+
TokenUsageMetrics,
|
|
8
|
+
} from '@solveo-ai/sdk-core';
|
|
9
|
+
|
|
10
|
+
export function useAnalytics(accountId?: string, days = 30) {
|
|
11
|
+
const { sdk } = useSolveo();
|
|
12
|
+
const [metrics, setMetrics] = useState<ConversationMetrics | null>(null);
|
|
13
|
+
const [sentiment, setSentiment] = useState<SentimentDistribution | null>(null);
|
|
14
|
+
const [feedback, setFeedback] = useState<FeedbackStats | null>(null);
|
|
15
|
+
const [tokenUsage, setTokenUsage] = useState<TokenUsageMetrics | null>(null);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState<Error | null>(null);
|
|
18
|
+
|
|
19
|
+
const loadAnalytics = useCallback(async () => {
|
|
20
|
+
if (!accountId) return;
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
try {
|
|
23
|
+
const [m, s, f, t] = await Promise.all([
|
|
24
|
+
sdk.analytics.getMetrics(accountId, days),
|
|
25
|
+
sdk.analytics.getSentiment(accountId, days),
|
|
26
|
+
sdk.analytics.getFeedback(accountId, days),
|
|
27
|
+
sdk.analytics.getTokenUsage(accountId, days),
|
|
28
|
+
]);
|
|
29
|
+
setMetrics(m);
|
|
30
|
+
setSentiment(s);
|
|
31
|
+
setFeedback(f);
|
|
32
|
+
setTokenUsage(t);
|
|
33
|
+
} catch (err: any) {
|
|
34
|
+
setError(err);
|
|
35
|
+
} finally {
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, [sdk, accountId, days]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (accountId) {
|
|
42
|
+
loadAnalytics();
|
|
43
|
+
}
|
|
44
|
+
}, [accountId, days]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
metrics,
|
|
48
|
+
sentiment,
|
|
49
|
+
feedback,
|
|
50
|
+
tokenUsage,
|
|
51
|
+
isLoading,
|
|
52
|
+
error,
|
|
53
|
+
reload: loadAnalytics,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import type { MessageAttachment } from '@solveo-ai/sdk-core';
|
|
4
|
+
|
|
5
|
+
export function useAttachments(conversationId?: string) {
|
|
6
|
+
const { sdk } = useSolveo();
|
|
7
|
+
const [uploading, setUploading] = useState(false);
|
|
8
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
9
|
+
|
|
10
|
+
const uploadAttachment = useCallback(async (
|
|
11
|
+
file: { uri: string; type?: string; name?: string },
|
|
12
|
+
attachmentType: 'IMAGE' | 'AUDIO' | 'VIDEO' | 'DOCUMENT' | 'VOICE_NOTE'
|
|
13
|
+
): Promise<MessageAttachment> => {
|
|
14
|
+
if (!conversationId) {
|
|
15
|
+
throw new Error('No conversation ID provided');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setUploading(true);
|
|
19
|
+
setUploadProgress(0);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Convert file URI to blob (platform-specific)
|
|
23
|
+
const response = await fetch(file.uri);
|
|
24
|
+
const blob = await response.blob();
|
|
25
|
+
|
|
26
|
+
const attachment = await sdk.widgetAttachments.upload(conversationId, blob, attachmentType);
|
|
27
|
+
setUploadProgress(100);
|
|
28
|
+
return attachment;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to upload attachment:', error);
|
|
31
|
+
throw error;
|
|
32
|
+
} finally {
|
|
33
|
+
setUploading(false);
|
|
34
|
+
setUploadProgress(0);
|
|
35
|
+
}
|
|
36
|
+
}, [sdk, conversationId]);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
uploading,
|
|
40
|
+
uploadProgress,
|
|
41
|
+
uploadAttachment,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import { storage } from '../utils/storage';
|
|
4
|
+
import type { Message, Conversation } from '@solveo-ai/sdk-core';
|
|
5
|
+
|
|
6
|
+
export interface UseChatOptions {
|
|
7
|
+
widgetId?: string;
|
|
8
|
+
agentId?: string;
|
|
9
|
+
persistConversation?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useChat(options: UseChatOptions = {}) {
|
|
13
|
+
const { sdk, config, streamingSocket, connectStreamingSocket, disconnectStreamingSocket } = useSolveo();
|
|
14
|
+
const [conversation, setConversation] = useState<Conversation | null>(null);
|
|
15
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
18
|
+
const [streamingContent, setStreamingContent] = useState('');
|
|
19
|
+
const [error, setError] = useState<Error | null>(null);
|
|
20
|
+
|
|
21
|
+
// Load persisted conversation
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (options.persistConversation) {
|
|
24
|
+
loadPersistedConversation();
|
|
25
|
+
}
|
|
26
|
+
}, [options.persistConversation]);
|
|
27
|
+
|
|
28
|
+
// Setup streaming socket listeners
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!streamingSocket || !conversation) return;
|
|
31
|
+
|
|
32
|
+
streamingSocket.on('message', (msg) => {
|
|
33
|
+
if (msg.type === 'agent_typing') {
|
|
34
|
+
setIsStreaming(true);
|
|
35
|
+
setStreamingContent('');
|
|
36
|
+
} else if (msg.type === 'agent_message_chunk') {
|
|
37
|
+
setStreamingContent((prev) => prev + (msg.content || ''));
|
|
38
|
+
} else if (msg.type === 'agent_message_complete') {
|
|
39
|
+
setIsStreaming(false);
|
|
40
|
+
if (msg.message_id) {
|
|
41
|
+
loadMessages(conversation.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
streamingSocket.on('error', (err) => {
|
|
47
|
+
setError(new Error(err.message || 'Streaming error'));
|
|
48
|
+
setIsStreaming(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
streamingSocket.off('message');
|
|
53
|
+
streamingSocket.off('error');
|
|
54
|
+
};
|
|
55
|
+
}, [streamingSocket, conversation]);
|
|
56
|
+
|
|
57
|
+
const loadPersistedConversation = async () => {
|
|
58
|
+
try {
|
|
59
|
+
const convId = await storage.getConversationId();
|
|
60
|
+
if (convId) {
|
|
61
|
+
const conv = await sdk.widgetConversations.get(convId);
|
|
62
|
+
setConversation(conv);
|
|
63
|
+
await loadMessages(convId);
|
|
64
|
+
}
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
console.error('Failed to load persisted conversation:', err);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const createConversation = useCallback(async () => {
|
|
71
|
+
setIsLoading(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const identity = await storage.getUserIdentity();
|
|
75
|
+
const conv = await sdk.widgetConversations.create({
|
|
76
|
+
widget_id: options.widgetId || config.widgetId,
|
|
77
|
+
agent_id: options.agentId,
|
|
78
|
+
customer_email: identity?.email || config.user?.email,
|
|
79
|
+
customer_name: identity?.name || config.user?.name,
|
|
80
|
+
});
|
|
81
|
+
setConversation(conv);
|
|
82
|
+
if (options.persistConversation) {
|
|
83
|
+
await storage.setConversationId(conv.id);
|
|
84
|
+
}
|
|
85
|
+
connectStreamingSocket(conv.id);
|
|
86
|
+
return conv;
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
setError(err);
|
|
89
|
+
throw err;
|
|
90
|
+
} finally {
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}, [sdk, options, config]);
|
|
94
|
+
|
|
95
|
+
const loadMessages = useCallback(async (conversationId: string) => {
|
|
96
|
+
try {
|
|
97
|
+
const msgs = await sdk.widgetMessages.list(conversationId);
|
|
98
|
+
setMessages(msgs);
|
|
99
|
+
if (options.persistConversation) {
|
|
100
|
+
await storage.setMessages(conversationId, msgs);
|
|
101
|
+
}
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
setError(err);
|
|
104
|
+
}
|
|
105
|
+
}, [sdk, options]);
|
|
106
|
+
|
|
107
|
+
const sendMessage = useCallback(async (content: string) => {
|
|
108
|
+
if (!conversation) {
|
|
109
|
+
throw new Error('No active conversation');
|
|
110
|
+
}
|
|
111
|
+
setIsLoading(true);
|
|
112
|
+
setError(null);
|
|
113
|
+
try {
|
|
114
|
+
if (streamingSocket?.isConnected()) {
|
|
115
|
+
streamingSocket.sendMessage(content);
|
|
116
|
+
} else {
|
|
117
|
+
const response = await sdk.widgetMessages.send(conversation.id, { content });
|
|
118
|
+
setMessages((prev) => [...prev, response.message, response.ai_response!].filter(Boolean));
|
|
119
|
+
}
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
setError(err);
|
|
122
|
+
throw err;
|
|
123
|
+
} finally {
|
|
124
|
+
setIsLoading(false);
|
|
125
|
+
}
|
|
126
|
+
}, [conversation, streamingSocket, sdk]);
|
|
127
|
+
|
|
128
|
+
const submitFeedback = useCallback(async (messageId: string, rating: 'THUMBS_UP' | 'THUMBS_DOWN') => {
|
|
129
|
+
if (!conversation) return;
|
|
130
|
+
try {
|
|
131
|
+
await sdk.widgetMessages.submitFeedback(conversation.id, messageId, rating);
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
setError(err);
|
|
134
|
+
}
|
|
135
|
+
}, [conversation, sdk]);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
conversation,
|
|
139
|
+
messages,
|
|
140
|
+
isLoading,
|
|
141
|
+
isStreaming,
|
|
142
|
+
streamingContent,
|
|
143
|
+
error,
|
|
144
|
+
createConversation,
|
|
145
|
+
sendMessage,
|
|
146
|
+
loadMessages,
|
|
147
|
+
submitFeedback,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import type { Conversation } from '@solveo-ai/sdk-core';
|
|
4
|
+
|
|
5
|
+
export function useConversations(accountId?: string) {
|
|
6
|
+
const { sdk } = useSolveo();
|
|
7
|
+
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState<Error | null>(null);
|
|
10
|
+
|
|
11
|
+
const loadConversations = useCallback(async () => {
|
|
12
|
+
if (!accountId) return;
|
|
13
|
+
setIsLoading(true);
|
|
14
|
+
try {
|
|
15
|
+
const data = await sdk.conversations.list(accountId);
|
|
16
|
+
setConversations(data);
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
setError(err);
|
|
19
|
+
} finally {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, [sdk, accountId]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (accountId) {
|
|
26
|
+
loadConversations();
|
|
27
|
+
}
|
|
28
|
+
}, [accountId]);
|
|
29
|
+
|
|
30
|
+
const resolveConversation = useCallback(async (id: string) => {
|
|
31
|
+
await sdk.conversations.resolve(id);
|
|
32
|
+
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, status: 'RESOLVED' as const } : c)));
|
|
33
|
+
}, [sdk]);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
conversations,
|
|
37
|
+
isLoading,
|
|
38
|
+
error,
|
|
39
|
+
resolveConversation,
|
|
40
|
+
reload: loadConversations,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import type { HumanAgent } from '@solveo-ai/sdk-core';
|
|
4
|
+
|
|
5
|
+
export function useEscalation() {
|
|
6
|
+
const { customerSocketManager, connectCustomerSocket, disconnectCustomerSocket } = useSolveo();
|
|
7
|
+
const [escalationId, setEscalationId] = useState<string | null>(null);
|
|
8
|
+
const [assignedAgent, setAssignedAgent] = useState<HumanAgent | null>(null);
|
|
9
|
+
const [status, setStatus] = useState<'idle' | 'requesting' | 'queued' | 'active' | 'resolved'>('idle');
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!customerSocketManager) return;
|
|
13
|
+
|
|
14
|
+
customerSocketManager.onAgentConnected((data) => {
|
|
15
|
+
setEscalationId(data.escalation_id);
|
|
16
|
+
setAssignedAgent({
|
|
17
|
+
name: data.agent_name,
|
|
18
|
+
avatar_url: data.agent_avatar,
|
|
19
|
+
} as HumanAgent);
|
|
20
|
+
setStatus('active');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
customerSocketManager.onQueueUpdate((data) => {
|
|
24
|
+
setStatus('queued');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
customerSocketManager.onEscalationResolved(() => {
|
|
28
|
+
setStatus('resolved');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
customerSocketManager.onHandedBack(() => {
|
|
32
|
+
setStatus('idle');
|
|
33
|
+
setEscalationId(null);
|
|
34
|
+
setAssignedAgent(null);
|
|
35
|
+
});
|
|
36
|
+
}, [customerSocketManager]);
|
|
37
|
+
|
|
38
|
+
const requestHuman = useCallback((conversationId: string) => {
|
|
39
|
+
connectCustomerSocket(conversationId);
|
|
40
|
+
if (customerSocketManager) {
|
|
41
|
+
customerSocketManager.requestHuman();
|
|
42
|
+
setStatus('requesting');
|
|
43
|
+
}
|
|
44
|
+
}, [customerSocketManager, connectCustomerSocket]);
|
|
45
|
+
|
|
46
|
+
const endChat = useCallback(() => {
|
|
47
|
+
if (escalationId && customerSocketManager) {
|
|
48
|
+
customerSocketManager.endChat(escalationId);
|
|
49
|
+
disconnectCustomerSocket();
|
|
50
|
+
setStatus('idle');
|
|
51
|
+
setEscalationId(null);
|
|
52
|
+
setAssignedAgent(null);
|
|
53
|
+
}
|
|
54
|
+
}, [escalationId, customerSocketManager, disconnectCustomerSocket]);
|
|
55
|
+
|
|
56
|
+
const submitCSAT = useCallback((rating: number) => {
|
|
57
|
+
if (escalationId && customerSocketManager) {
|
|
58
|
+
customerSocketManager.submitCSAT(escalationId, rating);
|
|
59
|
+
}
|
|
60
|
+
}, [escalationId, customerSocketManager]);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
escalationId,
|
|
64
|
+
assignedAgent,
|
|
65
|
+
status,
|
|
66
|
+
requestHuman,
|
|
67
|
+
endChat,
|
|
68
|
+
submitCSAT,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import type { Device } from '@solveoai/sdk-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Push notification event handlers
|
|
8
|
+
*/
|
|
9
|
+
export interface PushNotificationHandlers {
|
|
10
|
+
onNotificationReceived?: (notification: any) => void;
|
|
11
|
+
onNotificationTapped?: (notification: any) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Push notification state
|
|
16
|
+
*/
|
|
17
|
+
export interface PushNotificationState {
|
|
18
|
+
device: Device | null;
|
|
19
|
+
isRegistered: boolean;
|
|
20
|
+
isInitialized: boolean;
|
|
21
|
+
permissionStatus: 'granted' | 'denied' | 'undetermined';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook options
|
|
26
|
+
*/
|
|
27
|
+
export interface UsePushNotificationsOptions {
|
|
28
|
+
mode?: 'widget' | 'agent'; // 'widget' for customer-facing, 'agent' for dashboard
|
|
29
|
+
conversationId?: string; // Required for widget mode
|
|
30
|
+
autoRegister?: boolean; // Auto-register if permission granted
|
|
31
|
+
handlers?: PushNotificationHandlers;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Enhanced Push Notifications Hook for React Native
|
|
36
|
+
*
|
|
37
|
+
* Supports both customer (widget) and agent (dashboard) modes.
|
|
38
|
+
* Handles permissions, registration, and notification events.
|
|
39
|
+
*
|
|
40
|
+
* SETUP INSTRUCTIONS:
|
|
41
|
+
* 1. Install peer dependencies:
|
|
42
|
+
* npm install @react-native-firebase/app @react-native-firebase/messaging
|
|
43
|
+
* (or expo-notifications for Expo projects)
|
|
44
|
+
*
|
|
45
|
+
* 2. Configure Firebase/APNs:
|
|
46
|
+
* - Android: Add google-services.json to android/app/
|
|
47
|
+
* - iOS: Add GoogleService-Info.plist to ios/ and configure APNs
|
|
48
|
+
*
|
|
49
|
+
* 3. Add permissions to manifests:
|
|
50
|
+
* - Android: INTERNET, VIBRATE, RECEIVE_BOOT_COMPLETED
|
|
51
|
+
* - iOS: User Notifications capability
|
|
52
|
+
*/
|
|
53
|
+
export function usePushNotifications(options: UsePushNotificationsOptions = {}) {
|
|
54
|
+
const { sdk } = useSolveo();
|
|
55
|
+
const {
|
|
56
|
+
mode = 'widget',
|
|
57
|
+
conversationId,
|
|
58
|
+
autoRegister = false,
|
|
59
|
+
handlers,
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
const [state, setState] = useState<PushNotificationState>({
|
|
63
|
+
device: null,
|
|
64
|
+
isRegistered: false,
|
|
65
|
+
isInitialized: false,
|
|
66
|
+
permissionStatus: 'undetermined',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Request notification permission from the OS
|
|
71
|
+
*/
|
|
72
|
+
const requestPermission = useCallback(async (): Promise<boolean> => {
|
|
73
|
+
try {
|
|
74
|
+
// Try to import Firebase Messaging
|
|
75
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
76
|
+
|
|
77
|
+
const authStatus = await messaging().requestPermission();
|
|
78
|
+
const enabled =
|
|
79
|
+
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
|
|
80
|
+
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
|
|
81
|
+
|
|
82
|
+
setState(prev => ({
|
|
83
|
+
...prev,
|
|
84
|
+
permissionStatus: enabled ? 'granted' : 'denied',
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return enabled;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to request permission (Firebase Messaging not installed?):', error);
|
|
90
|
+
|
|
91
|
+
// Fallback: Try expo-notifications if Firebase not available
|
|
92
|
+
try {
|
|
93
|
+
const Notifications = await import('expo-notifications');
|
|
94
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
95
|
+
const granted = status === 'granted';
|
|
96
|
+
|
|
97
|
+
setState(prev => ({
|
|
98
|
+
...prev,
|
|
99
|
+
permissionStatus: granted ? 'granted' : 'denied',
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
return granted;
|
|
103
|
+
} catch (expoError) {
|
|
104
|
+
console.error('Failed to request permission (Expo Notifications not installed?):', expoError);
|
|
105
|
+
setState(prev => ({ ...prev, permissionStatus: 'denied' }));
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get FCM token from device
|
|
113
|
+
*/
|
|
114
|
+
const getToken = useCallback(async (): Promise<string | null> => {
|
|
115
|
+
try {
|
|
116
|
+
// Try Firebase Messaging first
|
|
117
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
118
|
+
const token = await messaging().getToken();
|
|
119
|
+
return token;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to get FCM token:', error);
|
|
122
|
+
|
|
123
|
+
// Fallback to Expo push token
|
|
124
|
+
try {
|
|
125
|
+
const Notifications = await import('expo-notifications');
|
|
126
|
+
const token = await Notifications.getExpoPushTokenAsync();
|
|
127
|
+
return token.data;
|
|
128
|
+
} catch (expoError) {
|
|
129
|
+
console.error('Failed to get Expo push token:', expoError);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register device for push notifications (customer/widget mode)
|
|
137
|
+
*/
|
|
138
|
+
const registerDevice = useCallback(
|
|
139
|
+
async (convId?: string, pushToken?: string, bundleId?: string): Promise<Device | null> => {
|
|
140
|
+
if (mode !== 'widget') {
|
|
141
|
+
console.warn('registerDevice is only for widget mode. Use registerAgentDevice for agent mode.');
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const targetConversationId = convId || conversationId;
|
|
146
|
+
if (!targetConversationId) {
|
|
147
|
+
console.error('conversationId is required for widget mode registration');
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const token = pushToken || (await getToken());
|
|
153
|
+
if (!token) {
|
|
154
|
+
throw new Error('Failed to get push token');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const platform = Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
158
|
+
const device = await sdk.widgetDevices.register({
|
|
159
|
+
conversation_id: targetConversationId,
|
|
160
|
+
platform,
|
|
161
|
+
push_token: token,
|
|
162
|
+
bundle_id: bundleId,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
setState(prev => ({ ...prev, device, isRegistered: true }));
|
|
166
|
+
return device;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Failed to register device:', error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
[sdk, conversationId, mode, getToken]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Register device for push notifications (agent/dashboard mode)
|
|
177
|
+
*/
|
|
178
|
+
const registerAgentDevice = useCallback(
|
|
179
|
+
async (pushToken?: string, deviceName?: string): Promise<any> => {
|
|
180
|
+
if (mode !== 'agent') {
|
|
181
|
+
console.warn('registerAgentDevice is only for agent mode. Use registerDevice for widget mode.');
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const token = pushToken || (await getToken());
|
|
187
|
+
if (!token) {
|
|
188
|
+
throw new Error('Failed to get push token');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const platform = Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
192
|
+
|
|
193
|
+
// Call authenticated endpoint for agent device registration
|
|
194
|
+
const response = await sdk.client.post('/user-devices', {
|
|
195
|
+
platform,
|
|
196
|
+
push_token: token,
|
|
197
|
+
device_name: deviceName || `${Platform.OS} Device`,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
setState(prev => ({
|
|
201
|
+
...prev,
|
|
202
|
+
device: response.data,
|
|
203
|
+
isRegistered: true,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
return response.data;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('Failed to register agent device:', error);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[sdk, mode, getToken]
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Unregister device from push notifications
|
|
217
|
+
*/
|
|
218
|
+
const unregisterDevice = useCallback(async () => {
|
|
219
|
+
if (!state.device) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
if (mode === 'widget') {
|
|
225
|
+
await sdk.widgetDevices.unregister(state.device.id);
|
|
226
|
+
} else {
|
|
227
|
+
await sdk.client.delete(`/user-devices/${state.device.id}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setState(prev => ({
|
|
231
|
+
...prev,
|
|
232
|
+
device: null,
|
|
233
|
+
isRegistered: false,
|
|
234
|
+
}));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Failed to unregister device:', error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}, [sdk, state.device, mode]);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Setup foreground notification handler
|
|
243
|
+
*/
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!handlers?.onNotificationReceived) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let unsubscribe: (() => void) | undefined;
|
|
250
|
+
|
|
251
|
+
const setupForegroundHandler = async () => {
|
|
252
|
+
try {
|
|
253
|
+
// Try Firebase Messaging
|
|
254
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
255
|
+
|
|
256
|
+
unsubscribe = messaging().onMessage(async remoteMessage => {
|
|
257
|
+
console.log('Foreground notification received:', remoteMessage);
|
|
258
|
+
handlers.onNotificationReceived?.(remoteMessage);
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Fallback to Expo
|
|
262
|
+
try {
|
|
263
|
+
const Notifications = await import('expo-notifications');
|
|
264
|
+
|
|
265
|
+
const subscription = Notifications.addNotificationReceivedListener(notification => {
|
|
266
|
+
console.log('Foreground notification received (Expo):', notification);
|
|
267
|
+
handlers.onNotificationReceived?.(notification);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
unsubscribe = () => subscription.remove();
|
|
271
|
+
} catch (expoError) {
|
|
272
|
+
console.error('Failed to setup foreground handler:', expoError);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
setupForegroundHandler();
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
unsubscribe?.();
|
|
281
|
+
};
|
|
282
|
+
}, [handlers?.onNotificationReceived]);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Setup notification tap handler
|
|
286
|
+
*/
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!handlers?.onNotificationTapped) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let unsubscribe: (() => void) | undefined;
|
|
293
|
+
|
|
294
|
+
const setupTapHandler = async () => {
|
|
295
|
+
try {
|
|
296
|
+
// Try Firebase Messaging
|
|
297
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
298
|
+
|
|
299
|
+
// Handle notification opened app from quit state
|
|
300
|
+
messaging()
|
|
301
|
+
.getInitialNotification()
|
|
302
|
+
.then(remoteMessage => {
|
|
303
|
+
if (remoteMessage) {
|
|
304
|
+
console.log('Notification opened app from quit state:', remoteMessage);
|
|
305
|
+
handlers.onNotificationTapped?.(remoteMessage);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Handle notification opened app from background state
|
|
310
|
+
unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => {
|
|
311
|
+
console.log('Notification opened app from background:', remoteMessage);
|
|
312
|
+
handlers.onNotificationTapped?.(remoteMessage);
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// Fallback to Expo
|
|
316
|
+
try {
|
|
317
|
+
const Notifications = await import('expo-notifications');
|
|
318
|
+
|
|
319
|
+
const subscription = Notifications.addNotificationResponseReceivedListener(response => {
|
|
320
|
+
console.log('Notification tapped (Expo):', response);
|
|
321
|
+
handlers.onNotificationTapped?.(response.notification);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
unsubscribe = () => subscription.remove();
|
|
325
|
+
} catch (expoError) {
|
|
326
|
+
console.error('Failed to setup tap handler:', expoError);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
setupTapHandler();
|
|
332
|
+
|
|
333
|
+
return () => {
|
|
334
|
+
unsubscribe?.();
|
|
335
|
+
};
|
|
336
|
+
}, [handlers?.onNotificationTapped]);
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Handle token refresh
|
|
340
|
+
*/
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
let unsubscribe: (() => void) | undefined;
|
|
343
|
+
|
|
344
|
+
const setupTokenRefreshHandler = async () => {
|
|
345
|
+
try {
|
|
346
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
347
|
+
|
|
348
|
+
unsubscribe = messaging().onTokenRefresh(async newToken => {
|
|
349
|
+
console.log('FCM token refreshed:', newToken);
|
|
350
|
+
|
|
351
|
+
// Re-register with new token
|
|
352
|
+
if (state.isRegistered) {
|
|
353
|
+
if (mode === 'widget' && conversationId) {
|
|
354
|
+
await registerDevice(conversationId, newToken);
|
|
355
|
+
} else if (mode === 'agent') {
|
|
356
|
+
await registerAgentDevice(newToken);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('Failed to setup token refresh handler:', error);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
setupTokenRefreshHandler();
|
|
366
|
+
|
|
367
|
+
return () => {
|
|
368
|
+
unsubscribe?.();
|
|
369
|
+
};
|
|
370
|
+
}, [state.isRegistered, mode, conversationId, registerDevice, registerAgentDevice]);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check permission status on mount
|
|
374
|
+
*/
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
const checkPermission = async () => {
|
|
377
|
+
try {
|
|
378
|
+
const messaging = await import('@react-native-firebase/messaging').then(m => m.default);
|
|
379
|
+
const authStatus = await messaging().hasPermission();
|
|
380
|
+
const granted =
|
|
381
|
+
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
|
|
382
|
+
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
|
|
383
|
+
|
|
384
|
+
setState(prev => ({
|
|
385
|
+
...prev,
|
|
386
|
+
permissionStatus: granted ? 'granted' : authStatus === messaging.AuthorizationStatus.NOT_DETERMINED ? 'undetermined' : 'denied',
|
|
387
|
+
isInitialized: true,
|
|
388
|
+
}));
|
|
389
|
+
|
|
390
|
+
// Auto-register if permission granted and autoRegister enabled
|
|
391
|
+
if (granted && autoRegister && !state.isRegistered) {
|
|
392
|
+
if (mode === 'widget' && conversationId) {
|
|
393
|
+
await registerDevice();
|
|
394
|
+
} else if (mode === 'agent') {
|
|
395
|
+
await registerAgentDevice();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Failed to check permission:', error);
|
|
400
|
+
setState(prev => ({ ...prev, isInitialized: true }));
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
checkPermission();
|
|
405
|
+
}, [autoRegister, mode, conversationId, registerDevice, registerAgentDevice]);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
...state,
|
|
409
|
+
requestPermission,
|
|
410
|
+
registerDevice,
|
|
411
|
+
registerAgentDevice,
|
|
412
|
+
unregisterDevice,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
|
|
4
|
+
export function useRealtime() {
|
|
5
|
+
const { customerSocketManager } = useSolveo();
|
|
6
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
7
|
+
const [agentTyping, setAgentTyping] = useState(false);
|
|
8
|
+
const [queuePosition, setQueuePosition] = useState<number | null>(null);
|
|
9
|
+
const [estimatedWait, setEstimatedWait] = useState<number | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!customerSocketManager) return;
|
|
13
|
+
|
|
14
|
+
const socket = customerSocketManager.getSocket();
|
|
15
|
+
|
|
16
|
+
socket.on('connect', () => setIsConnected(true));
|
|
17
|
+
socket.on('disconnect', () => setIsConnected(false));
|
|
18
|
+
|
|
19
|
+
customerSocketManager.onAgentTyping(() => {
|
|
20
|
+
setAgentTyping(true);
|
|
21
|
+
setTimeout(() => setAgentTyping(false), 3000);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
customerSocketManager.onQueueUpdate((data) => {
|
|
25
|
+
setQueuePosition(data.queue_position);
|
|
26
|
+
setEstimatedWait(data.estimated_wait);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
socket.off('connect');
|
|
31
|
+
socket.off('disconnect');
|
|
32
|
+
};
|
|
33
|
+
}, [customerSocketManager]);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
isConnected,
|
|
37
|
+
agentTyping,
|
|
38
|
+
queuePosition,
|
|
39
|
+
estimatedWait,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useSolveo } from '../SolveoProvider';
|
|
3
|
+
import { storage } from '../utils/storage';
|
|
4
|
+
import type { Widget, WidgetConfig } from '@solveo-ai/sdk-core';
|
|
5
|
+
|
|
6
|
+
export function useWidgetConfig(widgetId?: string) {
|
|
7
|
+
const { sdk, config } = useSolveo();
|
|
8
|
+
const effectiveWidgetId = widgetId || config.widgetId;
|
|
9
|
+
const [widget, setWidget] = useState<Widget | null>(null);
|
|
10
|
+
const [widgetConfig, setWidgetConfig] = useState<WidgetConfig | null>(null);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState<Error | null>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (effectiveWidgetId) {
|
|
16
|
+
loadConfig();
|
|
17
|
+
}
|
|
18
|
+
}, [effectiveWidgetId]);
|
|
19
|
+
|
|
20
|
+
const loadConfig = async () => {
|
|
21
|
+
if (!effectiveWidgetId) return;
|
|
22
|
+
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
// Try cache first
|
|
27
|
+
const cached = await storage.getWidgetConfig(effectiveWidgetId);
|
|
28
|
+
if (cached) {
|
|
29
|
+
setWidget(cached);
|
|
30
|
+
setWidgetConfig(cached.config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fetch fresh config
|
|
34
|
+
const w = await sdk.widgetConfig.getConfig(effectiveWidgetId);
|
|
35
|
+
setWidget(w);
|
|
36
|
+
setWidgetConfig(w.config);
|
|
37
|
+
await storage.setWidgetConfig(effectiveWidgetId, w);
|
|
38
|
+
} catch (err: any) {
|
|
39
|
+
setError(err);
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
widget,
|
|
47
|
+
widgetConfig,
|
|
48
|
+
isLoading,
|
|
49
|
+
error,
|
|
50
|
+
reload: loadConfig,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Provider
|
|
2
|
+
export { SolveoProvider, useSolveo, type SolveoProviderConfig, type SolveoProviderProps } from './SolveoProvider';
|
|
3
|
+
|
|
4
|
+
// Hooks
|
|
5
|
+
export * from './hooks';
|
|
6
|
+
|
|
7
|
+
// Utils
|
|
8
|
+
export { storage } from './utils/storage';
|
|
9
|
+
|
|
10
|
+
// Re-export core SDK types and utilities
|
|
11
|
+
export type {
|
|
12
|
+
Message,
|
|
13
|
+
Conversation,
|
|
14
|
+
Agent,
|
|
15
|
+
Widget,
|
|
16
|
+
MessageAttachment,
|
|
17
|
+
SentimentType,
|
|
18
|
+
ConversationStatus,
|
|
19
|
+
FeedbackType,
|
|
20
|
+
} from '@solveo-ai/sdk-core';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEYS = {
|
|
4
|
+
CONVERSATION_ID: '@solveo/conversation_id',
|
|
5
|
+
MESSAGES: '@solveo/messages',
|
|
6
|
+
USER_IDENTITY: '@solveo/user_identity',
|
|
7
|
+
WIDGET_CONFIG: '@solveo/widget_config',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const storage = {
|
|
11
|
+
async getConversationId(): Promise<string | null> {
|
|
12
|
+
return AsyncStorage.getItem(STORAGE_KEYS.CONVERSATION_ID);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async setConversationId(id: string): Promise<void> {
|
|
16
|
+
await AsyncStorage.setItem(STORAGE_KEYS.CONVERSATION_ID, id);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async clearConversationId(): Promise<void> {
|
|
20
|
+
await AsyncStorage.removeItem(STORAGE_KEYS.CONVERSATION_ID);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async getMessages(conversationId: string): Promise<any[]> {
|
|
24
|
+
const data = await AsyncStorage.getItem(`${STORAGE_KEYS.MESSAGES}_${conversationId}`);
|
|
25
|
+
return data ? JSON.parse(data) : [];
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async setMessages(conversationId: string, messages: any[]): Promise<void> {
|
|
29
|
+
await AsyncStorage.setItem(`${STORAGE_KEYS.MESSAGES}_${conversationId}`, JSON.stringify(messages));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async getUserIdentity(): Promise<{ email?: string; name?: string } | null> {
|
|
33
|
+
const data = await AsyncStorage.getItem(STORAGE_KEYS.USER_IDENTITY);
|
|
34
|
+
return data ? JSON.parse(data) : null;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async setUserIdentity(identity: { email?: string; name?: string }): Promise<void> {
|
|
38
|
+
await AsyncStorage.setItem(STORAGE_KEYS.USER_IDENTITY, JSON.stringify(identity));
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async getWidgetConfig(widgetId: string): Promise<any | null> {
|
|
42
|
+
const data = await AsyncStorage.getItem(`${STORAGE_KEYS.WIDGET_CONFIG}_${widgetId}`);
|
|
43
|
+
return data ? JSON.parse(data) : null;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async setWidgetConfig(widgetId: string, config: any): Promise<void> {
|
|
47
|
+
await AsyncStorage.setItem(`${STORAGE_KEYS.WIDGET_CONFIG}_${widgetId}`, JSON.stringify(config));
|
|
48
|
+
},
|
|
49
|
+
};
|