import defer * as Actions from '@revenge-mod/discord/actions'
import { onFluxEventDispatched } from '@revenge-mod/discord/flux'
import { waitForModules } from '@revenge-mod/modules/finders'
import { withName } from '@revenge-mod/modules/finders/filters'
import { callNativeMethod } from '@revenge-mod/modules/native'
import { instead } from '@revenge-mod/patcher'
import {
InternalPluginFlags,
isDefaultsOnlyBoot,
PluginFlags,
registerInternalPlugin,
} from '@revenge-mod/plugins/_'
import { onRunApplicationFinished } from '@revenge-mod/react/native'
import { asap } from '@revenge-mod/utils/callback'
import { getErrorStack } from '@revenge-mod/utils/error'
import { AppState } from 'react-native'
import { FullVersion } from '~constants'
import { cache as assetsCache } from '../../../../lib/assets/src/caches'
import { cache as modulesCache } from '../../../../lib/modules/src/caches'
import { mErrorChain } from '../../../../lib/modules/src/metro/runtime'
import pluginSettings from '../../start/settings'
import defer * as Alerts from './components/alerts'
import defer * as ErrorBoundaryScreen from './components/ErrorBoundaryScreen'
import type { Component, ReactNode } from 'react'
registerInternalPlugin(
{
id: 'revenge.recovery',
name: 'Recovery',
description:
'Handles errors and provides troubleshooting options for Revenge.',
author: 'Revenge',
icon: 'ShieldIcon',
dependencies: { [pluginSettings]: {} },
},
{
preInit({ cleanup }) {
cleanup(freezeDetectionService())
},
start({ cleanup }) {
// @as-require
import('./settings')
cleanup(errorBoundaryService())
asap(() => {
if (isDefaultsOnlyBoot) {
Actions.AlertActionCreators.openAlert(
'revenge-recovery',
,
)
}
if (assetsCache.outdated || modulesCache.outdated) {
Actions.AlertActionCreators.openAlert(
'revenge-loader-outdated',
,
)
}
})
},
},
PluginFlags.Enabled,
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
)
const FreezeDetectionTimeout = 5000
function freezeDetectionService() {
let currentId: number | undefined
let cleared = false
let sub: { remove(): void } | undefined
let unsubFlux: (() => void) | undefined
const setTimer = () => {
onRunApplicationFinished(() => {
if (cleared) return
if (currentId !== undefined) clearTimeout(currentId)
currentId = setTimeout(() => {
currentId = undefined
if (AppState.currentState !== 'active') return
try {
let text =
'App was unable to start. This is likely caused by a plugin.\n\n' +
'You can launch Recovery Mode in the Recovery menu.'
if (mErrorChain.length > 0) {
text += `\n\nErroring modules chain: ${mErrorChain.map(([id]) => id).join(', ')}`
text += `\n\nStacks:\n\n${mErrorChain.map(([id, stack]) => `[${id}] ${getErrorStack(stack)}`).join('\n\n')}`
} else {
text += '\n\nNo erroring modules were detected.'
}
callNativeMethod('revenge.alertError', [text, FullVersion])
} catch (e) {
console.error(
'Failed to call native method "revenge.alertError":',
e,
)
}
}, FreezeDetectionTimeout)
})
}
const clear = (e?: T) => {
cleared = true
if (currentId !== undefined) {
clearTimeout(currentId)
currentId = undefined
}
sub?.remove()
unsubFlux?.()
return e
}
unsubFlux = onFluxEventDispatched('APP_STATE_UPDATE', clear)
// clear() may have run before unsubStore/unsubFlux was assigned, so run it again to unsub
if (cleared) {
clear()
return clear
}
sub = AppState.addEventListener('change', state => {
if (state === 'active') setTimer()
else if (currentId !== undefined) {
clearTimeout(currentId)
currentId = undefined
}
})
if (AppState.currentState === 'active') setTimer()
return clear
}
function errorBoundaryService() {
const unsub = waitForModules(
withName('ErrorBoundary'),
exports => {
unsub()
instead(
exports.prototype,
'render',
function (this: DiscordErrorBoundary) {
if (this.state.error)
return (
{
this.setState({
error: null,
info: null,
})
}}
/>
)
return this.props.children
},
)
},
{
cached: true,
},
)
return unsub
}
declare class DiscordErrorBoundary extends Component<
{ children: ReactNode },
{
error: (Error & { componentStack?: string }) | unknown | null
info: { componentStack?: string } | null
}
> {
// render() is always called with `this` as the instance of DiscordErrorBoundary
render(this: DiscordErrorBoundary): ReactNode
discordErrorsSet: boolean
handleReload(): void
}