import { useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Platform, ScrollView, Text, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { openExternalUrl } from "@/utils/open-external-url"; import { BookOpen, Check, Copy, RotateCw, TriangleAlert } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { PaseoLogo } from "@/components/icons/paseo-logo"; import { Button } from "@/components/ui/button"; import { Fonts } from "@/constants/theme"; import { getDesktopDaemonLogs, type DesktopDaemonLogs, } from "@/desktop/daemon/desktop-daemon"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; type StartupSplashScreenProps = { bootstrapState?: { phase: "starting-daemon" | "connecting" | "online" | "error"; error: string | null; retry: () => void; }; }; const GITHUB_ISSUE_URL = "https://github.com/getpaseo/paseo/issues/new"; const DOCS_URL = "https://paseo.sh/docs"; const styles = StyleSheet.create((theme) => ({ container: { position: "relative", flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: theme.colors.surface0, paddingHorizontal: theme.spacing[8], paddingVertical: theme.spacing[8], }, containerError: { justifyContent: "flex-start", paddingTop: theme.spacing[16], }, centeredContent: { alignItems: "center", justifyContent: "center", maxWidth: 520, width: "100%", }, errorContent: { alignItems: "stretch", maxWidth: 720, width: "100%", gap: theme.spacing[6], }, errorHeader: { alignItems: "flex-start", }, title: { marginTop: theme.spacing[8], color: theme.colors.foreground, fontSize: theme.fontSize["3xl"], fontWeight: theme.fontWeight.semibold, textAlign: "center", }, titleError: { textAlign: "left", }, subtitleRow: { marginTop: theme.spacing[4], flexDirection: "row", alignItems: "center", gap: theme.spacing[3], }, progressSteps: { marginTop: theme.spacing[4], gap: theme.spacing[3], width: "100%", }, progressStepRow: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[3], }, subtitle: { marginTop: theme.spacing[8], color: theme.colors.foregroundMuted, fontSize: theme.fontSize.lg, textAlign: "center", }, subtitleInline: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.lg, textAlign: "center", }, errorDescription: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, lineHeight: 22, }, errorMessage: { color: theme.colors.destructive, fontSize: theme.fontSize.sm, lineHeight: 20, fontFamily: Fonts.mono, }, logsMeta: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, logsContainer: { height: 200, borderRadius: theme.borderRadius.xl, backgroundColor: theme.colors.surface1, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, overflow: "hidden", }, logsScroll: { flexGrow: 0, }, logsContent: { padding: theme.spacing[4], }, logsText: { fontFamily: Fonts.mono, fontSize: theme.fontSize.xs, color: theme.colors.foreground, lineHeight: 18, ...(Platform.OS === "web" ? { whiteSpace: "pre", overflowWrap: "normal", } : null), }, actionRow: { flexDirection: "row", gap: theme.spacing[3], flexWrap: "wrap", }, })); export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps) { const { theme } = useUnistyles(); const [daemonLogs, setDaemonLogs] = useState(null); const [logsError, setLogsError] = useState(null); const [isLoadingLogs, setIsLoadingLogs] = useState(false); const phase = bootstrapState?.phase; const isError = phase === "error"; const isSimpleSplash = bootstrapState === undefined; useEffect(() => { if (!isError) { setDaemonLogs(null); setLogsError(null); setIsLoadingLogs(false); return; } let isCancelled = false; setIsLoadingLogs(true); setLogsError(null); void getDesktopDaemonLogs() .then((logs) => { if (isCancelled) { return; } setDaemonLogs(logs); }) .catch((error) => { if (isCancelled) { return; } const message = error instanceof Error ? error.message : String(error); setDaemonLogs(null); setLogsError(`Unable to load daemon logs: ${message}`); }) .finally(() => { if (!isCancelled) { setIsLoadingLogs(false); } }); return () => { isCancelled = true; }; }, [isError]); const progressSteps = phase === "starting-daemon" ? [{ key: "starting-daemon", label: "Starting local server...", status: "active" as const }] : phase === "connecting" ? [ { key: "starting-daemon", label: "Started local server", status: "complete" as const }, { key: "connecting", label: "Connecting to local server...", status: "active" as const }, ] : [ { key: "starting-daemon", label: "Started local server", status: "complete" as const }, { key: "connecting", label: "Connected to local server", status: "complete" as const }, ]; const logsText = useMemo(() => { if (isLoadingLogs) { return "Loading daemon logs..."; } if (daemonLogs?.contents) { return daemonLogs.contents; } if (logsError) { return logsError; } return "No daemon logs available."; }, [daemonLogs?.contents, isLoadingLogs, logsError]); const handleCopyLogs = () => { const payload = daemonLogs?.logPath ? `${daemonLogs.logPath}\n\n${daemonLogs.contents}` : logsText; void Clipboard.setStringAsync(payload); }; if (isSimpleSplash) { return ( Starting up… ); } if (!isError) { return ( Welcome to Paseo {progressSteps.map((step) => ( {step.status === "complete" ? ( ) : ( )} {step.label} ))} ); } return ( Something went wrong The local server failed to start. If this keeps happening, please report the issue on GitHub and include the logs below. {bootstrapState.error} {daemonLogs?.logPath ? {daemonLogs.logPath} : null} {logsText} ); }