import { getAssetIdByName } from '@revenge-mod/assets'
import { styles } from '@revenge-mod/components/_'
import { Tokens } from '@revenge-mod/discord/common/tokens'
import { Design } from '@revenge-mod/discord/design'
import { ClientInfoModule } from '@revenge-mod/discord/native'
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
import { ReactNativeSafeAreaContext } from '@revenge-mod/externals/react-native-safe-area-context'
import { reloadApp } from '@revenge-mod/modules/native/app'
import { requestNextBootDefaultsOnly } from '@revenge-mod/plugins/_'
import { getErrorStack } from '@revenge-mod/utils/error'
import { ScrollView, StyleSheet, View } from 'react-native'
import { FullVersion } from '~/constants'
import { getRNVersion } from '~plugins/start/settings/definitions/shared'
import type { ComponentProps } from 'react'
const { createStyles, Button, Card, IconButton, Stack, Text } = Design
const useErrorBoundaryStyles = createStyles({
view: {
backgroundColor: Tokens.default.colors.BACKGROUND_BASE_LOWER,
paddingHorizontal: 16,
paddingVertical: 24,
flex: 1,
gap: 16,
},
})
const additionalStyles = StyleSheet.create({
headerContainer: {
gap: 4,
},
scrollView: {
gap: 8,
flex: 1,
},
stackTrace: {
fontFamily: 'monospace',
fontWeight: 'bold',
marginBottom: 8,
},
actions: {
flexShrink: 0,
},
})
interface RenderError extends Error {
componentStack?: string
}
export default function ErrorBoundaryScreen({
error,
rerender,
reload,
}: {
error: RenderError | unknown
rerender: () => void
reload: () => void
}) {
const errorBoundaryStyles = useErrorBoundaryStyles()
const Client = ClientInfoModule.getConstants()
return (
Error!
An error was thrown while rendering components. This could
be caused by plugins, Revenge or Discord.
{Client.Version} ({Client.Build}) • RN {getRNVersion()} •{' '}
{FullVersion}
{String(error)}
{error instanceof Error && error.stack && (
{parseStackTrace(
error.stack?.slice(String(error).length + 1),
).map(({ at, file, line, column }) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: This never gets rerendered
{at}
{'\n'}
{file}
{
<>
:{line}:{column}
>
}
))}
)}
{error instanceof Error &&
(error as RenderError).componentStack && (
{...parseStackTrace(
(error as RenderError).componentStack!.slice(1),
).map(({ at }) => [
'<',
// biome-ignore lint/correctness/useJsxKeyInIterable: This never gets rerendered
{at},
'/>\n',
])}
)}
)
}
export interface LabeledCardProps extends ComponentProps {
label: string
rawContent?: string
scrollable?: boolean
}
export function LabeledCard(props: LabeledCardProps) {
const ViewComponent = props.scrollable ? ScrollView : View
return (
{props.label}
{props.rawContent && (
{
Clipboard.setString(props.rawContent as string)
}}
/>
)}
{props.children}
)
}
interface StackFrame {
at: string
file: string
line: number | null
column: number | null
}
const IndexBundleFilePath = (
HermesInternal as HermesInternalObject
).getFunctionLocation(alert).fileName
const RevengeFilePath = (
HermesInternal as HermesInternalObject
).getFunctionLocation(ErrorBoundaryScreen).fileName
const StackFrameRegex = /at (.+) \(([^:]+):(\d+):(\d+)\)|at (.+)? \(([^)]+)\)/
function parseStackTrace(stackTrace: string): StackFrame[] {
const frames: StackFrame[] = []
const lines = stackTrace.split('\n')
for (const line of lines) {
const match = StackFrameRegex.exec(line.trim())
if (match) {
let at: string
let path: string
let ln: number | null = null
let col: number | null = null
if (match[3] && match[4]) {
// Case where line and column numbers are present
at = match[1]!
path = match[2]!
ln = Number(match[3])
col = Number(match[4])
} else {
// Case where line and column numbers are missing (native functions or just file path)
at = match[5]!
path = match[6]!
}
switch (path) {
case IndexBundleFilePath:
path = 'Discord'
break
case RevengeFilePath:
path = 'Revenge'
break
}
frames.push({
at: at,
file: path,
line: ln,
column: col,
})
} else {
// For lines that don't match the expected format
frames.push({
at: 'UNABLE TO PARSE LINE',
file: line,
line: null,
column: null,
})
}
}
return frames
}