diff --git a/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx b/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx index 696af69..512cec6 100644 --- a/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx +++ b/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx @@ -1,6 +1,5 @@ import React from "react"; import { hasStack, isComponentStack } from "@core/ui/reporter/utils/isStack"; -import parseErrorStack from "@core/ui/reporter/utils/parseErrorStack"; import { getDebugInfo, toggleSafeMode } from "@lib/api/debug"; import { BundleUpdaterManager } from "@lib/api/native/modules"; import { settings } from "@lib/api/settings"; @@ -10,449 +9,201 @@ import { tokens } from "@metro/common"; import { showToast } from "@lib/ui/toasts"; import { Button, - Card, SafeAreaProvider, SafeAreaView, Text, TableRowGroup, TableRow, - TableSwitchRow, - Stack, } from "@metro/common/components"; -import { ScrollView, View } from "react-native"; +import { ScrollView, View, Clipboard } from "react-native"; -import { - registeredPlugins, - disablePlugin, - getBisectBatches, - enablePlugin, - getPluginSettingsComponent, - isPluginEnabled, - pluginSettings, -} from "@lib/addons/plugins"; -import { getCorePlugins } from "@core/plugins"; import ErrorComponentStackCard from "./ErrorComponentStackCard"; import ErrorStackCard from "./ErrorStackCard"; -import { NavigationNative } from "@metro/common"; -const useStyles = createStyles({ +const useStyles = createStyles(() => ({ container: { flex: 1, backgroundColor: tokens.colors.BG_BASE_SECONDARY, - paddingHorizontal: 16, - height: "100%", - gap: 12, }, -}); + scrollContent: { + paddingBottom: 100, // Extra space for fixed buttons + }, + header: { + alignItems: "center", + paddingVertical: 32, + backgroundColor: tokens.colors.BG_BASE_SECONDARY, + }, + headerText: { + textAlign: "center", + marginBottom: 8, + }, + debugInfo: { + fontSize: 12, + color: tokens.colors.TEXT_MUTED, + textAlign: "center", + marginTop: 8, + }, + section: { + marginHorizontal: 16, + marginBottom: 12, + }, + fixedButtonContainer: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + backgroundColor: tokens.colors.BG_MOD_STRONG, + padding: 16, + gap: 8, + }, + buttonRow: { + flexDirection: "row", + gap: 8, + }, + fullButton: { + flex: 1, + }, + errorMessageRow: { + backgroundColor: tokens.colors.BG_MOD_FAINT, + borderRadius: 8, + marginVertical: 4, + overflow: "hidden", + }, + centeredErrorText: { + color: tokens.colors.TEXT_DANGER, + fontSize: 14, + textAlign: "center", + paddingVertical: 8, + flex: 1, + }, +})); -/** - * Module-level helper to collect all known plugin IDs (core + registered externals + installed keys). - * Exported and attached to window so external callers (or code executing before React mounts) - * can call it without needing the React component to mount first. - */ -export function collectAllPluginIdsModule(): string[] { - const ids = new Set(); - // registeredPlugins (includes externals known from repos) - try { - if ( - typeof registeredPlugins !== "undefined" && - registeredPlugins && - typeof (registeredPlugins as any).keys === "function" - ) { - for (const k of registeredPlugins.keys()) ids.add(k); - } - } catch {} - // installed plugin settings (may include plugins not registered yet) - try { - if (typeof pluginSettings !== "undefined" && pluginSettings) { - for (const k of Object.keys(pluginSettings || {})) ids.add(k); - } - } catch {} - return Array.from(ids); -} - -// Provide a global alias for callers that expect the global to exist early. -// Use both spellings seen in various callers. -// Provide global aliases (some callers used plural/singular variants) -// Ensure a safe, early no-op fallback is present on the global object so -// callers that run before this module is evaluated won't throw a ReferenceError. -(function () { - try { - const g = - typeof globalThis !== "undefined" - ? globalThis - : typeof window !== "undefined" - ? window - : {}; - // If the global already has a function, don't overwrite it. - if (typeof (g as any).collectAllPluginsIds !== "function") { - Object.defineProperty(g, "collectAllPluginsIds", { - configurable: true, - writable: true, - enumerable: false, - value: collectAllPluginIdsModule, - }); - } - if (typeof (g as any).collectAllPluginIds !== "function") { - Object.defineProperty(g, "collectAllPluginIds", { - configurable: true, - writable: true, - enumerable: false, - value: collectAllPluginIdsModule, - }); - } - } catch (e) { - // Best-effort only — avoid throwing during module evaluation - try { - console.error( - "[ShiggyCord] failed to install global collectAllPluginIds alias", - e, - ); - } catch {} - } -})(); - -// Component entrypoint export default function ErrorBoundaryScreen(props: { error: Error; rerender: () => void; }) { - // Hooks MUST be at the top level and unconditionally called. const styles = useStyles(); const debugInfo = getDebugInfo(); - const navigation = NavigationNative.useNavigation(); - // Bisect UI state (fixes ReferenceError when referenced below) - const [bisectBatches, setBisectBatches] = React.useState( - null, - ); - const [bisectIndex, setBisectIndex] = React.useState(0); - const [showBisectUI, setShowBisectUI] = React.useState(false); + const copyErrorDetails = () => { + const errorText = `Error: ${props.error.message}\n\nStack: ${props.error.stack || "No stack trace"}`; + Clipboard.setString(errorText); + showToast("Error details copied"); + }; - // Use a safe fallback so renders don't throw if state hasn't been initialized yet - const batches = bisectBatches ?? []; - const currentBatch = batches[bisectIndex] ?? []; - - // Move all error-analysis computations into stable hooks (useMemo). This - // ensures we don't conditionally call hooks later in render and keeps hook order stable. - const explicitPluginId = React.useMemo(() => { - try { - return ( - (props.error as any)?.pluginId ?? - (window as any)?.__SHIGGY_LAST_UNCAUGHT_ERROR?.pluginId ?? - (window as any)?.__SHIGGY_CURRENT_PLUGIN - ); - } catch { - return undefined; + const copyStackTrace = () => { + if (props.error.stack) { + Clipboard.setString(props.error.stack); + showToast("Stack trace copied"); + } else { + showToast("No stack trace available"); } - }, [props.error]); - - const parsedFrames = React.useMemo(() => { - try { - return parseErrorStack((props.error as any).stack) || []; - } catch { - return []; - } - }, [props.error]); - - const suspectedPlugins = React.useMemo(() => { - const suspected: Array<{ id: string; name: string }> = []; - try { - if (parsedFrames && parsedFrames.length) { - for (const [id, manifest] of registeredPlugins) { - try { - const jsPath = (manifest as any).jsPath; - if (!jsPath) continue; - for (const f of parsedFrames) { - if (!f.file) continue; - if ( - f.file.includes(jsPath) || - f.file.includes(`/plugins/scripts/${id}`) || - f.file.includes(`bunny-plugin/${id}`) - ) { - suspected.push({ - id, - name: - (manifest as any).display?.name ?? - (manifest as any).name ?? - id, - }); - break; - } - } - if (suspected.length) break; - } catch { - // ignore per-manifest errors - } - } - } - } catch { - // parsing failed — keep suspected empty - } - return suspected; - }, [parsedFrames]); - - const corePlugins = React.useMemo(() => { - try { - return getCorePlugins(); - } catch { - return {}; - } - }, []); - - const corePluginIds = React.useMemo( - () => Object.keys(corePlugins), - [corePlugins], - ); + }; return ( - - Uh oh. - - A crash occurred while rendering a component. This could be caused - by a plugin, ShiggyCord, or Discord itself. - - - {debugInfo.os.name}; {debugInfo.discord.build} ( - {debugInfo.discord.version}); {debugInfo.bunny.version} - - - - {props.error.message} + {/* Scrollable Content */} + + {/* Header - Using the same approach as About.tsx */} + + + Uh oh. + + + A crash occurred while rendering a component + + + {debugInfo.os.name} • Discord {debugInfo.discord.version} •{" "} + {debugInfo.bunny.version} + + - {explicitPluginId && registeredPlugins.has(explicitPluginId) ? ( - // 1) Prefer explicit annotation on the error (set by plugin loader) or current runtime marker. - (() => { - const manifest = registeredPlugins.get(explicitPluginId)!; - const name = - manifest.display?.name ?? manifest.name ?? explicitPluginId; - return ( - - - Suspected plugin - - This error is associated with a plugin based on runtime - context. + {/* Error Details */} + + + + + {props.error.message || "Unknown error occurred"} - - {name} ({explicitPluginId}) - - -