diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts
index 4744fc3..2713c5d 100644
--- a/lib/types/types.d.ts
+++ b/lib/types/types.d.ts
@@ -47,7 +47,7 @@ declare global {
declare global {
var __REACT_DEVTOOLS_GLOBAL_HOOK__: unknown | undefined
- var __reactDevTools:
+ var __REACT_DEVTOOLS__:
| {
version: string
exports: {
diff --git a/src/plugins/settings/SettingsConstants.ts b/src/plugins/settings/SettingsConstants.ts
index 0b9ab2e..61dba9e 100644
--- a/src/plugins/settings/SettingsConstants.ts
+++ b/src/plugins/settings/SettingsConstants.ts
@@ -8,6 +8,7 @@ export const MobileSetting = {
REVENGE_DEVELOPER: 'REVENGE_DEVELOPER',
// SUBSETTINGS
+
REVENGE_DISCORD_SERVER: 'REVENGE_DISCORD_SERVER',
REVENGE_GITHUB_ORGANIZATION: 'REVENGE_GITHUB_ORGANIZATION',
RELOAD_APP: 'RELOAD_APP',
@@ -17,6 +18,9 @@ export const MobileSetting = {
REACT_NATIVE_VERSION: 'REACT_NATIVE_VERSION',
HERMES_VERSION: 'HERMES_VERSION',
+ REACT_DEVTOOLS_VERSION: 'REACT_DEVTOOLS_VERSION',
+ REACT_DEVTOOLS_CONNECT: 'REACT_DEVTOOLS_CONNECT',
+ REACT_DEVTOOLS_DISCONNECT: 'REACT_DEVTOOLS_DISCONNECT',
EVALUATE_JAVASCRIPT: 'EVALUATE_JAVASCRIPT',
REVENGE_NOT_IMPLEMENTED: 'REVENGE_NOT_IMPLEMENTED',
diff --git a/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx
new file mode 100644
index 0000000..1b32e19
--- /dev/null
+++ b/src/plugins/settings/definitions/ReactDevToolsConnectSetting.tsx
@@ -0,0 +1,21 @@
+import { React } from '@revenge-mod/react'
+
+import AssetIcon from '../components/icons/AssetIcon'
+import { connectToDevTools, DevToolsContext, useIsDevToolsOpen } from '../devtools'
+import { MobileSetting } from '../SettingsConstants'
+
+import type { SettingsItem } from '@revenge-mod/discord/ui/settings'
+
+const ReactDevToolsConnectSetting: SettingsItem = {
+ parent: MobileSetting.REVENGE_DEVELOPER,
+ IconComponent: () => ,
+ title: () => 'Connect to React DevTools',
+ usePredicate: () => {
+ const dtOpen = useIsDevToolsOpen()
+ return DevToolsContext.available && !dtOpen
+ },
+ onPress: connectToDevTools,
+ type: 'pressable',
+}
+
+export default ReactDevToolsConnectSetting
diff --git a/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx
new file mode 100644
index 0000000..7491c0a
--- /dev/null
+++ b/src/plugins/settings/definitions/ReactDevToolsDisconnectSetting.tsx
@@ -0,0 +1,19 @@
+import { React } from '@revenge-mod/react'
+
+import AssetIcon from '../components/icons/AssetIcon'
+import { disconnectFromDevTools, useIsDevToolsOpen } from '../devtools'
+import { MobileSetting } from '../SettingsConstants'
+
+import type { SettingsItem } from '@revenge-mod/discord/ui/settings'
+
+const ReactDevToolsDisconnectSetting: SettingsItem = {
+ parent: MobileSetting.REVENGE_DEVELOPER,
+ IconComponent: () => ,
+ variant: 'danger',
+ title: () => 'Disconnect from React DevTools',
+ usePredicate: useIsDevToolsOpen,
+ onPress: disconnectFromDevTools,
+ type: 'pressable',
+}
+
+export default ReactDevToolsDisconnectSetting
diff --git a/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx b/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx
new file mode 100644
index 0000000..9c2d807
--- /dev/null
+++ b/src/plugins/settings/definitions/ReactDevToolsVersionSetting.tsx
@@ -0,0 +1,18 @@
+import { React } from '@revenge-mod/react'
+
+import AssetIcon from '../components/icons/AssetIcon'
+import { MobileSetting } from '../SettingsConstants'
+
+import type { SettingsItem } from '@revenge-mod/discord/ui/settings'
+import { DevToolsContext } from '../devtools'
+
+const ReactDevToolsVersionSetting: SettingsItem = {
+ parent: MobileSetting.REVENGE_DEVELOPER,
+ IconComponent: () => ,
+ title: () => 'React DevTools',
+ useDescription: () => __REACT_DEVTOOLS__!.version,
+ usePredicate: () => DevToolsContext.available,
+ type: 'static',
+}
+
+export default ReactDevToolsVersionSetting
diff --git a/src/plugins/settings/devtools.ts b/src/plugins/settings/devtools.ts
new file mode 100644
index 0000000..1e67d05
--- /dev/null
+++ b/src/plugins/settings/devtools.ts
@@ -0,0 +1,84 @@
+import { getAssetByName } from '@revenge-mod/assets'
+import { ToastActionCreators } from '@revenge-mod/discord/actions'
+import { React } from '@revenge-mod/react'
+import { getErrorStack } from '@revenge-mod/utils/errors'
+import { useReRender } from '@revenge-mod/utils/react'
+import { pluginApi } from '.'
+
+export const DevToolsContext: {
+ ws: WebSocket | undefined
+ address: string
+ open: boolean
+ available: boolean
+} = {
+ available: Boolean(globalThis.__REACT_DEVTOOLS__),
+ ws: undefined,
+ address: 'localhost:8097',
+ open: false,
+}
+
+type Subscription = (event: 1 | 2 | 3, err?: unknown) => void
+const subscriptions: Set = new Set()
+
+subscriptions.add((e, err) => {
+ if (e === 3) {
+ const actualError = (err as { message: string }).message ?? getErrorStack(err)
+
+ pluginApi.logger.error('React DevTools error:', actualError)
+
+ ToastActionCreators.open({
+ key: 'DEVTOOLS_ERROR',
+ icon: getAssetByName('CircleXIcon-primary')!.id,
+ content: actualError,
+ })
+ }
+})
+
+export function connectToDevTools() {
+ if (!DevToolsContext.available || DevToolsContext.ws) return
+
+ const websocket = (DevToolsContext.ws = new WebSocket(`ws://${DevToolsContext.address}`))
+
+ websocket.addEventListener('open', () => {
+ DevToolsContext.open = true
+ for (const sub of subscriptions) sub(1)
+ })
+
+ websocket.addEventListener('close', () => {
+ cleanup()
+ for (const sub of subscriptions) sub(2)
+ })
+
+ websocket.addEventListener('error', e => {
+ cleanup()
+ for (const sub of subscriptions) sub(3, e)
+ })
+
+ __REACT_DEVTOOLS__!.exports.connectToDevTools({
+ websocket,
+ })
+}
+
+function cleanup() {
+ DevToolsContext.open = false
+ DevToolsContext.ws = undefined
+}
+
+export function disconnectFromDevTools() {
+ if (DevToolsContext.ws) DevToolsContext.ws.close()
+}
+
+export function useIsDevToolsOpen() {
+ const rerender = useReRender()
+
+ React.useEffect(() => {
+ const sub: Subscription = e => (e === 1 || e === 2) && rerender()
+ subscriptions.add(sub)
+
+ return () => {
+ subscriptions.delete(sub)
+ }
+ }, [])
+
+ return DevToolsContext.open
+}
diff --git a/src/plugins/settings/register.ts b/src/plugins/settings/register.ts
index a96d19b..7ef52e8 100644
--- a/src/plugins/settings/register.ts
+++ b/src/plugins/settings/register.ts
@@ -13,6 +13,9 @@ import RevengeVersionSetting from './definitions/RevengeVersionSetting'
import HermesVersionSetting from './definitions/HermesVersionSetting'
import RevengeNotImplementedSetting from './definitions/RevengeNotImplementedSetting'
import EvaluateJavaScriptSetting from './definitions/EvaluateJavaScriptSetting'
+import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetting'
+import ReactDevToolsConnectSetting from './definitions/ReactDevToolsConnectSetting'
+import ReactDevToolsDisconnectSetting from './definitions/ReactDevToolsDisconnectSetting'
import ReloadAppSetting from './definitions/ReloadAppSetting'
import RevengeDiscordServerSetting from './definitions/RevengeDiscordServerSetting'
import RevengeGitHubOrganizationSetting from './definitions/RevengeGitHubOrganizationSetting'
@@ -30,6 +33,9 @@ registerSettingsItems({
[MobileSetting.REACT_VERSION]: ReactVersionSetting,
[MobileSetting.REACT_NATIVE_VERSION]: ReactNativeVersionSetting,
[MobileSetting.HERMES_VERSION]: HermesVersionSetting,
+ [MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting,
+ [MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting,
+ [MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting,
[MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting,
[MobileSetting.REVENGE_NOT_IMPLEMENTED]: RevengeNotImplementedSetting,
})
diff --git a/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx b/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx
index 5e9a737..a50c038 100644
--- a/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx
+++ b/src/plugins/settings/screens/RevengeDeveloperSettingScreen.tsx
@@ -1,20 +1,49 @@
+import { React, ReactNative } from '@revenge-mod/react'
+import { Components } from '@revenge-mod/discord/ui'
import { SettingListRenderer } from '@revenge-mod/discord/ui/settings'
import { MobileSetting } from '../SettingsConstants'
+import { DevToolsContext } from '../devtools'
+// TODO(plugins/settings): debug bridge
export default function RevengeDeveloperSettingScreen() {
return (
-
+
+ {globalThis.__REACT_DEVTOOLS__ && }
+
+
+ )
+}
+
+export function ReactDevToolsAddressSetting() {
+ return (
+
+ (DevToolsContext.address = text)}
+ returnKeyType="done"
+ />
+
)
}