Repository: LovesWorking/rn-better-dev-tools Branch: main Commit: d300ff7074bd Files: 78 Total size: 260.7 KB Directory structure: gitextract_s7vy5tt0/ ├── .eslintignore ├── .eslintrc.json ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .npmrc ├── DEVELOPMENT.md ├── GITHUB_RELEASE.md ├── README.md ├── RELEASE.md ├── assets/ │ └── icon.icns ├── auto-release.sh ├── build-and-pack.sh ├── build-macos.sh ├── config.js ├── copy-to-desktop.sh ├── entitlements.plist ├── forge.config.ts ├── forge.env.d.ts ├── index.html ├── package.json ├── postcss.config.js ├── release-version.sh ├── socket-client.js ├── src/ │ ├── auto-updater.ts │ ├── components/ │ │ ├── App.tsx │ │ └── external-dash/ │ │ ├── Dash.tsx │ │ ├── DeviceSelection.tsx │ │ ├── LogConsole.tsx │ │ ├── Main.tsx │ │ ├── NoDevicesConnected.tsx │ │ ├── UserInfo/ │ │ │ ├── DeviceSpecificationsSection.tsx │ │ │ ├── EnvironmentVariablesSection.tsx │ │ │ ├── InfoRow.tsx │ │ │ ├── StorageControlsSection.tsx │ │ │ ├── TargetGlowEffect.tsx │ │ │ ├── UserCardDetails.tsx │ │ │ ├── UserCardHeader.tsx │ │ │ └── index.ts │ │ ├── UserInfo.tsx │ │ ├── _hooks/ │ │ │ └── useConnectedUsers.ts │ │ ├── hooks/ │ │ │ └── useDevToolsEventHandler.ts │ │ ├── providers.tsx │ │ ├── shared/ │ │ │ ├── hydration.ts │ │ │ └── types.ts │ │ ├── types/ │ │ │ ├── ClientQuery.ts │ │ │ └── User.ts │ │ ├── useSyncQueriesWeb.ts │ │ └── utils/ │ │ ├── devToolsEvents.ts │ │ ├── logStore.ts │ │ ├── logger.ts │ │ ├── platformUtils.tsx │ │ ├── storageQueryKeys.ts │ │ └── storageStore.ts │ ├── config.ts │ ├── index.css │ ├── main.ts │ ├── preload.ts │ ├── react-query-external-sync/ │ │ ├── README.md │ │ ├── User.ts │ │ ├── hydration.ts │ │ ├── index.ts │ │ ├── platformUtils.ts │ │ ├── types.ts │ │ ├── useMySocket.ts │ │ ├── useSyncQueries.ts │ │ └── utils/ │ │ └── logger.ts │ ├── renderer.ts │ ├── renderer.tsx │ ├── server/ │ │ └── socketHandle.ts │ ├── server.ts │ └── types.d.ts ├── tailwind.config.js ├── tanstack-query-devtools-5.74.7.tgz ├── tanstack-react-query-devtools-5.75.7.tgz ├── tsconfig.json ├── vite.main.config.ts ├── vite.preload.config.ts └── vite.renderer.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ # Dependencies node_modules/ # Build outputs build/ dist/ .next/ out/ # IDE Specific .idea/ .vscode/ # Specific files with TypeScript issues src/components/external-dash/LogConsole.tsx ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:import/electron", "plugin:import/typescript" ], "parser": "@typescript-eslint/parser", "settings": { "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] } }, "import/ignore": ["@tanstack/react-query-devtools"] }, "rules": { "import/no-unresolved": [ "error", { "ignore": ["^@tanstack/react-query-devtools"] } ] } } ================================================ FILE: .github/workflows/build.yml ================================================ --- name: Build/release on: push: tags: - "v*" # Add permissions block permissions: contents: write jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] arch: [x64, arm64] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10.4.1 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: "pnpm" - name: Install dependencies run: pnpm install - name: Import certificates if: runner.os == 'macOS' env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | # Write certificate to file and decode it echo "$MACOS_CERTIFICATE" | base64 -D > certificate.p12 # Create keychain security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain # Import certificate security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain # Verify certificate import security find-identity -v -p codesigning build.keychain - name: Build and package app env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} ARCH: ${{ matrix.arch }} run: pnpm run make --arch=${{ matrix.arch }} - name: Upload artifact uses: actions/upload-artifact@v4 with: name: app-${{ matrix.os }}-${{ matrix.arch }} path: "out/**/*.zip" if-no-files-found: error release: needs: build runs-on: ubuntu-latest steps: - name: Download all uploaded artifacts uses: actions/download-artifact@v4 with: path: ./downloaded-artifacts - name: Create or update release uses: ncipollo/release-action@v1 if: startsWith(github.ref, 'refs/tags/') with: allowUpdates: true replacesArtifacts: true removeArtifacts: true artifacts: "downloaded-artifacts/**/*.zip" draft: false prerelease: false token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock .DS_Store # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Webpack .webpack/ # Vite .vite/ # Electron-Forge out/ # Yalc .yalc/.env # Environment variables .env.local .env.* ================================================ FILE: .npmrc ================================================ node-linker = hoisted ================================================ FILE: DEVELOPMENT.md ================================================ # Development Guide This guide covers everything you need to know about developing React Native DevTools. ## 🛠 Prerequisites - Node.js 20 or later - pnpm 10.4.1 or later - macOS for building and signing - Apple Developer account for signing - GitHub CLI (`gh`) for releases ## 🚀 Getting Started 1. Clone the repository: ```bash git clone https://github.com/LovesWorking/rn-better-dev-tools.git cd rn-better-dev-tools ``` 2. Install dependencies: ```bash pnpm install ``` 3. Create a `.env` file based on `.env.example`: ```bash cp .env.example .env ``` 4. Fill in your Apple Developer credentials in `.env`: ``` APPLE_ID=your.email@example.com APPLE_PASSWORD=app-specific-password APPLE_TEAM_ID=your-team-id ``` ## 🏗 Building ### Development Build ```bash # Start in development mode with hot reload pnpm start # Build and copy to desktop for testing pnpm run make:desktop # Build only pnpm run make ``` ### Release Build We provide an automated release script that: - Bumps version (minor) - Builds locally to verify - Commits changes - Creates and pushes tag - Monitors GitHub Action progress ```bash # Using npm script pnpm run auto-release # Or directly ./auto-release.sh ``` ## 🐛 Debugging ### Enable Debug Logs Add to your `.env`: ```bash DEBUG=electron-osx-sign* ``` ### Common Issues 1. **Build Hanging on "Finalizing package"** - Check Apple Developer credentials - Verify keychain access - Run with debug logs enabled 2. **Permission Issues** ```bash # Fix directory permissions sudo chown -R $(whoami) . # Clean build artifacts rm -rf .vite out ``` 3. **Certificate Issues** - Verify Apple Developer membership is active - Check Team ID matches in `.env` - Ensure app-specific password is correct ### Development Commands ```bash # Clean install pnpm run nuke # Package without making distributables pnpm run package # Run linter pnpm run lint ``` ## 📦 Project Structure ``` . ├── src/ # Source code │ ├── main.ts # Main process │ ├── preload.ts # Preload scripts │ └── components/ # React components ├── assets/ # Static assets ├── .github/workflows/ # GitHub Actions └── forge.config.ts # Electron Forge config ``` ## 🔄 Release Process ### Automatic Release ```bash ./auto-release.sh ``` ### Manual Release Steps 1. Update version in `package.json` 2. Build and test locally 3. Create and push tag 4. GitHub Action will build and publish ## 🧪 Testing Before submitting a PR: 1. Test in development mode (`pnpm start`) 2. Build and test locally (`pnpm run make:desktop`) 3. Verify all features work 4. Check console for errors 5. Run linter (`pnpm run lint`) ================================================ FILE: GITHUB_RELEASE.md ================================================ # GitHub Release Process This guide explains how to release updates to GitHub and enable auto-updating for React Native DevTools. ## Automated Release Process The recommended way to create a new release is to use the automated script: 1. Make sure your changes are committed to the main branch 2. Run the release script: ```bash pnpm run release # or directly with ./release-version.sh ``` 3. Follow the interactive prompts to: - Select the version bump type (patch, minor, major, or custom) - Enter release notes - Confirm the release 4. The script will: - Update the version in package.json - Commit and push the changes - Create and push a git tag - Monitor the GitHub Actions workflow - Automatically publish the release when complete ## Manual Releases If you need to manually publish a release: 1. Ensure you have a GitHub access token with "repo" permissions 2. Set the token as an environment variable: ```bash export GITHUB_TOKEN=your_github_token ``` 3. Run the pack script: ```bash pnpm run pack # or directly with ./build-and-pack.sh ``` ## GitHub Actions Automated Releases For automated releases using GitHub Actions: 1. Create a new tag following semantic versioning: ```bash git tag v1.x.x git push origin v1.x.x ``` 2. The GitHub Actions workflow will automatically build and release the app 3. The release will initially be created as a draft 4. Review the release and publish it when ready ## Auto-Updates The app is configured to automatically check for updates when running. When a new version is released on GitHub: 1. Users with the previous version will automatically receive update notifications 2. Updates are downloaded in the background 3. The update will be installed when the user restarts the app ## Configuration Files The auto-update system is configured in several files: - `forge.config.ts`: Contains the GitHub publisher configuration - `package.json`: Contains the electron-builder configuration - `src/auto-updater.ts`: Implements the auto-update checking logic - `.github/workflows/build.yml`: Defines the GitHub Actions workflow for automated builds ## Troubleshooting - If the auto-updater isn't working, check the log file at: - macOS: `~/Library/Logs/react-native-devtools/main.log` - Make sure your repository is public, or if it's private, ensure users have proper access tokens configured - To debug update issues, run the app with the `DEBUG` environment variable: ```bash DEBUG=electron-updater npm start ``` ## Version Updates The automated release script handles version updates for you, but if you need to do it manually: 1. Update the version in `package.json` 2. Commit the changes 3. Create a new tag matching the version number 4. Push the tag to GitHub The version format should follow semantic versioning: `MAJOR.MINOR.PATCH` ================================================ FILE: README.md ================================================ # React Native DevTools Enhanced developer tools for React Native applications, supporting React Query DevTools and device storage monitoring with a beautiful native interface. ![ios pokemon](https://github.com/user-attachments/assets/25ffb38c-2e41-4aa9-a3c7-6f74383a75fc) https://github.com/user-attachments/assets/5c0c5748-e031-427a-8ebf-9c085434e8ba ## Example app https://github.com/LovesWorking/RN-Dev-Tools-Example ### If you need internal React Query dev tools within the device you can use my other package here! https://github.com/LovesWorking/react-native-react-query-devtools ## ✨ Features - 🔄 Real-time React Query state monitoring - 💾 **Device storage monitoring with CRUD operations** - MMKV, AsyncStorage, and SecureStorage - 🌐 **Environment variables monitoring** - View and track public environment variables - 🎨 Beautiful native macOS interface - 🚀 Automatic connection to React apps - 📊 Query status visualization - 🔌 Socket.IO integration for reliable communication - ⚡️ Simple setup with NPM package - 📱 Works with **any React-based platform**: React Native, React Web, Next.js, Expo, tvOS, VR, etc. - 🛑 Zero-config production safety - automatically disabled in production builds ## 📦 Installation ### DevTools Desktop Application (macOS) > **⚠️ Important**: The desktop app has currently only been tested on Apple Silicon Macs (M1/M2/M3). > If you encounter issues on Intel-based Macs, please [open an issue](https://github.com/LovesWorking/rn-better-dev-tools/issues) > and we'll work together to fix it. 1. Download the latest release from the [Releases page](https://github.com/LovesWorking/rn-better-dev-tools/releases) 2. Extract the ZIP file 3. Move the app to your Applications folder 4. Launch the app ### React Application Integration The easiest way to connect your React application to the DevTools is by installing the npm package: ```bash # Using npm npm install --save-dev react-query-external-sync socket.io-client npm install expo-device # For automatic device detection # Using yarn yarn add -D react-query-external-sync socket.io-client yarn add expo-device # For automatic device detection # Using pnpm (recommended) pnpm add -D react-query-external-sync socket.io-client pnpm add expo-device # For automatic device detection ``` ## 🚀 Quick Start 1. Download and launch the React Native DevTools application 2. Add the hook to your application where you set up your React Query context: ```jsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useSyncQueriesExternal } from "react-query-external-sync"; // Import Platform for React Native or use other platform detection for web/desktop import { Platform } from "react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; import * as SecureStore from "expo-secure-store"; import * as ExpoDevice from "expo-device"; import { storage } from "./mmkv"; // Your MMKV instance // Create your query client const queryClient = new QueryClient(); function App() { return ( ); } function AppContent() { // Set up the sync hook - automatically disabled in production! useSyncQueriesExternal({ queryClient, socketURL: "http://localhost:42831", // Default port for React Native DevTools deviceName: Platform?.OS || "web", // Platform detection platform: Platform?.OS || "web", // Use appropriate platform identifier deviceId: Platform?.OS || "web", // Use a PERSISTENT identifier (see note below) isDevice: ExpoDevice.isDevice, // Automatically detects real devices vs emulators extraDeviceInfo: { // Optional additional info about your device appVersion: "1.0.0", // Add any relevant platform info }, enableLogs: false, envVariables: { NODE_ENV: process.env.NODE_ENV, // Add any private environment variables you want to monitor // Public environment variables are automatically loaded }, // Storage monitoring with CRUD operations mmkvStorage: storage, // MMKV storage for ['#storage', 'mmkv', 'key'] queries + monitoring asyncStorage: AsyncStorage, // AsyncStorage for ['#storage', 'async', 'key'] queries + monitoring secureStorage: SecureStore, // SecureStore for ['#storage', 'secure', 'key'] queries + monitoring secureStorageKeys: [ "userToken", "refreshToken", "biometricKey", "deviceId", ], // SecureStore keys to monitor }); // Your app content return ; } ``` 3. Start your React application 4. DevTools will automatically detect and connect to your running application ## 🔒 Production Safety The React Query External Sync package is automatically disabled in production builds. ```jsx // The package handles this internally: if (process.env.NODE_ENV !== "production") { useSyncQueries = require("./new-sync/useSyncQueries").useSyncQueries; } else { // In production, this becomes a no-op function useSyncQueries = () => ({ isConnected: false, connect: () => {}, disconnect: () => {}, socket: null, users: [], }); } ``` ### 📱 Using with Real Devices (Local Network) When testing on real devices connected to your local network, you'll need to use your host machine's IP address instead of `localhost`. Here's a helpful setup for Expo apps (contributed by [ShoeBoom](https://github.com/ShoeBoom)): ```jsx import Constants from "expo-constants"; import AsyncStorage from "@react-native-async-storage/async-storage"; import * as SecureStore from "expo-secure-store"; import { storage } from "./mmkv"; // Your MMKV instance // Get the host IP address dynamically const hostIP = Constants.expoGoConfig?.debuggerHost?.split(`:`)[0] || Constants.expoConfig?.hostUri?.split(`:`)[0]; function AppContent() { useSyncQueriesExternal({ queryClient, socketURL: `http://${hostIP}:42831`, // Use local network IP deviceName: Platform?.OS || "web", platform: Platform?.OS || "web", deviceId: Platform?.OS || "web", extraDeviceInfo: { appVersion: "1.0.0", }, enableLogs: false, envVariables: { NODE_ENV: process.env.NODE_ENV, }, // Storage monitoring mmkvStorage: storage, asyncStorage: AsyncStorage, secureStorage: SecureStore, secureStorageKeys: ["userToken", "refreshToken"], }); return ; } ``` > **Note**: For optimal connection, launch DevTools before starting your application. ## 💡 Usage Tips - Keep DevTools running while developing - Monitor query states in real-time - View detailed query information - Track cache updates and invalidations - **Monitor device storage**: View and modify MMKV, AsyncStorage, and SecureStorage data in real-time - **Track environment variables**: Monitor public environment variables across your application - **Use storage queries**: Access storage data via React Query with keys like `['#storage', 'mmkv', 'key']` - The hook is automatically disabled in production builds, no configuration needed ## 📱 Platform Support React Native DevTools works with **any React-based application**, regardless of platform: - 📱 Mobile: iOS, Android - 🖥️ Web: React, Next.js, Remix, etc. - 🖥️ Desktop: Electron, Tauri - 📺 TV: tvOS, Android TV - 🥽 VR/AR: Meta Quest, etc. - 💻 Cross-platform: Expo, React Native Web If your platform can run React and connect to a socket server, it will work with these DevTools! ## 💾 Storage Monitoring React Native DevTools now includes powerful storage monitoring capabilities with full CRUD operations: ### Supported Storage Types - **MMKV**: High-performance key-value storage - **AsyncStorage**: React Native's standard async storage - **SecureStorage**: Secure storage for sensitive data (Expo SecureStore) ### Features - **Real-time monitoring**: See storage changes as they happen - **CRUD operations**: Create, read, update, and delete storage entries directly from DevTools - **React Query integration**: Access storage data via queries like `['#storage', 'mmkv', 'keyName']` - **Type-safe operations**: Automatic serialization/deserialization of complex data types - **Secure data handling**: SecureStorage keys are monitored securely ### Usage Example ```jsx // In your app, use React Query to interact with storage import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; // Read from MMKV storage const { data: userData } = useQuery({ queryKey: ["#storage", "mmkv", "user"], // Data will be automatically synced with DevTools }); // Write to AsyncStorage const queryClient = useQueryClient(); const updateAsyncStorage = useMutation({ mutationFn: async ({ key, value }) => { await AsyncStorage.setItem(key, JSON.stringify(value)); // Invalidate to trigger sync queryClient.invalidateQueries(["#storage", "async", key]); }, }); ``` ## ⚙️ Configuration Options The `useSyncQueriesExternal` hook accepts the following options: | Option | Type | Required | Description | | ------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------- | | `queryClient` | QueryClient | Yes | Your React Query client instance | | `socketURL` | string | Yes | URL of the socket server (e.g., 'http://localhost:42831') | | `deviceName` | string | Yes | Human-readable name for your device | | `platform` | string | Yes | Platform identifier ('ios', 'android', 'web', 'macos', 'windows', etc.) | | `deviceId` | string | Yes | Unique identifier for your device | | `isDevice` | boolean | No | Whether running on a real device (true) or emulator (false). Used for Android socket URL handling (default: true) | | `extraDeviceInfo` | object | No | Additional device metadata to display in DevTools | | `enableLogs` | boolean | No | Enable console logging for debugging (default: false) | | `envVariables` | object | No | Private environment variables to sync with DevTools (public vars are auto-loaded) | | `mmkvStorage` | MmkvStorage | No | MMKV storage instance for real-time monitoring | | `asyncStorage` | AsyncStorage | No | AsyncStorage instance for polling-based monitoring | | `secureStorage` | SecureStore | No | SecureStore instance for secure data monitoring | | `secureStorageKeys` | string[] | No | Array of SecureStore keys to monitor (required if using secureStorage) | ## 🔮 Future Plans React Native DevTools is actively being developed with exciting features on the roadmap: - ✅ **Storage Viewers**: Beautiful interfaces for viewing and modifying storage (AsyncStorage, MMKV, SecureStorage) - **Now Available!** - 🌐 **Network Request Monitoring**: Track API calls, WebSockets, and GraphQL requests - ❌ **Failed Request Tracking**: Easily identify and debug network failures - 🔄 **Remote Expo DevTools**: Trigger Expo DevTools commands remotely without using the command line - 🧩 **Plugin System**: Allow community extensions for specialized debugging tasks - 🗄️ **Drizzle Studio Plugin**: Integration with Drizzle ORM for database management Stay tuned for updates! ## 🤝 Contributing I welcome contributions! See [Development Guide](DEVELOPMENT.md) for details on: - Setting up the development environment - Building and testing - Release process - Contribution guidelines ## 🐛 Troubleshooting Having issues? Check these common solutions: 1. **App Not Connecting** - Ensure DevTools is launched before your React app - Check that your React app is running - Verify you're on the same network - Make sure the `socketURL` is correctly pointing to localhost:42831 - Verify the Socket.IO client is properly installed in your app - Check that the `useSyncQueriesExternal` hook is properly implemented 2. **App Not Starting** - Verify you're using the latest version - Check system requirements (macOS with Apple Silicon chip) - Try reinstalling the application - If using an Intel Mac and encountering issues, please report them 3. **Socket Connection Issues** - Make sure no firewall is blocking the connection on port 42831 - Restart both the DevTools app and your React app - Check the console logs with `enableLogs: true` for any error messages - If the React Query data is too large the socket connection will crash! If you see the device connect and then disconnect with no logs this is what's happening. You'll need to fix your query cache to not be so large. 4. **Data Not Syncing** - Confirm you're passing the correct `queryClient` instance - Set `enableLogs: true` to see connection information 5. **Android Real Device Connection Issues** - If using a real Android device with React Native CLI and ADB, ensure `isDevice: true` - The package transforms `localhost` to `10.0.2.2` for emulators only - Use `ExpoDevice.isDevice` for automatic detection: `import * as ExpoDevice from "expo-device"` - Check network connectivity between your device and development machine 6. **DevTools App Issues** - Make sure your `deviceId` is persistent (see below) - Verify you're using the latest version - Check system requirements (macOS with Apple Silicon chip) - Try reinstalling the application - If using an Intel Mac and encountering issues, please report them 7. **Storage Monitoring Issues** - Ensure storage instances are properly passed to the hook - For SecureStorage, make sure `secureStorageKeys` array is provided - Check that storage permissions are granted on the device - Verify storage libraries are properly installed and configured - Use `enableLogs: true` to see storage sync information That's it! If you're still having issues, visit the [GitHub repository](https://github.com/LovesWorking/rn-better-dev-tools/issues) for support. ## 🏷️ Device Type Detection The `isDevice` prop helps the DevTools distinguish between real devices and simulators/emulators. This is **crucial for Android connectivity** - the package automatically handles URL transformation for Android emulators (localhost → 10.0.2.2) but needs to know if you're running on a real device to avoid this transformation. ### ⚠️ Android Connection Issue On real Android devices using React Native CLI and ADB, the automatic emulator detection can incorrectly transform `localhost` to `10.0.2.2`, breaking WebSocket connections. Setting `isDevice: true` prevents this transformation. **Recommended approaches:** ```jsx // Best approach using Expo Device (works with bare React Native too) import * as ExpoDevice from "expo-device"; useSyncQueriesExternal({ queryClient, socketURL: "http://localhost:42831", deviceName: Platform.OS, platform: Platform.OS, deviceId: Platform.OS, isDevice: ExpoDevice.isDevice, // Automatically detects real devices vs emulators // ... other props }); // Alternative: Simple approach using React Native's __DEV__ flag isDevice: !__DEV__, // true for production/real devices, false for development/simulators // Alternative: More sophisticated detection using react-native-device-info import DeviceInfo from 'react-native-device-info'; isDevice: !DeviceInfo.isEmulator(), // Automatically detects if running on emulator // Manual control for specific scenarios isDevice: Platform.OS === 'ios' ? !Platform.isPad : Platform.OS !== 'web', ``` ## ⚠️ Important Note About Device IDs The `deviceId` parameter must be **persistent** across app restarts and re-renders. Using a value that changes (like `Date.now()`) will cause each render to be treated as a new device. **Recommended approaches:** ```jsx // Simple approach for single devices deviceId: Platform.OS, // Works if you only have one device per platform // Better approach for multiple simulators/devices of same type // Using AsyncStorage, MMKV, or another storage solution const [deviceId, setDeviceId] = useState(Platform.OS); useEffect(() => { const loadOrCreateDeviceId = async () => { // Try to load existing ID const storedId = await AsyncStorage.getItem('deviceId'); if (storedId) { setDeviceId(storedId); } else { // First launch - generate and store a persistent ID const newId = `${Platform.OS}-${Date.now()}`; await AsyncStorage.setItem('deviceId', newId); setDeviceId(newId); } }; loadOrCreateDeviceId(); }, []); ``` For more detailed troubleshooting, see our [Development Guide](DEVELOPMENT.md). ## 📄 License MIT --- Made with ❤️ by [LovesWorking](https://github.com/LovesWorking) ## 🚀 More **Take a shortcut from web developer to mobile development fluency with guided learning** Enjoyed this project? Learn to use React Native to build production-ready, native mobile apps for both iOS and Android based on your existing web development skills. banner ================================================ FILE: RELEASE.md ================================================ # Releasing React Native DevTools This document outlines the process for creating a new release of React Native DevTools. ## Automated Release Process (Recommended) The easiest way to create a new release is to use our automated script: ```bash pnpm run release ``` This interactive script will: 1. Prompt you to select a version bump type (patch, minor, major, or custom) 2. Update the version in package.json 3. Ask for release notes 4. Commit and push the changes 5. Create and push a git tag 6. Monitor the GitHub Actions workflow 7. Automatically publish the release when complete ## Manual Release Process If you need to perform the release manually, follow these steps: 1. Update the version in `package.json`: ```json { "version": "x.y.z", ... } ``` 2. Commit your changes: ```bash git add package.json git commit -m "Bump version to x.y.z" ``` 3. Create and push a new tag: ```bash git tag -a vx.y.z -m "Version x.y.z" git push origin vx.y.z ``` 4. The GitHub Actions workflow will automatically: - Build the app for macOS - Create a draft release with all the built installers - Add the release notes 5. Go to the GitHub Releases page, review the draft release, add any additional notes, and publish it. ## Local Build and Package If you want to build the app locally without publishing: ```bash pnpm run pack ``` This will: - Build the app - Create installation packages - Copy them to your Desktop in a "release rn better tools" folder ## Testing Before Release Before creating a new release tag, make sure to: 1. Test the app thoroughly on your local machine 2. Run `pnpm run make` locally to ensure the build process completes without errors 3. Test the generated installers ## Troubleshooting If the GitHub Actions build fails: 1. Check the workflow logs for errors 2. Make sure the repository has the necessary secrets and permissions set up 3. Try running the build locally to isolate the issue ## Auto-Update Feature The app includes auto-update functionality. When a new release is published: 1. Existing users will be automatically notified of the update 2. The update will be downloaded in the background 3. The update will be installed when the user restarts the app See GITHUB_RELEASE.md for more details on the auto-update configuration. ================================================ FILE: assets/icon.icns ================================================ // This is a placeholder - you need to replace this with an actual .icns file // For now, you'll need to create or convert your icon to .icns format manually // You can use tools like iconutil or online converters to create .icns files ================================================ FILE: auto-release.sh ================================================ #!/bin/bash # Set error handling set -e # Colors for better logging RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Function for logging log() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" } error() { echo -e "${RED}[ERROR]${NC} $1" exit 1 } success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Check if we're in the right directory if [ ! -f "package.json" ]; then error "Must be run from project root directory" fi # Get current version from package.json CURRENT_VERSION=$(node -p "require('./package.json').version") log "Current version: $CURRENT_VERSION" # Calculate new version (minor bump) NEW_VERSION=$(node -p " const [major, minor, patch] = '$CURRENT_VERSION'.split('.'); \`\${major}.\${parseInt(minor) + 1}.0\` ") log "New version will be: $NEW_VERSION" # Update version in package.json log "Updating package.json version..." npm version $NEW_VERSION --no-git-tag-version # Build locally first to ensure everything works log "Building locally to verify..." pnpm run make:desktop || error "Local build failed" success "Local build successful!" # Stage all changes log "Staging changes..." git add . || error "Failed to stage changes" # Commit with conventional commit message log "Committing changes..." git commit -m "chore: release v$NEW_VERSION" || error "Failed to commit changes" # Create and push tag log "Creating tag v$NEW_VERSION..." git tag -d "v$NEW_VERSION" 2>/dev/null || true git push origin ":refs/tags/v$NEW_VERSION" 2>/dev/null || true git tag "v$NEW_VERSION" || error "Failed to create tag" # Push changes and tags log "Pushing changes and tags..." git push && git push --tags || error "Failed to push changes" success "Changes pushed successfully!" # Wait for GitHub Action to start log "Waiting for GitHub Action to start..." sleep 5 # Monitor GitHub Action progress MAX_ATTEMPTS=30 ATTEMPT=1 while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do STATUS=$(gh run list --json status,name --jq '.[0].status' 2>/dev/null || echo "unknown") if [ "$STATUS" = "completed" ]; then success "GitHub Action completed successfully!" break elif [ "$STATUS" = "failed" ]; then error "GitHub Action failed. Check the logs with: gh run view" fi log "Build still in progress... (Attempt $ATTEMPT/$MAX_ATTEMPTS)" sleep 10 ATTEMPT=$((ATTEMPT + 1)) done if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then warning "Timed out waiting for GitHub Action to complete. Check status manually with: gh run view" fi success "Release process completed!" log "New version v$NEW_VERSION is being published" log "You can check the release status at: https://github.com/LovesWorking/rn-better-dev-tools/releases" ================================================ FILE: build-and-pack.sh ================================================ #!/bin/bash # Exit on any error set -e # Display what's happening echo "🚀 Building and packaging React Native DevTools..." # Variables VERSION=$(node -e "console.log(require('./package.json').version)") OUTPUT_DIR="$HOME/Desktop/release rn better tools" APP_NAME="React Native DevTools" # Ensure the output directory exists mkdir -p "$OUTPUT_DIR" # Clean previous builds echo "🧹 Cleaning previous builds..." rm -rf out/ # Check if GITHUB_TOKEN is set if [ -n "$GITHUB_TOKEN" ]; then # If GITHUB_TOKEN is set, publish to GitHub echo "🌐 Publishing to GitHub..." npx electron-forge publish else # If GITHUB_TOKEN is not set, just build locally echo "🔨 Building application locally..." npx electron-forge make --targets=@electron-forge/maker-zip # Copy the output to the destination folder echo "📦 Copying packaged app to destination folder..." cp "out/make/zip/darwin/arm64/$APP_NAME-darwin-arm64-$VERSION.zip" "$OUTPUT_DIR/" # Create a dated copy for versioning/archiving purposes DATED_FILENAME="$APP_NAME-darwin-arm64-$VERSION-$(date +%Y%m%d%H%M).zip" cp "out/make/zip/darwin/arm64/$APP_NAME-darwin-arm64-$VERSION.zip" "$OUTPUT_DIR/$DATED_FILENAME" echo "✅ Build and packaging complete!" echo "📝 Files created:" echo " - $OUTPUT_DIR/$APP_NAME-darwin-arm64-$VERSION.zip" echo " - $OUTPUT_DIR/$DATED_FILENAME" echo "" echo "🖥️ Open folder using: open \"$OUTPUT_DIR\"" echo "" echo "⚠️ Note: To publish to GitHub, set the GITHUB_TOKEN environment variable and run this script again." fi ================================================ FILE: build-macos.sh ================================================ #!/bin/bash # Exit on error set -e echo "=== Building React Native DevTools for macOS ===" # Clean previous builds rm -rf out || true echo "✅ Cleaned previous builds" # Install dependencies pnpm install echo "✅ Dependencies installed" # Build package echo "🔨 Building macOS package..." pnpm run make # Check if ZIP was created ZIP_PATH=$(find out/make -name "*.zip" | head -n 1) if [ -f "$ZIP_PATH" ]; then echo "✅ ZIP package created at: $ZIP_PATH" echo "✅ Total size: $(du -h "$ZIP_PATH" | cut -f1)" echo "" echo "To run the app:" echo "1. Extract the ZIP file" echo "2. Move the app to your Applications folder" echo "3. Open the app (you may need to right-click and select Open for the first time)" echo "" echo "To release:" echo "1. Update version in package.json" echo "2. Commit changes" echo "3. Create and push tag: git tag -a v1.0.0 -m 'v1.0.0' && git push origin v1.0.0" echo "" echo "Build complete! 🎉" else echo "❌ ZIP package was not created. Check for errors above." exit 1 fi ================================================ FILE: config.js ================================================ /** * Socket.IO Test Client Config * CommonJS version for use with the test script */ // Server configuration const SERVER_PORT = 42831; // Using an uncommon port to avoid conflicts // Client configuration const CLIENT_URL = `http://localhost:${SERVER_PORT}`; module.exports = { SERVER_PORT, CLIENT_URL, }; ================================================ FILE: copy-to-desktop.sh ================================================ #!/bin/bash # Get the version from package.json VERSION=$(node -p "require('./package.json').version") # Create desktop directory if it doesn't exist DESKTOP_DIR="$HOME/Desktop/rn-dev-tools-releases" mkdir -p "$DESKTOP_DIR" # Copy the zip file to desktop cp "out/make/zip/darwin/arm64/React Native DevTools-darwin-arm64-$VERSION.zip" "$DESKTOP_DIR/" echo "✅ Release copied to $DESKTOP_DIR" ================================================ FILE: entitlements.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation com.apple.security.device.audio-input com.apple.security.device.bluetooth com.apple.security.device.camera com.apple.security.device.print com.apple.security.device.usb com.apple.security.personal-information.location ================================================ FILE: forge.config.ts ================================================ import { config as dotenvConfig } from "dotenv"; import * as path from "path"; // Load .env file from the project root dotenvConfig({ path: path.resolve(process.cwd(), ".env") }); // Debug logging to verify environment variables console.log("Environment variables loaded:", { APPLE_ID: process.env.APPLE_ID, TEAM_ID: process.env.APPLE_TEAM_ID, DEBUG: process.env.DEBUG, }); import type { ForgeConfig } from "@electron-forge/shared-types"; import { MakerZIP } from "@electron-forge/maker-zip"; import { VitePlugin } from "@electron-forge/plugin-vite"; import { FusesPlugin } from "@electron-forge/plugin-fuses"; import { FuseV1Options, FuseVersion } from "@electron/fuses"; import { PublisherGithub } from "@electron-forge/publisher-github"; const config: ForgeConfig = { packagerConfig: { asar: true, icon: "./assets/icon", // No file extension required appBundleId: "com.lovesworking.rn-dev-tools", appCategoryType: "public.app-category.developer-tools", executableName: "React Native DevTools", osxSign: { identity: "6EC9AE0A608BB7CBBA6BCC7936689773E76D63F0", }, // The osxSign config comes with defaults that work out of the box in most cases, so we recommend you start with an empty configuration object. // For a full list of configuration options, see https://js.electronforge.io/modules/_electron_forge_shared_types.InternalOptions.html#OsxSignOptions osxNotarize: { appleId: process.env.APPLE_ID!, appleIdPassword: process.env.APPLE_PASSWORD!, teamId: process.env.APPLE_TEAM_ID!, }, }, rebuildConfig: {}, makers: [ // Build a ZIP for all platforms new MakerZIP({}, ["darwin", "linux", "win32"]), // The following makers are commented out for now // new MakerSquirrel({}), // new MakerRpm({}), // new MakerDeb({}) ], publishers: [ new PublisherGithub({ repository: { owner: "lovesworking", // Replace with your GitHub username or organization name: "rn-better-dev-tools", // Replace with your repository name }, prerelease: false, // Set to true if you want to mark releases as pre-releases }), ], plugins: [ new VitePlugin({ // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // If you are familiar with Vite configuration, it will look really familiar. build: [ { // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. entry: "src/main.ts", config: "vite.main.config.ts", target: "main", }, { entry: "src/preload.ts", config: "vite.preload.config.ts", target: "preload", }, ], renderer: [ { name: "main_window", config: "vite.renderer.config.ts", }, ], }), // Fuses are used to enable/disable various Electron functionality // at package time, before code signing the application new FusesPlugin({ version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, [FuseV1Options.EnableNodeCliInspectArguments]: false, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true, }), ], }; export default config; ================================================ FILE: forge.env.d.ts ================================================ /// ================================================ FILE: index.html ================================================ React Native Dev Tools
================================================ FILE: package.json ================================================ { "name": "rn-better-dev-tools", "productName": "React Native DevTools", "version": "1.1.0", "description": "Enhanced developer tools for React Native applications (Only supports React Query DevTools Right Now)", "main": ".vite/build/main.js", "scripts": { "release": "./release-version.sh", "pack": "./build-and-pack.sh", "nuke": "rm -rf node_modules .vite/build pnpm-lock.yaml && pnpm store prune && pnpm cache clean && pnpm install", "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "make:desktop": "pnpm run make && ./copy-to-desktop.sh", "publish": "electron-forge publish", "lint": "eslint --ext .ts,.tsx .", "auto-release": "./auto-release.sh" }, "keywords": [], "author": { "name": "lovesworking", "email": "austinlovesworking@gmail.com" }, "license": "MIT", "pnpm": { "onlyBuiltDependencies": [ "electron" ] }, "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af", "devDependencies": { "@electron-forge/cli": "^7.8.0", "@electron-forge/maker-deb": "^7.8.0", "@electron-forge/maker-dmg": "^7.8.0", "@electron-forge/maker-rpm": "^7.8.0", "@electron-forge/maker-squirrel": "^7.8.0", "@electron-forge/maker-zip": "^7.8.0", "@electron-forge/plugin-auto-unpack-natives": "^7.8.0", "@electron-forge/plugin-fuses": "^7.8.0", "@electron-forge/plugin-vite": "^7.8.0", "@electron-forge/publisher-github": "^7.8.0", "@electron/fuses": "^1.8.0", "@types/express": "^5.0.1", "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@types/socket.io": "^3.0.2", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.16", "dotenv": "^16.4.7", "electron": "35.1.2", "eslint": "^8.0.1", "eslint-plugin-import": "^2.25.0", "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "ts-node": "^10.0.0", "typescript": "~4.5.4", "vite": "^5.0.12" }, "dependencies": { "@tanstack/query-devtools": "file:tanstack-query-devtools-5.74.7.tgz", "@tanstack/react-query": "^5.75.7", "@tanstack/react-query-devtools": "file:tanstack-react-query-devtools-5.75.7.tgz", "bufferutil": "^4.0.9", "electron-log": "^5.3.3", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.6.2", "express": "^4.21.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-use": "^17.6.0", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "utf-8-validate": "^6.0.5", "zustand": "^5.0.3" }, "build": { "appId": "com.lovesworking.rn-dev-tools", "productName": "React Native DevTools", "mac": { "category": "public.app-category.developer-tools", "target": "zip" }, "publish": { "provider": "github", "owner": "lovesworking", "repo": "rn-better-dev-tools" } } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: release-version.sh ================================================ #!/bin/bash # Exit on any error set -e # Colors for better output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Function to display status messages function echo_status() { echo -e "${BLUE}[INFO]${NC} $1" } # Function to display success messages function echo_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } # Function to display error messages function echo_error() { echo -e "${RED}[ERROR]${NC} $1" } # Function to display warning messages function echo_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Check if gh CLI is installed if ! command -v gh &> /dev/null; then echo_error "GitHub CLI (gh) is not installed. Please install it first." echo "You can install it with: brew install gh" exit 1 fi # Check if gh is authenticated if ! gh auth status &> /dev/null; then echo_error "You're not authenticated with GitHub CLI." echo "Please run: gh auth login" exit 1 fi # Ensure we're on the main branch current_branch=$(git rev-parse --abbrev-ref HEAD) if [ "$current_branch" != "main" ]; then echo_warning "You are not on the main branch. Switching to main..." git checkout main fi # Make sure the working directory is clean if ! git diff-index --quiet HEAD --; then echo_error "Your working directory has uncommitted changes." echo "Please commit or stash them before running this script." exit 1 fi # Pull latest changes echo_status "Pulling latest changes from main..." git pull origin main # Get current version from package.json current_version=$(node -e "console.log(require('./package.json').version)") echo_status "Current version: ${current_version}" # Prompt for version bump type echo "Select version bump type:" echo "1) Patch (1.0.0 -> 1.0.1) - For bug fixes" echo "2) Minor (1.0.0 -> 1.1.0) - For new features" echo "3) Major (1.0.0 -> 2.0.0) - For breaking changes" echo "4) Custom (Enter a specific version)" read -p "Enter your choice (1-4): " version_choice case $version_choice in 1) bump_type="patch" ;; 2) bump_type="minor" ;; 3) bump_type="major" ;; 4) read -p "Enter the new version (e.g., 1.2.3): " custom_version bump_type="custom" new_version=$custom_version ;; *) echo_error "Invalid choice. Exiting." exit 1 ;; esac # Calculate new version for non-custom bumps if [ "$bump_type" != "custom" ]; then # Split version by dots IFS='.' read -ra VERSION_PARTS <<< "$current_version" major=${VERSION_PARTS[0]} minor=${VERSION_PARTS[1]} patch=${VERSION_PARTS[2]} case $bump_type in patch) patch=$((patch + 1)) ;; minor) minor=$((minor + 1)) patch=0 ;; major) major=$((major + 1)) minor=0 patch=0 ;; esac new_version="${major}.${minor}.${patch}" fi echo_status "New version will be: ${new_version}" # Prompt for confirmation read -p "Proceed with release? (y/n): " confirm if [[ $confirm != "y" && $confirm != "Y" ]]; then echo_warning "Release canceled." exit 0 fi # Update version in package.json echo_status "Updating package.json version to ${new_version}..." # Use a temporary file to avoid issues with inline editing node -e " const fs = require('fs'); const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); packageJson.version = '${new_version}'; fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2) + '\n'); " # Prompt for release notes echo "Enter release notes (or leave empty for default message):" echo "Type your notes and press Ctrl+D when finished (or Ctrl+C to cancel)" release_notes=$(cat) if [ -z "$release_notes" ]; then release_notes="Version ${new_version}" fi # Commit the version change echo_status "Committing version change..." git add package.json git commit -m "Bump version to ${new_version}" # Push the changes echo_status "Pushing changes to main..." git push origin main # Create and push tag tag_name="v${new_version}" echo_status "Creating and pushing tag ${tag_name}..." git tag $tag_name git push origin $tag_name echo_success "Version ${new_version} has been released!" echo "GitHub Actions should now be building and publishing your release." # Monitor the GitHub Actions workflow echo_status "Monitoring workflow run..." sleep 3 # Give GitHub a moment to register the workflow # Find the latest run ID for the tag we just pushed echo_status "Getting the latest workflow run ID..." run_id=$(gh run list --workflow "Build and Release macOS App" --limit 1 --json databaseId --jq ".[0].databaseId") if [ -z "$run_id" ]; then echo_warning "Could not find the workflow run. Please check GitHub Actions manually." else echo_status "Workflow run ID: ${run_id}" echo_status "Watching workflow run (press Ctrl+C to stop watching)..." gh run watch $run_id # Check the status of the run run_status=$(gh run view $run_id --json conclusion --jq ".conclusion") if [ "$run_status" == "success" ]; then echo_success "Workflow completed successfully!" # Update the release notes echo_status "Updating release notes..." gh release edit $tag_name --title "Version ${new_version}" --notes "$release_notes" # Publish the release (remove the draft status) echo_status "Publishing release..." gh release edit $tag_name --draft=false echo_success "Release v${new_version} has been published!" echo "URL: https://github.com/lovesworking/rn-better-dev-tools/releases/tag/${tag_name}" else echo_error "Workflow failed or was canceled. Please check GitHub Actions." echo "URL: https://github.com/lovesworking/rn-better-dev-tools/actions" fi fi ================================================ FILE: socket-client.js ================================================ /** * Socket.IO Test Client * * This simple client connects to the Socket.IO server and allows * sending messages from the command line. */ // We need to use CommonJS require for this standalone script const { io } = require("socket.io-client"); // Import config values from CommonJS config file const { CLIENT_URL } = require("./config.js"); console.log("Starting Socket.IO test client..."); console.log(`Connecting to: ${CLIENT_URL}`); // Connect to the Socket.IO server const socket = io(CLIENT_URL); // Event handler for successful connection socket.on("connect", () => { console.log("✅ Connected to Socket.IO server"); console.log("Socket ID:", socket.id); // Send a test message after connection setTimeout(() => { sendMessage("Hello from test client!"); }, 1000); }); // Event handler for disconnection socket.on("disconnect", () => { console.log("❌ Disconnected from Socket.IO server"); }); // Event handler for connection errors socket.on("connect_error", (err) => { console.error("❌ Connection error:", err.message); }); // Event handler for receiving messages socket.on("message", (data) => { console.log(`📨 Received: "${data}"`); }); // Add handler for query-action events socket.on("query-action", (data) => { console.log(`📨 Received query-action:`, data); }); // Function to send a message function sendMessage(message) { socket.emit("message", message); console.log(`📤 Sent: "${message}"`); } // Allow sending messages from the command line process.stdin.on("data", (data) => { const message = data.toString().trim(); if (message) { sendMessage(message); } }); console.log("Type messages and press Enter to send. Press Ctrl+C to exit."); ================================================ FILE: src/auto-updater.ts ================================================ import { autoUpdater, UpdateInfo, ProgressInfo } from "electron-updater"; import log from "electron-log"; // Configure logger log.transports.file.level = "info"; autoUpdater.logger = log; export function setupAutoUpdater() { // Check for updates on startup autoUpdater.checkForUpdatesAndNotify(); // Set up auto updater events autoUpdater.on("checking-for-update", () => { log.info("Checking for update..."); }); autoUpdater.on("update-available", (info: UpdateInfo) => { log.info("Update available:", info); }); autoUpdater.on("update-not-available", (info: UpdateInfo) => { log.info("Update not available:", info); }); autoUpdater.on("error", (err: Error) => { log.error("Error in auto-updater:", err); }); autoUpdater.on("download-progress", (progressObj: ProgressInfo) => { let logMessage = `Download speed: ${progressObj.bytesPerSecond}`; logMessage = `${logMessage} - Downloaded ${progressObj.percent}%`; logMessage = `${logMessage} (${progressObj.transferred}/${progressObj.total})`; log.info(logMessage); }); autoUpdater.on("update-downloaded", (info: UpdateInfo) => { log.info("Update downloaded:", info); // Install the update when the app is quit // Alternatively, you could prompt the user here }); // Check for updates periodically const CHECK_INTERVAL = 1000 * 60 * 60; // Check every hour setInterval(() => { autoUpdater.checkForUpdatesAndNotify(); }, CHECK_INTERVAL); } ================================================ FILE: src/components/App.tsx ================================================ import Providers from "./external-dash/providers"; import Main from "./external-dash/Main"; export const App: React.FC = () => { return (
); }; ================================================ FILE: src/components/external-dash/Dash.tsx ================================================ import React, { useEffect, useState } from "react"; import { User } from "./types/User"; import "../../index.css"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools/production"; import { useLogStore } from "./utils/logStore"; import { onDevToolsEvent } from "./utils/devToolsEvents"; import { useDevToolsEventHandler } from "./hooks/useDevToolsEventHandler"; import { DeviceSelection } from "./DeviceSelection"; import { UserInfo } from "./UserInfo"; import { LogConsole } from "./LogConsole"; import { NoDevicesConnected } from "./NoDevicesConnected"; import { StorageControlsSection } from "./UserInfo/StorageControlsSection"; export const PlatformIcon: React.FC<{ platform: string }> = ({ platform }) => { const normalizedPlatform = platform?.toLowerCase() || ""; switch (normalizedPlatform) { case "ios": return ( ); case "android": return ( ); case "web": return ( ); case "tv": case "tvos": return ( ); default: return ( ); } }; export const getPlatformColor = (platform: string): string => { const normalizedPlatform = platform?.toLowerCase() || ""; switch (normalizedPlatform) { case "ios": return "text-gray-100"; case "android": return "text-green-300"; case "web": return "text-blue-300"; case "tv": case "tvos": return "text-purple-300"; default: return "text-gray-300"; } }; export const getPlatformBgColor = (platform: string): string => { const normalizedPlatform = platform?.toLowerCase() || ""; switch (normalizedPlatform) { case "ios": return "bg-blue-900/30 text-blue-200"; case "android": return "bg-green-900/30 text-green-200"; case "web": return "bg-cyan-900/30 text-cyan-200"; case "tv": case "tvos": return "bg-purple-900/30 text-purple-200"; default: return "bg-gray-800/60 text-gray-300"; } }; interface DashProps { allDevices: User[]; isDashboardConnected: boolean; targetDevice: User; setTargetDevice: (device: User) => void; } export const Dash: React.FC = ({ isDashboardConnected, allDevices, targetDevice, setTargetDevice, }) => { const [showOfflineDevices, setShowOfflineDevices] = useState(true); const { isEnabled, setIsEnabled, isVisible, setIsVisible } = useLogStore(); const filteredDevices = showOfflineDevices ? allDevices : allDevices.filter((device) => { if (typeof device === "string") { return false; } return device.isConnected; }); // Find the target device useEffect(() => { const foundDevice = filteredDevices?.find((device) => { return device.deviceId === targetDevice.deviceId; }); foundDevice && setTargetDevice(foundDevice); }, [setTargetDevice, filteredDevices, targetDevice]); useDevToolsEventHandler(); return (
{/* Connection Status */}
{/* Simple dot with no animations */}
{isDashboardConnected ? "Connected" : "Disconnected"}
{/* Right Section */}
{/* Offline Toggle */}
{/* Logs Toggle */} {/* Storage Controls */}
{/* Separator */}
{/* Device Selection */}
{/* Device count and stats */} {filteredDevices.length > 0 ? ( <>
{filteredDevices.length} {filteredDevices.length === 1 ? "device" : "devices"}
{allDevices.filter((d) => d.isConnected).length}{" "} online
{allDevices.filter((d) => !d.isConnected).length}{" "} offline
{targetDevice && (
Target
{targetDevice.deviceId === "All" ? "All Devices" : targetDevice.deviceName}
{targetDevice.deviceId !== "All" && targetDevice.platform && (
)}
)}
{/* Device List */}
{filteredDevices.map((device) => ( ))}
) : ( )}
{/* Console Panel */}
{isVisible ? (
setIsVisible(false)} allDevices={allDevices} />
) : ( )}
); }; ================================================ FILE: src/components/external-dash/DeviceSelection.tsx ================================================ import React, { useEffect, useState, useRef } from "react"; import { User } from "./types/User"; import { PlatformIcon } from "./utils/platformUtils"; interface Props { selectedDevice: User; setSelectedDevice: (device: User) => void; allDevices?: User[]; } interface DeviceOption { value: string; label: string; isOffline?: boolean; platform?: string; } export const DeviceSelection: React.FC = ({ selectedDevice, setSelectedDevice, allDevices = [], }: Props) => { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); // Generate options const generateOptions = (): DeviceOption[] => { if (allDevices.length === 0) { return [ { value: "All", label: "No Devices", }, ]; } if (allDevices.length === 1) { const device = allDevices[0]; return [ { value: device.deviceId, label: device.deviceName, isOffline: !device.isConnected, platform: device.platform, }, ]; } // Multiple devices - add 'All' option return [ { value: "All", label: "All Devices", }, ...allDevices.map((device) => ({ value: device.deviceId, label: device.deviceName, isOffline: !device.isConnected, platform: device.platform, })), ]; }; const deviceOptions = generateOptions(); // If there are no devices, select "All" useEffect(() => { if (!selectedDevice || !selectedDevice.deviceId) { if (deviceOptions.length > 0) { const foundDevice = allDevices.find( (device) => device.deviceId === "All" ); if (foundDevice) { setSelectedDevice(foundDevice); } } } // If the selected device is no longer in the device list, select "All" if ( selectedDevice && selectedDevice.deviceId !== "All" && !allDevices.find((device) => device.deviceId === selectedDevice.deviceId) ) { const foundDevice = allDevices.find( (device) => device.deviceId === "All" ); if (foundDevice) { setSelectedDevice(foundDevice); } } }, [allDevices, selectedDevice, setSelectedDevice, deviceOptions]); // Handle device selection const handleSelect = (deviceId: string) => { if (deviceId === "All") { setSelectedDevice({ id: "all-devices", deviceId: "All", deviceName: "All Devices", isConnected: true, platform: undefined, }); } else { const device = allDevices.find((d) => d.deviceId === deviceId); if (device) { setSelectedDevice(device); } } setIsOpen(false); }; // Get selected device label const getSelectedDeviceLabel = (): string => { if (!selectedDevice || !selectedDevice.deviceId) { return deviceOptions[0]?.label || "All Devices"; } return selectedDevice.deviceName || "Unknown Device"; }; const StatusDot: React.FC<{ isOffline?: boolean }> = ({ isOffline }) => ( ); return (
{isOpen && (
{/* Sticky header for All Devices */} {deviceOptions.length > 1 && deviceOptions[0].value === "All" && (
)} {/* Individual devices */}
{deviceOptions .filter((option) => option.value !== "All") .map((option, index) => ( ))}
)}
); }; ================================================ FILE: src/components/external-dash/LogConsole.tsx ================================================ import React, { useEffect, useRef, useState } from "react"; import { useLogStore, LogEntry, LogLevel } from "./utils/logStore"; import { PlatformIcon } from "./utils/platformUtils"; import { User } from "./types/User"; // Get log level color const getLogLevelColor = (level: LogLevel): string => { switch (level) { case "error": return "text-red-400"; case "warn": return "text-yellow-400"; case "debug": return "text-purple-400"; case "info": default: return "text-blue-400"; } }; const formatTimestamp = (date: Date): string => { return date.toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); }; interface LogEntryItemProps { log: LogEntry; } const LogEntryItem: React.FC = ({ log }) => { return (
{/* Timestamp */} {formatTimestamp(log.timestamp)} {/* Log level */} {log.level} {/* Device info if available */} {log.platform && ( {log.deviceName || "Unknown"} )} {/* Log message */} {log.message}
); }; interface DeviceOption { value: string; label: string; disabled?: boolean; isOffline?: boolean; platform?: string; } interface LogConsoleProps { onClose: () => void; allDevices: User[]; } export const LogConsole: React.FC = ({ onClose, allDevices, }) => { const logs = useLogStore((state: { logs: LogEntry[] }) => state.logs); const clearLogs = useLogStore( (state: { clearLogs: () => void }) => state.clearLogs ); const [filter, setFilter] = useState("all"); const [deviceFilter, setDeviceFilter] = useState("all"); // Auto-scroll functionality const scrollRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); // Resizable functionality const [height, setHeight] = useState(320); // Default height in pixels const resizableRef = useRef(null); const [isDragging, setIsDragging] = useState(false); // Calculate max height based on window height const calculateMaxHeight = () => { // Leave space for the header (approximately 64px) and some padding return window.innerHeight - 80; }; const handleResizeStart = (e: React.MouseEvent) => { e.preventDefault(); // Prevent text selection if (e.button !== 0) return; // Only handle left mouse button setIsDragging(true); const startY = e.clientY; const startHeight = height; const handleResizeMove = (e: MouseEvent) => { const deltaY = startY - e.clientY; const maxHeight = calculateMaxHeight(); const newHeight = Math.max( 200, Math.min(maxHeight, startHeight + deltaY) ); setHeight(newHeight); }; const handleResizeEnd = () => { setIsDragging(false); document.removeEventListener("mousemove", handleResizeMove); document.removeEventListener("mouseup", handleResizeEnd); document.body.style.cursor = "default"; }; document.addEventListener("mousemove", handleResizeMove); document.addEventListener("mouseup", handleResizeEnd); document.body.style.cursor = "ns-resize"; }; // Update max height on window resize useEffect(() => { const handleResize = () => { const maxHeight = calculateMaxHeight(); if (height > maxHeight) { setHeight(maxHeight); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [height]); useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = 0; } }, [logs, autoScroll]); // Reset device filter if selected device is no longer available useEffect(() => { if (deviceFilter !== "all") { const deviceExists = allDevices.some( (device) => device.deviceId === deviceFilter ); if (!deviceExists) { setDeviceFilter("all"); } } }, [allDevices, deviceFilter]); // Filter logs based on level and device const filteredLogs = logs.filter((log: LogEntry) => { const matchesLevel = filter === "all" || log.level === filter; const matchesDevice = deviceFilter === "all" || log.deviceId === deviceFilter; return matchesLevel && matchesDevice; }); // Generate device options based on available devices const deviceOptions: DeviceOption[] = (() => { if (allDevices?.length === 0) { return [{ value: "all", label: "All Devices" }]; } else if (allDevices?.length === 1) { // Only one device, no need for "All" option const device = allDevices[0]; return [ { value: device.deviceId, label: device.deviceName || "Unknown Device", isOffline: !device.isConnected, platform: device.platform, }, ]; } else { // Multiple devices, include "All" option return [ { value: "all", label: "All Devices" }, ...allDevices.map((device) => ({ value: device.deviceId, label: device.deviceName || "Unknown Device", isOffline: !device.isConnected, platform: device.platform, })), ]; } })(); const [isDeviceDropdownOpen, setIsDeviceDropdownOpen] = useState(false); const [isLevelDropdownOpen, setIsLevelDropdownOpen] = useState(false); const deviceDropdownRef = useRef(null); const levelDropdownRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( deviceDropdownRef.current && !deviceDropdownRef.current.contains(event.target as Node) ) { setIsDeviceDropdownOpen(false); } if ( levelDropdownRef.current && !levelDropdownRef.current.contains(event.target as Node) ) { setIsLevelDropdownOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const levelOptions = [ { value: "all", label: "All" }, { value: "info", label: "Info" }, { value: "warn", label: "Warn" }, { value: "error", label: "Error" }, { value: "debug", label: "Debug" }, ]; return ( <> {/* Resize handle */}
{/* Header */}
Console ({filteredLogs.length} logs)
{/* Device Filter */}
Device:
{isDeviceDropdownOpen && (
{deviceOptions.map((option) => ( ))}
)}
{/* Log Level Filter */}
Level:
{isLevelDropdownOpen && (
{levelOptions.map((option) => ( ))}
)}
{/* Auto-scroll toggle */}
Auto-scroll:
{/* Clear button */} {/* Close button */}
{/* Log entries container with dynamic height */}
{ if ((e.ctrlKey || e.metaKey) && e.key === "a") { e.preventDefault(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(e.currentTarget); selection?.removeAllRanges(); selection?.addRange(range); } }} tabIndex={0} > {filteredLogs.length > 0 ? ( filteredLogs.map((log: LogEntry) => ( )) ) : (
No logs to display
)}
); }; ================================================ FILE: src/components/external-dash/Main.tsx ================================================ import { useState } from "react"; import useConnectedUsers from "./_hooks/useConnectedUsers"; import { useSyncQueriesWeb } from "./useSyncQueriesWeb"; import { Dash } from "./Dash"; import { User } from "./types/User"; export default function Main() { const [targetDevice, setTargetDevice] = useState({ deviceId: "Please select a device", deviceName: "Please select a device", isConnected: false, id: "Please select a device", }); const { allDevices, isDashboardConnected } = useConnectedUsers(); useSyncQueriesWeb({ targetDevice, allDevices }); return ( ); } ================================================ FILE: src/components/external-dash/NoDevicesConnected.tsx ================================================ import React, { useState } from "react"; export const NoDevicesConnected = () => { const [copiedText, setCopiedText] = useState(null); const handleCopy = (text: string, label: string) => { navigator.clipboard.writeText(text); setCopiedText(label); setTimeout(() => setCopiedText(null), 2000); }; return ( <>

No Devices Connected

Please ensure this app is running and then start or restart your devices to establish a connection.

Troubleshooting steps:

  1. Verify the app is running
  2. Restart your development devices
  3. Please read the or for help
); }; ================================================ FILE: src/components/external-dash/UserInfo/DeviceSpecificationsSection.tsx ================================================ import React, { useState } from "react"; import { InfoRow } from "./InfoRow"; interface Props { extraDeviceInfo: Record; } const formatValue = (value: any, key?: string): React.ReactElement => { // Special handling for NODE_ENV if (key === "NODE_ENV" && typeof value === "string") { const env = value.toLowerCase(); let envStyles = ""; switch (env) { case "development": case "dev": envStyles = "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"; break; case "production": case "prod": envStyles = "bg-blue-900/80 text-blue-300 shadow-[0_0_8px_rgba(59,130,246,0.1)]"; break; case "staging": case "stage": envStyles = "bg-yellow-900/80 text-yellow-300 shadow-[0_0_8px_rgba(250,204,21,0.1)]"; break; case "test": case "testing": envStyles = "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"; break; default: envStyles = "bg-purple-900/80 text-purple-300 shadow-[0_0_8px_rgba(147,51,234,0.1)]"; } return ( {value} ); } // Check if it's a string boolean first if ( typeof value === "string" && (value.toLowerCase() === "true" || value.toLowerCase() === "false") ) { const boolValue = value.toLowerCase() === "true"; return ( {boolValue ? "true" : "false"} ); } if (typeof value === "boolean") { return ( {value ? "true" : "false"} ); } if (typeof value === "string") { return ( {value} ); } if (typeof value === "number") { return ( {value} ); } if (value === null) { return ( null ); } if (value === undefined) { return ( undefined ); } // For objects, arrays, etc. return ( {JSON.stringify(value)} ); }; export const DeviceSpecificationsSection: React.FC = ({ extraDeviceInfo, }) => { const hasDeviceInfo = Object.keys(extraDeviceInfo).length > 0; const [isExpanded, setIsExpanded] = useState(false); return ( <>
setIsExpanded(!isExpanded)} > Custom Properties
{isExpanded && ( <> {hasDeviceInfo ? ( <> {Object.entries(extraDeviceInfo).map(([key, value]) => (
{key}
{formatValue(value, key)}
))} ) : (
No custom properties available. Pass additional info via the{" "} extraDeviceInfo {" "} prop:
extraDeviceInfo: {"{"} "Environment": "staging", "Version": "1.2.3", "Feature_Flag": true, ... {"}"}
)} )} ); }; ================================================ FILE: src/components/external-dash/UserInfo/EnvironmentVariablesSection.tsx ================================================ import React, { useState } from "react"; import { InfoRow } from "./InfoRow"; interface Props { envVariables: Record; } const formatValue = (value: any, key?: string): React.ReactElement => { // Special handling for NODE_ENV if (key === "NODE_ENV" && typeof value === "string") { const env = value.toLowerCase(); let envStyles = ""; switch (env) { case "development": case "dev": envStyles = "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"; break; case "production": case "prod": envStyles = "bg-blue-900/80 text-blue-300 shadow-[0_0_8px_rgba(59,130,246,0.1)]"; break; case "staging": case "stage": envStyles = "bg-yellow-900/80 text-yellow-300 shadow-[0_0_8px_rgba(250,204,21,0.1)]"; break; case "test": case "testing": envStyles = "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"; break; default: envStyles = "bg-purple-900/80 text-purple-300 shadow-[0_0_8px_rgba(147,51,234,0.1)]"; } return ( {value} ); } // Check if it's a string boolean first if ( typeof value === "string" && (value.toLowerCase() === "true" || value.toLowerCase() === "false") ) { const boolValue = value.toLowerCase() === "true"; return ( {boolValue ? "true" : "false"} ); } if (typeof value === "boolean") { return ( {value ? "true" : "false"} ); } if (typeof value === "string") { return ( {value} ); } if (typeof value === "number") { return ( {value} ); } if (value === null) { return ( null ); } if (value === undefined) { return ( undefined ); } // For objects, arrays, etc. return ( {JSON.stringify(value)} ); }; export const EnvironmentVariablesSection: React.FC = ({ envVariables, }) => { const hasEnvVariables = Object.keys(envVariables).length > 0; const [isExpanded, setIsExpanded] = useState(false); return ( <>
setIsExpanded(!isExpanded)} > Environment Variables
{isExpanded && ( <> {hasEnvVariables ? ( <> {Object.entries(envVariables).map(([key, value]) => (
{key}
{formatValue(value, key)}
))} ) : (
No environment variables available. Pass ENV variables via the{" "} envVariables {" "} prop:
envVariables: {"{"} "NODE_ENV": "development", "API_URL": "https://api.example.com", ... {"}"}
)} )} ); }; ================================================ FILE: src/components/external-dash/UserInfo/InfoRow.tsx ================================================ import React from "react"; import { PlatformIcon } from "../utils/platformUtils"; interface Props { label: string; value: string; monospace?: boolean; className?: string; labelClassName?: string; } export const InfoRow: React.FC = ({ label, value, monospace, className = "text-[#F5F5F7]", labelClassName = "text-[#A1A1A6]", }) => ( <>
{label}:
{label === "Platform" && } {value}
); ================================================ FILE: src/components/external-dash/UserInfo/StorageControlsSection.tsx ================================================ import React, { useCallback, useMemo, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useStorageStore } from "../utils/storageStore"; import { StorageType, isStorageQuery, getStorageType, } from "../utils/storageQueryKeys"; interface StorageControlsSectionProps { deviceId?: string; // Made optional for global usage } export const StorageControlsSection: React.FC = React.memo(({ deviceId }) => { const queryClient = useQueryClient(); const [isExpanded, setIsExpanded] = useState(false); // Get the entire store state to ensure reactivity const { enabledStorageTypes, setStorageTypeEnabled } = useStorageStore(); const handleStorageToggle = useCallback( (storageType: StorageType) => { const isCurrentlyEnabled = enabledStorageTypes.has(storageType); const newEnabledState = !isCurrentlyEnabled; setStorageTypeEnabled(storageType, newEnabledState); if (!newEnabledState) { // Use requestAnimationFrame to defer heavy query operations // This prevents blocking the UI during transitions requestAnimationFrame(() => { const queryCache = queryClient.getQueryCache(); const allQueries = queryCache.getAll(); allQueries.forEach((query) => { if ( isStorageQuery(query.queryKey) && getStorageType(query.queryKey) === storageType ) { queryClient.removeQueries({ queryKey: query.queryKey }); } }); }); } }, [enabledStorageTypes, setStorageTypeEnabled, queryClient] ); const storageTypes = useMemo( () => [ { type: "mmkv" as StorageType, label: "MMKV", description: "High-performance key-value storage", icon: "⚡", }, { type: "async" as StorageType, label: "AsyncStorage", description: "React Native async storage", icon: "📱", }, { type: "secure" as StorageType, label: "SecureStorage", description: "Encrypted secure storage", icon: "🔒", }, ], [] ); const enabledCount = enabledStorageTypes.size; const totalCount = storageTypes.length; // If deviceId is provided, render the old card-style layout for user info if (deviceId) { return ( <>
setIsExpanded(!isExpanded)} > Storage Query Controls {enabledCount}/{totalCount} enabled
{isExpanded && (
{storageTypes.map(({ type, label, description, icon }) => { const isEnabled = enabledStorageTypes.has(type); return ( ); })}
Disabling a storage type will clear its queries from React Query DevTools and filter out new queries. Re-enabling will show new queries but won't restore previously cleared ones.
)} ); } // Global header layout - compact dropdown style return (
{isExpanded && (
{storageTypes.map(({ type, label, description, icon }) => { const isEnabled = enabledStorageTypes.has(type); return ( ); })}
Global setting that affects storage queries in React Query DevTools for all devices.
)}
); }); ================================================ FILE: src/components/external-dash/UserInfo/TargetGlowEffect.tsx ================================================ import React from "react"; interface Props { isTargeted: boolean; } export const TargetGlowEffect: React.FC = ({ isTargeted }) => { if (!isTargeted) return null; return ( <> {/* Extended glow effect - furthest back */}