@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.
- package/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -185
- 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
|
|
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
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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;
|
package/android/libs/README.md
DELETED
|
@@ -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.
|
|
Binary 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
|
-
}
|