react-native-inapp-inspector 2.3.14 → 2.3.15

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 (65) hide show
  1. package/dist/commonjs/components/AnimatedEntrance.js +3 -3
  2. package/dist/commonjs/components/ConsoleLogCard.js +174 -100
  3. package/dist/commonjs/components/Inspector/ConsoleTab.js +25 -16
  4. package/dist/commonjs/components/Inspector/CrashTab.js +19 -5
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +198 -98
  6. package/dist/commonjs/components/Inspector/InspectorHeader.js +2 -2
  7. package/dist/commonjs/components/Inspector/MainScreen.js +56 -56
  8. package/dist/commonjs/components/Inspector/NetworkTab.js +1 -335
  9. package/dist/commonjs/components/Inspector/PerformanceTab.js +32 -6
  10. package/dist/commonjs/components/Inspector/ReduxTab.js +3 -1
  11. package/dist/commonjs/components/Inspector/StorageTab.js +23 -5
  12. package/dist/commonjs/components/JsonViewer.js +14 -2
  13. package/dist/commonjs/components/LogCard.js +428 -227
  14. package/dist/commonjs/constants/version.d.ts +1 -1
  15. package/dist/commonjs/constants/version.js +1 -1
  16. package/dist/commonjs/customHooks/analyticsLogger.js +1 -1
  17. package/dist/commonjs/customHooks/consoleLogger.js +34 -2
  18. package/dist/commonjs/customHooks/crashHandler.js +1 -1
  19. package/dist/commonjs/customHooks/networkLogger.js +184 -26
  20. package/dist/commonjs/helpers/searchQueryParser.d.ts +1 -1
  21. package/dist/commonjs/helpers/searchQueryParser.js +37 -291
  22. package/dist/commonjs/index.js +141 -27
  23. package/dist/esm/components/AnimatedEntrance.js +3 -3
  24. package/dist/esm/components/ConsoleLogCard.js +141 -100
  25. package/dist/esm/components/Inspector/ConsoleTab.js +25 -16
  26. package/dist/esm/components/Inspector/CrashTab.js +19 -5
  27. package/dist/esm/components/Inspector/DeviceInfoTab.js +198 -98
  28. package/dist/esm/components/Inspector/InspectorHeader.js +3 -3
  29. package/dist/esm/components/Inspector/MainScreen.js +56 -56
  30. package/dist/esm/components/Inspector/NetworkTab.js +2 -336
  31. package/dist/esm/components/Inspector/PerformanceTab.js +32 -6
  32. package/dist/esm/components/Inspector/ReduxTab.js +3 -1
  33. package/dist/esm/components/Inspector/StorageTab.js +23 -5
  34. package/dist/esm/components/JsonViewer.js +14 -2
  35. package/dist/esm/components/LogCard.js +422 -221
  36. package/dist/esm/constants/version.d.ts +1 -1
  37. package/dist/esm/constants/version.js +1 -1
  38. package/dist/esm/customHooks/analyticsLogger.js +1 -1
  39. package/dist/esm/customHooks/consoleLogger.js +34 -2
  40. package/dist/esm/customHooks/crashHandler.js +1 -1
  41. package/dist/esm/customHooks/networkLogger.js +184 -26
  42. package/dist/esm/helpers/searchQueryParser.d.ts +1 -1
  43. package/dist/esm/helpers/searchQueryParser.js +37 -291
  44. package/dist/esm/index.js +141 -27
  45. package/package.json +1 -1
  46. package/src/components/AnimatedEntrance.tsx +3 -3
  47. package/src/components/ConsoleLogCard.tsx +154 -103
  48. package/src/components/Inspector/ConsoleTab.tsx +27 -18
  49. package/src/components/Inspector/CrashTab.tsx +19 -5
  50. package/src/components/Inspector/DeviceInfoTab.tsx +224 -102
  51. package/src/components/Inspector/InspectorHeader.tsx +5 -3
  52. package/src/components/Inspector/MainScreen.tsx +2 -3
  53. package/src/components/Inspector/NetworkTab.tsx +1 -402
  54. package/src/components/Inspector/PerformanceTab.tsx +36 -7
  55. package/src/components/Inspector/ReduxTab.tsx +9 -1
  56. package/src/components/Inspector/StorageTab.tsx +27 -7
  57. package/src/components/JsonViewer.tsx +15 -2
  58. package/src/components/LogCard.tsx +528 -339
  59. package/src/constants/version.ts +1 -1
  60. package/src/customHooks/analyticsLogger.ts +1 -1
  61. package/src/customHooks/consoleLogger.ts +36 -3
  62. package/src/customHooks/crashHandler.ts +1 -1
  63. package/src/customHooks/networkLogger.ts +214 -26
  64. package/src/helpers/searchQueryParser.ts +40 -285
  65. package/src/index.tsx +332 -211
@@ -13,7 +13,7 @@ import { AppColors } from '../../styles/AppColors';
13
13
  import { AppFonts } from '../../styles/AppFonts';
14
14
  import { METHOD_COLORS } from '../../constants';
15
15
  import { useTranslation } from '../../i18n';
16
- import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, ClockIcon, GlobeIcon, RequestIcon, ResponseIcon, HeadersIcon, AtomIcon, BoltIcon, ShieldAlertIcon, } from '../NetworkIcons';
16
+ import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CircleCheckIcon, CircleXIcon, LayersIcon, HeaderPauseIcon, ClockIcon, RequestIcon, ResponseIcon, AtomIcon, } from '../NetworkIcons';
17
17
  const NetworkTab = React.memo(() => {
18
18
  const { groupedData, search, setSearch, searchScope, setSearchScope, isRegexSearch, setIsRegexSearch, isCaseSensitive, setIsCaseSensitive, quickFilter, setQuickFilter, handleDelete, selectedLogs, sortOrder, setSortOrder, statusFilters, setStatusFilters, methodFilters, setMethodFilters, availableMethods, filteredLogs, logs, toggleSectionFilter, toggleSectionCollapse, minStart, totalRange, newLogIds, toggleSelect, setSelected, isNetworkPaused, setIsNetworkPaused, } = useInspector();
19
19
  const { t } = useTranslation();
@@ -135,85 +135,6 @@ const NetworkTab = React.memo(() => {
135
135
  color: '#E10098',
136
136
  },
137
137
  ], [quickCounts]);
138
- const SEARCH_SCOPES = [
139
- {
140
- id: 'all',
141
- label: 'All',
142
- icon: color => <LayersIcon size={9.5} color={color}/>,
143
- },
144
- {
145
- id: 'url',
146
- label: 'URL / Path',
147
- icon: color => <GlobeIcon size={9.5} color={color}/>,
148
- },
149
- {
150
- id: 'reqBody',
151
- label: 'Payload',
152
- icon: color => <RequestIcon size={9.5} color={color}/>,
153
- },
154
- {
155
- id: 'resBody',
156
- label: 'Response',
157
- icon: color => <ResponseIcon size={9.5} color={color}/>,
158
- },
159
- {
160
- id: 'headers',
161
- label: 'Headers',
162
- icon: color => <HeadersIcon size={9.5} color={color}/>,
163
- },
164
- ];
165
- const QUICK_API_QUERY_TAGS = useMemo(() => [
166
- {
167
- label: 'Failed',
168
- query: 'is:error',
169
- icon: (color) => <CircleXIcon size={9.5} color={color}/>,
170
- },
171
- {
172
- label: 'Slow (>1s)',
173
- query: 'slow:>1s',
174
- icon: (color) => <ClockIcon size={9.5} color={color}/>,
175
- },
176
- {
177
- label: 'POST',
178
- query: 'm:POST',
179
- icon: (color) => <RequestIcon size={9.5} color={color}/>,
180
- },
181
- {
182
- label: 'GET',
183
- query: 'm:GET',
184
- icon: (color) => <ResponseIcon size={9.5} color={color}/>,
185
- },
186
- {
187
- label: 'GraphQL',
188
- query: 'is:graphql',
189
- icon: (color) => <AtomIcon size={9.5} color={color}/>,
190
- },
191
- {
192
- label: 'Axios',
193
- query: 'client:axios',
194
- icon: (color) => <BoltIcon size={9.5} color={color}/>,
195
- },
196
- {
197
- label: '200 OK',
198
- query: 'status:200',
199
- icon: (color) => <CircleCheckIcon size={9.5} color={color}/>,
200
- },
201
- {
202
- label: '404',
203
- query: 'status:404',
204
- icon: (color) => <CircleAlertIcon size={9.5} color={color}/>,
205
- },
206
- {
207
- label: '500',
208
- query: 'status:500',
209
- icon: (color) => <CircleXIcon size={9.5} color={color}/>,
210
- },
211
- {
212
- label: 'HTTPS',
213
- query: 'is:https',
214
- icon: (color) => <ShieldAlertIcon size={9.5} color={color}/>,
215
- },
216
- ], []);
217
138
  const networkMetrics = useMemo(() => {
218
139
  if (!logs || logs.length === 0)
219
140
  return null;
@@ -359,61 +280,7 @@ const NetworkTab = React.memo(() => {
359
280
  <SearchIcon color={isSearchFocused
360
281
  ? AppColors.purple
361
282
  : AppColors.grayTextWeak} size={15}/>
362
- <TextInput placeholder="Search url, body, is:error, slow:>1s..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
363
-
364
-
365
- <View style={{
366
- flexDirection: 'row',
367
- alignItems: 'center',
368
- gap: 3,
369
- marginRight: 2,
370
- }}>
371
- <Pressable hitSlop={8} onPress={() => setIsCaseSensitive(prev => !prev)} style={{
372
- paddingHorizontal: 4.5,
373
- paddingVertical: 2,
374
- borderRadius: 4,
375
- backgroundColor: isCaseSensitive
376
- ? AppColors.purple
377
- : 'transparent',
378
- borderWidth: 1,
379
- borderColor: isCaseSensitive
380
- ? AppColors.purple
381
- : `${AppColors.grayBorderSecondary}`,
382
- }}>
383
- <Text style={{
384
- fontFamily: AppFonts.interBold,
385
- fontSize: 9.5,
386
- color: isCaseSensitive
387
- ? AppColors.white
388
- : AppColors.grayTextWeak,
389
- }}>
390
- Aa
391
- </Text>
392
- </Pressable>
393
-
394
- <Pressable hitSlop={8} onPress={() => setIsRegexSearch(prev => !prev)} style={{
395
- paddingHorizontal: 4.5,
396
- paddingVertical: 2,
397
- borderRadius: 4,
398
- backgroundColor: isRegexSearch
399
- ? AppColors.purple
400
- : 'transparent',
401
- borderWidth: 1,
402
- borderColor: isRegexSearch
403
- ? AppColors.purple
404
- : `${AppColors.grayBorderSecondary}`,
405
- }}>
406
- <Text style={{
407
- fontFamily: AppFonts.interBold,
408
- fontSize: 9.5,
409
- color: isRegexSearch
410
- ? AppColors.white
411
- : AppColors.grayTextWeak,
412
- }}>
413
- .*
414
- </Text>
415
- </Pressable>
416
- </View>
283
+ <TextInput placeholder="Search URL, status, method, headers, body..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
417
284
 
418
285
  {search.length > 0 && (<View style={{
419
286
  flexDirection: 'row',
@@ -489,94 +356,6 @@ const NetworkTab = React.memo(() => {
489
356
  </View>
490
357
 
491
358
 
492
- <View style={{ paddingBottom: 4 }}>
493
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
494
- paddingHorizontal: 12,
495
- flexDirection: 'row',
496
- alignItems: 'center',
497
- gap: 5,
498
- paddingVertical: 2,
499
- }}>
500
-
501
- {SEARCH_SCOPES.map(s => {
502
- const isActive = searchScope === s.id;
503
- return (<TouchableScale key={`scope_${s.id}`} onPress={() => setSearchScope(s.id)} hitSlop={6} style={{
504
- flexDirection: 'row',
505
- alignItems: 'center',
506
- gap: 3.5,
507
- paddingHorizontal: 7.5,
508
- paddingVertical: 3.5,
509
- borderRadius: 6,
510
- backgroundColor: isActive
511
- ? AppColors.purple
512
- : AppColors.grayBackground,
513
- borderWidth: 1,
514
- borderColor: isActive
515
- ? AppColors.purple
516
- : AppColors.grayBorderSecondary,
517
- }}>
518
- {s.icon(isActive ? AppColors.white : AppColors.grayTextWeak)}
519
- <Text style={{
520
- fontFamily: AppFonts.interBold,
521
- fontSize: 10,
522
- color: isActive ? AppColors.white : AppColors.grayText,
523
- }}>
524
- {s.label}
525
- </Text>
526
- </TouchableScale>);
527
- })}
528
-
529
- <View style={{
530
- width: 1,
531
- height: 14,
532
- backgroundColor: AppColors.dividerColor,
533
- marginHorizontal: 3,
534
- }}/>
535
-
536
-
537
- {QUICK_API_QUERY_TAGS.map(q => {
538
- const isActive = search.includes(q.query);
539
- return (<TouchableScale key={`quick_${q.query}`} onPress={() => {
540
- setSearch(prev => {
541
- if (prev.includes(q.query)) {
542
- return prev
543
- .replace(q.query, '')
544
- .replace(/\s+/g, ' ')
545
- .trim();
546
- }
547
- return `${prev} ${q.query}`.trim();
548
- });
549
- }} hitSlop={6} style={{
550
- flexDirection: 'row',
551
- alignItems: 'center',
552
- gap: 3.5,
553
- paddingHorizontal: 7.5,
554
- paddingVertical: 3.5,
555
- borderRadius: 6,
556
- backgroundColor: isActive
557
- ? `${AppColors.purple}20`
558
- : AppColors.grayBackground,
559
- borderWidth: 1,
560
- borderColor: isActive
561
- ? AppColors.purple
562
- : AppColors.grayBorderSecondary,
563
- }}>
564
- {q.icon(isActive ? AppColors.purple : AppColors.grayTextWeak)}
565
- <Text style={{
566
- fontFamily: AppFonts.interMedium,
567
- fontSize: 10,
568
- color: isActive
569
- ? AppColors.purple
570
- : AppColors.grayText,
571
- }}>
572
- {q.label}
573
- </Text>
574
- </TouchableScale>);
575
- })}
576
- </ScrollView>
577
- </View>
578
-
579
-
580
359
  <View style={{ marginBottom: 8 }}>
581
360
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
582
361
  paddingHorizontal: 12,
@@ -690,110 +469,6 @@ const NetworkTab = React.memo(() => {
690
469
  </View>
691
470
 
692
471
 
693
- {(isSearchFocused || search.length > 0) && (<View style={{
694
- flexDirection: 'row',
695
- alignItems: 'center',
696
- paddingHorizontal: 12,
697
- marginBottom: 6,
698
- gap: 6,
699
- }}>
700
- <Text style={{
701
- fontFamily: AppFonts.interBold,
702
- fontSize: 9.5,
703
- color: AppColors.grayTextWeak,
704
- textTransform: 'uppercase',
705
- letterSpacing: 0.5,
706
- }}>
707
- Scope:
708
- </Text>
709
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
710
- flexDirection: 'row',
711
- alignItems: 'center',
712
- gap: 5,
713
- }}>
714
- {SEARCH_SCOPES.map(s => {
715
- const isSelected = searchScope === s.id;
716
- const iconColor = isSelected ? AppColors.white : AppColors.grayText;
717
- return (<TouchableScale key={s.id} onPress={() => setSearchScope(s.id)}>
718
- <View style={{
719
- flexDirection: 'row',
720
- alignItems: 'center',
721
- gap: 4,
722
- paddingHorizontal: 7,
723
- paddingVertical: 2.5,
724
- borderRadius: 5,
725
- backgroundColor: isSelected
726
- ? AppColors.purple
727
- : `${AppColors.grayBorderSecondary}40`,
728
- borderWidth: 1,
729
- borderColor: isSelected
730
- ? AppColors.purple
731
- : AppColors.grayBorderSecondary,
732
- }}>
733
- {s.icon(iconColor)}
734
- <Text style={{
735
- fontFamily: AppFonts.interBold,
736
- fontSize: 9.5,
737
- color: isSelected
738
- ? AppColors.white
739
- : AppColors.grayText,
740
- }}>
741
- {s.label}
742
- </Text>
743
- </View>
744
- </TouchableScale>);
745
- })}
746
- </ScrollView>
747
- </View>)}
748
-
749
-
750
- {(isSearchFocused || search.length > 0) && (<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 8, maxHeight: 28 }} contentContainerStyle={{
751
- paddingHorizontal: 12,
752
- flexDirection: 'row',
753
- alignItems: 'center',
754
- gap: 5,
755
- }}>
756
- {QUICK_API_QUERY_TAGS.map(item => {
757
- const isSelected = search.includes(item.query);
758
- return (<TouchableScale key={item.query} onPress={() => {
759
- if (isSelected) {
760
- setSearch(prev => prev.replace(item.query, '').trim());
761
- }
762
- else {
763
- setSearch(prev => prev ? `${prev} ${item.query}`.trim() : item.query);
764
- }
765
- }}>
766
- <View style={{
767
- flexDirection: 'row',
768
- alignItems: 'center',
769
- gap: 4,
770
- paddingHorizontal: 7,
771
- paddingVertical: 2.5,
772
- borderRadius: 5,
773
- backgroundColor: isSelected
774
- ? AppColors.purple
775
- : `${AppColors.purple}14`,
776
- borderWidth: 1,
777
- borderColor: isSelected
778
- ? AppColors.purple
779
- : `${AppColors.purple}30`,
780
- }}>
781
- {item.icon(isSelected ? AppColors.white : AppColors.purple)}
782
- <Text style={{
783
- fontFamily: AppFonts.interBold,
784
- fontSize: 9.5,
785
- color: isSelected
786
- ? AppColors.white
787
- : AppColors.purple,
788
- }}>
789
- {item.label}
790
- </Text>
791
- </View>
792
- </TouchableScale>);
793
- })}
794
- </ScrollView>)}
795
-
796
-
797
472
  {(quickFilter !== 'all' ||
798
473
  search.trim().length > 0 ||
799
474
  statusFilters.size > 0 ||
@@ -833,15 +508,6 @@ const NetworkTab = React.memo(() => {
833
508
  </Text>
834
509
  </TouchableScale>
835
510
  </View>)}
836
-
837
- {(search ||
838
- statusFilters.size > 0 ||
839
- methodFilters.size > 0 ||
840
- quickFilter !== 'all') && (<Text style={styles.resultCount}>
841
- {filteredLogs.length === logs.length
842
- ? `${logs.length} requests`
843
- : `${filteredLogs.length} of ${logs.length} filtered requests`}
844
- </Text>)}
845
511
  </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 ||
846
512
  statusFilters.size > 0 ||
847
513
  methodFilters.size > 0 ||
@@ -148,10 +148,19 @@ const PerformanceTab = React.memo(() => {
148
148
  }, [isRecording]);
149
149
  const filteredEvents = useMemo(() => {
150
150
  return events.filter(e => {
151
- const matchSearch = !search ||
152
- e.label.toLowerCase().includes(search.toLowerCase()) ||
153
- e.detail.toLowerCase().includes(search.toLowerCase()) ||
154
- (e.source && e.source.toLowerCase().includes(search.toLowerCase()));
151
+ let matchSearch = true;
152
+ if (search && search.trim().length > 0) {
153
+ const queryTokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
154
+ const corpus = [
155
+ e.label || '',
156
+ e.detail || '',
157
+ e.source || '',
158
+ e.category || '',
159
+ e.severity || '',
160
+ e.bottleneckThread || '',
161
+ ].join(' ').toLowerCase();
162
+ matchSearch = queryTokens.every(tok => corpus.includes(tok));
163
+ }
155
164
  const matchFilter = filterCategory === 'ALL'
156
165
  ? true
157
166
  : filterCategory === 'JANKY'
@@ -776,10 +785,27 @@ const PerformanceTab = React.memo(() => {
776
785
 
777
786
  {filteredEvents.length === 0 ? (<View style={perfStyles.emptyCard}>
778
787
  <BoltIcon color={AppColors.amber500} size={30}/>
779
- <Text style={perfStyles.emptyTitle}>{t('performance.emptyTitle')}</Text>
788
+ <Text style={perfStyles.emptyTitle}>
789
+ {search.trim().length > 0 ? 'No Matching Events' : t('performance.emptyTitle')}
790
+ </Text>
780
791
  <Text style={perfStyles.emptySub}>
781
- {t('performance.emptySubtitle')}
792
+ {search.trim().length > 0
793
+ ? `No performance events match "${search}"`
794
+ : t('performance.emptySubtitle')}
782
795
  </Text>
796
+ {search.trim().length > 0 && (<TouchableScale onPress={() => setSearch('')} style={{
797
+ marginTop: 10,
798
+ paddingHorizontal: 14,
799
+ paddingVertical: 6,
800
+ borderRadius: 8,
801
+ backgroundColor: AppColors.grayBackground,
802
+ borderWidth: 1,
803
+ borderColor: AppColors.grayBorderSecondary,
804
+ }}>
805
+ <Text style={{ color: AppColors.brandPurple, fontSize: 12, fontFamily: AppFonts.interBold }}>
806
+ Clear Search
807
+ </Text>
808
+ </TouchableScale>)}
783
809
  </View>) : (filteredEvents.map(event => {
784
810
  const isOptimal = event.severity === 'optimal';
785
811
  const isWarning = event.severity === 'warning';
@@ -454,7 +454,9 @@ const ReduxTab = React.memo(() => {
454
454
  </View>
455
455
 
456
456
 
457
- <FlatList ref={listRef} data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.length > 0}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
457
+ <FlatList ref={listRef} data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.trim().length > 0} searchQuery={reduxSearch} onClearSearch={() => setReduxSearch('')} customTitle={reduxSearch.trim().length > 0 ? 'No Matching Slices' : 'No Redux State Detected'} customSub={reduxSearch.trim().length > 0
458
+ ? `No Redux slices match "${reduxSearch}"`
459
+ : 'Attach the Redux store or dispatch actions to see state slices.'}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
458
460
  reduxTabStyles.listContent,
459
461
  filteredSlices.length === 0 && { flexGrow: 1 },
460
462
  ]} keyboardShouldPersistTaps="handled"/>
@@ -232,8 +232,11 @@ export const StorageTab = React.memo(() => {
232
232
  const filteredEntries = useMemo(() => {
233
233
  if (!search.trim())
234
234
  return entries;
235
- const q = search.toLowerCase().trim();
236
- return entries.filter(e => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q));
235
+ const tokens = search.toLowerCase().trim().split(/\s+/).filter(Boolean);
236
+ return entries.filter(e => {
237
+ const corpus = `${e.key} ${e.value}`.toLowerCase();
238
+ return tokens.every(tok => corpus.includes(tok));
239
+ });
237
240
  }, [entries, search]);
238
241
  const totalBytes = useMemo(() => {
239
242
  return entries.reduce((sum, e) => sum + e.byteSize, 0);
@@ -371,13 +374,28 @@ export const StorageTab = React.memo(() => {
371
374
  <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
372
375
  </View>
373
376
  <Text style={styles.emptyTitle}>
374
- {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
377
+ {search.trim().length > 0
378
+ ? 'No matching keys found'
379
+ : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
375
380
  </Text>
376
381
  <Text style={styles.emptySubtitle}>
377
- {search
378
- ? 'Try modifying your search query'
382
+ {search.trim().length > 0
383
+ ? `No keys or values match "${search}"`
379
384
  : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
380
385
  </Text>
386
+ {search.trim().length > 0 && (<TouchableScale onPress={() => setSearch('')} style={{
387
+ marginTop: 12,
388
+ paddingHorizontal: 14,
389
+ paddingVertical: 6,
390
+ borderRadius: 8,
391
+ backgroundColor: AppColors.grayBackground,
392
+ borderWidth: 1,
393
+ borderColor: AppColors.grayBorderSecondary,
394
+ }}>
395
+ <Text style={{ color: AppColors.brandPurple, fontSize: 12, fontFamily: AppFonts.interBold }}>
396
+ Clear Search
397
+ </Text>
398
+ </TouchableScale>)}
381
399
  </View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>
382
400
 
383
401
  <TouchableScale onPress={() => {
@@ -111,10 +111,22 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth = 1
111
111
  <Text style={localStyles.tableCellValue}>{String(data)}</Text>
112
112
  </View>);
113
113
  }
114
- const keys = Object.keys(data);
114
+ let keys = Object.keys(data);
115
+ if (search && search.trim().length > 0) {
116
+ const q = search.trim().toLowerCase();
117
+ keys = keys.filter(key => {
118
+ const val = data[key];
119
+ const valStr = typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val);
120
+ return key.toLowerCase().includes(q) || valStr.toLowerCase().includes(q);
121
+ });
122
+ }
115
123
  if (keys.length === 0) {
116
124
  return (<View style={localStyles.emptyTable}>
117
- <Text style={localStyles.emptyTableText}>{t('network.jsonViewer.emptyTable')}</Text>
125
+ <Text style={localStyles.emptyTableText}>
126
+ {search && search.trim().length > 0
127
+ ? 'No matching keys or values found'
128
+ : t('network.jsonViewer.emptyTable')}
129
+ </Text>
118
130
  </View>);
119
131
  }
120
132
  return (<View style={localStyles.tableView}>