import React, { Component, Fragment, type ErrorInfo, type ReactNode } from "react"; import { Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet } from "react-native-unistyles"; import { Button } from "@/components/ui/button"; import { ScrollableCodeSurface } from "@/components/ui/scrollable-code-surface"; import { useIsCompactFormFactor } from "@/constants/layout"; import { formatCaughtValue } from "./root-error-details"; interface RootErrorBoundaryProps { children: ReactNode; } interface RootErrorBoundaryState { error: string | null; resetKey: number; } export class RootErrorBoundary extends Component { state: RootErrorBoundaryState = { error: null, resetKey: 0, }; static getDerivedStateFromError(error: unknown): Partial { return { error: formatCaughtValue(error) }; } componentDidCatch(error: unknown, errorInfo: ErrorInfo) { console.error("[RootErrorBoundary] Unhandled render error", { error: formatCaughtValue(error), componentStack: errorInfo.componentStack, }); } retry = () => { this.setState(({ resetKey }) => ({ error: null, resetKey: resetKey + 1, })); }; render() { const { error, resetKey } = this.state; if (error !== null) { return ; } return {this.props.children}; } } // A stack trace is reference material, not the screen. Cap it so the message and // the retry action stay the shape of the page; the rest scrolls. const DETAILS_MAX_HEIGHT = 300; interface RootErrorFallbackProps { error: string; onRetry: () => void; } export function RootErrorFallback({ error, onRetry }: RootErrorFallbackProps) { const { t } = useTranslation(); const insets = useSafeAreaInsets(); const isCompact = useIsCompactFormFactor(); const retry = ( ); return ( {t("rootError.title")} {t("rootError.body")} {t("rootError.details")} {error} {isCompact ? null : retry} {isCompact ? {retry} : null} ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surface0, }, content: { flex: 1, alignSelf: "center", width: "100%", maxWidth: 420, justifyContent: "center", gap: theme.spacing[6], paddingHorizontal: theme.spacing[6], paddingVertical: theme.spacing[8], }, header: { alignItems: "center", gap: theme.spacing[2], }, title: { color: theme.colors.foreground, fontSize: theme.fontSize.xl, fontWeight: theme.fontWeight.medium, textAlign: "center", }, body: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, lineHeight: 20, textAlign: "center", }, details: { flexShrink: 1, gap: theme.spacing[2], }, detailsLabel: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.medium, }, detailsSurface: { flexShrink: 1, }, detailsScroll: { flexShrink: 1, }, footer: { alignSelf: "center", width: "100%", maxWidth: 420, paddingHorizontal: theme.spacing[6], paddingBottom: theme.spacing[6], }, }));