react-native-network-lens 0.0.0-stage → 0.1.0

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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +207 -2
  3. package/lib/module/backHandler.js +31 -0
  4. package/lib/module/backHandler.js.map +1 -0
  5. package/lib/module/components/BodyViewer.js +207 -0
  6. package/lib/module/components/BodyViewer.js.map +1 -0
  7. package/lib/module/components/CodeList.js +104 -0
  8. package/lib/module/components/CodeList.js.map +1 -0
  9. package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
  10. package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
  11. package/lib/module/components/JsonTree/model.js +194 -0
  12. package/lib/module/components/JsonTree/model.js.map +1 -0
  13. package/lib/module/components/NetworkLens.js +110 -0
  14. package/lib/module/components/NetworkLens.js.map +1 -0
  15. package/lib/module/components/NetworkLensOverlay.js +158 -0
  16. package/lib/module/components/NetworkLensOverlay.js.map +1 -0
  17. package/lib/module/components/RawTextView.js +119 -0
  18. package/lib/module/components/RawTextView.js.map +1 -0
  19. package/lib/module/components/RequestDetails/RequestDetails.js +251 -0
  20. package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
  21. package/lib/module/components/RequestList/FilterBar.js +138 -0
  22. package/lib/module/components/RequestList/FilterBar.js.map +1 -0
  23. package/lib/module/components/RequestList/RequestList.js +163 -0
  24. package/lib/module/components/RequestList/RequestList.js.map +1 -0
  25. package/lib/module/components/RequestList/RequestRow.js +161 -0
  26. package/lib/module/components/RequestList/RequestRow.js.map +1 -0
  27. package/lib/module/components/RequestList/filters.js +57 -0
  28. package/lib/module/components/RequestList/filters.js.map +1 -0
  29. package/lib/module/components/ui/ActionSheet.js +94 -0
  30. package/lib/module/components/ui/ActionSheet.js.map +1 -0
  31. package/lib/module/components/ui/Button.js +90 -0
  32. package/lib/module/components/ui/Button.js.map +1 -0
  33. package/lib/module/components/ui/Pill.js +53 -0
  34. package/lib/module/components/ui/Pill.js.map +1 -0
  35. package/lib/module/components/ui/SearchInput.js +77 -0
  36. package/lib/module/components/ui/SearchInput.js.map +1 -0
  37. package/lib/module/components/ui/Section.js +147 -0
  38. package/lib/module/components/ui/Section.js.map +1 -0
  39. package/lib/module/components/ui/SegmentedControl.js +65 -0
  40. package/lib/module/components/ui/SegmentedControl.js.map +1 -0
  41. package/lib/module/components/ui/index.js +9 -0
  42. package/lib/module/components/ui/index.js.map +1 -0
  43. package/lib/module/core/Logger.js +230 -0
  44. package/lib/module/core/Logger.js.map +1 -0
  45. package/lib/module/core/NetworkRequest.js +155 -0
  46. package/lib/module/core/NetworkRequest.js.map +1 -0
  47. package/lib/module/core/XHRInterceptor.js +104 -0
  48. package/lib/module/core/XHRInterceptor.js.map +1 -0
  49. package/lib/module/core/har.js +72 -0
  50. package/lib/module/core/har.js.map +1 -0
  51. package/lib/module/core/loggerSingleton.js +12 -0
  52. package/lib/module/core/loggerSingleton.js.map +1 -0
  53. package/lib/module/hooks.js +42 -0
  54. package/lib/module/hooks.js.map +1 -0
  55. package/lib/module/index.js +34 -0
  56. package/lib/module/index.js.map +1 -0
  57. package/lib/module/package.json +1 -0
  58. package/lib/module/theme.js +136 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/module/utils/body.js +179 -0
  63. package/lib/module/utils/body.js.map +1 -0
  64. package/lib/module/utils/debounce.js +18 -0
  65. package/lib/module/utils/debounce.js.map +1 -0
  66. package/lib/module/utils/format.js +45 -0
  67. package/lib/module/utils/format.js.map +1 -0
  68. package/lib/module/utils/logger.js +6 -0
  69. package/lib/module/utils/logger.js.map +1 -0
  70. package/lib/module/utils/search.js +70 -0
  71. package/lib/module/utils/search.js.map +1 -0
  72. package/lib/module/utils/url.js +50 -0
  73. package/lib/module/utils/url.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/backHandler.d.ts +15 -0
  76. package/lib/typescript/src/backHandler.d.ts.map +1 -0
  77. package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
  78. package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
  79. package/lib/typescript/src/components/CodeList.d.ts +21 -0
  80. package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
  81. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
  82. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
  83. package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
  84. package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
  85. package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
  86. package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
  87. package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
  88. package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
  89. package/lib/typescript/src/components/RawTextView.d.ts +14 -0
  90. package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
  91. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
  92. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
  93. package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
  94. package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
  95. package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
  96. package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
  97. package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
  98. package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
  99. package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
  100. package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
  101. package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
  102. package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
  103. package/lib/typescript/src/components/ui/Button.d.ts +21 -0
  104. package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
  105. package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
  106. package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
  107. package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
  108. package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
  109. package/lib/typescript/src/components/ui/Section.d.ts +16 -0
  110. package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
  111. package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
  112. package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
  113. package/lib/typescript/src/components/ui/index.d.ts +7 -0
  114. package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
  115. package/lib/typescript/src/core/Logger.d.ts +48 -0
  116. package/lib/typescript/src/core/Logger.d.ts.map +1 -0
  117. package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
  118. package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
  119. package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
  120. package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
  121. package/lib/typescript/src/core/har.d.ts +65 -0
  122. package/lib/typescript/src/core/har.d.ts.map +1 -0
  123. package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
  124. package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
  125. package/lib/typescript/src/hooks.d.ts +11 -0
  126. package/lib/typescript/src/hooks.d.ts.map +1 -0
  127. package/lib/typescript/src/index.d.ts +33 -0
  128. package/lib/typescript/src/index.d.ts.map +1 -0
  129. package/lib/typescript/src/theme.d.ts +49 -0
  130. package/lib/typescript/src/theme.d.ts.map +1 -0
  131. package/lib/typescript/src/types.d.ts +80 -0
  132. package/lib/typescript/src/types.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/body.d.ts +19 -0
  134. package/lib/typescript/src/utils/body.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/debounce.d.ts +4 -0
  136. package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/format.d.ts +7 -0
  138. package/lib/typescript/src/utils/format.d.ts.map +1 -0
  139. package/lib/typescript/src/utils/logger.d.ts +2 -0
  140. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  141. package/lib/typescript/src/utils/search.d.ts +17 -0
  142. package/lib/typescript/src/utils/search.d.ts.map +1 -0
  143. package/lib/typescript/src/utils/url.d.ts +15 -0
  144. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  145. package/package.json +186 -4
  146. package/src/backHandler.ts +29 -0
  147. package/src/components/BodyViewer.tsx +240 -0
  148. package/src/components/CodeList.tsx +120 -0
  149. package/src/components/JsonTree/JsonTreeView.tsx +304 -0
  150. package/src/components/JsonTree/model.ts +264 -0
  151. package/src/components/NetworkLens.tsx +176 -0
  152. package/src/components/NetworkLensOverlay.tsx +187 -0
  153. package/src/components/RawTextView.tsx +163 -0
  154. package/src/components/RequestDetails/RequestDetails.tsx +313 -0
  155. package/src/components/RequestList/FilterBar.tsx +168 -0
  156. package/src/components/RequestList/RequestList.tsx +226 -0
  157. package/src/components/RequestList/RequestRow.tsx +150 -0
  158. package/src/components/RequestList/filters.ts +98 -0
  159. package/src/components/ui/ActionSheet.tsx +109 -0
  160. package/src/components/ui/Button.tsx +120 -0
  161. package/src/components/ui/Pill.tsx +53 -0
  162. package/src/components/ui/SearchInput.tsx +97 -0
  163. package/src/components/ui/Section.tsx +133 -0
  164. package/src/components/ui/SegmentedControl.tsx +79 -0
  165. package/src/components/ui/index.ts +6 -0
  166. package/src/core/Logger.ts +293 -0
  167. package/src/core/NetworkRequest.ts +216 -0
  168. package/src/core/XHRInterceptor.ts +179 -0
  169. package/src/core/har.ts +71 -0
  170. package/src/core/loggerSingleton.ts +14 -0
  171. package/src/hooks.ts +51 -0
  172. package/src/index.tsx +56 -0
  173. package/src/theme.ts +174 -0
  174. package/src/types.ts +83 -0
  175. package/src/utils/body.ts +194 -0
  176. package/src/utils/debounce.ts +18 -0
  177. package/src/utils/format.ts +50 -0
  178. package/src/utils/logger.ts +3 -0
  179. package/src/utils/search.ts +72 -0
  180. package/src/utils/url.ts +59 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Pure helpers that turn a JSON value into a flat list of visible rows, the way
3
+ * a browser devtools console renders objects. Keeping this free of React makes it
4
+ * easy to test and lets the UI virtualise the rows with a FlatList.
5
+ */
6
+
7
+ export type JsonValueType =
8
+ 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' | 'undefined';
9
+
10
+ export interface JsonRow {
11
+ /** Stable identifier: JSON-pointer-like path, e.g. `$.data.items[2].id` */
12
+ path: string;
13
+ key: string | null;
14
+ depth: number;
15
+ type: JsonValueType;
16
+ value: unknown;
17
+ expandable: boolean;
18
+ expanded: boolean;
19
+ childCount: number;
20
+ }
21
+
22
+ export const typeOf = (value: unknown): JsonValueType => {
23
+ if (value === null) return 'null';
24
+ if (Array.isArray(value)) return 'array';
25
+ const t = typeof value;
26
+ if (t === 'object') return 'object';
27
+ if (t === 'string') return 'string';
28
+ if (t === 'number' || t === 'bigint') return 'number';
29
+ if (t === 'boolean') return 'boolean';
30
+ return 'undefined';
31
+ };
32
+
33
+ export const isExpandable = (value: unknown): boolean => {
34
+ const type = typeOf(value);
35
+ if (type === 'array') return (value as unknown[]).length > 0;
36
+ if (type === 'object') return Object.keys(value as object).length > 0;
37
+ return false;
38
+ };
39
+
40
+ export const childEntries = (value: unknown): [string, unknown][] => {
41
+ if (Array.isArray(value))
42
+ return value.map((item, index) => [String(index), item]);
43
+ if (value && typeof value === 'object')
44
+ return Object.entries(value as object);
45
+ return [];
46
+ };
47
+
48
+ export const childPath = (
49
+ parent: string,
50
+ key: string,
51
+ parentIsArray: boolean
52
+ ) => (parentIsArray ? `${parent}[${key}]` : `${parent}.${key}`);
53
+
54
+ export const ROOT_PATH = '$';
55
+
56
+ /**
57
+ * Walks the value and returns the rows that are currently visible given the set
58
+ * of expanded paths.
59
+ */
60
+ export const flattenJson = (
61
+ value: unknown,
62
+ expanded: ReadonlySet<string>
63
+ ): JsonRow[] => {
64
+ const rows: JsonRow[] = [];
65
+ const visit = (
66
+ node: unknown,
67
+ key: string | null,
68
+ path: string,
69
+ depth: number
70
+ ) => {
71
+ const type = typeOf(node);
72
+ const expandable = isExpandable(node);
73
+ const isExpanded = expandable && expanded.has(path);
74
+ const children = expandable ? childEntries(node) : [];
75
+ rows.push({
76
+ path,
77
+ key,
78
+ depth,
79
+ type,
80
+ value: node,
81
+ expandable,
82
+ expanded: isExpanded,
83
+ childCount: children.length,
84
+ });
85
+ if (isExpanded) {
86
+ const isArray = type === 'array';
87
+ for (const [childKey, childValue] of children) {
88
+ visit(
89
+ childValue,
90
+ childKey,
91
+ childPath(path, childKey, isArray),
92
+ depth + 1
93
+ );
94
+ }
95
+ }
96
+ };
97
+ visit(value, null, ROOT_PATH, 0);
98
+ return rows;
99
+ };
100
+
101
+ /** Paths of every expandable node up to (and including) `maxDepth`. */
102
+ export const pathsToDepth = (value: unknown, maxDepth: number): Set<string> => {
103
+ const result = new Set<string>();
104
+ const visit = (node: unknown, path: string, depth: number) => {
105
+ if (!isExpandable(node)) return;
106
+ if (depth > maxDepth) return;
107
+ result.add(path);
108
+ const isArray = Array.isArray(node);
109
+ for (const [key, child] of childEntries(node)) {
110
+ visit(child, childPath(path, key, isArray), depth + 1);
111
+ }
112
+ };
113
+ visit(value, ROOT_PATH, 0);
114
+ return result;
115
+ };
116
+
117
+ export const allPaths = (value: unknown): Set<string> =>
118
+ pathsToDepth(value, Number.POSITIVE_INFINITY);
119
+
120
+ /** Paths of every expandable node in the whole tree below `path` (inclusive). */
121
+ export const subtreePaths = (value: unknown, path: string): Set<string> => {
122
+ const result = new Set<string>();
123
+ const visit = (node: unknown, nodePath: string) => {
124
+ if (!isExpandable(node)) return;
125
+ result.add(nodePath);
126
+ const isArray = Array.isArray(node);
127
+ for (const [key, child] of childEntries(node))
128
+ visit(child, childPath(nodePath, key, isArray));
129
+ };
130
+ visit(value, path);
131
+ return result;
132
+ };
133
+
134
+ /** Looks a node up by path, walking from the root. */
135
+ export const getAtPath = (root: unknown, path: string): unknown => {
136
+ if (path === ROOT_PATH) return root;
137
+ const segments = path
138
+ .slice(ROOT_PATH.length)
139
+ .split(/\.|\[|\]/)
140
+ .filter(Boolean);
141
+ let current: unknown = root;
142
+ for (const segment of segments) {
143
+ if (current === null || typeof current !== 'object') return undefined;
144
+ current = (current as Record<string, unknown>)[segment];
145
+ }
146
+ return current;
147
+ };
148
+
149
+ export interface SearchHit {
150
+ path: string;
151
+ /** Ancestor paths that must be expanded for the hit to be visible. */
152
+ ancestors: string[];
153
+ }
154
+
155
+ const primitiveText = (value: unknown): string => {
156
+ if (typeof value === 'string') return value;
157
+ if (value === null) return 'null';
158
+ if (value === undefined) return 'undefined';
159
+ return String(value);
160
+ };
161
+
162
+ /**
163
+ * Finds every node whose key or primitive value contains `query` (case-insensitive).
164
+ * Returns hits in document order along with the ancestors to expand.
165
+ */
166
+ export const searchJson = (value: unknown, query: string): SearchHit[] => {
167
+ const needle = query.trim().toLowerCase();
168
+ if (!needle) return [];
169
+ const hits: SearchHit[] = [];
170
+ const visit = (
171
+ node: unknown,
172
+ key: string | null,
173
+ path: string,
174
+ ancestors: string[]
175
+ ) => {
176
+ const keyMatches = key !== null && key.toLowerCase().includes(needle);
177
+ const expandable = isExpandable(node);
178
+ const valueMatches =
179
+ !expandable && primitiveText(node).toLowerCase().includes(needle);
180
+ if (keyMatches || valueMatches) hits.push({ path, ancestors });
181
+ if (expandable) {
182
+ const isArray = Array.isArray(node);
183
+ const nextAncestors = [...ancestors, path];
184
+ for (const [childKey, child] of childEntries(node)) {
185
+ visit(
186
+ child,
187
+ childKey,
188
+ childPath(path, childKey, isArray),
189
+ nextAncestors
190
+ );
191
+ }
192
+ }
193
+ };
194
+ visit(value, null, ROOT_PATH, []);
195
+ return hits;
196
+ };
197
+
198
+ const MAX_PREVIEW_ITEMS = 5;
199
+ const MAX_STRING_PREVIEW = 60;
200
+
201
+ export const previewString = (
202
+ value: string,
203
+ max = MAX_STRING_PREVIEW
204
+ ): string => (value.length > max ? `${value.slice(0, max)}…` : value);
205
+
206
+ /** Short inline summary used for collapsed objects/arrays, like `{id, name, …}`. */
207
+ export const collapsedPreview = (value: unknown): string => {
208
+ if (Array.isArray(value)) {
209
+ if (value.length === 0) return '[]';
210
+ const items = value
211
+ .slice(0, MAX_PREVIEW_ITEMS)
212
+ .map((item) => inlineValue(item));
213
+ const more = value.length > MAX_PREVIEW_ITEMS ? ', …' : '';
214
+ return `(${value.length}) [${items.join(', ')}${more}]`;
215
+ }
216
+ if (value && typeof value === 'object') {
217
+ const keys = Object.keys(value as object);
218
+ if (keys.length === 0) return '{}';
219
+ const shown = keys.slice(0, MAX_PREVIEW_ITEMS).join(', ');
220
+ const more = keys.length > MAX_PREVIEW_ITEMS ? ', …' : '';
221
+ return `{${shown}${more}}`;
222
+ }
223
+ return inlineValue(value);
224
+ };
225
+
226
+ export const inlineValue = (value: unknown): string => {
227
+ switch (typeOf(value)) {
228
+ case 'array':
229
+ return `Array(${(value as unknown[]).length})`;
230
+ case 'object':
231
+ return '{…}';
232
+ case 'string':
233
+ return `"${previewString(value as string, 24)}"`;
234
+ case 'null':
235
+ return 'null';
236
+ case 'undefined':
237
+ return 'undefined';
238
+ default:
239
+ return String(value);
240
+ }
241
+ };
242
+
243
+ /** Rough character count of a row, used to size the horizontal scroll area. */
244
+ export const rowCharCount = (row: JsonRow, indentChars: number): number => {
245
+ const keyPart = row.key === null ? 0 : row.key.length + 2;
246
+ let valuePart: number;
247
+ if (row.expandable) {
248
+ valuePart = row.expanded
249
+ ? row.type === 'array'
250
+ ? `Array(${row.childCount})`.length
251
+ : `Object (${row.childCount})`.length
252
+ : collapsedPreview(row.value).length;
253
+ } else {
254
+ valuePart = Math.min(displayValue(row.value).length, MAX_VALUE_CHARS);
255
+ }
256
+ return row.depth * indentChars + 2 + keyPart + valuePart;
257
+ };
258
+
259
+ export const MAX_VALUE_CHARS = 400;
260
+
261
+ export const displayValue = (value: unknown): string => {
262
+ if (typeof value === 'string') return `"${value}"`;
263
+ return primitiveText(value);
264
+ };
@@ -0,0 +1,176 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useState,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import { BackHandler, Share, StyleSheet, View } from 'react-native';
11
+ import { setLensBackHandler } from '../backHandler';
12
+ import { createHar } from '../core/har';
13
+ import logger from '../core/loggerSingleton';
14
+ import {
15
+ useLoggerEnabled,
16
+ useLoggerPaused,
17
+ useNetworkRequests,
18
+ } from '../hooks';
19
+ import { ThemeContext, useTheme, type ThemeInput } from '../theme';
20
+ import RequestDetails from './RequestDetails/RequestDetails';
21
+ import RequestList from './RequestList/RequestList';
22
+ import type { ActionItem } from './ui';
23
+
24
+ export interface NetworkLensProps {
25
+ /**
26
+ * Render nothing when `false`, e.g. `enabled={__DEV__ || featureFlag}`.
27
+ * @default true
28
+ */
29
+ enabled?: boolean;
30
+ /** `'light' | 'dark' | 'auto'` or a partial theme object. @default 'auto' */
31
+ theme?: ThemeInput;
32
+ /** Newest first (`desc`) or oldest first (`asc`). @default 'desc' */
33
+ sort?: 'asc' | 'desc';
34
+ /** Single-line rows. @default false */
35
+ compact?: boolean;
36
+ /** Limit the number of rows rendered. */
37
+ maxRows?: number;
38
+ /**
39
+ * Hook up your clipboard library to get "Copy" buttons, e.g.
40
+ * `copyToClipboard={Clipboard.setString}`.
41
+ */
42
+ copyToClipboard?: (text: string) => void;
43
+ /** Rendered above the request list's search bar (the overlay uses it for its title bar). */
44
+ header?: ReactNode;
45
+ }
46
+
47
+ export interface NetworkLensHandle {
48
+ /** Returns to the list if a request is open. Returns `true` when it handled the action. */
49
+ goBack: () => boolean;
50
+ }
51
+
52
+ const NetworkLens = forwardRef<NetworkLensHandle, NetworkLensProps>(
53
+ function NetworkLensRoot(
54
+ {
55
+ enabled = true,
56
+ theme = 'auto',
57
+ sort = 'desc',
58
+ compact = false,
59
+ maxRows,
60
+ copyToClipboard,
61
+ header,
62
+ },
63
+ ref
64
+ ) {
65
+ if (!enabled) return null;
66
+ return (
67
+ <ThemeContext.Provider value={theme}>
68
+ <Inspector
69
+ ref={ref}
70
+ sort={sort}
71
+ compact={compact}
72
+ maxRows={maxRows}
73
+ copyToClipboard={copyToClipboard}
74
+ header={header}
75
+ />
76
+ </ThemeContext.Provider>
77
+ );
78
+ }
79
+ );
80
+
81
+ type InspectorProps = Required<Pick<NetworkLensProps, 'sort' | 'compact'>> &
82
+ Pick<NetworkLensProps, 'maxRows' | 'copyToClipboard' | 'header'>;
83
+
84
+ const Inspector = forwardRef<NetworkLensHandle, InspectorProps>(
85
+ function InspectorInner(
86
+ { sort, compact, maxRows, copyToClipboard, header },
87
+ ref
88
+ ) {
89
+ const theme = useTheme();
90
+ const requests = useNetworkRequests();
91
+ const loggingEnabled = useLoggerEnabled();
92
+ const paused = useLoggerPaused();
93
+ const [selectedId, setSelectedId] = useState<string | null>(null);
94
+
95
+ const selected = useMemo(
96
+ () =>
97
+ selectedId === null
98
+ ? undefined
99
+ : (requests.find((r) => r.id === selectedId) ??
100
+ logger.getRequest(selectedId)),
101
+ [requests, selectedId]
102
+ );
103
+
104
+ const goBack = useCallback(() => {
105
+ if (selectedId === null) return false;
106
+ setSelectedId(null);
107
+ return true;
108
+ }, [selectedId]);
109
+
110
+ useImperativeHandle(ref, () => ({ goBack }), [goBack]);
111
+
112
+ // Android hardware back and the optional `getBackHandler()` integration.
113
+ useEffect(() => {
114
+ if (selectedId === null) return undefined;
115
+ setLensBackHandler(goBack);
116
+ const subscription = BackHandler.addEventListener(
117
+ 'hardwareBackPress',
118
+ goBack
119
+ );
120
+ return () => {
121
+ subscription.remove();
122
+ setLensBackHandler(undefined);
123
+ };
124
+ }, [selectedId, goBack]);
125
+
126
+ const menuActions = useMemo<ActionItem[]>(
127
+ () => [
128
+ {
129
+ label: paused ? 'Resume capturing' : 'Pause capturing',
130
+ onPress: () => logger.setPaused(!paused),
131
+ },
132
+ {
133
+ label: 'Export all as HAR',
134
+ onPress: async () => {
135
+ const har = await createHar(logger.getRequests());
136
+ Share.share({ message: JSON.stringify(har) }).catch(
137
+ () => undefined
138
+ );
139
+ },
140
+ },
141
+ { label: 'Clear requests', onPress: logger.clear, destructive: true },
142
+ ],
143
+ [paused]
144
+ );
145
+
146
+ return (
147
+ <View
148
+ style={[styles.container, { backgroundColor: theme.colors.background }]}
149
+ >
150
+ {selected ? (
151
+ <RequestDetails
152
+ request={selected}
153
+ onClose={goBack}
154
+ copyToClipboard={copyToClipboard}
155
+ />
156
+ ) : (
157
+ <RequestList
158
+ requests={requests}
159
+ sort={sort}
160
+ compact={compact}
161
+ maxRows={maxRows}
162
+ paused={paused}
163
+ loggingEnabled={loggingEnabled}
164
+ onPressItem={setSelectedId}
165
+ menuActions={menuActions}
166
+ header={header}
167
+ />
168
+ )}
169
+ </View>
170
+ );
171
+ }
172
+ );
173
+
174
+ const styles = StyleSheet.create({ container: { flex: 1 } });
175
+
176
+ export default NetworkLens;
@@ -0,0 +1,187 @@
1
+ import { useRef, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ Platform,
5
+ Pressable,
6
+ StatusBar,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import { useLoggerEnabled, useNetworkRequests } from '../hooks';
13
+ import { ThemeContext, useTheme, type ThemeInput } from '../theme';
14
+ import NetworkLens, {
15
+ type NetworkLensHandle,
16
+ type NetworkLensProps,
17
+ } from './NetworkLens';
18
+ import { IconButton } from './ui';
19
+
20
+ export interface NetworkLensOverlayProps extends Omit<
21
+ NetworkLensProps,
22
+ 'enabled' | 'theme' | 'header'
23
+ > {
24
+ /**
25
+ * Render nothing when `false`, e.g.
26
+ * `<NetworkLensOverlay enabled={__DEV__ || flags.networkInspector} />`
27
+ * @default true
28
+ */
29
+ enabled?: boolean;
30
+ theme?: ThemeInput;
31
+ /** Corner for the floating button. @default 'bottom-right' */
32
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
33
+ /** Distance from the screen edges in px. @default { x: 16, y: 32 } */
34
+ offset?: { x?: number; y?: number };
35
+ /** Controlled visibility of the inspector sheet. */
36
+ visible?: boolean;
37
+ onVisibleChange?: (visible: boolean) => void;
38
+ /** Hide the floating button when you open the sheet from your own dev menu. */
39
+ hideButton?: boolean;
40
+ /** Title of the inspector sheet. @default 'Network' */
41
+ title?: string;
42
+ }
43
+
44
+ /**
45
+ * Drop-in overlay: a floating badge with the number of captured requests that
46
+ * opens the inspector in a modal. Place it once near the root of your app.
47
+ */
48
+ const NetworkLensOverlay = ({
49
+ enabled = true,
50
+ theme = 'auto',
51
+ ...props
52
+ }: NetworkLensOverlayProps) => {
53
+ if (!enabled) return null;
54
+ return (
55
+ <ThemeContext.Provider value={theme}>
56
+ <Overlay {...props} theme={theme} />
57
+ </ThemeContext.Provider>
58
+ );
59
+ };
60
+
61
+ const Overlay = ({
62
+ theme,
63
+ position = 'bottom-right',
64
+ offset,
65
+ visible,
66
+ onVisibleChange,
67
+ hideButton,
68
+ title = 'Network',
69
+ ...lensProps
70
+ }: Omit<NetworkLensOverlayProps, 'enabled'> & { theme: ThemeInput }) => {
71
+ const t = useTheme();
72
+ const lensRef = useRef<NetworkLensHandle>(null);
73
+ const [internalVisible, setInternalVisible] = useState(false);
74
+ const open = visible ?? internalVisible;
75
+ const setOpen = (next: boolean) => {
76
+ if (visible === undefined) setInternalVisible(next);
77
+ onVisibleChange?.(next);
78
+ };
79
+
80
+ const requests = useNetworkRequests();
81
+ const loggingEnabled = useLoggerEnabled();
82
+ const hasErrors = requests.some(
83
+ (r) => r.state === 'failed' || r.status >= 400
84
+ );
85
+
86
+ const placement: ViewStyle = {
87
+ [position.startsWith('bottom') ? 'bottom' : 'top']: offset?.y ?? 32,
88
+ [position.endsWith('right') ? 'right' : 'left']: offset?.x ?? 16,
89
+ };
90
+
91
+ const header = (
92
+ <View style={styles.header}>
93
+ <Text style={[styles.title, { color: t.colors.text }]}>{title}</Text>
94
+ <IconButton
95
+ glyph="✕"
96
+ accessibilityLabel="Close network inspector"
97
+ onPress={() => setOpen(false)}
98
+ />
99
+ </View>
100
+ );
101
+
102
+ return (
103
+ <>
104
+ {!hideButton && (
105
+ <Pressable
106
+ accessibilityRole="button"
107
+ accessibilityLabel={`Open network inspector, ${requests.length} requests`}
108
+ onPress={() => setOpen(true)}
109
+ style={({ pressed }) => [
110
+ styles.fab,
111
+ placement,
112
+ {
113
+ backgroundColor: hasErrors
114
+ ? t.colors.statusBad
115
+ : t.colors.primary,
116
+ },
117
+ pressed && styles.fabPressed,
118
+ ]}
119
+ >
120
+ <Text style={[styles.fabCount, { color: t.colors.onPrimary }]}>
121
+ {loggingEnabled ? requests.length : '•'}
122
+ </Text>
123
+ <Text style={[styles.fabCaption, { color: t.colors.onPrimary }]}>
124
+ NET
125
+ </Text>
126
+ </Pressable>
127
+ )}
128
+ <Modal
129
+ visible={open}
130
+ animationType="slide"
131
+ presentationStyle={Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen'}
132
+ onRequestClose={() => {
133
+ // Android back button: step back inside the inspector first, then close.
134
+ if (!lensRef.current?.goBack()) setOpen(false);
135
+ }}
136
+ >
137
+ <View style={[styles.sheet, { backgroundColor: t.colors.surface }]}>
138
+ <NetworkLens
139
+ ref={lensRef}
140
+ {...lensProps}
141
+ theme={theme}
142
+ header={header}
143
+ />
144
+ </View>
145
+ </Modal>
146
+ </>
147
+ );
148
+ };
149
+
150
+ const styles = StyleSheet.create({
151
+ fab: {
152
+ position: 'absolute',
153
+ width: 56,
154
+ height: 56,
155
+ borderRadius: 28,
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ elevation: 6,
159
+ shadowColor: '#000',
160
+ shadowOpacity: 0.25,
161
+ shadowRadius: 6,
162
+ shadowOffset: { width: 0, height: 3 },
163
+ zIndex: 9999,
164
+ },
165
+ fabPressed: { opacity: 0.8 },
166
+ fabCount: { fontSize: 15, fontWeight: '800', fontVariant: ['tabular-nums'] },
167
+ fabCaption: {
168
+ fontSize: 9,
169
+ fontWeight: '700',
170
+ letterSpacing: 1,
171
+ marginTop: -2,
172
+ },
173
+ sheet: {
174
+ flex: 1,
175
+ paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
176
+ },
177
+ header: {
178
+ flexDirection: 'row',
179
+ alignItems: 'center',
180
+ justifyContent: 'space-between',
181
+ paddingLeft: 8,
182
+ paddingBottom: 6,
183
+ },
184
+ title: { fontSize: 17, fontWeight: '700' },
185
+ });
186
+
187
+ export default NetworkLensOverlay;