react-native-inapp-inspector 1.1.2 → 1.1.3

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 (36) hide show
  1. package/README.md +14 -0
  2. package/dist/commonjs/components/ConsoleLogCard.js +18 -0
  3. package/dist/commonjs/components/LogCard.js +17 -0
  4. package/dist/commonjs/components/NetworkIcons.d.ts +9 -2
  5. package/dist/commonjs/components/NetworkIcons.js +59 -3
  6. package/dist/commonjs/constants/version.d.ts +1 -1
  7. package/dist/commonjs/constants/version.js +1 -1
  8. package/dist/commonjs/customHooks/reduxLogger.d.ts +21 -7
  9. package/dist/commonjs/customHooks/reduxLogger.js +147 -48
  10. package/dist/commonjs/customHooks/webViewLogger.js +13 -8
  11. package/dist/commonjs/helpers/settingsStore.d.ts +24 -0
  12. package/dist/commonjs/helpers/settingsStore.js +74 -0
  13. package/dist/commonjs/index.d.ts +1 -1
  14. package/dist/commonjs/index.js +572 -46
  15. package/dist/commonjs/styles/index.d.ts +17 -1
  16. package/dist/commonjs/styles/index.js +25 -3
  17. package/dist/commonjs/types/index.d.ts +4 -0
  18. package/dist/esm/components/ConsoleLogCard.js +18 -0
  19. package/dist/esm/components/LogCard.js +17 -0
  20. package/dist/esm/components/NetworkIcons.d.ts +9 -2
  21. package/dist/esm/components/NetworkIcons.js +51 -2
  22. package/dist/esm/constants/version.d.ts +1 -1
  23. package/dist/esm/constants/version.js +1 -1
  24. package/dist/esm/customHooks/reduxLogger.d.ts +21 -7
  25. package/dist/esm/customHooks/reduxLogger.js +145 -47
  26. package/dist/esm/customHooks/webViewLogger.js +13 -8
  27. package/dist/esm/helpers/settingsStore.d.ts +24 -0
  28. package/dist/esm/helpers/settingsStore.js +67 -0
  29. package/dist/esm/index.d.ts +1 -1
  30. package/dist/esm/index.js +570 -47
  31. package/dist/esm/styles/index.d.ts +17 -1
  32. package/dist/esm/styles/index.js +25 -3
  33. package/dist/esm/types/index.d.ts +4 -0
  34. package/example/ios/example.xcodeproj/project.pbxproj +0 -8
  35. package/example/package-lock.json +4 -3
  36. package/package.json +1 -1
@@ -177,7 +177,7 @@ export declare const getRawStyles: (colors: typeof AppColors) => {
177
177
  };
178
178
  scrollTopBtn: {
179
179
  position: string;
180
- top: number;
180
+ bottom: number;
181
181
  right: number;
182
182
  width: number;
183
183
  height: number;
@@ -236,6 +236,22 @@ export declare const getRawStyles: (colors: typeof AppColors) => {
236
236
  borderRadius: number;
237
237
  backgroundColor: string;
238
238
  };
239
+ fabShineClip: {
240
+ position: string;
241
+ width: number;
242
+ height: number;
243
+ borderRadius: number;
244
+ overflow: string;
245
+ };
246
+ fabShineStreak: {
247
+ position: string;
248
+ top: number;
249
+ width: number;
250
+ height: number;
251
+ transform: {
252
+ rotate: string;
253
+ }[];
254
+ };
239
255
  fab: {
240
256
  width: number;
241
257
  height: number;
@@ -188,10 +188,11 @@ const getRawStyles = (colors) => ({
188
188
  borderColor: 'rgba(255, 255, 255, 0.08)',
189
189
  },
190
190
  listContent: { paddingBottom: 12 },
191
+ // #2 — scroll-to-top button, always shown at the bottom right.
191
192
  scrollTopBtn: {
192
193
  position: 'absolute',
193
- top: 12,
194
- right: 16,
194
+ bottom: 25,
195
+ right: 15,
195
196
  width: 38,
196
197
  height: 38,
197
198
  borderRadius: 19,
@@ -237,6 +238,22 @@ const getRawStyles = (colors) => ({
237
238
  borderRadius: 30,
238
239
  backgroundColor: `${colors.purple}25`,
239
240
  },
241
+ // #4 — circular clipping mask for the FAB shine sweep.
242
+ fabShineClip: {
243
+ position: 'absolute',
244
+ width: 62,
245
+ height: 62,
246
+ borderRadius: 31,
247
+ overflow: 'hidden',
248
+ },
249
+ // #4 — diagonal light streak that sweeps across the launcher.
250
+ fabShineStreak: {
251
+ position: 'absolute',
252
+ top: -20,
253
+ width: 26,
254
+ height: 102,
255
+ transform: [{ rotate: '25deg' }],
256
+ },
240
257
  fab: {
241
258
  width: 56,
242
259
  height: 56,
@@ -1137,7 +1154,12 @@ const getRawStyles = (colors) => ({
1137
1154
  color: colors.grayTextWeak,
1138
1155
  fontSize: 11,
1139
1156
  },
1140
- diffBlock: { paddingTop: 12, paddingHorizontal: 12, paddingBottom: 16, gap: 4 },
1157
+ diffBlock: {
1158
+ paddingTop: 12,
1159
+ paddingHorizontal: 12,
1160
+ paddingBottom: 16,
1161
+ gap: 4,
1162
+ },
1141
1163
  diffAdded: {
1142
1164
  color: colors.greenColor,
1143
1165
  fontFamily: react_native_1.Platform.OS === 'ios' ? 'Menlo' : 'monospace',
@@ -7,6 +7,8 @@ export interface ConsoleLog {
7
7
  timestamp: number;
8
8
  caller?: string;
9
9
  sourceMethod?: 'log' | 'info' | 'warn' | 'error';
10
+ /** #9 — number of consecutive identical logs collapsed into this entry. */
11
+ duplicateCount?: number;
10
12
  }
11
13
  export interface AnalyticsEvent {
12
14
  id: number;
@@ -36,6 +38,8 @@ export interface NetworkLog {
36
38
  response?: unknown;
37
39
  requestHeaders?: Record<string, string>;
38
40
  responseHeaders?: Record<string, string>;
41
+ /** #9 — number of consecutive identical requests collapsed into this entry. */
42
+ duplicateCount?: number;
39
43
  }
40
44
  export interface RouteInfo {
41
45
  path: string;
@@ -281,6 +281,24 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
281
281
  : `Object{${Object.keys(jsonContent.data).length}}`}
282
282
  </Text>
283
283
  </View>)}
284
+ {/* #9 — collapsed duplicate counter */}
285
+ {'duplicateCount' in item &&
286
+ item.duplicateCount != null &&
287
+ item.duplicateCount > 1 && (<View style={[
288
+ styles.badge,
289
+ {
290
+ backgroundColor: 'rgba(104, 75, 155, 0.1)',
291
+ borderColor: 'rgba(104, 75, 155, 0.25)',
292
+ borderWidth: 1,
293
+ },
294
+ ]}>
295
+ <Text style={[
296
+ styles.badgeText,
297
+ { color: AppColors.purple, fontWeight: '700' },
298
+ ]}>
299
+ ×{item.duplicateCount}
300
+ </Text>
301
+ </View>)}
284
302
  {isAnalyticsError && (<View style={[
285
303
  styles.badge,
286
304
  { backgroundColor: `${AppColors.skyBlue}15` },
@@ -78,6 +78,23 @@ const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, t
78
78
  .json
79
79
  </Text>
80
80
  </View>)}
81
+
82
+ {/* #9 — collapsed duplicate counter */}
83
+ {item.duplicateCount != null && item.duplicateCount > 1 && (<View style={[
84
+ styles.chip,
85
+ {
86
+ backgroundColor: `${AppColors.purple}15`,
87
+ borderColor: `${AppColors.purple}30`,
88
+ marginLeft: 6,
89
+ },
90
+ ]}>
91
+ <Text style={[
92
+ styles.chipText,
93
+ { color: AppColors.purple, fontWeight: '700' },
94
+ ]}>
95
+ ×{item.duplicateCount}
96
+ </Text>
97
+ </View>)}
81
98
  </View>
82
99
 
83
100
  <View style={styles.cardBottomRow}>
@@ -40,5 +40,12 @@ export declare const HtmlIcon: ({ color, size }: any) => React.JSX.Element;
40
40
  export declare const CssIcon: ({ color, size }: any) => React.JSX.Element;
41
41
  export declare const JsIcon: ({ color, size }: any) => React.JSX.Element;
42
42
  export declare const EyeIcon: ({ color, size }: any) => React.JSX.Element;
43
- export declare const SettingsIcon: ({ color, size }: any) => React.JSX.Element;
44
- export declare const FolderIcon: ({ color, size }: any) => React.JSX.Element;
43
+ export declare const SettingsIcon: ({ color, size, }: any) => React.JSX.Element;
44
+ export declare const FolderIcon: ({ color, size, }: any) => React.JSX.Element;
45
+ export declare const WipeIcon: ({ color, size }: any) => React.JSX.Element;
46
+ export declare const LayersIcon: ({ color, size, }: any) => React.JSX.Element;
47
+ export declare const UserIcon: ({ color, size }: any) => React.JSX.Element;
48
+ export declare const InfoCircleIcon: ({ color, size, }: any) => React.JSX.Element;
49
+ export declare const WarningTriangleIcon: ({ color, size, }: any) => React.JSX.Element;
50
+ export declare const ErrorCircleIcon: ({ color, size, }: any) => React.JSX.Element;
51
+ export declare const TrendingUpIcon: ({ color, size, }: any) => React.JSX.Element;
@@ -237,14 +237,63 @@ export const EyeIcon = ({ color = AppColors.grayTextWeak, size = 14 }) => {
237
237
  <Circle cx="12" cy="12" r="3" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
238
238
  </Svg>);
239
239
  };
240
- export const SettingsIcon = ({ color = AppColors.grayTextWeak, size = 14 }) => {
240
+ export const SettingsIcon = ({ color = AppColors.grayTextWeak, size = 14, }) => {
241
241
  return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
242
242
  <Path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
243
243
  <Circle cx="12" cy="12" r="3" stroke={color} strokeWidth="2"/>
244
244
  </Svg>);
245
245
  };
246
- export const FolderIcon = ({ color = AppColors.grayTextWeak, size = 14 }) => {
246
+ export const FolderIcon = ({ color = AppColors.grayTextWeak, size = 14, }) => {
247
247
  return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
248
248
  <Path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
249
249
  </Svg>);
250
250
  };
251
+ // #3 — Broom/sweep "wipe" icon used by the header Clear-Everything button.
252
+ export const WipeIcon = ({ color = '#FFFFFF', size = 16 }) => {
253
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
254
+ {/* Handle */}
255
+ <Path d="M19.5 3.5L12.7 10.3" stroke={color} strokeWidth="2" strokeLinecap="round"/>
256
+ {/* Brush head */}
257
+ <Path d="M13.5 9.5l1.5 1.5c.8.8.8 2 0 2.8L10 19l-5-5 5.2-5.5c.8-.8 2-.8 2.8 0z" stroke={color} strokeWidth="2" strokeLinejoin="round"/>
258
+ {/* Bristle strokes */}
259
+ <Path d="M7.5 16.5L5.5 18.5M10 19l-1.5 1.5" stroke={color} strokeWidth="2" strokeLinecap="round"/>
260
+ {/* Dust sparks */}
261
+ <Path d="M3 11.5h.01M5.5 8.5h.01" stroke={color} strokeWidth="2.4" strokeLinecap="round"/>
262
+ </Svg>);
263
+ };
264
+ // #7 — Icons for the inner filter chips / sub tabs.
265
+ export const LayersIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => {
266
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
267
+ <Path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
268
+ </Svg>);
269
+ };
270
+ export const UserIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => {
271
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
272
+ <Path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
273
+ <Circle cx="12" cy="7" r="4" stroke={color} strokeWidth="2"/>
274
+ </Svg>);
275
+ };
276
+ export const InfoCircleIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => {
277
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
278
+ <Circle cx="12" cy="12" r="10" stroke={color} strokeWidth="2"/>
279
+ <Path d="M12 16v-4M12 8h.01" stroke={color} strokeWidth="2" strokeLinecap="round"/>
280
+ </Svg>);
281
+ };
282
+ export const WarningTriangleIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => {
283
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
284
+ <Path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
285
+ <Path d="M12 9v4M12 17h.01" stroke={color} strokeWidth="2" strokeLinecap="round"/>
286
+ </Svg>);
287
+ };
288
+ export const ErrorCircleIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => {
289
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
290
+ <Circle cx="12" cy="12" r="10" stroke={color} strokeWidth="2"/>
291
+ <Path d="M15 9l-6 6M9 9l6 6" stroke={color} strokeWidth="2" strokeLinecap="round"/>
292
+ </Svg>);
293
+ };
294
+ export const TrendingUpIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => {
295
+ return (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
296
+ <Path d="M23 6l-9.5 9.5-5-5L1 18" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
297
+ <Path d="M17 6h6v6" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
298
+ </Svg>);
299
+ };
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.1.2";
1
+ export declare const LIB_VERSION = "1.1.3";
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED FILE — do not edit by hand.
2
2
  // Regenerated from package.json on every build by scripts/gen-version.js.
3
- export const LIB_VERSION = '1.1.2';
3
+ export const LIB_VERSION = '1.1.3';
@@ -1,16 +1,30 @@
1
- export declare const getReduxState: () => any;
2
- export declare const setReduxAutoRefresh: (val: boolean) => void;
3
- export declare const getReduxAutoRefresh: () => boolean;
4
- export declare const getLastActionForReducer: () => Record<string, any>;
5
- export declare const clearLastActionForReducer: () => void;
6
- export declare const getActionHistory: () => {
1
+ export interface ReduxHistoryEntry {
7
2
  id: number;
8
3
  type: string;
9
4
  payload: any;
10
5
  timestamp: string;
11
6
  affectedSlices: string[];
12
- }[];
7
+ }
8
+ export declare const getReduxState: () => any;
9
+ export declare const setReduxAutoRefresh: (val: boolean) => void;
10
+ export declare const getReduxAutoRefresh: () => boolean;
11
+ export declare const getLastActionForReducer: () => Record<string, any>;
12
+ export declare const clearLastActionForReducer: () => void;
13
+ export declare const getActionHistory: () => ReduxHistoryEntry[];
13
14
  export declare const clearActionHistory: () => void;
14
15
  export declare const setReduxState: (state: any) => void;
15
16
  export declare const subscribeReduxState: (cb: () => void) => () => void;
17
+ /**
18
+ * Standard Redux middleware — add it to your store to capture every action,
19
+ * including those dispatched from thunks, sagas and RTK Query:
20
+ *
21
+ * const store = configureStore({
22
+ * reducer,
23
+ * middleware: gDM => gDM().concat(inspectorReduxMiddleware),
24
+ * });
25
+ *
26
+ * Pair with `connectReduxStore(store)` (safe — they de-duplicate) or rely on
27
+ * the middleware alone; the initial snapshot is taken on the first action.
28
+ */
29
+ export declare const inspectorReduxMiddleware: (storeApi: any) => (next: (action: any) => any) => (action: any) => any;
16
30
  export declare const connectReduxStore: (store: any) => void;
@@ -1,8 +1,28 @@
1
+ // #8 — Redux inspector module.
2
+ //
3
+ // Two integration paths, both feeding the same timeline/state snapshot:
4
+ //
5
+ // 1. `inspectorReduxMiddleware` (recommended) — a standard Redux middleware.
6
+ // Because it sits inside the middleware chain it sees EVERY action,
7
+ // including ones dispatched from thunks, sagas, listeners and RTK Query.
8
+ //
9
+ // 2. `connectReduxStore(store)` — zero-config fallback. It wraps the outer
10
+ // `store.dispatch` AND diffs state on `store.subscribe`, so even actions
11
+ // that bypass the wrapped dispatch (thunk/saga internals capture the raw
12
+ // dispatch reference at store-creation time) still update the state tree
13
+ // and per-reducer "last action" consistently.
1
14
  let currentReduxState = null;
2
15
  const listeners = new Set();
3
16
  let globalReduxAutoRefresh = true;
4
17
  let lastActionForReducer = {};
5
18
  let actionHistory = [];
19
+ const MAX_HISTORY = 50;
20
+ let historyIdSeq = 0;
21
+ // Guards against double-instrumentation (e.g. connectReduxStore called twice,
22
+ // or middleware + connect used together) which previously produced duplicate
23
+ // timeline entries and inconsistent counts.
24
+ const connectedStores = new WeakSet();
25
+ let middlewareAttached = false;
6
26
  export const getReduxState = () => currentReduxState;
7
27
  export const setReduxAutoRefresh = (val) => {
8
28
  globalReduxAutoRefresh = val;
@@ -11,16 +31,16 @@ export const getReduxAutoRefresh = () => globalReduxAutoRefresh;
11
31
  export const getLastActionForReducer = () => lastActionForReducer;
12
32
  export const clearLastActionForReducer = () => {
13
33
  lastActionForReducer = {};
14
- listeners.forEach(cb => cb());
34
+ notify();
15
35
  };
16
36
  export const getActionHistory = () => actionHistory;
17
37
  export const clearActionHistory = () => {
18
38
  actionHistory = [];
19
- listeners.forEach(cb => cb());
39
+ notify();
20
40
  };
21
41
  export const setReduxState = (state) => {
22
42
  currentReduxState = state;
23
- listeners.forEach(cb => cb());
43
+ notify();
24
44
  };
25
45
  export const subscribeReduxState = (cb) => {
26
46
  listeners.add(cb);
@@ -28,6 +48,82 @@ export const subscribeReduxState = (cb) => {
28
48
  listeners.delete(cb);
29
49
  };
30
50
  };
51
+ function notify() {
52
+ listeners.forEach(cb => cb());
53
+ }
54
+ function actionTypeOf(action) {
55
+ if (typeof action === 'string')
56
+ return action;
57
+ if (action && typeof action === 'object' && action.type != null) {
58
+ return String(action.type);
59
+ }
60
+ if (typeof action === 'function') {
61
+ return action.name ? `thunk: ${action.name}` : 'thunk';
62
+ }
63
+ return 'UNKNOWN_ACTION';
64
+ }
65
+ function recordAction(action, prevState, nextState) {
66
+ const type = actionTypeOf(action);
67
+ const payload = action && typeof action === 'object' && action.payload !== undefined
68
+ ? action.payload
69
+ : null;
70
+ const timestamp = new Date().toLocaleTimeString();
71
+ const affectedSlices = [];
72
+ if (prevState &&
73
+ nextState &&
74
+ typeof prevState === 'object' &&
75
+ typeof nextState === 'object') {
76
+ Object.keys(nextState).forEach(key => {
77
+ if (prevState[key] !== nextState[key]) {
78
+ lastActionForReducer[key] = { type, payload, timestamp };
79
+ affectedSlices.push(key);
80
+ }
81
+ });
82
+ }
83
+ actionHistory.unshift({
84
+ id: ++historyIdSeq,
85
+ type,
86
+ payload,
87
+ timestamp,
88
+ affectedSlices,
89
+ });
90
+ if (actionHistory.length > MAX_HISTORY) {
91
+ actionHistory.length = MAX_HISTORY;
92
+ }
93
+ if (globalReduxAutoRefresh) {
94
+ currentReduxState = nextState;
95
+ }
96
+ // Timeline / last-action map always changed — notify even when the state
97
+ // tree snapshot is paused so those panels stay live.
98
+ notify();
99
+ }
100
+ /**
101
+ * Standard Redux middleware — add it to your store to capture every action,
102
+ * including those dispatched from thunks, sagas and RTK Query:
103
+ *
104
+ * const store = configureStore({
105
+ * reducer,
106
+ * middleware: gDM => gDM().concat(inspectorReduxMiddleware),
107
+ * });
108
+ *
109
+ * Pair with `connectReduxStore(store)` (safe — they de-duplicate) or rely on
110
+ * the middleware alone; the initial snapshot is taken on the first action.
111
+ */
112
+ export const inspectorReduxMiddleware = (storeApi) => (next) => (action) => {
113
+ middlewareAttached = true;
114
+ // Thunks are functions — let them run; their inner plain-action dispatches
115
+ // pass back through this same middleware, so nothing is lost.
116
+ if (typeof action === 'function') {
117
+ return next(action);
118
+ }
119
+ const prevState = storeApi.getState();
120
+ const result = next(action);
121
+ const nextState = storeApi.getState();
122
+ if (currentReduxState == null)
123
+ currentReduxState = nextState;
124
+ recordAction(action, prevState, nextState);
125
+ return result;
126
+ };
31
127
  export const connectReduxStore = (store) => {
32
128
  if (!store ||
33
129
  typeof store.getState !== 'function' ||
@@ -35,60 +131,62 @@ export const connectReduxStore = (store) => {
35
131
  console.warn('[NetworkInspector] Invalid Redux store passed to connectReduxStore.');
36
132
  return;
37
133
  }
38
- // Intercept dispatch calls to log actions and tie them to modified state slices
134
+ // Idempotent connecting the same store twice must not double-wrap
135
+ // dispatch or double-record the timeline.
136
+ if (connectedStores.has(store)) {
137
+ return;
138
+ }
139
+ connectedStores.add(store);
140
+ // Wrap the outer dispatch so directly dispatched actions get full
141
+ // type/payload attribution. Skipped when the middleware is already
142
+ // attached, otherwise every direct dispatch would be recorded twice.
39
143
  const originalDispatch = store.dispatch.bind(store);
144
+ let inWrappedDispatch = false;
40
145
  store.dispatch = (action) => {
41
- const prevState = store.getState();
42
- const result = originalDispatch(action);
43
- const nextState = store.getState();
44
- // Map the dispatched action to state slices that actually changed
45
- const affectedSlices = [];
46
- if (prevState &&
47
- nextState &&
48
- typeof prevState === 'object' &&
49
- typeof nextState === 'object' &&
50
- action &&
51
- typeof action === 'object') {
52
- Object.keys(nextState).forEach(key => {
53
- if (prevState[key] !== nextState[key]) {
54
- const actionObj = {
55
- type: action.type || 'UNKNOWN_ACTION',
56
- payload: action.payload !== undefined ? action.payload : null,
57
- timestamp: new Date().toLocaleTimeString(),
58
- };
59
- lastActionForReducer[key] = actionObj;
60
- affectedSlices.push(key);
61
- }
62
- });
63
- // Push to history
64
- actionHistory.unshift({
65
- id: Date.now() + Math.random(),
66
- type: action.type || 'UNKNOWN_ACTION',
67
- payload: action.payload !== undefined ? action.payload : null,
68
- timestamp: new Date().toLocaleTimeString(),
69
- affectedSlices,
70
- });
71
- // Cap size at 50
72
- if (actionHistory.length > 50) {
73
- actionHistory.pop();
146
+ if (middlewareAttached || typeof action === 'function') {
147
+ // Middleware handles recording, or it's a thunk whose inner dispatches
148
+ // will be picked up individually.
149
+ inWrappedDispatch = true;
150
+ try {
151
+ return originalDispatch(action);
152
+ }
153
+ finally {
154
+ inWrappedDispatch = false;
74
155
  }
75
156
  }
76
- if (globalReduxAutoRefresh) {
77
- // Refresh the displayed state tree snapshot.
78
- setReduxState(nextState);
157
+ const prevState = store.getState();
158
+ inWrappedDispatch = true;
159
+ let result;
160
+ try {
161
+ result = originalDispatch(action);
79
162
  }
80
- else {
81
- // Tree is paused, but the action timeline / last-action map still changed,
82
- // so notify subscribers to re-render those without moving the tree snapshot.
83
- listeners.forEach(cb => cb());
163
+ finally {
164
+ inWrappedDispatch = false;
84
165
  }
166
+ recordAction(action, prevState, store.getState());
85
167
  return result;
86
168
  };
87
169
  setReduxState(store.getState());
88
- // Listen to subscription for devtools updates or any other state changes
170
+ // Subscribe-diff fallback: catches state changes whose dispatch bypassed
171
+ // the wrapper above (thunk/saga internals hold the raw dispatch reference).
172
+ // Without this, the tree and per-reducer last-action drifted out of sync.
173
+ let lastSeenState = store.getState();
89
174
  store.subscribe(() => {
90
- if (globalReduxAutoRefresh) {
91
- setReduxState(store.getState());
175
+ const nextState = store.getState();
176
+ if (nextState === lastSeenState)
177
+ return;
178
+ const prevState = lastSeenState;
179
+ lastSeenState = nextState;
180
+ if (inWrappedDispatch || middlewareAttached) {
181
+ // Already recorded with proper attribution; just refresh the snapshot.
182
+ if (globalReduxAutoRefresh) {
183
+ currentReduxState = nextState;
184
+ notify();
185
+ }
186
+ return;
92
187
  }
188
+ // Change arrived outside the wrapped dispatch — record it so the
189
+ // timeline stays consistent, even without the original action type.
190
+ recordAction({ type: '@@inspector/EXTERNAL_STATE_CHANGE' }, prevState, nextState);
93
191
  });
94
192
  };
@@ -287,14 +287,19 @@ export const WebView = forwardRef((props, ref) => {
287
287
  onNavigationStateChange: handleNavigationStateChange,
288
288
  onLoadStart: handleLoadStart,
289
289
  onLoadEnd: handleLoadEnd,
290
- }), loading && showLoader && React.createElement(View, {
291
- style: {
292
- ...StyleSheet.absoluteFill,
293
- justifyContent: 'center',
294
- alignItems: 'center',
295
- backgroundColor: 'rgba(255, 255, 255, 0.4)',
296
- },
297
- }, React.createElement(ActivityIndicator, { size: 'large', color: '#684B9B' })));
290
+ }), loading &&
291
+ showLoader &&
292
+ React.createElement(View, {
293
+ style: {
294
+ ...StyleSheet.absoluteFill,
295
+ justifyContent: 'center',
296
+ alignItems: 'center',
297
+ backgroundColor: 'rgba(255, 255, 255, 0.4)',
298
+ },
299
+ }, React.createElement(ActivityIndicator, {
300
+ size: 'large',
301
+ color: '#684B9B',
302
+ })));
298
303
  });
299
304
  // Perform monkey-patching to intercept react-native-webview exports globally
300
305
  try {
@@ -0,0 +1,24 @@
1
+ export interface PersistedSettings {
2
+ isDark?: boolean;
3
+ modalHeightPercent?: number;
4
+ tabVisibility?: Record<string, boolean>;
5
+ defaultTab?: string;
6
+ maxNetworkLogs?: number;
7
+ maxConsoleLogs?: number;
8
+ showConsoleLevels?: {
9
+ info: boolean;
10
+ warn: boolean;
11
+ error: boolean;
12
+ };
13
+ webViewCaptureCssJs?: boolean;
14
+ reduxAutoRefresh?: boolean;
15
+ reduxExpandDepth?: number;
16
+ slowRequestThreshold?: number;
17
+ insightsShowConsoleAlerts?: boolean;
18
+ showDuplicateLogs?: boolean;
19
+ }
20
+ export declare function loadSettings(): Promise<PersistedSettings>;
21
+ /** Debounced save so rapid toggling doesn't hammer storage. */
22
+ export declare function saveSettings(settings: PersistedSettings): void;
23
+ export declare function clearPersistedSettings(): Promise<void>;
24
+ export declare const isPersistentStorageAvailable: () => boolean;
@@ -0,0 +1,67 @@
1
+ // #5 — Persistence layer for the inspector's settings selections.
2
+ //
3
+ // Backed by @react-native-async-storage/async-storage when the host app has it
4
+ // installed (most RN apps do), with a transparent in-memory fallback so this
5
+ // library never crashes and never forces a new native dependency.
6
+ let storage = null;
7
+ try {
8
+ // Optional dependency — resolved only if the host app already ships it.
9
+ const mod = require('@react-native-async-storage/async-storage');
10
+ storage = mod?.default ?? mod ?? null;
11
+ if (storage && typeof storage.getItem !== 'function')
12
+ storage = null;
13
+ }
14
+ catch {
15
+ storage = null;
16
+ }
17
+ // In-memory fallback (settings survive for the app session only).
18
+ const memory = new Map();
19
+ const SETTINGS_KEY = 'rn-inapp-inspector.settings.v1';
20
+ export async function loadSettings() {
21
+ try {
22
+ const raw = storage
23
+ ? await storage.getItem(SETTINGS_KEY)
24
+ : memory.get(SETTINGS_KEY) ?? null;
25
+ if (!raw)
26
+ return {};
27
+ const parsed = JSON.parse(raw);
28
+ return parsed && typeof parsed === 'object' ? parsed : {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ let saveTimer = null;
35
+ /** Debounced save so rapid toggling doesn't hammer storage. */
36
+ export function saveSettings(settings) {
37
+ if (saveTimer)
38
+ clearTimeout(saveTimer);
39
+ saveTimer = setTimeout(async () => {
40
+ try {
41
+ const raw = JSON.stringify(settings);
42
+ if (storage) {
43
+ await storage.setItem(SETTINGS_KEY, raw);
44
+ }
45
+ else {
46
+ memory.set(SETTINGS_KEY, raw);
47
+ }
48
+ }
49
+ catch {
50
+ // Persistence is best-effort — never crash the host app over it.
51
+ }
52
+ }, 250);
53
+ }
54
+ export async function clearPersistedSettings() {
55
+ try {
56
+ if (storage) {
57
+ await storage.removeItem(SETTINGS_KEY);
58
+ }
59
+ else {
60
+ memory.delete(SETTINGS_KEY);
61
+ }
62
+ }
63
+ catch {
64
+ // ignore
65
+ }
66
+ }
67
+ export const isPersistentStorageAvailable = () => storage != null;
@@ -6,4 +6,4 @@ export { setupConsoleLogger, clearConsoleLogs, subscribeConsoleLogs, } from './c
6
6
  export { setupAnalyticsLogger, logAnalyticsEvent, subscribeAnalyticsEvents, clearAnalyticsEvents, } from './customHooks/analyticsLogger';
7
7
  export { WebView, getWebViewLogs, getWebViewNavHistory, getWebViewHtml, getWebViewCss, getWebViewJs, getWebViewHtmlUrl, clearWebViewData, subscribeWebView, } from './customHooks/webViewLogger';
8
8
  export { default as ErrorBoundary } from './components/ErrorBoundary';
9
- export { connectReduxStore, getReduxState, subscribeReduxState, } from './customHooks/reduxLogger';
9
+ export { connectReduxStore, inspectorReduxMiddleware, getReduxState, subscribeReduxState, getActionHistory, clearActionHistory, } from './customHooks/reduxLogger';