From 8f2bf2dd43d48e59367f88c924a667980d5e13e3 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Tue, 9 Jun 2026 19:01:34 +0800 Subject: [PATCH 01/21] feat(device-utils): add getAndClearColdStartLocalNotification for iOS cold-start deep-link (OK-55681) LaunchOptionsStore gains an in-memory coldStartLocalNotification slot (JSON userInfo of a tapped LOCAL notification) plus a read-once takeColdStartLocalNotification(). The new Nitro method getAndClearColdStartLocalNotification() returns it and clears it; the host AppDelegate writes the slot via KVC on a killed-app notification tap. In-memory only (no NSUserDefaults): a new process = fresh nil, so it is launch-scoped and cannot replay a stale tap. Android returns "" (taps arrive via Intent extras there). Run yarn nitrogen + yarn prepare before publishing 3.0.54. --- .../ReactNativeDeviceUtils.kt | 7 ++++++ .../ios/ReactNativeDeviceUtils.swift | 24 +++++++++++++++++++ .../src/ReactNativeDeviceUtils.nitro.ts | 4 ++++ 3 files changed, 35 insertions(+) diff --git a/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt b/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt index 7b9e9be4..b871ae6b 100644 --- a/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt +++ b/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt @@ -907,6 +907,13 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven } } + // iOS-only: cold-start LOCAL notification deep-link payload. Android delivers + // notification taps through the launching Intent's extras, a separate path, + // so there is nothing to hand back here. + override fun getAndClearColdStartLocalNotification(): Promise { + return Promise.resolved("") + } + // MARK: - ExitModule override fun exitApp() { diff --git a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift index 0b142cea..620793f8 100644 --- a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift +++ b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift @@ -21,8 +21,26 @@ public class LaunchOptionsStore: NSObject { public var deviceToken: Data? public var startupTime: TimeInterval = 0 + // Cold-start deep-link: the JSON `userInfo` of a LOCAL notification the user + // tapped to launch the (killed) app. The legacy `NotificationCenter` broadcast + // in AppDelegate is fire-and-forget and is lost on cold start because JS has + // not registered a listener yet; this slot is a pull buffer JS drains once it + // boots. In-memory ONLY (no NSUserDefaults): a new process = fresh `nil`, so + // it is naturally launch-scoped and cannot replay a stale tap on a later + // unrelated cold start. AppDelegate writes it via KVC + // (`setValue(_:forKey:"coldStartLocalNotification")`), same bridge as the + // properties above. + public var coldStartLocalNotification: String? + private static let deviceTokenKey = "1k_device_token" + // Read-once: hand the payload to JS exactly once per launch. + public func takeColdStartLocalNotification() -> String { + let value = coldStartLocalNotification ?? "" + coldStartLocalNotification = nil + return value + } + public func getDeviceTokenString() -> String { // Prefer the JS-saved token (persisted across launches) if let saved = UserDefaults.standard.string(forKey: LaunchOptionsStore.deviceTokenKey), !saved.isEmpty { @@ -189,6 +207,12 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { } } + func getAndClearColdStartLocalNotification() throws -> Promise { + return Promise.async { + return LaunchOptionsStore.shared.takeColdStartLocalNotification() + } + } + // MARK: - ExitModule func exitApp() throws { diff --git a/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts b/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts index 5c00916f..593522b2 100644 --- a/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts +++ b/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts @@ -59,6 +59,10 @@ export interface ReactNativeDeviceUtils saveDeviceToken(token: string): Promise; registerDeviceToken(): Promise; getStartupTime(): Promise; + // Returns the JSON userInfo of a LOCAL notification the user tapped to launch + // the (killed) app, then clears it so it is delivered exactly once. Empty + // string when there is none. iOS-only meaningful; Android returns "". + getAndClearColdStartLocalNotification(): Promise; // ExitModule exitApp(): void; From 3de625bf97da0f7e0d41ead57fd8128c27e4c02f Mon Sep 17 00:00:00 2001 From: huhuanming Date: Tue, 9 Jun 2026 19:20:02 +0800 Subject: [PATCH 02/21] chore: bump version to 3.0.55 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index acec61b0..265210e6 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index 8a43f6e7..58d3ea96 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index b8bb5473..3457515f 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index b73eb98c..feec63b3 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 1d36fc37..d061053a 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index 138dcea9..d49d6a6d 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 2e0192e7..7c9e9034 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 711334eb..90b58c5d 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index 8f1ad821..b81020f5 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index e7beb378..fc2cbad2 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index ed344740..cfdef602 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 843361d6..09d9bb3a 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 09d9ca21..679de689 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index ba7c2b37..e0a123b3 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index b113f880..2ee0fad6 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.54", + "version": "3.0.55", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 1d3023cf..a4de7a6d 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 198c2e99..75e171e5 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index b85dc3cf..a9fe572a 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 7a1bf4ee..63619441 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 77625a03..87410d50 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index 446db557..bb26578d 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 7ad0e051..dc507607 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 6726b259..71316ed9 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index ebf0b69f..28e8541b 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 7fa553d2..5e5a5517 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index d4677fc9..384ddd2b 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.54", + "version": "3.0.55", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 55e54f2b..0fb590bb 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 6b7d5cba..0a7c578f 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.54", + "version": "3.0.55", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index 9d15bebe..0c957733 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index b5f3f2e9..2ff6d24f 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.54", + "version": "3.0.55", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 3cc13ca5..fac1b08b 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 647f8c9c..a60900c0 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.54", + "version": "3.0.55", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index 59028f00..a15816bd 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.54", + "version": "3.0.55", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From 55226a4df883ee0f4831af471503e93189922cfa Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 03:52:18 +0800 Subject: [PATCH 03/21] feat(chart-webview): warm-driver + offline chart instrumentation - iOS/Android: warm-boot the shared offline page + route page->native callbacks to owner ?? warmDriver so bars-state/load-end aren't dropped while the host that owns the WebView is the offscreen prewarm - source/bridge setters apply synchronously (drop scheduleReconcile, which caused an infinite reconcile loop on Android) - add ChartWV diagnostics via OneKeyLog (depend on ReactNativeNativeLogger) - fix Android compileReleaseKotlin: dispose() needs override --- .../ChartWebview.podspec | 3 + .../nitro/chartwebview/ChartWebview.kt | 55 ++++-- .../nitro/chartwebview/PooledChartWebView.kt | 18 +- .../ios/ChartWebview.swift | 162 ++++++++++++++---- 4 files changed, 191 insertions(+), 47 deletions(-) diff --git a/native-views/react-native-chart-webview/ChartWebview.podspec b/native-views/react-native-chart-webview/ChartWebview.podspec index 24f74b52..7527bb90 100644 --- a/native-views/react-native-chart-webview/ChartWebview.podspec +++ b/native-views/react-native-chart-webview/ChartWebview.podspec @@ -21,6 +21,9 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + # DEBUG instrumentation: route native chart lifecycle logs into the shared + # OneKeyLog file (app-latest.log) via `import ReactNativeNativeLogger`. + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/ChartWebview+autolinking.rb' add_nitrogen_files(s) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt index 7eb2934f..52222383 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt @@ -98,34 +98,37 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp // --- Source props --- + // Source/bridge setters call applySource (synchronous) — NOT scheduleReconcile, + // which caused an infinite reconcile loop (reconcile -> setSource -> prop + // re-apply -> reconcile ...). applySource sets warmDriver + warm-boots the page + // even when this host is not the visible owner, so post-attach bridge arrival + // still makes this host the warm driver (page->native callbacks fall back to it). private var _uri: String? = null override var uri: String? get() = _uri - set(value) { _uri = value; applySourceIfOwner() } + set(value) { _uri = value; applySource() } private var _localBundle: String? = null override var localBundle: String? get() = _localBundle - set(value) { _localBundle = value; applySourceIfOwner() } + set(value) { _localBundle = value; applySource() } private var _entry: String? = null override var entry: String? get() = _entry - set(value) { _entry = value; applySourceIfOwner() } + set(value) { _entry = value; applySource() } private var _paramsJson: String? = null override var paramsJson: String? get() = _paramsJson - set(value) { _paramsJson = value; applySourceIfOwner() } + set(value) { _paramsJson = value; applySource() } // Document-start bridge JS (single source of truth in the TS layer). Stored and // handed to the pooled WebView when we claim it, before its first load. private var _bridgeScript: String? = null override var bridgeScript: String? get() = _bridgeScript - // Re-apply the source: if this prop lands after the source props (a load can't - // run before the bridge is registered, so setSource deferred), this triggers it. - set(value) { _bridgeScript = value; applySourceIfOwner() } + set(value) { _bridgeScript = value; applySource() } // --- Singleton props --- // `pooled` + non-empty `reuseKey` => the backing WebView is shared (keyed by @@ -226,13 +229,22 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp ChartWebviewPool.adopt(key) adoptedPoolKey = key } + // WARM-BOOT (mirror of iOS): boot the shared offline page + register as the + // warm DRIVER as soon as ANY referencing host has the bridge, even when not + // the visible owner (offscreen prewarm runs with attached=false/active=false). + // page->native callbacks fall back to warmDriver when there's no owner, so the + // bars-state / load-end signals aren't dropped during warm — otherwise the + // chart stays on the loading mask. Idempotent: setSource dedupes on same URL, + // setBridgeScript no-ops once registered. + val bs = _bridgeScript + if (!bs.isNullOrEmpty()) { + entry.setBridgeScript(bs) + entry.warmDriver = this + entry.setSource(_uri, _localBundle, _entry, _paramsJson) + } if (wantsOwnership()) { entry.owner = this - // Register the document-start bridge before the first load (the prop is set - // by now; the pool may have been created earlier by a window-attach reconcile). - entry.setBridgeScript(_bridgeScript ?: "") entry.attachTo(container) - entry.setSource(_uri, _localBundle, _entry, _paramsJson) // Keep a fresh frame of OUR content while we own the WebView, so that when // we later go inactive (and the shared WebView reloads the other slot's // chart) our slot freezes to its own last frame, not the other content. @@ -263,11 +275,21 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp pooled.setSource(_uri, _localBundle, _entry, _paramsJson) } - private fun applySourceIfOwner() { + // Apply the source synchronously when a source/bridge prop changes. For a + // pooled host this ALSO registers it as the warmDriver and warm-boots the page + // even when it is not the visible owner, so page->native callbacks (bars-state / + // load-end) fall back to it when no host owns the pool. No scheduleReconcile — + // that looped. backing is null until the first reconcile assigns it (the + // reuseKey/pooled/active setters still scheduleReconcile), so warmDriver is set + // on the first prop change after the pool entry is acquired. + private fun applySource() { val pooled = backing ?: return - if (pooled.owner == this) { - // Register the bridge before any load setSource may trigger (setSource - // defers loading until the bridge is registered). + val bs = _bridgeScript + if (isPooled() && !bs.isNullOrEmpty()) { + pooled.setBridgeScript(bs) + pooled.warmDriver = this + pooled.setSource(_uri, _localBundle, _entry, _paramsJson) + } else if (pooled.owner == this) { pooled.setBridgeScript(_bridgeScript ?: "") pooled.setSource(_uri, _localBundle, _entry, _paramsJson) } @@ -359,7 +381,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp // WebView so it doesn't leak a Chromium renderer + JavascriptInterface per // mount/unmount. Pooled (shared) backing is intentionally left alive in the // warm pool — other hosts may still share it; its lifetime is the pool's. - fun dispose() { + override fun dispose() { stopOwnCapture() container.removeCallbacks(reconcileRunnable) container.removeCallbacks(revealFallbackRunnable) @@ -368,6 +390,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp if (pendingRevealHost == this) pendingRevealHost = null val entry = backing if (entry != null && entry.owner == this) entry.owner = null + if (entry != null && entry.warmDriver == this) entry.warmDriver = null // Balance the pool adopt() if this host ever joined a pooled key. Kept warm by // default (single-instance cache); the release path makes destroy() reachable. adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) } diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index a94d2d6c..3809262d 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -149,6 +149,11 @@ class PooledChartWebView private constructor( /** The host currently displaying this WebView; page events route here. */ var owner: HybridChartWebview? = null + // The host that warm-booted the page and can drive its symbol / receive its + // callbacks while there is no VISIBLE owner yet. Separate from `owner` (the + // YIELD path clears `owner`); callbacks fall back to this so bars-state / + // load-end aren't dropped during warm. Mirror of the iOS warmDriver. + var warmDriver: HybridChartWebview? = null private var assetLoader: WebViewAssetLoader? = null private var lastLoadedUrl: String? = null @@ -195,7 +200,7 @@ class PooledChartWebView private constructor( // script (see registerBridgeForOrigins). We deliberately do NOT re-inject // it here via evaluateJavascript — that ran on every page event regardless // of URL and would expose the privileged bridge to untrusted pages/frames. - owner?.dispatchLoadEnd() + (owner ?: warmDriver)?.dispatchLoadEnd() // Prime the snapshot so the first move already has a frame to mask with. refreshSnapshotSoon() } @@ -217,7 +222,16 @@ class PooledChartWebView private constructor( private inner class ChartBridge { @JavascriptInterface fun postMessage(message: String) { - runOnUiThread { owner?.dispatchMessage(message) } + runOnUiThread { + val target = owner ?: warmDriver + if (target == null) { + android.util.Log.w( + "ChartWV", + "msg DROPPED (no owner/warmDriver): ${message.take(80)}", + ) + } + target?.dispatchMessage(message) + } } } diff --git a/native-views/react-native-chart-webview/ios/ChartWebview.swift b/native-views/react-native-chart-webview/ios/ChartWebview.swift index 0e9faf9d..a3f44f80 100644 --- a/native-views/react-native-chart-webview/ios/ChartWebview.swift +++ b/native-views/react-native-chart-webview/ios/ChartWebview.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import WebKit +import ReactNativeNativeLogger // MARK: - Constants (shared) @@ -13,6 +14,25 @@ private enum ChartWebviewConst { static let messageHandlerName = "onekeyChart" } +// MARK: - Diagnostic logging (DEBUG instrumentation) +// +// Routes the chart-webview native lifecycle into the shared OneKeyLog file +// (app-latest.log) so the offline / prewarm / pool-ownership chain is visible +// alongside the JS `market => chart => *` events. Tag is "ChartWV". Raw NSLog +// is NOT enough — it never reaches app-latest.log, which is why the native side +// was previously invisible during log analysis. Truncates payloads to keep the +// file readable and avoid leaking message bodies. +enum ChartWVLog { + static func i(_ msg: String) { OneKeyLog.info("ChartWV", msg) } + static func w(_ msg: String) { OneKeyLog.warn("ChartWV", msg) } + static func e(_ msg: String) { OneKeyLog.error("ChartWV", msg) } + /// Truncate a (possibly large / sensitive) payload for logging. + static func clip(_ s: String?, _ n: Int = 120) -> String { + guard let s = s else { return "nil" } + return s.count <= n ? s : String(s.prefix(n)) + "…(\(s.count))" + } +} + // MARK: - ChartContainerView (window-attach detection) /// The host's `view`. Reports when it is attached to / detached from a window so @@ -69,9 +89,12 @@ class HybridChartWebview: HybridChartWebviewSpec { override init() { super.init() + ChartWVLog.i("host.init id=\(instanceId)") container.onWindowChange = { [weak self] attached in - self?.attached = attached - self?.scheduleReconcile() + guard let self = self else { return } + ChartWVLog.i("host.windowChange id=\(self.instanceId) attached=\(attached) reuseKey=\(self.reuseKey ?? "nil") pooled=\(String(describing: self.pooled)) active=\(String(describing: self.active))") + self.attached = attached + self.scheduleReconcile() } } @@ -97,15 +120,19 @@ class HybridChartWebview: HybridChartWebviewSpec { // MARK: - Props (source) - var uri: String? { didSet { applySourceIfOwner() } } - var localBundle: String? { didSet { applySourceIfOwner() } } - var entry: String? { didSet { applySourceIfOwner() } } - var paramsJson: String? { didSet { applySourceIfOwner() } } + // Source/bridge setters call applySource (synchronous) — NOT scheduleReconcile, + // which loops (reconcile -> setSource -> prop re-apply -> reconcile ...). + // applySource sets warmDriver + warm-boots the page even when this host is not + // the visible owner, so a bridge prop arriving after the window-attach reconcile + // still makes this host the warm driver (page->native callbacks fall back to it). + var uri: String? { didSet { applySource() } } + var localBundle: String? { didSet { applySource() } } + var entry: String? { didSet { applySource() } } + var paramsJson: String? { didSet { applySource() } } // Document-start bridge JS (single source of truth in the TS layer). Handed to - // the pooled WebView when we claim it, before its first load. Re-applying the - // source on change triggers a deferred load if this prop lands after the source. - var bridgeScript: String? { didSet { applySourceIfOwner() } } + // the pooled WebView when we claim it, before its first load. + var bridgeScript: String? { didSet { applySource() } } // MARK: - Props (singleton) @@ -140,6 +167,10 @@ class HybridChartWebview: HybridChartWebviewSpec { // Called by the backing PooledChartWebView while this host is the owner. func handleMessage(_ message: String) { + // page -> native. Truncated; OneKeyLog rate-limits/dedups repeats so streaming + // data won't flood. Key for Q2 (market chart no data): shows the page's + // $private kline requests and whether replies come back. + ChartWVLog.i("msg.in id=\(instanceId) \(ChartWVLog.clip(message, 200))") // The chart reports it has painted the new symbol after a switch; that's our // cue to drop the snapshot we held over the switch and reveal the live chart. if message.contains(HybridChartWebview.renderReadyMarker) { onContentRendered() } @@ -167,6 +198,25 @@ class HybridChartWebview: HybridChartWebviewSpec { private func wantsOwnership() -> Bool { attached && (active != false) } + // Apply the source synchronously on a source/bridge prop change. For a pooled + // host this ALSO registers it as the warmDriver and warm-boots the page even + // when it is not the visible owner, so page->native callbacks (bars-state / + // load-end) fall back to it when no host owns the pool. No scheduleReconcile — + // that loops. `backing` is nil until the first reconcile assigns it (reuseKey / + // pooled / active setters still scheduleReconcile), so warmDriver is set on the + // first prop change after the pool entry is acquired. + private func applySource() { + guard let pooled = backing else { return } + if isPooled(), let bs = bridgeScript, !bs.isEmpty { + pooled.setBridgeScript(bs) + pooled.warmDriver = self + pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) + } else if pooled.owner === self { + pooled.setBridgeScript(bridgeScript ?? "") + pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) + } + } + // A single React commit applies props one at a time, so the intermediate // states are inconsistent (e.g. `pooled` re-applied while `active` is still // stale). Reacting to each setter synchronously makes the wrong host claim @@ -201,13 +251,36 @@ class HybridChartWebview: HybridChartWebviewSpec { ChartWebviewPool.shared.adopt(key: key) adoptedPoolKey = key } + ChartWVLog.i("reconcilePooled id=\(instanceId) key=\(key) wantsOwnership=\(wantsOwnership()) (attached=\(attached) active=\(String(describing: active))) bridgeLen=\((bridgeScript ?? "").count) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil")") + + // Q1 FIX: WARM-BOOT the shared offline page as soon as ANY referencing host + // has the document-start bridge + a source — even when this host is NOT the + // visible owner (the offscreen prewarm runs with attached=false/active=false, + // so the old code, which only loaded inside `wantsOwnership()`, never booted + // the page until a real chart screen attached+focused). LOAD is now decoupled + // from view ownership: ownership below still governs attach / reveal / snapshot. + // Idempotent across hosts — the unified URL is constant (setSource logs + // SAME_URL), and setBridgeScript no-ops once registered. + if let bs = bridgeScript, !bs.isEmpty { + ChartWVLog.i("reconcilePooled.WARM id=\(instanceId) key=\(key) (owner-independent boot; wantsOwnership=\(wantsOwnership()))") + pooled.setBridgeScript(bs) + // Q1 FIX (data): register as the warm DRIVER (NOT owner — the YIELD branch + // below clears `owner`, which is why the previous provisional-owner attempt + // was wiped in the same reconcile). didFinish / page messages fall back to + // warmDriver when there's no visible owner, so the page is driven its symbol + // the instant it loads instead of waiting ~5s for a focused host to claim. + pooled.warmDriver = self + ChartWVLog.i("reconcilePooled.WARM_DRIVER id=\(instanceId) key=\(key)") + // setSource internally guards (bridgeRegistered / SAME_URL / NO_URL), so this + // is idempotent and a no-op when there is nothing new to load. + pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) + } + if wantsOwnership() { + let wasOwner = pooled.owner === self pooled.owner = self - // Register the document-start bridge before the first load (the prop is set - // by now; the pool may have been created earlier by a window-attach reconcile). - pooled.setBridgeScript(bridgeScript ?? "") + ChartWVLog.i("reconcilePooled.CLAIM id=\(instanceId) key=\(key) wasOwner=\(wasOwner)") pooled.attach(to: container) - pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) // Keep a fresh frame of OUR content while we own the WebView, so that when // we later go inactive (and the shared WebView reloads the other slot's // chart) our slot freezes to its own last frame, not the other content. @@ -224,6 +297,7 @@ class HybridChartWebview: HybridChartWebviewSpec { // Inactive: give up ownership only if we still hold it, and freeze to our // own last captured frame. We do NOT detach (that races a rapid re-claim). let wasOwner = pooled.owner === self + ChartWVLog.i("reconcilePooled.YIELD id=\(instanceId) key=\(key) wasOwner=\(wasOwner) (attached=\(attached) active=\(String(describing: active)))") if wasOwner { pooled.owner = nil } stopOwnCapture() showPlaceholder(ownSnapshot) @@ -268,13 +342,6 @@ class HybridChartWebview: HybridChartWebviewSpec { pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) } - private func applySourceIfOwner() { - guard let pooled = backing, pooled.owner === self else { return } - // Register the bridge before any load setSource may trigger (setSource defers - // loading until the bridge is registered). - pooled.setBridgeScript(bridgeScript ?? "") - pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) - } // MARK: - Snapshot placeholder (shown while this host is inactive) @@ -352,6 +419,13 @@ final class PooledChartWebView { let key: String weak var owner: HybridChartWebview? + // Q1 FIX (data): the host that warm-booted the page and can drive its symbol / + // service its callbacks while there is no VISIBLE owner yet. Separate from + // `owner` because the reconcile YIELD branch clears `owner` to nil (so a + // provisional owner never survived to nav.didFinish). Callbacks fall back to + // this when `owner` is nil, so the page is told its symbol the instant it loads + // instead of waiting ~5s for a focused host to attach/claim. + weak var warmDriver: HybridChartWebview? // The page's user-content controller, kept so the document-start bridge can be // added lazily (see setBridgeScript) once the host has the prop value. @@ -379,7 +453,7 @@ final class PooledChartWebView { init(key: String) { self.key = key PooledChartWebView.liveCount += 1 - NSLog("[ChartWebviewPool] WebView CREATED key=\(key) liveCount=\(PooledChartWebView.liveCount)") + ChartWVLog.i("pool.CREATE key=\(key) liveCount=\(PooledChartWebView.liveCount)") setupWebView() } @@ -388,7 +462,10 @@ final class PooledChartWebView { // subsequent calls are no-ops. Done lazily because the first reconcile // (window-attach) can run before the bridgeScript prop is applied. func setBridgeScript(_ bridgeScript: String) { - guard !bridgeScript.isEmpty, let userContent = userContent else { return } + guard !bridgeScript.isEmpty, let userContent = userContent else { + ChartWVLog.w("setBridgeScript.SKIP key=\(key) empty=\(bridgeScript.isEmpty) hasUserContent=\(userContent != nil) -> bridgeRegistered stays \(bridgeRegistered)") + return + } // Re-register if a second host sharing the reuseKey supplies a DIFFERENT script // instead of silently dropping it (fix #4). In the app's single-reuseKey + // constant-bridge reality this never fires; not latching keeps it correct. @@ -399,6 +476,7 @@ final class PooledChartWebView { } bridgeRegistered = true registeredBridgeScript = bridgeScript + ChartWVLog.i("setBridgeScript.REGISTERED key=\(key) len=\(bridgeScript.count)") let handlerName = ChartWebviewConst.messageHandlerName let shim = "(function(){window.__chartNativePost=function(s){" + "window.webkit.messageHandlers.\(handlerName).postMessage(s);};})();" @@ -498,7 +576,7 @@ final class PooledChartWebView { self.userContent = nil self.webView = nil PooledChartWebView.liveCount -= 1 - NSLog("[ChartWebviewPool] WebView DESTROYED key=\(self.key) liveCount=\(PooledChartWebView.liveCount)") + ChartWVLog.i("pool.DESTROY key=\(self.key) liveCount=\(PooledChartWebView.liveCount)") } } @@ -570,15 +648,26 @@ final class PooledChartWebView { // Never load before the document-start bridge is registered — otherwise the // page boots without the bridge and its first $private requests are lost. The // host re-calls setSource once the bridgeScript prop arrives. - guard bridgeRegistered else { return } + guard bridgeRegistered else { + ChartWVLog.w("setSource.BLOCKED key=\(key) bridgeRegistered=false -> NOT loading (will retry when bridgeScript arrives). uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil")") + return + } currentLocalBundle = localBundle - guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else { return } - guard urlString != lastLoadedUrl else { return } + guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else { + ChartWVLog.w("setSource.NO_URL key=\(key) uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil") -> nothing to load") + return + } + guard urlString != lastLoadedUrl else { + ChartWVLog.i("setSource.SAME_URL key=\(key) skip reload url=\(ChartWVLog.clip(urlString))") + return + } guard let url = URL(string: urlString) else { + ChartWVLog.e("setSource.INVALID_URL key=\(key) url=\(ChartWVLog.clip(urlString))") owner?.handleError("Invalid url: \(urlString)") return } lastLoadedUrl = urlString + ChartWVLog.i("setSource.LOAD key=\(key) url=\(ChartWVLog.clip(urlString))") runOnMain { [weak self] in self?.webView?.load(URLRequest(url: url)) } } @@ -622,8 +711,12 @@ final class PooledChartWebView { // MARK: - Bridge methods func postMessage(_ message: String) { + ChartWVLog.i("msg.out key=\(key) \(ChartWVLog.clip(message, 200))") runOnMain { [weak self] in - guard let self = self, let webView = self.webView else { return } + guard let self = self, let webView = self.webView else { + ChartWVLog.w("msg.out.DROPPED no webView") + return + } let jsStringLiteral = self.jsStringLiteral(from: message) let js = "window.postMessage(JSON.parse(\(jsStringLiteral)), '*')" webView.evaluateJavaScript(js, completionHandler: nil) @@ -677,7 +770,9 @@ extension ChartWebViewProxy: WKScriptMessageHandler { didReceive message: WKScriptMessage ) { guard message.name == ChartWebviewConst.messageHandlerName else { return } - let owner = pooled?.owner + // Q1 FIX: route page->native messages ($private data requests) to the warm + // driver when there's no visible owner, so they aren't dropped during warm. + let owner = pooled?.owner ?? pooled?.warmDriver if let body = message.body as? String { owner?.handleMessage(body) } else if let data = try? JSONSerialization.data(withJSONObject: message.body, options: []), @@ -693,16 +788,22 @@ extension ChartWebViewProxy: WKScriptMessageHandler { extension ChartWebViewProxy: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - pooled?.owner?.handleLoadEnd() + // Q1 FIX: fall back to warmDriver when there's no visible owner yet, so the + // page's load-complete callback (-> JS onLoadEnd -> SYMBOL_CHANGE) fires now. + let target = pooled?.owner ?? pooled?.warmDriver + ChartWVLog.i("nav.didFinish url=\(ChartWVLog.clip(webView.url?.absoluteString)) hasOwner=\(pooled?.owner != nil) viaWarmDriver=\(pooled?.owner == nil && pooled?.warmDriver != nil)") + target?.handleLoadEnd() // Prime the snapshot so the first move already has a frame to mask with. pooled?.refreshSnapshotSoon() } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + ChartWVLog.e("nav.didFail url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)") pooled?.owner?.handleError(error.localizedDescription) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + ChartWVLog.e("nav.didFailProvisional url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)") pooled?.owner?.handleError(error.localizedDescription) } } @@ -723,6 +824,7 @@ extension ChartWebViewProxy: WKURLSchemeHandler { } guard let localBundle = pooled?.currentLocalBundle, !localBundle.isEmpty else { + ChartWVLog.e("scheme.NO_BUNDLE url=\(ChartWVLog.clip(url.absoluteString)) -> 404 (currentLocalBundle empty)") respondNotFound(url: url, task: urlSchemeTask) return } @@ -733,9 +835,11 @@ extension ChartWebViewProxy: WKURLSchemeHandler { guard let fileURL = resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath), let data = try? Data(contentsOf: fileURL) else { + ChartWVLog.e("scheme.404 bundle=\(localBundle) path=\(relativePath) (resolved=\(resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath)?.path ?? "nil")) -> Not Found") respondNotFound(url: url, task: urlSchemeTask) return } + ChartWVLog.i("scheme.serve bundle=\(localBundle) path=\(relativePath) bytes=\(data.count)") let headers: [String: String] = [ "Content-Type": mimeTypeForPath(fileURL.pathExtension), From a129867edc37a03f8d2672f35f5066158abaf280 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 10:11:05 +0800 Subject: [PATCH 04/21] feat(chart-webview): Android pause-when-idle + debug toggle + attach retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android: pause the pooled WebView's renderer (onPause) when no host owns it and resume on claim — Android (unlike iOS WKWebView) never throttles an offscreen WebView, so the warm page burned a CPU core + grew RAM to OOM after leaving the chart. Per-instance onPause (NOT process-global pauseTimers). - attachToContainer: retry reparent up to 12 frames instead of giving up after 1 (a single retry stranded the WebView in the old container -> blank chart slot) - webviewDebuggingEnabled Nitro prop (iOS isInspectable / Android setWebContentsDebuggingEnabled) following the dev-mode toggle (Agent B) --- .../nitro/chartwebview/ChartWebview.kt | 27 +++++++ .../nitro/chartwebview/PooledChartWebView.kt | 74 +++++++++++++++++-- .../ios/ChartWebview.swift | 54 +++++++++++++- .../src/ChartWebview.nitro.ts | 10 +++ 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt index 52222383..fdd10024 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt @@ -165,6 +165,22 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp scheduleReconcile() } + // --- Debugging --- + // Make the backing WebView inspectable via chrome://inspect. Driven by the + // app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main + // react-native-webview (which calls WebView.setWebContentsDebuggingEnabled). + // Stored and applied to the backing WebView both here (on prop change) and at + // host claim, since `backing` is null until the first reconcile assigns it. + // CAVEAT: setWebContentsDebuggingEnabled is PROCESS-GLOBAL — see + // PooledChartWebView.setInspectable. + private var _webviewDebuggingEnabled: Boolean? = null + override var webviewDebuggingEnabled: Boolean? + get() = _webviewDebuggingEnabled + set(value) { + _webviewDebuggingEnabled = value + backing?.setInspectable(value) + } + // --- Event callbacks --- override var onMessage: ((message: String) -> Unit)? = null override var onLoadEnd: (() -> Unit)? = null @@ -222,6 +238,9 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp val key = effectiveKey() val entry = ChartWebviewPool.acquireShared(key, context) backing = entry + // Apply this host's debug preference to the (possibly freshly created) backing + // WebView. Mirrors the main react-native-webview's setWebContentsDebuggingEnabled. + entry.setInspectable(_webviewDebuggingEnabled) // Refcount the pool entry once per host (reconcile runs many times). Balanced // by releaseShared in dispose(). If the reuseKey changed, release the old one. if (adoptedPoolKey != key) { @@ -244,6 +263,8 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp } if (wantsOwnership()) { entry.owner = this + // PERF: a host is now showing the chart — make sure the renderer is running. + entry.resume() entry.attachTo(container) // Keep a fresh frame of OUR content while we own the WebView, so that when // we later go inactive (and the shared WebView reloads the other slot's @@ -260,6 +281,11 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp if (wasOwner) entry.owner = null stopOwnCapture() showPlaceholder(ownSnapshot) + // PERF: if nobody owns the shared WebView now, pause its renderer so it + // doesn't keep burning a CPU core + GPU + RAM in the background (Android + // doesn't auto-throttle offscreen WebViews like iOS does). Resumed on the + // next CLAIM. No-op until the page's first load completed. + entry.pauseIfIdle() } } @@ -269,6 +295,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp if (!wantsOwnership()) return val pooled = backing ?: PooledChartWebView.create(context, effectiveKey()) backing = pooled + pooled.setInspectable(_webviewDebuggingEnabled) pooled.owner = this pooled.setBridgeScript(_bridgeScript ?: "") pooled.attachTo(container) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index 3809262d..da636fd0 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -155,6 +155,43 @@ class PooledChartWebView private constructor( // load-end aren't dropped during warm. Mirror of the iOS warmDriver. var warmDriver: HybridChartWebview? = null + // PERF (Android only): Android's in-process WebView/Chromium does NOT throttle + // an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm + // pooled page keeps its rAF render loop + websockets + compositing alive + // FOREVER after the user leaves the chart — pinning a CPU core, growing RAM to + // OOM, and stealing the GPU from RN's RenderThread (every other screen stalls). + // We pause the WebView whenever no host owns it (after the first load) and + // resume on claim. Uses the PER-INSTANCE onPause()/onResume() — NOT the static + // pauseTimers()/resumeTimers(), which are process-global and would also freeze + // the app's other WebViews (inpage provider / web-embed / dapp browser). + private var paused = false + private var hasLoadedOnce = false + + fun markLoaded() { + hasLoadedOnce = true + } + + // Pause the renderer when nobody owns the shared WebView. onPause() stops + // drawing/compositing/animations (frees the GPU so navigation is smooth again) + // WITHOUT changing the view's visibility/attachment — we must NOT toggle + // visibility here, that left the WebView blank/white after re-claim. Skipped + // until the first load so we never freeze a booting page. Resumed (+ redraw) + // on the next CLAIM so the chart paints again. + fun pauseIfIdle() { + if (paused || !hasLoadedOnce || owner != null) return + paused = true + runOnUiThread { webView.onPause() } + } + + fun resume() { + if (!paused) return + paused = false + runOnUiThread { + webView.onResume() + webView.invalidate() + } + } + private var assetLoader: WebViewAssetLoader? = null private var lastLoadedUrl: String? = null private var lastLocalBundle: String? = null @@ -184,6 +221,25 @@ class PooledChartWebView private constructor( addJavascriptInterface(ChartBridge(), "AndroidChartBridge") } + // Apply the app's "Enable Native Webview Debugging" dev-mode toggle, mirroring + // how the main react-native-webview calls WebView.setWebContentsDebuggingEnabled. + // Called by the host both on the prop change and at host claim, so the toggle is + // honored even when this entry was created before the prop arrived. + // + // CAVEAT (PROCESS-GLOBAL): setWebContentsDebuggingEnabled is a STATIC method that + // flips remote-debugging for EVERY WebView in the whole process. Once any WebView + // (this chart, the main react-native-webview, etc.) enables it, it stays enabled + // process-wide until the app is killed — Android exposes no per-WebView toggle and + // no way to read the current value. So a null/false preference here cannot turn + // debugging back OFF once another WebView (or a prior true value) turned it ON; it + // simply does not re-enable it. We therefore only ever call the setter with `true` + // when explicitly enabled, leaving the process-global state untouched otherwise. + fun setInspectable(enabled: Boolean?) { + if (enabled == true) { + runOnUiThread { WebView.setWebContentsDebuggingEnabled(true) } + } + } + init { val n = liveCount.incrementAndGet() android.util.Log.d("ChartWebviewPool", "WebView CREATED key=$key liveCount=$n") @@ -196,6 +252,7 @@ class PooledChartWebView private constructor( override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) + markLoaded() // The bridge is delivered exclusively via the origin-scoped document-start // script (see registerBridgeForOrigins). We deliberately do NOT re-inject // it here via evaluateJavascript — that ran on every page event regardless @@ -239,23 +296,30 @@ class PooledChartWebView private constructor( fun attachTo(container: ViewGroup) { val generation = attachGeneration.incrementAndGet() runOnUiThread { - attachToContainer(container, generation, canRetry = true) + attachToContainer(container, generation, retriesLeft = 12) } } - private fun attachToContainer(container: ViewGroup, generation: Int, canRetry: Boolean) { + private fun attachToContainer(container: ViewGroup, generation: Int, retriesLeft: Int) { if (generation != attachGeneration.get()) return if (webView.parent === container) return (webView.parent as? ViewGroup)?.removeView(webView) val currentParent = webView.parent if (currentParent != null) { - if (canRetry) { - webView.post { attachToContainer(container, generation, canRetry = false) } + // The old container hasn't released the WebView yet (it can be mid-layout / + // mid-teardown when the previous host unmounts). Retry on the next frame + // instead of giving up after one attempt — a single retry was not enough and + // left the WebView stranded in the old (now offscreen) container, so the new + // chart screen showed a blank/white slot. + if (retriesLeft > 0) { + webView.post { + attachToContainer(container, generation, retriesLeft = retriesLeft - 1) + } } else { android.util.Log.w( "ChartWebviewPool", - "Skip attach key=$key because WebView parent was not cleared: $currentParent", + "Skip attach key=$key after retries because WebView parent was not cleared: $currentParent", ) } return diff --git a/native-views/react-native-chart-webview/ios/ChartWebview.swift b/native-views/react-native-chart-webview/ios/ChartWebview.swift index a3f44f80..bcd0423d 100644 --- a/native-views/react-native-chart-webview/ios/ChartWebview.swift +++ b/native-views/react-native-chart-webview/ios/ChartWebview.swift @@ -159,6 +159,20 @@ class HybridChartWebview: HybridChartWebviewSpec { } } + // MARK: - Props (debugging) + + // Make the backing WKWebView inspectable (Safari Web Inspector). Driven by the + // app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main + // react-native-webview (which sets WKWebView.isInspectable). nil => default to + // the DEBUG build behavior (see PooledChartWebView.applyInspectable). Applied to + // the backing WebView both here (on prop change) and at WebView creation, since + // `backing` is nil until the first reconcile assigns it. + var webviewDebuggingEnabled: Bool? { + didSet { + backing?.setInspectable(webviewDebuggingEnabled) + } + } + // MARK: - Props (events) var onMessage: ((_ message: String) -> Void)? @@ -244,6 +258,9 @@ class HybridChartWebview: HybridChartWebviewSpec { let key = effectiveKey() let pooled = ChartWebviewPool.shared.acquireShared(key: key) backing = pooled + // Apply this host's debug preference to the (possibly freshly created) backing + // WebView. Mirrors the main react-native-webview toggling WKWebView.isInspectable. + pooled.setInspectable(webviewDebuggingEnabled) // Refcount the entry once per host (reconcile runs many times). Balanced by // releaseShared in deinit. If the reuseKey changed, release the old one first. if adoptedPoolKey != key { @@ -336,6 +353,7 @@ class HybridChartWebview: HybridChartWebviewSpec { guard wantsOwnership() else { return } let pooled = backing ?? PooledChartWebView(key: effectiveKey()) backing = pooled + pooled.setInspectable(webviewDebuggingEnabled) pooled.owner = self pooled.setBridgeScript(bridgeScript ?? "") pooled.attach(to: container) @@ -440,6 +458,11 @@ final class PooledChartWebView { private var lastLoadedUrl: String? private var attachGeneration = 0 + // Whether the backing WKWebView should be inspectable (Safari Web Inspector). + // nil until a host applies its debug preference; defaults to the DEBUG build + // behavior (see resolvedInspectable), mirroring the main react-native-webview. + private var inspectablePreference: Bool? + /// Read by the scheme handler to resolve offline files. fileprivate var currentLocalBundle: String? @@ -520,10 +543,37 @@ final class PooledChartWebView { webView.navigationDelegate = proxy webView.uiDelegate = proxy webView.scrollView.bounces = false + self.webView = webView + // Honor the app's dev-mode webview-debug toggle instead of an unconditional + // `true`. nil (no preference applied yet) falls back to the DEBUG build default. + applyInspectable() + } + + // Set the host-driven debug preference and apply it to the live WebView. Called + // both on the prop change and at host claim, so the (possibly newly created) + // WebView always reflects the latest toggle value. + func setInspectable(_ enabled: Bool?) { + inspectablePreference = enabled + applyInspectable() + } + + // Resolve the effective inspectable value: explicit preference if set, otherwise + // the DEBUG build default (mirrors the main react-native-webview, which is + // inspectable in dev builds even without the toggle). + private func resolvedInspectable() -> Bool { + if let pref = inspectablePreference { return pref } + #if DEBUG + return true + #else + return false + #endif + } + + private func applyInspectable() { + guard let webView = webView else { return } if #available(iOS 16.4, *) { - webView.isInspectable = true + webView.isInspectable = resolvedInspectable() } - self.webView = webView } // MARK: - Reparenting diff --git a/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts b/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts index 570345aa..508537f2 100644 --- a/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts +++ b/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts @@ -39,6 +39,16 @@ export interface ChartWebviewProps extends HybridViewProps { // hardcodes `setIsActive`/`getIsActive`, causing a NoSuchMethodError at runtime. active?: boolean; + // --- Debugging --- + // Make the backing WebView inspectable (Safari Web Inspector on iOS, + // chrome://inspect on Android). Driven by the app's "Enable Native Webview + // Debugging" dev-mode toggle, mirroring how the main react-native-webview + // honors it. When omitted, the native side defaults to its dev-build default + // (iOS DEBUG / Android leaves it off). NOTE (Android): the underlying call + // WebView.setWebContentsDebuggingEnabled is PROCESS-GLOBAL — once any WebView + // enables it, it stays enabled process-wide until the app is killed. + webviewDebuggingEnabled?: boolean; + // --- Events --- // page -> JS, raw JSON string (the JS layer parses the $private payload). onMessage?: (message: string) => void; From 42893bd6d7f32f8a3bbf2b2cb1538ff82073f14c Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 17:53:51 +0800 Subject: [PATCH 05/21] fix(chart-webview): parent-checked forceDetach on pooled dispose (Android re-entry white-screen/stuck-loading) Pooled WebView re-parented across hosts left a stale parent on dispose, so re-entering a chart showed a white screen / infinite loading (issues 1 & 2). - forceDetach(): endViewTransition + removeView, fallback removeViewInLayout + requestLayout, so the parent is cleared synchronously even on a dead container. - detachFrom(): parent-checked, used by ChartWebview.dispose() for pooled hosts. - attachToContainer(): uses forceDetach, retries via container.post. Retains ChartDBG diagnostic counters (warmDriver/owner setters, throttled per-3s msgIn/native->page RATE, pauseIfIdle SKIP/PAUSE/RESUME) for ongoing on-device verification; to be removed before merge. --- .../nitro/chartwebview/ChartWebview.kt | 10 ++ .../nitro/chartwebview/PooledChartWebView.kt | 115 +++++++++++++++--- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt index fdd10024..0c0f79ae 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt @@ -49,6 +49,8 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp } private val instanceId = instanceIds.incrementAndGet() + // TEMP diagnostic id so the pool's owner/warmDriver logs can name which host. + val dbgId: Int get() = instanceId // Fabric lays its views out top-down and ignores layout requests from children // we add at runtime (the WebView / placeholder). Without forcing a measure + @@ -422,6 +424,14 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp // default (single-instance cache); the release path makes destroy() reachable. adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) } adoptedPoolKey = null + // Pooled host going away: release the shared WebView from THIS host's + // container, which is about to be dropped from the view tree. The pool keeps + // the WebView alive (warm) for the next host, but if we leave it parented to + // our dropped container the next host's attach can't clear that stale parent + // (removeView on a detached container isn't applied synchronously) and retries + // forever → blank/white chart. detachFrom is parent-checked, so if another host + // already re-claimed the WebView we leave it where it is. + if (entry != null && isPooled()) entry.detachFrom(container) // A private (non-pooled) instance is owned solely by this host — tear it down // so it doesn't leak a Chromium renderer + JavascriptInterface. if (entry != null && !isPooled()) entry.destroy() diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index da636fd0..5846cf21 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -52,6 +52,12 @@ class PooledChartWebView private constructor( const val ASSET_HOST = "appassets.androidplatform.net" private const val DEFAULT_ENTRY = "index.html" + // TEMP diagnostic tag (root-cause C1..C4 verification). All high-frequency + // signals are THROTTLED counters logged once per ~3s — never per-message — + // so this can't flood logcat / saturate the bridge like the earlier per-msg + // logging did. Read via: adb logcat -d -s ChartDBG. Remove once root-caused. + const val DBG = "ChartDBG" + // Tiny platform transport shim: defines the single hook the shared TS bridge // (CHART_BRIDGE_JS) calls. The bulk of the bridge lives in the TS layer and // arrives via the `bridgeScript` ctor arg — this is the only platform-specific @@ -148,12 +154,45 @@ class PooledChartWebView private constructor( } /** The host currently displaying this WebView; page events route here. */ - var owner: HybridChartWebview? = null + private var _owner: HybridChartWebview? = null + var owner: HybridChartWebview? + get() = _owner + set(value) { + if (_owner !== value) android.util.Log.i(DBG, "pool[$key] owner ${_owner?.dbgId} -> ${value?.dbgId}") + _owner = value + } // The host that warm-booted the page and can drive its symbol / receive its // callbacks while there is no VISIBLE owner yet. Separate from `owner` (the // YIELD path clears `owner`); callbacks fall back to this so bars-state / // load-end aren't dropped during warm. Mirror of the iOS warmDriver. - var warmDriver: HybridChartWebview? = null + private var _warmDriver: HybridChartWebview? = null + var warmDriver: HybridChartWebview? + get() = _warmDriver + set(value) { + if (_warmDriver !== value) android.util.Log.i(DBG, "pool[$key] warmDriver ${_warmDriver?.dbgId} -> ${value?.dbgId}") + _warmDriver = value + } + + // --- C1 diagnostic: throttled message-rate counters (JSI load). Incremented + // per message (cheap, no logging), flushed to ONE log line every ~3s by + // rateLogger. A high msgIn rate while NOT on a perps screen = the offscreen + // prewarm page is still pumping over JSI. + private val msgInCounter = AtomicInteger(0) + private val nativeToPageCounter = AtomicInteger(0) + private val rateHandler = Handler(Looper.getMainLooper()) + private val rateLogger = object : Runnable { + override fun run() { + val mi = msgInCounter.getAndSet(0) + val np = nativeToPageCounter.getAndSet(0) + if (mi > 0 || np > 0) { + android.util.Log.i( + DBG, + "pool[$key] RATE msgIn=$mi/3s native->page=$np/3s owner=${_owner?.dbgId} warm=${_warmDriver?.dbgId} paused=$paused", + ) + } + rateHandler.postDelayed(this, 3000) + } + } // PERF (Android only): Android's in-process WebView/Chromium does NOT throttle // an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm @@ -178,14 +217,21 @@ class PooledChartWebView private constructor( // until the first load so we never freeze a booting page. Resumed (+ redraw) // on the next CLAIM so the chart paints again. fun pauseIfIdle() { - if (paused || !hasLoadedOnce || owner != null) return + if (paused || !hasLoadedOnce || owner != null) { + // C4: if we SKIP because an owner still holds the shared page, the WebView + // keeps running (rAF/websocket) — i.e. it never idles while any host owns it. + android.util.Log.i(DBG, "pool[$key] pauseIfIdle SKIP paused=$paused loaded=$hasLoadedOnce owner=${_owner?.dbgId}") + return + } paused = true + android.util.Log.w(DBG, "pool[$key] PAUSE (renderer idle)") runOnUiThread { webView.onPause() } } fun resume() { if (!paused) return paused = false + android.util.Log.w(DBG, "pool[$key] RESUME") runOnUiThread { webView.onResume() webView.invalidate() @@ -243,6 +289,7 @@ class PooledChartWebView private constructor( init { val n = liveCount.incrementAndGet() android.util.Log.d("ChartWebviewPool", "WebView CREATED key=$key liveCount=$n") + rateHandler.postDelayed(rateLogger, 3000) webView.webViewClient = object : WebViewClientCompat() { override fun shouldInterceptRequest( @@ -279,19 +326,30 @@ class PooledChartWebView private constructor( private inner class ChartBridge { @JavascriptInterface fun postMessage(message: String) { + msgInCounter.incrementAndGet() // C1: throttled rate (flushed by rateLogger) runOnUiThread { val target = owner ?: warmDriver - if (target == null) { - android.util.Log.w( - "ChartWV", - "msg DROPPED (no owner/warmDriver): ${message.take(80)}", - ) - } target?.dispatchMessage(message) } } } + // Robustly detach the WebView from [parent], even when [parent] is a disposed + // host container that is mid-teardown / detached from the window and holds the + // child in a transition / disappearing-children list — where a plain removeView() + // is deferred and leaves webView.parent set, stranding the shared WebView and + // blanking the next chart slot. endViewTransition() clears any pending transition + // hold; removeViewInLayout() is the in-layout fallback if the child is still held. + private fun forceDetach(parent: ViewGroup) { + if (webView.parent !== parent) return + try { parent.endViewTransition(webView) } catch (e: Throwable) {} + parent.removeView(webView) + if (webView.parent === parent) { + try { parent.removeViewInLayout(webView) } catch (e: Throwable) {} + parent.requestLayout() + } + } + /** Move the WebView into [container], detaching it from any previous parent. */ fun attachTo(container: ViewGroup) { val generation = attachGeneration.incrementAndGet() @@ -304,16 +362,16 @@ class PooledChartWebView private constructor( if (generation != attachGeneration.get()) return if (webView.parent === container) return - (webView.parent as? ViewGroup)?.removeView(webView) + (webView.parent as? ViewGroup)?.let { forceDetach(it) } val currentParent = webView.parent - if (currentParent != null) { - // The old container hasn't released the WebView yet (it can be mid-layout / - // mid-teardown when the previous host unmounts). Retry on the next frame - // instead of giving up after one attempt — a single retry was not enough and - // left the WebView stranded in the old (now offscreen) container, so the new - // chart screen showed a blank/white slot. + if (currentParent != null && currentParent !== container) { + // The old container still hasn't released the WebView (a disposed host's + // container mid-teardown / detached from window: removeView is deferred and + // getParent() stays set). Retry — but on the TARGET container's handler, + // which is attached to the window so its queue keeps draining, instead of the + // detached webView/old parent whose post() runnables may never run. if (retriesLeft > 0) { - webView.post { + container.post { attachToContainer(container, generation, retriesLeft = retriesLeft - 1) } } else { @@ -338,6 +396,27 @@ class PooledChartWebView private constructor( refreshSnapshotSoon() } + /** + * Remove the WebView from [container] ONLY if it is still parented there. + * + * Called on a pooled host's final teardown (dispose): that host's container is + * about to be dropped from the view tree, and a pooled host deliberately does + * NOT detach on the YIELD path (to avoid racing a rapid re-claim). Without this, + * the shared WebView stays parented to the dropped/detached container; the next + * host's [attachTo] then keeps hitting "old parent not cleared" (removeView on a + * detached container isn't applied synchronously) and retries forever — leaving + * the new chart slot blank/white. The `parent === container` guard makes this + * safe against ordering: if a new host has already re-claimed and reparented the + * WebView, we must NOT rip it back off, so we only remove when WE still hold it. + */ + fun detachFrom(container: ViewGroup) { + runOnUiThread { + if (webView.parent === container) { + forceDetach(container) + } + } + } + /** Remove the WebView from its current parent (keeps it alive, warm). */ fun detachFromParent() { runOnUiThread { @@ -488,6 +567,7 @@ class PooledChartWebView private constructor( } fun postMessage(message: String) { + nativeToPageCounter.incrementAndGet() // C1: throttled rate (flushed by rateLogger) val payload = JSONObject.quote(message) val js = "(function(){try{window.postMessage(JSON.parse($payload), '*');}" + @@ -506,6 +586,7 @@ class PooledChartWebView private constructor( * leaking a Chromium renderer + JavascriptInterface. */ fun destroy() { + rateHandler.removeCallbacks(rateLogger) runOnUiThread { bridgeScriptHandler?.remove() bridgeScriptHandler = null From ade2ccdf5a2066950ff08fe66805f89a08471115 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 18:12:06 +0800 Subject: [PATCH 06/21] chore(chart-webview): drop debug instrumentation, keep necessary native logs Remove the ChartDBG diagnostic layer added for the Android root-cause work: - DBG tag, owner/warmDriver transition logs, dbgId getter - throttled msgIn/native->page rate counters + rateLogger handler - pauseIfIdle SKIP verbose log Keep only operational logs under the ChartWebviewPool tag: WebView CREATED/DESTROYED (singleton verification), renderer PAUSE/RESUME, and the attach-failed-after-retries warning. Logging-only change; behavior unchanged. --- .../nitro/chartwebview/ChartWebview.kt | 2 - .../nitro/chartwebview/PooledChartWebView.kt | 47 +++---------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt index 0c0f79ae..f30c5dce 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt @@ -49,8 +49,6 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp } private val instanceId = instanceIds.incrementAndGet() - // TEMP diagnostic id so the pool's owner/warmDriver logs can name which host. - val dbgId: Int get() = instanceId // Fabric lays its views out top-down and ignores layout requests from children // we add at runtime (the WebView / placeholder). Without forcing a measure + diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index 5846cf21..333d3008 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -52,11 +52,8 @@ class PooledChartWebView private constructor( const val ASSET_HOST = "appassets.androidplatform.net" private const val DEFAULT_ENTRY = "index.html" - // TEMP diagnostic tag (root-cause C1..C4 verification). All high-frequency - // signals are THROTTLED counters logged once per ~3s — never per-message — - // so this can't flood logcat / saturate the bridge like the earlier per-msg - // logging did. Read via: adb logcat -d -s ChartDBG. Remove once root-caused. - const val DBG = "ChartDBG" + // Operational log tag for this pool (lifecycle + error paths only). + private const val TAG = "ChartWebviewPool" // Tiny platform transport shim: defines the single hook the shared TS bridge // (CHART_BRIDGE_JS) calls. The bulk of the bridge lives in the TS layer and @@ -158,7 +155,6 @@ class PooledChartWebView private constructor( var owner: HybridChartWebview? get() = _owner set(value) { - if (_owner !== value) android.util.Log.i(DBG, "pool[$key] owner ${_owner?.dbgId} -> ${value?.dbgId}") _owner = value } // The host that warm-booted the page and can drive its symbol / receive its @@ -169,31 +165,9 @@ class PooledChartWebView private constructor( var warmDriver: HybridChartWebview? get() = _warmDriver set(value) { - if (_warmDriver !== value) android.util.Log.i(DBG, "pool[$key] warmDriver ${_warmDriver?.dbgId} -> ${value?.dbgId}") _warmDriver = value } - // --- C1 diagnostic: throttled message-rate counters (JSI load). Incremented - // per message (cheap, no logging), flushed to ONE log line every ~3s by - // rateLogger. A high msgIn rate while NOT on a perps screen = the offscreen - // prewarm page is still pumping over JSI. - private val msgInCounter = AtomicInteger(0) - private val nativeToPageCounter = AtomicInteger(0) - private val rateHandler = Handler(Looper.getMainLooper()) - private val rateLogger = object : Runnable { - override fun run() { - val mi = msgInCounter.getAndSet(0) - val np = nativeToPageCounter.getAndSet(0) - if (mi > 0 || np > 0) { - android.util.Log.i( - DBG, - "pool[$key] RATE msgIn=$mi/3s native->page=$np/3s owner=${_owner?.dbgId} warm=${_warmDriver?.dbgId} paused=$paused", - ) - } - rateHandler.postDelayed(this, 3000) - } - } - // PERF (Android only): Android's in-process WebView/Chromium does NOT throttle // an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm // pooled page keeps its rAF render loop + websockets + compositing alive @@ -218,20 +192,17 @@ class PooledChartWebView private constructor( // on the next CLAIM so the chart paints again. fun pauseIfIdle() { if (paused || !hasLoadedOnce || owner != null) { - // C4: if we SKIP because an owner still holds the shared page, the WebView - // keeps running (rAF/websocket) — i.e. it never idles while any host owns it. - android.util.Log.i(DBG, "pool[$key] pauseIfIdle SKIP paused=$paused loaded=$hasLoadedOnce owner=${_owner?.dbgId}") return } paused = true - android.util.Log.w(DBG, "pool[$key] PAUSE (renderer idle)") + android.util.Log.i(TAG, "pool[$key] PAUSE (renderer idle)") runOnUiThread { webView.onPause() } } fun resume() { if (!paused) return paused = false - android.util.Log.w(DBG, "pool[$key] RESUME") + android.util.Log.i(TAG, "pool[$key] RESUME") runOnUiThread { webView.onResume() webView.invalidate() @@ -288,8 +259,7 @@ class PooledChartWebView private constructor( init { val n = liveCount.incrementAndGet() - android.util.Log.d("ChartWebviewPool", "WebView CREATED key=$key liveCount=$n") - rateHandler.postDelayed(rateLogger, 3000) + android.util.Log.d(TAG, "WebView CREATED key=$key liveCount=$n") webView.webViewClient = object : WebViewClientCompat() { override fun shouldInterceptRequest( @@ -326,7 +296,6 @@ class PooledChartWebView private constructor( private inner class ChartBridge { @JavascriptInterface fun postMessage(message: String) { - msgInCounter.incrementAndGet() // C1: throttled rate (flushed by rateLogger) runOnUiThread { val target = owner ?: warmDriver target?.dispatchMessage(message) @@ -376,7 +345,7 @@ class PooledChartWebView private constructor( } } else { android.util.Log.w( - "ChartWebviewPool", + TAG, "Skip attach key=$key after retries because WebView parent was not cleared: $currentParent", ) } @@ -567,7 +536,6 @@ class PooledChartWebView private constructor( } fun postMessage(message: String) { - nativeToPageCounter.incrementAndGet() // C1: throttled rate (flushed by rateLogger) val payload = JSONObject.quote(message) val js = "(function(){try{window.postMessage(JSON.parse($payload), '*');}" + @@ -586,7 +554,6 @@ class PooledChartWebView private constructor( * leaking a Chromium renderer + JavascriptInterface. */ fun destroy() { - rateHandler.removeCallbacks(rateLogger) runOnUiThread { bridgeScriptHandler?.remove() bridgeScriptHandler = null @@ -596,7 +563,7 @@ class PooledChartWebView private constructor( (webView.parent as? ViewGroup)?.removeView(webView) webView.destroy() val n = liveCount.decrementAndGet() - android.util.Log.d("ChartWebviewPool", "WebView DESTROYED key=$key liveCount=$n") + android.util.Log.d(TAG, "WebView DESTROYED key=$key liveCount=$n") } } From 28c09b078cbe8d5b9a8495839f0313d7852d9219 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 18:16:51 +0800 Subject: [PATCH 07/21] chore: bump version to 3.0.56 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 265210e6..1932a968 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index 58d3ea96..bf301cf5 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index 3457515f..9268f1d6 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index feec63b3..3cafb8be 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index d061053a..3982128e 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index d49d6a6d..9b63de3d 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 7c9e9034..3aed790b 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 90b58c5d..60ced134 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index b81020f5..5181669d 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index fc2cbad2..6422a3cc 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index cfdef602..72bf9922 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 09d9bb3a..42d19d9c 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 679de689..3a895a5d 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index e0a123b3..bcd391d7 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 2ee0fad6..0108f7e3 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.55", + "version": "3.0.56", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index a4de7a6d..711205ef 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 75e171e5..3b62f5f4 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index a9fe572a..da32f249 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 63619441..290cb9d7 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 87410d50..94a7d90c 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index bb26578d..e655fee1 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index dc507607..6a5c8d48 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 71316ed9..34040312 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 28e8541b..978b8f79 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 5e5a5517..a6366fc3 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index 384ddd2b..686d0ab3 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.55", + "version": "3.0.56", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 0fb590bb..2ad67c43 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 0a7c578f..7831366d 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.55", + "version": "3.0.56", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index 0c957733..c988d25c 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 2ff6d24f..cc2b585a 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.55", + "version": "3.0.56", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index fac1b08b..7132e33a 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index a60900c0..80a44605 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.55", + "version": "3.0.56", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index a15816bd..d3878b3e 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.55", + "version": "3.0.56", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From e27735d38eefaed1301dd454045555f3ff364855 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 19:42:01 +0800 Subject: [PATCH 08/21] feat: add configurable android assetHost for chart webview --- .../nitro/chartwebview/ChartWebview.kt | 17 ++++-- .../nitro/chartwebview/PooledChartWebView.kt | 56 +++++++++++++++---- .../ios/ChartWebview.swift | 5 ++ .../src/ChartWebview.nitro.ts | 8 +++ 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt index f30c5dce..ea128d81 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt @@ -123,6 +123,15 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp get() = _paramsJson set(value) { _paramsJson = value; applySource() } + // ANDROID: the host the offline `localBundle` is served under. When set, the + // offline bundle is served from https:/// so the WebView reuses the + // real chart origin's same-origin storage (zero migration). Falls back to the + // built-in appassets host in the pool when null. (iOS ignores this prop.) + private var _assetHost: String? = null + override var assetHost: String? + get() = _assetHost + set(value) { _assetHost = value; applySource() } + // Document-start bridge JS (single source of truth in the TS layer). Stored and // handed to the pooled WebView when we claim it, before its first load. private var _bridgeScript: String? = null @@ -259,7 +268,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp if (!bs.isNullOrEmpty()) { entry.setBridgeScript(bs) entry.warmDriver = this - entry.setSource(_uri, _localBundle, _entry, _paramsJson) + entry.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost) } if (wantsOwnership()) { entry.owner = this @@ -299,7 +308,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp pooled.owner = this pooled.setBridgeScript(_bridgeScript ?: "") pooled.attachTo(container) - pooled.setSource(_uri, _localBundle, _entry, _paramsJson) + pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost) } // Apply the source synchronously when a source/bridge prop changes. For a @@ -315,10 +324,10 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp if (isPooled() && !bs.isNullOrEmpty()) { pooled.setBridgeScript(bs) pooled.warmDriver = this - pooled.setSource(_uri, _localBundle, _entry, _paramsJson) + pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost) } else if (pooled.owner == this) { pooled.setBridgeScript(_bridgeScript ?: "") - pooled.setSource(_uri, _localBundle, _entry, _paramsJson) + pooled.setSource(_uri, _localBundle, _entry, _paramsJson, _assetHost) } } diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index 333d3008..bbc80d13 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -126,12 +126,12 @@ class PooledChartWebView private constructor( } // The trusted origin set for the current source: the offline asset origin - // (https://appassets.androidplatform.net) always, plus the online/fallback - // `uri` origin when running in online mode. computeTargetUrl serves offline - // content from ASSET_HOST and online content from `uri`, so these are exactly - // the origins the privileged bridge legitimately runs in. + // (https://, default appassets.androidplatform.net) always, plus the + // online/fallback `uri` origin when running in online mode. computeTargetUrl + // serves offline content from `assetHost` and online content from `uri`, so + // these are exactly the origins the privileged bridge legitimately runs in. private fun trustedOriginsFor(uri: String?): Set { - val origins = linkedSetOf("https://$ASSET_HOST") + val origins = linkedSetOf("https://$assetHost") originOf(uri)?.let { origins.add(it) } return origins } @@ -212,6 +212,13 @@ class PooledChartWebView private constructor( private var assetLoader: WebViewAssetLoader? = null private var lastLoadedUrl: String? = null private var lastLocalBundle: String? = null + // The host the offline bundle is served under. Defaults to the built-in + // appassets host (old behavior); when the app passes `assetHost` we serve the + // bundle under the real chart origin so the WebView reuses that origin's + // same-origin storage (zero migration). Tracked so an assetHost change rebuilds + // the asset loader + recomputes the target URL, same as a localBundle change. + private var assetHost: String = ASSET_HOST + private var lastAssetHost: String = ASSET_HOST // Last rendered frame, used to mask the brief blank frame while the WebView's // surface is torn down and recreated during a reparent (move). Kept until @@ -515,13 +522,23 @@ class PooledChartWebView private constructor( * so reparenting / redundant prop re-applies never reload (which would lose * chart state, the whole point of pooling). */ - fun setSource(uri: String?, localBundle: String?, entry: String?, paramsJson: String?) { + fun setSource( + uri: String?, + localBundle: String?, + entry: String?, + paramsJson: String?, + assetHost: String?, + ) { // Never load before the document-start bridge is registered — otherwise the // page boots without the bridge and its first $private requests are lost. The // host re-calls setSource once the bridgeScript prop arrives. if (!bridgeRegistered) return - if (localBundle != lastLocalBundle) { + // Per-instance asset host: fall back to the built-in appassets host (old + // behavior) when the app doesn't pass one. Empty string is treated as absent. + this.assetHost = assetHost?.takeIf { it.isNotEmpty() } ?: ASSET_HOST + if (localBundle != lastLocalBundle || this.assetHost != lastAssetHost) { lastLocalBundle = localBundle + lastAssetHost = this.assetHost rebuildAssetLoader(localBundle) } // Register the document-start bridge scoped to exactly the origin(s) this load @@ -572,9 +589,28 @@ class PooledChartWebView private constructor( assetLoader = null return } + // Narrow the path handler to ONLY the offline bundle subtree + // (//, e.g. /tradingview-assets/) instead of intercepting all of + // "/". When the bundle is served under the real chart origin (`assetHost`), + // intercepting "/" would shadow every web path on that domain; with the narrow + // handler, unmatched paths fall through to the network — so an `assetHost` + // that is a real public domain keeps behaving normally off-bundle. + // computeTargetUrl produces https:////, which + // stays inside this handler's path. (Path built as // so a + // localBundle that already has a trailing slash doesn't double it.) + val bundleDir = localBundle.trim('/') + val bundlePath = "/$bundleDir/" + // WebViewAssetLoader strips the registered prefix before calling the handler, + // so AssetsPathHandler alone would resolve the remainder against the assets + // ROOT (assets/) and lose the bundle namespace. Re-prepend / + // so we still serve from assets//. Returning null on a + // miss lets WebViewAssetLoader fall through to the network. + val assets = WebViewAssetLoader.AssetsPathHandler(webView.context) + val namespacedHandler = + WebViewAssetLoader.PathHandler { suffix -> assets.handle("$bundleDir/$suffix") } assetLoader = WebViewAssetLoader.Builder() - .setDomain(ASSET_HOST) - .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(webView.context)) + .setDomain(assetHost) + .addPathHandler(bundlePath, namespacedHandler) .build() } @@ -590,7 +626,7 @@ class PooledChartWebView private constructor( val query = buildQueryFromParamsJson(paramsJson) return buildString { append("https://") - append(ASSET_HOST) + append(assetHost) append('/') // Serve the offline bundle from assets// (namespaced) rather // than the assets root, so it doesn't collide with other bundled assets diff --git a/native-views/react-native-chart-webview/ios/ChartWebview.swift b/native-views/react-native-chart-webview/ios/ChartWebview.swift index bcd0423d..06804c2a 100644 --- a/native-views/react-native-chart-webview/ios/ChartWebview.swift +++ b/native-views/react-native-chart-webview/ios/ChartWebview.swift @@ -129,6 +129,11 @@ class HybridChartWebview: HybridChartWebviewSpec { var localBundle: String? { didSet { applySource() } } var entry: String? { didSet { applySource() } } var paramsJson: String? { didSet { applySource() } } + // ANDROID ONLY. iOS serves offline content from the onekey-chart:// custom + // scheme (https can't be intercepted here), so this host stores the prop to + // satisfy the generated spec but never reads it — offline origin/behavior is + // unchanged on iOS. + var assetHost: String? // Document-start bridge JS (single source of truth in the TS layer). Handed to // the pooled WebView when we claim it, before its first load. diff --git a/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts b/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts index 508537f2..7732eed4 100644 --- a/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts +++ b/native-views/react-native-chart-webview/src/ChartWebview.nitro.ts @@ -20,6 +20,14 @@ export interface ChartWebviewProps extends HybridViewProps { // '{"symbol":"BTC","type":"market","theme":"dark"}'). Kept as a string so the // native side stays free of a structured-prop dependency. paramsJson?: string; + // ANDROID ONLY: the host the offline `localBundle` is served under, so Android + // can reuse the real TradingView origin (e.g. "tradingview.onekey.so") and read + // the existing same-origin WebView storage with zero migration. When omitted, + // Android falls back to the built-in "appassets.androidplatform.net" host + // (identical to the old behavior). IGNORED on iOS, which always serves offline + // content from the onekey-chart:// custom scheme (https can't be intercepted + // there). + assetHost?: string; // JS injected into the page at document-start (before any page JS), wiring the // page's outbound channels to the native transport. Single source of truth in From d32b317a400cf3efe602aab7a0ebb57db7d18a85 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 19:43:39 +0800 Subject: [PATCH 09/21] chore: bump version to 3.0.57 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 1932a968..f0b25ef7 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index bf301cf5..21a19c81 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index 9268f1d6..9c6a5a6a 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index 3cafb8be..4b8dd136 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 3982128e..89d5e9f2 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index 9b63de3d..84d3df9c 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 3aed790b..67e22006 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 60ced134..8edb6918 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index 5181669d..50f5e344 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 6422a3cc..9c746791 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 72bf9922..2ad1bb9a 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 42d19d9c..171c331a 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 3a895a5d..a37e24a6 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index bcd391d7..a538135d 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 0108f7e3..2687456a 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.56", + "version": "3.0.57", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 711205ef..751d0304 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 3b62f5f4..9fb15698 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index da32f249..8979bc25 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 290cb9d7..f520cadb 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 94a7d90c..88c77ce1 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index e655fee1..b15955cb 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 6a5c8d48..5d5411e8 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 34040312..767a3d68 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 978b8f79..0e6b5f46 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index a6366fc3..f43ed6ea 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index 686d0ab3..bb71ed51 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.56", + "version": "3.0.57", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 2ad67c43..67be982e 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 7831366d..711bd766 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.56", + "version": "3.0.57", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index c988d25c..e87bb30a 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index cc2b585a..92a27cbd 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.56", + "version": "3.0.57", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 7132e33a..af2461e3 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 80a44605..50a7810e 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.56", + "version": "3.0.57", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index d3878b3e..4c7cdd73 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.56", + "version": "3.0.57", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From 249167b1455ccf4d052fea2c763d23ed06477b78 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 22:04:48 +0800 Subject: [PATCH 10/21] fix(chart-webview): harden Android pooled WebView reuse & assetHost Address code-review findings on the Android pooled chart WebView: - weak owner/warmDriver refs (mirror iOS) so the immortal pool can't pin a disposed host's ReactContext/Activity - validate assetHost to a bare hostname before it becomes the privileged-bridge origin / WebViewAssetLoader.setDomain; fall back to the built-in host on malformed input - normalize localBundle (trim '/') in computeTargetUrl to avoid double-slash URLs that miss the handler prefix (blank chart) - attach retry uses postDelayed + last-resort forceDetach before giving up, so a stuck old parent can't leave the WebView unparented - log forceDetach failures instead of swallowing them - demote per-switch pause/resume logs to debug --- .../nitro/chartwebview/PooledChartWebView.kt | 101 ++++++++++++++---- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt index bbc80d13..25939529 100644 --- a/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +++ b/native-views/react-native-chart-webview/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt @@ -25,6 +25,7 @@ import androidx.webkit.WebViewClientCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import org.json.JSONObject +import java.lang.ref.WeakReference import java.net.URLEncoder import java.util.concurrent.atomic.AtomicInteger @@ -151,21 +152,25 @@ class PooledChartWebView private constructor( } /** The host currently displaying this WebView; page events route here. */ - private var _owner: HybridChartWebview? = null + // Weak (mirror of iOS): the pool entry is immortal, so a strong ref here would + // pin a disposed host — and its ReactContext/Activity — forever. A GC'd host + // reads back as null, which makes `entry.owner == this` correctly false. + private var _ownerRef: WeakReference? = null var owner: HybridChartWebview? - get() = _owner + get() = _ownerRef?.get() set(value) { - _owner = value + _ownerRef = value?.let { WeakReference(it) } } // The host that warm-booted the page and can drive its symbol / receive its // callbacks while there is no VISIBLE owner yet. Separate from `owner` (the // YIELD path clears `owner`); callbacks fall back to this so bars-state / // load-end aren't dropped during warm. Mirror of the iOS warmDriver. - private var _warmDriver: HybridChartWebview? = null + // Weak for the same immortal-pool reason as `owner` above. + private var _warmDriverRef: WeakReference? = null var warmDriver: HybridChartWebview? - get() = _warmDriver + get() = _warmDriverRef?.get() set(value) { - _warmDriver = value + _warmDriverRef = value?.let { WeakReference(it) } } // PERF (Android only): Android's in-process WebView/Chromium does NOT throttle @@ -195,14 +200,14 @@ class PooledChartWebView private constructor( return } paused = true - android.util.Log.i(TAG, "pool[$key] PAUSE (renderer idle)") + android.util.Log.d(TAG, "pool[$key] PAUSE (renderer idle)") runOnUiThread { webView.onPause() } } fun resume() { if (!paused) return paused = false - android.util.Log.i(TAG, "pool[$key] RESUME") + android.util.Log.d(TAG, "pool[$key] RESUME") runOnUiThread { webView.onResume() webView.invalidate() @@ -233,6 +238,9 @@ class PooledChartWebView private constructor( // How long the snapshot overlay stays up after a reparent, giving the WebView // time to draw its first frame in the new container (~5 frames). private val overlayHideDelayMs = 80L + // Delay between bounded attach retries (~1 vsync at 60Hz): long enough for the + // old parent's pending layout / removeView to flush, short enough to stay snappy. + private val attachRetryDelayMs = 16L private val attachGeneration = AtomicInteger(0) val webView: WebView = WebView(context).apply { @@ -318,10 +326,18 @@ class PooledChartWebView private constructor( // hold; removeViewInLayout() is the in-layout fallback if the child is still held. private fun forceDetach(parent: ViewGroup) { if (webView.parent !== parent) return - try { parent.endViewTransition(webView) } catch (e: Throwable) {} + try { + parent.endViewTransition(webView) + } catch (e: Throwable) { + android.util.Log.w(TAG, "forceDetach: endViewTransition failed", e) + } parent.removeView(webView) if (webView.parent === parent) { - try { parent.removeViewInLayout(webView) } catch (e: Throwable) {} + try { + parent.removeViewInLayout(webView) + } catch (e: Throwable) { + android.util.Log.w(TAG, "forceDetach: removeViewInLayout failed", e) + } parent.requestLayout() } } @@ -347,14 +363,25 @@ class PooledChartWebView private constructor( // which is attached to the window so its queue keeps draining, instead of the // detached webView/old parent whose post() runnables may never run. if (retriesLeft > 0) { - container.post { + // postDelayed (not a tight container.post spin): a small delay lets the + // old parent's pending layout / removeView flush between attempts, instead + // of re-checking on the very next vsync before anything could change. + container.postDelayed({ attachToContainer(container, generation, retriesLeft = retriesLeft - 1) - } + }, attachRetryDelayMs) } else { - android.util.Log.w( - TAG, - "Skip attach key=$key after retries because WebView parent was not cleared: $currentParent", - ) + // Last resort before giving up: the old parent never released the WebView + // through the normal path. Force-detach it and try the attach once more so + // we don't leave the WebView unparented (the blank-chart symptom). + (currentParent as? ViewGroup)?.let { forceDetach(it) } + if (webView.parent == null || webView.parent === container) { + attachToContainer(container, generation, retriesLeft = 0) + } else { + android.util.Log.w( + TAG, + "Skip attach key=$key after retries because WebView parent was not cleared: $currentParent", + ) + } } return } @@ -535,7 +562,10 @@ class PooledChartWebView private constructor( if (!bridgeRegistered) return // Per-instance asset host: fall back to the built-in appassets host (old // behavior) when the app doesn't pass one. Empty string is treated as absent. - this.assetHost = assetHost?.takeIf { it.isNotEmpty() } ?: ASSET_HOST + // Sanitize untrusted values before they reach the privileged-bridge origin + // ("https://$assetHost") and WebViewAssetLoader.setDomain — see sanitizeAssetHost. + this.assetHost = assetHost?.takeIf { it.isNotEmpty() } + ?.let { sanitizeAssetHost(it) } ?: ASSET_HOST if (localBundle != lastLocalBundle || this.assetHost != lastAssetHost) { lastLocalBundle = localBundle lastAssetHost = this.assetHost @@ -584,6 +614,35 @@ class PooledChartWebView private constructor( } } + // Validate an incoming assetHost prop before it becomes the trusted bridge + // origin ("https://$assetHost") and WebViewAssetLoader.setDomain(assetHost). A + // malformed value (scheme, path, '/', '@'/userinfo, whitespace, port, query) + // could corrupt the privileged-origin allowlist or throw on the UI thread, so a + // candidate that is not a bare hostname falls back to the built-in ASSET_HOST. + private fun sanitizeAssetHost(candidate: String): String { + val invalid = { + android.util.Log.w( + TAG, + "Ignoring invalid assetHost '$candidate'; falling back to default $ASSET_HOST", + ) + ASSET_HOST + } + // Cheap rejects first: a bare hostname has no whitespace and no '/'. + if (candidate.any { it.isWhitespace() } || candidate.contains('/')) return invalid() + return try { + val uri = java.net.URI("https://$candidate") + val bareHost = + uri.host == candidate && + uri.path.isNullOrEmpty() && + uri.userInfo == null && + uri.query == null && + uri.port == -1 + if (bareHost) candidate else invalid() + } catch (e: Exception) { + invalid() + } + } + private fun rebuildAssetLoader(localBundle: String?) { if (localBundle.isNullOrEmpty()) { assetLoader = null @@ -624,6 +683,12 @@ class PooledChartWebView private constructor( if (localBundle.isNullOrEmpty()) return null val entryPath = entry?.takeIf { it.isNotEmpty() } ?: DEFAULT_ENTRY val query = buildQueryFromParamsJson(paramsJson) + // Normalize the bundle dir the SAME way rebuildAssetLoader does (trim '/'), so + // the URL has exactly single slashes between host, bundle dir and entry. A raw + // localBundle like "/tradingview-assets/" would otherwise yield + // https://host//tradingview-assets//index.html, which misses the registered + // handler prefix → falls through to the network → blank chart. + val bundleDir = localBundle.trim('/') return buildString { append("https://") append(assetHost) @@ -632,7 +697,7 @@ class PooledChartWebView private constructor( // than the assets root, so it doesn't collide with other bundled assets // (e.g. web-embed). The dist uses relative asset paths (PUBLIC_URL='./'), // so loading the entry from a subpath resolves the rest correctly. - append(localBundle) + append(bundleDir) append('/') append(entryPath) if (query.isNotEmpty()) { From 79679012cc419f4624e0ca1fbf6b45346897f975 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 22:04:58 +0800 Subject: [PATCH 11/21] fix(device-utils): make iOS cold-start notification take atomic - guard the read-and-clear of coldStartLocalNotification with an os_unfair_lock so a concurrent get-and-clear can't double-drain the deep-link (the exactly-once invariant the method exists to provide) - add a presence-only log on the take path for field diagnosis (never logs the payload) --- .../ios/ReactNativeDeviceUtils.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift index 620793f8..0ee36139 100644 --- a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift +++ b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift @@ -1,6 +1,7 @@ import NitroModules import UIKit import ReactNativeNativeLogger +import os @objcMembers public class LaunchOptionsStore: NSObject { @@ -34,8 +35,20 @@ public class LaunchOptionsStore: NSObject { private static let deviceTokenKey = "1k_device_token" - // Read-once: hand the payload to JS exactly once per launch. + // Serializes access to `coldStartLocalNotification`. The getter resolves on a + // nitro background thread (Promise.async) while AppDelegate writes the slot on + // the main thread during launch, so the read-and-clear below must be one + // critical section to avoid a double-drain race (delivering the deep-link + // twice) and memory-visibility issues. NOTE: AppDelegate writes via KVC + // (`setValue(_:forKey:)`), which bypasses this lock, so we cannot fully + // synchronize the writer from here — we protect the take side as best we can. + private var coldStartLock = os_unfair_lock() + + // Read-once: hand the payload to JS exactly once per launch. The read+clear is + // performed atomically under `coldStartLock`. public func takeColdStartLocalNotification() -> String { + os_unfair_lock_lock(&coldStartLock) + defer { os_unfair_lock_unlock(&coldStartLock) } let value = coldStartLocalNotification ?? "" coldStartLocalNotification = nil return value @@ -209,7 +222,10 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { func getAndClearColdStartLocalNotification() throws -> Promise { return Promise.async { - return LaunchOptionsStore.shared.takeColdStartLocalNotification() + let value = LaunchOptionsStore.shared.takeColdStartLocalNotification() + // Presence only — never log the payload (it's a deep-link target / PII). + OneKeyLog.info("DeviceUtils", "getAndClearColdStartLocalNotification: hasPayload=\(!value.isEmpty)") + return value } } From c6e247bf01871bdd39632ce4268ea70bef1201e4 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 10 Jun 2026 22:07:25 +0800 Subject: [PATCH 12/21] chore: bump version to 3.0.58 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index f0b25ef7..e17240ea 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index 21a19c81..34360aa0 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index 9c6a5a6a..57f172f4 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index 4b8dd136..6b4757c9 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 89d5e9f2..dc145003 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index 84d3df9c..7b877b5b 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 67e22006..f7ebba51 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 8edb6918..b7e12558 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index 50f5e344..becfb94c 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 9c746791..6cd9ab58 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 2ad1bb9a..2f5a49e8 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 171c331a..332c9804 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index a37e24a6..68b80481 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index a538135d..5d593b0e 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 2687456a..40939ad9 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.57", + "version": "3.0.58", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 751d0304..4de5b89b 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 9fb15698..030c14c8 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index 8979bc25..ead81834 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index f520cadb..528df116 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 88c77ce1..e443c19d 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index b15955cb..49c2c667 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 5d5411e8..4f055791 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 767a3d68..8d7fcb17 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 0e6b5f46..d039320f 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index f43ed6ea..a138a0f7 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index bb71ed51..b4973a35 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.57", + "version": "3.0.58", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 67be982e..df8711e0 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 711bd766..086b09e2 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.57", + "version": "3.0.58", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index e87bb30a..c214b24b 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 92a27cbd..30643f12 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.57", + "version": "3.0.58", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index af2461e3..db890a60 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 50a7810e..5a852d40 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.57", + "version": "3.0.58", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index 4c7cdd73..ed373658 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.57", + "version": "3.0.58", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From b86fa5d806db133f93f965ceb3bb48876ac15d19 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 11 Jun 2026 01:46:16 +0800 Subject: [PATCH 13/21] Add cache pruning APIs for APKs and bundles Introduce safe cleanup routines for APK and bundle artifacts across platforms: add clearApkCache (Android implementation + iOS no-op) and expose it in the Nitro spec; introduce pruneStaleAppVersionBundles on Android and iOS to remove stale onekey-bundle artifacts (extracted dirs, download stages, orphan asc signatures, and stale fallback entries). Implementations are defensive: tolerate missing files, avoid deleting current app/bundle versions, and log progress; bundle pruning returns the count of deleted version directories. These changes help reclaim disk space and avoid leaked stale artifacts after native upgrades. --- .../ReactNativeAppUpdate.kt | 74 +++++++--- .../ios/ReactNativeAppUpdate.swift | 7 + .../src/ReactNativeAppUpdate.nitro.ts | 4 + .../ReactNativeBundleUpdate.kt | 135 ++++++++++++++++++ .../ios/ReactNativeBundleUpdate.swift | 124 ++++++++++++++++ .../src/ReactNativeBundleUpdate.nitro.ts | 4 + 6 files changed, 325 insertions(+), 23 deletions(-) diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt index d87b7938..fb8b5112 100644 --- a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt @@ -1191,6 +1191,44 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ return result } + /** + * Delete every file in cacheDir/apks/ (.apk / .partial / .progress / + * .SHA256SUMS.asc). Tolerates a missing directory and per-file delete + * failures (e.g. the OS reclaimed the cache mid-pass — see + * verifyExistingApk's Indeterminate handling for the same defensive + * stance). Pure file I/O: does NOT touch download/verification state, so + * it is safe to share between clearCache (full reset) and clearApkCache + * (JS-gated, no in-flight download to cancel). The [tag] flows into the + * log lines so the two callers stay distinguishable in logcat. + */ + private fun wipeApkCacheFiles(tag: String) { + val context = NitroModules.applicationContext + if (context == null) { + OneKeyLog.warn("AppUpdate", "$tag: application context unavailable, skipping file cleanup") + return + } + val apkDir = File(context.cacheDir, "apks") + if (!apkDir.exists()) { + OneKeyLog.info("AppUpdate", "$tag: apks cache directory does not exist, nothing to clean") + return + } + val filesToDelete = apkDir.listFiles() ?: emptyArray() + OneKeyLog.info("AppUpdate", "$tag: found ${filesToDelete.size} cached file(s) to delete in ${apkDir.absolutePath}") + var deletedCount = 0 + filesToDelete.forEach { file -> + val size = file.length() + // The file may already be gone (concurrent OS cache reclaim); + // treat a non-existent file as nothing to do, not a failure. + if (!file.exists() || file.delete()) { + OneKeyLog.debug("AppUpdate", "$tag: deleted ${file.name} (${size} bytes)") + deletedCount++ + } else { + OneKeyLog.warn("AppUpdate", "$tag: failed to delete ${file.name}") + } + } + OneKeyLog.info("AppUpdate", "$tag: completed, deleted $deletedCount/${filesToDelete.size} files") + } + override fun clearCache(): Promise { return Promise.async { OneKeyLog.info("AppUpdate", "clearCache: starting cleanup...") @@ -1200,29 +1238,19 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ OneKeyLog.info("AppUpdate", "clearCache: reset download state, cleared $verifiedCount verified file entries") // Clean up downloaded APK and ASC files from cacheDir/apks/ directory - val context = NitroModules.applicationContext - if (context != null) { - val apkDir = File(context.cacheDir, "apks") - if (apkDir.exists()) { - val filesToDelete = apkDir.listFiles() ?: emptyArray() - OneKeyLog.info("AppUpdate", "clearCache: found ${filesToDelete.size} cached file(s) to delete in ${apkDir.absolutePath}") - var deletedCount = 0 - filesToDelete.forEach { file -> - val size = file.length() - if (file.delete()) { - OneKeyLog.debug("AppUpdate", "clearCache: deleted ${file.name} (${size} bytes)") - deletedCount++ - } else { - OneKeyLog.warn("AppUpdate", "clearCache: failed to delete ${file.name}") - } - } - OneKeyLog.info("AppUpdate", "clearCache: completed, deleted $deletedCount/${filesToDelete.size} files") - } else { - OneKeyLog.info("AppUpdate", "clearCache: apks cache directory does not exist, nothing to clean") - } - } else { - OneKeyLog.warn("AppUpdate", "clearCache: application context unavailable, skipping file cleanup") - } + wipeApkCacheFiles("clearCache") + } + } + + override fun clearApkCache(): Promise { + return Promise.async { + // Wipe downloaded APK artifacts from cacheDir/apks/ only. The JS + // layer gates on app-update status before calling, so there is no + // in-flight download to cancel here: deliberately do NOT touch + // isDownloading / verifiedFiles (that's clearCache's job). Missing + // dir/files are tolerated. + OneKeyLog.info("AppUpdate", "clearApkCache: starting stale APK cache cleanup...") + wipeApkCacheFiles("clearApkCache") } } } diff --git a/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift b/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift index 24aa4c52..0498dbb4 100644 --- a/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift +++ b/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift @@ -54,6 +54,13 @@ class ReactNativeAppUpdate: HybridReactNativeAppUpdateSpec { return Promise.resolved(withResult: ()) } + func clearApkCache() throws -> Promise { + // APK is Android-only; nothing to wipe on iOS. Safe no-op so the + // cross-platform Nitro spec stays satisfied. + OneKeyLog.debug("AppUpdate", "clearApkCache not available on iOS") + return Promise.resolved(withResult: ()) + } + func addDownloadListener(callback: @escaping (DownloadEvent) -> Void) throws -> Double { let id = nextListenerId nextListenerId += 1 diff --git a/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts b/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts index 26e1567d..ad0bf5dd 100644 --- a/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts +++ b/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts @@ -24,6 +24,10 @@ export interface ReactNativeAppUpdate verifyAPK(params: AppUpdateFileParams): Promise; installAPK(params: AppUpdateFileParams): Promise; clearCache(): Promise; + // Wipe downloaded APK artifacts from cacheDir/apks (.apk / .partial / + // .SHA256SUMS.asc). Android-only; the JS layer gates on update status before + // calling so this does NOT cancel an in-flight download. iOS is a no-op stub. + clearApkCache(): Promise; // Verification & testing testVerification(): Promise; diff --git a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt index d57984e6..6821051a 100644 --- a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +++ b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt @@ -1757,6 +1757,141 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() { } } + /** + * Prunes every artifact whose appVersion != the running native binary + * version: stale onekey-bundle/* dirs, onekey-bundle-download/* stages + * (.zip / .partial / .progress / .resume), orphan asc signatures, and + * lingering fallback entries. Hard-refuses to delete the current + * appVersion's artifacts and the active currentBundleVersion. Tolerates + * already-missing files. Returns the count of deleted version directories. + * + * appVersion is parsed from the "{appVersion}-{bundleVersion}" stem using + * the SAME last-dash split as listLocalBundles / the installBundle fallback + * logic, so behavior matches the rest of the module. + */ + override fun pruneStaleAppVersionBundles(): Promise { + BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache() + return Promise.async { + val context = getContext() + val currentAppV = BundleUpdateStoreAndroid.getAppVersion(context) ?: "" + // Safety net: never delete the bundle backing the active pointer. + val currentBundleVersion = BundleUpdateStoreAndroid.getCurrentBundleVersion(context) + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: currentAppV=$currentAppV, currentBundleVersion=$currentBundleVersion") + + // Without a known native version we cannot decide what is stale; + // bail out rather than risk deleting the wrong artifacts. + if (currentAppV.isEmpty()) { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: empty currentAppV, skipping") + return@async 0.0 + } + + // Parses an "{appVersion}-{bundleVersion}" stem into its appVersion + // component using the same last-dash split as listLocalBundles. + // Returns null for stems without a dash or with an empty appVersion. + fun appVersionFromStem(stem: String): String? { + val lastDash = stem.lastIndexOf('-') + if (lastDash <= 0) return null + val appV = stem.substring(0, lastDash) + return if (appV.isEmpty()) null else appV + } + + // True when this entry stem must be kept: its appVersion matches the + // running binary, it IS the active currentBundleVersion, or it is + // unparseable (leave foreign names alone). + fun shouldKeep(stem: String): Boolean { + if (currentBundleVersion != null && stem == currentBundleVersion) return true + val appV = appVersionFromStem(stem) ?: return true + return appV == currentAppV + } + + var deletedDirCount = 0 + + // 1. onekey-bundle/* extracted dirs + val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)) + if (bundleDir.exists() && bundleDir.isDirectory) { + bundleDir.listFiles()?.forEach { child -> + if (!child.isDirectory) return@forEach + val name = child.name + // Skip non-version entries (asc dir, fallback json, etc.) + if (name == "asc" || name == "fallbackUpdateBundleData.json") return@forEach + if (shouldKeep(name)) return@forEach + try { + BundleUpdateStoreAndroid.deleteDir(child) + deletedDirCount++ + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale bundle dir $name") + } catch (e: Exception) { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete bundle dir $name: ${e.message}") + } + } + } + + // 2. onekey-bundle-download/* stages (zip / partial / progress / resume) + val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)) + if (downloadDir.exists() && downloadDir.isDirectory) { + downloadDir.listFiles()?.forEach { file -> + val name = file.name + // Strip the trailing extension chain to recover the + // "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial"). + var stem = name + for (suffix in listOf(".resume", ".progress", ".partial", ".zip")) { + if (stem.endsWith(suffix)) { + stem = stem.substring(0, stem.length - suffix.length) + } + } + if (shouldKeep(stem)) return@forEach + try { + if (file.isDirectory) { + BundleUpdateStoreAndroid.deleteDir(file) + } else { + file.delete() + } + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale download $name") + } catch (e: Exception) { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete download $name: ${e.message}") + } + } + } + + // 3. onekey-bundle/asc/*-signature.asc orphan signatures + val ascDir = File(BundleUpdateStoreAndroid.getAscDir(context)) + if (ascDir.exists() && ascDir.isDirectory) { + val suffix = "-signature.asc" + ascDir.listFiles()?.forEach { file -> + val name = file.name + if (!name.endsWith(suffix)) return@forEach + val stem = name.substring(0, name.length - suffix.length) + if (shouldKeep(stem)) return@forEach + try { + file.delete() + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted orphan asc $name") + } catch (e: Exception) { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete asc $name: ${e.message}") + } + } + } + + // 4. Persisted fallback list: drop entries whose appVersion != currentAppV. + // Fixes the latent leak where stale fallback entries linger after a + // native upgrade. Reuses the existing read/write helpers. + try { + val fallbackData = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context) + val prunedFallback = fallbackData.filter { entry -> + val appV = entry["appVersion"] + appV.isNullOrEmpty() || appV == currentAppV + } + if (prunedFallback.size != fallbackData.size) { + BundleUpdateStoreAndroid.writeFallbackUpdateBundleDataFile(prunedFallback, context) + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: pruned fallback entries ${fallbackData.size} -> ${prunedFallback.size}") + } + } catch (e: Exception) { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: fallback prune error: ${e.message}") + } + + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: completed, deletedDirCount=$deletedDirCount") + deletedDirCount.toDouble() + } + } + override fun resetToBuiltInBundle(): Promise { BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache() return Promise.async { diff --git a/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift index 3d362053..f490a54d 100644 --- a/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift +++ b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift @@ -1725,6 +1725,130 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec { } } + /// Parses an "{appVersion}-{bundleVersion}" folder/file stem into its + /// appVersion component using the SAME last-dash split as listLocalBundles + /// and the installBundle fallback logic. Returns nil when the stem has no + /// dash or an empty appVersion, so callers leave unrecognized entries + /// untouched. + private static func appVersionFromStem(_ stem: String) -> String? { + guard let lastDash = stem.range(of: "-", options: .backwards), + lastDash.lowerBound > stem.startIndex else { return nil } + let appVersion = String(stem[stem.startIndex.. Promise { + BundleUpdateStore.invalidateValidatedBundleInfoCache() + return Promise.async { + let fm = FileManager.default + let currentAppV = BundleUpdateStore.getCurrentNativeVersion() + // Safety net: never delete the bundle backing the active pointer. + let currentBundleVersion = BundleUpdateStore.currentBundleVersion() + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: currentAppV=\(currentAppV), currentBundleVersion=\(currentBundleVersion ?? "nil")") + + // Without a known native version we cannot decide what is stale; + // bail out rather than risk deleting the wrong artifacts. + guard !currentAppV.isEmpty else { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: empty currentAppV, skipping") + return 0 + } + + /// True when this entry stem must be kept: its appVersion matches + /// the running binary, it IS the active currentBundleVersion, or it + /// is unparseable (leave foreign names alone). + func shouldKeep(stem: String) -> Bool { + if let active = currentBundleVersion, stem == active { return true } + guard let appV = BundleUpdateStore.appVersionFromStem(stem) else { return true } + return appV == currentAppV + } + + var deletedDirCount = 0 + + // 1. onekey-bundle/* extracted dirs + let bundleDir = BundleUpdateStore.bundleDir() + if let contents = try? fm.contentsOfDirectory(atPath: bundleDir) { + for name in contents { + // Skip non-version entries (asc dir, fallback json, etc.) + if name == "asc" || name == "fallbackUpdateBundleData.json" { continue } + let fullPath = (bundleDir as NSString).appendingPathComponent(name) + var isDir: ObjCBool = false + guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue else { continue } + if shouldKeep(stem: name) { continue } + do { + try fm.removeItem(atPath: fullPath) + deletedDirCount += 1 + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale bundle dir \(name)") + } catch { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete bundle dir \(name): \(error)") + } + } + } + + // 2. onekey-bundle-download/* stages (zip / partial / progress / resume) + let downloadDir = BundleUpdateStore.downloadBundleDir() + if let contents = try? fm.contentsOfDirectory(atPath: downloadDir) { + for name in contents { + // Strip the trailing extension chain to recover the + // "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial"). + var stem = name + for suffix in [".resume", ".progress", ".partial", ".zip"] { + if stem.hasSuffix(suffix) { + stem = String(stem.dropLast(suffix.count)) + } + } + if shouldKeep(stem: stem) { continue } + let fullPath = (downloadDir as NSString).appendingPathComponent(name) + do { + try fm.removeItem(atPath: fullPath) + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale download \(name)") + } catch { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete download \(name): \(error)") + } + } + } + + // 3. onekey-bundle/asc/*-signature.asc orphan signatures + let ascDir = BundleUpdateStore.ascDir() + if let contents = try? fm.contentsOfDirectory(atPath: ascDir) { + let suffix = "-signature.asc" + for name in contents { + guard name.hasSuffix(suffix) else { continue } + let stem = String(name.dropLast(suffix.count)) + if shouldKeep(stem: stem) { continue } + let fullPath = (ascDir as NSString).appendingPathComponent(name) + do { + try fm.removeItem(atPath: fullPath) + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted orphan asc \(name)") + } catch { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete asc \(name): \(error)") + } + } + } + + // 4. Persisted fallback list: drop entries whose appVersion != currentAppV. + // Fixes the latent leak where stale fallback entries linger after a + // native upgrade. Reuses the existing read/write helpers. + let fallbackData = BundleUpdateStore.readFallbackUpdateBundleDataFile() + let prunedFallback = fallbackData.filter { entry in + guard let appV = entry["appVersion"], !appV.isEmpty else { return true } + return appV == currentAppV + } + if prunedFallback.count != fallbackData.count { + BundleUpdateStore.writeFallbackUpdateBundleDataFile(prunedFallback) + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: pruned fallback entries \(fallbackData.count) -> \(prunedFallback.count)") + } + + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: completed, deletedDirCount=\(deletedDirCount)") + return Double(deletedDirCount) + } + } + func resetToBuiltInBundle() throws -> Promise { BundleUpdateStore.invalidateValidatedBundleInfoCache() return Promise.async { diff --git a/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts b/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts index 5a993236..29fc3f23 100644 --- a/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts +++ b/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts @@ -99,6 +99,10 @@ export interface ReactNativeBundleUpdate clearBundle(): Promise; clearAllJSBundleData(): Promise; resetToBuiltInBundle(): Promise; + // Prune every artifact whose appVersion differs from the running native + // binary version (stale OTA dirs, download stages, orphan asc, lingering + // fallback entries). Returns the count of deleted version directories. + pruneStaleAppVersionBundles(): Promise; // Bundle data getFallbackUpdateBundleData(): Promise; From 58604216dc12c4aa5c3090a44839ee8dff3b4e97 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 11 Jun 2026 02:03:32 +0800 Subject: [PATCH 14/21] Protect APKs during cache cleanup; robust deletions Android app-update: change wipeApkCacheFiles to accept protectedPaths and skip deleting verified/pending-install APKs; clearApkCache now aborts if a download is in progress, collects verified file paths to protect, and logs skipped files to avoid races during native cleanup. Android bundle-update: make deleteDirectory return a boolean (treating missing entries as success and failing on any leftover child), propagate that result to callers and log warnings on incomplete deletes instead of always reporting success. iOS bundle-update: reference appVersionFromStem as Self.appVersionFromStem to use the correct static method scope. --- .../ReactNativeAppUpdate.kt | 37 ++++++++++++---- .../ReactNativeBundleUpdate.kt | 43 +++++++++++++------ .../ios/ReactNativeBundleUpdate.swift | 2 +- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt index fb8b5112..6bae92ab 100644 --- a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt @@ -1201,7 +1201,7 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ * (JS-gated, no in-flight download to cancel). The [tag] flows into the * log lines so the two callers stay distinguishable in logcat. */ - private fun wipeApkCacheFiles(tag: String) { + private fun wipeApkCacheFiles(tag: String, protectedPaths: Set = emptySet()) { val context = NitroModules.applicationContext if (context == null) { OneKeyLog.warn("AppUpdate", "$tag: application context unavailable, skipping file cleanup") @@ -1215,7 +1215,16 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ val filesToDelete = apkDir.listFiles() ?: emptyArray() OneKeyLog.info("AppUpdate", "$tag: found ${filesToDelete.size} cached file(s) to delete in ${apkDir.absolutePath}") var deletedCount = 0 + var skippedCount = 0 filesToDelete.forEach { file -> + // Never delete a verified, pending-install APK (its canonical path is + // still in verifiedFiles). Protects the install flow from a racing + // cleanup that slipped past the JS status gate. + if (protectedPaths.isNotEmpty() && file.canonicalPath in protectedPaths) { + OneKeyLog.info("AppUpdate", "$tag: skipping verified pending-install file ${file.name}") + skippedCount++ + return@forEach + } val size = file.length() // The file may already be gone (concurrent OS cache reclaim); // treat a non-existent file as nothing to do, not a failure. @@ -1226,7 +1235,7 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ OneKeyLog.warn("AppUpdate", "$tag: failed to delete ${file.name}") } } - OneKeyLog.info("AppUpdate", "$tag: completed, deleted $deletedCount/${filesToDelete.size} files") + OneKeyLog.info("AppUpdate", "$tag: completed, deleted $deletedCount/${filesToDelete.size} files (skipped $skippedCount protected)") } override fun clearCache(): Promise { @@ -1245,12 +1254,24 @@ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ override fun clearApkCache(): Promise { return Promise.async { // Wipe downloaded APK artifacts from cacheDir/apks/ only. The JS - // layer gates on app-update status before calling, so there is no - // in-flight download to cancel here: deliberately do NOT touch - // isDownloading / verifiedFiles (that's clearCache's job). Missing - // dir/files are tolerated. - OneKeyLog.info("AppUpdate", "clearApkCache: starting stale APK cache cleanup...") - wipeApkCacheFiles("clearApkCache") + // layer gates on app-update status before calling, but that read is + // not atomic with this delete, so guard natively too: + // - bail entirely if a download is in flight (would yank its + // .partial/.progress out from under the writer); + // - never delete a verified, pending-install APK (still tracked in + // verifiedFiles) so an open installer keeps a readable file. + // Deliberately do NOT touch isDownloading / verifiedFiles state here + // (that's clearCache's job). Missing dir/files are tolerated. + if (isDownloading.get()) { + OneKeyLog.info("AppUpdate", "clearApkCache: download in progress, skipping APK cache cleanup") + return@async + } + val protectedPaths = synchronized(verifiedFiles) { verifiedFiles.keys.toSet() } + OneKeyLog.info( + "AppUpdate", + "clearApkCache: starting stale APK cache cleanup (protecting ${protectedPaths.size} verified file(s))...", + ) + wipeApkCacheFiles("clearApkCache", protectedPaths) } } } diff --git a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt index 6821051a..4003835d 100644 --- a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +++ b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt @@ -1013,16 +1013,26 @@ object BundleUpdateStoreAndroid { } } - private fun deleteDirectory(directory: File) { - if (directory.exists()) { - directory.listFiles()?.forEach { file -> - if (file.isDirectory) deleteDirectory(file) else file.delete() - } - directory.delete() + /** + * Recursively deletes [directory], returning true only when nothing is left + * behind. An already-missing entry counts as success (tolerates concurrent + * removal); a child that fails to delete makes the whole call return false + * so callers never report a half-deleted tree as a clean delete. + */ + private fun deleteDirectory(directory: File): Boolean { + if (!directory.exists()) return true + var allDeleted = true + directory.listFiles()?.forEach { file -> + val ok = if (file.isDirectory) deleteDirectory(file) else (!file.exists() || file.delete()) + if (!ok) allDeleted = false } + // delete() on a non-empty dir returns false, so a child failure above + // naturally propagates here too. + val dirDeleted = !directory.exists() || directory.delete() + return allDeleted && dirDeleted } - fun deleteDir(dir: File) = deleteDirectory(dir) + fun deleteDir(dir: File): Boolean = deleteDirectory(dir) private const val MAX_UNZIPPED_SIZE = 512L * 1024 * 1024 // 512 MB limit @@ -1816,9 +1826,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() { if (name == "asc" || name == "fallbackUpdateBundleData.json") return@forEach if (shouldKeep(name)) return@forEach try { - BundleUpdateStoreAndroid.deleteDir(child) - deletedDirCount++ - OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale bundle dir $name") + if (BundleUpdateStoreAndroid.deleteDir(child)) { + deletedDirCount++ + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale bundle dir $name") + } else { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: incomplete delete of bundle dir $name (left behind)") + } } catch (e: Exception) { OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete bundle dir $name: ${e.message}") } @@ -1840,12 +1853,16 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() { } if (shouldKeep(stem)) return@forEach try { - if (file.isDirectory) { + val deleted = if (file.isDirectory) { BundleUpdateStoreAndroid.deleteDir(file) } else { - file.delete() + !file.exists() || file.delete() + } + if (deleted) { + OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale download $name") + } else { + OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete download $name") } - OneKeyLog.info("BundleUpdate", "pruneStaleAppVersionBundles: deleted stale download $name") } catch (e: Exception) { OneKeyLog.warn("BundleUpdate", "pruneStaleAppVersionBundles: failed to delete download $name: ${e.message}") } diff --git a/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift index f490a54d..a05eb521 100644 --- a/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift +++ b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift @@ -1764,7 +1764,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec { /// is unparseable (leave foreign names alone). func shouldKeep(stem: String) -> Bool { if let active = currentBundleVersion, stem == active { return true } - guard let appV = BundleUpdateStore.appVersionFromStem(stem) else { return true } + guard let appV = Self.appVersionFromStem(stem) else { return true } return appV == currentAppV } From 8ed9b54fe7e0478926f60e32bd1cd4f944f7253d Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 11 Jun 2026 02:05:03 +0800 Subject: [PATCH 15/21] chore: bump version to 3.0.59 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index e17240ea..c6d7eca6 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index 34360aa0..b7bf999a 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index 57f172f4..d85575e6 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index 6b4757c9..e5cefa33 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index dc145003..cdd45200 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index 7b877b5b..eb7007de 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index f7ebba51..8432ed15 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index b7e12558..782abc3e 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index becfb94c..4de49eac 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 6cd9ab58..f85de365 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 2f5a49e8..6c731464 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 332c9804..d38c8f60 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 68b80481..bd5e33cb 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index 5d593b0e..c224171e 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 40939ad9..9f4a46b9 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.58", + "version": "3.0.59", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 4de5b89b..80dde59f 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 030c14c8..e956881a 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index ead81834..44640be5 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 528df116..5b029746 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index e443c19d..1bafcb18 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index 49c2c667..78c0c884 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 4f055791..95d708de 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 8d7fcb17..283c0f76 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index d039320f..78fdac18 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index a138a0f7..479f7763 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index b4973a35..b27c5254 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.58", + "version": "3.0.59", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index df8711e0..5966df51 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 086b09e2..756c655a 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.58", + "version": "3.0.59", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index c214b24b..4740d00d 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 30643f12..c71195e9 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.58", + "version": "3.0.59", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index db890a60..3c606378 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 5a852d40..b0e70ede 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.58", + "version": "3.0.59", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index ed373658..f6a6fc6b 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.58", + "version": "3.0.59", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From 63cc99a8f5f84bb9f580649f133e831ffe705af9 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 11 Jun 2026 10:01:33 +0800 Subject: [PATCH 16/21] Bump iOS pods and clarify bundle comment Refresh example/react-native/ios/Podfile.lock to update native module pod specs/checksums and Podfile checksum. Also adjust a comment in ReactNativeBundleUpdate.kt to explicitly reference versioned onekey-bundle/ dirs and onekey-bundle-download/ stages for clarity. --- example/react-native/ios/Podfile.lock | 99 ++++++++++--------- .../ReactNativeBundleUpdate.kt | 2 +- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/example/react-native/ios/Podfile.lock b/example/react-native/ios/Podfile.lock index 14da6ef3..d339421c 100644 --- a/example/react-native/ios/Podfile.lock +++ b/example/react-native/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - AutoSizeInput (3.0.40): + - AutoSizeInput (3.0.59): - boost - DoubleConversion - fast_float @@ -29,7 +29,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - BackgroundThread (3.0.40): + - BackgroundThread (3.0.59): - boost - DoubleConversion - fast_float @@ -59,7 +59,7 @@ PODS: - SocketRocket - Yoga - boost (1.84.0) - - ChartWebview (3.0.40): + - ChartWebview (3.0.59): - boost - DoubleConversion - fast_float @@ -87,9 +87,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga - - CloudKitModule (3.0.40): + - CloudKitModule (3.0.59): - boost - DoubleConversion - fast_float @@ -131,7 +132,7 @@ PODS: - hermes-engine (0.14.0): - hermes-engine/Pre-built (= 0.14.0) - hermes-engine/Pre-built (0.14.0) - - KeychainModule (3.0.40): + - KeychainModule (3.0.59): - boost - DoubleConversion - fast_float @@ -225,7 +226,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - PerpDepthBar (3.0.40): + - PerpDepthBar (3.0.59): - boost - DoubleConversion - fast_float @@ -2139,7 +2140,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-pager-view (3.0.40): + - react-native-pager-view (3.0.59): - boost - DoubleConversion - fast_float @@ -2254,7 +2255,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view (3.0.40): + - react-native-tab-view (3.0.59): - boost - DoubleConversion - fast_float @@ -2272,7 +2273,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-tab-view/common (= 3.0.40) + - react-native-tab-view/common (= 3.0.59) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2283,7 +2284,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view/common (3.0.40): + - react-native-tab-view/common (3.0.59): - boost - DoubleConversion - fast_float @@ -2868,7 +2869,7 @@ PODS: - React-perflogger (= 0.83.0) - React-utils (= 0.83.0) - SocketRocket - - ReactNativeAppUpdate (3.0.40): + - ReactNativeAppUpdate (3.0.59): - boost - DoubleConversion - fast_float @@ -2899,7 +2900,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleCrypto (3.0.40): + - ReactNativeBundleCrypto (3.0.59): - boost - DoubleConversion - fast_float @@ -2930,7 +2931,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleUpdate (3.0.40): + - ReactNativeBundleUpdate (3.0.59): - boost - DoubleConversion - fast_float @@ -2965,7 +2966,7 @@ PODS: - SocketRocket - SSZipArchive (>= 2.5.4) - Yoga - - ReactNativeCheckBiometricAuthChanged (3.0.40): + - ReactNativeCheckBiometricAuthChanged (3.0.59): - boost - DoubleConversion - fast_float @@ -2996,7 +2997,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeDeviceUtils (3.0.40): + - ReactNativeDeviceUtils (3.0.59): - boost - DoubleConversion - fast_float @@ -3027,7 +3028,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeGetRandomValues (3.0.40): + - ReactNativeGetRandomValues (3.0.59): - boost - DoubleConversion - fast_float @@ -3058,7 +3059,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeLiteCard (3.0.40): + - ReactNativeLiteCard (3.0.59): - boost - DoubleConversion - fast_float @@ -3087,7 +3088,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeNativeLogger (3.0.40): + - ReactNativeNativeLogger (3.0.59): - boost - CocoaLumberjack/Swift (~> 3.8) - DoubleConversion @@ -3118,7 +3119,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativePerfMemory (3.0.40): + - ReactNativePerfMemory (3.0.59): - boost - DoubleConversion - fast_float @@ -3149,7 +3150,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeRangeDownloader (3.0.40): + - ReactNativeRangeDownloader (3.0.59): - boost - DoubleConversion - fast_float @@ -3180,7 +3181,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeSplashScreen (3.0.40): + - ReactNativeSplashScreen (3.0.59): - boost - DoubleConversion - fast_float @@ -3211,7 +3212,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeZipArchive (3.0.40): + - ReactNativeZipArchive (3.0.59): - boost - DoubleConversion - fast_float @@ -3300,7 +3301,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ScrollGuard (3.0.40): + - ScrollGuard (3.0.59): - boost - DoubleConversion - fast_float @@ -3330,7 +3331,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - SegmentSlider (3.0.38): + - SegmentSlider (3.0.59): - boost - DoubleConversion - fast_float @@ -3360,7 +3361,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Skeleton (3.0.40): + - Skeleton (3.0.59): - boost - DoubleConversion - fast_float @@ -3722,11 +3723,11 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AutoSizeInput: 9ac4b7c79249ed3a60831e83a41e0d2932643a4f - BackgroundThread: eeeb678905192edc8ed1cc52a27d158fde2bdb64 + AutoSizeInput: 310e18d0c09d2fc10a6a85b9f1ed50ebd9a370c8 + BackgroundThread: 84ccf1277d097cdc27be7655a7821578afee52b5 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - ChartWebview: 05851c420e0d76c7c170e9d12e83a0a5e558a254 - CloudKitModule: 8a9b788cf9457b77ba13f61779719b8b5f9ad995 + ChartWebview: 098a9791750418a908b33dc31f97fe3217c5b0f1 + CloudKitModule: e0cbf4c35db4299f768f975311494a4d1ed5d753 CocoaLumberjack: 5644158777912b7de7469fa881f8a3f259c2512a DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 @@ -3734,12 +3735,12 @@ SPEC CHECKSUMS: fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 70fdc9d0bb0d8532e0411dcb21e53ce5a160960a - KeychainModule: b50cdc3bb4ff0747e5291b8e7ad735f05b62b299 + KeychainModule: 2da5d9d6892728cb204ff133b190dffd4438fee2 MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df NitroMmkv: 0be91455465952f2b943f753b9ee7df028d89e5c NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 - PerpDepthBar: db6f3d8816b48a71cc87befc36a322b70fc1cab7 + PerpDepthBar: 1b4f18ae73c6d0429ef675d8ac8ab083a89b28e1 RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23 RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 @@ -3776,9 +3777,9 @@ SPEC CHECKSUMS: React-logger: 9e597cbeda7b8cc8aa8fb93860dade97190f69cc React-Mapbuffer: 20046c0447efaa7aace0b76085aa9bb35b0e8105 React-microtasksnativemodule: 0e837de56519c92d8a2e3097717df9497feb33cb - react-native-pager-view: 1f96cf6db9eb7ab523a24cf5b5cbb452bf3cdd8f + react-native-pager-view: 474df899a86ad5b4778d88e98d8a1a6c3c3ce024 react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 - react-native-tab-view: bceb7c3b79e3182a5c4baa4c2009d669c9df9577 + react-native-tab-view: 5b6f94785bf097b6266f43f1ab0a0cad0703b3f1 React-NativeModulesApple: 1a378198515f8e825c5931a7613e98da69320cee React-networking: bfd1695ada5a57023006ce05823ac5391c3ce072 React-oscompat: aedc0afbded67280de6bb6bfac8cfde0389e2b33 @@ -3812,26 +3813,26 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 554b421c45b7df35ac791da1b734335470b55fcc ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - ReactNativeAppUpdate: 9fba072ac56a55cd95d2d1ee6d4ccfbfd19ab56e - ReactNativeBundleCrypto: 88d5e6a3026db2c76f5a17d5656d175691f18c72 - ReactNativeBundleUpdate: 4aba5b1003ee6378225cd39a7156b87ef76548f8 - ReactNativeCheckBiometricAuthChanged: b94623ddae3f9ee72148ad2b89451604dc2b391c - ReactNativeDeviceUtils: 8d69dcd74eb898b3a70f86fac82bcca33c441f01 - ReactNativeGetRandomValues: c689c2d7cb3ece2b2f44ebef42856aaf387562a7 - ReactNativeLiteCard: d056e4c1eca34bac302a752a18df033e78b45269 - ReactNativeNativeLogger: a895e4d31eedb102880b07398bda71328f9d9a19 - ReactNativePerfMemory: 1287435ddd77dd594e651aa66785fefbda0e659c - ReactNativeRangeDownloader: 0f3a1cb2f5018be6363bea19b357468bb3386111 - ReactNativeSplashScreen: f67afa9771cb746089116ca2a1a96fd03ee8e7a0 - ReactNativeZipArchive: 8275163058fadf1d6257d1a121862660ff801be2 + ReactNativeAppUpdate: 0518869c3b42898a2972e2cfeb9bb609eb29b54f + ReactNativeBundleCrypto: dbd90abdd4ad9480e5829a7de8acca89d7a10582 + ReactNativeBundleUpdate: 9d99bd6f672ea49055582cfb5ae50b1ba96e8529 + ReactNativeCheckBiometricAuthChanged: 60eca3ff33c1ed5506efa5f9479db81ce72e814b + ReactNativeDeviceUtils: 4812f30bf8504fad52b8f93724a21d287bc71096 + ReactNativeGetRandomValues: 47bf1fa0869b379838d7012294e5f1076da8265b + ReactNativeLiteCard: f06315268b596815e32d8cfa419b7f1221f31242 + ReactNativeNativeLogger: 8d7d53a047cd0d3399133895d53a11d63d3ec2e3 + ReactNativePerfMemory: 18c3d0b2341622d9d8d5c7ef94b95578648dbcc2 + ReactNativeRangeDownloader: 006ffbd25ab794ccc1756d6e7659d4f8f222f509 + ReactNativeSplashScreen: c460831a79feeae73c4cb5c44408bc3a209e1ff5 + ReactNativeZipArchive: 54a063f41f08d54b6d8b8a54034f81e38a780972 RNScreens: 7f643ee0fd1407dc5085c7795460bd93da113b8f - ScrollGuard: 07463a68bb1a94a1ce1fdf6cdc7732d13761dd95 - SegmentSlider: 85639c3c8a1053f4a49f3bf4f6ec714dce9fbfaa - Skeleton: 513246ea6afbe9d97fb33be68207726544830f3b + ScrollGuard: c70be9a10309307c9d1a0e152b5acf63df80248c + SegmentSlider: 1755235f50c748d7f01892e2e565507ef8770fa6 + Skeleton: f6a2f5b5681f49d4644dca5ce5f90f396475bf59 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e -PODFILE CHECKSUM: 0e0b2382ae957b2560c79ae902707fbded29fb51 +PODFILE CHECKSUM: 11e5274bb8ec6380d5fbde829e700fe4f0cf9c8a COCOAPODS: 1.16.2 diff --git a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt index 4003835d..7906888c 100644 --- a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt +++ b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt @@ -1769,7 +1769,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() { /** * Prunes every artifact whose appVersion != the running native binary - * version: stale onekey-bundle/* dirs, onekey-bundle-download/* stages + * version: stale onekey-bundle/ dirs, onekey-bundle-download/ stages * (.zip / .partial / .progress / .resume), orphan asc signatures, and * lingering fallback entries. Hard-refuses to delete the current * appVersion's artifacts and the active currentBundleVersion. Tolerates From 3e9b82644f743e2a595d670e5b5e92193cd8dffa Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 11 Jun 2026 10:03:15 +0800 Subject: [PATCH 17/21] chore: bump version to 3.0.60 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index c6d7eca6..83452360 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index b7bf999a..dd179bb9 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index d85575e6..ada244cd 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index e5cefa33..4acc07e4 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index cdd45200..6f4d7e85 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index eb7007de..e6f4d3e3 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 8432ed15..87b02129 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 782abc3e..4d3aec26 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index 4de49eac..969646e2 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index f85de365..54d00261 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 6c731464..1df6af9b 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index d38c8f60..415a8486 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index bd5e33cb..68e6d860 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index c224171e..bcb420cd 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 9f4a46b9..4a2bbad4 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.59", + "version": "3.0.60", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 80dde59f..c323cffb 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index e956881a..9f37b2b3 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index 44640be5..e23603af 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 5b029746..3380dc96 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 1bafcb18..41a6da5e 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index 78c0c884..dc646bf1 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 95d708de..4e913376 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 283c0f76..86cc63cd 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 78fdac18..90c557b8 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 479f7763..65c61031 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index b27c5254..e9cc125e 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.59", + "version": "3.0.60", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 5966df51..0d52958b 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index 756c655a..bbe1a899 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.59", + "version": "3.0.60", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index 4740d00d..f085db24 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index c71195e9..541157d5 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.59", + "version": "3.0.60", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 3c606378..6e4d00d5 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index b0e70ede..fbb231a2 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.59", + "version": "3.0.60", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index f6a6fc6b..0ab8a6b3 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.59", + "version": "3.0.60", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From e387886ce6b059bb212a75ce97bf02934144a323 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Fri, 12 Jun 2026 02:10:21 +0800 Subject: [PATCH 18/21] fix: resolve split-bundle segments after eval to fix "Requiring unknown module" crash Language switches reload the JS runtime; lazily-loaded segment modules (locale JSON, icons) could be required via Metro's import().then(__r) before the segment's __d definitions ran, because loadSegment resolved its promise right after registerSegment only enqueued the eval. The resulting "Requiring unknown module" is an uncatchable Hermes fatal. Evaluate the segment and resolve only after eval completes, on all four paths (iOS main + background, Android main + background via new JNI). Adds a watchdog so the promise can't hang, distinct retryable-vs-fatal reject codes, fail-closed when the native primitive is unavailable, and reads segment files off the JS thread. --- .gitignore | 5 + .../android/src/main/cpp/cpp-adapter.cpp | 363 +++++++++++++++++ .../BackgroundThreadManager.kt | 164 ++++++-- .../BackgroundThreadModule.kt | 14 +- .../ios/BackgroundThread.mm | 49 ++- .../ios/BackgroundThreadManager.h | 43 ++ .../ios/BackgroundThreadManager.mm | 229 ++++++++++- .../android/CMakeLists.txt | 51 +++ .../android/build.gradle | 26 ++ .../src/main/cpp/SplitBundleLoaderJSI.cpp | 239 +++++++++++ .../SplitBundleLoaderModule.kt | 180 ++++++++- .../ios/SplitBundleLoader.mm | 373 ++++++++++++++++-- 12 files changed, 1661 insertions(+), 75 deletions(-) create mode 100644 native-modules/react-native-split-bundle-loader/android/CMakeLists.txt create mode 100644 native-modules/react-native-split-bundle-loader/android/src/main/cpp/SplitBundleLoaderJSI.cpp diff --git a/.gitignore b/.gitignore index 8f5b1635..1e1612d9 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,8 @@ scripts/nitro-view/template/android/.gradle # generated by react-native-builder-bob native-modules/*/lib/ native-views/*/lib/ + +# generated by Android externalNativeBuild (CMake/ndk) for any module +native-modules/*/android/.cxx/ +native-views/*/android/.cxx/ +**/android/.cxx/ diff --git a/native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp b/native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp index e747e423..714fb6df 100644 --- a/native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp +++ b/native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -447,6 +448,323 @@ static void installTimersOnRuntime(jsi::Runtime &rt) { LOGI("Timer + rAF + rIC polyfills installed on bg runtime"); } +// ── Background segment eval (fix A: eval-then-resolve on the BG runtime) ── +// +// WHY THIS EXISTS — the "Requiring unknown module" race on the BACKGROUND +// runtime: +// The Kotlin BackgroundThreadManager.registerSegmentInBackground previously +// called `ReactContext.registerSegment(...)`, which (bridgeless) routes through +// ReactHostImpl.registerSegment → ReactInstance.registerSegment → C++ +// ReactInstance::registerSegment, and that only `scheduleWork`s the +// evaluateJavaScript onto the RuntimeScheduler before returning. The Kotlin +// completion callback then fired immediately, so the JS promise resolved BEFORE +// the segment's `__d(...)` module definitions ran. Metro's +// `import().then(() => __r(moduleId))` could then run `__r` before the module +// table was populated → a fatal, uncatchable "Requiring unknown module". Locale +// segments load through THIS bg path, so a language switch could still crash. +// +// THE FIX (mirrors the MAIN-runtime SplitBundleLoaderJSI fix and the iOS +// callFunctionOnBufferedRuntimeExecutor: fix): +// We evaluate the segment OURSELVES on the BACKGROUND JS thread and signal +// completion in the SAME block, strictly AFTER eval. The accessor we use is the +// background runtime's own RuntimeExecutor (`gBgTimerExecutor`), captured in +// nativeInstallSharedBridge for the bg runtime (isMain=false). It dispatches via +// scheduleOnJSThread(isMain=false, ...) → nativeExecuteWork, which runs the work +// on the bg JS queue thread and then drainMicrotasks() — so this targets the +// BACKGROUND runtime, NOT the main one. This is the correct bg analogue of the +// main path's CallInvoker (the bg runtime is created by ReactHostImpl and the bg +// ReactContext does not surface a usable jsCallInvokerHolder the way the main +// one does, but its RuntimeExecutor already exists and routes to the bg JS +// thread). +// +// Off-thread read (fix F): the segment file is read on the CALLING (native +// module) thread before dispatch; only evaluateJavaScript + completion run on +// the bg JS thread. + +// Java callback contract — implemented in Kotlin as +// BackgroundThreadManager.SegmentEvalCallback.onComplete(error). Empty/null +// error string → success. A message prefixed with "NO_RUNTIME:" → bg runtime +// not ready (retryable); "IO_ERROR:" → file read failure (fatal); otherwise → +// eval throw (fatal). Resolved exactly once on the Kotlin side via its watchdog +// guard. +static void invokeBgSegmentCallback(jobject globalCallback, const std::string &error) { + JNIEnv *env = getJNIEnv(); + if (!env || !globalCallback) { + return; + } + jclass cls = env->GetObjectClass(globalCallback); + jmethodID mid = + env->GetMethodID(cls, "onComplete", "(Ljava/lang/String;)V"); + if (mid) { + jstring jerr = error.empty() ? nullptr : env->NewStringUTF(error.c_str()); + env->CallVoidMethod(globalCallback, mid, jerr); + if (env->ExceptionCheck()) { + LOGE("invokeBgSegmentCallback: JNI exception after onComplete"); + env->ExceptionDescribe(); + env->ExceptionClear(); + } + if (jerr) { + env->DeleteLocalRef(jerr); + } + } else { + LOGE("invokeBgSegmentCallback: onComplete method not found!"); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } + env->DeleteLocalRef(cls); +} + +// ── Pending bg-eval callback registry (fix: bounded global-ref lifetime) ── +// +// The bg-eval work lambda captures a JNI global ref to the SegmentEvalCallback +// and is enqueued onto gPendingWork for the bg JS thread. If that work is +// enqueued but NEVER runs, the captured global ref would leak and the JS promise +// would settle only via the Kotlin 30s watchdog. The drop paths that release it: +// - Java schedule fails (JNI exception / missing scheduleOnJSThread): the C++ +// executor erases gPendingWork[workId] and drains this eval. +// - bg context==null / ptr==0 in scheduleOnJSThread: Kotlin calls +// nativeDropScheduledWork(workId) — which erases the work AND drains — +// BEFORE it would call nativeExecuteWork. (nativeExecuteWork's own +// rt==nullptr guard merely returns and is NOT a drain path; it is never +// reached for a dead ptr because Kotlin intercepts that case first.) +// - nativeDestroy: intentionally leaks the work lambdas (their ~jsi::Function +// can't run on a torn-down runtime) but drains this registry to settle them. +// To make this strictly bounded, every in-flight bg eval registers here. Whoever +// settles it first (the work lambda after eval, OR a drain on a drop path) claims +// it via `settled`, invokes the Java callback exactly once, and deletes the +// global ref. This guarantees no leak and no double-invoke. +struct PendingBgEval { + jobject globalCallback; // owned: deleted by whoever settles + std::shared_ptr> settled; // exactly-once claim +}; +static std::mutex gBgEvalMutex; +static std::unordered_map gPendingBgEvals; +static int64_t gNextBgEvalId = 0; + +// Settle a pending bg eval exactly once: invoke the Java callback with `error` +// (empty => success) and release the global ref. The shared `settled` flag is +// the single source of truth for the one-shot — the registry entry may already +// be gone (claimed/erased by the other party), so this is self-contained. +static void settleBgEval(jobject globalCallback, + const std::shared_ptr> &settled, + const std::string &error) { + if (!settled) { + return; + } + bool expected = false; + if (!settled->compare_exchange_strong(expected, true)) { + // Already settled by the other party (lambda vs drain). Do nothing — + // the winner already invoked + deleted the global ref. + return; + } + invokeBgSegmentCallback(globalCallback, error); + JNIEnv *env = getJNIEnv(); + if (env && globalCallback) { + env->DeleteGlobalRef(globalCallback); + } +} + +// Settle ALL currently-registered bg evals with a retryable NO_RUNTIME-class +// failure and clear the registry. Used when the bg runtime is going away (or is +// unreachable) and any enqueued-but-unrun eval would otherwise leak its global +// ref and hang the JS promise until the Kotlin watchdog. Each settle is +// exactly-once (the work lambda may race us, but the shared flag arbitrates), +// so this never double-invokes. +static void drainPendingBgEvals(const std::string &reason) { + // Move the entries OUT under the lock and clear the registry, then settle + // OUTSIDE the lock. settleBgEval performs a Java upcall (onComplete via + // CallVoidMethod), so holding gBgEvalMutex across it would hold a native + // lock across arbitrary JS — a re-entrancy / deadlock hazard if a callback + // ever synchronously re-enters a gBgEvalMutex-taking path. PendingBgEval is + // copyable (jobject handle + shared_ptr); copies share the same global ref + // and `settled` flag, and the shared flag still arbitrates exactly-once + // against a racing work lambda, so the global ref is released exactly once. + std::vector drained; + { + std::lock_guard lock(gBgEvalMutex); + if (gPendingBgEvals.empty()) { + return; + } + LOGE("[SplitBundle] draining %zu pending bg eval(s): %s", + gPendingBgEvals.size(), reason.c_str()); + drained.reserve(gPendingBgEvals.size()); + for (auto &entry : gPendingBgEvals) { + drained.push_back(entry.second); + } + gPendingBgEvals.clear(); + } + for (auto &entry : drained) { + settleBgEval(entry.globalCallback, entry.settled, "NO_RUNTIME:" + reason); + } +} + +// Reads the whole file at `path` into `out`. Returns false on failure. +static bool readBgFileToString(const std::string &path, std::string &out) { + FILE *f = std::fopen(path.c_str(), "rb"); + if (f == nullptr) { + return false; + } + if (std::fseek(f, 0, SEEK_END) != 0) { + std::fclose(f); + return false; + } + long size = std::ftell(f); + if (size < 0) { + std::fclose(f); + return false; + } + if (std::fseek(f, 0, SEEK_SET) != 0) { + std::fclose(f); + return false; + } + out.resize(static_cast(size)); + size_t readBytes = + (size == 0) ? 0 : std::fread(&out[0], 1, static_cast(size), f); + std::fclose(f); + return readBytes == static_cast(size); +} + +// nativeEvaluateSegmentInBackground: schedule eval of the segment at `path` +// onto the BACKGROUND JS thread via the bg RuntimeExecutor and invoke `callback` +// from INSIDE that same block, strictly AFTER eval. Returns immediately; the +// callback fires later on the bg JS thread (or synchronously here on a fail-fast +// path such as bg runtime not ready / file read failure). +extern "C" JNIEXPORT void JNICALL +Java_com_backgroundthread_BackgroundThreadManager_nativeEvaluateSegmentInBackground( + JNIEnv *env, jobject /* thiz */, jstring segmentPath, jstring sourceURL, + jobject callback) { + + // Global-ref the callback: it is invoked later on a different thread. + jobject globalCallback = env->NewGlobalRef(callback); + // Shared one-shot claim flag for this eval — used by BOTH the work lambda + // (after eval) and any drop-path drain, so exactly one of them invokes the + // callback + deletes the global ref. + auto settled = std::make_shared>(false); + + const char *pathChars = segmentPath ? env->GetStringUTFChars(segmentPath, nullptr) : nullptr; + std::string path = pathChars ? std::string(pathChars) : std::string(); + if (pathChars) env->ReleaseStringUTFChars(segmentPath, pathChars); + + const char *urlChars = sourceURL ? env->GetStringUTFChars(sourceURL, nullptr) : nullptr; + std::string url = urlChars ? std::string(urlChars) : std::string("segment"); + if (urlChars) env->ReleaseStringUTFChars(sourceURL, urlChars); + + // Fail-fast on THIS thread: settle exactly once, no registry entry created. + auto finishOnThisThread = [&](const std::string &err) { + settleBgEval(globalCallback, settled, err); + }; + + if (path.empty()) { + finishOnThisThread("IO_ERROR:Empty segment path"); + return; + } + + // Snapshot the bg RuntimeExecutor under the timer mutex (the same lock that + // guards its assignment/teardown). If it's null the bg runtime hasn't + // installed its SharedBridge yet → retryable NO_RUNTIME. + RPCRuntimeExecutor executor; + { + std::lock_guard lock(gTimerMutex); + executor = gBgTimerExecutor; + } + if (!executor) { + finishOnThisThread("NO_RUNTIME:Background runtime executor not available"); + return; + } + + // F: read the segment file HERE, on the calling (native module) thread, + // BEFORE dispatch — so only evaluateJavaScript + completion run on the bg JS + // thread and the read does not block it or race the watchdog. + std::string source; + if (!readBgFileToString(path, source)) { + finishOnThisThread("IO_ERROR:Failed to read bg segment file: " + path); + return; + } + if (source.empty()) { + finishOnThisThread("IO_ERROR:Empty bg segment file: " + path); + return; + } + + // Register this in-flight eval BEFORE dispatch so that if the enqueued work + // never runs (schedule fails, context==null, ptr==0, or nativeDestroy drops + // pending work) the drain can settle it as a retryable NO_RUNTIME failure + // and release the global ref — instead of leaking it and leaning on the + // Kotlin 30s watchdog. The work lambda removes its own entry when it runs. + int64_t bgEvalId; + { + std::lock_guard lock(gBgEvalMutex); + bgEvalId = gNextBgEvalId++; + gPendingBgEvals[bgEvalId] = PendingBgEval{globalCallback, settled}; + } + + // Move the already-read buffer + the global callback ref into the work + // lambda. The lambda runs on the bg JS thread (via nativeExecuteWork), which + // also drainMicrotasks() after — preserving eval+resolve as one atomic turn. + executor([globalCallback, settled, bgEvalId, source = std::move(source), + url = std::move(url)](jsi::Runtime &rt) { + // We are running now → claim ownership and remove our registry entry so + // a concurrent nativeDestroy drain can't also touch this eval. + { + std::lock_guard lock(gBgEvalMutex); + gPendingBgEvals.erase(bgEvalId); + } + std::string error; + try { + LOGI("[SplitBundle] bg evaluating segment %s (%zu bytes)", url.c_str(), + source.size()); + auto buffer = + std::make_shared(std::move(source)); + // Runs the segment's top-level __d(...) synchronously on the bg JS + // thread before returning. + rt.evaluateJavaScript(std::move(buffer), url); + LOGI("[SplitBundle] bg segment %s evaluated", url.c_str()); + } catch (const jsi::JSError &e) { + error = std::string("Bg segment eval JSError for ") + url + ": " + + e.getMessage(); + LOGE("[SplitBundle] %s", error.c_str()); + } catch (const std::exception &e) { + error = std::string("Bg segment eval failed for ") + url + ": " + + e.what(); + LOGE("[SplitBundle] %s", error.c_str()); + } catch (...) { + error = std::string("Bg segment eval failed for ") + url + + " (unknown C++ exception)"; + LOGE("[SplitBundle] %s", error.c_str()); + } + // Resolve/reject from INSIDE this same bg-JS-thread block, strictly + // AFTER eval above — the ordering guarantee that fixes the race. The + // shared `settled` flag makes this a no-op if a drain already claimed + // it (it would not have, since we erased our entry above, but the flag + // keeps the invariant airtight against any reorder). + settleBgEval(globalCallback, settled, error); + }); +} + +// nativeDropScheduledWork: clean up after a scheduleOnJSThread drop path where +// CallVoidMethod itself SUCCEEDED but Kotlin then found the bg runtime +// unreachable (context==null / ptr==0) and returned WITHOUT calling +// nativeExecuteWork for this `workId`. Two things must be released: +// 1. gPendingWork[workId] — the stored work lambda (holding the segment +// SOURCE BUFFER). nativeExecuteWork is the only other eraser and it will +// never run for this id, so without this it leaks until nativeDestroy. +// 2. The in-flight bg eval(s) — settle as retryable NO_RUNTIME so the JNI +// global ref is released and the JS promise resolves now instead of +// hanging on the 30s watchdog. drain-all is sound: an unreachable bg JS +// thread dooms every enqueued bg eval equally. +// Exactly-once via the shared `settled` flag, so a recovered runtime that later +// DOES run stale work (it can't — we erased it) would be a harmless no-op. +extern "C" JNIEXPORT void JNICALL +Java_com_backgroundthread_BackgroundThreadManager_nativeDropScheduledWork( + JNIEnv * /* env */, jobject /* thiz */, jlong workId) { + { + std::lock_guard lock(gWorkMutex); + gPendingWork.erase(static_cast(workId)); + } + drainPendingBgEvals("Background runtime unreachable when scheduling segment eval"); +} + // ── nativeInstallSharedBridge ─────────────────────────────────────────── // Install SharedStore and SharedRPC into a runtime. extern "C" JNIEXPORT void JNICALL @@ -471,8 +789,24 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge( RPCRuntimeExecutor executor = [ref, capturedIsMain](std::function work) { JNIEnv *env = getJNIEnv(); + + // Settle any enqueued-but-now-unrunnable bg eval when this work will NOT + // reach nativeExecuteWork. Bg eval lambdas are only ever dispatched via + // the bg executor, so this is gated on !capturedIsMain — a main-thread + // schedule hiccup must never falsely reject healthy bg evals. This + // mirrors the existing context==null / ptr==0 (nativeDropScheduledWork) + // and nativeDestroy drop paths: drain-all is sound because a failed bg + // schedule means the bg JS thread is unreachable, so every enqueued bg + // eval is equally doomed. NO_RUNTIME is retryable, so JS re-attempts. + auto drainBgEvalsIfBg = [capturedIsMain](const char *reason) { + if (!capturedIsMain) { + drainPendingBgEvals(reason); + } + }; + if (!env || !ref) { LOGE("executor: env=%p, ref=%p — aborting", env, ref.get()); + drainBgEvalsIfBg("Background executor env/ref unavailable when scheduling segment eval"); return; } @@ -485,6 +819,7 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge( jclass cls = env->GetObjectClass(ref.get()); jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)V"); + bool scheduled = false; if (mid) { LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", capturedIsMain, (long)workId); env->CallVoidMethod(ref.get(), mid, static_cast(capturedIsMain), static_cast(workId)); @@ -492,6 +827,8 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge( LOGE("executor: JNI exception after scheduleOnJSThread"); env->ExceptionDescribe(); env->ExceptionClear(); + } else { + scheduled = true; } } else { LOGE("executor: scheduleOnJSThread method not found!"); @@ -501,6 +838,21 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge( } } env->DeleteLocalRef(cls); + + // Schedule failed (JNI exception or missing method): nativeExecuteWork + // will never run this workId, so erase it now to free the stored work + // (and its captured segment source buffer) instead of leaking it until + // nativeDestroy. Then settle the corresponding bg eval(s) so the JNI + // global ref is released and the JS promise resolves immediately rather + // than hanging on the Kotlin 30s watchdog. Erasing also makes a late + // Kotlin enqueue (if scheduleOnJSThread threw AFTER posting) a no-op. + if (!scheduled) { + { + std::lock_guard lock(gWorkMutex); + gPendingWork.erase(workId); + } + drainBgEvalsIfBg("Background JS thread unreachable when scheduling segment eval"); + } }; std::string runtimeId = isMain ? "main" : "background"; @@ -646,6 +998,11 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy( // Drain pending cross-runtime work. Each std::function may capture a // shared_ptr tied to the destroyed runtime; leak them // for the same reason as above. + // + // NOTE: the bg-eval work lambdas captured here also hold a JNI global ref + // to a SegmentEvalCallback. Leaking the std::function leaks that ref AND + // leaves the JS promise pending — so we settle those explicitly below via + // gPendingBgEvals (the entry survives independently of the leaked lambda). { std::lock_guard lock(gWorkMutex); for (auto &entry : gPendingWork) { @@ -654,5 +1011,11 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy( gPendingWork.clear(); } + // Drain pending bg-eval callbacks: the bg runtime is gone, so any eval that + // was enqueued but never ran must be settled NOW (retryable NO_RUNTIME) so + // the JS promise resolves immediately and the global ref is released — + // rather than leaking and relying on the Kotlin 30s watchdog. + drainPendingBgEvals("Background runtime destroyed before segment eval ran"); + LOGI("Native resources cleaned up"); } diff --git a/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt b/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt index f5b7a2ca..fe169ad1 100644 --- a/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt +++ b/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt @@ -24,6 +24,7 @@ import com.facebook.react.shell.MainReactPackage import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean /** * Singleton manager for the background React Native runtime. @@ -63,6 +64,12 @@ class BackgroundThreadManager private constructor() { companion object { private const val MODULE_NAME = "background" + // Bounded watchdog: if the bg JS thread never drains to our scheduled + // eval (e.g. the bg entry bundle never finished evaluating), reject as a + // RETRYABLE timeout rather than leaving the JS promise pending forever. + // Matches the main-runtime SplitBundleLoader watchdog. + private const val BG_SEGMENT_EVAL_TIMEOUT_MS = 30_000L + init { System.loadLibrary("background_thread") } @@ -78,6 +85,20 @@ class BackgroundThreadManager private constructor() { } } + /** + * Completion contract invoked by the native (JNI) side AFTER the bg segment + * has been evaluated into the BACKGROUND runtime. Called from the bg JS + * thread (or synchronously on the caller thread for fail-fast paths). + * + * @param error null on success; a non-empty message on failure. A message + * prefixed with "NO_RUNTIME:" means the bg runtime is not ready yet + * (retryable); "IO_ERROR:" means the segment file read failed (fatal); + * any other message is a segment JS/Hermes eval throw (fatal). + */ + fun interface SegmentEvalCallback { + fun onComplete(error: String?) + } + // ── JNI declarations ──────────────────────────────────────────────────── private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean) @@ -85,6 +106,34 @@ class BackgroundThreadManager private constructor() { private external fun nativeDestroy() private external fun nativeExecuteWork(runtimePtr: Long, workId: Long) + /** + * Evaluate the segment at [segmentPath] into the BACKGROUND runtime on its + * JS thread and invoke [callback] from inside that same JS-thread block, + * strictly AFTER the segment's `__d(...)` module definitions have run. This + * is the ordering guarantee that fixes the bg "Requiring unknown module" + * race (the bg analogue of the main-runtime SplitBundleLoaderJSI fix). + * + * Returns immediately; [callback] fires later on the bg JS thread (or + * synchronously on the calling thread for fail-fast paths such as the bg + * runtime not being ready or the segment file failing to read). + */ + private external fun nativeEvaluateSegmentInBackground( + segmentPath: String, + sourceURL: String, + callback: SegmentEvalCallback + ) + + /** + * Settle every in-flight bg segment eval as a retryable NO_RUNTIME failure + * without tearing the runtime down. Called from [scheduleOnJSThread] when + * the bg runtime is momentarily unreachable (context == null / ptr == 0) so + * any eval enqueued onto the native pending-work map — which will never be + * drained on the JS thread in that state — releases its JNI global ref and + * settles the JS promise immediately, instead of leaking until the next + * teardown or relying on the bg watchdog. Exactly-once on the native side. + */ + private external fun nativeDropScheduledWork(workId: Long) + /** * Synchronously mark the SharedRPC listener for `runtimeId` as dead * before the underlying JS runtime is torn down. See @@ -412,8 +461,17 @@ class BackgroundThreadManager private constructor() { BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}") if (context == null) { BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}") + // The just-enqueued native work will never reach the bg JS thread. + // Drop it now: erase gPendingWork[workId] (frees the captured segment + // source buffer) and, if it was a bg segment eval, settle it + // (retryable NO_RUNTIME) so its JNI global ref is released and the JS + // promise resolves instead of leaking until teardown / the bg watchdog. + if (!isMain) { + nativeDropScheduledWork(workId) + } + return } - context?.runOnJSQueueThread { + context.runOnJSQueueThread { // Re-read ptr inside the block — if a reload happened between // scheduling and execution, the old ptr may be stale. val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr @@ -426,6 +484,13 @@ class BackgroundThreadManager private constructor() { } } else { BTLogger.error("scheduleOnJSThread: ptr is 0! isMain=$isMain") + // Same as the null-context case: the work won't run on this + // (stale/torn-down) bg runtime. Drop gPendingWork[workId] (frees + // the source buffer) and settle any pending bg eval so it doesn't + // leak its global ref / hang the JS promise. + if (!isMain) { + nativeDropScheduledWork(workId) + } } } } @@ -433,51 +498,98 @@ class BackgroundThreadManager private constructor() { // ── Segment Registration (Phase 2.5 spike) ───────────────────────────── /** - * Register a HBC segment in the background runtime. - * Uses CatalystInstance.registerSegment() on the background ReactContext. + * Evaluate a HBC segment into the background runtime with completion + * callback (fix A: eval-then-resolve). * - * @param segmentId The segment ID to register - * @param path Absolute file path to the .seg.hbc file - * @throws IllegalStateException if background runtime is not started - * @throws IllegalArgumentException if segment file does not exist - */ - /** - * Register a HBC segment in the background runtime with completion callback. - * Dispatches to the background JS queue thread and invokes the callback - * only after registerSegment has actually executed. + * Previously this called `ReactContext.registerSegment(...)`, whose + * completion fires BEFORE the segment bytecode is evaluated into the runtime + * (registerSegment only ENQUEUES the eval). That races Metro's + * `import().then(() => __r(moduleId))` and produces a fatal, uncatchable + * "Requiring unknown module" — locale segments load through this bg path, so + * a language switch could still crash. We now evaluate the segment OURSELVES + * on the bg JS thread via [nativeEvaluateSegmentInBackground] and resolve + * ONLY after eval completes, so eval + resolve are one atomic JS-thread turn. * - * @param segmentId The segment ID to register + * @param segmentId The segment ID (used only for the synthetic source URL) * @param path Absolute file path to the .seg.hbc file - * @param onComplete Called with null on success, or an Exception on failure + * @param onComplete Called with (code=null) on success, or + * (code=, message) on failure. The code is one of + * the SHARED split-bundle contract codes so the JS loader's retryable set + * { SPLIT_BUNDLE_NO_RUNTIME, SPLIT_BUNDLE_TIMEOUT } classifies correctly. */ - fun registerSegmentInBackground(segmentId: Int, path: String, onComplete: (Exception?) -> Unit) { + fun registerSegmentInBackground( + segmentId: Int, + path: String, + onComplete: (code: String?, message: String?) -> Unit + ) { if (!isStarted) { - onComplete(IllegalStateException("Background runtime not started")) + // Bg runtime not started yet → retryable (the loader will re-attempt + // once the bg host is up). + onComplete("SPLIT_BUNDLE_NO_RUNTIME", "Background runtime not started") return } val file = File(path) if (!file.exists()) { - onComplete(IllegalArgumentException("Segment file not found: $path")) + onComplete("SPLIT_BUNDLE_NOT_FOUND", "Segment file not found: $path") return } val context = bgReactHost?.currentReactContext if (context == null) { - onComplete(IllegalStateException("Background ReactContext not available")) + onComplete("SPLIT_BUNDLE_NO_RUNTIME", "Background ReactContext not available") return } - // Use ReactContext.registerSegment which works in both bridge - // and bridgeless modes. + // One-shot guard: native success/error AND the watchdog can each try to + // settle; only the first wins. + val settled = AtomicBoolean(false) + val sourceURL = "seg-$segmentId.js" + val segStart = System.nanoTime() + + // Bounded watchdog: if the bg JS thread never drains to our eval, reject + // with the RETRYABLE SPLIT_BUNDLE_TIMEOUT instead of hanging forever. + val watchdog = Handler(Looper.getMainLooper()) + val timeoutRunnable = Runnable { + if (settled.compareAndSet(false, true)) { + BTLogger.error("[SplitBundle] bg segment id=$segmentId eval timed out after ${BG_SEGMENT_EVAL_TIMEOUT_MS}ms (bg entry bundle likely never finished evaluating); rejecting as retryable timeout") + onComplete("SPLIT_BUNDLE_TIMEOUT", "Bg segment eval timed out: id=$segmentId") + } + } + watchdog.postDelayed(timeoutRunnable, BG_SEGMENT_EVAL_TIMEOUT_MS) + try { - context.registerSegment(segmentId, path) { - BTLogger.info("Segment registered in background runtime: id=$segmentId, path=$path") - onComplete(null) + nativeEvaluateSegmentInBackground(path, sourceURL) { error -> + if (settled.compareAndSet(false, true)) { + watchdog.removeCallbacks(timeoutRunnable) + if (error == null) { + val segMs = (System.nanoTime() - segStart) / 1_000_000.0 + BTLogger.info("[SplitBundle] bg segment id=$segmentId evaluated in ${String.format("%.1f", segMs)}ms (eval-complete)") + onComplete(null, null) + } else { + // Native prefixes its failures so we can map to the + // shared contract codes: NO_RUNTIME (retryable), + // IO_ERROR (fatal), else eval throw (fatal). + when { + error.startsWith("NO_RUNTIME:") -> + onComplete("SPLIT_BUNDLE_NO_RUNTIME", error.removePrefix("NO_RUNTIME:")) + error.startsWith("IO_ERROR:") -> + onComplete("SPLIT_BUNDLE_IO_ERROR", error.removePrefix("IO_ERROR:")) + else -> + onComplete("SPLIT_BUNDLE_EVAL_ERROR", error) + } + } + } + } + } catch (e: Throwable) { + // nativeEvaluateSegmentInBackground itself failed to dispatch (e.g. + // UnsatisfiedLinkError). Fail closed; do NOT fall back to the + // race-prone registerSegment path. + if (settled.compareAndSet(false, true)) { + watchdog.removeCallbacks(timeoutRunnable) + BTLogger.error("[SplitBundle] FATAL: nativeEvaluateSegmentInBackground threw for id=$segmentId: ${e.message}") + onComplete("SPLIT_BUNDLE_NATIVE_UNAVAILABLE", "Bg native segment eval unavailable: ${e.message}") } - } catch (e: Exception) { - BTLogger.error("Failed to register segment in background runtime: ${e.message}") - onComplete(e) } } diff --git a/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt b/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt index 300f9dba..1563e2f6 100644 --- a/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt +++ b/native-modules/react-native-background-thread/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt @@ -30,9 +30,17 @@ class BackgroundThreadModule(reactContext: ReactApplicationContext) : override fun loadSegmentInBackground(segmentId: Double, path: String, promise: Promise) { BackgroundThreadManager.getInstance() - .registerSegmentInBackground(segmentId.toInt(), path) { error -> - if (error != null) { - promise.reject("BG_SEGMENT_LOAD_ERROR", error.message, error) + .registerSegmentInBackground(segmentId.toInt(), path) { code, message -> + if (code != null) { + // Reject with the SHARED split-bundle contract code (e.g. + // SPLIT_BUNDLE_NO_RUNTIME / SPLIT_BUNDLE_TIMEOUT are + // retryable; SPLIT_BUNDLE_IO_ERROR / SPLIT_BUNDLE_EVAL_ERROR + // / SPLIT_BUNDLE_NATIVE_UNAVAILABLE / SPLIT_BUNDLE_NOT_FOUND + // are not) so the JS loader classifies retryability the same + // way it does for the main path. registerSegmentInBackground + // always supplies one of these contract codes here, so there + // is no legacy/opaque default string to fall back to. + promise.reject(code, message) } else { promise.resolve(null) } diff --git a/native-modules/react-native-background-thread/ios/BackgroundThread.mm b/native-modules/react-native-background-thread/ios/BackgroundThread.mm index 960b3d3a..a376bdaa 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundThread.mm +++ b/native-modules/react-native-background-thread/ios/BackgroundThread.mm @@ -38,7 +38,54 @@ - (void)loadSegmentInBackground:(double)segmentId path:path completion:^(NSError * _Nullable error) { if (error) { - reject(@"BG_SEGMENT_LOAD_ERROR", error.localizedDescription, error); + // Fix 1/E: map the manager's DISTINCT NSError.code to its own JS + // reject code (matching SplitBundleLoader's main-runtime mapping and + // the shared error-code contract) so JS can classify + // retryable-vs-fatal. EVERY EBgMgrSegmentEvalError case is mapped + // EXPLICITLY here — there is no silent `default → NO_RUNTIME` that + // could MISCLASSIFY a fatal failure (e.g. a missing segment file) + // as a transient runtime-not-ready that gets retried/masked. The old + // code both collapsed bg failures into one opaque + // `BG_SEGMENT_LOAD_ERROR` and (before that) let raw codes 1/2 fall + // through `default` to retryable NO_RUNTIME. + NSString *rejectCode; + switch ((EBgMgrSegmentEvalError)error.code) { + case EBgMgrSegmentEvalErrorNotStarted: + case EBgMgrSegmentEvalErrorNilInstance: + // Bg runtime not started / RCTInstance nil — TRANSIENT: the + // bg host simply wasn't ready yet; a later attempt may win. + rejectCode = @"SPLIT_BUNDLE_NO_RUNTIME"; // retryable + break; + case EBgMgrSegmentEvalErrorFileNotFound: + // Segment file missing — FATAL packaging/OTA corruption. + rejectCode = @"SPLIT_BUNDLE_NOT_FOUND"; // fatal + break; + case EBgMgrSegmentEvalErrorIvarMissing: + // `_rctInstance` ivar reflection failed — STRUCTURAL/PERMANENT + // (an RN bump renamed the private field). Retrying is futile. + rejectCode = @"SPLIT_BUNDLE_NATIVE_UNAVAILABLE"; // fatal + break; + case EBgMgrSegmentEvalErrorIORead: + rejectCode = @"SPLIT_BUNDLE_IO_ERROR"; // fatal + break; + case EBgMgrSegmentEvalErrorEvalThrow: + rejectCode = @"SPLIT_BUNDLE_EVAL_ERROR"; // fatal (segment bug) + break; + case EBgMgrSegmentEvalErrorTimeout: + rejectCode = @"SPLIT_BUNDLE_TIMEOUT"; // retryable + break; + default: + // Defensive LAST RESORT only: every real EBgMgrSegmentEvalError + // case is handled above, so reaching here means the manager + // emitted an unmapped code (a bug). Choose retryable + // NO_RUNTIME so a stale/unknown native build degrades safely + // rather than permanently poisoning a segment — but log it + // loudly so the unmapped code is caught and named. + [BTLogger warn:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: UNMAPPED EBgMgrSegmentEvalError code=%ld — defaulting to retryable NO_RUNTIME. This is a native bug: add an explicit case.", (long)error.code]]; + rejectCode = @"SPLIT_BUNDLE_NO_RUNTIME"; // retryable (defensive) + break; + } + reject(rejectCode, error.localizedDescription, error); } else { resolve(nil); } diff --git a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.h b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.h index 4c4e97a4..09185f60 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.h +++ b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.h @@ -6,6 +6,49 @@ NS_ASSUME_NONNULL_BEGIN @class RCTReactNativeFactory; @class RCTHost; +/// NSError `code` values produced by `registerSegmentInBackground:` / +/// `evaluateSegmentInBackground:` under the `BackgroundThread` error domain. +/// +/// These are DISTINCT (kept numerically aligned with SplitBundleLoader's +/// `ESegmentEvalError`) so the TurboModule boundary +/// (`BackgroundThread.loadSegmentInBackground`) can map each to its OWN JS +/// reject code and JS can classify retryable-vs-fatal — rather than collapsing +/// every bg failure into one opaque code (fix E). EVERY failure the manager +/// produces MUST have a NAMED case here so the boundary's switch maps it +/// explicitly: a raw integer literal would fall through to the boundary's +/// defensive `default` (→ retryable NO_RUNTIME) and silently MISCLASSIFY a +/// fatal failure (e.g. a missing segment file) as a transient one that gets +/// retried/masked instead of surfaced. The mapping the boundary applies is: +/// - NotStarted (bg runtime not started yet) → `SPLIT_BUNDLE_NO_RUNTIME` (retryable) +/// - FileNotFound (segment file missing) → `SPLIT_BUNDLE_NOT_FOUND` (fatal) +/// - NilInstance (bg RCTInstance is nil) → `SPLIT_BUNDLE_NO_RUNTIME` (retryable) +/// - IORead (file read/mmap failed) → `SPLIT_BUNDLE_IO_ERROR` (fatal) +/// - EvalThrow (segment JS/Hermes bug) → `SPLIT_BUNDLE_EVAL_ERROR` (fatal) +/// - Timeout (buffered executor never ran)→ `SPLIT_BUNDLE_TIMEOUT` (retryable) +/// - IvarMissing (`_rctInstance` reflection +/// failed — STRUCTURAL/permanent)→ `SPLIT_BUNDLE_NATIVE_UNAVAILABLE` (fatal) +/// +/// WHY NilInstance and IvarMissing are split (and not both NO_RUNTIME): a nil +/// RCTInstance is TRANSIENT — the bg host just isn't up yet, a later attempt +/// can succeed. A missing `_rctInstance` ivar is STRUCTURAL/PERMANENT — an RN +/// version bump renamed/removed the private field our reflection depends on, so +/// bg segment loading is disabled until the native code is updated. Retrying +/// the latter is futile, so it maps to fatal NATIVE_UNAVAILABLE. +/// +/// Declared here (not file-local in the .mm) so the boundary references these +/// by NAME — a future renumbering stays exact instead of drifting against a +/// hardcoded magic-number switch. Values 3-6 are kept STABLE; 1/2/7 fill in the +/// previously-raw `registerSegmentInBackground:` codes and the structural split. +typedef NS_ENUM(NSInteger, EBgMgrSegmentEvalError) { + EBgMgrSegmentEvalErrorNotStarted = 1, // bg runtime not started yet (retryable) + EBgMgrSegmentEvalErrorFileNotFound = 2, // segment file missing (fatal) + EBgMgrSegmentEvalErrorNilInstance = 3, // bg RCTInstance is nil (retryable) + EBgMgrSegmentEvalErrorIORead = 4, // file read/mmap failed (fatal) + EBgMgrSegmentEvalErrorEvalThrow = 5, // segment JS/Hermes bug (fatal) + EBgMgrSegmentEvalErrorTimeout = 6, // buffered executor never ran (retryable) + EBgMgrSegmentEvalErrorIvarMissing = 7, // `_rctInstance` ivar reflection failed (fatal, structural) +}; + @interface BackgroundThreadManager : NSObject /// Shared instance for singleton pattern diff --git a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm index fb18bfa9..1c9fd826 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm +++ b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm @@ -20,6 +20,82 @@ #import #import #import +#import +#include +#include + +namespace { + +// Zero-copy jsi::Buffer that retains its NSData for the async (buffered) +// executor block's lifetime — same rationale as SplitBundleLoader's +// NSDataJSIBuffer (M4/M5): no second full copy of the segment bytes, and the +// mmap'd/heap bytes stay alive because the buffer owns the NSData. +class BgMgrNSDataJSIBuffer : public facebook::jsi::Buffer { + public: + explicit BgMgrNSDataJSIBuffer(NSData *data) : data_(data) {} + size_t size() const override { return data_.length; } + const uint8_t *data() const override { + return static_cast(data_.bytes); + } + + private: + NSData *data_; // strong retain (ARC). +}; + +} // namespace + +// Exactly-once settle guard for the bg watchdog — same design as +// SplitBundleLoader's SBLSettleGuard. An ARC object captured by both the +// executor block and the watchdog dispatch_after; ARC keeps its lock alive +// until both release, so neither block needs (or may do) a manual free — +// avoiding the use-after-free that would occur if either freed the lock while +// the other still runs. +@interface BgMgrSettleGuard : NSObject +- (BOOL)tryClaim; +@end + +@implementation BgMgrSettleGuard { + os_unfair_lock _lock; + BOOL _settled; +} +- (instancetype)init { + if (self = [super init]) { + _lock = OS_UNFAIR_LOCK_INIT; + _settled = NO; + } + return self; +} +- (BOOL)tryClaim { + os_unfair_lock_lock(&_lock); + BOOL won = !_settled; + if (won) { + _settled = YES; + } + os_unfair_lock_unlock(&_lock); + return won; +} +@end + +// Watchdog window for the bg buffered runtime executor (H2 = bg-runtime port of +// SplitBundleLoader's C1). The bg runtime's buffered executor stays buffered +// until the bg entry bundle finishes evaluating in +// BackgroundReactNativeDelegate.hostDidStart:; if that never completes (host +// teardown, OTA-resolve abort), the block never runs — without this watchdog +// the JS promise would hang inflightSegments forever. +// +// Fix G: 30s (matching Android and the main-runtime watchdog). The buffered +// executor stays buffered until the bg ENTRY bundle finishes evaluating; on a +// slow/throttled cold start that entry eval can itself exceed 10s, which would +// falsely trip the watchdog on a load that was about to succeed. 30s keeps the +// genuine-wedge safety net while leaving generous headroom for slow cold starts. +static const NSTimeInterval kBgSegmentEvalWatchdogSeconds = 30.0; + +// EBgMgrSegmentEvalError NSError `code` values are declared in +// BackgroundThreadManager.h so the TurboModule boundary +// (BackgroundThread.loadSegmentInBackground) can map each distinct code to its +// own JS reject code by NAME. They stay numerically aligned with +// SplitBundleLoader's ESegmentEvalError; see installProdBundleLoader.ts for the +// retryable-vs-fatal classification (H3 / fix E). @interface BackgroundThreadManager () @property (nonatomic, strong) BackgroundReactNativeDelegate *reactNativeFactoryDelegate; @@ -201,33 +277,166 @@ - (void)registerSegmentInBackground:(NSNumber *)segmentId completion:(void (^)(NSError * _Nullable error))completion { if (!self.isStarted || !self.reactNativeFactoryDelegate) { + // Transient: the bg runtime just isn't up yet. NotStarted → NO_RUNTIME + // (retryable). Previously a raw `code:1` which fell through the + // boundary's `default` — same destination, but now NAMED so the mapping + // is explicit and can never drift (fix 1 / E). NSError *error = [NSError errorWithDomain:@"BackgroundThread" - code:1 + code:EBgMgrSegmentEvalErrorNotStarted userInfo:@{NSLocalizedDescriptionKey: @"Background runtime not started"}]; if (completion) completion(error); return; } - // Verify the file exists + // Verify the file exists. FATAL: a missing segment file is real packaging / + // OTA corruption — retrying just re-misses. Previously a raw `code:2` that + // the boundary's `default` MISCLASSIFIED as retryable NO_RUNTIME, masking + // the corruption; now FileNotFound → NOT_FOUND (fatal) (fix 1 / E — the + // NO-SHIP blocker). if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { NSError *error = [NSError errorWithDomain:@"BackgroundThread" - code:2 + code:EBgMgrSegmentEvalErrorFileNotFound userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment file not found: %@", path]}]; if (completion) completion(error); return; } - BOOL success = [self.reactNativeFactoryDelegate registerSegmentWithId:segmentId path:path]; - if (success) { - if (completion) completion(nil); - } else { + [self evaluateSegmentInBackground:segmentId path:path completion:completion]; +} + +// Evaluate-then-resolve segment load for the BACKGROUND runtime (H2). +// +// WHY THIS REPLACES registerSegmentWithId: + immediate completion(nil): +// The old path called `[delegate registerSegmentWithId:path:]` (which routes to +// RCTInstance/ReactInstance::registerSegment — that only ENQUEUES +// `runtime.evaluateJavaScript(segment)` on the runtime scheduler and returns) +// then resolved the JS promise immediately. That is the exact +// "Requiring unknown module" race the MAIN runtime already fixed in +// SplitBundleLoader: Metro's `import().then(() => __r(moduleId))` microtask can +// run `__r` BEFORE the scheduled eval populated the module table → a FATAL, +// uncatchable crash. Locale segments load through THIS path in the bg runtime +// (ServiceSetting.refreshLocaleMessages → import('./json/*.json') runs in +// kit-bg), so a language switch could still crash. +// +// Fix: evaluate the segment OURSELVES inside one +// `callFunctionOnBufferedRuntimeExecutor:` block on the bg RCTInstance and +// signal completion in that SAME block, strictly AFTER eval — making eval + +// resolve one atomic unit so any subsequent `__r(moduleId)` finds the module. +// The bg RCTInstance is the delegate's private `_rctInstance` ivar; we read it +// reflectively (the same pattern installSharedBridgeInMainRuntime: uses on the +// main host) to keep this fix self-contained in this file rather than widening +// the delegate's surface. Includes the same exactly-once + watchdog guard as +// the main-runtime fix (C1) because the bg buffered executor is likewise +// buffered until the bg entry bundle finishes evaluating in hostDidStart:. +- (void)evaluateSegmentInBackground:(NSNumber *)segmentId + path:(NSString *)path + completion:(void (^)(NSError * _Nullable error))completion +{ + // Reach the bg RCTInstance via the delegate's `_rctInstance` ivar. `id` + // (not a typed RCTInstance*) mirrors installSharedBridgeInMainRuntime:'s + // untyped handling and avoids needing the RCTInstance header here. + BackgroundReactNativeDelegate *delegate = self.reactNativeFactoryDelegate; + Ivar ivar = class_getInstanceVariable([delegate class], "_rctInstance"); + if (!ivar) { + // Loud failure (L7 parity): a future RN/delegate refactor that renames + // this ivar silently disables ALL bg segment loading — surface it. + // STRUCTURAL/PERMANENT (not transient): retrying can never recreate a + // renamed ivar, so this maps to fatal NATIVE_UNAVAILABLE — distinct from + // the nil-instance case below, which IS transient. Misclassifying this + // as NO_RUNTIME would make JS retry a permanently-broken reflection. + [BTLogger error:[NSString stringWithFormat:@"[SplitBundle] FATAL: _rctInstance ivar not found on %@ — bg segment loading is DISABLED.", [delegate class]]]; NSError *error = [NSError errorWithDomain:@"BackgroundThread" - code:3 - userInfo:@{NSLocalizedDescriptionKey: - @"Failed to register segment in background runtime"}]; + code:EBgMgrSegmentEvalErrorIvarMissing + userInfo:@{NSLocalizedDescriptionKey: @"_rctInstance ivar not found on bg delegate"}]; + if (completion) completion(error); + return; + } + id instance = object_getIvar(delegate, ivar); + if (!instance) { + [BTLogger error:@"[SplitBundle] bg loadSegment: background RCTInstance not available"]; + NSError *error = [NSError errorWithDomain:@"BackgroundThread" + code:EBgMgrSegmentEvalErrorNilInstance + userInfo:@{NSLocalizedDescriptionKey: @"Background RCTInstance not available"}]; if (completion) completion(error); + return; } + + // M4/M5: mmap + zero-copy buffer (retains the NSData for the async block). + NSError *readError = nil; + NSData *data = [NSData dataWithContentsOfFile:path + options:NSDataReadingMappedIfSafe + error:&readError]; + if (!data || data.length == 0) { + NSError *error = [NSError errorWithDomain:@"BackgroundThread" + code:EBgMgrSegmentEvalErrorIORead + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read bg segment at %@%@", path, readError ? [NSString stringWithFormat:@": %@", readError.localizedDescription] : @""]}]; + if (completion) completion(error); + return; + } + + // M6: synthetic `seg-.js` source URL for in-segment crash symbolication. + int segIdInt = segmentId.intValue; + NSString *sourceURL = [NSString stringWithFormat:@"seg-%d.js", segIdInt]; + + // C1: exactly-once guard shared (and retained) by the executor block and the + // watchdog. ARC-owned so the lock outlives both with no manual free. + BgMgrSettleGuard *settleGuard = [[BgMgrSettleGuard alloc] init]; + + CFAbsoluteTime dispatchStart = CFAbsoluteTimeGetCurrent(); + [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: evaluating %@ (id=%d, %lu bytes)", sourceURL, segIdInt, (unsigned long)data.length]]; + + // No __weak dance needed here (unlike the SharedRPC executor): this block + // does not re-dispatch onto the instance — it runs synchronously inside the + // instance's own runtime executor with a live `runtime &`, so by the time it + // executes the instance is necessarily still alive. + [instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) { + @autoreleasepool { + BOOL won = [settleGuard tryClaim]; + NSError *evalError = nil; + CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent(); + try { + auto buffer = std::make_shared(data); + runtime.evaluateJavaScript(buffer, [sourceURL UTF8String]); + double evalMs = (CFAbsoluteTimeGetCurrent() - evalStart) * 1000.0; + [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluated in %.1fms", sourceURL, evalMs]]; + } catch (const std::exception &e) { + evalError = [NSError errorWithDomain:@"BackgroundThread" + code:EBgMgrSegmentEvalErrorEvalThrow + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment evaluation failed for %@: %s", sourceURL, e.what()]}]; + [BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluation threw: %s", sourceURL, e.what()]]; + } catch (...) { + evalError = [NSError errorWithDomain:@"BackgroundThread" + code:EBgMgrSegmentEvalErrorEvalThrow + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment evaluation failed for %@ (unknown C++ exception)", sourceURL]}]; + [BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluation threw an unknown exception", sourceURL]]; + } + if (won) { + // Resolve AFTER eval — the ordering guarantee that fixes the race. + if (completion) completion(evalError); + } else { + [BTLogger warn:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluated AFTER watchdog already settled (bg entry was wedged >%.0fs)", sourceURL, kBgSegmentEvalWatchdogSeconds]]; + } + } + }]; + + // C1 watchdog — fires only on a genuine wedge (bg entry bundle never + // finished evaluating). Rejects with a retryable timeout so the JS loader + // re-attempts. settleGuard is retained by this block, so the lock stays + // valid even if the executor block later runs. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBgSegmentEvalWatchdogSeconds * NSEC_PER_SEC)), + dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + if ([settleGuard tryClaim]) { + [BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ WATCHDOG fired after %.0fs — bg runtime executor never ran (bg entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, kBgSegmentEvalWatchdogSeconds]]; + NSError *timeoutError = [NSError errorWithDomain:@"BackgroundThread" + code:EBgMgrSegmentEvalErrorTimeout + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment %@ eval timed out after %.0fs (buffered runtime executor never ran)", sourceURL, kBgSegmentEvalWatchdogSeconds]}]; + if (completion) completion(timeoutError); + } + }); + + double dispatchMs = (CFAbsoluteTimeGetCurrent() - dispatchStart) * 1000.0; + [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ dispatched in %.1fms (resolve fires after eval; watchdog %.0fs)", sourceURL, dispatchMs, kBgSegmentEvalWatchdogSeconds]]; } #pragma mark - Restart diff --git a/native-modules/react-native-split-bundle-loader/android/CMakeLists.txt b/native-modules/react-native-split-bundle-loader/android/CMakeLists.txt new file mode 100644 index 00000000..3c7ea54d --- /dev/null +++ b/native-modules/react-native-split-bundle-loader/android/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.13) + +# Native (JNI) side of the SplitBundleLoader module. +# +# WHY THIS EXISTS: +# The Kotlin `loadSegment` previously called `ReactContext.registerSegment`, +# whose completion callback fires BEFORE the segment bytecode is evaluated +# into the runtime (the C++ ReactInstance::registerSegment only ENQUEUES the +# eval onto the RuntimeScheduler and returns). That races Metro's +# `import().then(() => __r(moduleId))` and can produce a fatal, uncatchable +# "Requiring unknown module". +# +# This library evaluates the segment OURSELVES on the JS thread via the +# bridgeless CallInvoker (which receives `jsi::Runtime&`) and signals +# completion in that SAME callback, so eval + resolve are one atomic unit — +# mirroring the iOS `callFunctionOnBufferedRuntimeExecutor:` fix. +project(splitbundleloader) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_VERBOSE_MAKEFILE ON) + +add_library( + splitbundleloader + SHARED + src/main/cpp/SplitBundleLoaderJSI.cpp +) + +target_include_directories( + splitbundleloader + PRIVATE + src/main/cpp +) + +# jsi, reactnative (which carries CallInvoker / CallInvokerHolder headers and +# the merged libreactnative.so), and fbjni are all prefab targets exposed by +# the react-android AAR. See ReactAndroid build.gradle.kts prefab entries: +# - jsi (../ReactCommon/jsi/) +# - reactnative (turbomodule/ReactCommon/CallInvokerHolder.h, +# callinvoker/ReactCommon/CallInvoker.h, ...) +find_package(ReactAndroid REQUIRED CONFIG) +find_package(fbjni REQUIRED CONFIG) + +target_link_libraries( + splitbundleloader + android + log + fbjni::fbjni + ReactAndroid::jsi + ReactAndroid::reactnative +) diff --git a/native-modules/react-native-split-bundle-loader/android/build.gradle b/native-modules/react-native-split-bundle-loader/android/build.gradle index 7c23f785..edc97715 100644 --- a/native-modules/react-native-split-bundle-loader/android/build.gradle +++ b/native-modules/react-native-split-bundle-loader/android/build.gradle @@ -33,10 +33,36 @@ android { defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + // C++20 to match React Native's prefab targets. + cppFlags "-O2", "-frtti", "-fexceptions", "-std=c++20" + // The react-android / hermestooling prefab targets are built against + // the shared STL; without this the default static STL is selected and + // CMake configure fails ("static STL but library requires a shared + // STL [//ReactAndroid/hermestooling]"). Matches react-native-background-thread. + arguments "-DANDROID_STL=c++_shared" + // Only build the ABIs the app ships. + abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + } + } } buildFeatures { buildConfig true + // Consume react-android / fbjni prefab packages (jsi, reactnative, fbjni) + // from the AAR for the native (JNI) side. + prefab true + } + + // Native (JNI) side: evaluates split-bundle segments on the JS thread and + // resolves the promise strictly AFTER eval (fixes the "Requiring unknown + // module" race). See src/main/cpp/SplitBundleLoaderJSI.cpp. + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } } buildTypes { diff --git a/native-modules/react-native-split-bundle-loader/android/src/main/cpp/SplitBundleLoaderJSI.cpp b/native-modules/react-native-split-bundle-loader/android/src/main/cpp/SplitBundleLoaderJSI.cpp new file mode 100644 index 00000000..f9e08751 --- /dev/null +++ b/native-modules/react-native-split-bundle-loader/android/src/main/cpp/SplitBundleLoaderJSI.cpp @@ -0,0 +1,239 @@ +/* + * SplitBundleLoaderJSI.cpp + * + * JNI bridge that evaluates a Metro split-bundle segment into the CURRENT + * Hermes runtime and signals completion ONLY AFTER the segment's `__d(...)` + * module definitions have actually run. + * + * WHY THIS EXISTS — the "Requiring unknown module" race: + * The previous Kotlin implementation called `ReactContext.registerSegment`, + * which routes (bridgeless) through `ReactHostImpl.registerSegment` → + * `ReactInstance.registerSegment` → C++ `ReactInstance::registerSegment`, and + * that only does `runtimeScheduler_->scheduleWork([]{ evaluateJavaScript() })` + * — i.e. it ENQUEUES the eval and returns. `ReactHostImpl.registerSegment` + * then invokes the completion callback immediately on `Task.IMMEDIATE_EXECUTOR`, + * so the loadSegment promise resolves BEFORE the segment is evaluated. Metro's + * `import().then(() => __r(moduleId))` microtask can therefore run `__r` before + * the module table is populated → a fatal, uncatchable "Requiring unknown + * module" inside metroRequire. + * + * THE FIX (mirrors iOS callFunctionOnBufferedRuntimeExecutor:): + * We evaluate the segment OURSELVES on the JS thread via the bridgeless + * CallInvoker. `CallInvoker::invokeAsync(CallFunc&&)` schedules the callback + * onto the SAME RuntimeScheduler that registerSegment would have used, and the + * callback receives `jsi::Runtime&`. We read the segment file, call + * `runtime.evaluateJavaScript(...)`, and invoke the completion callback from + * INSIDE that same block, strictly AFTER eval. Eval + completion are one atomic + * unit of work on the JS thread, so any subsequent `__r(moduleId)` is + * guaranteed to find the module. + * + * SEGMENT FORMAT: OneKey segments are standalone-evaluatable Metro bundles + * (top-level `__d(moduleId, factory, deps)` calls; the `.seg.hbc` is just the + * Hermes-compiled form of the same `.seg.js`). C++ ReactInstance::registerSegment + * itself just calls `runtime.evaluateJavaScript(buffer, ...)` with no RAM/indexed + * segment manifest wiring, confirming these are evaluatable as plain scripts. + */ + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#define SBL_LOG_TAG "SplitBundleLoader" +#define SBL_LOGI(...) \ + __android_log_print(ANDROID_LOG_INFO, SBL_LOG_TAG, __VA_ARGS__) +#define SBL_LOGW(...) \ + __android_log_print(ANDROID_LOG_WARN, SBL_LOG_TAG, __VA_ARGS__) + +namespace facebook::react::splitbundleloader { + +namespace { + +// Reads the whole file at `path` into a std::string. Returns false on failure. +bool readFileToString(const std::string& path, std::string& out) { + FILE* f = std::fopen(path.c_str(), "rb"); + if (f == nullptr) { + return false; + } + if (std::fseek(f, 0, SEEK_END) != 0) { + std::fclose(f); + return false; + } + long size = std::ftell(f); + if (size < 0) { + std::fclose(f); + return false; + } + if (std::fseek(f, 0, SEEK_SET) != 0) { + std::fclose(f); + return false; + } + out.resize(static_cast(size)); + size_t read = (size == 0) + ? 0 + : std::fread(&out[0], 1, static_cast(size), f); + std::fclose(f); + return read == static_cast(size); +} + +} // namespace + +// Java callback contract — implemented in Kotlin as +// SplitBundleLoaderModule.SegmentEvalCallback. `onComplete(null)` on success, +// `onComplete(errorMessage)` on failure. Invoked exactly once. +// +// Error-message prefix convention (read by the Kotlin side to pick the contract +// reject code): a message prefixed with "IO_ERROR:" is a segment file read/mmap +// failure → SPLIT_BUNDLE_IO_ERROR (non-retryable). Any other message is a +// segment JS/Hermes eval throw → SPLIT_BUNDLE_EVAL_ERROR (non-retryable). +struct JSegmentEvalCallback + : jni::JavaClass { + static constexpr auto kJavaDescriptor = + "Lcom/splitbundleloader/SplitBundleLoaderModule$SegmentEvalCallback;"; + + // `error` empty → success (passes Java null); non-empty → failure (passes the + // message). We always pass null on success and a non-empty message on failure, + // so this mapping is unambiguous. `const` so it can be invoked through a + // (const) global_ref / alias_ref. + void onComplete(const std::string& error) const { + static const auto method = + javaClassStatic()->getMethod)>( + "onComplete"); + jni::local_ref arg = + error.empty() ? jni::local_ref(nullptr) + : jni::make_jstring(error); + method(self(), arg); + } +}; + +class SplitBundleLoaderJSI + : public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/splitbundleloader/SplitBundleLoaderModule;"; + + // Schedules evaluation of the segment at `segmentPath` onto the JS thread via + // the bridgeless CallInvoker, then invokes `callback` from INSIDE the same JS + // thread block, strictly AFTER the segment has been evaluated. This is the + // ordering guarantee that fixes the "Requiring unknown module" race. + // + // Does NOT block the calling (native modules) thread — returns immediately + // after scheduling. Resolution happens later on the JS thread. + static void nativeEvaluateSegment( + jni::alias_ref /* unused */, + jni::alias_ref callInvokerHolder, + jni::alias_ref segmentPath, + jni::alias_ref sourceURL, + jni::alias_ref callback) { + // Capture everything we need as values / global refs because the callback + // runs later on a different thread. + auto globalCallback = jni::make_global(callback); + + if (!callInvokerHolder) { + globalCallback->onComplete("CallInvokerHolder is null"); + return; + } + + std::shared_ptr callInvoker = + callInvokerHolder->cthis()->getCallInvoker(); + if (!callInvoker) { + globalCallback->onComplete("CallInvoker is null"); + return; + } + + std::string path = segmentPath ? segmentPath->toStdString() : std::string(); + std::string url = + sourceURL ? sourceURL->toStdString() : std::string("segment"); + + if (path.empty()) { + globalCallback->onComplete("Empty segment path"); + return; + } + + // F: Read the segment file HERE, on the calling (native module) thread, + // BEFORE invokeAsync. Doing the disk read inside the JS-thread callback + // would block the JS thread on I/O and race the Kotlin watchdog. We move + // the already-read buffer into the lambda so only evaluateJavaScript + + // completion run on the JS thread (mirrors iOS, which mmaps off-thread and + // only evaluates on the runtime thread). The read error is surfaced as a + // dedicated IO error so JS can classify it as NON-retryable. + std::string source; + bool ioOk = readFileToString(path, source); + if (!ioOk) { + globalCallback->onComplete("IO_ERROR:Failed to read segment file: " + path); + return; + } + if (source.empty()) { + globalCallback->onComplete("IO_ERROR:Empty segment file: " + path); + return; + } + + // CallFunc = std::function; this runs on the JS thread + // on the SAME RuntimeScheduler the segment registration would have used. + callInvoker->invokeAsync([globalCallback, + source = std::move(source), + url = std::move(url)](jsi::Runtime& runtime) { + std::string error; + try { + { + SBL_LOGI( + "[SplitBundle] evaluating segment %s (%zu bytes)", + url.c_str(), + source.size()); + auto buffer = std::make_shared(std::move(source)); + // Evaluate the segment into the CURRENT runtime. evaluateJavaScript + // runs the segment's top-level __d(...) module definitions + // synchronously on this JS thread before returning. + runtime.evaluateJavaScript(std::move(buffer), url); + SBL_LOGI("[SplitBundle] segment %s evaluated", url.c_str()); + } + } catch (const jsi::JSError& e) { + error = std::string("Segment evaluation JSError for ") + url + ": " + + e.getMessage(); + SBL_LOGW("[SplitBundle] %s", error.c_str()); + } catch (const std::exception& e) { + error = std::string("Segment evaluation failed for ") + url + ": " + + e.what(); + SBL_LOGW("[SplitBundle] %s", error.c_str()); + } catch (...) { + error = std::string("Segment evaluation failed for ") + url + + " (unknown C++ exception)"; + SBL_LOGW("[SplitBundle] %s", error.c_str()); + } + + // Attach to the JVM for this (JS) thread before calling back into Java. + // The JS thread is a native (pthread) thread not implicitly attached to + // the JVM; ThreadScope ensures a valid JNIEnv for the callback. + jni::ThreadScope ts; + // Resolve/reject from INSIDE this same JS-thread block, strictly AFTER + // eval above — the ordering guarantee that fixes the race. + globalCallback->onComplete(error); + }); + } + + static void registerNatives() { + // Static JNI methods on a plain JavaClass (NOT a HybridClass): bind via + // the class's registerNatives, not registerHybrid. + javaClassStatic()->registerNatives({ + makeNativeMethod( + "nativeEvaluateSegment", + SplitBundleLoaderJSI::nativeEvaluateSegment), + }); + } +}; + +} // namespace facebook::react::splitbundleloader + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /* reserved */) { + return facebook::jni::initialize(vm, [] { + facebook::react::splitbundleloader::SplitBundleLoaderJSI::registerNatives(); + }); +} diff --git a/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt b/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt index c138d5a0..bdaac147 100644 --- a/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt +++ b/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt @@ -5,13 +5,18 @@ import android.content.res.AssetManager import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.turbomodule.core.CallInvokerHolderImpl import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.security.MessageDigest import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicBoolean +import android.os.Handler +import android.os.Looper /** * TurboModule entry point for SplitBundleLoader. @@ -26,9 +31,65 @@ import java.util.concurrent.Semaphore class SplitBundleLoaderModule(reactContext: ReactApplicationContext) : NativeSplitBundleLoaderSpec(reactContext) { + /** + * Completion contract invoked by the native (JNI) side AFTER the segment + * has been evaluated into the runtime. Called from the JS thread. + * + * @param error null on success; a non-empty message on failure. A failure + * message prefixed with "IO_ERROR:" denotes a segment file read/mmap + * failure (mapped to SPLIT_BUNDLE_IO_ERROR); any other message denotes a + * segment JS/Hermes eval throw (mapped to SPLIT_BUNDLE_EVAL_ERROR). + */ + fun interface SegmentEvalCallback { + fun onComplete(error: String?) + } + companion object { const val NAME = "SplitBundleLoader" private const val BUILTIN_EXTRACT_DIR = "onekey-builtin-segments" + + // Bounded watchdog: if the JS thread is wedged and the segment eval + // never runs, reject rather than leaving the JS promise pending forever. + // Generous because a cold JS thread under load can legitimately take a + // while to drain to our scheduled eval. + private const val SEGMENT_EVAL_TIMEOUT_MS = 30_000L + + // Loads the JNI library that provides nativeEvaluateSegment. Wrapped so + // a missing/failed load is detectable: loadSegment then fail-closes with + // SPLIT_BUNDLE_NATIVE_UNAVAILABLE instead of crashing (we deliberately do + // NOT fall back to the legacy registerSegment path). + @JvmStatic + @Volatile + var nativeLibLoaded: Boolean = false + private set + + init { + nativeLibLoaded = try { + System.loadLibrary("splitbundleloader") + true + } catch (e: Throwable) { + SBLLogger.warn("[SplitBundle] failed to load native lib 'splitbundleloader': ${e.message}") + false + } + } + + /** + * JNI entry point. Schedules evaluation of the segment at [segmentPath] + * onto the JS thread via the bridgeless CallInvoker and invokes + * [callback] from inside that same JS-thread block, strictly AFTER the + * segment's `__d(...)` module definitions have run. This is the ordering + * guarantee that fixes the "Requiring unknown module" race. + * + * Returns immediately; [callback] fires later on the JS thread. + */ + @JvmStatic + @OptIn(FrameworkAPI::class) + external fun nativeEvaluateSegment( + callInvokerHolder: CallInvokerHolderImpl, + segmentPath: String, + sourceURL: String, + callback: SegmentEvalCallback + ) // #18: Limit concurrent asset extractions to avoid I/O contention private const val MAX_CONCURRENT_EXTRACTS = 2 private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS) @@ -197,6 +258,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) : // loadSegment // ----------------------------------------------------------------------- + @OptIn(FrameworkAPI::class) override fun loadSegment( segmentId: Double, segmentKey: String, @@ -229,15 +291,119 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) : return } - // Use ReactContext.registerSegment which works in both bridge - // and bridgeless modes. In bridge mode it delegates to - // CatalystInstance; in bridgeless mode it delegates to ReactHost. val reactContext = reactApplicationContext val segStart = System.nanoTime() - reactContext.registerSegment(segId, absolutePath) { - val segMs = (System.nanoTime() - segStart) / 1_000_000.0 - SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) registered in ${String.format("%.1f", segMs)}ms") - promise.resolve(null) + + // PRIMARY PATH (fixes the "Requiring unknown module" race): + // Evaluate the segment OURSELVES on the JS thread via the bridgeless + // CallInvoker and resolve ONLY after eval completes. We intentionally + // do NOT use ReactContext.registerSegment here: that resolves its + // callback immediately (on Task.IMMEDIATE_EXECUTOR) while the actual + // segment eval is merely ENQUEUED onto the RuntimeScheduler, so the + // promise resolves BEFORE the segment's __d(...) module definitions + // run — Metro's import().then(() => __r(moduleId)) can then hit a + // fatal "Requiring unknown module". nativeEvaluateSegment collapses + // eval + resolve into one JS-thread block (mirrors the iOS + // callFunctionOnBufferedRuntimeExecutor: fix). + val callInvokerHolder = + reactContext.jsCallInvokerHolder as? CallInvokerHolderImpl + + if (nativeLibLoaded && callInvokerHolder != null) { + val sourceURL = File(absolutePath).name + // One-shot guard: native success/error AND the watchdog can each + // try to settle the promise; only the first wins. + val settled = AtomicBoolean(false) + + // Bounded watchdog: if the JS thread never drains to our eval, + // reject instead of hanging the JS promise forever. + val watchdog = Handler(Looper.getMainLooper()) + val timeoutRunnable = Runnable { + if (settled.compareAndSet(false, true)) { + SBLLogger.warn("[SplitBundle] segment $segmentKey (id=$segId) eval timed out after ${SEGMENT_EVAL_TIMEOUT_MS}ms") + // D: Reject with the RETRYABLE timeout code from the shared + // contract (NOT SPLIT_BUNDLE_EVAL_TIMEOUT). A wedged JS + // thread is a transient condition; using the retryable + // SPLIT_BUNDLE_TIMEOUT lets the JS loader re-attempt rather + // than caching this segment as a permanent failure. + promise.reject( + "SPLIT_BUNDLE_TIMEOUT", + "Segment eval timed out: $segmentKey (id=$segId)" + ) + } + } + watchdog.postDelayed(timeoutRunnable, SEGMENT_EVAL_TIMEOUT_MS) + + // Catch Throwable, NOT just Exception, around the native call. + // nativeEvaluateSegment is `external`: if the symbol is + // registered-but-broken (or the lib half-loaded) the JVM raises + // UnsatisfiedLinkError, which is a java.lang.Error — it would + // sail past the outer `catch (e: Exception)` and leave the JS + // promise unsettled forever (the watchdog would eventually fire, + // but only after a 30s hang). Settle exactly once here via the + // same AtomicBoolean one-shot guard and cancel the watchdog. + try { + nativeEvaluateSegment( + callInvokerHolder, + absolutePath, + sourceURL + ) { error -> + if (settled.compareAndSet(false, true)) { + watchdog.removeCallbacks(timeoutRunnable) + if (error == null) { + val segMs = (System.nanoTime() - segStart) / 1_000_000.0 + SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) evaluated in ${String.format("%.1f", segMs)}ms (eval-complete)") + promise.resolve(null) + } else { + // The native side prefixes I/O failures (file + // read/mmap) with "IO_ERROR:" so we can map them to + // SPLIT_BUNDLE_IO_ERROR (non-retryable). Everything + // else is a segment JS/Hermes eval throw → + // SPLIT_BUNDLE_EVAL_ERROR (also non-retryable, a real + // bug in the segment's own code). + if (error.startsWith("IO_ERROR:")) { + val msg = error.removePrefix("IO_ERROR:") + SBLLogger.warn("[SplitBundle] segment $segmentKey (id=$segId) IO failed: $msg") + promise.reject("SPLIT_BUNDLE_IO_ERROR", msg) + } else { + SBLLogger.warn("[SplitBundle] segment $segmentKey (id=$segId) eval failed: $error") + promise.reject("SPLIT_BUNDLE_EVAL_ERROR", error) + } + } + } + } + } catch (t: Throwable) { + // UnsatisfiedLinkError / any native dispatch failure. The + // callback never fired (the native side never got far enough + // to invoke it), so settle the promise ourselves — exactly + // once — as a fatal, non-retryable native fault. + if (settled.compareAndSet(false, true)) { + watchdog.removeCallbacks(timeoutRunnable) + SBLLogger.error("[SplitBundle] FATAL: nativeEvaluateSegment threw for $segmentKey (id=$segId): ${t.message}") + promise.reject( + "SPLIT_BUNDLE_NATIVE_UNAVAILABLE", + "Native segment eval threw: $segmentKey (id=$segId): ${t.message}" + ) + } + } + } else { + // B: FAIL CLOSED. On this app (RN 0.81, NewArch / bridgeless), + // jsCallInvokerHolder is always a CallInvokerHolderImpl and the + // JNI lib ships in the AAR, so reaching here means a genuine + // native fault (loadLibrary failed or the holder cast failed) — + // NOT a benign legacy-bridge mode (this app never runs the old + // bridge). We deliberately do NOT fall back to the legacy + // ReactContext.registerSegment path: that resolves before eval + // and reintroduces the intermittent, uncatchable "Requiring + // unknown module" native crash. Failing closed (segment load + // unavailable → JS error boundary) is strictly better than an + // intermittent crash, so we reject with the dedicated + // NATIVE_UNAVAILABLE code (non-retryable; the JS loader will not + // hammer-retry a structurally broken native primitive). + SBLLogger.error("[SplitBundle] FATAL: native eval primitive unavailable (libLoaded=$nativeLibLoaded, holderCast=${callInvokerHolder != null}) for $segmentKey (id=$segId); failing closed instead of using the race-prone legacy registerSegment fallback") + promise.reject( + "SPLIT_BUNDLE_NATIVE_UNAVAILABLE", + "Native segment eval unavailable (libLoaded=$nativeLibLoaded, holderCast=${callInvokerHolder != null}): $segmentKey (id=$segId)" + ) } } catch (e: Exception) { promise.reject("SPLIT_BUNDLE_LOAD_ERROR", e.message, e) diff --git a/native-modules/react-native-split-bundle-loader/ios/SplitBundleLoader.mm b/native-modules/react-native-split-bundle-loader/ios/SplitBundleLoader.mm index a35a91fe..f5e0e42b 100644 --- a/native-modules/react-native-split-bundle-loader/ios/SplitBundleLoader.mm +++ b/native-modules/react-native-split-bundle-loader/ios/SplitBundleLoader.mm @@ -5,7 +5,105 @@ #import #import #import +#import #include +#include + +namespace { + +// Zero-copy jsi::Buffer over an NSData (M4/M5). +// +// WHY: the previous code did `std::string(data.bytes, data.length)` inside a +// jsi::StringBuffer — a FULL second copy of the (potentially multi-MB) segment +// bytes on top of whatever the NSData read already cost. For the hot segment +// path that doubles peak memory and adds a memcpy on the JS thread. +// +// This buffer instead RETAINS the NSData and hands jsi the NSData's own bytes +// directly. Combined with NSDataReadingMappedIfSafe at the read site, the +// segment is mmap'd and Hermes parses straight from the mapped pages — no heap +// copy at all on the happy path. The retained NSData also makes the buffer's +// lifetime explicit and self-owned: the executor block runs ASYNCHRONOUSLY +// (it's buffered until the entry bundle finishes), so the NSData MUST outlive +// the originating scope. Holding it inside the Buffer (which jsi keeps alive +// via the shared_ptr for the duration of evaluateJavaScript) guarantees that. +class NSDataJSIBuffer : public facebook::jsi::Buffer { + public: + explicit NSDataJSIBuffer(NSData *data) : data_(data) {} + size_t size() const override { return data_.length; } + const uint8_t *data() const override { + return static_cast(data_.bytes); + } + + private: + NSData *data_; // strong retain (ARC) — keeps mmap/heap bytes alive for jsi. +}; + +} // namespace + +// Exactly-once settle guard for the C1 watchdog. Wraps a BOOL behind an +// os_unfair_lock. WHY AN OBJECT (not a __block BOOL + manual free): the executor +// block and the watchdog dispatch_after BOTH capture it and BOTH may run +// (dispatch_after is not cancellable, and on a wedge the executor can run AFTER +// the watchdog). Tying the lock's lifetime to ARC — both blocks retain this +// object, it deallocs only after both release — eliminates the use-after-free a +// manual free() in either block would cause. `tryClaim` returns YES to exactly +// one caller; the loser does nothing. +@interface SBLSettleGuard : NSObject +- (BOOL)tryClaim; +@end + +@implementation SBLSettleGuard { + os_unfair_lock _lock; + BOOL _settled; +} +- (instancetype)init { + if (self = [super init]) { + _lock = OS_UNFAIR_LOCK_INIT; + _settled = NO; + } + return self; +} +- (BOOL)tryClaim { + os_unfair_lock_lock(&_lock); + BOOL won = !_settled; + if (won) { + _settled = YES; + } + os_unfair_lock_unlock(&_lock); + return won; +} +@end + +// Watchdog window for the buffered runtime executor (C1). Post-entry eval is +// sub-millisecond, so this only ever elapses on a genuine wedge (entry bundle +// never finished evaluating) — never on a healthy slow device. +// +// Fix G: 30s (matching Android and the bg-runtime watchdog). The executor stays +// buffered until the main ENTRY bundle finishes evaluating; on a slow/throttled +// cold start that entry eval can itself exceed 10s, which would falsely trip the +// watchdog on a segment load that was about to succeed. 30s keeps the +// genuine-wedge safety net while leaving headroom for slow cold starts. +static const NSTimeInterval kSegmentEvalWatchdogSeconds = 30.0; + +// NSError `code` values produced by +evaluateSegmentAtPath:... . These are +// DISTINCT (L8) so loadSegment: can map each to its own JS reject code and JS +// can classify retryable-vs-fatal: +// - HostMissing / NilInstance → SPLIT_BUNDLE_NO_RUNTIME (retryable): +// the runtime/host simply wasn't ready yet; a later attempt may succeed. +// - IvarMissing → SPLIT_BUNDLE_NATIVE_UNAVAILABLE (NOT retryable): a renamed +// ivar is a structural/build defect that no retry can fix. +// - Timeout → SPLIT_BUNDLE_TIMEOUT (retryable): buffered executor never ran. +// - IORead → SPLIT_BUNDLE_IO_ERROR: file read/mmap failed. +// - EvalThrow → SPLIT_BUNDLE_EVAL_ERROR (NOT retryable): a real bug in the +// segment's own JS/Hermes code; retrying just re-throws. +typedef NS_ENUM(NSInteger, ESegmentEvalError) { + ESegmentEvalErrorHostMissing = 1, + ESegmentEvalErrorIvarMissing = 2, + ESegmentEvalErrorNilInstance = 3, + ESegmentEvalErrorIORead = 4, + ESegmentEvalErrorEvalThrow = 5, + ESegmentEvalErrorTimeout = 6, +}; @implementation SplitBundleLoader @@ -127,30 +225,200 @@ + (nullable NSString *)otaBundlePath return result; } -// MARK: - Segment registration helper +// MARK: - RCTHost resolution helper -/// Registers a segment with the current runtime via bridgeless (RCTHost) architecture (#13). -/// -/// Thread safety (#57): This method is called from the TurboModule (JS thread). -/// No queue dispatch is needed. -+ (BOOL)registerSegment:(int)segmentId path:(NSString *)path error:(NSError **)outError +/// Resolves the bridgeless RCTHost via the AppDelegate's `reactHost` accessor +/// (New Architecture). Returns nil when the host is unavailable so callers can +/// reject gracefully. Extracted so both segment registration and the +/// evaluate-then-resolve path (#race) share one lookup. ++ (nullable RCTHost *)currentReactHost { - // Bridgeless (New Architecture): get RCTHost via AppDelegate id appDelegate = [UIApplication sharedApplication].delegate; if ([appDelegate respondsToSelector:NSSelectorFromString(@"reactHost")]) { RCTHost *host = [appDelegate performSelector:NSSelectorFromString(@"reactHost")]; - if (host && [host respondsToSelector:@selector(registerSegmentWithId:path:)]) { - [host registerSegmentWithId:@(segmentId) path:path]; - return YES; + if (host) { + return host; } } + return nil; +} + +// MARK: - Segment evaluation helper (resolve-after-eval, fixes lazy-segment race) - if (outError) { - *outError = [NSError errorWithDomain:@"SplitBundleLoader" - code:1 - userInfo:@{NSLocalizedDescriptionKey: @"RCTHost not available for segment registration"}]; +/// Evaluates a segment bundle into the CURRENT runtime and invokes `onEvaluated` +/// from INSIDE the same runtime-executor block, immediately after the segment's +/// `__d(...)` module definitions have run. +/// +/// WHY THIS EXISTS — the "Requiring unknown module" race (#race): +/// The previous implementation called `RCTHost registerSegmentWithId:path:`, +/// which routes to `ReactInstance::registerSegment` → +/// `runtimeScheduler_->scheduleWork([]{ runtime.evaluateJavaScript(segment) })`. +/// That only ENQUEUES the eval onto the runtime scheduler and returns; the +/// loadSegment promise was resolved IMMEDIATELY afterwards. Metro's +/// `import().then(() => __r(moduleId))` microtask could therefore run `__r` +/// BEFORE the scheduled eval populated the module table → a FATAL, uncatchable +/// "Requiring unknown module" inside metroRequire. +/// +/// We cannot fix this by merely scheduling `resolve` after `registerSegment` on +/// the same executor: `RuntimeScheduler_Modern::scheduleWork` pushes +/// ImmediatePriority tasks into a `std::priority_queue` keyed only on +/// `expirationTime` (RuntimeScheduler_Modern.cpp / Task.h `TaskPriorityComparer`). +/// `std::priority_queue` is NOT stable, so two same-tick tasks have UNDEFINED +/// relative order — FIFO is not guaranteed under the Modern scheduler. +/// +/// Instead we evaluate the segment OURSELVES inside a single +/// `callFunctionOnBufferedRuntimeExecutor:` block (exactly like +/// `loadEntryBundle:`) and signal completion in that SAME block. Eval and the +/// resolve are now one atomic unit of work — there is no cross-task ordering to +/// lose, so any subsequent `__r(moduleId)` is guaranteed to find the module. +/// +/// This is safe because OneKey segments are STANDALONE-EVALUATABLE Metro +/// bundles (the serializer emits `baseJSBundle`/`bundleToString` output — plain +/// top-level `__d(moduleId, factory, deps)` calls, NOT Hermes RAM/indexed +/// segments that would require the registerSegment manifest wiring). The paired +/// `.seg.hbc` is just the Hermes-compiled form of that same source, which +/// `evaluateJavaScript` runs identically to the entry bundle's `.hbc`. +/// +/// `onEvaluated` is invoked EXACTLY ONCE with nil on success or a populated +/// NSError on failure. The NSError `code` is meaningful and mapped by the caller +/// to a distinct JS reject code (see ESegmentEvalError below + loadSegment:): +/// callers use it to classify retryable (no-runtime / timeout) vs fatal +/// (eval-throw / IO) failures. ++ (void)evaluateSegmentAtPath:(NSString *)bundlePath + segmentId:(int)segmentId + segmentKey:(NSString *)segmentKey + onEvaluated:(void (^)(NSError *_Nullable error))onEvaluated +{ + RCTHost *host = [SplitBundleLoader currentReactHost]; + if (!host) { + onEvaluated([NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorHostMissing + userInfo:@{NSLocalizedDescriptionKey: @"RCTHost not available for segment evaluation"}]); + return; } - return NO; + + // Reach the RCTInstance the same way loadEntryBundle: does, so we can use + // the buffered runtime executor primitive (callFunctionOnBufferedRuntimeExecutor:). + Ivar ivar = class_getInstanceVariable([host class], "_instance"); + if (!ivar) { + // L7: a missing `_instance` ivar means a future RN bump renamed/removed + // the field our reflection depends on. That silently disables ALL + // segment loading, so log loudly (error, with the class name) instead of + // failing quietly — the next RN upgrade then surfaces visibly in logs. + [SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] FATAL: _instance ivar not found on %@ — segment loading is DISABLED. An RN upgrade likely renamed this private field; SplitBundleLoader reflection must be updated.", [host class]]]; + onEvaluated([NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorIvarMissing + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"_instance ivar not found on %@", [host class]]}]); + return; + } + + RCTInstance *instance = object_getIvar(host, ivar); + if (!instance) { + onEvaluated([NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorNilInstance + userInfo:@{NSLocalizedDescriptionKey: @"RCTInstance is nil"}]); + return; + } + + // M4/M5: mmap the segment (NSDataReadingMappedIfSafe) instead of a full + // heap read, then wrap it zero-copy in NSDataJSIBuffer (which retains the + // NSData). Net effect: no second copy, and the bytes stay alive for the + // async executor block because the buffer owns the NSData. + NSError *readError = nil; + NSData *data = [NSData dataWithContentsOfFile:bundlePath + options:NSDataReadingMappedIfSafe + error:&readError]; + if (!data || data.length == 0) { + onEvaluated([NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorIORead + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read segment at %@%@", bundlePath, readError ? [NSString stringWithFormat:@": %@", readError.localizedDescription] : @""]}]); + return; + } + + // M6: preserve a meaningful eval source URL so in-segment crash frames are + // symbolicated. RN's registerSegment used + // `JSExecutor::getSyntheticBundlePath(segmentId, segmentPath)`, which for a + // non-main segment yields `seg-.js` (see cxxreact/JSExecutor.cpp). We + // replicate that exact form so Hermes/Metro attribute frames to the segment + // the same way the native path did — using `lastPathComponent` here would + // degrade symbolication. + NSString *sourceURL = [NSString stringWithFormat:@"seg-%d.js", segmentId]; + CFAbsoluteTime dispatchStart = CFAbsoluteTimeGetCurrent(); + [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadSegment: evaluating %@ (key=%@, %lu bytes)", sourceURL, segmentKey, (unsigned long)data.length]]; + + // C1: the executor block below is BUFFERED — callFunctionOnBufferedRuntimeExecutor + // does not run it until the main entry bundle finishes evaluating + // (RCTInstance/ReactInstance.cpp). If a segment load is requested before the + // entry completes (early startup, reload teardown, host swap) and the entry + // never completes, the block NEVER runs → onEvaluated would never fire → + // the JS promise hangs forever and inflightSegments wedges with no timeout. + // + // Guard: invoke `onEvaluated` EXACTLY ONCE. SBLSettleGuard wraps a BOOL + // behind an os_unfair_lock; whichever racer wins — the executor block (happy + // path) or the watchdog timer (genuine wedge) — gets YES from tryClaim and + // settles the promise; the loser does nothing. The guard is an ARC object + // captured (retained) by BOTH blocks, so its lock outlives both with no + // manual free() and therefore no use-after-free (both blocks can run, in + // either order, on a wedge). + SBLSettleGuard *settleGuard = [[SBLSettleGuard alloc] init]; + + [instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) { + @autoreleasepool { + // If the watchdog already fired (entry took >30s then unwedged), + // the JS promise is already rejected — still evaluate the segment + // (the module table benefits) but don't double-settle. + BOOL won = [settleGuard tryClaim]; + NSError *evalError = nil; + CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent(); + try { + auto buffer = std::make_shared(data); + runtime.evaluateJavaScript(buffer, [sourceURL UTF8String]); + double evalMs = (CFAbsoluteTimeGetCurrent() - evalStart) * 1000.0; + [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated in %.1fms", sourceURL, evalMs]]; + } catch (const std::exception &e) { + // L8: a JS/Hermes eval throw is a REAL BUG in the segment's own + // code, not a transient runtime-readiness problem. Mapped to a + // NON-retryable code by the caller so JS caches it as failed. + evalError = [NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorEvalThrow + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment evaluation failed for %@: %s", sourceURL, e.what()]}]; + [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluation threw: %s", sourceURL, e.what()]]; + } catch (...) { + evalError = [NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorEvalThrow + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment evaluation failed for %@ (unknown C++ exception)", sourceURL]}]; + [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluation threw an unknown exception", sourceURL]]; + } + if (won) { + // Resolve/reject the JS promise from INSIDE this same block, + // strictly AFTER the segment eval above — the ordering guarantee + // that fixes the "Requiring unknown module" race (see method doc). + onEvaluated(evalError); + } else { + [SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated AFTER watchdog already settled (entry was wedged >%.0fs)", sourceURL, kSegmentEvalWatchdogSeconds]]; + } + } + }]; + + // C1 watchdog. Fires only on a genuine wedge: in steady state the entry + // bundle is long done and the buffered block runs sub-millisecond, so the + // guard is already claimed (tryClaim returns NO) long before this elapses. + // On a real wedge it settles the promise with a distinct, RETRYABLE timeout + // error so the JS loader can re-attempt instead of hanging inflightSegments + // forever. The block retains settleGuard, so the lock stays valid even if + // the executor block later runs (entry finally evaluates). + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kSegmentEvalWatchdogSeconds * NSEC_PER_SEC)), + dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + if ([settleGuard tryClaim]) { + [SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ (key=%@) WATCHDOG fired after %.0fs — runtime executor never ran (entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, segmentKey, kSegmentEvalWatchdogSeconds]]; + onEvaluated([NSError errorWithDomain:@"SplitBundleLoader" + code:ESegmentEvalErrorTimeout + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment %@ eval timed out after %.0fs (buffered runtime executor never ran)", segmentKey, kSegmentEvalWatchdogSeconds]}]); + } + }); + + double dispatchMs = (CFAbsoluteTimeGetCurrent() - dispatchStart) * 1000.0; + [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ dispatched in %.1fms (resolve fires after eval; watchdog %.0fs)", sourceURL, dispatchMs, kSegmentEvalWatchdogSeconds]]; } // MARK: - getRuntimeBundleContext @@ -280,7 +548,12 @@ + (void)loadEntryBundle:(NSString *)bundlePath inHost:(id)host return; } - NSData *data = [NSData dataWithContentsOfFile:bundlePath]; + // M5: mmap + zero-copy (NSDataJSIBuffer retains the NSData for the async block), + // mirroring the segment path. Entry bundle is the largest single read, so this + // saves the biggest single copy. + NSData *data = [NSData dataWithContentsOfFile:bundlePath + options:NSDataReadingMappedIfSafe + error:nil]; if (!data || data.length == 0) { [SBLLogger warn:[NSString stringWithFormat:@"loadEntryBundle: failed to read bundle at %@", bundlePath]]; return; @@ -293,9 +566,8 @@ + (void)loadEntryBundle:(NSString *)bundlePath inHost:(id)host [instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) { @autoreleasepool { CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent(); - auto buffer = std::make_shared( - std::string(static_cast(data.bytes), data.length)); - runtime.evaluateJavaScript(std::move(buffer), [sourceURL UTF8String]); + auto buffer = std::make_shared(data); + runtime.evaluateJavaScript(buffer, [sourceURL UTF8String]); double evalMs = (CFAbsoluteTimeGetCurrent() - evalStart) * 1000.0; [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadEntryBundle: %@ evaluated in %.1fms", sourceURL, evalMs]]; } @@ -342,17 +614,62 @@ - (void)loadSegment:(double)segmentId return; } - // Register segment (#13: supports both bridge and bridgeless) - NSError *regError = nil; - if ([SplitBundleLoader registerSegment:segId path:absolutePath error:®Error]) { + // Evaluate the segment into the current runtime and resolve ONLY after + // its module definitions have actually run (#race). We intentionally do + // NOT use registerSegmentWithId: + immediate resolve here: that resolves + // before the scheduler-enqueued eval completes, so Metro's + // `import().then(() => __r(moduleId))` can hit "Requiring unknown module" + // (a fatal, uncatchable crash). See +evaluateSegmentAtPath:... doc for + // the full ordering rationale (Modern scheduler priority_queue is not + // FIFO, so we collapse eval+resolve into one runtime-executor block). + // segId is threaded through for the synthetic eval source URL (M6) and + // for log parity with the previous register flow. + [SplitBundleLoader evaluateSegmentAtPath:absolutePath + segmentId:segId + segmentKey:segmentKey + onEvaluated:^(NSError *_Nullable evalError) { + if (evalError) { + // L8: map the helper's distinct NSError code to a distinct JS + // reject code so JS can classify retryable vs fatal (see + // ESegmentEvalError + installProdBundleLoader.ts H3). + NSString *rejectCode; + switch ((ESegmentEvalError)evalError.code) { + case ESegmentEvalErrorTimeout: + rejectCode = @"SPLIT_BUNDLE_TIMEOUT"; // retryable + break; + case ESegmentEvalErrorIORead: + rejectCode = @"SPLIT_BUNDLE_IO_ERROR"; // fatal + break; + case ESegmentEvalErrorEvalThrow: + rejectCode = @"SPLIT_BUNDLE_EVAL_ERROR"; // fatal (segment bug) + break; + case ESegmentEvalErrorIvarMissing: + // Fix 2: `_instance` ivar reflection failed — STRUCTURAL/ + // PERMANENT. An RN version bump renamed/removed the + // private field our reflection depends on, so segment + // loading is disabled until the native code is updated. + // Retrying can NEVER recreate a renamed ivar, so this is + // fatal NATIVE_UNAVAILABLE — NOT retryable NO_RUNTIME. By + // contrast HostMissing / NilInstance below are genuinely + // transient (host/instance not up yet → a later attempt + // may succeed). + rejectCode = @"SPLIT_BUNDLE_NATIVE_UNAVAILABLE"; // fatal + break; + case ESegmentEvalErrorHostMissing: + case ESegmentEvalErrorNilInstance: + default: + rejectCode = @"SPLIT_BUNDLE_NO_RUNTIME"; // retryable + break; + } + reject(rejectCode, + evalError.localizedDescription ?: @"Segment evaluation failed", + evalError); + return; + } double segMs = (CFAbsoluteTimeGetCurrent() - segStart) * 1000.0; - [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] Loaded segment %@ (id=%d) in %.1fms", segmentKey, segId, segMs]]; + [SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] Loaded segment %@ (id=%d) in %.1fms (eval-complete)", segmentKey, segId, segMs]]; resolve(nil); - } else { - reject(@"SPLIT_BUNDLE_NO_RUNTIME", - regError.localizedDescription ?: @"Runtime not available", - regError); - } + }]; } @catch (NSException *exception) { reject(@"SPLIT_BUNDLE_LOAD_ERROR", [NSString stringWithFormat:@"Failed to load segment %@: %@", segmentKey, exception.reason], From a48c47433026289a062c77e5673c1e0eb45103dc Mon Sep 17 00:00:00 2001 From: huhuanming Date: Fri, 12 Jun 2026 02:10:30 +0800 Subject: [PATCH 19/21] chore: wire all native modules into the example app for build verification Adds every native-module as a workspace dev pod to the example so iOS/Android builds compile and link all module sources together; includes a packagingOptions pickFirsts to resolve a duplicate META-INF collision from transitive deps. Test scaffolding only; no module source changes. --- example/react-native/android/app/build.gradle | 17 + example/react-native/ios/Podfile.lock | 420 ++++++++++++++++-- example/react-native/package.json | 10 + yarn.lock | 30 +- 4 files changed, 419 insertions(+), 58 deletions(-) diff --git a/example/react-native/android/app/build.gradle b/example/react-native/android/app/build.gradle index 6f4821a9..162c612b 100644 --- a/example/react-native/android/app/build.gradle +++ b/example/react-native/android/app/build.gradle @@ -114,6 +114,23 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } + + // Several transitive deps (e.g. google-auth-library-*) ship duplicate + // META-INF metadata jars; without this the MergeJavaRes task fails with + // DuplicateRelativeFileException. Example-app packaging only. + packagingOptions { + resources { + pickFirsts += [ + "META-INF/INDEX.LIST", + "META-INF/DEPENDENCIES", + "META-INF/LICENSE", + "META-INF/LICENSE.txt", + "META-INF/NOTICE", + "META-INF/NOTICE.txt", + "META-INF/*.kotlin_module", + ] + } + } } dependencies { diff --git a/example/react-native/ios/Podfile.lock b/example/react-native/ios/Podfile.lock index d339421c..3cbb4d20 100644 --- a/example/react-native/ios/Podfile.lock +++ b/example/react-native/ios/Podfile.lock @@ -1,5 +1,61 @@ PODS: - - AutoSizeInput (3.0.59): + - AesCrypto (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - AsyncStorage (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - AutoSizeInput (3.0.60): - boost - DoubleConversion - fast_float @@ -29,7 +85,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - BackgroundThread (3.0.59): + - BackgroundThread (3.0.60): - boost - DoubleConversion - fast_float @@ -59,7 +115,7 @@ PODS: - SocketRocket - Yoga - boost (1.84.0) - - ChartWebview (3.0.59): + - ChartWebview (3.0.60): - boost - DoubleConversion - fast_float @@ -90,7 +146,35 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - CloudKitModule (3.0.59): + - CloudFs (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - CloudKitModule (3.0.60): - boost - DoubleConversion - fast_float @@ -124,6 +208,34 @@ PODS: - CocoaLumberjack/Core (3.9.0) - CocoaLumberjack/Swift (3.9.0): - CocoaLumberjack/Core + - DnsLookup (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - DoubleConversion (1.1.6) - fast_float (8.0.0) - FBLazyVector (0.83.0) @@ -132,7 +244,7 @@ PODS: - hermes-engine (0.14.0): - hermes-engine/Pre-built (= 0.14.0) - hermes-engine/Pre-built (0.14.0) - - KeychainModule (3.0.59): + - KeychainModule (3.0.60): - boost - DoubleConversion - fast_float @@ -166,6 +278,34 @@ PODS: - MMKV (2.2.4): - MMKVCore (~> 2.2.4) - MMKVCore (2.2.4) + - NetworkInfo (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - NitroMmkv (4.1.2): - boost - DoubleConversion @@ -226,7 +366,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - PerpDepthBar (3.0.59): + - Pbkdf2 (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - PerpDepthBar (3.0.60): - boost - DoubleConversion - fast_float @@ -256,6 +424,34 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - Ping (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2140,7 +2336,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-pager-view (3.0.59): + - react-native-pager-view (3.0.60): - boost - DoubleConversion - fast_float @@ -2255,7 +2451,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view (3.0.59): + - react-native-tab-view (3.0.60): - boost - DoubleConversion - fast_float @@ -2273,7 +2469,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-tab-view/common (= 3.0.59) + - react-native-tab-view/common (= 3.0.60) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2284,7 +2480,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view/common (3.0.59): + - react-native-tab-view/common (3.0.60): - boost - DoubleConversion - fast_float @@ -2869,7 +3065,7 @@ PODS: - React-perflogger (= 0.83.0) - React-utils (= 0.83.0) - SocketRocket - - ReactNativeAppUpdate (3.0.59): + - ReactNativeAppUpdate (3.0.60): - boost - DoubleConversion - fast_float @@ -2900,7 +3096,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleCrypto (3.0.59): + - ReactNativeBundleCrypto (3.0.60): - boost - DoubleConversion - fast_float @@ -2931,7 +3127,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleUpdate (3.0.59): + - ReactNativeBundleUpdate (3.0.60): - boost - DoubleConversion - fast_float @@ -2966,7 +3162,7 @@ PODS: - SocketRocket - SSZipArchive (>= 2.5.4) - Yoga - - ReactNativeCheckBiometricAuthChanged (3.0.59): + - ReactNativeCheckBiometricAuthChanged (3.0.60): - boost - DoubleConversion - fast_float @@ -2997,7 +3193,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeDeviceUtils (3.0.59): + - ReactNativeDeviceUtils (3.0.60): - boost - DoubleConversion - fast_float @@ -3028,7 +3224,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeGetRandomValues (3.0.59): + - ReactNativeGetRandomValues (3.0.60): - boost - DoubleConversion - fast_float @@ -3059,7 +3255,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeLiteCard (3.0.59): + - ReactNativeLiteCard (3.0.60): - boost - DoubleConversion - fast_float @@ -3088,7 +3284,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeNativeLogger (3.0.59): + - ReactNativeNativeLogger (3.0.60): - boost - CocoaLumberjack/Swift (~> 3.8) - DoubleConversion @@ -3119,7 +3315,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativePerfMemory (3.0.59): + - ReactNativePerfMemory (3.0.60): - boost - DoubleConversion - fast_float @@ -3150,7 +3346,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeRangeDownloader (3.0.59): + - ReactNativePerfStats (3.0.60): - boost - DoubleConversion - fast_float @@ -3181,7 +3377,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeSplashScreen (3.0.59): + - ReactNativeRangeDownloader (3.0.60): - boost - DoubleConversion - fast_float @@ -3212,7 +3408,38 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeZipArchive (3.0.59): + - ReactNativeSplashScreen (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - ReactNativeZipArchive (3.0.60): - boost - DoubleConversion - fast_float @@ -3301,7 +3528,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ScrollGuard (3.0.59): + - ScrollGuard (3.0.60): - boost - DoubleConversion - fast_float @@ -3331,7 +3558,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - SegmentSlider (3.0.59): + - SegmentSlider (3.0.60): - boost - DoubleConversion - fast_float @@ -3361,7 +3588,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Skeleton (3.0.59): + - Skeleton (3.0.60): - boost - DoubleConversion - fast_float @@ -3392,15 +3619,76 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) + - SplitBundleLoader (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga - SSZipArchive (2.6.0) + - TcpSocket (3.0.60): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - Yoga (0.0.0) DEPENDENCIES: + - "AesCrypto (from `../../../node_modules/@onekeyfe/react-native-aes-crypto`)" + - "AsyncStorage (from `../../../node_modules/@onekeyfe/react-native-async-storage`)" - "AutoSizeInput (from `../../../node_modules/@onekeyfe/react-native-auto-size-input`)" - "BackgroundThread (from `../../../node_modules/@onekeyfe/react-native-background-thread`)" - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) - "ChartWebview (from `../../../node_modules/@onekeyfe/react-native-chart-webview`)" + - "CloudFs (from `../../../node_modules/@onekeyfe/react-native-cloud-fs`)" - "CloudKitModule (from `../../../node_modules/@onekeyfe/react-native-cloud-kit-module`)" + - "DnsLookup (from `../../../node_modules/@onekeyfe/react-native-dns-lookup`)" - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) @@ -3408,9 +3696,12 @@ DEPENDENCIES: - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - "KeychainModule (from `../../../node_modules/@onekeyfe/react-native-keychain-module`)" + - "NetworkInfo (from `../../../node_modules/@onekeyfe/react-native-network-info`)" - NitroMmkv (from `../../../node_modules/react-native-mmkv`) - NitroModules (from `../../../node_modules/react-native-nitro-modules`) + - "Pbkdf2 (from `../../../node_modules/@onekeyfe/react-native-pbkdf2`)" - "PerpDepthBar (from `../../../node_modules/@onekeyfe/react-native-perp-depth-bar`)" + - "Ping (from `../../../node_modules/@onekeyfe/react-native-ping`)" - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`) @@ -3493,6 +3784,7 @@ DEPENDENCIES: - "ReactNativeLiteCard (from `../../../node_modules/@onekeyfe/react-native-lite-card`)" - "ReactNativeNativeLogger (from `../../../node_modules/@onekeyfe/react-native-native-logger`)" - "ReactNativePerfMemory (from `../../../node_modules/@onekeyfe/react-native-perf-memory`)" + - "ReactNativePerfStats (from `../../../node_modules/@onekeyfe/react-native-perf-stats`)" - "ReactNativeRangeDownloader (from `../../../node_modules/@onekeyfe/react-native-range-downloader`)" - "ReactNativeSplashScreen (from `../../../node_modules/@onekeyfe/react-native-splash-screen`)" - "ReactNativeZipArchive (from `../../../node_modules/@onekeyfe/react-native-zip-archive`)" @@ -3501,6 +3793,8 @@ DEPENDENCIES: - "SegmentSlider (from `../../../node_modules/@onekeyfe/react-native-segment-slider`)" - "Skeleton (from `../../../node_modules/@onekeyfe/react-native-skeleton`)" - SocketRocket (~> 0.7.1) + - "SplitBundleLoader (from `../../../node_modules/@onekeyfe/react-native-split-bundle-loader`)" + - "TcpSocket (from `../../../node_modules/@onekeyfe/react-native-tcp-socket`)" - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -3512,6 +3806,10 @@ SPEC REPOS: - SSZipArchive EXTERNAL SOURCES: + AesCrypto: + :path: "../../../node_modules/@onekeyfe/react-native-aes-crypto" + AsyncStorage: + :path: "../../../node_modules/@onekeyfe/react-native-async-storage" AutoSizeInput: :path: "../../../node_modules/@onekeyfe/react-native-auto-size-input" BackgroundThread: @@ -3520,8 +3818,12 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" ChartWebview: :path: "../../../node_modules/@onekeyfe/react-native-chart-webview" + CloudFs: + :path: "../../../node_modules/@onekeyfe/react-native-cloud-fs" CloudKitModule: :path: "../../../node_modules/@onekeyfe/react-native-cloud-kit-module" + DnsLookup: + :path: "../../../node_modules/@onekeyfe/react-native-dns-lookup" DoubleConversion: :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -3537,12 +3839,18 @@ EXTERNAL SOURCES: :tag: hermes-v0.14.0 KeychainModule: :path: "../../../node_modules/@onekeyfe/react-native-keychain-module" + NetworkInfo: + :path: "../../../node_modules/@onekeyfe/react-native-network-info" NitroMmkv: :path: "../../../node_modules/react-native-mmkv" NitroModules: :path: "../../../node_modules/react-native-nitro-modules" + Pbkdf2: + :path: "../../../node_modules/@onekeyfe/react-native-pbkdf2" PerpDepthBar: :path: "../../../node_modules/@onekeyfe/react-native-perp-depth-bar" + Ping: + :path: "../../../node_modules/@onekeyfe/react-native-ping" RCT-Folly: :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -3705,6 +4013,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@onekeyfe/react-native-native-logger" ReactNativePerfMemory: :path: "../../../node_modules/@onekeyfe/react-native-perf-memory" + ReactNativePerfStats: + :path: "../../../node_modules/@onekeyfe/react-native-perf-stats" ReactNativeRangeDownloader: :path: "../../../node_modules/@onekeyfe/react-native-range-downloader" ReactNativeSplashScreen: @@ -3719,28 +4029,39 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@onekeyfe/react-native-segment-slider" Skeleton: :path: "../../../node_modules/@onekeyfe/react-native-skeleton" + SplitBundleLoader: + :path: "../../../node_modules/@onekeyfe/react-native-split-bundle-loader" + TcpSocket: + :path: "../../../node_modules/@onekeyfe/react-native-tcp-socket" Yoga: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AutoSizeInput: 310e18d0c09d2fc10a6a85b9f1ed50ebd9a370c8 - BackgroundThread: 84ccf1277d097cdc27be7655a7821578afee52b5 + AesCrypto: 90dd6b77a8dc5f0ab1ea9ba0ea54aa23394d55d3 + AsyncStorage: b0f317fcd23eb31b90e0747188dcbbc3fdec8d15 + AutoSizeInput: 00f8ee043fb30100eeb489cb2f51418116ca56c8 + BackgroundThread: e1d3fd83828c8ddf7642c280ea2cdf37f6561065 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - ChartWebview: 098a9791750418a908b33dc31f97fe3217c5b0f1 - CloudKitModule: e0cbf4c35db4299f768f975311494a4d1ed5d753 + ChartWebview: b2a305740b90d82db52d692aab756b8c883f7401 + CloudFs: 8a30627f0689584234b89813d643001a11cffb9a + CloudKitModule: 00d6e1a3c1555f8c418d9dc2f47cc363dc9f64dd CocoaLumberjack: 5644158777912b7de7469fa881f8a3f259c2512a + DnsLookup: 4326300dda38e443a9d53b4e2d205290d3dd5baf DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 70fdc9d0bb0d8532e0411dcb21e53ce5a160960a - KeychainModule: 2da5d9d6892728cb204ff133b190dffd4438fee2 + KeychainModule: 7f11298a3df7967b683b32276677fce6ce9d0f67 MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df + NetworkInfo: 0c669260b36bb2eb065abc24bf0054ef7f6e40f5 NitroMmkv: 0be91455465952f2b943f753b9ee7df028d89e5c NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 - PerpDepthBar: 1b4f18ae73c6d0429ef675d8ac8ab083a89b28e1 + Pbkdf2: 59da23726f6cc9f60b4d705c731a376bcbc7982f + PerpDepthBar: 3818a85ae88e6094e76884274c8ddc58b34febc1 + Ping: cca9445d60113561bd50a5fe84c6a2ea8d9374a1 RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23 RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 @@ -3777,9 +4098,9 @@ SPEC CHECKSUMS: React-logger: 9e597cbeda7b8cc8aa8fb93860dade97190f69cc React-Mapbuffer: 20046c0447efaa7aace0b76085aa9bb35b0e8105 React-microtasksnativemodule: 0e837de56519c92d8a2e3097717df9497feb33cb - react-native-pager-view: 474df899a86ad5b4778d88e98d8a1a6c3c3ce024 + react-native-pager-view: b9e84bf816eb3a46cb7a71da44b65940bedb3baa react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 - react-native-tab-view: 5b6f94785bf097b6266f43f1ab0a0cad0703b3f1 + react-native-tab-view: 864f38e4c749f6278f15f9af6317c093b20bb124 React-NativeModulesApple: 1a378198515f8e825c5931a7613e98da69320cee React-networking: bfd1695ada5a57023006ce05823ac5391c3ce072 React-oscompat: aedc0afbded67280de6bb6bfac8cfde0389e2b33 @@ -3813,24 +4134,27 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 554b421c45b7df35ac791da1b734335470b55fcc ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - ReactNativeAppUpdate: 0518869c3b42898a2972e2cfeb9bb609eb29b54f - ReactNativeBundleCrypto: dbd90abdd4ad9480e5829a7de8acca89d7a10582 - ReactNativeBundleUpdate: 9d99bd6f672ea49055582cfb5ae50b1ba96e8529 - ReactNativeCheckBiometricAuthChanged: 60eca3ff33c1ed5506efa5f9479db81ce72e814b - ReactNativeDeviceUtils: 4812f30bf8504fad52b8f93724a21d287bc71096 - ReactNativeGetRandomValues: 47bf1fa0869b379838d7012294e5f1076da8265b - ReactNativeLiteCard: f06315268b596815e32d8cfa419b7f1221f31242 - ReactNativeNativeLogger: 8d7d53a047cd0d3399133895d53a11d63d3ec2e3 - ReactNativePerfMemory: 18c3d0b2341622d9d8d5c7ef94b95578648dbcc2 - ReactNativeRangeDownloader: 006ffbd25ab794ccc1756d6e7659d4f8f222f509 - ReactNativeSplashScreen: c460831a79feeae73c4cb5c44408bc3a209e1ff5 - ReactNativeZipArchive: 54a063f41f08d54b6d8b8a54034f81e38a780972 + ReactNativeAppUpdate: ce91317526d6ed4756b72a54a877b5c5192f4202 + ReactNativeBundleCrypto: 00f01b8b08ce2c51386b0ea80cd12152d079e30d + ReactNativeBundleUpdate: 380cf31996142a473b5d76deb41ef6124bef9aef + ReactNativeCheckBiometricAuthChanged: 400dad53ffce69788f433d29942f19bcb7410693 + ReactNativeDeviceUtils: 088a46ee50319d2fd48ad300fa843ece66926ca9 + ReactNativeGetRandomValues: e03d3984a16bb42766db2fe60d424d82931022f4 + ReactNativeLiteCard: 5b32552f8bdb1317b9a4230a879291bf4ea1f09b + ReactNativeNativeLogger: 058f50a5030caa112c2f6a1b838622002dab064b + ReactNativePerfMemory: c27ba1c122f2287207ba441b5a5d13d9e7ee0267 + ReactNativePerfStats: a2e1d0fd0cefd0e470c080919827b45ee31f6887 + ReactNativeRangeDownloader: db22973c10e1bcfba5f9fbdc962d850f68f54600 + ReactNativeSplashScreen: 3e04eff0ed92d7c421cd8a4f25e21e058365a575 + ReactNativeZipArchive: 25a2d886abceee1dc2f1dc7e5c6c052973de438c RNScreens: 7f643ee0fd1407dc5085c7795460bd93da113b8f - ScrollGuard: c70be9a10309307c9d1a0e152b5acf63df80248c - SegmentSlider: 1755235f50c748d7f01892e2e565507ef8770fa6 - Skeleton: f6a2f5b5681f49d4644dca5ce5f90f396475bf59 + ScrollGuard: 5f89f8305f39ee689bf3674c21fcfbcd9c4119ad + SegmentSlider: 9ba28a578a10204e593247ea58b4d9b545c3dc6a + Skeleton: 668c92edd655c798b2a93535343dce62bf31da13 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + SplitBundleLoader: e00c9f7e2a999db78be8a41ff0ff35ca9c56038b SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea + TcpSocket: a588a2695edfb61de6988b991ef1cfaaff1a375e Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e PODFILE CHECKSUM: 11e5274bb8ec6380d5fbde829e700fe4f0cf9c8a diff --git a/example/react-native/package.json b/example/react-native/package.json index d959dde8..cf10c522 100644 --- a/example/react-native/package.json +++ b/example/react-native/package.json @@ -11,28 +11,38 @@ "test": "jest" }, "dependencies": { + "@onekeyfe/react-native-aes-crypto": "workspace:*", "@onekeyfe/react-native-app-update": "workspace:*", + "@onekeyfe/react-native-async-storage": "workspace:*", "@onekeyfe/react-native-auto-size-input": "workspace:*", "@onekeyfe/react-native-background-thread": "workspace:*", "@onekeyfe/react-native-bundle-crypto": "workspace:*", "@onekeyfe/react-native-bundle-update": "workspace:*", "@onekeyfe/react-native-chart-webview": "workspace:*", "@onekeyfe/react-native-check-biometric-auth-changed": "workspace:*", + "@onekeyfe/react-native-cloud-fs": "workspace:*", "@onekeyfe/react-native-cloud-kit-module": "workspace:*", "@onekeyfe/react-native-device-utils": "workspace:*", + "@onekeyfe/react-native-dns-lookup": "workspace:*", "@onekeyfe/react-native-get-random-values": "workspace:*", "@onekeyfe/react-native-keychain-module": "workspace:*", "@onekeyfe/react-native-lite-card": "workspace:*", "@onekeyfe/react-native-native-logger": "workspace:*", + "@onekeyfe/react-native-network-info": "workspace:*", "@onekeyfe/react-native-pager-view": "workspace:*", + "@onekeyfe/react-native-pbkdf2": "workspace:*", "@onekeyfe/react-native-perf-memory": "workspace:*", + "@onekeyfe/react-native-perf-stats": "workspace:*", "@onekeyfe/react-native-perp-depth-bar": "workspace:*", + "@onekeyfe/react-native-ping": "workspace:*", "@onekeyfe/react-native-range-downloader": "workspace:*", "@onekeyfe/react-native-scroll-guard": "workspace:*", "@onekeyfe/react-native-segment-slider": "workspace:*", "@onekeyfe/react-native-skeleton": "workspace:*", "@onekeyfe/react-native-splash-screen": "workspace:*", + "@onekeyfe/react-native-split-bundle-loader": "workspace:*", "@onekeyfe/react-native-tab-view": "workspace:*", + "@onekeyfe/react-native-tcp-socket": "workspace:*", "@onekeyfe/react-native-zip-archive": "workspace:*", "@react-native/new-app-screen": "0.83.0", "@react-navigation/native": "^7.1.33", diff --git a/yarn.lock b/yarn.lock index 727cd033..266ebb7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,28 +2744,38 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@onekeyfe/react-native-aes-crypto": "workspace:*" "@onekeyfe/react-native-app-update": "workspace:*" + "@onekeyfe/react-native-async-storage": "workspace:*" "@onekeyfe/react-native-auto-size-input": "workspace:*" "@onekeyfe/react-native-background-thread": "workspace:*" "@onekeyfe/react-native-bundle-crypto": "workspace:*" "@onekeyfe/react-native-bundle-update": "workspace:*" "@onekeyfe/react-native-chart-webview": "workspace:*" "@onekeyfe/react-native-check-biometric-auth-changed": "workspace:*" + "@onekeyfe/react-native-cloud-fs": "workspace:*" "@onekeyfe/react-native-cloud-kit-module": "workspace:*" "@onekeyfe/react-native-device-utils": "workspace:*" + "@onekeyfe/react-native-dns-lookup": "workspace:*" "@onekeyfe/react-native-get-random-values": "workspace:*" "@onekeyfe/react-native-keychain-module": "workspace:*" "@onekeyfe/react-native-lite-card": "workspace:*" "@onekeyfe/react-native-native-logger": "workspace:*" + "@onekeyfe/react-native-network-info": "workspace:*" "@onekeyfe/react-native-pager-view": "workspace:*" + "@onekeyfe/react-native-pbkdf2": "workspace:*" "@onekeyfe/react-native-perf-memory": "workspace:*" + "@onekeyfe/react-native-perf-stats": "workspace:*" "@onekeyfe/react-native-perp-depth-bar": "workspace:*" + "@onekeyfe/react-native-ping": "workspace:*" "@onekeyfe/react-native-range-downloader": "workspace:*" "@onekeyfe/react-native-scroll-guard": "workspace:*" "@onekeyfe/react-native-segment-slider": "workspace:*" "@onekeyfe/react-native-skeleton": "workspace:*" "@onekeyfe/react-native-splash-screen": "workspace:*" + "@onekeyfe/react-native-split-bundle-loader": "workspace:*" "@onekeyfe/react-native-tab-view": "workspace:*" + "@onekeyfe/react-native-tcp-socket": "workspace:*" "@onekeyfe/react-native-zip-archive": "workspace:*" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" @@ -2832,7 +2842,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-aes-crypto@workspace:native-modules/react-native-aes-crypto": +"@onekeyfe/react-native-aes-crypto@workspace:*, @onekeyfe/react-native-aes-crypto@workspace:native-modules/react-native-aes-crypto": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-aes-crypto@workspace:native-modules/react-native-aes-crypto" dependencies: @@ -2901,7 +2911,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-async-storage@workspace:native-modules/react-native-async-storage": +"@onekeyfe/react-native-async-storage@workspace:*, @onekeyfe/react-native-async-storage@workspace:native-modules/react-native-async-storage": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-async-storage@workspace:native-modules/react-native-async-storage" dependencies: @@ -3149,7 +3159,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-cloud-fs@workspace:native-modules/react-native-cloud-fs": +"@onekeyfe/react-native-cloud-fs@workspace:*, @onekeyfe/react-native-cloud-fs@workspace:native-modules/react-native-cloud-fs": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-cloud-fs@workspace:native-modules/react-native-cloud-fs" dependencies: @@ -3236,7 +3246,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-dns-lookup@workspace:native-modules/react-native-dns-lookup": +"@onekeyfe/react-native-dns-lookup@workspace:*, @onekeyfe/react-native-dns-lookup@workspace:native-modules/react-native-dns-lookup": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-dns-lookup@workspace:native-modules/react-native-dns-lookup" dependencies: @@ -3410,7 +3420,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-network-info@workspace:native-modules/react-native-network-info": +"@onekeyfe/react-native-network-info@workspace:*, @onekeyfe/react-native-network-info@workspace:native-modules/react-native-network-info": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-network-info@workspace:native-modules/react-native-network-info" dependencies: @@ -3458,7 +3468,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-pbkdf2@workspace:native-modules/react-native-pbkdf2": +"@onekeyfe/react-native-pbkdf2@workspace:*, @onekeyfe/react-native-pbkdf2@workspace:native-modules/react-native-pbkdf2": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-pbkdf2@workspace:native-modules/react-native-pbkdf2" dependencies: @@ -3527,7 +3537,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-perf-stats@workspace:native-modules/react-native-perf-stats": +"@onekeyfe/react-native-perf-stats@workspace:*, @onekeyfe/react-native-perf-stats@workspace:native-modules/react-native-perf-stats": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-perf-stats@workspace:native-modules/react-native-perf-stats" dependencies: @@ -3599,7 +3609,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-ping@workspace:native-modules/react-native-ping": +"@onekeyfe/react-native-ping@workspace:*, @onekeyfe/react-native-ping@workspace:native-modules/react-native-ping": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-ping@workspace:native-modules/react-native-ping" dependencies: @@ -3777,7 +3787,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-split-bundle-loader@workspace:native-modules/react-native-split-bundle-loader": +"@onekeyfe/react-native-split-bundle-loader@workspace:*, @onekeyfe/react-native-split-bundle-loader@workspace:native-modules/react-native-split-bundle-loader": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-split-bundle-loader@workspace:native-modules/react-native-split-bundle-loader" dependencies: @@ -3829,7 +3839,7 @@ __metadata: languageName: unknown linkType: soft -"@onekeyfe/react-native-tcp-socket@workspace:native-modules/react-native-tcp-socket": +"@onekeyfe/react-native-tcp-socket@workspace:*, @onekeyfe/react-native-tcp-socket@workspace:native-modules/react-native-tcp-socket": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-tcp-socket@workspace:native-modules/react-native-tcp-socket" dependencies: From a5fae968b9863921484f831d0618c5135e57a551 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Fri, 12 Jun 2026 02:13:03 +0800 Subject: [PATCH 20/21] chore: bump version to 3.0.61 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-crypto/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-range-downloader/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 2 +- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-chart-webview/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-perp-depth-bar/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-segment-slider/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 83452360..829a17b1 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index dd179bb9..3ecadb73 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index ada244cd..fb8481cb 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index 4acc07e4..b7f5c320 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 6f4d7e85..f102ba0b 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index e6f4d3e3..7b20b693 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 87b02129..e3c180a6 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 4d3aec26..37368903 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index 969646e2..fe366058 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 54d00261..5eef4bae 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 1df6af9b..f56f6b35 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 415a8486..46041c3c 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 68e6d860..f5554c1f 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index bcb420cd..d66c7089 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index 4a2bbad4..95c8cc32 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.60", + "version": "3.0.61", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index c323cffb..223f5ac8 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 9f37b2b3..ea76506f 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index e23603af..f853e720 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 3380dc96..e9d75418 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 41a6da5e..fd7d1480 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index dc646bf1..a255201d 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 4e913376..8618e6ea 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index 86cc63cd..86ed11b7 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 90c557b8..dae95ac7 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 65c61031..74477da6 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index e9cc125e..aa043e6f 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.60", + "version": "3.0.61", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 0d52958b..611537f3 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index bbe1a899..d5af0151 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.60", + "version": "3.0.61", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index f085db24..709ff6fe 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 541157d5..31461c65 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.60", + "version": "3.0.61", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 6e4d00d5..19c43ebe 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index fbb231a2..6070bbaf 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.60", + "version": "3.0.61", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index 0ab8a6b3..54afdf4d 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.60", + "version": "3.0.61", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", From 14443b67f81ad797546a3918cf3df89099a80a1c Mon Sep 17 00:00:00 2001 From: huhuanming Date: Fri, 12 Jun 2026 02:18:10 +0800 Subject: [PATCH 21/21] Update Podfile.lock --- example/react-native/ios/Podfile.lock | 136 +++++++++++++------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/example/react-native/ios/Podfile.lock b/example/react-native/ios/Podfile.lock index 3cbb4d20..92b4b7eb 100644 --- a/example/react-native/ios/Podfile.lock +++ b/example/react-native/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - AesCrypto (3.0.60): + - AesCrypto (3.0.61): - boost - DoubleConversion - fast_float @@ -27,7 +27,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - AsyncStorage (3.0.60): + - AsyncStorage (3.0.61): - boost - DoubleConversion - fast_float @@ -55,7 +55,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - AutoSizeInput (3.0.60): + - AutoSizeInput (3.0.61): - boost - DoubleConversion - fast_float @@ -85,7 +85,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - BackgroundThread (3.0.60): + - BackgroundThread (3.0.61): - boost - DoubleConversion - fast_float @@ -115,7 +115,7 @@ PODS: - SocketRocket - Yoga - boost (1.84.0) - - ChartWebview (3.0.60): + - ChartWebview (3.0.61): - boost - DoubleConversion - fast_float @@ -146,7 +146,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - CloudFs (3.0.60): + - CloudFs (3.0.61): - boost - DoubleConversion - fast_float @@ -174,7 +174,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - CloudKitModule (3.0.60): + - CloudKitModule (3.0.61): - boost - DoubleConversion - fast_float @@ -208,7 +208,7 @@ PODS: - CocoaLumberjack/Core (3.9.0) - CocoaLumberjack/Swift (3.9.0): - CocoaLumberjack/Core - - DnsLookup (3.0.60): + - DnsLookup (3.0.61): - boost - DoubleConversion - fast_float @@ -244,7 +244,7 @@ PODS: - hermes-engine (0.14.0): - hermes-engine/Pre-built (= 0.14.0) - hermes-engine/Pre-built (0.14.0) - - KeychainModule (3.0.60): + - KeychainModule (3.0.61): - boost - DoubleConversion - fast_float @@ -278,7 +278,7 @@ PODS: - MMKV (2.2.4): - MMKVCore (~> 2.2.4) - MMKVCore (2.2.4) - - NetworkInfo (3.0.60): + - NetworkInfo (3.0.61): - boost - DoubleConversion - fast_float @@ -366,7 +366,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Pbkdf2 (3.0.60): + - Pbkdf2 (3.0.61): - boost - DoubleConversion - fast_float @@ -394,7 +394,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - PerpDepthBar (3.0.60): + - PerpDepthBar (3.0.61): - boost - DoubleConversion - fast_float @@ -424,7 +424,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Ping (3.0.60): + - Ping (3.0.61): - boost - DoubleConversion - fast_float @@ -2336,7 +2336,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-pager-view (3.0.60): + - react-native-pager-view (3.0.61): - boost - DoubleConversion - fast_float @@ -2451,7 +2451,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view (3.0.60): + - react-native-tab-view (3.0.61): - boost - DoubleConversion - fast_float @@ -2469,7 +2469,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-tab-view/common (= 3.0.60) + - react-native-tab-view/common (= 3.0.61) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2480,7 +2480,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-tab-view/common (3.0.60): + - react-native-tab-view/common (3.0.61): - boost - DoubleConversion - fast_float @@ -3065,7 +3065,7 @@ PODS: - React-perflogger (= 0.83.0) - React-utils (= 0.83.0) - SocketRocket - - ReactNativeAppUpdate (3.0.60): + - ReactNativeAppUpdate (3.0.61): - boost - DoubleConversion - fast_float @@ -3096,7 +3096,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleCrypto (3.0.60): + - ReactNativeBundleCrypto (3.0.61): - boost - DoubleConversion - fast_float @@ -3127,7 +3127,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleUpdate (3.0.60): + - ReactNativeBundleUpdate (3.0.61): - boost - DoubleConversion - fast_float @@ -3162,7 +3162,7 @@ PODS: - SocketRocket - SSZipArchive (>= 2.5.4) - Yoga - - ReactNativeCheckBiometricAuthChanged (3.0.60): + - ReactNativeCheckBiometricAuthChanged (3.0.61): - boost - DoubleConversion - fast_float @@ -3193,7 +3193,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeDeviceUtils (3.0.60): + - ReactNativeDeviceUtils (3.0.61): - boost - DoubleConversion - fast_float @@ -3224,7 +3224,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeGetRandomValues (3.0.60): + - ReactNativeGetRandomValues (3.0.61): - boost - DoubleConversion - fast_float @@ -3255,7 +3255,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeLiteCard (3.0.60): + - ReactNativeLiteCard (3.0.61): - boost - DoubleConversion - fast_float @@ -3284,7 +3284,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeNativeLogger (3.0.60): + - ReactNativeNativeLogger (3.0.61): - boost - CocoaLumberjack/Swift (~> 3.8) - DoubleConversion @@ -3315,7 +3315,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativePerfMemory (3.0.60): + - ReactNativePerfMemory (3.0.61): - boost - DoubleConversion - fast_float @@ -3346,7 +3346,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativePerfStats (3.0.60): + - ReactNativePerfStats (3.0.61): - boost - DoubleConversion - fast_float @@ -3377,7 +3377,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeRangeDownloader (3.0.60): + - ReactNativeRangeDownloader (3.0.61): - boost - DoubleConversion - fast_float @@ -3408,7 +3408,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeSplashScreen (3.0.60): + - ReactNativeSplashScreen (3.0.61): - boost - DoubleConversion - fast_float @@ -3439,7 +3439,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeZipArchive (3.0.60): + - ReactNativeZipArchive (3.0.61): - boost - DoubleConversion - fast_float @@ -3528,7 +3528,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ScrollGuard (3.0.60): + - ScrollGuard (3.0.61): - boost - DoubleConversion - fast_float @@ -3558,7 +3558,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - SegmentSlider (3.0.60): + - SegmentSlider (3.0.61): - boost - DoubleConversion - fast_float @@ -3588,7 +3588,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Skeleton (3.0.60): + - Skeleton (3.0.61): - boost - DoubleConversion - fast_float @@ -3619,7 +3619,7 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - SplitBundleLoader (3.0.60): + - SplitBundleLoader (3.0.61): - boost - DoubleConversion - fast_float @@ -3649,7 +3649,7 @@ PODS: - SocketRocket - Yoga - SSZipArchive (2.6.0) - - TcpSocket (3.0.60): + - TcpSocket (3.0.61): - boost - DoubleConversion - fast_float @@ -4037,31 +4037,31 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AesCrypto: 90dd6b77a8dc5f0ab1ea9ba0ea54aa23394d55d3 - AsyncStorage: b0f317fcd23eb31b90e0747188dcbbc3fdec8d15 - AutoSizeInput: 00f8ee043fb30100eeb489cb2f51418116ca56c8 - BackgroundThread: e1d3fd83828c8ddf7642c280ea2cdf37f6561065 + AesCrypto: 5d79d012eda0c688e81fb1e5efb66697a8bf63b2 + AsyncStorage: d36fdf626a5daea9ab7ddf147743f91cb8fc47b2 + AutoSizeInput: 46d8edf6010443fdca7dc3a69bb44553780b1155 + BackgroundThread: 28c77551040ecc7a70dabf132b840f7b6b1cb7ef boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - ChartWebview: b2a305740b90d82db52d692aab756b8c883f7401 - CloudFs: 8a30627f0689584234b89813d643001a11cffb9a - CloudKitModule: 00d6e1a3c1555f8c418d9dc2f47cc363dc9f64dd + ChartWebview: df5cfa93f2fff737382f08041ba8034932266674 + CloudFs: 7d28ea2cc410fcae83fa573f4d5c313f3203352d + CloudKitModule: 9f6c25469fa12adbcf55b0f9ec9a26bb489978c6 CocoaLumberjack: 5644158777912b7de7469fa881f8a3f259c2512a - DnsLookup: 4326300dda38e443a9d53b4e2d205290d3dd5baf + DnsLookup: 2b92e6ee2a0925934097334e7024214ee39f3fd0 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 70fdc9d0bb0d8532e0411dcb21e53ce5a160960a - KeychainModule: 7f11298a3df7967b683b32276677fce6ce9d0f67 + KeychainModule: 91e5f89dfcc8981beb4ee8ad1480eed76ff1d96a MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df - NetworkInfo: 0c669260b36bb2eb065abc24bf0054ef7f6e40f5 + NetworkInfo: aff4df0ccff078e448ad3da1c5620e7285f129fc NitroMmkv: 0be91455465952f2b943f753b9ee7df028d89e5c NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 - Pbkdf2: 59da23726f6cc9f60b4d705c731a376bcbc7982f - PerpDepthBar: 3818a85ae88e6094e76884274c8ddc58b34febc1 - Ping: cca9445d60113561bd50a5fe84c6a2ea8d9374a1 + Pbkdf2: 723893ae9eaa320168c62cb825a4bcef89703894 + PerpDepthBar: ce30834568bd8b9ea933267437defb431c6c576d + Ping: 8f45505d4f773a5f6348852679e2801e1bf052ac RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23 RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 @@ -4098,9 +4098,9 @@ SPEC CHECKSUMS: React-logger: 9e597cbeda7b8cc8aa8fb93860dade97190f69cc React-Mapbuffer: 20046c0447efaa7aace0b76085aa9bb35b0e8105 React-microtasksnativemodule: 0e837de56519c92d8a2e3097717df9497feb33cb - react-native-pager-view: b9e84bf816eb3a46cb7a71da44b65940bedb3baa + react-native-pager-view: 5bde74d01c86137851dddc589a91566e5fa2a490 react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 - react-native-tab-view: 864f38e4c749f6278f15f9af6317c093b20bb124 + react-native-tab-view: 84fd866023d2753d09cdb1dd62cbe086fb39ccc7 React-NativeModulesApple: 1a378198515f8e825c5931a7613e98da69320cee React-networking: bfd1695ada5a57023006ce05823ac5391c3ce072 React-oscompat: aedc0afbded67280de6bb6bfac8cfde0389e2b33 @@ -4134,27 +4134,27 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 554b421c45b7df35ac791da1b734335470b55fcc ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - ReactNativeAppUpdate: ce91317526d6ed4756b72a54a877b5c5192f4202 - ReactNativeBundleCrypto: 00f01b8b08ce2c51386b0ea80cd12152d079e30d - ReactNativeBundleUpdate: 380cf31996142a473b5d76deb41ef6124bef9aef - ReactNativeCheckBiometricAuthChanged: 400dad53ffce69788f433d29942f19bcb7410693 - ReactNativeDeviceUtils: 088a46ee50319d2fd48ad300fa843ece66926ca9 - ReactNativeGetRandomValues: e03d3984a16bb42766db2fe60d424d82931022f4 - ReactNativeLiteCard: 5b32552f8bdb1317b9a4230a879291bf4ea1f09b - ReactNativeNativeLogger: 058f50a5030caa112c2f6a1b838622002dab064b - ReactNativePerfMemory: c27ba1c122f2287207ba441b5a5d13d9e7ee0267 - ReactNativePerfStats: a2e1d0fd0cefd0e470c080919827b45ee31f6887 - ReactNativeRangeDownloader: db22973c10e1bcfba5f9fbdc962d850f68f54600 - ReactNativeSplashScreen: 3e04eff0ed92d7c421cd8a4f25e21e058365a575 - ReactNativeZipArchive: 25a2d886abceee1dc2f1dc7e5c6c052973de438c + ReactNativeAppUpdate: d4931ea70ba02c29bec1a3db68ef1b2d0b39f2c2 + ReactNativeBundleCrypto: 9b44893be06c117be94f9d5eb7f2666c0b4ee731 + ReactNativeBundleUpdate: 0a352e1fe87dfeefea7b7e477834c6830900500d + ReactNativeCheckBiometricAuthChanged: a422d1c4e87f5de024409e8c6b97469bebe271ec + ReactNativeDeviceUtils: f5b15156b5864fc43ba71b3ec09ad9ec3111e9bb + ReactNativeGetRandomValues: 94a7f6bc3fcb91991d6aaa574d4ad0c140ff5285 + ReactNativeLiteCard: 663a9d5772550d83de6aa79282c9c3fc5410fd15 + ReactNativeNativeLogger: 6337f2f51c7f7c62a8575764799a74d22ffec372 + ReactNativePerfMemory: b0d2ac23e8bcd2e07418efe92a3e19379666bb2e + ReactNativePerfStats: bdff04f45bbd161e0e3c907d1bb36e8ddb7410c0 + ReactNativeRangeDownloader: 73efe1ec206257b2f425d58e7e238f45221ae707 + ReactNativeSplashScreen: 6223f61235b36f12f468e369064416854608aa75 + ReactNativeZipArchive: bdf08f7ee00352d2b2894449c8b1b6810f40f2b2 RNScreens: 7f643ee0fd1407dc5085c7795460bd93da113b8f - ScrollGuard: 5f89f8305f39ee689bf3674c21fcfbcd9c4119ad - SegmentSlider: 9ba28a578a10204e593247ea58b4d9b545c3dc6a - Skeleton: 668c92edd655c798b2a93535343dce62bf31da13 + ScrollGuard: 61f6774ecb5ac6ccd5fbeb34830a117ab3e2d4f0 + SegmentSlider: 295a0ade25855058c11d2b185ad13762b1f4a166 + Skeleton: 07da4b794e0e2b58bd75ba820d0c2e6e87c9225e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - SplitBundleLoader: e00c9f7e2a999db78be8a41ff0ff35ca9c56038b + SplitBundleLoader: 8421ede8cbc7f15a7608b59b126fcfe22aca5ccc SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea - TcpSocket: a588a2695edfb61de6988b991ef1cfaaff1a375e + TcpSocket: bd4063e4dda0fb7f6952376592e3efdeac915710 Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e PODFILE CHECKSUM: 11e5274bb8ec6380d5fbde829e700fe4f0cf9c8a