diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index ffcf76ad163d09..b5b2a7e7d0f1af 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -16,7 +16,7 @@ import com.facebook.react.tasks.GenerateCodegenSchemaTask import com.facebook.react.tasks.GeneratePackageListTask import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForApp import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForLibraries -import com.facebook.react.utils.AgpConfiguratorUtils.configureDevPorts +import com.facebook.react.utils.AgpConfiguratorUtils.configureDevServerLocation import com.facebook.react.utils.AgpConfiguratorUtils.configureNamespaceForLibraries import com.facebook.react.utils.BackwardCompatUtils.configureBackwardCompatibilityReactMap import com.facebook.react.utils.DependencyUtils.configureDependencies @@ -70,7 +70,7 @@ class ReactPlugin : Plugin { configureReactNativeNdk(project, extension) configureBuildConfigFieldsForApp(project, extension) - configureDevPorts(project) + configureDevServerLocation(project) configureBackwardCompatibilityReactMap(project) configureJavaToolChains(project) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt index 39344aac7716cb..b700a19244a1fd 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt @@ -13,6 +13,8 @@ import com.facebook.react.ReactExtension import com.facebook.react.utils.ProjectUtils.isHermesEnabled import com.facebook.react.utils.ProjectUtils.isNewArchEnabled import java.io.File +import java.net.NetworkInterface +import java.net.Inet4Address import javax.xml.parsers.DocumentBuilder import javax.xml.parsers.DocumentBuilderFactory import org.gradle.api.Action @@ -50,13 +52,14 @@ internal object AgpConfiguratorUtils { } } - fun configureDevPorts(project: Project) { + fun configureDevServerLocation(project: Project) { val devServerPort = project.properties["reactNativeDevServerPort"]?.toString() ?: DEFAULT_DEV_SERVER_PORT val action = Action { project.extensions.getByType(AndroidComponentsExtension::class.java).finalizeDsl { ext -> + ext.defaultConfig.resValue("string", "react_native_dev_server_ip", getHostIpAddress()) ext.defaultConfig.resValue("integer", "react_native_dev_server_port", devServerPort) } } @@ -104,3 +107,11 @@ fun getPackageNameFromManifest(manifest: File): String? { return null } } + +internal fun getHostIpAddress(): String = + NetworkInterface.getNetworkInterfaces().asSequence() + .filter { it.isUp && !it.isLoopback } + .flatMap { it.inetAddresses.asSequence() } + .filter { it is Inet4Address && !it.isLoopbackAddress } + .map { it.hostAddress } + .firstOrNull() ?: "localhost" \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 7658a23fce3b62..531637e937d04e 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -522,6 +522,7 @@ android { buildConfigField("boolean", "UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE", "false") resValue("integer", "react_native_dev_server_port", reactNativeDevServerPort()) + resValue("string", "react_native_dev_server_ip", "localhost") testApplicationId = "com.facebook.react.tests.gradle" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java index 072c46ef18affe..4503fd3b035cb8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java @@ -20,11 +20,13 @@ import android.graphics.Typeface; import android.hardware.SensorManager; import android.os.Build; +import android.text.InputType; import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListAdapter; @@ -62,6 +64,8 @@ import com.facebook.react.devsupport.interfaces.StackFrame; import com.facebook.react.modules.core.RCTNativeAppEventEmitter; import com.facebook.react.modules.debug.interfaces.DeveloperSettings; +import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; +import com.facebook.react.packagerconnection.PackagerConnectionSettings; import com.facebook.react.packagerconnection.RequestHandler; import java.io.File; import java.net.MalformedURLException; @@ -376,24 +380,91 @@ public void onOptionSelected() { return; } - final EditText input = new EditText(context); - input.setHint("localhost:8081"); + PackagerConnectionSettings settings = mDevSettings.getPackagerConnectionSettings(); + final String currentHost = settings.getDebugServerHost(); + settings.setDebugServerHost(""); + final String defaultHost = settings.getDebugServerHost(); + settings.setDebugServerHost(currentHost); + + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + int paddingSmall = (int) (4 * context.getResources().getDisplayMetrics().density); + int paddingLarge = (int) (16 * context.getResources().getDisplayMetrics().density); + layout.setPadding(paddingLarge, paddingLarge, paddingLarge, paddingLarge); - AlertDialog bundleLocationDialog = + final TextView label = new TextView(context); + label.setText(mApplicationContext.getString(R.string.catalyst_change_bundle_location_input_label)); + label.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + final EditText input = new EditText(context); + // This makes it impossible to enter a newline in the input field + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint(mApplicationContext.getString(R.string.catalyst_change_bundle_location_input_hint)); + input.setBackgroundResource(android.R.drawable.edit_text); + input.setHintTextColor(0xFFCCCCCC); + input.setTextColor(0xFF000000); + input.setText(currentHost); + + final Button defaultHostSuggestion = new Button(context); + defaultHostSuggestion.setText(defaultHost); + defaultHostSuggestion.setTextSize(12); + defaultHostSuggestion.setAllCaps(false); + defaultHostSuggestion.setOnClickListener(v -> input.setText(defaultHost)); + + final Button localhostSuggestion = new Button(context); + localhostSuggestion.setText("localhost:8081"); + localhostSuggestion.setTextSize(12); + localhostSuggestion.setAllCaps(false); + localhostSuggestion.setOnClickListener(v -> input.setText("localhost:8081")); + + LinearLayout suggestionRow = new LinearLayout(context); + suggestionRow.setOrientation(LinearLayout.HORIZONTAL); + suggestionRow.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + suggestionRow.addView(defaultHostSuggestion); + suggestionRow.addView(localhostSuggestion); + + final TextView instructions = new TextView(context); + instructions.setText(mApplicationContext.getString(R.string.catalyst_change_bundle_location_instructions, AndroidInfoHelpers.getAdbReverseTcpCommand(mApplicationContext))); + final LinearLayout.LayoutParams instructionsParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + instructionsParams.setMargins(0, paddingSmall, 0, paddingLarge); + instructions.setLayoutParams(instructionsParams); + + final Button applyChangesButton = new Button(context); + applyChangesButton.setText(mApplicationContext.getString(R.string.catalyst_change_bundle_location_apply)); + + final Button cancelButton = new Button(context); + cancelButton.setText(mApplicationContext.getString(R.string.catalyst_change_bundle_location_cancel)); + + layout.addView(label); + layout.addView(input); + layout.addView(suggestionRow); + layout.addView(instructions); + layout.addView(applyChangesButton); + layout.addView(cancelButton); + + final AlertDialog bundleLocationDialog = new AlertDialog.Builder(context) .setTitle(mApplicationContext.getString(R.string.catalyst_change_bundle_location)) - .setView(input) - .setPositiveButton( - android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String host = input.getText().toString(); - mDevSettings.getPackagerConnectionSettings().setDebugServerHost(host); - handleReloadJS(); - } - }) + .setView(layout) .create(); + + applyChangesButton.setOnClickListener(v -> { + String host = input.getText().toString(); + mDevSettings.getPackagerConnectionSettings().setDebugServerHost(host); + handleReloadJS(); + bundleLocationDialog.dismiss(); + }); + cancelButton.setOnClickListener(v -> { + bundleLocationDialog.dismiss(); + }); + bundleLocationDialog.show(); }); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.kt index b85015aa724b06..dbc270d703c5ea 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoHelpers.kt @@ -30,10 +30,12 @@ public object AndroidInfoHelpers { private fun isRunningOnStockEmulator(): Boolean = Build.FINGERPRINT.contains("generic") || Build.FINGERPRINT.startsWith("google/sdk_gphone") - @JvmStatic public fun getServerHost(port: Int): String = getServerIpAddress(port) + @JvmStatic public fun getServerHost(port: Int): String = getServerIpAddress(null, port) @JvmStatic - public fun getServerHost(context: Context): String = getServerIpAddress(getDevServerPort(context)) + public fun getServerHost(context: Context): String = getServerIpAddress(context, getDevServerPort(context)) + + @JvmStatic public fun getServerHost(context: Context, port: Int): String = getServerIpAddress(context, port) @JvmStatic public fun getAdbReverseTcpCommand(port: Int): String = "adb reverse tcp:$port tcp:$port" @@ -86,13 +88,14 @@ public object AndroidInfoHelpers { private fun getDevServerPort(context: Context): Int = context.resources.getInteger(R.integer.react_native_dev_server_port) - private fun getServerIpAddress(port: Int): String { + private fun getServerIpAddress(context: Context?, port: Int): String { val ipAddress: String = when { getMetroHostPropValue().isNotEmpty() -> getMetroHostPropValue() isRunningOnGenymotion() -> GENYMOTION_LOCALHOST isRunningOnStockEmulator() -> EMULATOR_LOCALHOST - else -> DEVICE_LOCALHOST + context == null -> DEVICE_LOCALHOST + else -> context.resources.getString(R.string.react_native_dev_server_ip) } return String.format(Locale.US, "%s:%d", ipAddress, port) } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JSLoader.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JSLoader.cpp index 3ba56c93629b32..b7cc0cf30b47a9 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JSLoader.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JSLoader.cpp @@ -87,9 +87,13 @@ loadScriptFromAssets(AAssetManager* manager, const std::string& assetName) { } throw std::runtime_error( - "Unable to load script. Make sure you're " - "either running Metro (run 'npx react-native start') or that your bundle '" + - assetName + "' is packaged correctly for release."); + "Unable to load script.\n\n" + "Make sure you're running Metro or that your " + "bundle '" + assetName + "' is packaged correctly for release.\n\n" + "The device must be on the same Wi-Fi network as your computer to connect to Metro.\n\n" + "To use USB instead, shake the device to open the Dev Menu and set " + "the bundler location to \"localhost:8081\" and run:\n" + " adb reverse tcp:8081 tcp:8081"); } } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/res/devsupport/values/strings.xml b/packages/react-native/ReactAndroid/src/main/res/devsupport/values/strings.xml index 8eca4e032d1eb8..1749492fa76632 100644 --- a/packages/react-native/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/packages/react-native/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -3,6 +3,11 @@ Reload Failed to load bundle. Try restarting the bundler or reconnecting your device. Change Bundle Location + Provide a custom bundler address and port: + 127.0.0.1:8081 + If you experience slow reloads or unstable network connection, you can route data via USB cable instead of WiFi:\n 1. Connect your device via USB\n 2. Set the bundle location to `localhost:8081`\n 3. Run this command in your terminal:\n      `%1$s` + Apply Changes + Cancel Failed to open DevTools. Please check that the dev server is running and reload the app. Open DevTools Connect to the bundler to debug JavaScript