@teardown/dev-client 2.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.
Files changed (32) hide show
  1. package/android/build.gradle.kts +34 -0
  2. package/android/react-native.config.js +10 -0
  3. package/android/src/main/AndroidManifest.xml +7 -0
  4. package/android/src/main/java/com/teardown/devclient/DevSettingsModule.kt +130 -0
  5. package/android/src/main/java/com/teardown/devclient/ShakeDetectorModule.kt +118 -0
  6. package/android/src/main/java/com/teardown/devclient/TeardownDevClientPackage.kt +23 -0
  7. package/ios/TeardownDevClient/DevSettingsModule.swift +135 -0
  8. package/ios/TeardownDevClient/ShakeDetector.swift +102 -0
  9. package/ios/TeardownDevClient/TeardownDevClient.h +14 -0
  10. package/ios/TeardownDevClient/TeardownDevClient.mm +42 -0
  11. package/ios/TeardownDevClient.podspec +23 -0
  12. package/package.json +56 -0
  13. package/src/components/dev-menu/dev-menu.tsx +254 -0
  14. package/src/components/dev-menu/index.ts +5 -0
  15. package/src/components/error-overlay/error-overlay.tsx +256 -0
  16. package/src/components/error-overlay/index.ts +5 -0
  17. package/src/components/index.ts +7 -0
  18. package/src/components/splash-screen/index.ts +5 -0
  19. package/src/components/splash-screen/splash-screen.tsx +99 -0
  20. package/src/dev-client-provider.tsx +204 -0
  21. package/src/hooks/index.ts +24 -0
  22. package/src/hooks/use-bundler-status.ts +139 -0
  23. package/src/hooks/use-dev-menu.ts +306 -0
  24. package/src/hooks/use-splash-screen.ts +177 -0
  25. package/src/index.ts +77 -0
  26. package/src/native/dev-settings.ts +132 -0
  27. package/src/native/index.ts +16 -0
  28. package/src/native/shake-detector.ts +105 -0
  29. package/src/types.ts +235 -0
  30. package/src/utils/bundler-url.ts +103 -0
  31. package/src/utils/index.ts +19 -0
  32. package/src/utils/platform.ts +64 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * DevMenu Component
3
+ *
4
+ * A modal developer menu that provides quick access to
5
+ * development tools and actions.
6
+ */
7
+
8
+ import type React from "react";
9
+ import { useCallback, useMemo } from "react";
10
+ import {
11
+ Modal,
12
+ ScrollView,
13
+ StyleSheet,
14
+ Text,
15
+ type TextStyle,
16
+ TouchableOpacity,
17
+ View,
18
+ type ViewStyle,
19
+ } from "react-native";
20
+ import type { BundlerStatus, DevMenuItem } from "../../types";
21
+
22
+ /**
23
+ * DevMenu component props
24
+ */
25
+ export interface DevMenuProps {
26
+ /** Whether the menu is visible */
27
+ visible: boolean;
28
+
29
+ /** Callback when menu should close */
30
+ onDismiss: () => void;
31
+
32
+ /** Menu items to display */
33
+ items: DevMenuItem[];
34
+
35
+ /** Current bundler status */
36
+ bundlerStatus?: BundlerStatus;
37
+
38
+ /** Whether an action is loading */
39
+ isLoading?: boolean;
40
+
41
+ /** Custom header component */
42
+ renderHeader?: () => React.ReactNode;
43
+
44
+ /** Theme variant */
45
+ theme?: "light" | "dark" | "auto";
46
+
47
+ /** Test ID for testing */
48
+ testID?: string;
49
+ }
50
+
51
+ /**
52
+ * Get theme colors
53
+ */
54
+ function getThemeColors(theme: "light" | "dark" | "auto") {
55
+ // For auto, we'd typically check system appearance
56
+ // For now, default to light
57
+ const isDark = theme === "dark";
58
+
59
+ return {
60
+ overlay: "rgba(0, 0, 0, 0.5)",
61
+ background: isDark ? "#1a1a1a" : "#ffffff",
62
+ border: isDark ? "#333333" : "#e0e0e0",
63
+ text: isDark ? "#ffffff" : "#1a1a1a",
64
+ textSecondary: isDark ? "#aaaaaa" : "#666666",
65
+ itemBackground: isDark ? "#2a2a2a" : "#f5f5f5",
66
+ itemHover: isDark ? "#333333" : "#e8e8e8",
67
+ accent: "#007aff",
68
+ success: "#34c759",
69
+ error: "#ff3b30",
70
+ };
71
+ }
72
+
73
+ /**
74
+ * DevMenu component
75
+ */
76
+ export function DevMenu(props: DevMenuProps): React.JSX.Element | null {
77
+ const { visible, onDismiss, items, bundlerStatus, isLoading = false, renderHeader, theme = "light", testID } = props;
78
+
79
+ const colors = useMemo(() => getThemeColors(theme), [theme]);
80
+
81
+ const handleItemPress = useCallback(
82
+ async (item: DevMenuItem) => {
83
+ if (isLoading) return;
84
+
85
+ try {
86
+ await item.onPress();
87
+ } catch (error) {
88
+ console.error(`[DevMenu] Action failed: ${item.label}`, error);
89
+ }
90
+ },
91
+ [isLoading]
92
+ );
93
+
94
+ // Filter items based on dev mode
95
+ const visibleItems = useMemo(() => {
96
+ return items.filter((item) => {
97
+ if (item.devOnly && !__DEV__) {
98
+ return false;
99
+ }
100
+ return true;
101
+ });
102
+ }, [items]);
103
+
104
+ const dynamicStyles = useMemo(
105
+ () => ({
106
+ container: {
107
+ backgroundColor: colors.background,
108
+ } as ViewStyle,
109
+ header: {
110
+ borderBottomColor: colors.border,
111
+ } as ViewStyle,
112
+ title: {
113
+ color: colors.text,
114
+ } as TextStyle,
115
+ subtitle: {
116
+ color: colors.textSecondary,
117
+ } as TextStyle,
118
+ menuItem: {
119
+ backgroundColor: colors.itemBackground,
120
+ } as ViewStyle,
121
+ menuItemText: {
122
+ color: colors.text,
123
+ } as TextStyle,
124
+ closeButton: {
125
+ backgroundColor: colors.itemBackground,
126
+ } as ViewStyle,
127
+ closeButtonText: {
128
+ color: colors.accent,
129
+ } as TextStyle,
130
+ statusDot: {
131
+ backgroundColor: bundlerStatus?.connected ? colors.success : colors.error,
132
+ } as ViewStyle,
133
+ }),
134
+ [colors, bundlerStatus?.connected]
135
+ );
136
+
137
+ if (!visible) {
138
+ return null;
139
+ }
140
+
141
+ return (
142
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onDismiss} testID={testID}>
143
+ <View style={styles.overlay}>
144
+ <View style={[styles.container, dynamicStyles.container]}>
145
+ {renderHeader ? (
146
+ renderHeader()
147
+ ) : (
148
+ <View style={[styles.header, dynamicStyles.header]}>
149
+ <View style={styles.headerContent}>
150
+ <Text style={[styles.title, dynamicStyles.title]}>Developer Menu</Text>
151
+ <View style={styles.statusRow}>
152
+ <View style={[styles.statusDot, dynamicStyles.statusDot]} />
153
+ <Text style={[styles.subtitle, dynamicStyles.subtitle]}>
154
+ {bundlerStatus?.connected ? `Connected to ${bundlerStatus.url}` : "Disconnected"}
155
+ </Text>
156
+ </View>
157
+ </View>
158
+ </View>
159
+ )}
160
+
161
+ <ScrollView style={styles.menuList} showsVerticalScrollIndicator={false}>
162
+ {visibleItems.map((item) => (
163
+ <TouchableOpacity
164
+ key={item.id}
165
+ style={[styles.menuItem, dynamicStyles.menuItem]}
166
+ onPress={() => handleItemPress(item)}
167
+ disabled={isLoading}
168
+ activeOpacity={0.7}
169
+ testID={`dev-menu-item-${item.id}`}
170
+ >
171
+ <Text style={[styles.menuItemText, dynamicStyles.menuItemText]}>{item.label}</Text>
172
+ </TouchableOpacity>
173
+ ))}
174
+ </ScrollView>
175
+
176
+ <TouchableOpacity
177
+ style={[styles.closeButton, dynamicStyles.closeButton]}
178
+ onPress={onDismiss}
179
+ activeOpacity={0.7}
180
+ testID="dev-menu-close"
181
+ >
182
+ <Text style={[styles.closeButtonText, dynamicStyles.closeButtonText]}>Close</Text>
183
+ </TouchableOpacity>
184
+ </View>
185
+ </View>
186
+ </Modal>
187
+ );
188
+ }
189
+
190
+ const styles = StyleSheet.create({
191
+ overlay: {
192
+ flex: 1,
193
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
194
+ justifyContent: "center",
195
+ alignItems: "center",
196
+ },
197
+ container: {
198
+ width: "85%",
199
+ maxWidth: 400,
200
+ maxHeight: "70%",
201
+ borderRadius: 12,
202
+ overflow: "hidden",
203
+ shadowColor: "#000",
204
+ shadowOffset: { width: 0, height: 2 },
205
+ shadowOpacity: 0.25,
206
+ shadowRadius: 8,
207
+ elevation: 5,
208
+ },
209
+ header: {
210
+ padding: 16,
211
+ borderBottomWidth: 1,
212
+ },
213
+ headerContent: {
214
+ gap: 4,
215
+ },
216
+ title: {
217
+ fontSize: 18,
218
+ fontWeight: "600",
219
+ },
220
+ statusRow: {
221
+ flexDirection: "row",
222
+ alignItems: "center",
223
+ gap: 6,
224
+ },
225
+ statusDot: {
226
+ width: 8,
227
+ height: 8,
228
+ borderRadius: 4,
229
+ },
230
+ subtitle: {
231
+ fontSize: 12,
232
+ },
233
+ menuList: {
234
+ maxHeight: 300,
235
+ },
236
+ menuItem: {
237
+ padding: 16,
238
+ marginHorizontal: 12,
239
+ marginVertical: 4,
240
+ borderRadius: 8,
241
+ },
242
+ menuItemText: {
243
+ fontSize: 16,
244
+ },
245
+ closeButton: {
246
+ padding: 16,
247
+ alignItems: "center",
248
+ marginTop: 8,
249
+ },
250
+ closeButtonText: {
251
+ fontSize: 16,
252
+ fontWeight: "500",
253
+ },
254
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * DevMenu component exports
3
+ */
4
+
5
+ export { DevMenu, type DevMenuProps } from "./dev-menu";
@@ -0,0 +1,256 @@
1
+ /**
2
+ * ErrorOverlay Component
3
+ *
4
+ * Displays error information in development mode with
5
+ * stack trace and action buttons.
6
+ */
7
+
8
+ import type React from "react";
9
+ import { useCallback, useState } from "react";
10
+ import { ScrollView, StyleSheet, Text, type TextStyle, TouchableOpacity, View, type ViewStyle } from "react-native";
11
+ import { reload } from "../../native/dev-settings";
12
+
13
+ /**
14
+ * ErrorOverlay component props
15
+ */
16
+ export interface ErrorOverlayProps {
17
+ /** Error to display */
18
+ error: Error | null;
19
+
20
+ /** Error info from React error boundary */
21
+ errorInfo?: React.ErrorInfo | null;
22
+
23
+ /** Callback when error is dismissed */
24
+ onDismiss?: () => void;
25
+
26
+ /** Callback when retry is pressed */
27
+ onRetry?: () => void;
28
+
29
+ /** Whether to show in production (default: false) */
30
+ showInProduction?: boolean;
31
+
32
+ /** Test ID for testing */
33
+ testID?: string;
34
+ }
35
+
36
+ /**
37
+ * Parse stack trace into formatted lines
38
+ */
39
+ function parseStackTrace(stack: string | undefined): string[] {
40
+ if (!stack) return [];
41
+
42
+ return stack
43
+ .split("\n")
44
+ .map((line) => line.trim())
45
+ .filter((line) => line.length > 0);
46
+ }
47
+
48
+ /**
49
+ * ErrorOverlay component
50
+ *
51
+ * Displays a full-screen error overlay with:
52
+ * - Error message
53
+ * - Stack trace (collapsible)
54
+ * - Reload and Dismiss buttons
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * <ErrorOverlay
59
+ * error={new Error('Something went wrong')}
60
+ * onDismiss={() => setError(null)}
61
+ * onRetry={() => fetchData()}
62
+ * />
63
+ * ```
64
+ */
65
+ export function ErrorOverlay(props: ErrorOverlayProps): React.JSX.Element | null {
66
+ const { error, errorInfo, onDismiss, onRetry, showInProduction = false, testID = "error-overlay" } = props;
67
+
68
+ const [showStack, setShowStack] = useState(true);
69
+
70
+ const handleReload = useCallback(() => {
71
+ reload();
72
+ }, []);
73
+
74
+ const handleRetry = useCallback(() => {
75
+ onRetry?.();
76
+ }, [onRetry]);
77
+
78
+ const handleDismiss = useCallback(() => {
79
+ onDismiss?.();
80
+ }, [onDismiss]);
81
+
82
+ const toggleStack = useCallback(() => {
83
+ setShowStack((prev) => !prev);
84
+ }, []);
85
+
86
+ // Don't show in production unless explicitly enabled
87
+ if (!__DEV__ && !showInProduction) {
88
+ return null;
89
+ }
90
+
91
+ // Don't show if no error
92
+ if (!error) {
93
+ return null;
94
+ }
95
+
96
+ const stackLines = parseStackTrace(error.stack);
97
+ const componentStack = errorInfo?.componentStack?.split("\n").filter((line) => line.trim()) ?? [];
98
+
99
+ return (
100
+ <View style={styles.container} testID={testID}>
101
+ <View style={styles.header}>
102
+ <Text style={styles.title}>Error</Text>
103
+ <Text style={styles.message}>{error.message || "An unexpected error occurred"}</Text>
104
+ </View>
105
+
106
+ <TouchableOpacity style={styles.stackToggle} onPress={toggleStack} activeOpacity={0.7}>
107
+ <Text style={styles.stackToggleText}>{showStack ? "Hide Stack Trace ▼" : "Show Stack Trace ▶"}</Text>
108
+ </TouchableOpacity>
109
+
110
+ {showStack && (
111
+ <ScrollView style={styles.stackContainer} showsVerticalScrollIndicator>
112
+ <Text style={styles.stackTitle}>Stack Trace:</Text>
113
+ {stackLines.map((line, index) => (
114
+ <Text key={`stack-${index}`} style={styles.stackLine}>
115
+ {line}
116
+ </Text>
117
+ ))}
118
+
119
+ {componentStack.length > 0 && (
120
+ <>
121
+ <Text style={[styles.stackTitle, styles.componentStackTitle]}>Component Stack:</Text>
122
+ {componentStack.map((line, index) => (
123
+ <Text key={`component-${index}`} style={styles.stackLine}>
124
+ {line}
125
+ </Text>
126
+ ))}
127
+ </>
128
+ )}
129
+ </ScrollView>
130
+ )}
131
+
132
+ <View style={styles.actions}>
133
+ <TouchableOpacity
134
+ style={[styles.button, styles.reloadButton]}
135
+ onPress={handleReload}
136
+ activeOpacity={0.7}
137
+ testID="error-overlay-reload"
138
+ >
139
+ <Text style={styles.reloadButtonText}>Reload App</Text>
140
+ </TouchableOpacity>
141
+
142
+ {onRetry && (
143
+ <TouchableOpacity
144
+ style={[styles.button, styles.retryButton]}
145
+ onPress={handleRetry}
146
+ activeOpacity={0.7}
147
+ testID="error-overlay-retry"
148
+ >
149
+ <Text style={styles.retryButtonText}>Retry</Text>
150
+ </TouchableOpacity>
151
+ )}
152
+
153
+ {onDismiss && (
154
+ <TouchableOpacity
155
+ style={[styles.button, styles.dismissButton]}
156
+ onPress={handleDismiss}
157
+ activeOpacity={0.7}
158
+ testID="error-overlay-dismiss"
159
+ >
160
+ <Text style={styles.dismissButtonText}>Dismiss</Text>
161
+ </TouchableOpacity>
162
+ )}
163
+ </View>
164
+ </View>
165
+ );
166
+ }
167
+
168
+ const styles = StyleSheet.create({
169
+ container: {
170
+ ...StyleSheet.absoluteFillObject,
171
+ backgroundColor: "#1a1a1a",
172
+ padding: 16,
173
+ zIndex: 10000,
174
+ } as ViewStyle,
175
+ header: {
176
+ marginBottom: 16,
177
+ } as ViewStyle,
178
+ title: {
179
+ fontSize: 24,
180
+ fontWeight: "bold",
181
+ color: "#ff3b30",
182
+ marginBottom: 8,
183
+ } as TextStyle,
184
+ message: {
185
+ fontSize: 16,
186
+ color: "#ffffff",
187
+ lineHeight: 22,
188
+ } as TextStyle,
189
+ stackToggle: {
190
+ paddingVertical: 8,
191
+ marginBottom: 8,
192
+ } as ViewStyle,
193
+ stackToggleText: {
194
+ fontSize: 14,
195
+ color: "#007aff",
196
+ fontWeight: "500",
197
+ } as TextStyle,
198
+ stackContainer: {
199
+ flex: 1,
200
+ backgroundColor: "#2a2a2a",
201
+ borderRadius: 8,
202
+ padding: 12,
203
+ marginBottom: 16,
204
+ } as ViewStyle,
205
+ stackTitle: {
206
+ fontSize: 12,
207
+ fontWeight: "600",
208
+ color: "#888888",
209
+ marginBottom: 8,
210
+ textTransform: "uppercase",
211
+ } as TextStyle,
212
+ componentStackTitle: {
213
+ marginTop: 16,
214
+ } as TextStyle,
215
+ stackLine: {
216
+ fontSize: 11,
217
+ fontFamily: "monospace",
218
+ color: "#cccccc",
219
+ lineHeight: 16,
220
+ marginBottom: 2,
221
+ } as TextStyle,
222
+ actions: {
223
+ flexDirection: "row",
224
+ gap: 12,
225
+ } as ViewStyle,
226
+ button: {
227
+ flex: 1,
228
+ paddingVertical: 14,
229
+ borderRadius: 8,
230
+ alignItems: "center",
231
+ } as ViewStyle,
232
+ reloadButton: {
233
+ backgroundColor: "#ff3b30",
234
+ } as ViewStyle,
235
+ reloadButtonText: {
236
+ color: "#ffffff",
237
+ fontWeight: "600",
238
+ fontSize: 16,
239
+ } as TextStyle,
240
+ retryButton: {
241
+ backgroundColor: "#007aff",
242
+ } as ViewStyle,
243
+ retryButtonText: {
244
+ color: "#ffffff",
245
+ fontWeight: "600",
246
+ fontSize: 16,
247
+ } as TextStyle,
248
+ dismissButton: {
249
+ backgroundColor: "#3a3a3a",
250
+ } as ViewStyle,
251
+ dismissButtonText: {
252
+ color: "#ffffff",
253
+ fontWeight: "600",
254
+ fontSize: 16,
255
+ } as TextStyle,
256
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ErrorOverlay component exports
3
+ */
4
+
5
+ export { ErrorOverlay, type ErrorOverlayProps } from "./error-overlay";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Component exports
3
+ */
4
+
5
+ export { DevMenu, type DevMenuProps } from "./dev-menu";
6
+ export { ErrorOverlay, type ErrorOverlayProps } from "./error-overlay";
7
+ export { SplashScreen, type SplashScreenProps } from "./splash-screen";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SplashScreen component exports
3
+ */
4
+
5
+ export { SplashScreen, type SplashScreenProps } from "./splash-screen";
@@ -0,0 +1,99 @@
1
+ /**
2
+ * SplashScreen Component
3
+ *
4
+ * A customizable splash screen that displays while the app loads.
5
+ */
6
+
7
+ import type React from "react";
8
+ import { Animated, Image, type ImageSourcePropType, StyleSheet, View } from "react-native";
9
+
10
+ /**
11
+ * SplashScreen component props
12
+ */
13
+ export interface SplashScreenProps {
14
+ /** Whether the splash screen is visible */
15
+ visible: boolean;
16
+
17
+ /** Background color */
18
+ backgroundColor?: string;
19
+
20
+ /** Logo image source */
21
+ logo?: ImageSourcePropType;
22
+
23
+ /** Logo resize mode */
24
+ resizeMode?: "contain" | "cover" | "center";
25
+
26
+ /** Current opacity (for fade animation) */
27
+ opacity?: number;
28
+
29
+ /** Whether fade is in progress */
30
+ isFading?: boolean;
31
+
32
+ /** Test ID for testing */
33
+ testID?: string;
34
+ }
35
+
36
+ /**
37
+ * SplashScreen component
38
+ *
39
+ * Displays a customizable splash screen with optional logo.
40
+ * Supports fade-out animation via the opacity prop.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <SplashScreen
45
+ * visible={true}
46
+ * backgroundColor="#ffffff"
47
+ * logo={require('./assets/logo.png')}
48
+ * opacity={1}
49
+ * />
50
+ * ```
51
+ */
52
+ export function SplashScreen(props: SplashScreenProps): React.JSX.Element | null {
53
+ const {
54
+ visible,
55
+ backgroundColor = "#ffffff",
56
+ logo,
57
+ resizeMode = "contain",
58
+ opacity = 1,
59
+ isFading = false,
60
+ testID = "splash-screen",
61
+ } = props;
62
+
63
+ if (!visible) {
64
+ return null;
65
+ }
66
+
67
+ return (
68
+ <Animated.View
69
+ style={[
70
+ styles.container,
71
+ {
72
+ backgroundColor,
73
+ opacity,
74
+ },
75
+ ]}
76
+ testID={testID}
77
+ pointerEvents={isFading ? "none" : "auto"}
78
+ >
79
+ <View style={styles.content}>{logo && <Image source={logo} style={styles.logo} resizeMode={resizeMode} />}</View>
80
+ </Animated.View>
81
+ );
82
+ }
83
+
84
+ const styles = StyleSheet.create({
85
+ container: {
86
+ ...StyleSheet.absoluteFillObject,
87
+ justifyContent: "center",
88
+ alignItems: "center",
89
+ zIndex: 9999,
90
+ },
91
+ content: {
92
+ justifyContent: "center",
93
+ alignItems: "center",
94
+ },
95
+ logo: {
96
+ width: 150,
97
+ height: 150,
98
+ },
99
+ });