react-native-debug-toolkit 0.1.1

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.
@@ -0,0 +1,666 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { View, Text, StyleSheet, Clipboard, Dimensions } from 'react-native'
3
+ import { ScrollView, Pressable } from 'react-native-gesture-handler'
4
+
5
+ const { width: SCREEN_WIDTH } = Dimensions.get('window')
6
+
7
+ const CopyButton = ({ text, style }) => {
8
+ const [copied, setCopied] = useState(false)
9
+
10
+ const handleCopy = async () => {
11
+ await Clipboard.setString(text)
12
+ setCopied(true)
13
+ setTimeout(() => setCopied(false), 2000)
14
+ }
15
+
16
+ return (
17
+ <Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
18
+ <Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
19
+ </Pressable>
20
+ )
21
+ }
22
+
23
+ const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
24
+ const [expanded, setExpanded] = useState(initiallyExpanded)
25
+
26
+ return (
27
+ <View style={styles.collapsibleSection}>
28
+ <Pressable
29
+ style={styles.sectionHeader}
30
+ onPress={() => setExpanded(!expanded)}>
31
+ <Text style={styles.sectionTitle}>{title}</Text>
32
+ <Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
33
+ </Pressable>
34
+ {expanded && children}
35
+ </View>
36
+ )
37
+ }
38
+
39
+ const LongTextContent = ({ text }) => {
40
+ return (
41
+ <View style={styles.longTextContainer}>
42
+ <Text style={styles.jsonString} selectable={true}>
43
+ {text}
44
+ </Text>
45
+ </View>
46
+ )
47
+ }
48
+
49
+ const JSONValue = ({ value, path = '', level = 0, maxExpandLevel = 2 }) => {
50
+ const [expanded, setExpanded] = useState(level < maxExpandLevel)
51
+
52
+ if (value === null) {
53
+ return <Text style={styles.jsonNull}>null</Text>
54
+ }
55
+
56
+ if (value === undefined) {
57
+ return <Text style={styles.jsonNull}>undefined</Text>
58
+ }
59
+
60
+ if (typeof value === 'boolean') {
61
+ return <Text style={styles.jsonBoolean}>{value.toString()}</Text>
62
+ }
63
+
64
+ if (typeof value === 'number') {
65
+ return <Text style={styles.jsonNumber}>{value}</Text>
66
+ }
67
+
68
+ if (typeof value === 'string') {
69
+ if (value.length > 150) {
70
+ return (
71
+ <CollapsibleSection
72
+ title={`String (${value.length} chars)`}
73
+ initiallyExpanded={false}>
74
+ <LongTextContent text={value} />
75
+ </CollapsibleSection>
76
+ )
77
+ }
78
+
79
+ return (
80
+ <Text style={styles.jsonString} selectable={true}>
81
+ {value}
82
+ </Text>
83
+ )
84
+ }
85
+
86
+ if (Array.isArray(value)) {
87
+ if (value.length === 0) {
88
+ return <Text style={styles.jsonArray}>[]</Text>
89
+ }
90
+
91
+ return (
92
+ <View style={styles.jsonContainer}>
93
+ <Pressable
94
+ onPress={() => setExpanded(!expanded)}
95
+ style={styles.jsonToggle}>
96
+ <Text style={styles.jsonBrackets}>
97
+ [{!expanded && '...'}
98
+ {expanded && value.length > 0 && ''}
99
+ </Text>
100
+ {!expanded && (
101
+ <Text style={styles.jsonCollapsed}> Array({value.length}) </Text>
102
+ )}
103
+ </Pressable>
104
+
105
+ {expanded && (
106
+ <View style={styles.jsonChildren}>
107
+ {value.map((item, index) => (
108
+ <View key={`${path}-${index}`} style={styles.jsonArrayItem}>
109
+ <Text style={styles.jsonKey}>{index}: </Text>
110
+ <View style={styles.jsonValue}>
111
+ <JSONValue
112
+ value={item}
113
+ path={`${path}[${index}]`}
114
+ level={level + 1}
115
+ />
116
+ </View>
117
+ </View>
118
+ ))}
119
+ </View>
120
+ )}
121
+
122
+ {expanded && <Text style={styles.jsonBrackets}>]</Text>}
123
+ </View>
124
+ )
125
+ }
126
+
127
+ if (typeof value === 'object') {
128
+ const keys = Object.keys(value)
129
+
130
+ if (keys.length === 0) {
131
+ return <Text style={styles.jsonObject}>{'{}'}</Text>
132
+ }
133
+
134
+ return (
135
+ <View style={styles.jsonContainer}>
136
+ <Pressable
137
+ onPress={() => setExpanded(!expanded)}
138
+ style={styles.jsonToggle}>
139
+ <Text style={styles.jsonBrackets}>
140
+ {'{'}
141
+ {!expanded && '...'}
142
+ {expanded && keys.length > 0 && ''}
143
+ </Text>
144
+ {!expanded && (
145
+ <Text style={styles.jsonCollapsed}> Object({keys.length}) </Text>
146
+ )}
147
+ </Pressable>
148
+
149
+ {expanded && (
150
+ <View style={styles.jsonChildren}>
151
+ {keys.map((key) => (
152
+ <View key={`${path}-${key}`} style={styles.jsonProperty}>
153
+ <Text style={styles.jsonKey}>{key}: </Text>
154
+ <View style={styles.jsonValue}>
155
+ <JSONValue
156
+ value={value[key]}
157
+ path={`${path}.${key}`}
158
+ level={level + 1}
159
+ />
160
+ </View>
161
+ </View>
162
+ ))}
163
+ </View>
164
+ )}
165
+
166
+ {expanded && <Text style={styles.jsonBrackets}>{'}'}</Text>}
167
+ </View>
168
+ )
169
+ }
170
+
171
+ return <Text>{String(value)}</Text>
172
+ }
173
+
174
+ const ApiStatus = ({ status, success }) => {
175
+ let statusColor = '#666'
176
+ let statusText = ''
177
+
178
+ if (typeof status === 'number') {
179
+ statusText = status.toString()
180
+
181
+ if (status >= 200 && status < 300) {
182
+ statusColor = '#00C851'
183
+ } else if (status >= 400) {
184
+ statusColor = '#ff4444'
185
+ }
186
+ } else if (success === false) {
187
+ statusColor = '#ff4444'
188
+ statusText = 'Error'
189
+ } else if (success === true) {
190
+ statusColor = '#00C851'
191
+ statusText = 'Success'
192
+ }
193
+
194
+ return (
195
+ <Text style={[styles.statusPill, { backgroundColor: statusColor }]}>
196
+ {statusText}
197
+ </Text>
198
+ )
199
+ }
200
+
201
+ const HttpLogDetails = ({ log }) => {
202
+ const formatDataSize = (data) => {
203
+ if (!data) {
204
+ return '(empty)'
205
+ }
206
+
207
+ try {
208
+ const stringData = typeof data === 'string' ? data : JSON.stringify(data)
209
+ // Use string length as an approximation instead of TextEncoder
210
+ const bytes = stringData.length
211
+
212
+ if (bytes < 1024) {
213
+ return `(${bytes} B)`
214
+ } else if (bytes < 1024 * 1024) {
215
+ return `(${(bytes / 1024).toFixed(1)} KB)`
216
+ } else {
217
+ return `(${(bytes / (1024 * 1024)).toFixed(1)} MB)`
218
+ }
219
+ } catch (e) {
220
+ return '(size unknown)'
221
+ }
222
+ }
223
+
224
+ const generateCurl = useCallback(() => {
225
+ if (!log.request) {
226
+ return 'curl command unavailable'
227
+ }
228
+
229
+ let curl = `curl -X ${log.request.method || 'GET'} '${log.request.url}'`
230
+
231
+ // Add headers
232
+ if (log.request.headers && Object.keys(log.request.headers).length > 0) {
233
+ Object.entries(log.request.headers).forEach(([key, value]) => {
234
+ if (typeof value === 'string') {
235
+ curl += ` \\\n -H '${key}: ${value}'`
236
+ }
237
+ })
238
+ }
239
+
240
+ // Add body
241
+ if (log.request.body) {
242
+ try {
243
+ const bodyStr =
244
+ typeof log.request.body === 'string'
245
+ ? log.request.body
246
+ : JSON.stringify(log.request.body)
247
+
248
+ curl += ` \\\n -d '${bodyStr.replace(/'/g, "'\\''")}'`
249
+ } catch (e) {
250
+ // Skip body if it can't be stringified
251
+ }
252
+ }
253
+
254
+ return curl
255
+ }, [log])
256
+
257
+ if (!log) {
258
+ return (
259
+ <View style={styles.errorContainer}>
260
+ <Text style={styles.errorText}>Log data is missing</Text>
261
+ </View>
262
+ )
263
+ }
264
+
265
+ const request = log.request || {}
266
+ const response = log.response || {}
267
+ const status = response.status
268
+ const success = response.success
269
+ const error = log.error
270
+ const duration = log.duration
271
+
272
+ const requestData = request.body
273
+ const responseData = response.data
274
+ const responseSize = formatDataSize(responseData)
275
+
276
+ return (
277
+ <ScrollView
278
+ style={styles.container}
279
+ contentContainerStyle={styles.contentContainer}
280
+ showsVerticalScrollIndicator={true}
281
+ scrollEventThrottle={16}
282
+ keyboardShouldPersistTaps='handled'
283
+ nestedScrollEnabled={true}>
284
+ <View style={styles.header}>
285
+ <View style={styles.headerInfo}>
286
+ <Text style={styles.url} numberOfLines={2} selectable={true}>
287
+ {request.url || 'Unknown URL'}
288
+ </Text>
289
+ <View style={styles.methodRow}>
290
+ <Text
291
+ style={[
292
+ styles.method,
293
+ { color: getMethodColor(request.method) },
294
+ ]}>
295
+ {(request.method || 'GET').toUpperCase()}
296
+ </Text>
297
+ {status && <ApiStatus status={status} success={success} />}
298
+ </View>
299
+ </View>
300
+ <CopyButton text={request.url || ''} />
301
+ </View>
302
+
303
+ <CollapsibleSection title='Request' initiallyExpanded={true}>
304
+ <View style={styles.content}>
305
+ {requestData && (
306
+ <View style={styles.dataSection}>
307
+ <View style={styles.dataSectionHeader}>
308
+ <Text style={styles.dataSectionTitle}>
309
+ Body {formatDataSize(requestData)}
310
+ </Text>
311
+ <CopyButton
312
+ text={
313
+ typeof requestData === 'string'
314
+ ? requestData
315
+ : JSON.stringify(requestData, null, 2)
316
+ }
317
+ />
318
+ </View>
319
+ <View style={styles.dataContent}>
320
+ <JSONValue value={requestData} maxExpandLevel={1} />
321
+ </View>
322
+ </View>
323
+ )}
324
+
325
+ <CollapsibleSection title='Headers'>
326
+ <View style={styles.dataContent}>
327
+ <JSONValue value={request.headers || {}} maxExpandLevel={0} />
328
+ </View>
329
+ </CollapsibleSection>
330
+ </View>
331
+ </CollapsibleSection>
332
+
333
+ <CollapsibleSection
334
+ title={`Response ${responseSize}`}
335
+ initiallyExpanded={true}>
336
+ <View style={styles.content}>
337
+ <View style={styles.row}>
338
+ <Text style={styles.label}>Status:</Text>
339
+ <Text style={styles.value}>
340
+ {status || (success === false ? 'Error' : 'Unknown')}
341
+ {status && response.statusText ? ` (${response.statusText})` : ''}
342
+ </Text>
343
+ </View>
344
+
345
+ {duration && (
346
+ <View style={styles.row}>
347
+ <Text style={styles.label}>Duration:</Text>
348
+ <Text style={styles.value}>{duration}ms</Text>
349
+ </View>
350
+ )}
351
+
352
+ {error && (
353
+ <View style={styles.errorSection}>
354
+ <Text style={styles.errorLabel}>Error:</Text>
355
+ <Text style={styles.errorValue} selectable={true}>
356
+ {error}
357
+ </Text>
358
+ </View>
359
+ )}
360
+
361
+ {responseData && (
362
+ <View style={styles.dataSection}>
363
+ <View style={styles.dataSectionHeader}>
364
+ <Text style={styles.dataSectionTitle}>Body</Text>
365
+ <CopyButton
366
+ text={
367
+ typeof responseData === 'string'
368
+ ? responseData
369
+ : JSON.stringify(responseData, null, 2)
370
+ }
371
+ />
372
+ </View>
373
+ <View style={styles.dataContent}>
374
+ <JSONValue value={responseData} maxExpandLevel={1} />
375
+ </View>
376
+ </View>
377
+ )}
378
+
379
+ <CollapsibleSection title='Headers'>
380
+ <View style={styles.dataContent}>
381
+ <JSONValue value={response.headers || {}} maxExpandLevel={0} />
382
+ </View>
383
+ </CollapsibleSection>
384
+ </View>
385
+ </CollapsibleSection>
386
+
387
+ <CollapsibleSection title='cURL Command'>
388
+ <View style={styles.content}>
389
+ <View style={styles.codeBlockContainer}>
390
+ <View style={styles.codeBlockHeader}>
391
+ <Text style={styles.codeBlockLabel}>Debug with cURL</Text>
392
+ <CopyButton text={generateCurl()} />
393
+ </View>
394
+ <View style={styles.codeBlock}>
395
+ <Text style={styles.codeText} selectable={true}>
396
+ {generateCurl()}
397
+ </Text>
398
+ </View>
399
+ </View>
400
+ </View>
401
+ </CollapsibleSection>
402
+
403
+ <CollapsibleSection title='Timing'>
404
+ <View style={styles.content}>
405
+ <Text style={styles.label}>Time:</Text>
406
+ <Text style={styles.value}>
407
+ {log.timestamp
408
+ ? new Date(log.timestamp).toLocaleString()
409
+ : 'Unknown'}
410
+ </Text>
411
+
412
+ <Text style={styles.label}>Duration:</Text>
413
+ <Text style={styles.value}>
414
+ {log.duration ? `${log.duration}ms` : 'Unknown'}
415
+ </Text>
416
+ </View>
417
+ </CollapsibleSection>
418
+ </ScrollView>
419
+ )
420
+ }
421
+
422
+ const getMethodColor = (method) => {
423
+ switch (method?.toUpperCase()) {
424
+ case 'GET':
425
+ return '#0D96F2' // Standard API blue
426
+ case 'POST':
427
+ return '#49CC90' // Swagger green
428
+ case 'PUT':
429
+ return '#FCA130' // Standard PUT orange
430
+ case 'DELETE':
431
+ return '#F93E3E' // Standard DELETE red
432
+ default:
433
+ return '#666666'
434
+ }
435
+ }
436
+
437
+ const styles = StyleSheet.create({
438
+ container: {
439
+ flex: 1,
440
+ backgroundColor: '#fff',
441
+ },
442
+ contentContainer: {
443
+ paddingBottom: 20,
444
+ },
445
+ header: {
446
+ flexDirection: 'row',
447
+ padding: 15,
448
+ borderBottomWidth: 1,
449
+ borderBottomColor: '#eee',
450
+ alignItems: 'flex-start',
451
+ },
452
+ headerInfo: {
453
+ flex: 1,
454
+ marginRight: 10,
455
+ },
456
+ url: {
457
+ fontSize: 14,
458
+ color: '#333',
459
+ marginBottom: 6,
460
+ fontWeight: '500',
461
+ },
462
+ methodRow: {
463
+ flexDirection: 'row',
464
+ alignItems: 'center',
465
+ },
466
+ method: {
467
+ fontSize: 14,
468
+ fontWeight: 'bold',
469
+ marginRight: 8,
470
+ },
471
+ statusPill: {
472
+ paddingHorizontal: 8,
473
+ paddingVertical: 2,
474
+ borderRadius: 12,
475
+ color: 'white',
476
+ fontSize: 12,
477
+ fontWeight: 'bold',
478
+ },
479
+ collapsibleSection: {
480
+ marginBottom: 1,
481
+ backgroundColor: '#fff',
482
+ },
483
+ sectionHeader: {
484
+ flexDirection: 'row',
485
+ justifyContent: 'space-between',
486
+ alignItems: 'center',
487
+ padding: 15,
488
+ backgroundColor: '#f5f5f5',
489
+ },
490
+ sectionTitle: {
491
+ fontSize: 15,
492
+ fontWeight: 'bold',
493
+ color: '#333',
494
+ },
495
+ expandIcon: {
496
+ fontSize: 14,
497
+ color: '#666',
498
+ },
499
+ content: {
500
+ padding: 15,
501
+ },
502
+ row: {
503
+ marginBottom: 10,
504
+ },
505
+ label: {
506
+ fontSize: 14,
507
+ fontWeight: 'bold',
508
+ color: '#666',
509
+ marginBottom: 5,
510
+ },
511
+ value: {
512
+ fontSize: 14,
513
+ color: '#333',
514
+ marginBottom: 10,
515
+ },
516
+ errorContainer: {
517
+ flex: 1,
518
+ justifyContent: 'center',
519
+ alignItems: 'center',
520
+ padding: 20,
521
+ },
522
+ errorText: {
523
+ color: '#ff4444',
524
+ fontSize: 16,
525
+ },
526
+ errorSection: {
527
+ backgroundColor: '#fff8f8',
528
+ padding: 10,
529
+ borderRadius: 4,
530
+ borderWidth: 1,
531
+ borderColor: '#ffdddd',
532
+ marginBottom: 15,
533
+ },
534
+ errorLabel: {
535
+ fontSize: 14,
536
+ fontWeight: 'bold',
537
+ color: '#ff4444',
538
+ marginBottom: 5,
539
+ },
540
+ errorValue: {
541
+ fontSize: 14,
542
+ color: '#ff4444',
543
+ },
544
+ dataSection: {
545
+ marginBottom: 15,
546
+ },
547
+ dataSectionHeader: {
548
+ flexDirection: 'row',
549
+ justifyContent: 'space-between',
550
+ alignItems: 'center',
551
+ marginBottom: 5,
552
+ },
553
+ dataSectionTitle: {
554
+ fontSize: 14,
555
+ fontWeight: 'bold',
556
+ color: '#666',
557
+ },
558
+ dataContent: {
559
+ backgroundColor: '#f8f9fa',
560
+ padding: 10,
561
+ borderRadius: 4,
562
+ borderWidth: 1,
563
+ borderColor: '#e9ecef',
564
+ },
565
+ codeBlockContainer: {
566
+ marginBottom: 15,
567
+ },
568
+ codeBlockHeader: {
569
+ flexDirection: 'row',
570
+ justifyContent: 'space-between',
571
+ alignItems: 'center',
572
+ marginBottom: 5,
573
+ },
574
+ codeBlockLabel: {
575
+ fontSize: 12,
576
+ color: '#666',
577
+ fontWeight: 'bold',
578
+ },
579
+ codeBlock: {
580
+ backgroundColor: '#f8f9fa',
581
+ padding: 10,
582
+ borderRadius: 4,
583
+ borderWidth: 1,
584
+ borderColor: '#e9ecef',
585
+ },
586
+ codeText: {
587
+ fontSize: 13,
588
+ color: '#333',
589
+ fontFamily: 'Courier',
590
+ },
591
+ copyButton: {
592
+ backgroundColor: '#e9ecef',
593
+ paddingHorizontal: 10,
594
+ paddingVertical: 5,
595
+ borderRadius: 4,
596
+ },
597
+ copyButtonText: {
598
+ fontSize: 12,
599
+ color: '#666',
600
+ },
601
+ jsonContainer: {
602
+ marginVertical: 2,
603
+ },
604
+ jsonToggle: {
605
+ flexDirection: 'row',
606
+ alignItems: 'center',
607
+ },
608
+ jsonBrackets: {
609
+ color: '#666',
610
+ fontWeight: 'bold',
611
+ },
612
+ jsonCollapsed: {
613
+ color: '#888',
614
+ fontSize: 12,
615
+ fontStyle: 'italic',
616
+ },
617
+ jsonChildren: {
618
+ paddingLeft: 16,
619
+ borderLeftWidth: 1,
620
+ borderLeftColor: '#e0e0e0',
621
+ },
622
+ jsonProperty: {
623
+ flexDirection: 'row',
624
+ flexWrap: 'wrap',
625
+ marginVertical: 2,
626
+ },
627
+ jsonArrayItem: {
628
+ flexDirection: 'row',
629
+ flexWrap: 'wrap',
630
+ marginVertical: 2,
631
+ },
632
+ jsonKey: {
633
+ color: '#7B61AB',
634
+ fontWeight: '500',
635
+ },
636
+ jsonValue: {
637
+ flex: 1,
638
+ },
639
+ jsonString: {
640
+ color: '#CB772F',
641
+ },
642
+ jsonNumber: {
643
+ color: '#2878D0',
644
+ },
645
+ jsonBoolean: {
646
+ color: '#2878D0',
647
+ fontWeight: 'bold',
648
+ },
649
+ jsonNull: {
650
+ color: '#A0A0A0',
651
+ fontStyle: 'italic',
652
+ },
653
+ jsonObject: {
654
+ color: '#666',
655
+ },
656
+ jsonArray: {
657
+ color: '#666',
658
+ },
659
+ longTextContainer: {
660
+ maxHeight: 200,
661
+ paddingHorizontal: 8,
662
+ paddingVertical: 5,
663
+ },
664
+ })
665
+
666
+ export default HttpLogDetails