@mobileai/react-native 0.9.10 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +11 -0
  2. package/lib/module/components/AIAgent.js +635 -39
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AgentChatBar.js +309 -13
  5. package/lib/module/components/AgentChatBar.js.map +1 -1
  6. package/lib/module/config/endpoints.js +22 -0
  7. package/lib/module/config/endpoints.js.map +1 -0
  8. package/lib/module/core/systemPrompt.js +126 -100
  9. package/lib/module/core/systemPrompt.js.map +1 -1
  10. package/lib/module/services/AudioInputService.js +9 -0
  11. package/lib/module/services/AudioInputService.js.map +1 -1
  12. package/lib/module/services/flags/FlagService.js +1 -1
  13. package/lib/module/services/flags/FlagService.js.map +1 -1
  14. package/lib/module/services/telemetry/TelemetryService.js +44 -15
  15. package/lib/module/services/telemetry/TelemetryService.js.map +1 -1
  16. package/lib/module/services/telemetry/device.js +80 -10
  17. package/lib/module/services/telemetry/device.js.map +1 -1
  18. package/lib/module/services/telemetry/deviceMetadata.js +10 -0
  19. package/lib/module/services/telemetry/deviceMetadata.js.map +1 -0
  20. package/lib/module/support/EscalationEventSource.js +168 -0
  21. package/lib/module/support/EscalationEventSource.js.map +1 -0
  22. package/lib/module/support/EscalationSocket.js +46 -7
  23. package/lib/module/support/EscalationSocket.js.map +1 -1
  24. package/lib/module/support/SupportChatModal.js +544 -0
  25. package/lib/module/support/SupportChatModal.js.map +1 -0
  26. package/lib/module/support/TicketStore.js +93 -0
  27. package/lib/module/support/TicketStore.js.map +1 -0
  28. package/lib/module/support/escalateTool.js +45 -13
  29. package/lib/module/support/escalateTool.js.map +1 -1
  30. package/lib/module/support/index.js +2 -0
  31. package/lib/module/support/index.js.map +1 -1
  32. package/lib/typescript/src/components/AIAgent.d.ts +24 -1
  33. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  34. package/lib/typescript/src/components/AgentChatBar.d.ts +24 -2
  35. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  36. package/lib/typescript/src/config/endpoints.d.ts +18 -0
  37. package/lib/typescript/src/config/endpoints.d.ts.map +1 -0
  38. package/lib/typescript/src/core/systemPrompt.d.ts +4 -13
  39. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  40. package/lib/typescript/src/core/types.d.ts +1 -1
  41. package/lib/typescript/src/core/types.d.ts.map +1 -1
  42. package/lib/typescript/src/hooks/useAction.d.ts +2 -2
  43. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
  44. package/lib/typescript/src/index.d.ts +1 -1
  45. package/lib/typescript/src/index.d.ts.map +1 -1
  46. package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -1
  47. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +2 -1
  48. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -1
  49. package/lib/typescript/src/services/telemetry/device.d.ts +15 -4
  50. package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -1
  51. package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts +6 -0
  52. package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +1 -0
  53. package/lib/typescript/src/support/EscalationEventSource.d.ts +38 -0
  54. package/lib/typescript/src/support/EscalationEventSource.d.ts.map +1 -0
  55. package/lib/typescript/src/support/EscalationSocket.d.ts +7 -1
  56. package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -1
  57. package/lib/typescript/src/support/SupportChatModal.d.ts +21 -0
  58. package/lib/typescript/src/support/SupportChatModal.d.ts.map +1 -0
  59. package/lib/typescript/src/support/TicketStore.d.ts +34 -0
  60. package/lib/typescript/src/support/TicketStore.d.ts.map +1 -0
  61. package/lib/typescript/src/support/escalateTool.d.ts +16 -1
  62. package/lib/typescript/src/support/escalateTool.d.ts.map +1 -1
  63. package/lib/typescript/src/support/index.d.ts +2 -1
  64. package/lib/typescript/src/support/index.d.ts.map +1 -1
  65. package/lib/typescript/src/support/types.d.ts +15 -0
  66. package/lib/typescript/src/support/types.d.ts.map +1 -1
  67. package/package.json +5 -1
  68. package/src/components/AIAgent.tsx +622 -38
  69. package/src/components/AgentChatBar.tsx +348 -9
  70. package/src/config/endpoints.ts +22 -0
  71. package/src/core/systemPrompt.ts +126 -100
  72. package/src/core/types.ts +1 -1
  73. package/src/hooks/useAction.ts +2 -2
  74. package/src/index.ts +1 -0
  75. package/src/services/AudioInputService.ts +9 -0
  76. package/src/services/flags/FlagService.ts +1 -1
  77. package/src/services/telemetry/TelemetryService.ts +46 -14
  78. package/src/services/telemetry/device.ts +88 -11
  79. package/src/services/telemetry/deviceMetadata.ts +13 -0
  80. package/src/support/EscalationEventSource.ts +190 -0
  81. package/src/support/EscalationSocket.ts +47 -8
  82. package/src/support/SupportChatModal.tsx +563 -0
  83. package/src/support/TicketStore.ts +100 -0
  84. package/src/support/escalateTool.ts +53 -13
  85. package/src/support/index.ts +2 -0
  86. package/src/support/types.ts +14 -0
@@ -0,0 +1,563 @@
1
+ /**
2
+ * SupportChatModal — full-screen chat modal for human support conversations.
3
+ * Shows message history (bubbles with timestamps/avatars), typing indicator, and reply input.
4
+ * Supports native swipe-down-to-dismiss on iOS pageSheet.
5
+ */
6
+
7
+ import { useState, useEffect, useRef } from 'react';
8
+ import {
9
+ Modal,
10
+ View,
11
+ Text,
12
+ TextInput,
13
+ Pressable,
14
+ ScrollView,
15
+ StyleSheet,
16
+ Platform,
17
+ StatusBar,
18
+ Keyboard,
19
+ } from 'react-native';
20
+ import type { AIMessage } from '../core/types';
21
+ import { CloseIcon, SendArrowIcon, LoadingDots } from '../components/Icons';
22
+
23
+ // ─── Props ─────────────────────────────────────────────────────
24
+
25
+ interface SupportChatModalProps {
26
+ visible: boolean;
27
+ messages: AIMessage[];
28
+ onSend: (message: string) => void;
29
+ onClose: () => void;
30
+ isAgentTyping?: boolean;
31
+ isThinking?: boolean;
32
+ /** Optional: externally controlled scroll trigger. Pass when messages update externally. */
33
+ scrollToEndTrigger?: number;
34
+ /** Ticket status — when 'closed' or 'resolved', input is hidden and a banner is shown. */
35
+ ticketStatus?: string;
36
+ }
37
+
38
+ // ─── Helpers ───────────────────────────────────────────────────
39
+
40
+ function formatRelativeTime(timestamp: number): string {
41
+ const diff = Date.now() - timestamp;
42
+ if (diff < 60000) return 'just now';
43
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
44
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
45
+ return new Date(timestamp).toLocaleDateString();
46
+ }
47
+
48
+ function shouldShowDateSeparator(prev: AIMessage | undefined, curr: AIMessage): boolean {
49
+ if (!prev) return true;
50
+ const prevDay = new Date(prev.timestamp).toDateString();
51
+ const currDay = new Date(curr.timestamp).toDateString();
52
+ return prevDay !== currDay;
53
+ }
54
+
55
+ function formatDateSeparator(timestamp: number): string {
56
+ const now = new Date();
57
+ const date = new Date(timestamp);
58
+ if (date.toDateString() === now.toDateString()) return 'Today';
59
+ const yesterday = new Date(now);
60
+ yesterday.setDate(yesterday.getDate() - 1);
61
+ if (date.toDateString() === yesterday.toDateString()) return 'Yesterday';
62
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
63
+ }
64
+
65
+ // ─── Agent Avatar ──────────────────────────────────────────────
66
+
67
+ function AgentAvatar() {
68
+ return (
69
+ <View style={s.agentAvatar}>
70
+ <View style={s.avatarHead} />
71
+ <View style={s.avatarBody} />
72
+ </View>
73
+ );
74
+ }
75
+
76
+ // ─── Main Component ────────────────────────────────────────────
77
+
78
+ const CLOSED_STATUSES = ['closed', 'resolved'];
79
+
80
+ export function SupportChatModal({
81
+ visible,
82
+ messages,
83
+ onSend,
84
+ onClose,
85
+ isAgentTyping = false,
86
+ isThinking = false,
87
+ scrollToEndTrigger = 0,
88
+ ticketStatus,
89
+ }: SupportChatModalProps) {
90
+ const isClosed = !!ticketStatus && CLOSED_STATUSES.includes(ticketStatus);
91
+ const [text, setText] = useState('');
92
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ const scrollRef = useRef<any>(null);
95
+
96
+ // Scroll to bottom when new messages arrive or typing indicator changes
97
+ useEffect(() => {
98
+ if (messages.length > 0 || isAgentTyping) {
99
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 150);
100
+ }
101
+ }, [messages.length, isAgentTyping]);
102
+
103
+ // Scroll when externally triggered (e.g., after message update in parent)
104
+ useEffect(() => {
105
+ if (scrollToEndTrigger && scrollToEndTrigger > 0) {
106
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 200);
107
+ }
108
+ }, [scrollToEndTrigger]);
109
+
110
+ // Manually track keyboard height — reliable inside iOS pageSheet modals
111
+ // where KeyboardAvoidingView miscalculates the offset from screen origin.
112
+ useEffect(() => {
113
+ const showSub = Keyboard.addListener('keyboardWillShow', (e) => {
114
+ setKeyboardHeight(e.endCoordinates.height);
115
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 100);
116
+ });
117
+ const hideSub = Keyboard.addListener('keyboardWillHide', () => {
118
+ setKeyboardHeight(0);
119
+ });
120
+ return () => {
121
+ showSub.remove();
122
+ hideSub.remove();
123
+ };
124
+ }, []);
125
+
126
+ const handleSend = () => {
127
+ if (!text.trim() || isThinking) return;
128
+ onSend(text.trim());
129
+ setText('');
130
+ };
131
+
132
+ const isEmpty = messages.length === 0 && !isAgentTyping;
133
+
134
+ return (
135
+ <Modal
136
+ visible={visible}
137
+ animationType="slide"
138
+ presentationStyle="pageSheet"
139
+ onRequestClose={onClose}
140
+ >
141
+ <StatusBar barStyle="light-content" />
142
+ <View style={[s.container, keyboardHeight > 0 && { paddingBottom: keyboardHeight }]}>
143
+ {/* Drag grip indicator */}
144
+ <View style={s.dragHandle}>
145
+ <View style={s.dragGrip} />
146
+ </View>
147
+
148
+ {/* ── Header ── */}
149
+ <View style={s.header}>
150
+ <Pressable onPress={onClose} style={s.headerBtn} hitSlop={12}>
151
+ <CloseIcon size={20} color="rgba(255,255,255,0.7)" />
152
+ </Pressable>
153
+ <View style={s.headerCenter}>
154
+ <Text style={s.headerTitle}>Support Chat</Text>
155
+ <View style={s.headerStatus}>
156
+ <View style={[s.statusDot, isClosed && s.statusDotClosed]} />
157
+ <Text style={s.headerSubtitle}>
158
+ {isClosed ? 'Conversation closed' : 'Agent online'}
159
+ </Text>
160
+ </View>
161
+ </View>
162
+ <View style={s.headerBtn} />
163
+ </View>
164
+
165
+ {/* ── Messages ── */}
166
+ {isEmpty ? (
167
+ <View style={s.emptyState}>
168
+ <View style={s.emptyIcon}>
169
+ <View style={s.emptyBubble} />
170
+ <View style={s.emptyTail} />
171
+ </View>
172
+ <Text style={s.emptyTitle}>No messages yet</Text>
173
+ <Text style={s.emptySubtitle}>Start the conversation below</Text>
174
+ </View>
175
+ ) : (
176
+ <ScrollView
177
+ ref={scrollRef}
178
+ style={s.messagesList}
179
+ contentContainerStyle={s.messagesContent}
180
+ showsVerticalScrollIndicator={false}
181
+ keyboardShouldPersistTaps="handled"
182
+ >
183
+ {messages.map((msg, i) => {
184
+ const isUser = msg.role === 'user';
185
+ const prev = messages[i - 1];
186
+ const showDate = shouldShowDateSeparator(prev, msg);
187
+
188
+ return (
189
+ <View key={msg.id}>
190
+ {/* Date separator */}
191
+ {showDate && (
192
+ <View style={s.dateSeparator}>
193
+ <View style={s.dateLine} />
194
+ <Text style={s.dateText}>{formatDateSeparator(msg.timestamp)}</Text>
195
+ <View style={s.dateLine} />
196
+ </View>
197
+ )}
198
+
199
+ {/* Message row */}
200
+ <View style={[s.messageRow, isUser && s.messageRowUser]}>
201
+ {/* Agent avatar (left side) */}
202
+ {!isUser && <AgentAvatar />}
203
+
204
+ <View style={s.bubbleColumn}>
205
+ <View
206
+ style={[
207
+ s.bubble,
208
+ isUser ? s.bubbleUser : s.bubbleAgent,
209
+ ]}
210
+ >
211
+ <Text style={[s.bubbleText, !isUser && s.bubbleTextAgent]}>
212
+ {msg.content}
213
+ </Text>
214
+ </View>
215
+ <Text style={[s.timestamp, isUser && s.timestampUser]}>
216
+ {formatRelativeTime(msg.timestamp)}
217
+ </Text>
218
+ </View>
219
+ </View>
220
+ </View>
221
+ );
222
+ })}
223
+
224
+ {/* Typing indicator */}
225
+ {isAgentTyping && (
226
+ <View style={s.messageRow}>
227
+ <AgentAvatar />
228
+ <View style={s.bubbleColumn}>
229
+ <View style={[s.bubble, s.bubbleAgent, s.typingBubble]}>
230
+ <LoadingDots size={20} color="rgba(255,255,255,0.6)" />
231
+ </View>
232
+ </View>
233
+ </View>
234
+ )}
235
+ </ScrollView>
236
+ )}
237
+
238
+ {/* ── Input Row or Closed Banner ── */}
239
+ {isClosed ? (
240
+ <View style={s.closedBanner}>
241
+ <Text style={s.closedBannerText}>
242
+ This conversation has been closed. Start a new request to get help.
243
+ </Text>
244
+ </View>
245
+ ) : (
246
+ <View style={s.inputRow}>
247
+ <TextInput
248
+ style={s.input}
249
+ placeholder="Type a message..."
250
+ placeholderTextColor="rgba(255,255,255,0.35)"
251
+ value={text}
252
+ onChangeText={setText}
253
+ onSubmitEditing={handleSend}
254
+ returnKeyType="send"
255
+ editable={!isThinking}
256
+ />
257
+ <Pressable
258
+ style={[s.sendBtn, text.trim() && !isThinking ? s.sendBtnActive : s.sendBtnInactive]}
259
+ onPress={handleSend}
260
+ disabled={!text.trim() || isThinking}
261
+ >
262
+ <SendArrowIcon size={18} color={text.trim() && !isThinking ? '#fff' : 'rgba(255,255,255,0.3)'} />
263
+ </Pressable>
264
+ </View>
265
+ )}
266
+ </View>
267
+ </Modal>
268
+ );
269
+ }
270
+
271
+ // ─── Styles ────────────────────────────────────────────────────
272
+
273
+ const s = StyleSheet.create({
274
+ container: {
275
+ flex: 1,
276
+ backgroundColor: '#0f0f1e',
277
+ },
278
+
279
+ // ── Drag Handle ──
280
+ dragHandle: {
281
+ alignItems: 'center',
282
+ paddingTop: Platform.OS === 'ios' ? 52 : 16,
283
+ paddingBottom: 6,
284
+ },
285
+ dragGrip: {
286
+ width: 36,
287
+ height: 5,
288
+ borderRadius: 3,
289
+ backgroundColor: 'rgba(255,255,255,0.2)',
290
+ },
291
+
292
+ // ── Header ──
293
+ header: {
294
+ flexDirection: 'row',
295
+ alignItems: 'center',
296
+ justifyContent: 'space-between',
297
+ paddingHorizontal: 12,
298
+ paddingTop: 10,
299
+ paddingBottom: 14,
300
+ backgroundColor: 'rgba(255,255,255,0.03)',
301
+ borderBottomWidth: StyleSheet.hairlineWidth,
302
+ borderBottomColor: 'rgba(255,255,255,0.08)',
303
+ },
304
+ headerBtn: {
305
+ width: 36,
306
+ height: 36,
307
+ alignItems: 'center',
308
+ justifyContent: 'center',
309
+ },
310
+ headerCenter: {
311
+ alignItems: 'center',
312
+ },
313
+ headerTitle: {
314
+ color: '#fff',
315
+ fontSize: 17,
316
+ fontWeight: '700',
317
+ letterSpacing: 0.3,
318
+ },
319
+ headerStatus: {
320
+ flexDirection: 'row',
321
+ alignItems: 'center',
322
+ gap: 5,
323
+ marginTop: 3,
324
+ },
325
+ statusDot: {
326
+ width: 7,
327
+ height: 7,
328
+ borderRadius: 4,
329
+ backgroundColor: '#34C759',
330
+ },
331
+ statusDotClosed: {
332
+ backgroundColor: '#8E8E93',
333
+ },
334
+ headerSubtitle: {
335
+ color: 'rgba(255,255,255,0.5)',
336
+ fontSize: 12,
337
+ fontWeight: '500',
338
+ },
339
+
340
+ // ── Messages ──
341
+ messagesList: {
342
+ flex: 1,
343
+ },
344
+ messagesContent: {
345
+ paddingHorizontal: 16,
346
+ paddingVertical: 12,
347
+ paddingBottom: 16,
348
+ },
349
+
350
+ // ── Date Separator ──
351
+ dateSeparator: {
352
+ flexDirection: 'row',
353
+ alignItems: 'center',
354
+ paddingVertical: 16,
355
+ gap: 12,
356
+ },
357
+ dateLine: {
358
+ flex: 1,
359
+ height: StyleSheet.hairlineWidth,
360
+ backgroundColor: 'rgba(255,255,255,0.08)',
361
+ },
362
+ dateText: {
363
+ color: 'rgba(255,255,255,0.3)',
364
+ fontSize: 11,
365
+ fontWeight: '600',
366
+ textTransform: 'uppercase',
367
+ letterSpacing: 0.5,
368
+ },
369
+
370
+ // ── Message Row ──
371
+ messageRow: {
372
+ flexDirection: 'row',
373
+ alignItems: 'flex-end',
374
+ marginBottom: 4,
375
+ gap: 8,
376
+ },
377
+ messageRowUser: {
378
+ justifyContent: 'flex-end',
379
+ },
380
+ bubbleColumn: {
381
+ maxWidth: '72%',
382
+ },
383
+
384
+ // ── Bubble ──
385
+ bubble: {
386
+ borderRadius: 18,
387
+ paddingHorizontal: 14,
388
+ paddingVertical: 10,
389
+ },
390
+ bubbleUser: {
391
+ backgroundColor: '#7B68EE',
392
+ borderBottomRightRadius: 6,
393
+ elevation: 2,
394
+ shadowColor: '#7B68EE',
395
+ shadowOffset: { width: 0, height: 2 },
396
+ shadowOpacity: 0.25,
397
+ shadowRadius: 4,
398
+ },
399
+ bubbleAgent: {
400
+ backgroundColor: 'rgba(255,255,255,0.08)',
401
+ borderBottomLeftRadius: 6,
402
+ },
403
+ bubbleText: {
404
+ fontSize: 15,
405
+ lineHeight: 21,
406
+ color: '#fff',
407
+ },
408
+ bubbleTextAgent: {
409
+ color: 'rgba(255,255,255,0.9)',
410
+ },
411
+
412
+ // ── Timestamp ──
413
+ timestamp: {
414
+ color: 'rgba(255,255,255,0.25)',
415
+ fontSize: 11,
416
+ marginTop: 4,
417
+ marginLeft: 4,
418
+ marginBottom: 6,
419
+ },
420
+ timestampUser: {
421
+ textAlign: 'right',
422
+ marginRight: 4,
423
+ marginLeft: 0,
424
+ },
425
+
426
+ // ── Agent Avatar ──
427
+ agentAvatar: {
428
+ width: 30,
429
+ height: 30,
430
+ borderRadius: 15,
431
+ backgroundColor: '#7B68EE',
432
+ alignItems: 'center',
433
+ justifyContent: 'center',
434
+ marginBottom: 14,
435
+ },
436
+ avatarHead: {
437
+ width: 10,
438
+ height: 10,
439
+ borderRadius: 5,
440
+ backgroundColor: 'rgba(255,255,255,0.9)',
441
+ marginTop: 2,
442
+ },
443
+ avatarBody: {
444
+ width: 16,
445
+ height: 6,
446
+ borderTopLeftRadius: 8,
447
+ borderTopRightRadius: 8,
448
+ backgroundColor: 'rgba(255,255,255,0.9)',
449
+ marginTop: 1,
450
+ },
451
+
452
+ // ── Typing Indicator ──
453
+ typingBubble: {
454
+ flexDirection: 'row',
455
+ alignItems: 'center',
456
+ paddingVertical: 12,
457
+ paddingHorizontal: 16,
458
+ minWidth: 60,
459
+ },
460
+
461
+ // ── Empty State ──
462
+ emptyState: {
463
+ flex: 1,
464
+ alignItems: 'center',
465
+ justifyContent: 'center',
466
+ paddingBottom: 60,
467
+ },
468
+ emptyIcon: {
469
+ width: 64,
470
+ height: 64,
471
+ alignItems: 'center',
472
+ justifyContent: 'center',
473
+ marginBottom: 20,
474
+ },
475
+ emptyBubble: {
476
+ width: 48,
477
+ height: 36,
478
+ borderRadius: 12,
479
+ backgroundColor: 'rgba(255,255,255,0.08)',
480
+ borderWidth: 1,
481
+ borderColor: 'rgba(255,255,255,0.12)',
482
+ },
483
+ emptyTail: {
484
+ position: 'absolute',
485
+ bottom: 10,
486
+ left: 16,
487
+ width: 0,
488
+ height: 0,
489
+ borderTopWidth: 8,
490
+ borderTopColor: 'rgba(255,255,255,0.08)',
491
+ borderRightWidth: 8,
492
+ borderRightColor: 'transparent',
493
+ },
494
+ emptyTitle: {
495
+ color: 'rgba(255,255,255,0.5)',
496
+ fontSize: 17,
497
+ fontWeight: '600',
498
+ marginBottom: 6,
499
+ },
500
+ emptySubtitle: {
501
+ color: 'rgba(255,255,255,0.25)',
502
+ fontSize: 14,
503
+ },
504
+
505
+ // ── Closed Banner ──
506
+ closedBanner: {
507
+ paddingHorizontal: 16,
508
+ paddingVertical: 16,
509
+ paddingBottom: Platform.OS === 'ios' ? 36 : 16,
510
+ backgroundColor: 'rgba(255,255,255,0.03)',
511
+ borderTopWidth: StyleSheet.hairlineWidth,
512
+ borderTopColor: 'rgba(255,255,255,0.06)',
513
+ alignItems: 'center',
514
+ },
515
+ closedBannerText: {
516
+ color: 'rgba(255,255,255,0.35)',
517
+ fontSize: 13,
518
+ textAlign: 'center',
519
+ lineHeight: 19,
520
+ },
521
+
522
+ // ── Input Row ──
523
+ inputRow: {
524
+ flexDirection: 'row',
525
+ alignItems: 'center',
526
+ gap: 10,
527
+ paddingHorizontal: 16,
528
+ paddingVertical: 12,
529
+ paddingBottom: Platform.OS === 'ios' ? 36 : 16,
530
+ backgroundColor: 'rgba(255,255,255,0.02)',
531
+ borderTopWidth: StyleSheet.hairlineWidth,
532
+ borderTopColor: 'rgba(255,255,255,0.06)',
533
+ },
534
+ input: {
535
+ flex: 1,
536
+ backgroundColor: 'rgba(255,255,255,0.06)',
537
+ borderWidth: 1,
538
+ borderColor: 'rgba(255,255,255,0.08)',
539
+ borderRadius: 24,
540
+ paddingHorizontal: 18,
541
+ paddingVertical: 11,
542
+ color: '#fff',
543
+ fontSize: 16,
544
+ },
545
+ sendBtn: {
546
+ width: 42,
547
+ height: 42,
548
+ borderRadius: 21,
549
+ justifyContent: 'center',
550
+ alignItems: 'center',
551
+ },
552
+ sendBtnActive: {
553
+ backgroundColor: '#7B68EE',
554
+ elevation: 3,
555
+ shadowColor: '#7B68EE',
556
+ shadowOffset: { width: 0, height: 2 },
557
+ shadowOpacity: 0.3,
558
+ shadowRadius: 4,
559
+ },
560
+ sendBtnInactive: {
561
+ backgroundColor: 'rgba(255,255,255,0.06)',
562
+ },
563
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * TicketStore — persists the active support ticket across app restarts.
3
+ *
4
+ * Uses @react-native-async-storage/async-storage as an optional peer dependency.
5
+ * If AsyncStorage is not installed, all methods silently no-op and the feature
6
+ * degrades gracefully (tickets are still shown while the app is open, just not
7
+ * restored after a restart).
8
+ *
9
+ * Usage:
10
+ * await TicketStore.save(ticketId, analyticsKey); // on escalation start
11
+ * const pending = await TicketStore.get(); // on AIAgent mount
12
+ * await TicketStore.clear(); // on modal close / ticket closed
13
+ */
14
+
15
+ const STORAGE_KEY = '@mobileai_pending_ticket';
16
+
17
+ interface PendingTicket {
18
+ ticketId: string;
19
+ analyticsKey: string;
20
+ }
21
+
22
+ /** Try to load AsyncStorage at runtime. Optional peer dep — not bundled. */
23
+ function getAsyncStorage(): any | null {
24
+ try {
25
+ // Suppress the RN red box that AsyncStorage triggers when its native module
26
+ // isn't linked ("NativeModule: AsyncStorage is null").
27
+ const origError = console.error;
28
+ console.error = (...args: unknown[]) => {
29
+ const msg = args[0];
30
+ if (typeof msg === 'string' && msg.includes('AsyncStorage')) return;
31
+ origError.apply(console, args);
32
+ };
33
+ try {
34
+ const mod = require('@react-native-async-storage/async-storage');
35
+ const candidate = mod?.default ?? mod?.AsyncStorage ?? null;
36
+ if (candidate && typeof candidate.getItem === 'function') {
37
+ return candidate;
38
+ }
39
+ return null;
40
+ } finally {
41
+ console.error = origError;
42
+ }
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export const TicketStore = {
49
+ /**
50
+ * Persist the active ticket so it survives an app restart.
51
+ */
52
+ async save(ticketId: string, analyticsKey: string): Promise<void> {
53
+ const AS = getAsyncStorage();
54
+ if (!AS) {
55
+ console.warn(
56
+ '[TicketStore] @react-native-async-storage/async-storage is not installed — ' +
57
+ 'ticket will not persist across app restarts. ' +
58
+ 'Run: npx expo install @react-native-async-storage/async-storage'
59
+ );
60
+ return;
61
+ }
62
+ try {
63
+ await AS.setItem(STORAGE_KEY, JSON.stringify({ ticketId, analyticsKey }));
64
+ console.log('[TicketStore] Ticket saved:', ticketId);
65
+ } catch (err) {
66
+ console.error('[TicketStore] Failed to save ticket:', err);
67
+ }
68
+ },
69
+
70
+ /**
71
+ * Retrieve the persisted pending ticket, if any.
72
+ * Returns null if nothing is stored or AsyncStorage is unavailable.
73
+ */
74
+ async get(): Promise<PendingTicket | null> {
75
+ const AS = getAsyncStorage();
76
+ if (!AS) return null;
77
+ try {
78
+ const raw = await AS.getItem(STORAGE_KEY);
79
+ if (!raw) return null;
80
+ return JSON.parse(raw) as PendingTicket;
81
+ } catch (err) {
82
+ console.error('[TicketStore] Failed to read ticket:', err);
83
+ return null;
84
+ }
85
+ },
86
+
87
+ /**
88
+ * Remove the stored ticket (ticket closed or user dismissed modal).
89
+ */
90
+ async clear(): Promise<void> {
91
+ const AS = getAsyncStorage();
92
+ if (!AS) return;
93
+ try {
94
+ await AS.removeItem(STORAGE_KEY);
95
+ console.log('[TicketStore] Pending ticket cleared');
96
+ } catch (err) {
97
+ console.error('[TicketStore] Failed to clear ticket:', err);
98
+ }
99
+ },
100
+ };