@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.
- package/android/build.gradle.kts +34 -0
- package/android/react-native.config.js +10 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/teardown/devclient/DevSettingsModule.kt +130 -0
- package/android/src/main/java/com/teardown/devclient/ShakeDetectorModule.kt +118 -0
- package/android/src/main/java/com/teardown/devclient/TeardownDevClientPackage.kt +23 -0
- package/ios/TeardownDevClient/DevSettingsModule.swift +135 -0
- package/ios/TeardownDevClient/ShakeDetector.swift +102 -0
- package/ios/TeardownDevClient/TeardownDevClient.h +14 -0
- package/ios/TeardownDevClient/TeardownDevClient.mm +42 -0
- package/ios/TeardownDevClient.podspec +23 -0
- package/package.json +56 -0
- package/src/components/dev-menu/dev-menu.tsx +254 -0
- package/src/components/dev-menu/index.ts +5 -0
- package/src/components/error-overlay/error-overlay.tsx +256 -0
- package/src/components/error-overlay/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/components/splash-screen/index.ts +5 -0
- package/src/components/splash-screen/splash-screen.tsx +99 -0
- package/src/dev-client-provider.tsx +204 -0
- package/src/hooks/index.ts +24 -0
- package/src/hooks/use-bundler-status.ts +139 -0
- package/src/hooks/use-dev-menu.ts +306 -0
- package/src/hooks/use-splash-screen.ts +177 -0
- package/src/index.ts +77 -0
- package/src/native/dev-settings.ts +132 -0
- package/src/native/index.ts +16 -0
- package/src/native/shake-detector.ts +105 -0
- package/src/types.ts +235 -0
- package/src/utils/bundler-url.ts +103 -0
- package/src/utils/index.ts +19 -0
- 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,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,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
|
+
});
|