react-native-inapp-inspector 2.2.2 → 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 (159) 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 +9 -1
  64. package/src/analytics.ts +35 -0
  65. package/src/bundle.ts +25 -0
  66. package/src/components/AnalyticsDetail.tsx +870 -0
  67. package/src/components/AnalyticsEventCard.tsx +442 -0
  68. package/src/components/AnalyticsGraph.tsx +583 -0
  69. package/src/components/AnimatedEntrance.tsx +64 -0
  70. package/src/components/AppHeaderLogo.tsx +109 -0
  71. package/src/components/BrandCircleIcon.tsx +144 -0
  72. package/src/components/BrandSquareIcon.tsx +144 -0
  73. package/src/components/CodeSnippet.tsx +725 -0
  74. package/src/components/ConsoleLogCard.tsx +628 -0
  75. package/src/components/CopyButton.tsx +87 -0
  76. package/src/components/DiffViewer.tsx +82 -0
  77. package/src/components/DomainHeader.tsx +237 -0
  78. package/src/components/EmptyState.tsx +73 -0
  79. package/src/components/EndOfListFooter.tsx +100 -0
  80. package/src/components/ErrorBoundary.tsx +688 -0
  81. package/src/components/HeadersSection.tsx +192 -0
  82. package/src/components/HighlightText.tsx +100 -0
  83. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  84. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  85. package/src/components/Inspector/BundleTab.tsx +4760 -0
  86. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  87. package/src/components/Inspector/CrashDetail.tsx +941 -0
  88. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  89. package/src/components/Inspector/CrashTab.tsx +871 -0
  90. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  91. package/src/components/Inspector/FabLauncher.tsx +80 -0
  92. package/src/components/Inspector/InspectorContext.tsx +28 -0
  93. package/src/components/Inspector/InspectorHeader.tsx +972 -0
  94. package/src/components/Inspector/LogDetail.tsx +1431 -0
  95. package/src/components/Inspector/MainScreen.tsx +262 -0
  96. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  97. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  98. package/src/components/Inspector/NetworkTab.tsx +1183 -0
  99. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  100. package/src/components/Inspector/PerformanceTab.tsx +1928 -0
  101. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  102. package/src/components/Inspector/ReduxTab.tsx +954 -0
  103. package/src/components/Inspector/SettingsPanel.tsx +3447 -0
  104. package/src/components/Inspector/StorageTab.tsx +1048 -0
  105. package/src/components/Inspector/TabBar.tsx +207 -0
  106. package/src/components/Inspector/TelemetryConsentModal.tsx +416 -0
  107. package/src/components/Inspector/UpdateAvailableModal.tsx +557 -0
  108. package/src/components/JsonViewer.tsx +486 -0
  109. package/src/components/LogCard.tsx +491 -0
  110. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  111. package/src/components/MetaAccordion.tsx +340 -0
  112. package/src/components/MiniBarChart.tsx +42 -0
  113. package/src/components/MiniLineChart.tsx +33 -0
  114. package/src/components/NetworkIcons.tsx +2221 -0
  115. package/src/components/SectionHeader.tsx +113 -0
  116. package/src/components/SegmentedTabs.tsx +83 -0
  117. package/src/components/Slider.tsx +300 -0
  118. package/src/components/SourcePageCard.tsx +148 -0
  119. package/src/components/Toast.tsx +131 -0
  120. package/src/components/TouchableScale.tsx +91 -0
  121. package/src/components/TreeNode.tsx +186 -0
  122. package/src/console.ts +24 -0
  123. package/src/constants/index.ts +38 -0
  124. package/src/constants/version.ts +3 -0
  125. package/src/crash.ts +32 -0
  126. package/src/customHooks/analyticsLogger.ts +336 -0
  127. package/src/customHooks/bundleAnalyzer.ts +1256 -0
  128. package/src/customHooks/consoleLogger.ts +497 -0
  129. package/src/customHooks/crashHandler.ts +944 -0
  130. package/src/customHooks/logFilters.ts +32 -0
  131. package/src/customHooks/networkLogger.ts +419 -0
  132. package/src/customHooks/performanceTracker.ts +1014 -0
  133. package/src/customHooks/reduxLogger.ts +406 -0
  134. package/src/customHooks/storageInspector.ts +509 -0
  135. package/src/customHooks/useAccordion.tsx +60 -0
  136. package/src/decorators/index.ts +184 -0
  137. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  138. package/src/helpers/index.ts +860 -0
  139. package/src/helpers/memoryManager.ts +138 -0
  140. package/src/helpers/searchQueryParser.ts +283 -0
  141. package/src/helpers/settingsStore.ts +171 -0
  142. package/src/helpers/telemetry.ts +505 -0
  143. package/src/helpers/toast.ts +17 -0
  144. package/src/i18n/index.ts +69 -0
  145. package/src/i18n/locales/en.json +1058 -0
  146. package/src/index.tsx +2356 -0
  147. package/src/native/NativeInspector.ts +397 -0
  148. package/src/native/NativeNetworkInspector.ts +24 -0
  149. package/src/network.ts +22 -0
  150. package/src/performance.ts +38 -0
  151. package/src/redux.ts +23 -0
  152. package/src/storage.ts +18 -0
  153. package/src/styles/AppColors.ts +408 -0
  154. package/src/styles/AppFonts.ts +8 -0
  155. package/src/styles/common.ts +209 -0
  156. package/src/styles/index.ts +1570 -0
  157. package/src/types/enums.ts +199 -0
  158. package/src/types/index.ts +34 -0
  159. package/src/types/interfaces.ts +515 -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;