package app.pulse.updates import android.content.Context import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.ReactPackageTurboModuleManagerDelegate import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.defaults.DefaultComponentsRegistry import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate import com.facebook.react.fabric.ComponentFactory import com.facebook.react.runtime.BindingsInstaller import com.facebook.react.runtime.JSRuntimeFactory import com.facebook.react.runtime.ReactHostDelegate import com.facebook.react.runtime.ReactHostImpl import com.facebook.react.runtime.hermes.HermesInstance import java.lang.ref.WeakReference /** * Custom ReactHostDelegate that supports dynamic bundle reloading. * * Unlike DefaultReactHostDelegate which caches the bundle loader at construction time, * this delegate re-queries PulseController every time jsBundleLoader is accessed. * This allows reload() to pick up the new bundle path after an OTA update. */ @OptIn(UnstableReactNativeAPI::class) private class PulseReactHostDelegate( private val weakContext: WeakReference, override val jsMainModulePath: String, override val reactPackages: List, override val jsRuntimeFactory: JSRuntimeFactory = HermesInstance(), override val bindingsInstaller: BindingsInstaller? = null, override val turboModuleManagerDelegateBuilder: ReactPackageTurboModuleManagerDelegate.Builder = DefaultTurboModuleManagerDelegate.Builder(), private val exceptionHandler: (Exception) -> Unit = { throw it } ) : ReactHostDelegate { /** * Override jsBundleLoader to dynamically query PulseController. * This is called every time React Native needs the bundle, including on reload. */ override val jsBundleLoader: JSBundleLoader get() { val context = weakContext.get() ?: throw IllegalStateException("Context is no longer available") val bundleFile = PulseUpdatesModule.getBundleFile(context) return if (bundleFile != null) { pulseLog("PulseReactHostDelegate", "jsBundleLoader: ${bundleFile.absolutePath}") JSBundleLoader.createFileLoader(bundleFile.absolutePath) } else { pulseLog("PulseReactHostDelegate", "jsBundleLoader: using default asset bundle") JSBundleLoader.createAssetLoader(context, "assets://index.android.bundle", true) } } override fun handleInstanceException(error: Exception) = exceptionHandler(error) } /** * Factory for creating a ReactHost that supports dynamic bundle reloading. * * Usage in MainApplication: * ```kotlin * override val reactHost: ReactHost * get() = PulseReactHostFactory.createReactHost( * applicationContext, * packages = reactNativeHost.packages, // or getPackages() * jsMainModuleName = "index", * useDevSupport = BuildConfig.DEBUG * ) * ``` */ object PulseReactHostFactory { @Volatile private var reactHost: ReactHost? = null @OptIn(UnstableReactNativeAPI::class) @JvmStatic fun createReactHost( context: Context, packages: List, jsMainModuleName: String = "index", useDevSupport: Boolean = false ): ReactHost { if (reactHost == null) { synchronized(this) { if (reactHost == null) { val weakContext = WeakReference(context.applicationContext) pulseLog("PulseReactHostFactory", "Creating ReactHost with ${packages.size} packages, jsMainModuleName=$jsMainModuleName") val delegate = PulseReactHostDelegate( weakContext = weakContext, jsMainModulePath = jsMainModuleName, reactPackages = packages ) val componentFactory = ComponentFactory() DefaultComponentsRegistry.register(componentFactory) reactHost = ReactHostImpl( context.applicationContext, delegate, componentFactory, true, // allowPackagerServerAccess useDevSupport ) } } } return reactHost!! } }