react-native-in-app-debugger 1.0.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.
- package/Api.jsx +211 -0
- package/Variables.jsx +30 -0
- package/index.jsx +111 -0
- package/package.json +12 -0
- package/useAnimation.ts +55 -0
- package/useApiInterceptor.js +124 -0
package/Api.jsx
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Text, SectionList, TextInput, View, Alert, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import Clipboard from '@react-native-clipboard/clipboard';
|
|
4
|
+
|
|
5
|
+
const MAX_URL_LENGTH = 100;
|
|
6
|
+
|
|
7
|
+
const Row = ({ item }) => {
|
|
8
|
+
const [tab, setTab] = useState('response-body');
|
|
9
|
+
const hasResponse = item.response;
|
|
10
|
+
const Tab = ({ value, label }) => {
|
|
11
|
+
const isSelected = value === tab;
|
|
12
|
+
return (
|
|
13
|
+
<TouchableOpacity
|
|
14
|
+
activeOpacity={isSelected ? 1 : 0.7}
|
|
15
|
+
onPress={() => setTab(value)}
|
|
16
|
+
style={[styles.selectionTab, { backgroundColor: isSelected ? 'white' : undefined }]}
|
|
17
|
+
>
|
|
18
|
+
<Text style={{ color: isSelected ? '#000' : '#fff', textAlign: 'center' }}>{label}</Text>
|
|
19
|
+
</TouchableOpacity>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.details}>
|
|
24
|
+
{item.request.url.length > MAX_URL_LENGTH && (
|
|
25
|
+
<Text style={{ color: '#ffffff99', paddingVertical: 20 }}>{item.request.url}</Text>
|
|
26
|
+
)}
|
|
27
|
+
<View>
|
|
28
|
+
<View style={{ flexDirection: 'row' }}>
|
|
29
|
+
<Tab value='response-body' label='Response Body' />
|
|
30
|
+
{!!item.request.data && <Tab value='request-body' label='Request Body' />}
|
|
31
|
+
<Tab value='request-header' label='Request Header' />
|
|
32
|
+
</View>
|
|
33
|
+
|
|
34
|
+
{tab === 'response-body' && hasResponse && (
|
|
35
|
+
<Text style={{ color: 'white' }}>{JSON.stringify(item.response.data, undefined, 4)}</Text>
|
|
36
|
+
)}
|
|
37
|
+
{tab === 'request-body' && (
|
|
38
|
+
<Text style={{ color: 'white' }}>{JSON.stringify(item.request.data, undefined, 4)}</Text>
|
|
39
|
+
)}
|
|
40
|
+
{tab === 'request-header' && (
|
|
41
|
+
<Text style={{ color: 'white' }}>{JSON.stringify(item.request.headers, undefined, 4)}</Text>
|
|
42
|
+
)}
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default (props) => {
|
|
49
|
+
const [filter, setFilter] = useState('');
|
|
50
|
+
const [errorOnly, setErrorOnly] = useState(false);
|
|
51
|
+
const [filterUrlOnly, setFilterUrlOnly] = useState(true);
|
|
52
|
+
const [expands, setExpands] = useState({});
|
|
53
|
+
const apis = props.apis.filter((a) => !errorOnly || a.response?.status < 200 || a.response?.status >= 400);
|
|
54
|
+
|
|
55
|
+
const hasError = apis.some((a) => a.response?.status < 200 || a.response?.status >= 400);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<View style={{ flexDirection: 'row', paddingLeft: 5, alignItems: 'center', gap: 5 }}>
|
|
60
|
+
{!!apis.length && !filter && (
|
|
61
|
+
<TouchableOpacity
|
|
62
|
+
style={{ padding: 5, backgroundColor: 'white', borderRadius: 5 }}
|
|
63
|
+
onPress={() =>
|
|
64
|
+
Alert.alert('Are you sure', 'You want to clear all logs', [
|
|
65
|
+
{ text: 'Delete', onPress: props.clear, style: 'cancel' },
|
|
66
|
+
{ text: 'Cancel' },
|
|
67
|
+
])
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
<Text style={{ color: 'black' }}>Clear {apis.length} APIs</Text>
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
)}
|
|
73
|
+
{hasError && !filter && (
|
|
74
|
+
<TouchableOpacity style={{ padding: 5 }} onPress={() => setErrorOnly((v) => !v)}>
|
|
75
|
+
<Text style={{ color: 'red', textDecorationLine: errorOnly ? 'line-through' : undefined }}>Error Only</Text>
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
)}
|
|
78
|
+
<View style={{ flexDirection: 'row', flex: 1 }}>
|
|
79
|
+
<TextInput
|
|
80
|
+
value={filter}
|
|
81
|
+
placeholder='Filter...'
|
|
82
|
+
clearButtonMode='always'
|
|
83
|
+
placeholderTextColor='grey'
|
|
84
|
+
style={{ paddingHorizontal: 5, color: 'white', flex: 1 }}
|
|
85
|
+
onChangeText={(t) => setFilter(t.toLowerCase())}
|
|
86
|
+
/>
|
|
87
|
+
</View>
|
|
88
|
+
{!!filter && (
|
|
89
|
+
<TouchableOpacity style={{ padding: 5 }} onPress={() => setFilterUrlOnly((v) => !v)}>
|
|
90
|
+
<Text style={{ color: '#ffffff88' }}>{filterUrlOnly ? 'by URL' : 'by URL & body'}</Text>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
)}
|
|
93
|
+
</View>
|
|
94
|
+
<SectionList
|
|
95
|
+
contentContainerStyle={{ padding: 5 }}
|
|
96
|
+
keyExtractor={(i) => i.id}
|
|
97
|
+
stickySectionHeadersEnabled
|
|
98
|
+
sections={apis
|
|
99
|
+
.filter((a) =>
|
|
100
|
+
!filter || filterUrlOnly
|
|
101
|
+
? a.request.url.includes(filter)
|
|
102
|
+
: JSON.stringify(a).toLowerCase().includes(filter),
|
|
103
|
+
)
|
|
104
|
+
.map((data) => ({ data: [data], id: data.id }))}
|
|
105
|
+
renderItem={(i) => (expands[i.item.id] ? <Row {...i} /> : <View style={{ height: 20 }} />)}
|
|
106
|
+
renderSectionHeader={({ section: { data } }) => {
|
|
107
|
+
const item = data[0];
|
|
108
|
+
const hasResponse = !!item.response;
|
|
109
|
+
|
|
110
|
+
let duration = 0;
|
|
111
|
+
if (item.response?.datetime) {
|
|
112
|
+
let [hr, min, sec] = item.request.datetime.split(' ')[0].split(':');
|
|
113
|
+
const start = new Date();
|
|
114
|
+
start.setHours(hr);
|
|
115
|
+
start.setMinutes(min);
|
|
116
|
+
start.setSeconds(sec);
|
|
117
|
+
[hr, min, sec] = item.response.datetime.split(' ')[0].split(':');
|
|
118
|
+
const end = new Date();
|
|
119
|
+
end.setHours(hr);
|
|
120
|
+
end.setMinutes(min);
|
|
121
|
+
end.setSeconds(sec);
|
|
122
|
+
duration = (end.getTime() - start.getTime()) / 1000;
|
|
123
|
+
}
|
|
124
|
+
const isExpand = expands[item.id];
|
|
125
|
+
return (
|
|
126
|
+
<View style={styles.rowHeader}>
|
|
127
|
+
<Text
|
|
128
|
+
selectable
|
|
129
|
+
style={{
|
|
130
|
+
flex: 1,
|
|
131
|
+
color: hasResponse ? (item.response.error ? 'red' : 'white') : 'yellow',
|
|
132
|
+
marginVertical: 10,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<Text style={{ opacity: 0.7 }}>
|
|
136
|
+
{item.request.method +
|
|
137
|
+
` (${item.response?.status ?? 'no response'})` +
|
|
138
|
+
' - ' +
|
|
139
|
+
item.request.datetime +
|
|
140
|
+
(hasResponse ? ' - ' + duration + ' second(s)' : '') +
|
|
141
|
+
'\n'}
|
|
142
|
+
</Text>
|
|
143
|
+
{item.request.url.slice(0, MAX_URL_LENGTH)}
|
|
144
|
+
{item.request.url.length > MAX_URL_LENGTH && '.......'}
|
|
145
|
+
</Text>
|
|
146
|
+
<View style={{ gap: 4 }}>
|
|
147
|
+
<TouchableOpacity
|
|
148
|
+
onPress={() =>
|
|
149
|
+
setExpands((v) => {
|
|
150
|
+
if (!isExpand) return { ...v, [item.id]: true };
|
|
151
|
+
const newV = { ...v };
|
|
152
|
+
delete newV[item.id];
|
|
153
|
+
return newV;
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
style={styles.actionButton}
|
|
157
|
+
>
|
|
158
|
+
<Text style={{ color: 'black' }}>{isExpand ? 'Hide' : 'Show'}</Text>
|
|
159
|
+
</TouchableOpacity>
|
|
160
|
+
<TouchableOpacity
|
|
161
|
+
onPress={() => {
|
|
162
|
+
const content = { ...item };
|
|
163
|
+
delete content.id;
|
|
164
|
+
Clipboard.setString(JSON.stringify(content, undefined, 4));
|
|
165
|
+
}}
|
|
166
|
+
style={styles.actionButton}
|
|
167
|
+
>
|
|
168
|
+
<Text style={{ color: 'black' }}>Copy</Text>
|
|
169
|
+
</TouchableOpacity>
|
|
170
|
+
</View>
|
|
171
|
+
</View>
|
|
172
|
+
);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const styles = StyleSheet.create({
|
|
180
|
+
details: {
|
|
181
|
+
padding: 5,
|
|
182
|
+
backgroundColor: '#171717',
|
|
183
|
+
paddingTop: 10,
|
|
184
|
+
paddingBottom: 40,
|
|
185
|
+
},
|
|
186
|
+
actionButton: { backgroundColor: 'white', borderRadius: 5, padding: 4 },
|
|
187
|
+
rowHeader: {
|
|
188
|
+
flexDirection: 'row',
|
|
189
|
+
gap: 5,
|
|
190
|
+
backgroundColor: 'black',
|
|
191
|
+
padding: 5,
|
|
192
|
+
paddingTop: 20,
|
|
193
|
+
shadowColor: 'black',
|
|
194
|
+
shadowOffset: {
|
|
195
|
+
width: 0,
|
|
196
|
+
height: 2,
|
|
197
|
+
},
|
|
198
|
+
shadowRadius: 5,
|
|
199
|
+
shadowOpacity: 1,
|
|
200
|
+
// TODO shadow not working on android
|
|
201
|
+
elevation: 10,
|
|
202
|
+
zIndex: 99,
|
|
203
|
+
},
|
|
204
|
+
selectionTab: {
|
|
205
|
+
borderBottomColor: 'white',
|
|
206
|
+
borderBottomWidth: 2,
|
|
207
|
+
flex: 1,
|
|
208
|
+
borderTopEndRadius: 10,
|
|
209
|
+
borderTopStartRadius: 10,
|
|
210
|
+
},
|
|
211
|
+
});
|
package/Variables.jsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Text, FlatList, TextInput } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export default ({ envs }) => {
|
|
5
|
+
const [filter, setFilter] = useState('');
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<TextInput
|
|
10
|
+
value={filter}
|
|
11
|
+
placeholder='Filter...'
|
|
12
|
+
placeholderTextColor='grey'
|
|
13
|
+
style={{ paddingHorizontal: 5, color: 'white' }}
|
|
14
|
+
onChangeText={(t) => setFilter(t.toLowerCase())}
|
|
15
|
+
/>
|
|
16
|
+
<FlatList
|
|
17
|
+
contentContainerStyle={{ padding: 5, paddingBottom: 20 }}
|
|
18
|
+
data={Object.keys(envs).filter(
|
|
19
|
+
(k) => !filter || envs[k].toLowerCase().includes(filter) || k.toLowerCase().includes(filter),
|
|
20
|
+
)}
|
|
21
|
+
keyExtractor={(i) => i}
|
|
22
|
+
renderItem={({ item }) => (
|
|
23
|
+
<Text selectable style={{ color: 'white', marginVertical: 10 }}>
|
|
24
|
+
{item + ' : ' + envs[item]}
|
|
25
|
+
</Text>
|
|
26
|
+
)}
|
|
27
|
+
/>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
};
|
package/index.jsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
// hooks are prevented to be called conditionally, but in this case, bundle id / package name will never changed in run time, so it is safe to call the hooks under that condition
|
|
3
|
+
|
|
4
|
+
import useAnimation from './useAnimation';
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { Animated, Text, StyleSheet, TouchableOpacity, View, SafeAreaView, Dimensions } from 'react-native';
|
|
7
|
+
import DeviceInfo from 'react-native-device-info';
|
|
8
|
+
import Variables from './Variables';
|
|
9
|
+
import Api from './Api';
|
|
10
|
+
import useApiInterceptor from './useApiInterceptor';
|
|
11
|
+
|
|
12
|
+
const dimension = Dimensions.get('window');
|
|
13
|
+
|
|
14
|
+
const Label = (props) => <Text {...props} numberOfLines={1} ellipsizeMode='tail' style={[styles.label, props.style]} />;
|
|
15
|
+
|
|
16
|
+
export default ({ variables, env }) => {
|
|
17
|
+
const { apis, clear } = useApiInterceptor();
|
|
18
|
+
|
|
19
|
+
const [tab, setTab] = useState('api');
|
|
20
|
+
|
|
21
|
+
const errors = apis.filter((a) => a.response?.error).length;
|
|
22
|
+
const numPendingApiCalls = apis.filter((a) => !a.response).length;
|
|
23
|
+
let badgeHeight = 30;
|
|
24
|
+
if (variables.GIT_BRANCH) badgeHeight += 10;
|
|
25
|
+
if (variables.BUILD_DATE_TIME) badgeHeight += 10;
|
|
26
|
+
|
|
27
|
+
const { translateX, translateY, borderRadius, width, height, isOpen, panResponder, setIsOpen } =
|
|
28
|
+
useAnimation(badgeHeight);
|
|
29
|
+
return (
|
|
30
|
+
<Animated.View
|
|
31
|
+
style={{
|
|
32
|
+
transform: [{ translateX }, { translateY }],
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
borderRadius,
|
|
35
|
+
backgroundColor: '#000000' + (isOpen ? 'dd' : 'bb'),
|
|
36
|
+
height,
|
|
37
|
+
width,
|
|
38
|
+
}}
|
|
39
|
+
{...(isOpen ? {} : panResponder.panHandlers)}
|
|
40
|
+
>
|
|
41
|
+
{!isOpen ? (
|
|
42
|
+
<TouchableOpacity onPress={() => setIsOpen(true)} style={styles.box}>
|
|
43
|
+
<View style={styles.badgeContainer}>
|
|
44
|
+
{!!numPendingApiCalls && (
|
|
45
|
+
<View style={[styles.badge, { backgroundColor: 'orange' }]}>
|
|
46
|
+
<Text style={{ fontSize: 8, color: 'white' }}>{numPendingApiCalls}</Text>
|
|
47
|
+
</View>
|
|
48
|
+
)}
|
|
49
|
+
{!!errors && (
|
|
50
|
+
<View style={[styles.badge, { backgroundColor: 'red' }]}>
|
|
51
|
+
<Text style={{ fontSize: 8, color: 'white' }}>{errors}</Text>
|
|
52
|
+
</View>
|
|
53
|
+
)}
|
|
54
|
+
</View>
|
|
55
|
+
<Label>{env + ' ' + DeviceInfo.getReadableVersion()}</Label>
|
|
56
|
+
<Label style={{ fontSize: 6 }}>{DeviceInfo.getDeviceId() + ' ' + DeviceInfo.getSystemVersion()}</Label>
|
|
57
|
+
<Label style={{ fontSize: 6 }}>{dimension.width + 'x' + dimension.height}</Label>
|
|
58
|
+
{variables.GIT_BRANCH && <Label style={{ fontSize: 6 }}>{variables.GIT_BRANCH}</Label>}
|
|
59
|
+
{variables.BUILD_DATE_TIME && <Label style={{ fontSize: 6 }}>{variables.BUILD_DATE_TIME}</Label>}
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
) : (
|
|
62
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
63
|
+
<View style={{ flexDirection: 'row', padding: 5 }}>
|
|
64
|
+
<View style={{ flex: 1, flexDirection: 'row' }}>
|
|
65
|
+
{['api', 'env'].map((t) => {
|
|
66
|
+
const isSelected = t === tab;
|
|
67
|
+
return (
|
|
68
|
+
<TouchableOpacity
|
|
69
|
+
onPress={() => setTab(t)}
|
|
70
|
+
activeOpacity={isSelected ? 1 : 0.7}
|
|
71
|
+
key={t}
|
|
72
|
+
style={{ flex: 1, borderBottomWidth: +isSelected, borderColor: 'white' }}
|
|
73
|
+
>
|
|
74
|
+
<Text style={{ color: 'white', textAlign: 'center' }}>{t.toUpperCase()}</Text>
|
|
75
|
+
</TouchableOpacity>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</View>
|
|
79
|
+
<TouchableOpacity onPress={() => setIsOpen(false)}>
|
|
80
|
+
<Text style={styles.close}>X</Text>
|
|
81
|
+
</TouchableOpacity>
|
|
82
|
+
</View>
|
|
83
|
+
{tab === 'env' && <Variables variables={variables} />}
|
|
84
|
+
{tab === 'api' && <Api apis={apis} clear={clear} />}
|
|
85
|
+
</SafeAreaView>
|
|
86
|
+
)}
|
|
87
|
+
</Animated.View>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const styles = StyleSheet.create({
|
|
92
|
+
box: {
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
width: '100%',
|
|
95
|
+
height: '100%',
|
|
96
|
+
},
|
|
97
|
+
label: { color: 'white', textAlign: 'center', fontSize: 8 },
|
|
98
|
+
badgeContainer: {
|
|
99
|
+
gap: 3,
|
|
100
|
+
flexDirection: 'row',
|
|
101
|
+
top: -8,
|
|
102
|
+
right: -3,
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
zIndex: 999,
|
|
105
|
+
},
|
|
106
|
+
badge: {
|
|
107
|
+
padding: 4,
|
|
108
|
+
borderRadius: 999,
|
|
109
|
+
},
|
|
110
|
+
close: { color: 'white', fontWeight: 'bold', fontSize: 16, paddingHorizontal: 10 },
|
|
111
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-in-app-debugger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.jsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "fattahmuhyiddeen",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
package/useAnimation.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, PanResponder, useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const defaultBadgeWidth = 80;
|
|
5
|
+
const defaultBorderRadius = 20;
|
|
6
|
+
const und = { useNativeDriver: false };
|
|
7
|
+
const touchThreshold = 10;
|
|
8
|
+
|
|
9
|
+
export default (defaultBadgeHeight) => {
|
|
10
|
+
const cachePosition = useRef({ x: 0, y: 50 });
|
|
11
|
+
const position = useRef<any>(new Animated.ValueXY(cachePosition.current)).current;
|
|
12
|
+
const borderRadius = useRef(new Animated.Value(defaultBorderRadius)).current;
|
|
13
|
+
const badgeHeight = useRef(new Animated.Value(defaultBadgeHeight)).current;
|
|
14
|
+
const badgeWidth = useRef(new Animated.Value(defaultBadgeWidth)).current;
|
|
15
|
+
const { width, height } = useWindowDimensions();
|
|
16
|
+
|
|
17
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
18
|
+
|
|
19
|
+
const panResponder = useRef(
|
|
20
|
+
PanResponder.create({
|
|
21
|
+
onStartShouldSetPanResponder: () => false,
|
|
22
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
23
|
+
const { dx, dy } = gestureState;
|
|
24
|
+
return Math.abs(dx) > touchThreshold || Math.abs(dy) > touchThreshold;
|
|
25
|
+
},
|
|
26
|
+
onPanResponderGrant: () => {
|
|
27
|
+
position.setOffset({ x: position.x._value, y: position.y._value });
|
|
28
|
+
},
|
|
29
|
+
onPanResponderMove: Animated.event([null, { dx: position.x, dy: position.y }], und),
|
|
30
|
+
onPanResponderRelease: (_, g) => {
|
|
31
|
+
position.flattenOffset();
|
|
32
|
+
cachePosition.current = { x: g.moveX > width / 2 ? width - defaultBadgeWidth : 0, y: g.moveY };
|
|
33
|
+
Animated.spring(position, { ...und, toValue: cachePosition.current }).start();
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
).current;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
Animated.spring(position, { toValue: isOpen ? { x: 0, y: 0 } : cachePosition.current, ...und }).start();
|
|
40
|
+
Animated.spring(borderRadius, { toValue: isOpen ? 0 : defaultBorderRadius, ...und }).start();
|
|
41
|
+
Animated.spring(badgeHeight, { toValue: isOpen ? height : defaultBadgeHeight, ...und }).start();
|
|
42
|
+
Animated.spring(badgeWidth, { toValue: isOpen ? width : defaultBadgeWidth, ...und }).start();
|
|
43
|
+
}, [isOpen]);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
height: badgeHeight,
|
|
47
|
+
width: badgeWidth,
|
|
48
|
+
panResponder,
|
|
49
|
+
translateX: position.x,
|
|
50
|
+
translateY: position.y,
|
|
51
|
+
isOpen,
|
|
52
|
+
setIsOpen,
|
|
53
|
+
borderRadius,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor.js';
|
|
4
|
+
|
|
5
|
+
const filterNonBusinessRelatedAPI = true;
|
|
6
|
+
const MAX_NUM_OF_API = 50;
|
|
7
|
+
|
|
8
|
+
const now = () =>
|
|
9
|
+
new Date().toLocaleTimeString('en-US', {
|
|
10
|
+
hour: 'numeric',
|
|
11
|
+
minute: 'numeric',
|
|
12
|
+
second: 'numeric',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const shouldExclude = (url, method) =>
|
|
16
|
+
['HEAD'].includes(method) ||
|
|
17
|
+
url.includes('codepush') ||
|
|
18
|
+
url.includes('localhost') ||
|
|
19
|
+
url.includes('applicationinsights.azure.com');
|
|
20
|
+
|
|
21
|
+
const parse = (data) => {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default () => {
|
|
30
|
+
const [apis, setApis] = useState([]);
|
|
31
|
+
|
|
32
|
+
const makeRequest = (data) => {
|
|
33
|
+
const request = {
|
|
34
|
+
...data,
|
|
35
|
+
datetime: now(),
|
|
36
|
+
};
|
|
37
|
+
setApis((v) =>
|
|
38
|
+
[{ request, id: Date.now().toString(36) + Math.random().toString(36) }, ...v].slice(0, MAX_NUM_OF_API),
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const receiveResponse = (data) => {
|
|
43
|
+
const error = data.status < 200 || data.status >= 400;
|
|
44
|
+
|
|
45
|
+
setApis((v) => {
|
|
46
|
+
const oldData = [...v];
|
|
47
|
+
for (let i = 0; i < oldData.length; i++) {
|
|
48
|
+
const old = oldData[i];
|
|
49
|
+
if (old.response || old.request.url !== data.config.url || old.request.method !== data.config.method) continue;
|
|
50
|
+
|
|
51
|
+
oldData[i].response = {
|
|
52
|
+
...data,
|
|
53
|
+
datetime: now(),
|
|
54
|
+
error,
|
|
55
|
+
};
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return oldData;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
XHRInterceptor.enableInterception();
|
|
65
|
+
// console.log('API interceptor status', XHRInterceptor.isInterceptorEnabled());
|
|
66
|
+
XHRInterceptor.setSendCallback((...obj) => {
|
|
67
|
+
const data = parse(obj[0]);
|
|
68
|
+
|
|
69
|
+
const { _method: method, _url: url, _headers: headers } = obj[1];
|
|
70
|
+
if (filterNonBusinessRelatedAPI) {
|
|
71
|
+
if (shouldExclude(url, method)) return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
makeRequest({
|
|
75
|
+
url,
|
|
76
|
+
headers,
|
|
77
|
+
method,
|
|
78
|
+
data,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
XHRInterceptor.setResponseCallback((...obj) => {
|
|
83
|
+
const xhr = obj[5];
|
|
84
|
+
const { _method: method, _url: url, _response, status } = xhr;
|
|
85
|
+
if (filterNonBusinessRelatedAPI) {
|
|
86
|
+
if (shouldExclude(url, method)) return;
|
|
87
|
+
}
|
|
88
|
+
const data = parse(_response);
|
|
89
|
+
|
|
90
|
+
xhr.addEventListener('load', function () {
|
|
91
|
+
try {
|
|
92
|
+
const reader = new FileReader();
|
|
93
|
+
reader.readAsText(xhr.response);
|
|
94
|
+
reader.onload = function () {
|
|
95
|
+
const response = JSON.parse(reader.result);
|
|
96
|
+
receiveResponse({
|
|
97
|
+
config: {
|
|
98
|
+
url,
|
|
99
|
+
method,
|
|
100
|
+
},
|
|
101
|
+
data: response,
|
|
102
|
+
status,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log(e);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!data.blobId) {
|
|
111
|
+
receiveResponse({
|
|
112
|
+
config: {
|
|
113
|
+
url,
|
|
114
|
+
method,
|
|
115
|
+
},
|
|
116
|
+
data,
|
|
117
|
+
status,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
return { apis, clear: () => setApis([]) };
|
|
124
|
+
};
|