@hot-updater/react-native 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  143. package/android/src/newarch/ReactIntegrationManager.kt +0 -185
  144. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,77 @@
1
+ import type {
2
+ BundleDownloadedInfo,
3
+ UpdateCheckResult,
4
+ UpdateError,
5
+ UpdateHttpResponse,
6
+ } from "@hot-updater/protocol";
7
+
8
+ import type { LaunchReporter } from "./appReady";
9
+ import { takeNativeUpdateEvents } from "./native";
10
+
11
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
12
+ value !== null && typeof value === "object" && !Array.isArray(value);
13
+
14
+ /**
15
+ * Reports the update checks native code ran without JavaScript, such as a
16
+ * brownfield app's native `HotUpdaterInstance`, with the catalog and artifact
17
+ * responses they received, to the instance's plugins before this launch's
18
+ * report, which they precede. Native takes and clears them in one step, so
19
+ * each is reported at most once. A native error carries a message instead of
20
+ * the caught error, and each event keeps the time it happened as
21
+ * `occurredAt`.
22
+ */
23
+ export const replayNativeUpdateEvents = (emit: LaunchReporter["emit"]) => {
24
+ let value: string | null;
25
+ try {
26
+ value = takeNativeUpdateEvents();
27
+ } catch {
28
+ return;
29
+ }
30
+ if (value === null) return;
31
+
32
+ let entries: unknown;
33
+ try {
34
+ entries = JSON.parse(value);
35
+ } catch {
36
+ return;
37
+ }
38
+ if (!Array.isArray(entries)) return;
39
+
40
+ for (const entry of entries) {
41
+ if (!isRecord(entry) || !isRecord(entry.payload)) continue;
42
+ const payload =
43
+ typeof entry.occurredAt === "number"
44
+ ? { ...entry.payload, occurredAt: entry.occurredAt }
45
+ : entry.payload;
46
+ switch (entry.name) {
47
+ case "onUpdateCheck":
48
+ emit("onUpdateCheck", () => payload as unknown as UpdateCheckResult);
49
+ break;
50
+ case "onBundleDownloaded":
51
+ emit(
52
+ "onBundleDownloaded",
53
+ () => payload as unknown as BundleDownloadedInfo,
54
+ );
55
+ break;
56
+ case "onHttpResponse":
57
+ emit("onHttpResponse", () => payload as unknown as UpdateHttpResponse);
58
+ break;
59
+ case "onUpdateError": {
60
+ const { message, ...error } = payload;
61
+ emit(
62
+ "onUpdateError",
63
+ () =>
64
+ ({
65
+ ...error,
66
+ cause: new Error(
67
+ typeof message === "string"
68
+ ? message
69
+ : "The native update check failed",
70
+ ),
71
+ }) as unknown as UpdateError,
72
+ );
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ };
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "1.0.7";
1
+ export const HOT_UPDATER_SDK_VERSION = "1.1.0";
@@ -54,11 +54,20 @@ export interface InstallArtifactParams {
54
54
 
55
55
  export interface Spec extends TurboModule {
56
56
  // Methods
57
+ /**
58
+ * Restarts the app process on Android and reloads React Native in place on
59
+ * iOS, so the app loads the current bundle.
60
+ */
57
61
  reload(): Promise<void>;
58
62
  /**
59
- * Android process restart path used by `setReloadBehavior("processRestart")`.
60
- *
61
- * iOS exposes the same method name for API parity, but it behaves the same as `reload()`.
63
+ * @deprecated Kept for JavaScript↔native compatibility within this major
64
+ * version. Released JavaScript calls it on Android, and this SDK's
65
+ * JavaScript still calls it on Android because an older native build's
66
+ * `reload()` reloads React Native in place there. Native `reload()` now
67
+ * restarts the process on Android too: the Kotlin and Objective-C
68
+ * `reloadProcess` implementations run the same code as `reload()`, a
69
+ * process restart on Android and an in-place reload on iOS. The next major
70
+ * version removes it.
62
71
  */
63
72
  reloadProcess(): Promise<void>;
64
73
  /**
@@ -234,6 +243,14 @@ export interface Spec extends TurboModule {
234
243
  */
235
244
  setStorageItem: (key: string, value: string | null) => void;
236
245
 
246
+ /**
247
+ * Takes the events of the update checks native code ran without JavaScript
248
+ * (a native `HotUpdaterInstance`), as a JSON array, and clears them in one
249
+ * step.
250
+ * Returns null when there are none.
251
+ */
252
+ takeNativeUpdateEvents: () => string | null;
253
+
237
254
  // EventEmitter
238
255
  addListener(eventName: string): void;
239
256
  removeListeners(count: number): void;
@@ -1,50 +0,0 @@
1
- # Bundled Libraries
2
-
3
- ## hot-updater-brotli-dec-1.2.0.jar
4
-
5
- **Why bundled:** The official `org.brotli:dec` package on Maven Central is stuck at v0.1.2 (released May 2017) and contains critical security vulnerabilities. Google has not published newer versions to Maven Central despite releasing v1.2.0 with security fixes.
6
-
7
- **Source:** Built from [google/brotli v1.2.0](https://github.com/google/brotli/releases/tag/v1.2.0)
8
-
9
- **Security Fixes:**
10
- - CVE-2020-8927: Buffer overflow in Brotli versions < 1.0.8
11
- - CVE-2025-6176: DoS vulnerability in versions ≤ 1.1.0 (fixed in v1.2.0)
12
-
13
- **Build Command:**
14
- ```bash
15
- git clone --depth 1 --branch v1.2.0 https://github.com/google/brotli.git
16
- cd brotli/java/org/brotli/dec
17
- mvn clean package -DskipTests
18
- cp target/org.brotli.dec-1.2.0-SNAPSHOT.jar <destination>/org.brotli.dec-1.2.0.jar
19
- ```
20
-
21
- The generated jar is then relocated before being bundled:
22
-
23
- ```gradle
24
- relocate "org.brotli.dec", "com.hotupdater.vendor.brotli.dec"
25
- ```
26
-
27
- The relocation keeps Hot Updater's security-fixed Brotli decoder isolated from
28
- apps or frameworks that also depend on `org.brotli:dec`.
29
-
30
- **Verification:**
31
- - **Original SHA256:** `2d12a2d7fb52fd7f944564fb378aaccd41389cf02ea2e5eb6b3a6477188cced8`
32
- - **Relocated SHA256:** `4e9aee81466e21e365fd291bd29ca0540dc174ce64d29a7613ff336e4276bcdf`
33
- - **Size:** 99KB
34
- - **Built:** 2026-02-12
35
- - **Git Commit:** [028fb5a](https://github.com/google/brotli/commit/028fb5a23661f123017c060daa546b55cf4bde29) (v1.2.0 tag)
36
- - **Relocated Package:** `com.hotupdater.vendor.brotli.dec`
37
-
38
- **API Compatibility:**
39
- The v1.2.0 decoder is API compatible with v0.1.2. Hot Updater imports the
40
- relocated package internally, so apps can keep their own `org.brotli:dec`
41
- dependency without duplicate classes:
42
- - Relocated package: `com.hotupdater.vendor.brotli.dec`
43
- - Same class: `BrotliInputStream`
44
- - Same constructor: `BrotliInputStream(InputStream)`
45
-
46
- **Why Not Apache Commons Compress?**
47
- Apache Commons Compress depends on `org.brotli:dec:0.1.2` as an optional dependency, so switching to it would not fix the vulnerability—it would just make it transitive.
48
-
49
- **Future Updates:**
50
- When Google releases new versions of Brotli (e.g., v1.3.0), rebuild this JAR using the same process, relocate it into the Hot Updater vendor package, and update the SHA256 checksum and build date in this file.
@@ -1,38 +0,0 @@
1
- package com.hotupdater
2
-
3
- import com.facebook.react.ReactApplication
4
- import com.facebook.react.bridge.JSBundleLoader
5
-
6
- /**
7
- * React Native 0.81 and older can run the New Architecture on the bridge, which
8
- * only ReactNativeHost reaches. Only builds for those versions compile this file.
9
- */
10
- internal object BridgeReactInstance {
11
- fun setJSBundle(
12
- application: ReactApplication,
13
- bundleLoader: JSBundleLoader?,
14
- ) {
15
- @Suppress("DEPRECATION")
16
- val instanceManager = application.reactNativeHost.reactInstanceManager
17
- val bundleLoaderField = instanceManager::class.java.getDeclaredField("mBundleLoader")
18
- bundleLoaderField.isAccessible = true
19
- bundleLoaderField.set(instanceManager, bundleLoader)
20
- }
21
-
22
- fun reload(application: ReactApplication) {
23
- @Suppress("DEPRECATION")
24
- val reactNativeHost = application.reactNativeHost
25
- try {
26
- reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
27
- } catch (e: Exception) {
28
- val currentActivity = reactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
29
- if (currentActivity == null) {
30
- return
31
- }
32
-
33
- currentActivity.runOnUiThread {
34
- currentActivity.recreate()
35
- }
36
- }
37
- }
38
- }
@@ -1,24 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.util.Log
4
- import com.facebook.react.ReactApplication
5
- import com.facebook.react.bridge.JSBundleLoader
6
-
7
- /**
8
- * React Native 0.82 and later have no bridge, so an app without a ReactHost has
9
- * no instance to update. Builds for those versions compile this file instead of
10
- * the bridge one, and never reference ReactNativeHost.
11
- */
12
- @Suppress("UNUSED_PARAMETER")
13
- internal object BridgeReactInstance {
14
- fun setJSBundle(
15
- application: ReactApplication,
16
- bundleLoader: JSBundleLoader?,
17
- ) {
18
- Log.d("HotUpdater", "No ReactHost to set the JS bundle on")
19
- }
20
-
21
- fun reload(application: ReactApplication) {
22
- Log.d("HotUpdater", "No ReactHost to reload")
23
- }
24
- }
@@ -1,34 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.app.Application
4
- import android.content.Context
5
- import com.facebook.react.ReactApplication
6
- import com.facebook.react.bridge.JSBundleLoader
7
-
8
- open class ReactIntegrationManagerBase(
9
- private val context: Context,
10
- ) {
11
- fun getJSBundlerLoader(bundleFileUrl: String): JSBundleLoader? {
12
- val bundleLoader: JSBundleLoader?
13
-
14
- if (bundleFileUrl.lowercase().startsWith("assets://")) {
15
- bundleLoader =
16
- JSBundleLoader.createAssetLoader(
17
- context,
18
- bundleFileUrl,
19
- false,
20
- )
21
- } else {
22
- bundleLoader = JSBundleLoader.createFileLoader(bundleFileUrl)
23
- }
24
- return bundleLoader
25
- }
26
-
27
- public fun getReactApplication(application: Application?): ReactApplication {
28
- if (application is ReactApplication) {
29
- return application
30
- } else {
31
- throw IllegalArgumentException("Application does not implement ReactApplication")
32
- }
33
- }
34
- }
@@ -1,185 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.content.Context
4
- import android.util.Log
5
- import com.facebook.react.ReactApplication
6
- import com.facebook.react.ReactHost
7
- import com.facebook.react.ReactInstanceEventListener
8
- import com.facebook.react.bridge.ReactApplicationContext
9
- import com.facebook.react.bridge.ReactContext
10
- import com.facebook.react.common.LifecycleState
11
- import kotlinx.coroutines.suspendCancellableCoroutine
12
- import kotlin.coroutines.resume
13
-
14
- class ReactIntegrationManager(
15
- private val context: Context,
16
- ) : ReactIntegrationManagerBase(context) {
17
- /**
18
- * Sets the JS bundle using ReactHost directly
19
- * @param reactHost The ReactHost instance
20
- * @param bundleURL The bundle URL to set
21
- */
22
- private fun setJSBundleInternal(
23
- reactHost: ReactHost,
24
- bundleURL: String,
25
- ) {
26
- try {
27
- // Try both Java and Kotlin field names for compatibility
28
- val reactHostDelegateField =
29
- try {
30
- reactHost::class.java.getDeclaredField("mReactHostDelegate")
31
- } catch (e: NoSuchFieldException) {
32
- try {
33
- reactHost::class.java.getDeclaredField("reactHostDelegate")
34
- } catch (e2: NoSuchFieldException) {
35
- throw RuntimeException("Neither mReactHostDelegate nor reactHostDelegate field found", e2)
36
- }
37
- }
38
-
39
- reactHostDelegateField.isAccessible = true
40
- val reactHostDelegate = reactHostDelegateField.get(reactHost)
41
- // Try _jsBundleLoader first (Expo SDK 55+ ExpoReactHostDelegate uses a computed
42
- // property with a private backing field), then fall back to jsBundleLoader
43
- // (DefaultReactHostDelegate and older Expo versions).
44
- val jsBundleLoaderField =
45
- try {
46
- reactHostDelegate::class.java.getDeclaredField("_jsBundleLoader")
47
- } catch (e: NoSuchFieldException) {
48
- reactHostDelegate::class.java.getDeclaredField("jsBundleLoader")
49
- }
50
- jsBundleLoaderField.isAccessible = true
51
- jsBundleLoaderField.set(reactHostDelegate, getJSBundlerLoader(bundleURL))
52
- } catch (e: Exception) {
53
- Log.d("HotUpdater", "Failed to setJSBundle with ReactHost: ${e.message}")
54
- }
55
- }
56
-
57
- /**
58
- * Reload the React Native application using ReactHost directly.
59
- * @param reactHost The ReactHost instance
60
- */
61
- private suspend fun reloadInternal(reactHost: ReactHost) {
62
- try {
63
- // Ensure initialized; if not, start and wait
64
- waitForReactContextInitialized(reactHost)
65
-
66
- val activity = reactHost.currentReactContext?.currentActivity
67
- if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) {
68
- reactHost.onHostResume(activity)
69
- }
70
- reactHost.reload("Requested by HotUpdater")
71
- } catch (e: Exception) {
72
- Log.d("HotUpdater", "Failed to reload with ReactHost: ${e.message}")
73
- }
74
- }
75
-
76
- private fun getReactApplication(): ReactApplication? {
77
- if (context is ReactApplicationContext) {
78
- val activity = context.currentActivity
79
- val application = activity?.application
80
- if (application is ReactApplication) {
81
- return application
82
- }
83
- }
84
-
85
- return null
86
- }
87
-
88
- /**
89
- * Sets the JS bundle.
90
- * Priority: ReactHostHolder (if set) > application.reactHost > the bridge (React Native 0.81 and older)
91
- * @param bundleURL The bundle URL to set
92
- */
93
- public fun setJSBundle(bundleURL: String) {
94
- try {
95
- // 1. First, check if ReactHost was set via HotUpdater.setReactHost()
96
- val configuredHost = ReactHostHolder.getReactHost()
97
- if (configuredHost != null) {
98
- setJSBundleInternal(configuredHost, bundleURL)
99
- return
100
- }
101
-
102
- // 2. Try to get ReactApplication from context
103
- val application = getReactApplication()
104
- if (application == null) {
105
- Log.d("HotUpdater", "No ReactHost set and application is not ReactApplication")
106
- return
107
- }
108
-
109
- // 3. Try application's ReactHost (new architecture)
110
- val reactHost = application.reactHost
111
- if (reactHost != null) {
112
- setJSBundleInternal(reactHost, bundleURL)
113
- return
114
- }
115
-
116
- // 4. Fall back to the bridge, which React Native 0.81 and older can run
117
- BridgeReactInstance.setJSBundle(application, getJSBundlerLoader(bundleURL))
118
- } catch (e: Exception) {
119
- Log.d("HotUpdater", "Failed to setJSBundle: ${e.message}")
120
- }
121
- }
122
-
123
- /**
124
- * Reload the React Native application.
125
- * Priority: ReactHostHolder (if set) > application.reactHost > the bridge (React Native 0.81 and older)
126
- */
127
- public suspend fun reload() {
128
- try {
129
- // 1. First, check if ReactHost was set via HotUpdater.setReactHost()
130
- val configuredHost = ReactHostHolder.getReactHost()
131
- if (configuredHost != null) {
132
- reloadInternal(configuredHost)
133
- return
134
- }
135
-
136
- // 2. Try to get ReactApplication from context
137
- val application = getReactApplication()
138
- if (application == null) {
139
- Log.d("HotUpdater", "No ReactHost set and application is not ReactApplication")
140
- return
141
- }
142
-
143
- // 3. Try application's ReactHost (new architecture)
144
- val reactHost = application.reactHost
145
- if (reactHost != null) {
146
- reloadInternal(reactHost)
147
- return
148
- }
149
-
150
- // 4. Fall back to the bridge, which React Native 0.81 and older can run
151
- BridgeReactInstance.reload(application)
152
- } catch (e: Exception) {
153
- Log.d("HotUpdater", "Failed to reload: ${e.message}")
154
- }
155
- }
156
-
157
- /**
158
- * Waits until ReactContext is initialized.
159
- * @return true if ReactContext was already initialized; false if we waited for it.
160
- */
161
- suspend fun waitForReactContextInitialized(reactHost: ReactHost): Boolean {
162
- return try {
163
- // If already initialized, return immediately
164
- if (reactHost.currentReactContext != null) return true
165
-
166
- // Wait for initialization; MainApplication handles starting the host
167
- suspendCancellableCoroutine { continuation ->
168
- val listener =
169
- object : ReactInstanceEventListener {
170
- override fun onReactContextInitialized(context: ReactContext) {
171
- reactHost.removeReactInstanceEventListener(this)
172
- if (continuation.isActive) continuation.resume(Unit)
173
- }
174
- }
175
-
176
- reactHost.addReactInstanceEventListener(listener)
177
- continuation.invokeOnCancellation { reactHost.removeReactInstanceEventListener(listener) }
178
- }
179
- false
180
- } catch (e: Exception) {
181
- Log.d("HotUpdater", "waitForReactContextInitialized failed: ${e.message}")
182
- true
183
- }
184
- }
185
- }
@@ -1,120 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.content.Context
4
- import android.util.Log
5
- import com.facebook.react.ReactApplication
6
- import com.facebook.react.ReactInstanceEventListener
7
- import com.facebook.react.ReactInstanceManager
8
- import com.facebook.react.bridge.JSBundleLoader
9
- import com.facebook.react.bridge.ReactApplicationContext
10
- import com.facebook.react.bridge.ReactContext
11
- import kotlinx.coroutines.suspendCancellableCoroutine
12
- import java.lang.reflect.Field
13
- import kotlin.coroutines.resume
14
-
15
- class ReactIntegrationManager(
16
- private val context: Context,
17
- ) : ReactIntegrationManagerBase(context) {
18
- private fun getReactApplication(): ReactApplication? {
19
- // 1. Try to get from ReactApplicationContext's current activity
20
- if (context is ReactApplicationContext) {
21
- val activity = context.currentActivity
22
- val application = activity?.application
23
- if (application is ReactApplication) {
24
- return application
25
- }
26
- }
27
-
28
- return null
29
- }
30
-
31
- /**
32
- * Sets the JS bundle.
33
- * Gets ReactApplication from context and uses reactNativeHost.
34
- * @param bundleURL The bundle URL to set
35
- */
36
- public fun setJSBundle(bundleURL: String) {
37
- try {
38
- val application = getReactApplication()
39
- if (application == null) {
40
- Log.d("HotUpdater", "Application is not ReactApplication")
41
- return
42
- }
43
-
44
- val instanceManager = application.reactNativeHost.reactInstanceManager
45
- val bundleLoader: JSBundleLoader? = this.getJSBundlerLoader(bundleURL)
46
- val bundleLoaderField: Field =
47
- instanceManager::class.java.getDeclaredField("mBundleLoader")
48
- bundleLoaderField.isAccessible = true
49
-
50
- if (bundleLoader != null) {
51
- bundleLoaderField.set(instanceManager, bundleLoader)
52
- } else {
53
- bundleLoaderField.set(instanceManager, null)
54
- }
55
- } catch (e: Exception) {
56
- Log.d("HotUpdater", "Failed to setJSBundle: ${e.message}")
57
- }
58
- }
59
-
60
- /**
61
- * Reload the React Native application.
62
- * Gets ReactApplication from context and uses reactNativeHost.
63
- */
64
- public suspend fun reload() {
65
- try {
66
- val application = getReactApplication()
67
- if (application == null) {
68
- Log.d("HotUpdater", "Application is not ReactApplication")
69
- return
70
- }
71
-
72
- val instanceManager = application.reactNativeHost.reactInstanceManager
73
-
74
- // Ensure initialized; if not, start and wait
75
- waitForReactContextInitialized(instanceManager)
76
-
77
- instanceManager.recreateReactContextInBackground()
78
- } catch (e: Exception) {
79
- try {
80
- val application = getReactApplication() ?: return
81
- val instanceManager = application.reactNativeHost.reactInstanceManager
82
- val currentActivity = instanceManager.currentReactContext?.currentActivity
83
- if (currentActivity == null) {
84
- return
85
- }
86
-
87
- currentActivity.runOnUiThread {
88
- currentActivity.recreate()
89
- }
90
- } catch (e2: Exception) {
91
- Log.d("HotUpdater", "Failed to reload: ${e2.message}")
92
- }
93
- }
94
- }
95
-
96
- /**
97
- * Waits until ReactContext is initialized using ReactInstanceManager.
98
- * @param instanceManager The ReactInstanceManager instance
99
- * @return true if ReactContext was already initialized; false if we waited for it.
100
- */
101
- suspend fun waitForReactContextInitialized(instanceManager: ReactInstanceManager): Boolean {
102
- // If already initialized, return immediately and indicate so
103
- if (instanceManager.currentReactContext != null) return true
104
-
105
- // Otherwise, wait for initialization; MainApplication handles starting the instance
106
- suspendCancellableCoroutine { continuation ->
107
- val listener =
108
- object : ReactInstanceEventListener {
109
- override fun onReactContextInitialized(context: ReactContext) {
110
- instanceManager.removeReactInstanceEventListener(this)
111
- if (continuation.isActive) continuation.resume(Unit)
112
- }
113
- }
114
-
115
- instanceManager.addReactInstanceEventListener(listener)
116
- continuation.invokeOnCancellation { instanceManager.removeReactInstanceEventListener(listener) }
117
- }
118
- return false
119
- }
120
- }