react-native-inapp-inspector 2.2.3 → 2.2.4

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/dist/commonjs/components/AnalyticsDetail.js +5 -1
  2. package/dist/commonjs/components/AnalyticsEventCard.js +2 -1
  3. package/dist/commonjs/components/DiffViewer.js +1 -1
  4. package/dist/commonjs/components/Inspector/BundleTab.js +22 -0
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.d.ts +3 -0
  6. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +835 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -14
  8. package/dist/commonjs/components/Inspector/LogDetail.js +6 -3
  9. package/dist/commonjs/components/Inspector/MainScreen.js +7 -3
  10. package/dist/commonjs/components/Inspector/NetworkTab.js +92 -15
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +38 -12
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +440 -229
  13. package/dist/commonjs/components/Inspector/StorageTab.d.ts +3 -0
  14. package/dist/commonjs/components/Inspector/StorageTab.js +892 -0
  15. package/dist/commonjs/components/Inspector/TabBar.js +14 -0
  16. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +11 -2
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +4 -1
  18. package/dist/commonjs/components/NetworkIcons.d.ts +6 -0
  19. package/dist/commonjs/components/NetworkIcons.js +30 -1
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/customHooks/bundleAnalyzer.js +44 -22
  23. package/dist/commonjs/customHooks/storageInspector.d.ts +67 -0
  24. package/dist/commonjs/customHooks/storageInspector.js +486 -0
  25. package/dist/commonjs/i18n/locales/en.json +8 -2
  26. package/dist/commonjs/index.d.ts +1 -0
  27. package/dist/commonjs/index.js +16 -1
  28. package/dist/commonjs/storage.d.ts +5 -0
  29. package/dist/commonjs/storage.js +18 -0
  30. package/dist/commonjs/types/enums.d.ts +5 -0
  31. package/dist/commonjs/types/enums.js +5 -0
  32. package/dist/esm/components/AnalyticsDetail.js +6 -2
  33. package/dist/esm/components/AnalyticsEventCard.js +2 -1
  34. package/dist/esm/components/DiffViewer.js +1 -1
  35. package/dist/esm/components/Inspector/BundleTab.js +22 -0
  36. package/dist/esm/components/Inspector/DeviceInfoTab.d.ts +3 -0
  37. package/dist/esm/components/Inspector/DeviceInfoTab.js +796 -0
  38. package/dist/esm/components/Inspector/InspectorHeader.js +13 -15
  39. package/dist/esm/components/Inspector/LogDetail.js +7 -4
  40. package/dist/esm/components/Inspector/MainScreen.js +7 -3
  41. package/dist/esm/components/Inspector/NetworkTab.js +93 -16
  42. package/dist/esm/components/Inspector/PerformanceTab.js +39 -13
  43. package/dist/esm/components/Inspector/SettingsPanel.js +441 -230
  44. package/dist/esm/components/Inspector/StorageTab.d.ts +3 -0
  45. package/dist/esm/components/Inspector/StorageTab.js +853 -0
  46. package/dist/esm/components/Inspector/TabBar.js +15 -1
  47. package/dist/esm/components/Inspector/TelemetryConsentModal.js +11 -2
  48. package/dist/esm/components/Inspector/UpdateAvailableModal.js +4 -1
  49. package/dist/esm/components/NetworkIcons.d.ts +6 -0
  50. package/dist/esm/components/NetworkIcons.js +23 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/bundleAnalyzer.js +44 -22
  54. package/dist/esm/customHooks/storageInspector.d.ts +67 -0
  55. package/dist/esm/customHooks/storageInspector.js +469 -0
  56. package/dist/esm/i18n/locales/en.json +8 -2
  57. package/dist/esm/index.d.ts +1 -0
  58. package/dist/esm/index.js +5 -0
  59. package/dist/esm/storage.d.ts +5 -0
  60. package/dist/esm/storage.js +5 -0
  61. package/dist/esm/types/enums.d.ts +5 -0
  62. package/dist/esm/types/enums.js +5 -0
  63. package/package.json +8 -1
  64. package/src/components/AnalyticsDetail.tsx +6 -1
  65. package/src/components/AnalyticsEventCard.tsx +2 -1
  66. package/src/components/DiffViewer.tsx +1 -1
  67. package/src/components/Inspector/BundleTab.tsx +29 -0
  68. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  69. package/src/components/Inspector/InspectorHeader.tsx +13 -14
  70. package/src/components/Inspector/LogDetail.tsx +15 -11
  71. package/src/components/Inspector/MainScreen.tsx +7 -3
  72. package/src/components/Inspector/NetworkTab.tsx +104 -16
  73. package/src/components/Inspector/PerformanceTab.tsx +46 -12
  74. package/src/components/Inspector/SettingsPanel.tsx +570 -304
  75. package/src/components/Inspector/StorageTab.tsx +1048 -0
  76. package/src/components/Inspector/TabBar.tsx +20 -0
  77. package/src/components/Inspector/TelemetryConsentModal.tsx +26 -2
  78. package/src/components/Inspector/UpdateAvailableModal.tsx +13 -1
  79. package/src/components/NetworkIcons.tsx +103 -0
  80. package/src/constants/version.ts +1 -1
  81. package/src/customHooks/bundleAnalyzer.ts +47 -22
  82. package/src/customHooks/storageInspector.ts +509 -0
  83. package/src/i18n/locales/en.json +8 -2
  84. package/src/index.tsx +21 -1
  85. package/src/storage.ts +18 -0
  86. package/src/types/enums.ts +5 -0
@@ -0,0 +1,853 @@
1
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { View, Text, ScrollView, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
3
+ import TouchableScale from '../TouchableScale';
4
+ import { AppColors } from '../../styles/AppColors';
5
+ import { AppFonts } from '../../styles/AppFonts';
6
+ import { useTranslation } from '../../i18n';
7
+ import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, CopyIcon, CheckIcon, LayersIcon, ResetIcon, } from '../NetworkIcons';
8
+ import { fetchStorageEntries, setStorageEntry, removeStorageEntry, clearStorageDriver, isAsyncStorageConnected, isMMKVConnected, getRegisteredMMKVInstanceIds, subscribeToStorageChanges, } from '../../customHooks/storageInspector';
9
+ import { copyToClipboard } from '../../helpers';
10
+ import { showToast } from '../../helpers/toast';
11
+ export const StorageTab = React.memo(() => {
12
+ const { t } = useTranslation();
13
+ const [activeDriver, setActiveDriver] = useState('asyncStorage');
14
+ const [activeMMKVId, setActiveMMKVId] = useState('default');
15
+ const [entries, setEntries] = useState([]);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [search, setSearch] = useState('');
18
+ const [expandedKeys, setExpandedKeys] = useState({});
19
+ // Modal State for Create / Edit
20
+ const [modalVisible, setModalVisible] = useState(false);
21
+ const [modalMode, setModalMode] = useState('create');
22
+ const [editKeyName, setEditKeyName] = useState('');
23
+ const [editValue, setEditValue] = useState('');
24
+ const [editType, setEditType] = useState('string');
25
+ const [jsonError, setJsonError] = useState(null);
26
+ const mmkvInstanceIds = useMemo(() => getRegisteredMMKVInstanceIds(), []);
27
+ // Fetch entries
28
+ const loadEntries = useCallback(async () => {
29
+ setIsLoading(true);
30
+ try {
31
+ const data = await fetchStorageEntries(activeDriver, activeDriver === 'mmkv' ? activeMMKVId : undefined);
32
+ setEntries(data);
33
+ }
34
+ catch {
35
+ setEntries([]);
36
+ }
37
+ finally {
38
+ setIsLoading(false);
39
+ }
40
+ }, [activeDriver, activeMMKVId]);
41
+ useEffect(() => {
42
+ loadEntries();
43
+ const unsubscribe = subscribeToStorageChanges(loadEntries);
44
+ return unsubscribe;
45
+ }, [loadEntries]);
46
+ // Toggle JSON accordion
47
+ const toggleExpand = (key) => {
48
+ setExpandedKeys(prev => ({ ...prev, [key]: !prev[key] }));
49
+ };
50
+ // Open Create Modal
51
+ const handleOpenCreate = () => {
52
+ setModalMode('create');
53
+ setEditKeyName('');
54
+ setEditValue('');
55
+ setEditType('string');
56
+ setJsonError(null);
57
+ setModalVisible(true);
58
+ };
59
+ // Open Edit Modal
60
+ const handleOpenEdit = (entry) => {
61
+ setModalMode('edit');
62
+ setEditKeyName(entry.key);
63
+ setEditValue(entry.value);
64
+ setEditType(entry.type === 'json' ? 'json' : entry.type === 'number' ? 'number' : entry.type === 'boolean' ? 'boolean' : 'string');
65
+ setJsonError(null);
66
+ setModalVisible(true);
67
+ };
68
+ // Delete single key
69
+ const handleDeleteKey = (key) => {
70
+ Alert.alert('Delete Storage Key', `Are you sure you want to permanently delete "${key}" from ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'}?`, [
71
+ { text: 'Cancel', style: 'cancel' },
72
+ {
73
+ text: 'Delete',
74
+ style: 'destructive',
75
+ onPress: async () => {
76
+ const ok = await removeStorageEntry(activeDriver, key, activeDriver === 'mmkv' ? activeMMKVId : undefined);
77
+ if (ok) {
78
+ showToast(`Deleted "${key}"`);
79
+ loadEntries();
80
+ }
81
+ else {
82
+ showToast(`Failed to delete "${key}"`);
83
+ }
84
+ },
85
+ },
86
+ ]);
87
+ };
88
+ // Clear all keys
89
+ const handleClearAll = () => {
90
+ Alert.alert('Wipe All Storage Keys', `Are you sure you want to clear all ${entries.length} keys in ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'}? This cannot be undone.`, [
91
+ { text: 'Cancel', style: 'cancel' },
92
+ {
93
+ text: 'Wipe All',
94
+ style: 'destructive',
95
+ onPress: async () => {
96
+ const ok = await clearStorageDriver(activeDriver, activeDriver === 'mmkv' ? activeMMKVId : undefined);
97
+ if (ok) {
98
+ showToast('All storage keys wiped');
99
+ loadEntries();
100
+ }
101
+ },
102
+ },
103
+ ]);
104
+ };
105
+ // Save Modal (Create / Update)
106
+ const handleSaveModal = async () => {
107
+ if (!editKeyName.trim()) {
108
+ showToast('Key name cannot be empty');
109
+ return;
110
+ }
111
+ let finalVal = editValue;
112
+ if (editType === 'json') {
113
+ try {
114
+ // Validate JSON
115
+ const parsed = JSON.parse(editValue);
116
+ finalVal = JSON.stringify(parsed);
117
+ }
118
+ catch (err) {
119
+ setJsonError('Invalid JSON: ' + err.message);
120
+ return;
121
+ }
122
+ }
123
+ else if (editType === 'number') {
124
+ if (isNaN(Number(editValue))) {
125
+ setJsonError('Value must be a valid number');
126
+ return;
127
+ }
128
+ }
129
+ const success = await setStorageEntry(activeDriver, editKeyName.trim(), finalVal, activeDriver === 'mmkv' ? activeMMKVId : undefined);
130
+ if (success) {
131
+ showToast(modalMode === 'create' ? `Added "${editKeyName.trim()}"` : `Updated "${editKeyName.trim()}"`);
132
+ setModalVisible(false);
133
+ loadEntries();
134
+ }
135
+ else {
136
+ showToast('Error saving storage key');
137
+ }
138
+ };
139
+ // Format / Beautify JSON in modal
140
+ const handleBeautifyJson = () => {
141
+ try {
142
+ const parsed = JSON.parse(editValue);
143
+ setEditValue(JSON.stringify(parsed, null, 2));
144
+ setJsonError(null);
145
+ setEditType('json');
146
+ showToast('Formatted JSON');
147
+ }
148
+ catch {
149
+ setJsonError('Cannot format: invalid JSON syntax');
150
+ }
151
+ };
152
+ // Filter entries
153
+ const filteredEntries = useMemo(() => {
154
+ if (!search.trim())
155
+ return entries;
156
+ const q = search.toLowerCase().trim();
157
+ return entries.filter(e => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q));
158
+ }, [entries, search]);
159
+ const totalBytes = useMemo(() => {
160
+ return entries.reduce((sum, e) => sum + e.byteSize, 0);
161
+ }, [entries]);
162
+ const formattedTotalSize = useMemo(() => {
163
+ if (totalBytes < 1024)
164
+ return `${totalBytes} B`;
165
+ if (totalBytes < 1024 * 1024)
166
+ return `${(totalBytes / 1024).toFixed(1)} KB`;
167
+ return `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`;
168
+ }, [totalBytes]);
169
+ const getTypeBadge = (type) => {
170
+ switch (type) {
171
+ case 'json':
172
+ return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
173
+ case 'number':
174
+ return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
175
+ case 'boolean':
176
+ return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
177
+ case 'null':
178
+ return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
179
+ default:
180
+ return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
181
+ }
182
+ };
183
+ const isConnected = activeDriver === 'asyncStorage'
184
+ ? isAsyncStorageConnected()
185
+ : isMMKVConnected();
186
+ return (<View style={styles.container}>
187
+ {/* ── Driver Sub-Tabs ── */}
188
+ <View style={styles.driverTabsWrapper}>
189
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.driverTabsContainer}>
190
+ <TouchableScale onPress={() => setActiveDriver('asyncStorage')} style={[
191
+ styles.driverTabPill,
192
+ activeDriver === 'asyncStorage' && styles.driverTabPillActive,
193
+ ]}>
194
+ <DatabaseIcon size={13} color={activeDriver === 'asyncStorage'
195
+ ? AppColors.white
196
+ : AppColors.grayText}/>
197
+ <Text style={[
198
+ styles.driverTabText,
199
+ activeDriver === 'asyncStorage' && styles.driverTabTextActive,
200
+ ]}>
201
+ AsyncStorage
202
+ </Text>
203
+ {activeDriver === 'asyncStorage' && entries.length > 0 && (<View style={styles.driverCountBadge}>
204
+ <Text style={styles.driverCountText}>{entries.length}</Text>
205
+ </View>)}
206
+ </TouchableScale>
207
+
208
+ <TouchableScale onPress={() => setActiveDriver('mmkv')} style={[
209
+ styles.driverTabPill,
210
+ activeDriver === 'mmkv' && styles.driverTabPillActive,
211
+ ]}>
212
+ <LayersIcon size={13} color={activeDriver === 'mmkv'
213
+ ? AppColors.white
214
+ : AppColors.grayText}/>
215
+ <Text style={[
216
+ styles.driverTabText,
217
+ activeDriver === 'mmkv' && styles.driverTabTextActive,
218
+ ]}>
219
+ MMKV
220
+ </Text>
221
+ {activeDriver === 'mmkv' && entries.length > 0 && (<View style={styles.driverCountBadge}>
222
+ <Text style={styles.driverCountText}>{entries.length}</Text>
223
+ </View>)}
224
+ </TouchableScale>
225
+
226
+ {/* MMKV Named Instance Dropdown/Pills */}
227
+ {activeDriver === 'mmkv' && mmkvInstanceIds.length > 1 && (<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, marginLeft: 6 }}>
228
+ {mmkvInstanceIds.map(id => (<TouchableScale key={id} onPress={() => setActiveMMKVId(id)} style={[
229
+ styles.instancePill,
230
+ activeMMKVId === id && styles.instancePillActive,
231
+ ]}>
232
+ <Text style={[
233
+ styles.instancePillText,
234
+ activeMMKVId === id && styles.instancePillTextActive,
235
+ ]}>
236
+ {id}
237
+ </Text>
238
+ </TouchableScale>))}
239
+ </View>)}
240
+ </ScrollView>
241
+ </View>
242
+
243
+ {/* ── Search & Actions Bar ── */}
244
+ <View style={styles.actionBar}>
245
+ <View style={styles.searchBar}>
246
+ <SearchIcon size={14} color={AppColors.grayTextWeak}/>
247
+ <TextInput value={search} onChangeText={setSearch} placeholder={`Search ${entries.length} keys & values...`} placeholderTextColor={AppColors.grayTextWeak} style={styles.searchInput} clearButtonMode="while-editing"/>
248
+ {search.length > 0 && (<TouchableScale onPress={() => setSearch('')} hitSlop={8}>
249
+ <ClearIcon size={13} color={AppColors.grayTextWeak}/>
250
+ </TouchableScale>)}
251
+ </View>
252
+
253
+ {/* Action Buttons: Add Key & Refresh */}
254
+ <TouchableScale onPress={handleOpenCreate} style={styles.addKeyBtn} hitSlop={6}>
255
+ <PlusIcon size={13} color={AppColors.white}/>
256
+ <Text style={styles.addKeyBtnText}>Add</Text>
257
+ </TouchableScale>
258
+
259
+ <TouchableScale onPress={loadEntries} style={styles.actionIconBtn} hitSlop={6}>
260
+ <ResetIcon size={13} color={AppColors.grayText}/>
261
+ </TouchableScale>
262
+
263
+ {entries.length > 0 && (<TouchableScale onPress={handleClearAll} style={[styles.actionIconBtn, { borderColor: `${AppColors.errorColor}33`, backgroundColor: `${AppColors.errorColor}12` }]} hitSlop={6}>
264
+ <TrashIcon size={13} color={AppColors.errorColor}/>
265
+ </TouchableScale>)}
266
+ </View>
267
+
268
+ {/* ── Stats Strip ── */}
269
+ <View style={styles.statsStrip}>
270
+ <Text style={styles.statsText}>
271
+ {filteredEntries.length} of {entries.length} Keys • {formattedTotalSize}
272
+ </Text>
273
+ <Text style={styles.statsSubtext}>
274
+ {activeDriver === 'asyncStorage' ? 'SQLite Key-Value' : `MMKV (${activeMMKVId})`}
275
+ </Text>
276
+ </View>
277
+
278
+ {/* ── Storage Entries List ── */}
279
+ {isLoading ? (<View style={styles.loadingContainer}>
280
+ <ActivityIndicator size="small" color={AppColors.purple}/>
281
+ <Text style={styles.loadingText}>Loading storage entries...</Text>
282
+ </View>) : filteredEntries.length === 0 ? (<ScrollView contentContainerStyle={styles.emptyContainer} showsVerticalScrollIndicator={false}>
283
+ <View style={styles.emptyIconWrap}>
284
+ <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
285
+ </View>
286
+ <Text style={styles.emptyTitle}>
287
+ {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
288
+ </Text>
289
+ <Text style={styles.emptySubtitle}>
290
+ {search
291
+ ? 'Try modifying your search query'
292
+ : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
293
+ </Text>
294
+ </ScrollView>) : (<ScrollView style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
295
+ {filteredEntries.map(entry => {
296
+ const isExpanded = Boolean(expandedKeys[entry.key]);
297
+ const badge = getTypeBadge(entry.type);
298
+ const formattedBytes = entry.byteSize < 1024
299
+ ? `${entry.byteSize} B`
300
+ : `${(entry.byteSize / 1024).toFixed(1)} KB`;
301
+ return (<View key={entry.key} style={styles.entryCard}>
302
+ {/* Entry Header: Key name, Type Badge, Size, Actions */}
303
+ <View style={styles.entryHeader}>
304
+ <View style={{ flex: 1, marginRight: 8 }}>
305
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
306
+ <Text style={styles.entryKey} numberOfLines={1} selectable>
307
+ {entry.key}
308
+ </Text>
309
+ <View style={[
310
+ styles.typeBadge,
311
+ { backgroundColor: badge.bg, borderColor: `${badge.color}33` },
312
+ ]}>
313
+ <Text style={[styles.typeBadgeText, { color: badge.color }]}>
314
+ {badge.label}
315
+ </Text>
316
+ </View>
317
+ <Text style={styles.sizeText}>{formattedBytes}</Text>
318
+ </View>
319
+ </View>
320
+
321
+ {/* Actions: Copy, Edit, Delete */}
322
+ <View style={styles.entryActions}>
323
+ <TouchableScale hitSlop={6} onPress={() => {
324
+ copyToClipboard(entry.value, entry.key);
325
+ showToast(`Copied value of "${entry.key}"`);
326
+ }} style={styles.entryActionBtn}>
327
+ <CopyIcon size={12} color={AppColors.grayText}/>
328
+ </TouchableScale>
329
+
330
+ <TouchableScale hitSlop={6} onPress={() => handleOpenEdit(entry)} style={styles.entryActionBtn}>
331
+ <PencilIcon size={12} color={AppColors.purple}/>
332
+ </TouchableScale>
333
+
334
+ <TouchableScale hitSlop={6} onPress={() => handleDeleteKey(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
335
+ <TrashIcon size={12} color={AppColors.errorColor}/>
336
+ </TouchableScale>
337
+ </View>
338
+ </View>
339
+
340
+ {/* Entry Value Preview / Viewer */}
341
+ <TouchableScale onPress={() => toggleExpand(entry.key)} style={styles.valuePreviewBox}>
342
+ <Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable>
343
+ {entry.type === 'json' && entry.parsedValue
344
+ ? isExpanded
345
+ ? JSON.stringify(entry.parsedValue, null, 2)
346
+ : entry.value
347
+ : entry.value}
348
+ </Text>
349
+ {entry.value.length > 80 && (<Text style={styles.expandHint}>
350
+ {isExpanded ? '▲ Collapse' : '▼ Expand'}
351
+ </Text>)}
352
+ </TouchableScale>
353
+ </View>);
354
+ })}
355
+ <View style={{ height: 60 }}/>
356
+ </ScrollView>)}
357
+
358
+ {/* ── Create / Edit Key Modal ── */}
359
+ <Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
360
+ <View style={styles.modalOverlay}>
361
+ <View style={styles.modalCard}>
362
+ <View style={styles.modalHeader}>
363
+ <Text style={styles.modalTitle}>
364
+ {modalMode === 'create' ? 'Add Storage Key' : `Edit: ${editKeyName}`}
365
+ </Text>
366
+ <TouchableScale onPress={() => setModalVisible(false)} hitSlop={8}>
367
+ <ClearIcon size={16} color={AppColors.grayText}/>
368
+ </TouchableScale>
369
+ </View>
370
+
371
+ {/* Key Name Input */}
372
+ <View style={styles.inputGroup}>
373
+ <Text style={styles.inputLabel}>KEY NAME</Text>
374
+ <TextInput value={editKeyName} onChangeText={setEditKeyName} placeholder="e.g. user_session, auth_token" placeholderTextColor={AppColors.grayTextWeak} editable={modalMode === 'create'} style={[
375
+ styles.textInput,
376
+ modalMode === 'edit' && styles.textInputDisabled,
377
+ ]} autoCapitalize="none"/>
378
+ </View>
379
+
380
+ {/* Type Selector Strip */}
381
+ <View style={styles.inputGroup}>
382
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
383
+ <Text style={styles.inputLabel}>VALUE TYPE</Text>
384
+ {editType === 'json' && (<TouchableScale onPress={handleBeautifyJson} hitSlop={6}>
385
+ <Text style={styles.formatJsonBtn}>Beautify JSON</Text>
386
+ </TouchableScale>)}
387
+ </View>
388
+ <View style={styles.typeSelectorStrip}>
389
+ {['string', 'json', 'number', 'boolean'].map(t => {
390
+ const isSel = editType === t;
391
+ return (<TouchableScale key={t} onPress={() => {
392
+ setEditType(t);
393
+ setJsonError(null);
394
+ }} style={[
395
+ styles.typePill,
396
+ isSel && styles.typePillActive,
397
+ ]}>
398
+ <Text style={[
399
+ styles.typePillText,
400
+ isSel && styles.typePillTextActive,
401
+ ]}>
402
+ {t.toUpperCase()}
403
+ </Text>
404
+ </TouchableScale>);
405
+ })}
406
+ </View>
407
+ </View>
408
+
409
+ {/* Value Input */}
410
+ <View style={styles.inputGroup}>
411
+ <Text style={styles.inputLabel}>VALUE CONTENT</Text>
412
+ <TextInput value={editValue} onChangeText={v => {
413
+ setEditValue(v);
414
+ if (jsonError)
415
+ setJsonError(null);
416
+ }} placeholder={editType === 'json'
417
+ ? '{"userId": 123, "isLoggedIn": true}'
418
+ : editType === 'number'
419
+ ? '42'
420
+ : editType === 'boolean'
421
+ ? 'true'
422
+ : 'String value...'} placeholderTextColor={AppColors.grayTextWeak} style={[styles.textInput, styles.textAreaInput]} multiline textAlignVertical="top" autoCapitalize="none"/>
423
+ {jsonError && (<Text style={styles.errorText}>⚠️ {jsonError}</Text>)}
424
+ </View>
425
+
426
+ {/* Modal Actions */}
427
+ <View style={styles.modalActions}>
428
+ <TouchableScale onPress={() => setModalVisible(false)} style={styles.cancelBtn}>
429
+ <Text style={styles.cancelBtnText}>Cancel</Text>
430
+ </TouchableScale>
431
+ <TouchableScale onPress={handleSaveModal} style={styles.saveBtn}>
432
+ <CheckIcon size={14} color={AppColors.white}/>
433
+ <Text style={styles.saveBtnText}>Save Key</Text>
434
+ </TouchableScale>
435
+ </View>
436
+ </View>
437
+ </View>
438
+ </Modal>
439
+ </View>);
440
+ });
441
+ const styles = StyleSheet.create({
442
+ container: {
443
+ flex: 1,
444
+ backgroundColor: AppColors.grayBackground,
445
+ },
446
+ driverTabsWrapper: {
447
+ backgroundColor: AppColors.primaryLight,
448
+ borderBottomWidth: 1,
449
+ borderBottomColor: AppColors.dividerColor,
450
+ paddingVertical: 8,
451
+ },
452
+ driverTabsContainer: {
453
+ paddingHorizontal: 12,
454
+ flexDirection: 'row',
455
+ alignItems: 'center',
456
+ gap: 6,
457
+ },
458
+ driverTabPill: {
459
+ flexDirection: 'row',
460
+ alignItems: 'center',
461
+ gap: 6,
462
+ paddingHorizontal: 11,
463
+ paddingVertical: 6,
464
+ borderRadius: 8,
465
+ backgroundColor: AppColors.grayBackground,
466
+ borderWidth: 1,
467
+ borderColor: AppColors.grayBorderSecondary,
468
+ },
469
+ driverTabPillActive: {
470
+ backgroundColor: AppColors.purple,
471
+ borderColor: AppColors.purple,
472
+ },
473
+ driverTabText: {
474
+ fontFamily: AppFonts.interBold,
475
+ fontSize: 11.5,
476
+ color: AppColors.grayText,
477
+ },
478
+ driverTabTextActive: {
479
+ color: AppColors.white,
480
+ },
481
+ driverCountBadge: {
482
+ backgroundColor: 'rgba(255, 255, 255, 0.25)',
483
+ paddingHorizontal: 5,
484
+ paddingVertical: 1,
485
+ borderRadius: 6,
486
+ },
487
+ driverCountText: {
488
+ fontFamily: AppFonts.interBold,
489
+ fontSize: 9,
490
+ color: AppColors.white,
491
+ },
492
+ instancePill: {
493
+ paddingHorizontal: 7,
494
+ paddingVertical: 3,
495
+ borderRadius: 6,
496
+ backgroundColor: `${AppColors.purple}14`,
497
+ borderWidth: 1,
498
+ borderColor: `${AppColors.purple}30`,
499
+ },
500
+ instancePillActive: {
501
+ backgroundColor: AppColors.purple,
502
+ borderColor: AppColors.purple,
503
+ },
504
+ instancePillText: {
505
+ fontFamily: AppFonts.interBold,
506
+ fontSize: 10,
507
+ color: AppColors.purple,
508
+ },
509
+ instancePillTextActive: {
510
+ color: AppColors.white,
511
+ },
512
+ actionBar: {
513
+ flexDirection: 'row',
514
+ alignItems: 'center',
515
+ paddingHorizontal: 12,
516
+ paddingVertical: 8,
517
+ backgroundColor: AppColors.primaryLight,
518
+ borderBottomWidth: 1,
519
+ borderBottomColor: AppColors.dividerColor,
520
+ gap: 6,
521
+ },
522
+ searchBar: {
523
+ flex: 1,
524
+ flexDirection: 'row',
525
+ alignItems: 'center',
526
+ backgroundColor: AppColors.grayBackground,
527
+ borderRadius: 8,
528
+ paddingHorizontal: 9,
529
+ paddingVertical: Platform.OS === 'ios' ? 6 : 2,
530
+ borderWidth: 1,
531
+ borderColor: AppColors.grayBorderSecondary,
532
+ gap: 6,
533
+ },
534
+ searchInput: {
535
+ flex: 1,
536
+ fontFamily: AppFonts.interMedium,
537
+ fontSize: 12,
538
+ color: AppColors.primaryBlack,
539
+ padding: 0,
540
+ },
541
+ addKeyBtn: {
542
+ flexDirection: 'row',
543
+ alignItems: 'center',
544
+ gap: 4,
545
+ paddingHorizontal: 10,
546
+ paddingVertical: 6,
547
+ borderRadius: 8,
548
+ backgroundColor: AppColors.purple,
549
+ },
550
+ addKeyBtnText: {
551
+ fontFamily: AppFonts.interBold,
552
+ fontSize: 11,
553
+ color: AppColors.white,
554
+ },
555
+ actionIconBtn: {
556
+ width: 30,
557
+ height: 30,
558
+ borderRadius: 8,
559
+ alignItems: 'center',
560
+ justifyContent: 'center',
561
+ backgroundColor: AppColors.grayBackground,
562
+ borderWidth: 1,
563
+ borderColor: AppColors.grayBorderSecondary,
564
+ },
565
+ statsStrip: {
566
+ flexDirection: 'row',
567
+ alignItems: 'center',
568
+ justifyContent: 'space-between',
569
+ paddingHorizontal: 12,
570
+ paddingVertical: 5,
571
+ backgroundColor: `${AppColors.grayBorderSecondary}40`,
572
+ },
573
+ statsText: {
574
+ fontFamily: AppFonts.interMedium,
575
+ fontSize: 10,
576
+ color: AppColors.grayText,
577
+ },
578
+ statsSubtext: {
579
+ fontFamily: AppFonts.interBold,
580
+ fontSize: 9.5,
581
+ color: AppColors.grayTextWeak,
582
+ },
583
+ scrollArea: {
584
+ flex: 1,
585
+ },
586
+ scrollContent: {
587
+ padding: 12,
588
+ gap: 10,
589
+ paddingBottom: 100,
590
+ },
591
+ entryCard: {
592
+ backgroundColor: AppColors.primaryLight,
593
+ borderRadius: 10,
594
+ borderWidth: 1,
595
+ borderColor: AppColors.grayBorderSecondary,
596
+ padding: 10,
597
+ gap: 8,
598
+ shadowColor: AppColors.black,
599
+ shadowOpacity: 0.02,
600
+ shadowRadius: 3,
601
+ shadowOffset: { width: 0, height: 1 },
602
+ elevation: 1,
603
+ },
604
+ entryHeader: {
605
+ flexDirection: 'row',
606
+ alignItems: 'center',
607
+ justifyContent: 'space-between',
608
+ },
609
+ entryKey: {
610
+ fontFamily: AppFonts.interBold,
611
+ fontSize: 12.5,
612
+ color: AppColors.primaryBlack,
613
+ },
614
+ typeBadge: {
615
+ paddingHorizontal: 5,
616
+ paddingVertical: 1,
617
+ borderRadius: 4,
618
+ borderWidth: 1,
619
+ },
620
+ typeBadgeText: {
621
+ fontFamily: AppFonts.interBold,
622
+ fontSize: 8.5,
623
+ },
624
+ sizeText: {
625
+ fontFamily: AppFonts.interRegular,
626
+ fontSize: 10,
627
+ color: AppColors.grayTextWeak,
628
+ },
629
+ entryActions: {
630
+ flexDirection: 'row',
631
+ alignItems: 'center',
632
+ gap: 5,
633
+ },
634
+ entryActionBtn: {
635
+ padding: 5,
636
+ borderRadius: 5,
637
+ backgroundColor: AppColors.grayBackground,
638
+ },
639
+ valuePreviewBox: {
640
+ backgroundColor: AppColors.grayBackground,
641
+ borderRadius: 6,
642
+ padding: 8,
643
+ borderWidth: 1,
644
+ borderColor: `${AppColors.grayBorderSecondary}80`,
645
+ },
646
+ valuePreviewText: {
647
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
648
+ fontSize: 11,
649
+ color: AppColors.primaryBlack,
650
+ lineHeight: 16,
651
+ },
652
+ expandHint: {
653
+ fontFamily: AppFonts.interBold,
654
+ fontSize: 9.5,
655
+ color: AppColors.purple,
656
+ marginTop: 4,
657
+ },
658
+ loadingContainer: {
659
+ flex: 1,
660
+ alignItems: 'center',
661
+ justifyContent: 'center',
662
+ gap: 8,
663
+ },
664
+ loadingText: {
665
+ fontFamily: AppFonts.interMedium,
666
+ fontSize: 12,
667
+ color: AppColors.grayText,
668
+ },
669
+ emptyContainer: {
670
+ padding: 24,
671
+ alignItems: 'center',
672
+ justifyContent: 'center',
673
+ },
674
+ emptyIconWrap: {
675
+ width: 56,
676
+ height: 56,
677
+ borderRadius: 28,
678
+ backgroundColor: `${AppColors.grayBorderSecondary}40`,
679
+ alignItems: 'center',
680
+ justifyContent: 'center',
681
+ marginBottom: 12,
682
+ },
683
+ emptyTitle: {
684
+ fontFamily: AppFonts.interBold,
685
+ fontSize: 14,
686
+ color: AppColors.primaryBlack,
687
+ marginBottom: 4,
688
+ },
689
+ emptySubtitle: {
690
+ fontFamily: AppFonts.interRegular,
691
+ fontSize: 11.5,
692
+ color: AppColors.grayText,
693
+ textAlign: 'center',
694
+ maxWidth: 280,
695
+ },
696
+ connectGuideCard: {
697
+ marginTop: 20,
698
+ width: '100%',
699
+ backgroundColor: AppColors.primaryLight,
700
+ borderRadius: 10,
701
+ borderWidth: 1,
702
+ borderColor: AppColors.grayBorderSecondary,
703
+ padding: 12,
704
+ gap: 6,
705
+ },
706
+ connectGuideTitle: {
707
+ fontFamily: AppFonts.interBold,
708
+ fontSize: 11,
709
+ color: AppColors.purple,
710
+ },
711
+ connectGuideCode: {
712
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
713
+ fontSize: 10,
714
+ color: AppColors.grayText,
715
+ lineHeight: 15,
716
+ },
717
+ modalOverlay: {
718
+ flex: 1,
719
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
720
+ justifyContent: 'center',
721
+ alignItems: 'center',
722
+ padding: 16,
723
+ },
724
+ modalCard: {
725
+ width: '100%',
726
+ maxWidth: 400,
727
+ backgroundColor: AppColors.primaryLight,
728
+ borderRadius: 14,
729
+ padding: 16,
730
+ gap: 12,
731
+ shadowColor: AppColors.black,
732
+ shadowOpacity: 0.2,
733
+ shadowRadius: 10,
734
+ shadowOffset: { width: 0, height: 4 },
735
+ elevation: 5,
736
+ },
737
+ modalHeader: {
738
+ flexDirection: 'row',
739
+ alignItems: 'center',
740
+ justifyContent: 'space-between',
741
+ borderBottomWidth: 1,
742
+ borderBottomColor: AppColors.dividerColor,
743
+ paddingBottom: 10,
744
+ },
745
+ modalTitle: {
746
+ fontFamily: AppFonts.interBold,
747
+ fontSize: 14,
748
+ color: AppColors.primaryBlack,
749
+ },
750
+ inputGroup: {
751
+ gap: 4,
752
+ },
753
+ inputLabel: {
754
+ fontFamily: AppFonts.interBold,
755
+ fontSize: 9.5,
756
+ color: AppColors.grayTextWeak,
757
+ letterSpacing: 0.5,
758
+ },
759
+ textInput: {
760
+ backgroundColor: AppColors.grayBackground,
761
+ borderRadius: 8,
762
+ paddingHorizontal: 10,
763
+ paddingVertical: 8,
764
+ fontFamily: AppFonts.interMedium,
765
+ fontSize: 12,
766
+ color: AppColors.primaryBlack,
767
+ borderWidth: 1,
768
+ borderColor: AppColors.grayBorderSecondary,
769
+ },
770
+ textInputDisabled: {
771
+ backgroundColor: `${AppColors.grayBorderSecondary}40`,
772
+ color: AppColors.grayText,
773
+ },
774
+ textAreaInput: {
775
+ minHeight: 90,
776
+ maxHeight: 160,
777
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
778
+ fontSize: 11,
779
+ },
780
+ typeSelectorStrip: {
781
+ flexDirection: 'row',
782
+ alignItems: 'center',
783
+ gap: 6,
784
+ },
785
+ typePill: {
786
+ flex: 1,
787
+ alignItems: 'center',
788
+ paddingVertical: 6,
789
+ borderRadius: 6,
790
+ backgroundColor: AppColors.grayBackground,
791
+ borderWidth: 1,
792
+ borderColor: AppColors.grayBorderSecondary,
793
+ },
794
+ typePillActive: {
795
+ backgroundColor: AppColors.purple,
796
+ borderColor: AppColors.purple,
797
+ },
798
+ typePillText: {
799
+ fontFamily: AppFonts.interBold,
800
+ fontSize: 10,
801
+ color: AppColors.grayText,
802
+ },
803
+ typePillTextActive: {
804
+ color: AppColors.white,
805
+ },
806
+ formatJsonBtn: {
807
+ fontFamily: AppFonts.interBold,
808
+ fontSize: 10,
809
+ color: AppColors.purple,
810
+ },
811
+ errorText: {
812
+ fontFamily: AppFonts.interMedium,
813
+ fontSize: 10,
814
+ color: AppColors.errorColor,
815
+ marginTop: 2,
816
+ },
817
+ modalActions: {
818
+ flexDirection: 'row',
819
+ alignItems: 'center',
820
+ gap: 10,
821
+ marginTop: 4,
822
+ },
823
+ cancelBtn: {
824
+ flex: 1,
825
+ alignItems: 'center',
826
+ paddingVertical: 10,
827
+ borderRadius: 8,
828
+ backgroundColor: AppColors.grayBackground,
829
+ borderWidth: 1,
830
+ borderColor: AppColors.grayBorderSecondary,
831
+ },
832
+ cancelBtnText: {
833
+ fontFamily: AppFonts.interBold,
834
+ fontSize: 12,
835
+ color: AppColors.grayText,
836
+ },
837
+ saveBtn: {
838
+ flex: 1.5,
839
+ flexDirection: 'row',
840
+ alignItems: 'center',
841
+ justifyContent: 'center',
842
+ gap: 5,
843
+ paddingVertical: 10,
844
+ borderRadius: 8,
845
+ backgroundColor: AppColors.purple,
846
+ },
847
+ saveBtnText: {
848
+ fontFamily: AppFonts.interBold,
849
+ fontSize: 12,
850
+ color: AppColors.white,
851
+ },
852
+ });
853
+ export default StorageTab;