@zizwar/react-native-debug-console 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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * React Native Debug Console - Floating Button
3
+ * A floating action button that shows error count and toggles the debug console
4
+ */
5
+
6
+ import React from 'react';
7
+ import {
8
+ TouchableOpacity,
9
+ View,
10
+ Text,
11
+ StyleSheet,
12
+ Platform,
13
+ } from 'react-native';
14
+ import { useDebugConsole } from './DebugProvider';
15
+ import { DebugIcon } from './Icons';
16
+
17
+ export const DebugButton: React.FC = () => {
18
+ const { toggleVisibility, logs, config } = useDebugConsole();
19
+
20
+ if (!config.enabled) return null;
21
+
22
+ const errorCount = logs.filter(l => l.type === 'error').length;
23
+ const position = config.buttonPosition;
24
+ const colors = config.colors;
25
+
26
+ return (
27
+ <TouchableOpacity
28
+ style={[
29
+ styles.floatingButton,
30
+ { backgroundColor: colors.buttonBackground },
31
+ position.bottom !== undefined && { bottom: position.bottom },
32
+ position.top !== undefined && { top: position.top },
33
+ position.right !== undefined && { right: position.right },
34
+ position.left !== undefined && { left: position.left },
35
+ ]}
36
+ onPress={toggleVisibility}
37
+ activeOpacity={0.8}
38
+ >
39
+ <DebugIcon name="bug" size={20} color={colors.buttonIcon} />
40
+ {errorCount > 0 && (
41
+ <View style={styles.errorBadge}>
42
+ <Text style={[styles.errorBadgeText, { color: colors.buttonBackground }]}>
43
+ {errorCount > 9 ? '9+' : errorCount}
44
+ </Text>
45
+ </View>
46
+ )}
47
+ </TouchableOpacity>
48
+ );
49
+ };
50
+
51
+ const styles = StyleSheet.create({
52
+ floatingButton: {
53
+ position: 'absolute',
54
+ width: 50,
55
+ height: 50,
56
+ borderRadius: 25,
57
+ justifyContent: 'center',
58
+ alignItems: 'center',
59
+ elevation: 5,
60
+ shadowColor: '#000',
61
+ shadowOffset: { width: 0, height: 2 },
62
+ shadowOpacity: 0.3,
63
+ shadowRadius: 4,
64
+ zIndex: 9999,
65
+ },
66
+ errorBadge: {
67
+ position: 'absolute',
68
+ top: -4,
69
+ right: -4,
70
+ backgroundColor: '#fff',
71
+ borderRadius: 10,
72
+ minWidth: 20,
73
+ height: 20,
74
+ justifyContent: 'center',
75
+ alignItems: 'center',
76
+ paddingHorizontal: 4,
77
+ },
78
+ errorBadgeText: {
79
+ fontSize: 11,
80
+ fontWeight: 'bold',
81
+ },
82
+ });
83
+
84
+ export default DebugButton;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * React Native Debug Console - Main Component
3
+ * All-in-one wrapper that provides the debug console functionality
4
+ */
5
+
6
+ import React, { ReactNode } from 'react';
7
+ import { View, StyleSheet } from 'react-native';
8
+ import { DebugProvider } from './DebugProvider';
9
+ import { DebugOverlay } from './DebugOverlay';
10
+ import { DebugButton } from './DebugButton';
11
+ import { DebugConsoleConfig } from './types';
12
+
13
+ interface DebugConsoleProps {
14
+ children: ReactNode;
15
+ /** Configuration options */
16
+ config?: DebugConsoleConfig;
17
+ /** Show the floating button (default: true) */
18
+ showButton?: boolean;
19
+ /** Custom floating button component */
20
+ customButton?: React.ComponentType;
21
+ /** Custom overlay component */
22
+ customOverlay?: React.ComponentType;
23
+ }
24
+
25
+ /**
26
+ * DebugConsole - Main wrapper component
27
+ *
28
+ * Wrap your app with this component to enable debug console functionality.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * import { DebugConsole } from 'react-native-debug-console';
33
+ *
34
+ * export default function App() {
35
+ * return (
36
+ * <DebugConsole
37
+ * config={{
38
+ * enabled: process.env.EXPO_PUBLIC_DEBUG === 'true',
39
+ * appName: 'MyApp',
40
+ * }}
41
+ * >
42
+ * <YourApp />
43
+ * </DebugConsole>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ export const DebugConsole: React.FC<DebugConsoleProps> = ({
49
+ children,
50
+ config,
51
+ showButton = true,
52
+ customButton: CustomButton,
53
+ customOverlay: CustomOverlay,
54
+ }) => {
55
+ // If disabled, just render children without any debug UI
56
+ if (config?.enabled === false) {
57
+ return <>{children}</>;
58
+ }
59
+
60
+ const ButtonComponent = CustomButton || DebugButton;
61
+ const OverlayComponent = CustomOverlay || DebugOverlay;
62
+
63
+ return (
64
+ <DebugProvider config={config}>
65
+ <View style={styles.container}>
66
+ {children}
67
+ {showButton && <ButtonComponent />}
68
+ <OverlayComponent />
69
+ </View>
70
+ </DebugProvider>
71
+ );
72
+ };
73
+
74
+ /**
75
+ * withDebugConsole - HOC to wrap any component with debug console
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * import { withDebugConsole } from 'react-native-debug-console';
80
+ *
81
+ * const App = () => <YourApp />;
82
+ *
83
+ * export default withDebugConsole(App, {
84
+ * enabled: __DEV__,
85
+ * appName: 'MyApp',
86
+ * });
87
+ * ```
88
+ */
89
+ export function withDebugConsole<P extends object>(
90
+ WrappedComponent: React.ComponentType<P>,
91
+ config?: DebugConsoleConfig
92
+ ) {
93
+ const WithDebugConsole: React.FC<P> = (props) => {
94
+ return (
95
+ <DebugConsole config={config}>
96
+ <WrappedComponent {...props} />
97
+ </DebugConsole>
98
+ );
99
+ };
100
+
101
+ WithDebugConsole.displayName = `WithDebugConsole(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
102
+
103
+ return WithDebugConsole;
104
+ }
105
+
106
+ const styles = StyleSheet.create({
107
+ container: {
108
+ flex: 1,
109
+ },
110
+ });
111
+
112
+ export default DebugConsole;
@@ -0,0 +1,310 @@
1
+ /**
2
+ * React Native Debug Console - Overlay
3
+ * Full-screen modal displaying captured logs
4
+ */
5
+
6
+ import React from 'react';
7
+ import {
8
+ View,
9
+ Text,
10
+ ScrollView,
11
+ TouchableOpacity,
12
+ StyleSheet,
13
+ Modal,
14
+ SafeAreaView,
15
+ Share,
16
+ Platform,
17
+ } from 'react-native';
18
+ import { useDebugConsole } from './DebugProvider';
19
+ import { LogEntry } from './types';
20
+ import { DebugIcon } from './Icons';
21
+
22
+ interface LogEntryItemProps {
23
+ entry: LogEntry;
24
+ colors: {
25
+ surface: string;
26
+ text: string;
27
+ textSecondary: string;
28
+ error: string;
29
+ warning: string;
30
+ info: string;
31
+ log: string;
32
+ };
33
+ }
34
+
35
+ const LogEntryItem: React.FC<LogEntryItemProps> = ({ entry, colors }) => {
36
+ const getTypeColor = (type: LogEntry['type']) => {
37
+ switch (type) {
38
+ case 'error': return colors.error;
39
+ case 'warn': return colors.warning;
40
+ case 'info': return colors.info;
41
+ default: return colors.textSecondary;
42
+ }
43
+ };
44
+
45
+ const getTypeIcon = (type: LogEntry['type']): 'close-circle' | 'warning' | 'information-circle' | 'ellipse' => {
46
+ switch (type) {
47
+ case 'error': return 'close-circle';
48
+ case 'warn': return 'warning';
49
+ case 'info': return 'information-circle';
50
+ default: return 'ellipse';
51
+ }
52
+ };
53
+
54
+ const formatTime = (date: Date) => {
55
+ return date.toLocaleTimeString('en-US', {
56
+ hour12: false,
57
+ hour: '2-digit',
58
+ minute: '2-digit',
59
+ second: '2-digit',
60
+ });
61
+ };
62
+
63
+ return (
64
+ <View style={[
65
+ styles.logEntry,
66
+ { backgroundColor: colors.surface, borderLeftColor: getTypeColor(entry.type) }
67
+ ]}>
68
+ <View style={styles.logHeader}>
69
+ <DebugIcon name={getTypeIcon(entry.type)} size={14} color={getTypeColor(entry.type)} />
70
+ <Text style={[styles.logTime, { color: colors.textSecondary }]}>
71
+ {formatTime(entry.timestamp)}
72
+ </Text>
73
+ <Text style={[styles.logType, { color: getTypeColor(entry.type) }]}>
74
+ {entry.type.toUpperCase()}
75
+ </Text>
76
+ </View>
77
+ <Text style={[styles.logMessage, { color: colors.text }]} selectable>
78
+ {entry.message}
79
+ </Text>
80
+ </View>
81
+ );
82
+ };
83
+
84
+ export const DebugOverlay: React.FC = () => {
85
+ const { logs, isVisible, hideConsole, clearLogs, config } = useDebugConsole();
86
+ const colors = config.colors;
87
+
88
+ const handleShare = async () => {
89
+ const logText = logs.map(log =>
90
+ `[${log.timestamp.toISOString()}] [${log.type.toUpperCase()}] ${log.message}`
91
+ ).join('\n');
92
+
93
+ try {
94
+ await Share.share({
95
+ message: logText,
96
+ title: `${config.appName} Debug Logs`,
97
+ });
98
+ } catch (error) {
99
+ // Silently fail if sharing not available
100
+ }
101
+ };
102
+
103
+ if (!isVisible || !config.enabled) return null;
104
+
105
+ const errorCount = logs.filter(l => l.type === 'error').length;
106
+ const warnCount = logs.filter(l => l.type === 'warn').length;
107
+
108
+ return (
109
+ <Modal
110
+ visible={isVisible}
111
+ animationType="slide"
112
+ transparent={false}
113
+ onRequestClose={hideConsole}
114
+ >
115
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
116
+ {/* Header */}
117
+ <View style={[styles.header, { borderBottomColor: colors.surface }]}>
118
+ <View style={styles.headerLeft}>
119
+ <DebugIcon name="bug" size={24} color={colors.error} />
120
+ <Text style={[styles.headerTitle, { color: colors.text }]}>Debug Console</Text>
121
+ </View>
122
+ <View style={styles.headerStats}>
123
+ {errorCount > 0 && (
124
+ <View style={[styles.statBadge, { backgroundColor: colors.error }]}>
125
+ <Text style={styles.statText}>{errorCount} errors</Text>
126
+ </View>
127
+ )}
128
+ {warnCount > 0 && (
129
+ <View style={[styles.statBadge, { backgroundColor: colors.warning }]}>
130
+ <Text style={[styles.statText, { color: '#000' }]}>{warnCount} warns</Text>
131
+ </View>
132
+ )}
133
+ </View>
134
+ <TouchableOpacity onPress={hideConsole} style={styles.closeButton}>
135
+ <DebugIcon name="close" size={24} color={colors.text} />
136
+ </TouchableOpacity>
137
+ </View>
138
+
139
+ {/* Actions */}
140
+ <View style={[styles.actions, { borderBottomColor: colors.surface }]}>
141
+ <TouchableOpacity
142
+ style={[styles.actionButton, { backgroundColor: colors.surface }]}
143
+ onPress={clearLogs}
144
+ >
145
+ <DebugIcon name="trash" size={18} color={colors.error} />
146
+ <Text style={[styles.actionText, { color: colors.text }]}>Clear</Text>
147
+ </TouchableOpacity>
148
+ <TouchableOpacity
149
+ style={[styles.actionButton, { backgroundColor: colors.surface }]}
150
+ onPress={handleShare}
151
+ >
152
+ <DebugIcon name="share" size={18} color={colors.info} />
153
+ <Text style={[styles.actionText, { color: colors.text }]}>Share</Text>
154
+ </TouchableOpacity>
155
+ <Text style={[styles.logCount, { color: colors.textSecondary }]}>
156
+ {logs.length} logs
157
+ </Text>
158
+ </View>
159
+
160
+ {/* Logs */}
161
+ <ScrollView
162
+ style={styles.logsContainer}
163
+ contentContainerStyle={styles.logsContent}
164
+ showsVerticalScrollIndicator={true}
165
+ >
166
+ {logs.length === 0 ? (
167
+ <View style={styles.emptyState}>
168
+ <DebugIcon name="document" size={48} color={colors.textSecondary} />
169
+ <Text style={[styles.emptyText, { color: colors.textSecondary }]}>
170
+ No logs yet
171
+ </Text>
172
+ <Text style={[styles.emptySubtext, { color: colors.textSecondary }]}>
173
+ Console logs will appear here
174
+ </Text>
175
+ </View>
176
+ ) : (
177
+ logs.map(entry => (
178
+ <LogEntryItem key={entry.id} entry={entry} colors={colors} />
179
+ ))
180
+ )}
181
+ </ScrollView>
182
+
183
+ {/* Footer */}
184
+ <View style={[styles.footer, { borderTopColor: colors.surface }]}>
185
+ <Text style={[styles.footerText, { color: colors.textSecondary }]}>
186
+ Tap on log to copy • {config.appName}
187
+ </Text>
188
+ </View>
189
+ </SafeAreaView>
190
+ </Modal>
191
+ );
192
+ };
193
+
194
+ const styles = StyleSheet.create({
195
+ container: {
196
+ flex: 1,
197
+ },
198
+ header: {
199
+ flexDirection: 'row',
200
+ alignItems: 'center',
201
+ justifyContent: 'space-between',
202
+ paddingHorizontal: 16,
203
+ paddingVertical: 12,
204
+ borderBottomWidth: 1,
205
+ },
206
+ headerLeft: {
207
+ flexDirection: 'row',
208
+ alignItems: 'center',
209
+ gap: 8,
210
+ },
211
+ headerTitle: {
212
+ fontSize: 18,
213
+ fontWeight: 'bold',
214
+ },
215
+ headerStats: {
216
+ flexDirection: 'row',
217
+ gap: 8,
218
+ },
219
+ statBadge: {
220
+ paddingHorizontal: 8,
221
+ paddingVertical: 4,
222
+ borderRadius: 12,
223
+ },
224
+ statText: {
225
+ fontSize: 12,
226
+ fontWeight: 'bold',
227
+ color: '#fff',
228
+ },
229
+ closeButton: {
230
+ padding: 8,
231
+ },
232
+ actions: {
233
+ flexDirection: 'row',
234
+ alignItems: 'center',
235
+ paddingHorizontal: 16,
236
+ paddingVertical: 8,
237
+ borderBottomWidth: 1,
238
+ gap: 16,
239
+ },
240
+ actionButton: {
241
+ flexDirection: 'row',
242
+ alignItems: 'center',
243
+ gap: 6,
244
+ paddingVertical: 6,
245
+ paddingHorizontal: 12,
246
+ borderRadius: 8,
247
+ },
248
+ actionText: {
249
+ fontSize: 14,
250
+ },
251
+ logCount: {
252
+ marginLeft: 'auto',
253
+ fontSize: 12,
254
+ },
255
+ logsContainer: {
256
+ flex: 1,
257
+ },
258
+ logsContent: {
259
+ padding: 8,
260
+ },
261
+ logEntry: {
262
+ borderRadius: 8,
263
+ padding: 10,
264
+ marginBottom: 8,
265
+ borderLeftWidth: 3,
266
+ },
267
+ logHeader: {
268
+ flexDirection: 'row',
269
+ alignItems: 'center',
270
+ gap: 8,
271
+ marginBottom: 4,
272
+ },
273
+ logTime: {
274
+ fontSize: 11,
275
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
276
+ },
277
+ logType: {
278
+ fontSize: 10,
279
+ fontWeight: 'bold',
280
+ },
281
+ logMessage: {
282
+ fontSize: 12,
283
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
284
+ lineHeight: 18,
285
+ },
286
+ emptyState: {
287
+ alignItems: 'center',
288
+ justifyContent: 'center',
289
+ paddingVertical: 60,
290
+ },
291
+ emptyText: {
292
+ marginTop: 12,
293
+ fontSize: 16,
294
+ fontWeight: '600',
295
+ },
296
+ emptySubtext: {
297
+ marginTop: 4,
298
+ fontSize: 14,
299
+ },
300
+ footer: {
301
+ padding: 12,
302
+ borderTopWidth: 1,
303
+ alignItems: 'center',
304
+ },
305
+ footerText: {
306
+ fontSize: 12,
307
+ },
308
+ });
309
+
310
+ export default DebugOverlay;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * React Native Debug Console - Provider
3
+ * Context provider that captures console logs and manages state
4
+ */
5
+
6
+ import React, {
7
+ createContext,
8
+ useContext,
9
+ useState,
10
+ useCallback,
11
+ useEffect,
12
+ useMemo,
13
+ ReactNode,
14
+ } from 'react';
15
+ import {
16
+ LogEntry,
17
+ LogType,
18
+ DebugConsoleConfig,
19
+ DebugConsoleContextType,
20
+ DEFAULT_CONFIG,
21
+ } from './types';
22
+
23
+ const DebugConsoleContext = createContext<DebugConsoleContextType | undefined>(undefined);
24
+
25
+ // Store original console methods
26
+ const originalConsole = {
27
+ log: console.log.bind(console),
28
+ warn: console.warn.bind(console),
29
+ error: console.error.bind(console),
30
+ info: console.info.bind(console),
31
+ };
32
+
33
+ interface DebugProviderProps {
34
+ children: ReactNode;
35
+ config?: DebugConsoleConfig;
36
+ }
37
+
38
+ export const DebugProvider: React.FC<DebugProviderProps> = ({ children, config: userConfig }) => {
39
+ // Merge user config with defaults
40
+ const config = useMemo<Required<DebugConsoleConfig>>(() => {
41
+ return {
42
+ ...DEFAULT_CONFIG,
43
+ ...userConfig,
44
+ colors: {
45
+ ...DEFAULT_CONFIG.colors,
46
+ ...userConfig?.colors,
47
+ },
48
+ buttonPosition: {
49
+ ...DEFAULT_CONFIG.buttonPosition,
50
+ ...userConfig?.buttonPosition,
51
+ },
52
+ };
53
+ }, [userConfig]);
54
+
55
+ const [logs, setLogs] = useState<LogEntry[]>([]);
56
+ const [isVisible, setIsVisible] = useState(false);
57
+
58
+ const addLog = useCallback((type: LogType, message: string, data?: any) => {
59
+ const entry: LogEntry = {
60
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
61
+ timestamp: new Date(),
62
+ type,
63
+ message: typeof message === 'string' ? message : JSON.stringify(message),
64
+ data,
65
+ };
66
+
67
+ setLogs(prev => {
68
+ const newLogs = [...prev, entry];
69
+ // Keep only last N logs based on config
70
+ if (newLogs.length > config.maxLogs) {
71
+ return newLogs.slice(-config.maxLogs);
72
+ }
73
+ return newLogs;
74
+ });
75
+ }, [config.maxLogs]);
76
+
77
+ const clearLogs = useCallback(() => {
78
+ setLogs([]);
79
+ }, []);
80
+
81
+ const toggleVisibility = useCallback(() => {
82
+ setIsVisible(prev => !prev);
83
+ }, []);
84
+
85
+ const showConsole = useCallback(() => {
86
+ setIsVisible(true);
87
+ }, []);
88
+
89
+ const hideConsole = useCallback(() => {
90
+ setIsVisible(false);
91
+ }, []);
92
+
93
+ // Override console methods to capture logs
94
+ useEffect(() => {
95
+ if (!config.enabled) {
96
+ return;
97
+ }
98
+
99
+ const formatArgs = (args: any[]): string => {
100
+ return args.map(arg => {
101
+ if (typeof arg === 'object') {
102
+ try {
103
+ return JSON.stringify(arg, null, 2);
104
+ } catch {
105
+ return String(arg);
106
+ }
107
+ }
108
+ return String(arg);
109
+ }).join(' ');
110
+ };
111
+
112
+ const wrapConsole = (type: LogType, originalFn: (...args: any[]) => void) => {
113
+ return (...args: any[]) => {
114
+ // Always call original console method
115
+ originalFn(...args);
116
+
117
+ // Add to our logs
118
+ const message = formatArgs(args);
119
+ addLog(type, message);
120
+ };
121
+ };
122
+
123
+ // Override console methods based on config
124
+ if (config.captureLog) {
125
+ console.log = wrapConsole('log', originalConsole.log);
126
+ }
127
+ if (config.captureWarn) {
128
+ console.warn = wrapConsole('warn', originalConsole.warn);
129
+ }
130
+ if (config.captureError) {
131
+ console.error = wrapConsole('error', originalConsole.error);
132
+ }
133
+ if (config.captureInfo) {
134
+ console.info = wrapConsole('info', originalConsole.info);
135
+ }
136
+
137
+ // Restore original methods on cleanup
138
+ return () => {
139
+ console.log = originalConsole.log;
140
+ console.warn = originalConsole.warn;
141
+ console.error = originalConsole.error;
142
+ console.info = originalConsole.info;
143
+ };
144
+ }, [config.enabled, config.captureLog, config.captureWarn, config.captureError, config.captureInfo, addLog]);
145
+
146
+ const contextValue = useMemo<DebugConsoleContextType>(() => ({
147
+ logs,
148
+ isVisible,
149
+ config,
150
+ addLog,
151
+ clearLogs,
152
+ toggleVisibility,
153
+ showConsole,
154
+ hideConsole,
155
+ }), [logs, isVisible, config, addLog, clearLogs, toggleVisibility, showConsole, hideConsole]);
156
+
157
+ return (
158
+ <DebugConsoleContext.Provider value={contextValue}>
159
+ {children}
160
+ </DebugConsoleContext.Provider>
161
+ );
162
+ };
163
+
164
+ /**
165
+ * Hook to access the debug console context
166
+ */
167
+ export const useDebugConsole = (): DebugConsoleContextType => {
168
+ const context = useContext(DebugConsoleContext);
169
+ if (!context) {
170
+ throw new Error('useDebugConsole must be used within a DebugProvider');
171
+ }
172
+ return context;
173
+ };
174
+
175
+ /**
176
+ * Optional hook that doesn't throw if used outside provider
177
+ */
178
+ export const useDebugConsoleOptional = (): DebugConsoleContextType | null => {
179
+ return useContext(DebugConsoleContext) ?? null;
180
+ };
181
+
182
+ export default DebugProvider;