react-native-inapp-inspector 1.1.28 → 1.1.31

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 (77) hide show
  1. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +29 -80
  2. package/dist/commonjs/components/AnalyticsDetail.js +668 -108
  3. package/dist/commonjs/components/AppHeaderLogo.js +15 -15
  4. package/dist/commonjs/components/ConsoleLogCard.js +112 -66
  5. package/dist/commonjs/components/CopyButton.js +15 -1
  6. package/dist/commonjs/components/DomainHeader.js +64 -32
  7. package/dist/commonjs/components/HighlightText.js +7 -1
  8. package/dist/commonjs/components/Inspector/AnalyticsTab.js +332 -221
  9. package/dist/commonjs/components/Inspector/ConsoleTab.js +162 -56
  10. package/dist/commonjs/components/Inspector/InspectorHeader.js +70 -119
  11. package/dist/commonjs/components/Inspector/LogDetail.js +53 -17
  12. package/dist/commonjs/components/Inspector/MainScreen.js +4 -0
  13. package/dist/commonjs/components/Inspector/NetworkDetail.js +24 -24
  14. package/dist/commonjs/components/Inspector/NetworkTab.js +4 -1
  15. package/dist/commonjs/components/Inspector/ReduxDetail.js +237 -42
  16. package/dist/commonjs/components/Inspector/ReduxTab.js +456 -35
  17. package/dist/commonjs/components/JsonViewer.js +26 -4
  18. package/dist/commonjs/components/LogCard.js +103 -11
  19. package/dist/commonjs/components/NetworkIcons.js +19 -10
  20. package/dist/commonjs/components/Toast.d.ts +3 -0
  21. package/dist/commonjs/components/Toast.js +152 -0
  22. package/dist/commonjs/components/TouchableScale.d.ts +1 -0
  23. package/dist/commonjs/components/TouchableScale.js +3 -3
  24. package/dist/commonjs/components/TreeNode.js +12 -2
  25. package/dist/commonjs/constants/index.js +6 -6
  26. package/dist/commonjs/constants/version.d.ts +1 -1
  27. package/dist/commonjs/constants/version.js +1 -1
  28. package/dist/commonjs/customHooks/reduxLogger.js +128 -5
  29. package/dist/commonjs/helpers/index.d.ts +4 -0
  30. package/dist/commonjs/helpers/index.js +107 -92
  31. package/dist/commonjs/helpers/toast.d.ts +4 -0
  32. package/dist/commonjs/helpers/toast.js +20 -0
  33. package/dist/commonjs/i18n/locales/en.json +49 -3
  34. package/dist/commonjs/styles/AppColors.d.ts +6 -0
  35. package/dist/commonjs/styles/AppColors.js +6 -0
  36. package/dist/commonjs/styles/index.d.ts +82 -15
  37. package/dist/commonjs/styles/index.js +92 -26
  38. package/dist/commonjs/types/interfaces.d.ts +7 -0
  39. package/dist/esm/components/AnalyticsDetail.js +670 -110
  40. package/dist/esm/components/AppHeaderLogo.js +15 -15
  41. package/dist/esm/components/ConsoleLogCard.js +114 -68
  42. package/dist/esm/components/CopyButton.js +15 -1
  43. package/dist/esm/components/DomainHeader.js +64 -32
  44. package/dist/esm/components/HighlightText.js +7 -1
  45. package/dist/esm/components/Inspector/AnalyticsTab.js +334 -223
  46. package/dist/esm/components/Inspector/ConsoleTab.js +163 -57
  47. package/dist/esm/components/Inspector/InspectorHeader.js +71 -120
  48. package/dist/esm/components/Inspector/LogDetail.js +55 -19
  49. package/dist/esm/components/Inspector/MainScreen.js +4 -0
  50. package/dist/esm/components/Inspector/NetworkDetail.js +25 -25
  51. package/dist/esm/components/Inspector/NetworkTab.js +4 -1
  52. package/dist/esm/components/Inspector/ReduxDetail.js +239 -44
  53. package/dist/esm/components/Inspector/ReduxTab.js +458 -37
  54. package/dist/esm/components/JsonViewer.js +26 -4
  55. package/dist/esm/components/LogCard.js +105 -13
  56. package/dist/esm/components/NetworkIcons.js +19 -10
  57. package/dist/esm/components/Toast.d.ts +3 -0
  58. package/dist/esm/components/Toast.js +117 -0
  59. package/dist/esm/components/TouchableScale.d.ts +1 -0
  60. package/dist/esm/components/TouchableScale.js +3 -3
  61. package/dist/esm/components/TreeNode.js +12 -2
  62. package/dist/esm/constants/index.js +6 -6
  63. package/dist/esm/constants/version.d.ts +1 -1
  64. package/dist/esm/constants/version.js +1 -1
  65. package/dist/esm/customHooks/reduxLogger.js +128 -5
  66. package/dist/esm/helpers/index.d.ts +4 -0
  67. package/dist/esm/helpers/index.js +102 -92
  68. package/dist/esm/helpers/toast.d.ts +4 -0
  69. package/dist/esm/helpers/toast.js +15 -0
  70. package/dist/esm/i18n/locales/en.json +49 -3
  71. package/dist/esm/styles/AppColors.d.ts +6 -0
  72. package/dist/esm/styles/AppColors.js +6 -0
  73. package/dist/esm/styles/index.d.ts +82 -15
  74. package/dist/esm/styles/index.js +92 -26
  75. package/dist/esm/types/interfaces.d.ts +7 -0
  76. package/ios/NetworkInspectorModule.m +0 -10
  77. package/package.json +13 -10
@@ -1,55 +1,234 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react';
2
2
  import { FlatList, Pressable, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
3
+ import { useTranslation } from '../../i18n';
3
4
  import { useInspector } from './InspectorContext';
4
5
  import TouchableScale from '../TouchableScale';
5
6
  import AnimatedEntrance from '../AnimatedEntrance';
6
7
  import EmptyState from '../EmptyState';
7
8
  import EndOfListFooter from '../EndOfListFooter';
8
9
  import HighlightText from '../HighlightText';
9
- import { getSize } from '../../helpers';
10
+ import { getSize, openInVSCode } from '../../helpers';
10
11
  import styles from '../../styles';
11
12
  import { AppColors } from '../../styles/AppColors';
12
13
  import { AppFonts } from '../../styles/AppFonts';
13
- import { TerminalIcon, SearchIcon, ClearIcon, ClockIcon, ForwardChevronIcon, SortArrowIcon, BoltIcon, TextAaIcon, } from '../NetworkIcons';
14
+ import { getActionHistory, getReduxAutoRefresh, setReduxAutoRefresh, } from '../../customHooks/reduxLogger';
15
+ import { TerminalIcon, SearchIcon, ClearIcon, LayersIcon, ClockIcon, ForwardChevronIcon, SortArrowIcon, BoltIcon, TextAaIcon, HeaderPauseIcon, } from '../NetworkIcons';
16
+ const getOriginBadge = (originType) => {
17
+ switch (originType) {
18
+ case 'saga':
19
+ return {
20
+ label: 'SAGA',
21
+ icon: '⚡',
22
+ bg: AppColors.purple100,
23
+ text: AppColors.brandPurple,
24
+ border: AppColors.purple200,
25
+ };
26
+ case 'thunk':
27
+ return {
28
+ label: 'THUNK',
29
+ icon: '⚛️',
30
+ bg: AppColors.amber100,
31
+ text: AppColors.amber800Warm,
32
+ border: AppColors.amber200,
33
+ };
34
+ case 'ui':
35
+ return {
36
+ label: 'UI',
37
+ icon: '📱',
38
+ bg: AppColors.sky100,
39
+ text: AppColors.sky600,
40
+ border: AppColors.sky400,
41
+ };
42
+ case 'listener':
43
+ return {
44
+ label: 'LISTENER',
45
+ icon: '👂',
46
+ bg: AppColors.teal100,
47
+ text: AppColors.teal700,
48
+ border: AppColors.teal400,
49
+ };
50
+ default:
51
+ return {
52
+ label: 'DIRECT',
53
+ icon: '⚡',
54
+ bg: AppColors.slate100,
55
+ text: AppColors.slate700,
56
+ border: AppColors.slate200,
57
+ };
58
+ }
59
+ };
60
+ const getKeyChipStyle = (key) => {
61
+ const k = key.toLowerCase();
62
+ if (k.includes('load') || k.includes('pend') || k.includes('status')) {
63
+ return {
64
+ bg: AppColors.amber100,
65
+ border: AppColors.amber200,
66
+ text: AppColors.amber800Warm,
67
+ dot: AppColors.darkOrange,
68
+ };
69
+ }
70
+ if (k.includes('err') || k.includes('fail') || k.includes('reject')) {
71
+ return {
72
+ bg: AppColors.red100,
73
+ border: AppColors.errorBorder,
74
+ text: AppColors.red600,
75
+ dot: AppColors.errorColor,
76
+ };
77
+ }
78
+ if (k.includes('user') || k.includes('auth') || k.includes('token') || k.includes('profile')) {
79
+ return {
80
+ bg: AppColors.indigo50,
81
+ border: AppColors.indigo400,
82
+ text: AppColors.indigo600Alt,
83
+ dot: AppColors.indigo500,
84
+ };
85
+ }
86
+ if (k.includes('item') || k.includes('data') || k.includes('list') || k.includes('entit') || k.includes('count')) {
87
+ return {
88
+ bg: AppColors.emeraldBg,
89
+ border: AppColors.emeraldBorder,
90
+ text: AppColors.emerald700,
91
+ dot: AppColors.emerald500,
92
+ };
93
+ }
94
+ return {
95
+ bg: AppColors.slate100,
96
+ border: AppColors.slate200,
97
+ text: AppColors.slate700,
98
+ dot: AppColors.slate400,
99
+ };
100
+ };
14
101
  const ReduxTab = React.memo(() => {
102
+ const { t } = useTranslation();
15
103
  const { reduxState, reduxLastActionMap, reduxSearch, setReduxSearch, setSelectedReduxSlice, } = useInspector();
16
104
  // Sort mode: 'latest' (newest updated first) vs 'alpha' (A-Z)
17
105
  const [sortMode, setSortMode] = useState('latest');
106
+ // Pause / Live Auto-Refresh State
107
+ const [isReduxPaused, setIsReduxPaused] = useState(!getReduxAutoRefresh());
108
+ const handleTogglePause = useCallback(() => {
109
+ setIsReduxPaused(prev => {
110
+ const next = !prev;
111
+ setReduxAutoRefresh(!next);
112
+ return next;
113
+ });
114
+ }, []);
18
115
  // Total state size calculation
19
116
  const totalStateSize = useMemo(() => {
20
117
  return getSize(reduxState);
21
118
  }, [reduxState]);
22
- // Compute slice items with exact timestamps
119
+ // Fetch full action history for timeline counts
120
+ const allActions = useMemo(() => {
121
+ return getActionHistory();
122
+ }, [reduxLastActionMap]);
123
+ // Compute slice items with exact timestamps & rich details
23
124
  const sliceItems = useMemo(() => {
24
125
  if (!reduxState || typeof reduxState !== 'object')
25
126
  return [];
26
127
  const keys = Object.keys(reduxState);
27
128
  return keys.map(key => {
28
129
  const sliceVal = reduxState[key];
29
- const keysCount = sliceVal && typeof sliceVal === 'object'
30
- ? Object.keys(sliceVal).length
31
- : typeof sliceVal !== 'undefined'
32
- ? 1
33
- : 0;
34
- const lastAction = reduxLastActionMap[key];
35
- const updatedAt = lastAction?.updatedAt || 0;
130
+ const isArray = Array.isArray(sliceVal);
131
+ const isObject = sliceVal && typeof sliceVal === 'object';
132
+ let typeLabel = 'Primitive';
133
+ let keysCount = 0;
134
+ let topKeys = [];
135
+ let totalKeysCount = 0;
136
+ if (isArray) {
137
+ keysCount = sliceVal.length;
138
+ typeLabel = `Array [${keysCount}]`;
139
+ }
140
+ else if (isObject) {
141
+ const objKeys = Object.keys(sliceVal);
142
+ totalKeysCount = objKeys.length;
143
+ keysCount = totalKeysCount;
144
+ topKeys = objKeys.slice(0, 4);
145
+ typeLabel = `Object {${keysCount}}`;
146
+ }
147
+ else if (typeof sliceVal !== 'undefined') {
148
+ keysCount = 1;
149
+ typeLabel = typeof sliceVal;
150
+ }
151
+ // Determine state status
152
+ let status = 'live';
153
+ let statusMessage;
154
+ if (isObject) {
155
+ if (sliceVal.loading === true ||
156
+ sliceVal.isLoading === true ||
157
+ sliceVal.status === 'loading' ||
158
+ sliceVal.status === 'pending') {
159
+ status = 'loading';
160
+ statusMessage = 'Loading';
161
+ }
162
+ else if (sliceVal.error ||
163
+ sliceVal.hasError ||
164
+ sliceVal.status === 'error' ||
165
+ sliceVal.status === 'failed') {
166
+ status = 'error';
167
+ statusMessage = typeof sliceVal.error === 'string' ? sliceVal.error : 'Error';
168
+ }
169
+ else if (keysCount === 0) {
170
+ status = 'empty';
171
+ statusMessage = 'Empty';
172
+ }
173
+ }
174
+ const lastActionRaw = reduxLastActionMap[key];
175
+ const updatedAt = lastActionRaw?.updatedAt || 0;
176
+ // Count actions affecting this slice in timeline
177
+ const timelineCount = allActions.filter(action => {
178
+ if (!action.affectedSlices || action.affectedSlices.length === 0)
179
+ return true;
180
+ return action.affectedSlices.includes(key);
181
+ }).length;
182
+ let lastAction = undefined;
183
+ if (lastActionRaw) {
184
+ let payloadPreview;
185
+ if (lastActionRaw.payload !== undefined && lastActionRaw.payload !== null) {
186
+ try {
187
+ if (typeof lastActionRaw.payload === 'object') {
188
+ const pKeys = Object.keys(lastActionRaw.payload);
189
+ payloadPreview = `{ ${pKeys.slice(0, 3).join(', ')}${pKeys.length > 3 ? '...' : ''} }`;
190
+ }
191
+ else {
192
+ payloadPreview = String(lastActionRaw.payload);
193
+ }
194
+ }
195
+ catch { }
196
+ }
197
+ lastAction = {
198
+ type: lastActionRaw.type,
199
+ timestamp: lastActionRaw.timestamp,
200
+ updatedAt: lastActionRaw.updatedAt,
201
+ payloadPreview,
202
+ originType: lastActionRaw.originType,
203
+ callerFile: lastActionRaw.callerFile,
204
+ callerLine: lastActionRaw.callerLine,
205
+ callerCol: lastActionRaw.callerCol,
206
+ };
207
+ }
36
208
  return {
37
209
  id: `slice-${key}`,
38
210
  name: key,
39
211
  keysCount,
40
212
  sizeStr: getSize(sliceVal),
213
+ typeLabel,
214
+ topKeys,
215
+ totalKeysCount,
216
+ status,
217
+ statusMessage,
218
+ timelineCount,
41
219
  lastAction,
42
220
  updatedAt,
43
221
  };
44
222
  });
45
- }, [reduxState, reduxLastActionMap]);
223
+ }, [reduxState, reduxLastActionMap, allActions]);
46
224
  // Filter & Sort slices: by default, the most recently updated item is at the FIRST position
47
225
  const filteredSlices = useMemo(() => {
48
226
  let list = sliceItems;
49
227
  if (reduxSearch.trim().length > 0) {
50
228
  const q = reduxSearch.toLowerCase();
51
229
  list = list.filter(item => item.name.toLowerCase().includes(q) ||
52
- (item.lastAction && item.lastAction.type.toLowerCase().includes(q)));
230
+ (item.lastAction && item.lastAction.type.toLowerCase().includes(q)) ||
231
+ item.topKeys.some(k => k.toLowerCase().includes(q)));
53
232
  }
54
233
  return [...list].sort((a, b) => {
55
234
  if (sortMode === 'latest') {
@@ -82,7 +261,7 @@ const ReduxTab = React.memo(() => {
82
261
  reduxTabStyles.card,
83
262
  isRecentlyUpdated && reduxTabStyles.cardRecentlyUpdated,
84
263
  ]}>
85
- {/* Top row: Badge + Slice Name + Timestamp + Chevron */}
264
+ {/* Top row: Badge + Slice Name + Status + Timestamp + Chevron */}
86
265
  <View style={reduxTabStyles.cardHeader}>
87
266
  <View style={reduxTabStyles.sliceBadge}>
88
267
  <Text style={reduxTabStyles.sliceBadgeText}>SLICE</Text>
@@ -90,6 +269,18 @@ const ReduxTab = React.memo(() => {
90
269
 
91
270
  <HighlightText text={item.name} search={reduxSearch} style={reduxTabStyles.sliceName} highlightStyle={reduxTabStyles.highlight}/>
92
271
 
272
+ {/* Status Badge */}
273
+ {item.status === 'loading' ? (<View style={reduxTabStyles.loadingPill}>
274
+ <Text style={reduxTabStyles.loadingPillText}>⏳ Loading</Text>
275
+ </View>) : item.status === 'error' ? (<View style={reduxTabStyles.errorPill}>
276
+ <Text style={reduxTabStyles.errorPillText}>⚠️ Error</Text>
277
+ </View>) : item.status === 'empty' ? (<View style={reduxTabStyles.emptyPill}>
278
+ <Text style={reduxTabStyles.emptyPillText}>Empty</Text>
279
+ </View>) : (<View style={reduxTabStyles.livePill}>
280
+ <View style={reduxTabStyles.liveDot}/>
281
+ <Text style={reduxTabStyles.livePillText}>Live</Text>
282
+ </View>)}
283
+
93
284
  {item.lastAction?.timestamp && (<View style={reduxTabStyles.timePill}>
94
285
  <ClockIcon color={AppColors.purple} size={10}/>
95
286
  <Text style={reduxTabStyles.timeText}>
@@ -103,27 +294,116 @@ const ReduxTab = React.memo(() => {
103
294
  {/* Middle row: Stats pills */}
104
295
  <View style={reduxTabStyles.statsRow}>
105
296
  <View style={reduxTabStyles.pill}>
106
- <Text style={reduxTabStyles.pillLabel}>Keys:</Text>
107
- <Text style={reduxTabStyles.pillValue}>{item.keysCount}</Text>
297
+ <LayersIcon color={AppColors.grayTextWeak} size={11}/>
298
+ <Text style={reduxTabStyles.pillValue}>{item.typeLabel}</Text>
108
299
  </View>
109
300
  <View style={reduxTabStyles.pill}>
110
301
  <Text style={reduxTabStyles.pillLabel}>Size:</Text>
111
302
  <Text style={reduxTabStyles.pillValue}>{item.sizeStr}</Text>
112
303
  </View>
113
- <View style={reduxTabStyles.livePill}>
114
- <View style={reduxTabStyles.liveDot}/>
115
- <Text style={reduxTabStyles.livePillText}>Live</Text>
116
- </View>
304
+ {item.timelineCount > 0 && (<View style={reduxTabStyles.pill}>
305
+ <Text style={reduxTabStyles.pillLabel}>Actions:</Text>
306
+ <Text style={reduxTabStyles.pillValue}>{item.timelineCount}</Text>
307
+ </View>)}
117
308
  </View>
118
309
 
119
- {/* Bottom row: Last Dispatched Action */}
310
+ {/* Keys Preview Chips (if object has keys) */}
311
+ {item.topKeys.length > 0 && (<View style={reduxTabStyles.keysContainer}>
312
+ <Text style={reduxTabStyles.keysHeading}>Keys:</Text>
313
+ <View style={reduxTabStyles.keysWrap}>
314
+ {item.topKeys.map((k, kIdx) => {
315
+ const chipStyle = getKeyChipStyle(k);
316
+ return (<View key={kIdx} style={[
317
+ reduxTabStyles.keyChip,
318
+ {
319
+ backgroundColor: chipStyle.bg,
320
+ borderColor: chipStyle.border,
321
+ },
322
+ ]}>
323
+ <View style={{
324
+ width: 5,
325
+ height: 5,
326
+ borderRadius: 2.5,
327
+ backgroundColor: chipStyle.dot,
328
+ }}/>
329
+ <Text style={[
330
+ reduxTabStyles.keyChipText,
331
+ { color: chipStyle.text },
332
+ ]} numberOfLines={1}>
333
+ {k}
334
+ </Text>
335
+ </View>);
336
+ })}
337
+ {item.totalKeysCount > item.topKeys.length && (<View style={reduxTabStyles.keyMoreChip}>
338
+ <Text style={reduxTabStyles.keyMoreChipText}>
339
+ +{item.totalKeysCount - item.topKeys.length}
340
+ </Text>
341
+ </View>)}
342
+ </View>
343
+ </View>)}
344
+
345
+ {/* Bottom row: Last Dispatched Action with Origin & Trigger Line */}
120
346
  {item.lastAction && (<View style={reduxTabStyles.lastActionRow}>
121
- <Text style={reduxTabStyles.lastActionLabel}>Last Action:</Text>
122
- <HighlightText text={item.lastAction.type} search={reduxSearch} style={reduxTabStyles.lastActionValue} highlightStyle={reduxTabStyles.highlight} numberOfLines={1}/>
347
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5, flex: 1, minWidth: 0, flexWrap: 'wrap' }}>
348
+ <BoltIcon color={AppColors.brandPurple} size={12}/>
349
+ <Text style={reduxTabStyles.lastActionLabel}>{t('redux.last', 'Last')}:</Text>
350
+
351
+ {/* Origin Badge */}
352
+ {item.lastAction.originType && (<View style={{
353
+ flexDirection: 'row',
354
+ alignItems: 'center',
355
+ gap: 2.5,
356
+ backgroundColor: getOriginBadge(item.lastAction.originType).bg,
357
+ paddingHorizontal: 4.5,
358
+ paddingVertical: 1.5,
359
+ borderRadius: 3.5,
360
+ borderWidth: 1,
361
+ borderColor: getOriginBadge(item.lastAction.originType).border,
362
+ }}>
363
+ <Text style={{ fontSize: 8.5 }}>
364
+ {getOriginBadge(item.lastAction.originType).icon}
365
+ </Text>
366
+ <Text style={{
367
+ fontFamily: AppFonts.interBold,
368
+ fontSize: 8,
369
+ color: getOriginBadge(item.lastAction.originType).text,
370
+ letterSpacing: 0.3,
371
+ }}>
372
+ {getOriginBadge(item.lastAction.originType).label}
373
+ </Text>
374
+ </View>)}
375
+
376
+ <HighlightText text={item.lastAction.type} search={reduxSearch} style={reduxTabStyles.lastActionValue} highlightStyle={reduxTabStyles.highlight} numberOfLines={1}/>
377
+
378
+ {/* Triggered from file preview */}
379
+ {item.lastAction.callerFile && (<Pressable onPress={() => openInVSCode(item.lastAction.callerFile, item.lastAction.callerLine, item.lastAction.callerCol)} hitSlop={6} style={{
380
+ flexDirection: 'row',
381
+ alignItems: 'center',
382
+ gap: 2,
383
+ backgroundColor: AppColors.sky100,
384
+ paddingHorizontal: 4.5,
385
+ paddingVertical: 1.5,
386
+ borderRadius: 3,
387
+ borderWidth: 0.5,
388
+ borderColor: AppColors.sky400,
389
+ }}>
390
+ <Text style={{
391
+ fontFamily: AppFonts.interMedium,
392
+ fontSize: 8.5,
393
+ color: AppColors.sky600,
394
+ }}>
395
+ 📄 {item.lastAction.callerFile.split('/').pop()}:{item.lastAction.callerLine || 1}
396
+ </Text>
397
+ </Pressable>)}
398
+
399
+ {item.lastAction.payloadPreview && (<Text style={reduxTabStyles.payloadPreviewText} numberOfLines={1}>
400
+ {item.lastAction.payloadPreview}
401
+ </Text>)}
402
+ </View>
123
403
  </View>)}
124
404
  </TouchableScale>
125
405
  </AnimatedEntrance>);
126
- }, [reduxSearch, setSelectedReduxSlice]);
406
+ }, [reduxSearch, setSelectedReduxSlice, t]);
127
407
  if (!reduxState) {
128
408
  return (<View style={styles.emptyContainer}>
129
409
  <View style={styles.emptyIconWrap}>
@@ -148,8 +428,24 @@ const ReduxTab = React.memo(() => {
148
428
  <Text style={reduxTabStyles.statBigVal}>{totalStateSize}</Text>
149
429
  </View>
150
430
  <View style={reduxTabStyles.statDivider}/>
151
- <View style={[reduxTabStyles.statCol, { flex: 1.5 }]}>
152
- <Text style={reduxTabStyles.statHeading}>LAST ACTION</Text>
431
+ <View style={[reduxTabStyles.statCol, { flex: 1.6 }]}>
432
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
433
+ <Text style={reduxTabStyles.statHeading}>LAST ACTION</Text>
434
+ {lastGlobalAction?.originType && (<View style={{
435
+ backgroundColor: getOriginBadge(lastGlobalAction.originType).bg,
436
+ paddingHorizontal: 4,
437
+ paddingVertical: 1,
438
+ borderRadius: 3,
439
+ }}>
440
+ <Text style={{
441
+ fontFamily: AppFonts.interBold,
442
+ fontSize: 7.5,
443
+ color: getOriginBadge(lastGlobalAction.originType).text,
444
+ }}>
445
+ {getOriginBadge(lastGlobalAction.originType).label}
446
+ </Text>
447
+ </View>)}
448
+ </View>
153
449
  <Text style={reduxTabStyles.statLastAction} numberOfLines={1}>
154
450
  {lastGlobalAction ? lastGlobalAction.type : 'Initial'}
155
451
  </Text>
@@ -170,6 +466,19 @@ const ReduxTab = React.memo(() => {
170
466
  </Pressable>)}
171
467
  </View>
172
468
 
469
+ {/* Pause / Resume Live Updates Button */}
470
+ <TouchableOpacity style={[
471
+ styles.toolbarBtn,
472
+ isReduxPaused && {
473
+ backgroundColor: `${AppColors.darkOrange}1F`,
474
+ borderColor: AppColors.darkOrange,
475
+ },
476
+ ]} onPress={handleTogglePause} hitSlop={6}>
477
+ <HeaderPauseIcon isPaused={isReduxPaused} color={isReduxPaused
478
+ ? AppColors.darkOrange
479
+ : AppColors.grayTextStrong} size={16}/>
480
+ </TouchableOpacity>
481
+
173
482
  {/* Sort Button */}
174
483
  <TouchableOpacity style={[
175
484
  styles.toolbarBtn,
@@ -183,11 +492,16 @@ const ReduxTab = React.memo(() => {
183
492
  <Text style={reduxTabStyles.resultCount}>
184
493
  Showing {filteredSlices.length} of {sliceItems.length} state slices
185
494
  </Text>
186
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
187
- {sortMode === 'latest' ? (<BoltIcon color={AppColors.brandPurple} size={11}/>) : (<TextAaIcon color={AppColors.brandPurple} size={11}/>)}
188
- <Text style={reduxTabStyles.sortLabel}>
189
- {sortMode === 'latest' ? 'Newest Updates First' : 'Alphabetical (A-Z)'}
190
- </Text>
495
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
496
+ {isReduxPaused && (<View style={reduxTabStyles.pausedBannerPill}>
497
+ <Text style={reduxTabStyles.pausedBannerText}>PAUSED</Text>
498
+ </View>)}
499
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
500
+ {sortMode === 'latest' ? (<BoltIcon color={AppColors.brandPurple} size={11}/>) : (<TextAaIcon color={AppColors.brandPurple} size={11}/>)}
501
+ <Text style={reduxTabStyles.sortLabel}>
502
+ {sortMode === 'latest' ? 'Newest Updates First' : 'Alphabetical (A-Z)'}
503
+ </Text>
504
+ </View>
191
505
  </View>
192
506
  </View>
193
507
  </View>
@@ -390,17 +704,104 @@ const reduxTabStyles = StyleSheet.create({
390
704
  livePillText: {
391
705
  fontFamily: AppFonts.interBold,
392
706
  fontSize: 10,
393
- color: '#047857',
707
+ color: AppColors.emerald700,
394
708
  },
395
- lastActionRow: {
709
+ loadingPill: {
710
+ backgroundColor: AppColors.amber100,
711
+ paddingHorizontal: 6,
712
+ paddingVertical: 3,
713
+ borderRadius: 5,
714
+ borderWidth: 1,
715
+ borderColor: AppColors.amber200,
716
+ },
717
+ loadingPillText: {
718
+ fontFamily: AppFonts.interBold,
719
+ fontSize: 9.5,
720
+ color: AppColors.amber800Warm,
721
+ },
722
+ errorPill: {
723
+ backgroundColor: AppColors.red100,
724
+ paddingHorizontal: 6,
725
+ paddingVertical: 3,
726
+ borderRadius: 5,
727
+ borderWidth: 1,
728
+ borderColor: AppColors.errorBorder,
729
+ },
730
+ errorPillText: {
731
+ fontFamily: AppFonts.interBold,
732
+ fontSize: 9.5,
733
+ color: AppColors.red600,
734
+ },
735
+ emptyPill: {
736
+ backgroundColor: AppColors.grayBackground,
737
+ paddingHorizontal: 6,
738
+ paddingVertical: 3,
739
+ borderRadius: 5,
740
+ borderWidth: 1,
741
+ borderColor: AppColors.dividerColor,
742
+ },
743
+ emptyPillText: {
744
+ fontFamily: AppFonts.interMedium,
745
+ fontSize: 9.5,
746
+ color: AppColors.grayTextWeak,
747
+ },
748
+ keysContainer: {
396
749
  flexDirection: 'row',
397
750
  alignItems: 'center',
398
751
  gap: 6,
752
+ marginTop: 4,
753
+ marginBottom: 4,
754
+ },
755
+ keysHeading: {
756
+ fontFamily: AppFonts.interMedium,
757
+ fontSize: 10,
758
+ color: AppColors.grayTextWeak,
759
+ },
760
+ keysWrap: {
761
+ flexDirection: 'row',
762
+ flexWrap: 'wrap',
763
+ gap: 4,
764
+ flex: 1,
765
+ },
766
+ keyChip: {
767
+ flexDirection: 'row',
768
+ alignItems: 'center',
769
+ gap: 4,
770
+ backgroundColor: AppColors.slate100,
771
+ paddingHorizontal: 6,
772
+ paddingVertical: 2.5,
773
+ borderRadius: 5,
774
+ borderWidth: 1,
775
+ borderColor: AppColors.slate200,
776
+ maxWidth: 120,
777
+ },
778
+ keyChipText: {
779
+ fontFamily: AppFonts.interBold,
780
+ fontSize: 9.5,
781
+ color: AppColors.slate700,
782
+ },
783
+ keyMoreChip: {
784
+ backgroundColor: AppColors.purple100,
785
+ paddingHorizontal: 5,
786
+ paddingVertical: 2,
787
+ borderRadius: 4,
788
+ borderWidth: 1,
789
+ borderColor: AppColors.purple200,
790
+ },
791
+ keyMoreChipText: {
792
+ fontFamily: AppFonts.interBold,
793
+ fontSize: 9,
794
+ color: AppColors.brandPurple,
795
+ },
796
+ lastActionRow: {
797
+ flexDirection: 'row',
798
+ alignItems: 'center',
799
+ gap: 5,
399
800
  backgroundColor: AppColors.grayBackground,
400
801
  borderRadius: 6,
401
802
  paddingHorizontal: 8,
402
- paddingVertical: 4,
403
- marginTop: 2,
803
+ paddingVertical: 5,
804
+ marginTop: 4,
404
805
  borderWidth: 1,
405
806
  borderColor: AppColors.dividerColor,
406
807
  },
@@ -410,15 +811,35 @@ const reduxTabStyles = StyleSheet.create({
410
811
  color: AppColors.grayTextWeak,
411
812
  },
412
813
  lastActionValue: {
413
- flex: 1,
414
814
  fontFamily: AppFonts.interBold,
415
815
  fontSize: 11,
416
816
  color: AppColors.primaryBlack,
417
817
  },
818
+ payloadPreviewText: {
819
+ flex: 1,
820
+ fontFamily: AppFonts.interRegular,
821
+ fontSize: 10,
822
+ color: AppColors.grayTextWeak,
823
+ marginLeft: 4,
824
+ },
418
825
  highlight: {
419
- backgroundColor: '#FEF08A',
420
- color: '#854D0E',
826
+ backgroundColor: AppColors.yellow200,
827
+ color: AppColors.yellow800,
421
828
  fontFamily: AppFonts.interBold,
422
829
  },
830
+ pausedBannerPill: {
831
+ backgroundColor: AppColors.amber100,
832
+ paddingHorizontal: 6,
833
+ paddingVertical: 2,
834
+ borderRadius: 4,
835
+ borderWidth: 1,
836
+ borderColor: AppColors.amber200,
837
+ },
838
+ pausedBannerText: {
839
+ fontFamily: AppFonts.interBold,
840
+ fontSize: 9,
841
+ color: AppColors.amber800Warm,
842
+ letterSpacing: 0.5,
843
+ },
423
844
  });
424
845
  export default ReduxTab;
@@ -55,9 +55,9 @@ const JsonTableRow = React.memo(({ itemKey, val, search, }) => {
55
55
  </View>);
56
56
  });
57
57
  // ── JsonViewer Component ─────────────────────────────────────────────────────
58
- const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth, wrap, fullHeight = false, maxHeight, mode: externalMode, onModeChange, hideTabs = false, }) => {
58
+ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth = 1, wrap, fullHeight = false, maxHeight, mode: externalMode, onModeChange, hideTabs = false, }) => {
59
59
  const { t } = useTranslation();
60
- const [internalMode, setInternalMode] = useState('pretty');
60
+ const [internalMode, setInternalMode] = useState('raw');
61
61
  const mode = externalMode ?? internalMode;
62
62
  const setMode = (newMode) => {
63
63
  if (onModeChange) {
@@ -78,6 +78,10 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth, wr
78
78
  return String(data);
79
79
  }
80
80
  }, [data]);
81
+ const [showFullRaw, setShowFullRaw] = useState(false);
82
+ const RAW_LIMIT = 50000;
83
+ const isTruncated = rawText.length > RAW_LIMIT && !showFullRaw;
84
+ const displayRawText = isTruncated ? rawText.slice(0, RAW_LIMIT) : rawText;
81
85
  useEffect(() => {
82
86
  if (externalMode) {
83
87
  setInternalMode(externalMode);
@@ -148,7 +152,7 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth, wr
148
152
  <View style={contentWrapperStyle}>
149
153
  {mode === 'pretty' && (isEmpty ? (<View style={localStyles.emptyTable}>
150
154
  <Text style={localStyles.emptyTableText}>
151
- {t('network.emptyResponse')}
155
+ {t('network.jsonViewer.emptyPayload')}
152
156
  </Text>
153
157
  </View>) : (wrap ? (fullHeight ? (<ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1 }}>
154
158
  <View style={[styles.codeBlock, { width: '100%' }]}>{tree}</View>
@@ -163,12 +167,30 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth, wr
163
167
  </ScrollView>))))}
164
168
 
165
169
  {mode === 'raw' && (<ScrollView style={scrollStyle} contentContainerStyle={localStyles.rawContainer} showsVerticalScrollIndicator={true} nestedScrollEnabled={true}>
166
- <HighlightText text={rawText} search={search} detectLinks={true} style={localStyles.rawPlainText} highlightStyle={{
170
+ <HighlightText text={displayRawText} search={search} detectLinks={rawText.length < 20000} style={localStyles.rawPlainText} selectable={true} highlightStyle={{
167
171
  backgroundColor: AppColors.yellowHighlight,
168
172
  color: AppColors.primaryBlack,
169
173
  borderRadius: 3,
170
174
  paddingHorizontal: 2,
171
175
  }}/>
176
+ {isTruncated && (<TouchableOpacity onPress={() => setShowFullRaw(true)} style={{
177
+ marginTop: 10,
178
+ backgroundColor: `${AppColors.brandPurple}15`,
179
+ paddingVertical: 8,
180
+ paddingHorizontal: 12,
181
+ borderRadius: 6,
182
+ alignItems: 'center',
183
+ borderWidth: 1,
184
+ borderColor: `${AppColors.brandPurple}30`,
185
+ }}>
186
+ <Text style={{
187
+ fontFamily: AppFonts.interBold,
188
+ fontSize: 11,
189
+ color: AppColors.brandPurple,
190
+ }}>
191
+ Showing first 50 KB ({Math.round(rawText.length / 1024)} KB total) · Tap to load full
192
+ </Text>
193
+ </TouchableOpacity>)}
172
194
  </ScrollView>)}
173
195
 
174
196
  {mode === 'table' && (fullHeight ? (<ScrollView style={scrollStyle}>