Skip to content

React Native DevTools not working on iOS physical device - Shows "Connect to Metro to debug JavaScript" Description #1626

@NikhilMasurkar

Description

@NikhilMasurkar

React Native DevTools not working on iOS physical device - Shows "Connect to Metro to debug JavaScript"

Description

I'm unable to open the React Native DevTools on iOS physical devices in React Native 0.82.1. When I start Metro and press j in the terminal, the DevTools don't open. Similarly, when I open the Dev Menu it only shows the message "Connect to Metro to debug JavaScript" insted of open devtool. The app connects to Metro bundler successfully and hot reload works fine, but the debugger won't open at all.

Important notes:

  • ✅ Works fine on Android physical devices
  • ✅ Metro bundler is running and app connects successfully (reload works)
  • ❌ DevTools not working on iOS physical device
  • 🔄 Recently migrated AppDelegate from Objective-C (.mm) to Swift

Environment

React Native Info

React Native: 0.82.1
Node: v24.2.0
iOS: 15.5+
Platform: iOS (Physical Device)
Hermes: Enabled
New Architecture: Enabled

Device

  • Physical iPhone device (not simulator)
  • Connected via USB and same WiFi network as development machine

Steps to Reproduce

  1. Create/use React Native 0.82.1 project with Hermes and New Architecture enabled
  2. Migrate AppDelegate from Objective-C to Swift (files shown below)
  3. Build and run on physical iOS device
  4. Shake device to open Dev Menu
  5. Tap "Open Debugger"
  6. Expected: Chrome DevTools opens with Hermes debugger
  7. Actual: Shows message "Connect to Metro to debug JavaScript" but debugger doesn't open

Relevant Code

AppDelegate.swift (Converted from .mm)

import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import Firebase
import RNBootSplash
import FBSDKCoreKit
import AppTrackingTransparency
import GoogleSignIn

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  var window: UIWindow?
  var reactNativeDelegate: ReactNativeDelegate?
  var reactNativeFactory: RCTReactNativeFactory?
  
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    
    // Initialize Firebase
    if FirebaseApp.app() == nil {
      var googleServiceInfofilePath = ""
      #if DEV
        googleServiceInfofilePath = Bundle.main.path(forResource: "GoogleServices-Info-Dev", ofType: "plist") ?? ""
      #else
        googleServiceInfofilePath = Bundle.main.path(forResource: "GoogleServices-Info", ofType: "plist") ?? ""
      #endif
      FirebaseApp.configure()
    }
    
    // Initialize Facebook SDK
    ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
    
    // Request App Tracking Transparency (iOS 14+)
    if #available(iOS 14, *) {
      ATTrackingManager.requestTrackingAuthorization { _ in
        AppEvents.shared.activateApp()
      }
    } else {
      AppEvents.shared.activateApp()
    }
    
    // Setup React Native
    let delegate = ReactNativeDelegate()
    let factory = RCTReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()
    reactNativeDelegate = delegate
    reactNativeFactory = factory
    
    window = UIWindow(frame: UIScreen.main.bounds)
    
    factory.startReactNative(
      withModuleName: "Fin1",
      in: window,
      launchOptions: launchOptions
    )
    
    return true
  }
  
  func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return Orientation.getOrientation()
  }
  
  func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // Add any other URL handlers you're using (e.g. Facebook SDK)
    return ApplicationDelegate.shared.application(app, open: url, options: options) ||
           GIDSignIn.sharedInstance.handle(url) ||
           RCTLinkingManager.application(app, open: url, options: options)
  }
  
  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
  }
}

class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
  
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    self.bundleURL()
  }

  override func customize(_ rootView: RCTRootView) {
    super.customize(rootView)
    RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
  }

  override func bundleURL() -> URL? {
    #if DEBUG
      return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
    #else
      return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
    #endif
    } 
}

Previous AppDelegate.mm (Original - Before Swift Conversion)

#import "AppDelegate.h"
 
 #import <React/RCTBundleURLProvider.h>
 #import <Firebase.h>
 #import "RNSplashScreen.h"
 #import <FBSDKCoreKit/FBSDKCoreKit-swift.h>
 #import "Orientation.h"
 #import <React/RCTLinkingManager.h>
 #import <ReactAppDependencyProvider/RCTAppDependencyProvider.h> 

 
 @implementation AppDelegate
 
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
   self.moduleName = @"Fin1";
   self.dependencyProvider = [RCTAppDependencyProvider new];
   if ([FIRApp defaultApp] == nil) {
     NSString *googleServiceInfofilePath = @"";
     #if DEV
       googleServiceInfofilePath = [[NSBundle mainBundle] pathForResource:@"GoogleServices-Info-Dev" ofType:@"plist"];
     #else
       googleServiceInfofilePath = [[NSBundle mainBundle] pathForResource:@"GoogleServices-Info" ofType:@"plist"];
     #endif
     [FIRApp configure];
   }
 
   [FBSDKApplicationDelegate.sharedInstance initializeSDK];
   [[FBSDKApplicationDelegate sharedInstance] application:application
                        didFinishLaunchingWithOptions:launchOptions];
   
   // You can add your custom initial props in the dictionary below.
   // They will be passed down to the ViewController used by React Native.
   self.initialProps = @{};
   return [super application:application didFinishLaunchingWithOptions:launchOptions];
 }
 
 - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
 {
   return [self bundleURL];
 }
  
 - (NSURL *)bundleURL
 
 {
 #if DEBUG
   return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
 #else
   return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
 #endif
 }
 
 - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
   return [Orientation getOrientation];
 }
 
 - (BOOL)application:(UIApplication *)application
    openURL:(NSURL *)url
    options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
 {
   return [RCTLinkingManager application:application openURL:url options:options];
 }
 
 - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
  restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
 {
  return [RCTLinkingManager application:application
                   continueUserActivity:userActivity
                     restorationHandler:restorationHandler];
 }
 // - (BOOL)application:(UIApplication *)application 
 //             openURL:(NSURL *)url 
 //             options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
 //   // Add any custom logic here.
 //   return [[FBSDKApplicationDelegate sharedInstance]application:application
 //                                                       openURL:url
 //                                                       options:options];
 // }
 
	 @end

metro.config.js

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const {
  wrapWithReanimatedMetroConfig,
} = require('react-native-reanimated/metro-config');

/**
 * Metro configuration
 * https://reactnative.dev/docs/metro
 *
 * @type {import('@react-native/metro-config').MetroConfig}
 */
const config = {};

const mergedConfig = mergeConfig(getDefaultConfig(__dirname), config);

module.exports = wrapWithReanimatedMetroConfig(mergedConfig);

Podfile

require Pod::Executable.execute_command('node', ['-p',
  'require.resolve(
    "react-native/scripts/react_native_pods.rb",
    {paths: [process.argv[1]]},
  )', __dir__]).strip

platform :ios, 15.5
prepare_react_native_project!

linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
  Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
  use_frameworks! :linkage => linkage.to_sym
end

project 'Fin1',
        'Dev.Debug' => :debug,
        'Dev.Release' => :release

target 'Fin1' do
  config = use_native_modules!
 
  use_frameworks! :linkage => :static
  $RNFirebaseAsStaticFramework = true
  use_react_native!(
    :path => config[:reactNativePath],

    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/..",
    :fabric_enabled => true,
    :hermes_enabled => true,
    :new_arch_enabled => true 
  )
  
  pod 'XCDYouTubeKit'
  pod 'FBSDKCoreKit'

  post_install do |installer|
    react_native_post_install(
      installer,
      config[:reactNativePath],
       :mac_catalyst_enabled => false,
      # :ccache_enabled => true
    )
    
    bitcode_strip_path = `xcrun --find bitcode_strip`.chop!

    def strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path)
      framework_path = File.join(Dir.pwd, framework_relative_path)
      command = "#{bitcode_strip_path} #{framework_path} -r -o #{framework_path}"
      puts "Stripping bitcode: #{command}"
      system(command)
    end

    framework_paths = [
      "Pods/VdoFramework/VdoFramework.xcframework/ios-arm64/VdoFramework.framework/VdoFramework"
    ]

    framework_paths.each do |framework_relative_path|
      strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path)
  end
end
end

What I've Tried

  1. ✅ Verified Hermes is enabled in Podfile
  2. ✅ Cleaned build folder and reinstalled pods multiple times
  3. ✅ Verified Metro bundler is running and accessible
  4. ✅ Checked chrome://inspect - no "Hermes React Native" target appears
  5. ✅ Tried pressing j in Metro terminal - no debugger opens
  6. ✅ Verified app is built in Debug mode (Dev.Debug configuration)
  7. ✅ Both Mac and iPhone are on same WiFi network
  8. ✅ Manually configured bundler IP address via "Configure Bundler" in Dev Menu
  9. ❌ Still can't open debugger

Additional Context

  • This issue started after upgrading from an earlier React Native version where debugging worked fine
  • Android physical device debugging works perfectly with the same setup
  • The Swift conversion was done carefully to match the Objective-C implementation

Questions

  1. Is there a known issue with React Native DevTools on iOS physical devices in 0.82.1?
  2. Could the Swift migration be causing debugger initialization issues?
  3. Are there additional configurations needed for physical device debugging in 0.82+?

Any help would be greatly appreciated! This is blocking our development workflow on iOS devices.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions