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 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
+ }
@@ -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
+ };