import React, { Component, Fragment, type ErrorInfo, type ReactNode } from "react"; import { Pressable, ScrollView, Text, View } from "react-native"; import type { PressableStateCallbackType, StyleProp, ViewStyle } from "react-native"; import { useTranslation } from "react-i18next"; import { StyleSheet } from "react-native-unistyles"; 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}; } } interface RootErrorFallbackProps { error: string; onRetry: () => void; } function RootErrorFallback({ error, onRetry }: RootErrorFallbackProps) { const { t } = useTranslation(); return ( {t("rootError.kicker")} {t("rootError.title")} {t("rootError.body")} {t("rootError.details")} {error} {t("common.actions.retry")} ); } function retryButtonStyle({ pressed }: PressableStateCallbackType): StyleProp { return [styles.retryButton, pressed ? styles.retryButtonPressed : null]; } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surface0, }, contentContainer: { flexGrow: 1, justifyContent: "center", paddingHorizontal: theme.spacing[6], paddingVertical: theme.spacing[8], }, content: { alignSelf: "center", width: "100%", maxWidth: 520, gap: theme.spacing[4], }, kicker: { color: theme.colors.destructive, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, title: { color: theme.colors.foreground, fontSize: theme.fontSize.xl, fontWeight: theme.fontWeight.semibold, }, body: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, lineHeight: 22, }, messageBox: { gap: theme.spacing[2], borderWidth: 1, borderColor: theme.colors.borderAccent, borderRadius: theme.borderRadius.lg, backgroundColor: theme.colors.surface1, padding: theme.spacing[4], }, messageLabel: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, message: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, lineHeight: 20, }, retryButton: { alignSelf: "flex-start", minHeight: 40, justifyContent: "center", borderRadius: theme.borderRadius.md, backgroundColor: theme.colors.accent, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], }, retryButtonPressed: { opacity: 0.85, }, retryButtonText: { color: theme.colors.accentForeground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, }));