react-native-in-app-debugger 1.0.42 → 1.0.44

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.
package/Api/Row.jsx ADDED
@@ -0,0 +1,85 @@
1
+ import React, { useState } from 'react';
2
+ import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import Text from '../Text';
4
+ import Highlight from '../Highlight';
5
+
6
+ export const MAX_URL_LENGTH = 100;
7
+
8
+ export default ({ item, filter }) => {
9
+ const tabs = [
10
+ { value: 'Response Body' },
11
+ { value: 'Request Body', hide: !item.request.data },
12
+ { value: 'Request Header' },
13
+ ];
14
+ const [tab, setTab] = useState(tabs[0].value);
15
+ const hasResponse = item.response;
16
+ const Tab = ({ value, hide }) => {
17
+ if (hide) return null;
18
+ const isSelected = value === tab;
19
+ return (
20
+ <TouchableOpacity
21
+ activeOpacity={isSelected ? 1 : 0.7}
22
+ onPress={() => setTab(value)}
23
+ style={[styles.selectionTab, { backgroundColor: isSelected ? 'white' : undefined }]}
24
+ >
25
+ <Text
26
+ style={{
27
+ color: isSelected ? '#000' : '#ffffff88',
28
+ textAlign: 'center',
29
+ }}
30
+ >
31
+ {value}
32
+ </Text>
33
+ </TouchableOpacity>
34
+ );
35
+ };
36
+
37
+ return (
38
+ <View style={styles.container}>
39
+ {item.request.url.length > MAX_URL_LENGTH && (
40
+ <Text style={{ color: '#ffffff99', paddingVertical: 20 }}>
41
+ <Highlight text={item.request.url} filter={filter} />
42
+ </Text>
43
+ )}
44
+ <View>
45
+ <View style={{ flexDirection: 'row' }}>
46
+ {tabs.map((t) => (
47
+ <Tab key={t.value} {...t} />
48
+ ))}
49
+ </View>
50
+
51
+ {tab === tabs[0].value && hasResponse && (
52
+ <Text style={{ color: 'white' }}>
53
+ <Highlight text={JSON.stringify(item.response.data, undefined, 4)} filter={filter} />
54
+ </Text>
55
+ )}
56
+ {tab === tabs[1].value && (
57
+ <Text style={{ color: 'white' }}>
58
+ <Highlight text={JSON.stringify(item.request.data, undefined, 4)} filter={filter} />
59
+ </Text>
60
+ )}
61
+ {tab === tabs[2].value && (
62
+ <Text style={{ color: 'white' }}>
63
+ <Highlight text={JSON.stringify(item.request.headers, undefined, 4)} filter={filter} />
64
+ </Text>
65
+ )}
66
+ </View>
67
+ </View>
68
+ );
69
+ };
70
+
71
+ const styles = StyleSheet.create({
72
+ container: {
73
+ padding: 5,
74
+ backgroundColor: '#171717',
75
+ paddingTop: 10,
76
+ paddingBottom: 40,
77
+ },
78
+ selectionTab: {
79
+ borderBottomColor: 'white',
80
+ borderBottomWidth: 2,
81
+ flex: 1,
82
+ borderTopEndRadius: 10,
83
+ borderTopStartRadius: 10,
84
+ },
85
+ });
package/Api/index.jsx ADDED
@@ -0,0 +1,203 @@
1
+ import React, { useState } from 'react';
2
+ import { SectionList, TextInput, View, Alert, StyleSheet, TouchableOpacity } from 'react-native';
3
+ import Text from '../Text';
4
+ import Highlight from '../Highlight';
5
+ import Bookmark from '../Bookmark';
6
+ import getRandomBrightColor from '../utils/getRandomBrightColor';
7
+ import { MAX_URL_LENGTH } from './Row';
8
+ let Clipboard;
9
+ try {
10
+ Clipboard = require('@react-native-clipboard/clipboard')?.default;
11
+ } catch (error) {
12
+ // console.error("Error importing Clipboard:", error);
13
+ }
14
+
15
+ import Row from './Row';
16
+
17
+ const isError = (a) => a.response?.status < 200 || a.response?.status >= 400;
18
+ export default (props) => {
19
+ const [filter, setFilter] = useState('');
20
+ const [errorOnly, setErrorOnly] = useState(false);
21
+ const [expands, setExpands] = useState({});
22
+ const apis = props.apis.filter((a) => !errorOnly || isError(a));
23
+
24
+ const hasError = apis.some(isError);
25
+
26
+ return (
27
+ <>
28
+ <View style={styles.container}>
29
+ {!!apis.length && !filter && (
30
+ <TouchableOpacity
31
+ style={{ padding: 5, backgroundColor: 'white', borderRadius: 5 }}
32
+ onPress={() =>
33
+ Alert.alert('Are you sure', 'You want to clear all logs', [
34
+ { text: 'Delete', onPress: props.clear, style: 'cancel' },
35
+ { text: 'Cancel' },
36
+ ])
37
+ }
38
+ >
39
+ <Text style={{ color: 'black', fontSize: 10 }}>Clear {props.apis.length} APIs</Text>
40
+ </TouchableOpacity>
41
+ )}
42
+ {hasError && !filter && (
43
+ <TouchableOpacity style={{ padding: 5 }} onPress={() => setErrorOnly((v) => !v)}>
44
+ <Text
45
+ style={{
46
+ color: 'red',
47
+ textDecorationLine: errorOnly ? 'line-through' : undefined,
48
+ fontSize: 10,
49
+ }}
50
+ >
51
+ {apis.filter(isError).length} error
52
+ {apis.filter(isError).length > 1 ? 's' : ''}
53
+ </Text>
54
+ </TouchableOpacity>
55
+ )}
56
+ {!!props.blacklists.length && !filter && (
57
+ <TouchableOpacity
58
+ style={{ padding: 5, backgroundColor: 'white', borderRadius: 5 }}
59
+ onPress={() =>
60
+ Alert.alert('Are you sure', 'You want to clear all blacklists', [
61
+ { text: 'Clear', onPress: () => props.setBlacklists(), style: 'cancel' },
62
+ { text: 'Cancel' },
63
+ ])
64
+ }
65
+ >
66
+ <Text style={{ color: 'black', fontSize: 10 }}>Clear {props.blacklists.length} Blacklists</Text>
67
+ </TouchableOpacity>
68
+ )}
69
+
70
+ <TextInput
71
+ value={filter}
72
+ placeholder='Filter...'
73
+ clearButtonMode='always'
74
+ placeholderTextColor='grey'
75
+ style={{ paddingHorizontal: 5, color: 'white', flex: 1 }}
76
+ onChangeText={(t) => setFilter(t.toLowerCase())}
77
+ />
78
+ </View>
79
+ {!filter && !!props.maxNumOfApiToStore && apis.length >= props.maxNumOfApiToStore && (
80
+ <Text style={{ color: '#ffffff88', padding: 10 }}>Capped to only latest {props.maxNumOfApiToStore} APIs</Text>
81
+ )}
82
+ <SectionList
83
+ keyExtractor={(i) => i.id}
84
+ stickySectionHeadersEnabled
85
+ showsVerticalScrollIndicator
86
+ sections={apis
87
+ .filter((a) => !filter || JSON.stringify(a).toLowerCase().includes(filter))
88
+ .map((data) => ({ data: [data], id: data.id }))}
89
+ renderItem={(i) => (expands[i.item.id] ? <Row {...i} filter={filter} /> : <View style={{ height: 20 }} />)}
90
+ renderSectionHeader={({ section: { data } }) => {
91
+ const item = data[0];
92
+ const hasResponse = !!item.response;
93
+
94
+ const duration = item.response?.timestamp ? ~~(item.response?.timestamp - item.request.timestamp) / 1000 : 0;
95
+ const isExpand = expands[item.id];
96
+ const bookmarkColor = props.bookmarks[item.id];
97
+ const color = hasResponse ? (item.response.error ? 'red' : 'white') : 'yellow';
98
+
99
+ return (
100
+ <View style={styles.rowHeader}>
101
+ <Bookmark
102
+ color={bookmarkColor}
103
+ onPress={() => {
104
+ props.setBookmarks((v) => {
105
+ if (!bookmarkColor) return { ...v, [item.id]: getRandomBrightColor() };
106
+ const newV = { ...v };
107
+ delete newV[item.id];
108
+ return newV;
109
+ });
110
+ }}
111
+ />
112
+ <Text selectable style={{ flex: 1, color, marginVertical: 10 }}>
113
+ <Text style={{ opacity: 0.7 }}>
114
+ {item.request.method +
115
+ ` (${item.response?.status ?? 'no response'})` +
116
+ ' - ' +
117
+ item.request.time +
118
+ (hasResponse ? ' - ' + duration + ' second(s)' : '') +
119
+ '\n'}
120
+ </Text>
121
+ <Highlight text={item.request.url.slice(0, MAX_URL_LENGTH)} filter={filter} />
122
+ {item.request.url.length > MAX_URL_LENGTH && '.......'}
123
+ </Text>
124
+ <View style={{ gap: 4 }}>
125
+ <TouchableOpacity
126
+ onPress={() =>
127
+ setExpands((v) => {
128
+ if (!isExpand) return { ...v, [item.id]: true };
129
+ const newV = { ...v };
130
+ delete newV[item.id];
131
+ return newV;
132
+ })
133
+ }
134
+ style={styles.actionButton}
135
+ >
136
+ <Text style={{ color: 'black', fontSize: 10 }}>{isExpand ? 'Hide' : 'Show'}</Text>
137
+ </TouchableOpacity>
138
+ {!!Clipboard && (
139
+ <TouchableOpacity
140
+ onPress={() => {
141
+ const content = { ...item };
142
+ delete content.id;
143
+ Clipboard.setString(JSON.stringify(content, undefined, 4));
144
+ }}
145
+ style={styles.actionButton}
146
+ >
147
+ <Text style={{ color: 'black', fontSize: 10 }}>Copy</Text>
148
+ </TouchableOpacity>
149
+ )}
150
+ <TouchableOpacity
151
+ onPress={() => {
152
+ Alert.alert(
153
+ 'Are you sure',
154
+ `You want to blacklist: \n\n(${item.request.method}) ${item.request.url} \n\nwhere all history logs for this API will be removed and all future request for this API will not be recorded?`,
155
+ [
156
+ {
157
+ text: 'Blacklist',
158
+ onPress: () => props.setBlacklists({ method: item.request.method, url: item.request.url }),
159
+ style: 'cancel',
160
+ },
161
+ { text: 'Cancel' },
162
+ ],
163
+ );
164
+ }}
165
+ style={styles.actionButton}
166
+ >
167
+ <Text style={{ color: 'black', fontSize: 10 }}>Blacklist</Text>
168
+ </TouchableOpacity>
169
+ </View>
170
+ </View>
171
+ );
172
+ }}
173
+ />
174
+ </>
175
+ );
176
+ };
177
+
178
+ const styles = StyleSheet.create({
179
+ container: {
180
+ flexDirection: 'row',
181
+ paddingLeft: 5,
182
+ alignItems: 'center',
183
+ gap: 5,
184
+ },
185
+ actionButton: { backgroundColor: 'white', borderRadius: 5, padding: 4 },
186
+ rowHeader: {
187
+ flexDirection: 'row',
188
+ gap: 5,
189
+ backgroundColor: 'black',
190
+ padding: 5,
191
+ paddingTop: 10,
192
+ shadowColor: 'black',
193
+ shadowOffset: {
194
+ width: 0,
195
+ height: 2,
196
+ },
197
+ shadowRadius: 5,
198
+ shadowOpacity: 1,
199
+ // TODO shadow not working on android
200
+ elevation: 10,
201
+ zIndex: 99,
202
+ },
203
+ });
package/Bookmark.jsx ADDED
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+
4
+ export default ({ size = 10, color = '#222', onPress }) => {
5
+ const tristyle = { borderRightWidth: size / 2, borderTopWidth: size / 2, borderTopColor: color };
6
+ return (
7
+ <View>
8
+ <View style={{ width: size, height: size, backgroundColor: color }} />
9
+ <View style={{ flexDirection: 'row' }}>
10
+ <View style={[styles.triangleCorner, tristyle]} />
11
+ <View style={[styles.triangleCorner, tristyle, { transform: [{ rotate: '90deg' }] }]} />
12
+ </View>
13
+ <Pressable
14
+ onPress={onPress}
15
+ style={[
16
+ {
17
+ width: size,
18
+ height: size + size / 2,
19
+ },
20
+ styles.pressable,
21
+ ]}
22
+ />
23
+ </View>
24
+ );
25
+ };
26
+
27
+ const styles = StyleSheet.create({
28
+ triangleCorner: {
29
+ width: 0,
30
+ height: 0,
31
+ backgroundColor: 'transparent',
32
+ borderStyle: 'solid',
33
+ borderRightColor: 'transparent',
34
+ },
35
+ pressable: {
36
+ position: 'absolute',
37
+ transform: [{ scale: 1.7 }],
38
+ },
39
+ });
package/README.md CHANGED
@@ -93,3 +93,7 @@ If this library is installed, when user expand any selected API, there will be a
93
93
 
94
94
  <img width="535" alt="image" src="https://github.com/fattahmuhyiddeen/react-native-in-app-debugger/assets/24792201/d4f58ee3-e553-4cae-91df-ba7e26d8cd70">
95
95
 
96
+
97
+ #### React Native Async Storage (https://www.npmjs.com/package/@react-native-async-storage/async-storage)
98
+
99
+ If this library is installed, the blacklist will be persisted
package/X.jsx ADDED
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
+
4
+ export default ({ size = 20, color = 'white', onPress }) => {
5
+ const panelStyle = { top: size / 2, width: size, backgroundColor: color };
6
+ return (
7
+ <TouchableOpacity
8
+ onPress={onPress}
9
+ style={{
10
+ width: size,
11
+ height: size,
12
+ transform: [{ scale: 1.7 }],
13
+ }}
14
+ >
15
+ <View style={{ width: size, height: size, transform: [{ scale: 0.5 }] }}>
16
+ <View style={[styles.panel, panelStyle, { transform: [{ rotate: '45deg' }] }]} />
17
+ <View style={[styles.panel, panelStyle, { transform: [{ rotate: '-45deg' }] }]} />
18
+ </View>
19
+ </TouchableOpacity>
20
+ );
21
+ };
22
+
23
+ const styles = StyleSheet.create({
24
+ panel: {
25
+ height: 3,
26
+ position: 'absolute',
27
+ },
28
+ });
package/index.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import {
3
3
  Animated,
4
4
  StyleSheet,
@@ -8,6 +8,8 @@ import {
8
8
  Dimensions,
9
9
  } from "react-native";
10
10
  import Text from "./Text";
11
+ import X from './X';
12
+
11
13
  let DeviceInfo;
12
14
  try {
13
15
  DeviceInfo = require("react-native-device-info");
@@ -15,6 +17,13 @@ try {
15
17
  // console.error("Error importing DeviceInfo:", error);
16
18
  }
17
19
 
20
+ let LocalStorage;
21
+ try {
22
+ LocalStorage = require("@react-native-async-storage/async-storage/src").default;
23
+ } catch (error) {
24
+ // console.error("Error importing LocalStorage:", error);
25
+ }
26
+
18
27
  import useAnimation from "./useAnimation";
19
28
  import Variables from "./Variables";
20
29
  import Api from "./Api";
@@ -50,8 +59,34 @@ export default ({
50
59
  labels = [],
51
60
  interceptResponse
52
61
  }) => {
53
- const [blacklists, setBlacklists] = useState([]);
54
- const { apis, clear } = useApiInterceptor(maxNumOfApiToStore, blacklists, interceptResponse);
62
+ const [blacklists, setB] = useState([]);
63
+
64
+ const setBlacklists = d => {
65
+ if (!d) {
66
+ setB([]);
67
+ LocalStorage?.removeItem('in-app-debugger-blacklist');
68
+ } else {
69
+ setB(v => {
70
+ const newValue = Array.isArray(d) ? d : [...v, d];
71
+ LocalStorage?.setItem('in-app-debugger-blacklist', JSON.stringify(newValue));
72
+ return newValue;
73
+ });
74
+ }
75
+ }
76
+
77
+ if (LocalStorage) {
78
+ useEffect(() => {
79
+ setTimeout(() => {
80
+ LocalStorage.getItem('in-app-debugger-blacklist').then(d => {
81
+ if (d) {
82
+ setBlacklists(JSON.parse(d));
83
+ }
84
+ });
85
+ }, 4000);
86
+ },[]);
87
+ }
88
+
89
+ const { apis, ...restApiInterceptor } = useApiInterceptor(maxNumOfApiToStore, blacklists, interceptResponse);
55
90
 
56
91
  const [tab, setTab] = useState("api");
57
92
 
@@ -154,16 +189,15 @@ export default ({
154
189
  );
155
190
  })}
156
191
  </View>
157
- <TouchableOpacity onPress={() => setIsOpen(false)}>
158
- <Text style={styles.close}>X</Text>
159
- </TouchableOpacity>
192
+ <X onPress={() => setIsOpen(false)} />
160
193
  </View>
161
194
  {tab === "variables" && !!variables && (
162
195
  <Variables variables={variables} />
163
196
  )}
164
197
  {tab === "api" && (
165
198
  <Api
166
- {...{apis, clear, setBlacklists, blacklists, maxNumOfApiToStore}}
199
+ {...{apis, setBlacklists, blacklists, maxNumOfApiToStore}}
200
+ {...restApiInterceptor}
167
201
  />
168
202
  )}
169
203
  </SafeAreaView>
@@ -191,12 +225,6 @@ const styles = StyleSheet.create({
191
225
  padding: 3,
192
226
  borderRadius: 999,
193
227
  },
194
- close: {
195
- color: "white",
196
- fontWeight: "bold",
197
- fontSize: 16,
198
- paddingHorizontal: 10,
199
- },
200
228
  labelContainer: {
201
229
  backgroundColor: "black",
202
230
  flexDirection: "row",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-in-app-debugger",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "This library's main usage is to be used by Non-Technical testers during UAT, SIT or any testing phase.",
5
5
  "main": "index.jsx",
6
6
  "scripts": {
@@ -1,13 +1,13 @@
1
- import { useEffect, useState } from "react";
2
- import XHRInterceptor from "react-native/Libraries/Network/XHRInterceptor.js";
1
+ import { useEffect, useState } from 'react';
2
+ import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor.js';
3
3
 
4
4
  const filterNonBusinessRelatedAPI = true;
5
5
 
6
6
  const shouldExclude = (url, method) =>
7
- ["HEAD"].includes(method) ||
8
- url.includes("codepush") ||
9
- url.includes("localhost") ||
10
- url.includes("applicationinsights.azure.com");
7
+ ['HEAD'].includes(method) ||
8
+ url.includes('codepush') ||
9
+ url.includes('localhost') ||
10
+ url.includes('applicationinsights.azure.com');
11
11
 
12
12
  const parse = (data) => {
13
13
  try {
@@ -19,13 +19,14 @@ const parse = (data) => {
19
19
 
20
20
  export default (maxNumOfApiToStore, blacklists, interceptResponse) => {
21
21
  const [apis, setApis] = useState([]);
22
+ const [bookmarks, setBookmarks] = useState({});
22
23
 
23
24
  const makeRequest = (data) => {
24
- if (blacklists.some(b => b.url === data.url && b.method === data.method)) return;
25
+ if (blacklists.some((b) => b.url === data.url && b.method === data.method)) return;
25
26
  const date = new Date();
26
27
  let hour = date.getHours();
27
- const minute = (date.getMinutes() + "").padStart(2, "0");
28
- const second = (date.getSeconds() + "").padStart(2, "0");
28
+ const minute = (date.getMinutes() + '').padStart(2, '0');
29
+ const second = (date.getSeconds() + '').padStart(2, '0');
29
30
 
30
31
  const request = {
31
32
  ...data,
@@ -42,10 +43,8 @@ export default (maxNumOfApiToStore, blacklists, interceptResponse) => {
42
43
  };
43
44
 
44
45
  useEffect(() => {
45
- setApis((v) => (
46
- v.filter(v => !blacklists.some(b => b.url === v.request.url && b.method === v.request.method))
47
- ))
48
- }, [blacklists])
46
+ setApis((v) => v.filter((v) => !blacklists.some((b) => b.url === v.request.url && b.method === v.request.method)));
47
+ }, [blacklists]);
49
48
 
50
49
  const receiveResponse = (data) => {
51
50
  const error = data.status < 200 || data.status >= 400;
@@ -54,12 +53,7 @@ export default (maxNumOfApiToStore, blacklists, interceptResponse) => {
54
53
  const oldData = [...v];
55
54
  for (let i = 0; i < oldData.length; i++) {
56
55
  const old = oldData[i];
57
- if (
58
- old.response ||
59
- old.request.url !== data.config.url ||
60
- old.request.method !== data.config.method
61
- )
62
- continue;
56
+ if (old.response || old.request.url !== data.config.url || old.request.method !== data.config.method) continue;
63
57
 
64
58
  oldData[i].response = {
65
59
  ...data,
@@ -77,7 +71,7 @@ export default (maxNumOfApiToStore, blacklists, interceptResponse) => {
77
71
  XHRInterceptor.enableInterception();
78
72
  // console.log('API interceptor status', XHRInterceptor.isInterceptorEnabled());
79
73
  XHRInterceptor.setSendCallback((...obj) => {
80
- obj[1].responseType = "text";
74
+ obj[1].responseType = 'text';
81
75
  const data = parse(obj[0]);
82
76
 
83
77
  const { _method: method, _url: url, _headers: headers } = obj[1];
@@ -112,5 +106,5 @@ export default (maxNumOfApiToStore, blacklists, interceptResponse) => {
112
106
  });
113
107
  }, []);
114
108
 
115
- return { apis, clear: () => setApis([]) };
109
+ return { apis, clear: () => setApis([]), bookmarks, setBookmarks };
116
110
  };
@@ -0,0 +1,51 @@
1
+ export default () => {
2
+ let h = Math.floor(Math.random() * 360); // Hue: 0 to 360 degrees
3
+ let s = Math.floor(Math.random() * 51) + 50; // Saturation: 50% to 100%
4
+ let l = Math.floor(Math.random() * 41) + 50; // Lightness: 50% to 90%
5
+
6
+ return hslToRgb(h, s, l);
7
+ };
8
+
9
+ function hslToRgb(h, s, l) {
10
+ s /= 100;
11
+ l /= 100;
12
+
13
+ let c = (1 - Math.abs(2 * l - 1)) * s;
14
+ let x = c * (1 - Math.abs(((h / 60) % 2) - 1));
15
+ let m = l - c / 2;
16
+ let r = 0,
17
+ g = 0,
18
+ b = 0;
19
+
20
+ if (0 <= h && h < 60) {
21
+ r = c;
22
+ g = x;
23
+ b = 0;
24
+ } else if (60 <= h && h < 120) {
25
+ r = x;
26
+ g = c;
27
+ b = 0;
28
+ } else if (120 <= h && h < 180) {
29
+ r = 0;
30
+ g = c;
31
+ b = x;
32
+ } else if (180 <= h && h < 240) {
33
+ r = 0;
34
+ g = x;
35
+ b = c;
36
+ } else if (240 <= h && h < 300) {
37
+ r = x;
38
+ g = 0;
39
+ b = c;
40
+ } else if (300 <= h && h < 360) {
41
+ r = c;
42
+ g = 0;
43
+ b = x;
44
+ }
45
+
46
+ r = Math.round((r + m) * 255);
47
+ g = Math.round((g + m) * 255);
48
+ b = Math.round((b + m) * 255);
49
+
50
+ return `rgb(${r}, ${g}, ${b})`;
51
+ }
package/Api.jsx DELETED
@@ -1,300 +0,0 @@
1
- import React, { useState } from "react";
2
- import {
3
- SectionList,
4
- TextInput,
5
- View,
6
- Alert,
7
- StyleSheet,
8
- TouchableOpacity,
9
- } from "react-native";
10
- import Text from "./Text";
11
- import Highlight from "./Highlight";
12
- let Clipboard;
13
- try {
14
- Clipboard = require("@react-native-clipboard/clipboard")?.default;
15
- } catch (error) {
16
- // console.error("Error importing Clipboard:", error);
17
- }
18
-
19
- const MAX_URL_LENGTH = 100;
20
-
21
- const Row = ({ item, filter }) => {
22
- const tabs = [
23
- { value: "Response Body" },
24
- { value: "Request Body", hide: !item.request.data },
25
- { value: "Request Header" },
26
- ];
27
- const [tab, setTab] = useState(tabs[0].value);
28
- const hasResponse = item.response;
29
- const Tab = ({ value, hide }) => {
30
- if (hide) return null;
31
- const isSelected = value === tab;
32
- return (
33
- <TouchableOpacity
34
- activeOpacity={isSelected ? 1 : 0.7}
35
- onPress={() => setTab(value)}
36
- style={[
37
- styles.selectionTab,
38
- { backgroundColor: isSelected ? "white" : undefined },
39
- ]}
40
- >
41
- <Text
42
- style={{
43
- color: isSelected ? "#000" : "#ffffff88",
44
- textAlign: "center",
45
- }}
46
- >
47
- {value}
48
- </Text>
49
- </TouchableOpacity>
50
- );
51
- };
52
-
53
- return (
54
- <View style={styles.details}>
55
- {item.request.url.length > MAX_URL_LENGTH && (
56
- <Text style={{ color: "#ffffff99", paddingVertical: 20 }}>
57
- <Highlight text={item.request.url} filter={filter} />
58
- </Text>
59
- )}
60
- <View>
61
- <View style={{ flexDirection: "row" }}>
62
- {tabs.map((t) => <Tab key={t.value} {...t} />)}
63
- </View>
64
-
65
- {tab === tabs[0].value && hasResponse && (
66
- <Text style={{ color: "white" }}>
67
- <Highlight
68
- text={JSON.stringify(item.response.data, undefined, 4)}
69
- filter={filter}
70
- />
71
- </Text>
72
- )}
73
- {tab === tabs[1].value && (
74
- <Text style={{ color: "white" }}>
75
- <Highlight
76
- text={JSON.stringify(item.request.data, undefined, 4)}
77
- filter={filter}
78
- />
79
- </Text>
80
- )}
81
- {tab === tabs[2].value && (
82
- <Text style={{ color: "white" }}>
83
- <Highlight
84
- text={JSON.stringify(item.request.headers, undefined, 4)}
85
- filter={filter}
86
- />
87
- </Text>
88
- )}
89
- </View>
90
- </View>
91
- );
92
- };
93
-
94
- const isError = (a) => a.response?.status < 200 || a.response?.status >= 400;
95
- export default (props) => {
96
- const [filter, setFilter] = useState("");
97
- const [errorOnly, setErrorOnly] = useState(false);
98
- const [expands, setExpands] = useState({});
99
- const apis = props.apis.filter((a) => !errorOnly || isError(a));
100
-
101
- const hasError = apis.some(isError);
102
-
103
- return (
104
- <>
105
- <View style={styles.container}>
106
- {!!apis.length && !filter && (
107
- <TouchableOpacity
108
- style={{ padding: 5, backgroundColor: "white", borderRadius: 5 }}
109
- onPress={() =>
110
- Alert.alert("Are you sure", "You want to clear all logs", [
111
- { text: "Delete", onPress: props.clear, style: "cancel" },
112
- { text: "Cancel" },
113
- ])
114
- }
115
- >
116
- <Text style={{ color: "black", fontSize: 10 }}>
117
- Clear {props.apis.length} APIs
118
- </Text>
119
- </TouchableOpacity>
120
- )}
121
- {hasError && !filter && (
122
- <TouchableOpacity
123
- style={{ padding: 5 }}
124
- onPress={() => setErrorOnly((v) => !v)}
125
- >
126
- <Text
127
- style={{
128
- color: "red",
129
- textDecorationLine: errorOnly ? "line-through" : undefined,
130
- fontSize: 10,
131
- }}
132
- >
133
- {apis.filter(isError).length} error
134
- {apis.filter(isError).length > 1 ? "s" : ""}
135
- </Text>
136
- </TouchableOpacity>
137
- )}
138
- {!!props.blacklists.length && !filter && (
139
- <TouchableOpacity
140
- style={{ padding: 5, backgroundColor: "white", borderRadius: 5 }}
141
- onPress={() =>
142
- Alert.alert("Are you sure", "You want to clear all blacklists", [
143
- { text: "Clear", onPress: () => props.setBlacklists([]), style: "cancel" },
144
- { text: "Cancel" },
145
- ])
146
- }
147
- >
148
- <Text style={{ color: "black", fontSize: 10 }}>
149
- Clear {props.blacklists.length} Blacklists
150
- </Text>
151
- </TouchableOpacity>
152
- )}
153
-
154
- <TextInput
155
- value={filter}
156
- placeholder="Filter..."
157
- clearButtonMode="always"
158
- placeholderTextColor="grey"
159
- style={{ paddingHorizontal: 5, color: "white", flex: 1 }}
160
- onChangeText={(t) => setFilter(t.toLowerCase())}
161
- />
162
- </View>
163
- {!filter &&
164
- !!props.maxNumOfApiToStore &&
165
- apis.length >= props.maxNumOfApiToStore && (
166
- <Text style={{ color: "#ffffff88", padding: 10 }}>
167
- Capped to only latest {props.maxNumOfApiToStore} APIs
168
- </Text>
169
- )}
170
- <SectionList
171
- keyExtractor={(i) => i.id}
172
- stickySectionHeadersEnabled
173
- showsVerticalScrollIndicator
174
- sections={apis
175
- .filter((a) =>
176
- !filter || JSON.stringify(a).toLowerCase().includes(filter)
177
- )
178
- .map((data) => ({ data: [data], id: data.id }))}
179
- renderItem={(i) =>
180
- expands[i.item.id] ? (
181
- <Row {...i} filter={filter} />
182
- ) : (
183
- <View style={{ height: 20 }} />
184
- )
185
- }
186
- renderSectionHeader={({ section: { data } }) => {
187
- const item = data[0];
188
- const hasResponse = !!item.response;
189
-
190
- const duration = item.response?.timestamp ? ~~(item.response?.timestamp - item.request.timestamp) / 1000 : 0;
191
- const isExpand = expands[item.id];
192
- const color = hasResponse ? item.response.error ? "red" : "white" : "yellow";
193
-
194
- return (
195
- <View style={styles.rowHeader}>
196
- <Text
197
- selectable
198
- style={{flex: 1, color, marginVertical: 10}}
199
- >
200
- <Text style={{ opacity: 0.7 }}>
201
- {item.request.method +
202
- ` (${item.response?.status ?? "no response"})` +
203
- " - " +
204
- item.request.time +
205
- (hasResponse ? " - " + duration + " second(s)" : "") +
206
- "\n"}
207
- </Text>
208
- <Highlight
209
- text={item.request.url.slice(0, MAX_URL_LENGTH)}
210
- filter={filter}
211
- />
212
- {item.request.url.length > MAX_URL_LENGTH && "......."}
213
- </Text>
214
- <View style={{ gap: 4 }}>
215
- <TouchableOpacity
216
- onPress={() =>
217
- setExpands((v) => {
218
- if (!isExpand) return { ...v, [item.id]: true };
219
- const newV = { ...v };
220
- delete newV[item.id];
221
- return newV;
222
- })
223
- }
224
- style={styles.actionButton}
225
- >
226
- <Text style={{ color: "black", fontSize: 10 }}>
227
- {isExpand ? "Hide" : "Show"}
228
- </Text>
229
- </TouchableOpacity>
230
- {!!Clipboard && (
231
- <TouchableOpacity
232
- onPress={() => {
233
- const content = { ...item };
234
- delete content.id;
235
- Clipboard.setString(JSON.stringify(content, undefined, 4));
236
- }}
237
- style={styles.actionButton}
238
- >
239
- <Text style={{ color: "black", fontSize: 10 }}>Copy</Text>
240
- </TouchableOpacity>
241
- )}
242
- <TouchableOpacity
243
- onPress={() => {
244
- Alert.alert("Are you sure", `You want to blacklist: \n\n(${item.request.method}) ${item.request.url} \n\nwhere all history logs for this API will be removed and all future request for this API will not be recorded?`, [
245
- { text: "Blacklist", onPress: () => props.setBlacklists(v => [...v, item.request]), style: "cancel" },
246
- { text: "Cancel" },
247
- ])
248
- }}
249
- style={styles.actionButton}
250
- >
251
- <Text style={{ color: "black", fontSize: 10 }}>Blacklist</Text>
252
- </TouchableOpacity>
253
- </View>
254
- </View>
255
- );
256
- }}
257
- />
258
- </>
259
- );
260
- };
261
-
262
- const styles = StyleSheet.create({
263
- container: {
264
- flexDirection: "row",
265
- paddingLeft: 5,
266
- alignItems: "center",
267
- gap: 5,
268
- },
269
- details: {
270
- padding: 5,
271
- backgroundColor: "#171717",
272
- paddingTop: 10,
273
- paddingBottom: 40,
274
- },
275
- actionButton: { backgroundColor: "white", borderRadius: 5, padding: 4 },
276
- rowHeader: {
277
- flexDirection: "row",
278
- gap: 5,
279
- backgroundColor: "black",
280
- padding: 5,
281
- paddingTop: 10,
282
- shadowColor: "black",
283
- shadowOffset: {
284
- width: 0,
285
- height: 2,
286
- },
287
- shadowRadius: 5,
288
- shadowOpacity: 1,
289
- // TODO shadow not working on android
290
- elevation: 10,
291
- zIndex: 99,
292
- },
293
- selectionTab: {
294
- borderBottomColor: "white",
295
- borderBottomWidth: 2,
296
- flex: 1,
297
- borderTopEndRadius: 10,
298
- borderTopStartRadius: 10,
299
- },
300
- });