@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 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
+ };