react-native-srschat 0.1.43 ā 0.1.45
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 +18 -0
- package/lib/commonjs/components/productCard.js +34 -5
- package/lib/commonjs/components/productCard.js.map +1 -1
- package/lib/commonjs/contexts/AppContext.js +78 -31
- package/lib/commonjs/contexts/AppContext.js.map +1 -1
- package/lib/commonjs/hooks/Stream.js +2 -2
- package/lib/commonjs/hooks/Stream.js.map +1 -1
- package/lib/commonjs/utils/audioRecorder.js +49 -21
- package/lib/commonjs/utils/audioRecorder.js.map +1 -1
- package/lib/commonjs/utils/storage.js +74 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/module/components/productCard.js +34 -5
- package/lib/module/components/productCard.js.map +1 -1
- package/lib/module/contexts/AppContext.js +77 -31
- package/lib/module/contexts/AppContext.js.map +1 -1
- package/lib/module/hooks/Stream.js +2 -2
- package/lib/module/hooks/Stream.js.map +1 -1
- package/lib/module/utils/audioRecorder.js +50 -22
- package/lib/module/utils/audioRecorder.js.map +1 -1
- package/lib/module/utils/storage.js +65 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/typescript/components/productCard.d.ts.map +1 -1
- package/lib/typescript/contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
- package/lib/typescript/utils/storage.d.ts +28 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/components/productCard.js +34 -4
- package/src/contexts/AppContext.js +94 -35
- package/src/hooks/Stream.js +2 -2
- package/src/utils/audioRecorder.js +52 -27
- package/src/utils/storage.ts +86 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React, {createContext, useContext, useState, useEffect, useMemo } from "react";
|
|
2
2
|
import uuid from 'react-native-uuid';
|
|
3
3
|
import axios from "axios";
|
|
4
|
-
import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
4
|
+
// import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
5
|
+
import { loadChat, updateChat, defaultState } from '../utils/storage';
|
|
5
6
|
|
|
6
7
|
export const AppContext = createContext();
|
|
7
8
|
|
|
8
9
|
export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConfig = {}, children }) => {
|
|
9
|
-
|
|
10
10
|
const theme = {
|
|
11
11
|
userMessage: '#003764',
|
|
12
12
|
botMessage: '#003764',
|
|
@@ -16,6 +16,9 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
16
16
|
inlineButtonColor: '#dbd4c8',
|
|
17
17
|
primaryColor: '#161616'
|
|
18
18
|
};
|
|
19
|
+
|
|
20
|
+
const TRACK_CLICK_URL = "https://srs-external-agent-logging-586731320826.us-central1.run.app/track-click"
|
|
21
|
+
const ADD_TO_CART_URL = "https://srs-external-agent-logging-586731320826.us-central1.run.app/add-to-cart"
|
|
19
22
|
|
|
20
23
|
// Backend URLs
|
|
21
24
|
const BASE_URL = data.env === "stage"
|
|
@@ -28,31 +31,24 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
28
31
|
const defaultMessage = [
|
|
29
32
|
{type: "ai", text: "Hi there š Iām Poseidon, your Heritage Pool+ AI Agent. I can help you during your online visit with Product and Account information. How can I help you today?"}
|
|
30
33
|
]
|
|
31
|
-
|
|
32
34
|
const maintenanceMessage = [
|
|
33
|
-
{ type: "ai", text: "Hi there š I
|
|
35
|
+
{ type: "ai", text: "Hi there š I'm Poseidon, your Heritage Pool+ AI Agent. I'm currently undergoing maintenance to improve my services. Thank you for your patience and understanding!",},
|
|
34
36
|
];
|
|
35
37
|
|
|
38
|
+
|
|
39
|
+
// user data variables
|
|
40
|
+
const customerToken = data.customer_token;
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
// Base Variables
|
|
37
44
|
const [showModal, setShowModal] = useState("Icon");
|
|
38
45
|
const [input, setInput] = useState('');
|
|
39
|
-
const [messages, setMessages] = useState(
|
|
46
|
+
const [messages, setMessages] = useState(defaultState.messages);
|
|
40
47
|
const [conversationStartTime, setConversationStartTime] = useState(null);
|
|
41
48
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
42
49
|
const [lastMessageId, setLastMessageId] = useState("");
|
|
43
50
|
const [sessionId, setSessionId] = useState(null);
|
|
44
51
|
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
const newSessionId = uuid.v4(); // Generate UUID v4
|
|
47
|
-
setSessionId(newSessionId);
|
|
48
|
-
}, []);
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (showModal == "Off") {
|
|
52
|
-
setShowModal("Icon")
|
|
53
|
-
}
|
|
54
|
-
}, [uiConfig.showIcon]);
|
|
55
|
-
|
|
56
52
|
// Message UI
|
|
57
53
|
const [typingIndicator, setTypingIndicator] = useState(false);
|
|
58
54
|
const [ghostMessage, setGhostMessage] = useState(false);
|
|
@@ -61,6 +57,69 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
61
57
|
const [disclaimer, setDisclaimer] = useState(false);
|
|
62
58
|
const [startStreaming, setStartStreaming] = useState(false);
|
|
63
59
|
|
|
60
|
+
// Load persisted state on mount
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!customerToken) return;
|
|
63
|
+
|
|
64
|
+
const init = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const cachedState = await loadChat(customerToken);
|
|
67
|
+
|
|
68
|
+
// Update all stateful values
|
|
69
|
+
setTypingIndicator(cachedState.typingIndicator);
|
|
70
|
+
setGhostMessage(cachedState.ghostMessage);
|
|
71
|
+
setGhostCard(cachedState.ghostCard);
|
|
72
|
+
setStopActivated(cachedState.stopActivated);
|
|
73
|
+
setDisclaimer(cachedState.disclaimer);
|
|
74
|
+
setStartStreaming(cachedState.startStreaming);
|
|
75
|
+
setMessages(cachedState.messages);
|
|
76
|
+
setShowModal(cachedState.showModal);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error loading chat state:', error);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
init();
|
|
83
|
+
}, [customerToken]);
|
|
84
|
+
|
|
85
|
+
// Persist state changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!customerToken) return;
|
|
88
|
+
|
|
89
|
+
const persistState = async () => {
|
|
90
|
+
const currentState = {
|
|
91
|
+
typingIndicator,
|
|
92
|
+
ghostMessage,
|
|
93
|
+
ghostCard,
|
|
94
|
+
stopActivated,
|
|
95
|
+
disclaimer,
|
|
96
|
+
startStreaming,
|
|
97
|
+
messages,
|
|
98
|
+
showModal
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await updateChat(customerToken, currentState);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
persistState();
|
|
105
|
+
}, [
|
|
106
|
+
customerToken,
|
|
107
|
+
typingIndicator,
|
|
108
|
+
ghostMessage,
|
|
109
|
+
ghostCard,
|
|
110
|
+
stopActivated,
|
|
111
|
+
disclaimer,
|
|
112
|
+
startStreaming,
|
|
113
|
+
messages,
|
|
114
|
+
showModal
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (showModal == "Off") {
|
|
119
|
+
setShowModal("Icon")
|
|
120
|
+
}
|
|
121
|
+
}, [uiConfig.showIcon]);
|
|
122
|
+
|
|
64
123
|
const stopGenerating = () => {
|
|
65
124
|
setTypingIndicator(false);
|
|
66
125
|
setStopActivated(true);
|
|
@@ -138,26 +197,26 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
138
197
|
setShowModal("Welcome");
|
|
139
198
|
}
|
|
140
199
|
if (messages.length > 1) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
200
|
+
const newState = {
|
|
201
|
+
...defaultState,
|
|
202
|
+
messages: maintenance ? maintenanceMessage : defaultMessage,
|
|
203
|
+
showModal: maintenance ? "ChatWindow" : "Welcome"
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Update all state variables
|
|
207
|
+
setMessages(newState.messages);
|
|
148
208
|
setTypingIndicator(false);
|
|
149
209
|
setGhostMessage(false);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
} */
|
|
210
|
+
setShowModal(newState.showModal);
|
|
211
|
+
|
|
212
|
+
// Generate new session
|
|
213
|
+
const newSessionId = uuid.v4();
|
|
214
|
+
setSessionId(newSessionId);
|
|
215
|
+
|
|
216
|
+
// Clear persisted state
|
|
217
|
+
if (customerToken) {
|
|
218
|
+
await updateChat(customerToken, newState);
|
|
219
|
+
}
|
|
161
220
|
}
|
|
162
221
|
};
|
|
163
222
|
|
|
@@ -267,7 +326,7 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
267
326
|
startStreaming, setStartStreaming, maintenance, setMaintenance, feedback, setFeedback, handleFeedback, feedbackOpen, setFeedbackOpen,
|
|
268
327
|
writeFeedback, setWriteFeedback, writeAnswer, setWriteAnswer, BASE_URL, lastMessageId, setLastMessageId,
|
|
269
328
|
onProductCardClick, onAddToCartClick, data, sessionId, setSessionId, handleWrittenFeedback, switchFeedbackOpen, confirmDisclaimer,
|
|
270
|
-
formatChatHistory, uiConfig, handleVoiceSend
|
|
329
|
+
formatChatHistory, uiConfig, handleVoiceSend, TRACK_CLICK_URL, ADD_TO_CART_URL
|
|
271
330
|
}}
|
|
272
331
|
>
|
|
273
332
|
{children}
|
package/src/hooks/Stream.js
CHANGED
|
@@ -52,8 +52,8 @@ export function useWebSocketMessage() {
|
|
|
52
52
|
user_query: lastUserMessage,
|
|
53
53
|
session_id: String(sessionId),
|
|
54
54
|
conversation_start_time: conversationStartTime,
|
|
55
|
-
/* customer_name: "Cristin Connerney",
|
|
56
|
-
user_UUID:
|
|
55
|
+
// /* customer_name: "Cristin Connerney",
|
|
56
|
+
user_UUID: data.user_id || "mobile_user_unspecified",
|
|
57
57
|
device: "mobile",
|
|
58
58
|
window_location: "mobile"
|
|
59
59
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// audioRecorder.js
|
|
2
2
|
|
|
3
|
-
import { Platform
|
|
4
|
-
import Voice from '@react-native-
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import Voice from '@react-native-voice/voice';
|
|
5
5
|
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
|
|
6
6
|
import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
7
7
|
|
|
@@ -30,6 +30,12 @@ export async function initVoice(onResult) {
|
|
|
30
30
|
resultCallback = onResult;
|
|
31
31
|
finalResult = '';
|
|
32
32
|
|
|
33
|
+
// Check if Voice module is available
|
|
34
|
+
if (!Voice) {
|
|
35
|
+
console.error('Voice module is not available');
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
// First check if speech recognition is available
|
|
34
40
|
const isAvailable = await Voice.isAvailable();
|
|
35
41
|
if (!isAvailable) {
|
|
@@ -72,7 +78,7 @@ export async function initVoice(onResult) {
|
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
Voice.onSpeechError = async (e) => {
|
|
75
|
-
console.error('onSpeechError: ', e);
|
|
81
|
+
// console.error('onSpeechError: ', e);
|
|
76
82
|
|
|
77
83
|
if (silenceTimer) {
|
|
78
84
|
clearTimeout(silenceTimer);
|
|
@@ -86,13 +92,13 @@ export async function initVoice(onResult) {
|
|
|
86
92
|
await cleanupVoiceSession();
|
|
87
93
|
|
|
88
94
|
// Only send error to callback if it's not a "No speech detected" error
|
|
89
|
-
if (!isNoSpeechError) {
|
|
90
|
-
|
|
91
|
-
} else {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
95
|
+
// if (!isNoSpeechError) {
|
|
96
|
+
// resultCallback(null, e.error?.message || 'Speech recognition error');
|
|
97
|
+
// } else {
|
|
98
|
+
// console.log('No speech detected, ignoring error');
|
|
99
|
+
// // Optionally, call the callback with null parameters or a special indicator
|
|
100
|
+
// resultCallback(null, null); // This won't trigger an error alert in the component
|
|
101
|
+
// }
|
|
96
102
|
};
|
|
97
103
|
|
|
98
104
|
Voice.onSpeechResults = (e) => {
|
|
@@ -164,6 +170,12 @@ const cleanupVoiceSession = async () => {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
try {
|
|
173
|
+
// Check if Voice module is available
|
|
174
|
+
if (!Voice) {
|
|
175
|
+
console.log('Voice module not available during cleanup');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
// First try to stop if still recognizing
|
|
168
180
|
const isRecognizing = await Voice.isRecognizing();
|
|
169
181
|
if (isRecognizing) {
|
|
@@ -189,7 +201,9 @@ const cleanupVoiceSession = async () => {
|
|
|
189
201
|
console.error('Error in cleanupVoiceSession:', error);
|
|
190
202
|
// Final attempt to destroy on error
|
|
191
203
|
try {
|
|
192
|
-
|
|
204
|
+
if (Voice) {
|
|
205
|
+
await Voice.destroy();
|
|
206
|
+
}
|
|
193
207
|
} catch (e) {
|
|
194
208
|
console.error('Final destroy attempt failed:', e);
|
|
195
209
|
}
|
|
@@ -200,6 +214,12 @@ const cleanupVoiceSession = async () => {
|
|
|
200
214
|
|
|
201
215
|
export async function startRecording() {
|
|
202
216
|
try {
|
|
217
|
+
// Check if Voice module is available
|
|
218
|
+
if (!Voice) {
|
|
219
|
+
console.error('Voice module is not available');
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
203
223
|
// Ensure cleanup of any existing session
|
|
204
224
|
await cleanupVoiceSession();
|
|
205
225
|
|
|
@@ -221,7 +241,7 @@ export async function startRecording() {
|
|
|
221
241
|
|
|
222
242
|
export async function stopRecording() {
|
|
223
243
|
try {
|
|
224
|
-
if (!isCurrentlyRecording) return;
|
|
244
|
+
if (!isCurrentlyRecording || !Voice) return;
|
|
225
245
|
|
|
226
246
|
// Set this first to prevent race conditions
|
|
227
247
|
isCurrentlyRecording = false;
|
|
@@ -259,13 +279,13 @@ export async function stopRecording() {
|
|
|
259
279
|
|
|
260
280
|
export async function cancelRecording() {
|
|
261
281
|
try {
|
|
282
|
+
if (!Voice) return;
|
|
262
283
|
await Voice.cancel();
|
|
263
284
|
await cleanupVoiceSession();
|
|
264
285
|
} catch (error) {
|
|
265
286
|
console.error('Error canceling voice recognition:', error);
|
|
266
287
|
await cleanupVoiceSession();
|
|
267
288
|
}
|
|
268
|
-
|
|
269
289
|
}
|
|
270
290
|
|
|
271
291
|
export async function requestAudioPermission() {
|
|
@@ -299,24 +319,22 @@ export async function requestAudioPermission() {
|
|
|
299
319
|
|
|
300
320
|
async function requestAndroidPermission() {
|
|
301
321
|
try {
|
|
302
|
-
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
// Request microphone permission
|
|
325
|
+
const micPermission = await request(PERMISSIONS.ANDROID.RECORD_AUDIO);
|
|
326
|
+
if (micPermission !== RESULTS.GRANTED) {
|
|
327
|
+
console.log('Microphone permission denied');
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Skip checking speech recognition services which is causing errors
|
|
303
332
|
const services = await Voice.getSpeechRecognitionServices();
|
|
304
333
|
if (!services || services.length === 0) {
|
|
305
334
|
console.error('No speech recognition services available');
|
|
306
335
|
return false;
|
|
307
336
|
}
|
|
308
|
-
|
|
309
|
-
const granted = await PermissionsAndroid.request(
|
|
310
|
-
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
311
|
-
{
|
|
312
|
-
title: 'Microphone Permission',
|
|
313
|
-
message: 'This app needs access to your microphone for voice recognition.',
|
|
314
|
-
buttonPositive: 'OK',
|
|
315
|
-
buttonNegative: 'Cancel',
|
|
316
|
-
}
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
337
|
+
return true;
|
|
320
338
|
} catch (error) {
|
|
321
339
|
console.error('Error requesting Android permission:', error);
|
|
322
340
|
return false;
|
|
@@ -355,13 +373,20 @@ export function resetStoredPermission() {
|
|
|
355
373
|
}
|
|
356
374
|
|
|
357
375
|
export function cleanup() {
|
|
376
|
+
if (!Voice) {
|
|
377
|
+
console.log('Voice module not available during cleanup');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
358
381
|
Voice.destroy().then(() => {
|
|
359
382
|
Voice.removeAllListeners();
|
|
360
383
|
cleanupVoiceSession();
|
|
361
384
|
}).catch(error => {
|
|
362
385
|
console.error('Error in cleanup:', error);
|
|
363
386
|
// Try one more time
|
|
364
|
-
Voice
|
|
387
|
+
if (Voice) {
|
|
388
|
+
Voice.destroy().catch(e => console.error('Final cleanup attempt failed:', e));
|
|
389
|
+
}
|
|
365
390
|
});
|
|
366
391
|
}
|
|
367
392
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
|
|
3
|
+
export interface ChatMessage {
|
|
4
|
+
type: string;
|
|
5
|
+
text: string | string[];
|
|
6
|
+
form?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatState {
|
|
10
|
+
typingIndicator: boolean;
|
|
11
|
+
ghostMessage: boolean;
|
|
12
|
+
ghostCard: boolean;
|
|
13
|
+
stopActivated: boolean;
|
|
14
|
+
disclaimer: boolean;
|
|
15
|
+
startStreaming: boolean;
|
|
16
|
+
messages: ChatMessage[];
|
|
17
|
+
showIcon: boolean;
|
|
18
|
+
toggleChat: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Module-level singleton to cache chat states by token
|
|
22
|
+
const chatStore: Record<string, ChatState> = {};
|
|
23
|
+
|
|
24
|
+
const getStorageKey = (token: string) => `srschat_${token}`;
|
|
25
|
+
|
|
26
|
+
export const defaultState: ChatState = {
|
|
27
|
+
typingIndicator: false,
|
|
28
|
+
ghostMessage: false,
|
|
29
|
+
ghostCard: false,
|
|
30
|
+
stopActivated: false,
|
|
31
|
+
disclaimer: false,
|
|
32
|
+
startStreaming: false,
|
|
33
|
+
messages: [{
|
|
34
|
+
type: "ai",
|
|
35
|
+
text: "Hi there š I'm Poseidon, your Heritage Pool+ AI Agent. I can help you during your online visit with Product and Account information. How can I help you today?"
|
|
36
|
+
}],
|
|
37
|
+
showIcon: true,
|
|
38
|
+
toggleChat: false
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads chat state for a specific customer token
|
|
43
|
+
* Returns cached state if available, otherwise loads from AsyncStorage
|
|
44
|
+
*/
|
|
45
|
+
export const loadChat = async (token: string): Promise<ChatState> => {
|
|
46
|
+
// Return from in-memory cache if available
|
|
47
|
+
if (chatStore[token]) {
|
|
48
|
+
return chatStore[token];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Otherwise load from AsyncStorage
|
|
52
|
+
try {
|
|
53
|
+
const key = getStorageKey(token);
|
|
54
|
+
const storedData = await AsyncStorage.getItem(key);
|
|
55
|
+
|
|
56
|
+
if (storedData) {
|
|
57
|
+
const parsedData = JSON.parse(storedData) as ChatState;
|
|
58
|
+
// Cache in memory for future access
|
|
59
|
+
chatStore[token] = parsedData;
|
|
60
|
+
return parsedData;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error loading chat state:', error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Return default state if nothing found
|
|
67
|
+
return defaultState;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Updates chat state for a specific customer token
|
|
72
|
+
* Updates both in-memory cache and persists to AsyncStorage
|
|
73
|
+
*/
|
|
74
|
+
export const updateChat = async (token: string, next: ChatState): Promise<void> => {
|
|
75
|
+
try {
|
|
76
|
+
const key = getStorageKey(token);
|
|
77
|
+
|
|
78
|
+
// Update in-memory cache
|
|
79
|
+
chatStore[token] = next;
|
|
80
|
+
|
|
81
|
+
// Persist to AsyncStorage
|
|
82
|
+
await AsyncStorage.setItem(key, JSON.stringify(next));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error updating chat state:', error);
|
|
85
|
+
}
|
|
86
|
+
};
|