From 8f2bf2dd43d48e59367f88c924a667980d5e13e3 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Tue, 9 Jun 2026 19:01:34 +0800 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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",