@onekeyfe/react-native-chart-webview 3.0.54 → 3.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/ChartWebview.podspec +3 -0
  2. package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +74 -16
  3. package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +139 -13
  4. package/ios/ChartWebview.swift +185 -31
  5. package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
  6. package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
  7. package/lib/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
  8. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
  9. package/lib/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
  10. package/lib/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
  11. package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
  12. package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
  13. package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
  14. package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
  15. package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
  16. package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
  17. package/lib/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
  18. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
  19. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
  20. package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
  21. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
  22. package/lib/nitrogen/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
  23. package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
  24. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
  25. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
  26. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
  27. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
  28. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
  29. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
  30. package/lib/nitrogen/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
  31. package/lib/typescript/src/ChartWebview.nitro.d.ts +1 -0
  32. package/lib/typescript/src/ChartWebview.nitro.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +1 -0
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
  36. package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
  37. package/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
  39. package/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
  40. package/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
  41. package/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
  42. package/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
  43. package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
  44. package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
  45. package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
  46. package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
  47. package/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
  48. package/package.json +1 -1
  49. package/src/ChartWebview.nitro.ts +10 -0
@@ -1,6 +1,7 @@
1
1
  import Foundation
2
2
  import UIKit
3
3
  import WebKit
4
+ import ReactNativeNativeLogger
4
5
 
5
6
  // MARK: - Constants (shared)
6
7
 
@@ -13,6 +14,25 @@ private enum ChartWebviewConst {
13
14
  static let messageHandlerName = "onekeyChart"
14
15
  }
15
16
 
17
+ // MARK: - Diagnostic logging (DEBUG instrumentation)
18
+ //
19
+ // Routes the chart-webview native lifecycle into the shared OneKeyLog file
20
+ // (app-latest.log) so the offline / prewarm / pool-ownership chain is visible
21
+ // alongside the JS `market => chart => *` events. Tag is "ChartWV". Raw NSLog
22
+ // is NOT enough — it never reaches app-latest.log, which is why the native side
23
+ // was previously invisible during log analysis. Truncates payloads to keep the
24
+ // file readable and avoid leaking message bodies.
25
+ enum ChartWVLog {
26
+ static func i(_ msg: String) { OneKeyLog.info("ChartWV", msg) }
27
+ static func w(_ msg: String) { OneKeyLog.warn("ChartWV", msg) }
28
+ static func e(_ msg: String) { OneKeyLog.error("ChartWV", msg) }
29
+ /// Truncate a (possibly large / sensitive) payload for logging.
30
+ static func clip(_ s: String?, _ n: Int = 120) -> String {
31
+ guard let s = s else { return "nil" }
32
+ return s.count <= n ? s : String(s.prefix(n)) + "…(\(s.count))"
33
+ }
34
+ }
35
+
16
36
  // MARK: - ChartContainerView (window-attach detection)
17
37
 
18
38
  /// The host's `view`. Reports when it is attached to / detached from a window so
@@ -69,9 +89,12 @@ class HybridChartWebview: HybridChartWebviewSpec {
69
89
 
70
90
  override init() {
71
91
  super.init()
92
+ ChartWVLog.i("host.init id=\(instanceId)")
72
93
  container.onWindowChange = { [weak self] attached in
73
- self?.attached = attached
74
- self?.scheduleReconcile()
94
+ guard let self = self else { return }
95
+ ChartWVLog.i("host.windowChange id=\(self.instanceId) attached=\(attached) reuseKey=\(self.reuseKey ?? "nil") pooled=\(String(describing: self.pooled)) active=\(String(describing: self.active))")
96
+ self.attached = attached
97
+ self.scheduleReconcile()
75
98
  }
76
99
  }
77
100
 
@@ -97,15 +120,19 @@ class HybridChartWebview: HybridChartWebviewSpec {
97
120
 
98
121
  // MARK: - Props (source)
99
122
 
100
- var uri: String? { didSet { applySourceIfOwner() } }
101
- var localBundle: String? { didSet { applySourceIfOwner() } }
102
- var entry: String? { didSet { applySourceIfOwner() } }
103
- var paramsJson: String? { didSet { applySourceIfOwner() } }
123
+ // Source/bridge setters call applySource (synchronous) NOT scheduleReconcile,
124
+ // which loops (reconcile -> setSource -> prop re-apply -> reconcile ...).
125
+ // applySource sets warmDriver + warm-boots the page even when this host is not
126
+ // the visible owner, so a bridge prop arriving after the window-attach reconcile
127
+ // still makes this host the warm driver (page->native callbacks fall back to it).
128
+ var uri: String? { didSet { applySource() } }
129
+ var localBundle: String? { didSet { applySource() } }
130
+ var entry: String? { didSet { applySource() } }
131
+ var paramsJson: String? { didSet { applySource() } }
104
132
 
105
133
  // Document-start bridge JS (single source of truth in the TS layer). Handed to
106
- // the pooled WebView when we claim it, before its first load. Re-applying the
107
- // source on change triggers a deferred load if this prop lands after the source.
108
- var bridgeScript: String? { didSet { applySourceIfOwner() } }
134
+ // the pooled WebView when we claim it, before its first load.
135
+ var bridgeScript: String? { didSet { applySource() } }
109
136
 
110
137
  // MARK: - Props (singleton)
111
138
 
@@ -132,6 +159,20 @@ class HybridChartWebview: HybridChartWebviewSpec {
132
159
  }
133
160
  }
134
161
 
162
+ // MARK: - Props (debugging)
163
+
164
+ // Make the backing WKWebView inspectable (Safari Web Inspector). Driven by the
165
+ // app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main
166
+ // react-native-webview (which sets WKWebView.isInspectable). nil => default to
167
+ // the DEBUG build behavior (see PooledChartWebView.applyInspectable). Applied to
168
+ // the backing WebView both here (on prop change) and at WebView creation, since
169
+ // `backing` is nil until the first reconcile assigns it.
170
+ var webviewDebuggingEnabled: Bool? {
171
+ didSet {
172
+ backing?.setInspectable(webviewDebuggingEnabled)
173
+ }
174
+ }
175
+
135
176
  // MARK: - Props (events)
136
177
 
137
178
  var onMessage: ((_ message: String) -> Void)?
@@ -140,6 +181,10 @@ class HybridChartWebview: HybridChartWebviewSpec {
140
181
 
141
182
  // Called by the backing PooledChartWebView while this host is the owner.
142
183
  func handleMessage(_ message: String) {
184
+ // page -> native. Truncated; OneKeyLog rate-limits/dedups repeats so streaming
185
+ // data won't flood. Key for Q2 (market chart no data): shows the page's
186
+ // $private kline requests and whether replies come back.
187
+ ChartWVLog.i("msg.in id=\(instanceId) \(ChartWVLog.clip(message, 200))")
143
188
  // The chart reports it has painted the new symbol after a switch; that's our
144
189
  // cue to drop the snapshot we held over the switch and reveal the live chart.
145
190
  if message.contains(HybridChartWebview.renderReadyMarker) { onContentRendered() }
@@ -167,6 +212,25 @@ class HybridChartWebview: HybridChartWebviewSpec {
167
212
 
168
213
  private func wantsOwnership() -> Bool { attached && (active != false) }
169
214
 
215
+ // Apply the source synchronously on a source/bridge prop change. For a pooled
216
+ // host this ALSO registers it as the warmDriver and warm-boots the page even
217
+ // when it is not the visible owner, so page->native callbacks (bars-state /
218
+ // load-end) fall back to it when no host owns the pool. No scheduleReconcile —
219
+ // that loops. `backing` is nil until the first reconcile assigns it (reuseKey /
220
+ // pooled / active setters still scheduleReconcile), so warmDriver is set on the
221
+ // first prop change after the pool entry is acquired.
222
+ private func applySource() {
223
+ guard let pooled = backing else { return }
224
+ if isPooled(), let bs = bridgeScript, !bs.isEmpty {
225
+ pooled.setBridgeScript(bs)
226
+ pooled.warmDriver = self
227
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
228
+ } else if pooled.owner === self {
229
+ pooled.setBridgeScript(bridgeScript ?? "")
230
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
231
+ }
232
+ }
233
+
170
234
  // A single React commit applies props one at a time, so the intermediate
171
235
  // states are inconsistent (e.g. `pooled` re-applied while `active` is still
172
236
  // stale). Reacting to each setter synchronously makes the wrong host claim
@@ -194,6 +258,9 @@ class HybridChartWebview: HybridChartWebviewSpec {
194
258
  let key = effectiveKey()
195
259
  let pooled = ChartWebviewPool.shared.acquireShared(key: key)
196
260
  backing = pooled
261
+ // Apply this host's debug preference to the (possibly freshly created) backing
262
+ // WebView. Mirrors the main react-native-webview toggling WKWebView.isInspectable.
263
+ pooled.setInspectable(webviewDebuggingEnabled)
197
264
  // Refcount the entry once per host (reconcile runs many times). Balanced by
198
265
  // releaseShared in deinit. If the reuseKey changed, release the old one first.
199
266
  if adoptedPoolKey != key {
@@ -201,13 +268,36 @@ class HybridChartWebview: HybridChartWebviewSpec {
201
268
  ChartWebviewPool.shared.adopt(key: key)
202
269
  adoptedPoolKey = key
203
270
  }
271
+ ChartWVLog.i("reconcilePooled id=\(instanceId) key=\(key) wantsOwnership=\(wantsOwnership()) (attached=\(attached) active=\(String(describing: active))) bridgeLen=\((bridgeScript ?? "").count) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil")")
272
+
273
+ // Q1 FIX: WARM-BOOT the shared offline page as soon as ANY referencing host
274
+ // has the document-start bridge + a source — even when this host is NOT the
275
+ // visible owner (the offscreen prewarm runs with attached=false/active=false,
276
+ // so the old code, which only loaded inside `wantsOwnership()`, never booted
277
+ // the page until a real chart screen attached+focused). LOAD is now decoupled
278
+ // from view ownership: ownership below still governs attach / reveal / snapshot.
279
+ // Idempotent across hosts — the unified URL is constant (setSource logs
280
+ // SAME_URL), and setBridgeScript no-ops once registered.
281
+ if let bs = bridgeScript, !bs.isEmpty {
282
+ ChartWVLog.i("reconcilePooled.WARM id=\(instanceId) key=\(key) (owner-independent boot; wantsOwnership=\(wantsOwnership()))")
283
+ pooled.setBridgeScript(bs)
284
+ // Q1 FIX (data): register as the warm DRIVER (NOT owner — the YIELD branch
285
+ // below clears `owner`, which is why the previous provisional-owner attempt
286
+ // was wiped in the same reconcile). didFinish / page messages fall back to
287
+ // warmDriver when there's no visible owner, so the page is driven its symbol
288
+ // the instant it loads instead of waiting ~5s for a focused host to claim.
289
+ pooled.warmDriver = self
290
+ ChartWVLog.i("reconcilePooled.WARM_DRIVER id=\(instanceId) key=\(key)")
291
+ // setSource internally guards (bridgeRegistered / SAME_URL / NO_URL), so this
292
+ // is idempotent and a no-op when there is nothing new to load.
293
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
294
+ }
295
+
204
296
  if wantsOwnership() {
297
+ let wasOwner = pooled.owner === self
205
298
  pooled.owner = self
206
- // Register the document-start bridge before the first load (the prop is set
207
- // by now; the pool may have been created earlier by a window-attach reconcile).
208
- pooled.setBridgeScript(bridgeScript ?? "")
299
+ ChartWVLog.i("reconcilePooled.CLAIM id=\(instanceId) key=\(key) wasOwner=\(wasOwner)")
209
300
  pooled.attach(to: container)
210
- pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
211
301
  // Keep a fresh frame of OUR content while we own the WebView, so that when
212
302
  // we later go inactive (and the shared WebView reloads the other slot's
213
303
  // chart) our slot freezes to its own last frame, not the other content.
@@ -224,6 +314,7 @@ class HybridChartWebview: HybridChartWebviewSpec {
224
314
  // Inactive: give up ownership only if we still hold it, and freeze to our
225
315
  // own last captured frame. We do NOT detach (that races a rapid re-claim).
226
316
  let wasOwner = pooled.owner === self
317
+ ChartWVLog.i("reconcilePooled.YIELD id=\(instanceId) key=\(key) wasOwner=\(wasOwner) (attached=\(attached) active=\(String(describing: active)))")
227
318
  if wasOwner { pooled.owner = nil }
228
319
  stopOwnCapture()
229
320
  showPlaceholder(ownSnapshot)
@@ -262,19 +353,13 @@ class HybridChartWebview: HybridChartWebviewSpec {
262
353
  guard wantsOwnership() else { return }
263
354
  let pooled = backing ?? PooledChartWebView(key: effectiveKey())
264
355
  backing = pooled
356
+ pooled.setInspectable(webviewDebuggingEnabled)
265
357
  pooled.owner = self
266
358
  pooled.setBridgeScript(bridgeScript ?? "")
267
359
  pooled.attach(to: container)
268
360
  pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
269
361
  }
270
362
 
271
- private func applySourceIfOwner() {
272
- guard let pooled = backing, pooled.owner === self else { return }
273
- // Register the bridge before any load setSource may trigger (setSource defers
274
- // loading until the bridge is registered).
275
- pooled.setBridgeScript(bridgeScript ?? "")
276
- pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
277
- }
278
363
 
279
364
  // MARK: - Snapshot placeholder (shown while this host is inactive)
280
365
 
@@ -352,6 +437,13 @@ final class PooledChartWebView {
352
437
 
353
438
  let key: String
354
439
  weak var owner: HybridChartWebview?
440
+ // Q1 FIX (data): the host that warm-booted the page and can drive its symbol /
441
+ // service its callbacks while there is no VISIBLE owner yet. Separate from
442
+ // `owner` because the reconcile YIELD branch clears `owner` to nil (so a
443
+ // provisional owner never survived to nav.didFinish). Callbacks fall back to
444
+ // this when `owner` is nil, so the page is told its symbol the instant it loads
445
+ // instead of waiting ~5s for a focused host to attach/claim.
446
+ weak var warmDriver: HybridChartWebview?
355
447
 
356
448
  // The page's user-content controller, kept so the document-start bridge can be
357
449
  // added lazily (see setBridgeScript) once the host has the prop value.
@@ -366,6 +458,11 @@ final class PooledChartWebView {
366
458
  private var lastLoadedUrl: String?
367
459
  private var attachGeneration = 0
368
460
 
461
+ // Whether the backing WKWebView should be inspectable (Safari Web Inspector).
462
+ // nil until a host applies its debug preference; defaults to the DEBUG build
463
+ // behavior (see resolvedInspectable), mirroring the main react-native-webview.
464
+ private var inspectablePreference: Bool?
465
+
369
466
  /// Read by the scheme handler to resolve offline files.
370
467
  fileprivate var currentLocalBundle: String?
371
468
 
@@ -379,7 +476,7 @@ final class PooledChartWebView {
379
476
  init(key: String) {
380
477
  self.key = key
381
478
  PooledChartWebView.liveCount += 1
382
- NSLog("[ChartWebviewPool] WebView CREATED key=\(key) liveCount=\(PooledChartWebView.liveCount)")
479
+ ChartWVLog.i("pool.CREATE key=\(key) liveCount=\(PooledChartWebView.liveCount)")
383
480
  setupWebView()
384
481
  }
385
482
 
@@ -388,7 +485,10 @@ final class PooledChartWebView {
388
485
  // subsequent calls are no-ops. Done lazily because the first reconcile
389
486
  // (window-attach) can run before the bridgeScript prop is applied.
390
487
  func setBridgeScript(_ bridgeScript: String) {
391
- guard !bridgeScript.isEmpty, let userContent = userContent else { return }
488
+ guard !bridgeScript.isEmpty, let userContent = userContent else {
489
+ ChartWVLog.w("setBridgeScript.SKIP key=\(key) empty=\(bridgeScript.isEmpty) hasUserContent=\(userContent != nil) -> bridgeRegistered stays \(bridgeRegistered)")
490
+ return
491
+ }
392
492
  // Re-register if a second host sharing the reuseKey supplies a DIFFERENT script
393
493
  // instead of silently dropping it (fix #4). In the app's single-reuseKey +
394
494
  // constant-bridge reality this never fires; not latching keeps it correct.
@@ -399,6 +499,7 @@ final class PooledChartWebView {
399
499
  }
400
500
  bridgeRegistered = true
401
501
  registeredBridgeScript = bridgeScript
502
+ ChartWVLog.i("setBridgeScript.REGISTERED key=\(key) len=\(bridgeScript.count)")
402
503
  let handlerName = ChartWebviewConst.messageHandlerName
403
504
  let shim = "(function(){window.__chartNativePost=function(s){"
404
505
  + "window.webkit.messageHandlers.\(handlerName).postMessage(s);};})();"
@@ -442,10 +543,37 @@ final class PooledChartWebView {
442
543
  webView.navigationDelegate = proxy
443
544
  webView.uiDelegate = proxy
444
545
  webView.scrollView.bounces = false
546
+ self.webView = webView
547
+ // Honor the app's dev-mode webview-debug toggle instead of an unconditional
548
+ // `true`. nil (no preference applied yet) falls back to the DEBUG build default.
549
+ applyInspectable()
550
+ }
551
+
552
+ // Set the host-driven debug preference and apply it to the live WebView. Called
553
+ // both on the prop change and at host claim, so the (possibly newly created)
554
+ // WebView always reflects the latest toggle value.
555
+ func setInspectable(_ enabled: Bool?) {
556
+ inspectablePreference = enabled
557
+ applyInspectable()
558
+ }
559
+
560
+ // Resolve the effective inspectable value: explicit preference if set, otherwise
561
+ // the DEBUG build default (mirrors the main react-native-webview, which is
562
+ // inspectable in dev builds even without the toggle).
563
+ private func resolvedInspectable() -> Bool {
564
+ if let pref = inspectablePreference { return pref }
565
+ #if DEBUG
566
+ return true
567
+ #else
568
+ return false
569
+ #endif
570
+ }
571
+
572
+ private func applyInspectable() {
573
+ guard let webView = webView else { return }
445
574
  if #available(iOS 16.4, *) {
446
- webView.isInspectable = true
575
+ webView.isInspectable = resolvedInspectable()
447
576
  }
448
- self.webView = webView
449
577
  }
450
578
 
451
579
  // MARK: - Reparenting
@@ -498,7 +626,7 @@ final class PooledChartWebView {
498
626
  self.userContent = nil
499
627
  self.webView = nil
500
628
  PooledChartWebView.liveCount -= 1
501
- NSLog("[ChartWebviewPool] WebView DESTROYED key=\(self.key) liveCount=\(PooledChartWebView.liveCount)")
629
+ ChartWVLog.i("pool.DESTROY key=\(self.key) liveCount=\(PooledChartWebView.liveCount)")
502
630
  }
503
631
  }
504
632
 
@@ -570,15 +698,26 @@ final class PooledChartWebView {
570
698
  // Never load before the document-start bridge is registered — otherwise the
571
699
  // page boots without the bridge and its first $private requests are lost. The
572
700
  // host re-calls setSource once the bridgeScript prop arrives.
573
- guard bridgeRegistered else { return }
701
+ guard bridgeRegistered else {
702
+ ChartWVLog.w("setSource.BLOCKED key=\(key) bridgeRegistered=false -> NOT loading (will retry when bridgeScript arrives). uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil")")
703
+ return
704
+ }
574
705
  currentLocalBundle = localBundle
575
- guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else { return }
576
- guard urlString != lastLoadedUrl else { return }
706
+ guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else {
707
+ ChartWVLog.w("setSource.NO_URL key=\(key) uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil") -> nothing to load")
708
+ return
709
+ }
710
+ guard urlString != lastLoadedUrl else {
711
+ ChartWVLog.i("setSource.SAME_URL key=\(key) skip reload url=\(ChartWVLog.clip(urlString))")
712
+ return
713
+ }
577
714
  guard let url = URL(string: urlString) else {
715
+ ChartWVLog.e("setSource.INVALID_URL key=\(key) url=\(ChartWVLog.clip(urlString))")
578
716
  owner?.handleError("Invalid url: \(urlString)")
579
717
  return
580
718
  }
581
719
  lastLoadedUrl = urlString
720
+ ChartWVLog.i("setSource.LOAD key=\(key) url=\(ChartWVLog.clip(urlString))")
582
721
  runOnMain { [weak self] in self?.webView?.load(URLRequest(url: url)) }
583
722
  }
584
723
 
@@ -622,8 +761,12 @@ final class PooledChartWebView {
622
761
  // MARK: - Bridge methods
623
762
 
624
763
  func postMessage(_ message: String) {
764
+ ChartWVLog.i("msg.out key=\(key) \(ChartWVLog.clip(message, 200))")
625
765
  runOnMain { [weak self] in
626
- guard let self = self, let webView = self.webView else { return }
766
+ guard let self = self, let webView = self.webView else {
767
+ ChartWVLog.w("msg.out.DROPPED no webView")
768
+ return
769
+ }
627
770
  let jsStringLiteral = self.jsStringLiteral(from: message)
628
771
  let js = "window.postMessage(JSON.parse(\(jsStringLiteral)), '*')"
629
772
  webView.evaluateJavaScript(js, completionHandler: nil)
@@ -677,7 +820,9 @@ extension ChartWebViewProxy: WKScriptMessageHandler {
677
820
  didReceive message: WKScriptMessage
678
821
  ) {
679
822
  guard message.name == ChartWebviewConst.messageHandlerName else { return }
680
- let owner = pooled?.owner
823
+ // Q1 FIX: route page->native messages ($private data requests) to the warm
824
+ // driver when there's no visible owner, so they aren't dropped during warm.
825
+ let owner = pooled?.owner ?? pooled?.warmDriver
681
826
  if let body = message.body as? String {
682
827
  owner?.handleMessage(body)
683
828
  } else if let data = try? JSONSerialization.data(withJSONObject: message.body, options: []),
@@ -693,16 +838,22 @@ extension ChartWebViewProxy: WKScriptMessageHandler {
693
838
 
694
839
  extension ChartWebViewProxy: WKNavigationDelegate {
695
840
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
696
- pooled?.owner?.handleLoadEnd()
841
+ // Q1 FIX: fall back to warmDriver when there's no visible owner yet, so the
842
+ // page's load-complete callback (-> JS onLoadEnd -> SYMBOL_CHANGE) fires now.
843
+ let target = pooled?.owner ?? pooled?.warmDriver
844
+ ChartWVLog.i("nav.didFinish url=\(ChartWVLog.clip(webView.url?.absoluteString)) hasOwner=\(pooled?.owner != nil) viaWarmDriver=\(pooled?.owner == nil && pooled?.warmDriver != nil)")
845
+ target?.handleLoadEnd()
697
846
  // Prime the snapshot so the first move already has a frame to mask with.
698
847
  pooled?.refreshSnapshotSoon()
699
848
  }
700
849
 
701
850
  func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
851
+ ChartWVLog.e("nav.didFail url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)")
702
852
  pooled?.owner?.handleError(error.localizedDescription)
703
853
  }
704
854
 
705
855
  func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
856
+ ChartWVLog.e("nav.didFailProvisional url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)")
706
857
  pooled?.owner?.handleError(error.localizedDescription)
707
858
  }
708
859
  }
@@ -723,6 +874,7 @@ extension ChartWebViewProxy: WKURLSchemeHandler {
723
874
  }
724
875
 
725
876
  guard let localBundle = pooled?.currentLocalBundle, !localBundle.isEmpty else {
877
+ ChartWVLog.e("scheme.NO_BUNDLE url=\(ChartWVLog.clip(url.absoluteString)) -> 404 (currentLocalBundle empty)")
726
878
  respondNotFound(url: url, task: urlSchemeTask)
727
879
  return
728
880
  }
@@ -733,9 +885,11 @@ extension ChartWebViewProxy: WKURLSchemeHandler {
733
885
 
734
886
  guard let fileURL = resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath),
735
887
  let data = try? Data(contentsOf: fileURL) else {
888
+ ChartWVLog.e("scheme.404 bundle=\(localBundle) path=\(relativePath) (resolved=\(resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath)?.path ?? "nil")) -> Not Found")
736
889
  respondNotFound(url: url, task: urlSchemeTask)
737
890
  return
738
891
  }
892
+ ChartWVLog.i("scheme.serve bundle=\(localBundle) path=\(relativePath) bytes=\(data.count)")
739
893
 
740
894
  let headers: [String: String] = [
741
895
  "Content-Type": mimeTypeForPath(fileURL.pathExtension),
@@ -117,6 +117,15 @@ namespace margelo::nitro::chartwebview {
117
117
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* active */)>("setActive");
118
118
  method(_javaPart, active.has_value() ? jni::JBoolean::valueOf(active.value()) : nullptr);
119
119
  }
120
+ std::optional<bool> JHybridChartWebviewSpec::getWebviewDebuggingEnabled() {
121
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JBoolean>()>("getWebviewDebuggingEnabled");
122
+ auto __result = method(_javaPart);
123
+ return __result != nullptr ? std::make_optional(static_cast<bool>(__result->value())) : std::nullopt;
124
+ }
125
+ void JHybridChartWebviewSpec::setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) {
126
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* webviewDebuggingEnabled */)>("setWebviewDebuggingEnabled");
127
+ method(_javaPart, webviewDebuggingEnabled.has_value() ? jni::JBoolean::valueOf(webviewDebuggingEnabled.value()) : nullptr);
128
+ }
120
129
  std::optional<std::function<void(const std::string& /* message */)>> JHybridChartWebviewSpec::getOnMessage() {
121
130
  static const auto method = javaClassStatic()->getMethod<jni::local_ref<JFunc_void_std__string::javaobject>()>("getOnMessage_cxx");
122
131
  auto __result = method(_javaPart);
@@ -66,6 +66,8 @@ namespace margelo::nitro::chartwebview {
66
66
  void setPooled(std::optional<bool> pooled) override;
67
67
  std::optional<bool> getActive() override;
68
68
  void setActive(std::optional<bool> active) override;
69
+ std::optional<bool> getWebviewDebuggingEnabled() override;
70
+ void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) override;
69
71
  std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() override;
70
72
  void setOnMessage(const std::optional<std::function<void(const std::string& /* message */)>>& onMessage) override;
71
73
  std::optional<std::function<void()>> getOnLoadEnd() override;
@@ -68,6 +68,10 @@ void JHybridChartWebviewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass
68
68
  view->setActive(props.active.value);
69
69
  // TODO: Set isDirty = false
70
70
  }
71
+ if (props.webviewDebuggingEnabled.isDirty) {
72
+ view->setWebviewDebuggingEnabled(props.webviewDebuggingEnabled.value);
73
+ // TODO: Set isDirty = false
74
+ }
71
75
  if (props.onMessage.isDirty) {
72
76
  view->setOnMessage(props.onMessage.value);
73
77
  // TODO: Set isDirty = false
@@ -90,6 +90,12 @@ abstract class HybridChartWebviewSpec: HybridView() {
90
90
  @set:Keep
91
91
  abstract var active: Boolean?
92
92
 
93
+ @get:DoNotStrip
94
+ @get:Keep
95
+ @set:DoNotStrip
96
+ @set:Keep
97
+ abstract var webviewDebuggingEnabled: Boolean?
98
+
93
99
  abstract var onMessage: ((message: String) -> Unit)?
94
100
 
95
101
  private var onMessage_cxx: Func_void_std__string?
@@ -114,6 +114,13 @@ namespace margelo::nitro::chartwebview {
114
114
  inline void setActive(std::optional<bool> active) noexcept override {
115
115
  _swiftPart.setActive(active);
116
116
  }
117
+ inline std::optional<bool> getWebviewDebuggingEnabled() noexcept override {
118
+ auto __result = _swiftPart.getWebviewDebuggingEnabled();
119
+ return __result;
120
+ }
121
+ inline void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) noexcept override {
122
+ _swiftPart.setWebviewDebuggingEnabled(webviewDebuggingEnabled);
123
+ }
117
124
  inline std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() noexcept override {
118
125
  auto __result = _swiftPart.getOnMessage();
119
126
  return __result;
@@ -111,6 +111,11 @@ using namespace margelo::nitro::chartwebview::views;
111
111
  swiftPart.setActive(newViewProps.active.value);
112
112
  newViewProps.active.isDirty = false;
113
113
  }
114
+ // webviewDebuggingEnabled: optional
115
+ if (newViewProps.webviewDebuggingEnabled.isDirty) {
116
+ swiftPart.setWebviewDebuggingEnabled(newViewProps.webviewDebuggingEnabled.value);
117
+ newViewProps.webviewDebuggingEnabled.isDirty = false;
118
+ }
114
119
  // onMessage: optional
115
120
  if (newViewProps.onMessage.isDirty) {
116
121
  swiftPart.setOnMessage(newViewProps.onMessage.value);
@@ -19,6 +19,7 @@ public protocol HybridChartWebviewSpec_protocol: HybridObject, HybridView {
19
19
  var reuseKey: String? { get set }
20
20
  var pooled: Bool? { get set }
21
21
  var active: Bool? { get set }
22
+ var webviewDebuggingEnabled: Bool? { get set }
22
23
  var onMessage: ((_ message: String) -> Void)? { get set }
23
24
  var onLoadEnd: (() -> Void)? { get set }
24
25
  var onError: ((_ message: String) -> Void)? { get set }
@@ -306,6 +306,30 @@ open class HybridChartWebviewSpec_cxx {
306
306
  }
307
307
  }
308
308
 
309
+ public final var webviewDebuggingEnabled: bridge.std__optional_bool_ {
310
+ @inline(__always)
311
+ get {
312
+ return { () -> bridge.std__optional_bool_ in
313
+ if let __unwrappedValue = self.__implementation.webviewDebuggingEnabled {
314
+ return bridge.create_std__optional_bool_(__unwrappedValue)
315
+ } else {
316
+ return .init()
317
+ }
318
+ }()
319
+ }
320
+ @inline(__always)
321
+ set {
322
+ self.__implementation.webviewDebuggingEnabled = { () -> Bool? in
323
+ if bridge.has_value_std__optional_bool_(newValue) {
324
+ let __unwrapped = bridge.get_std__optional_bool_(newValue)
325
+ return __unwrapped
326
+ } else {
327
+ return nil
328
+ }
329
+ }()
330
+ }
331
+ }
332
+
309
333
  public final var onMessage: bridge.std__optional_std__function_void_const_std__string_____message______ {
310
334
  @inline(__always)
311
335
  get {
@@ -30,6 +30,8 @@ namespace margelo::nitro::chartwebview {
30
30
  prototype.registerHybridSetter("pooled", &HybridChartWebviewSpec::setPooled);
31
31
  prototype.registerHybridGetter("active", &HybridChartWebviewSpec::getActive);
32
32
  prototype.registerHybridSetter("active", &HybridChartWebviewSpec::setActive);
33
+ prototype.registerHybridGetter("webviewDebuggingEnabled", &HybridChartWebviewSpec::getWebviewDebuggingEnabled);
34
+ prototype.registerHybridSetter("webviewDebuggingEnabled", &HybridChartWebviewSpec::setWebviewDebuggingEnabled);
33
35
  prototype.registerHybridGetter("onMessage", &HybridChartWebviewSpec::getOnMessage);
34
36
  prototype.registerHybridSetter("onMessage", &HybridChartWebviewSpec::setOnMessage);
35
37
  prototype.registerHybridGetter("onLoadEnd", &HybridChartWebviewSpec::getOnLoadEnd);
@@ -62,6 +62,8 @@ namespace margelo::nitro::chartwebview {
62
62
  virtual void setPooled(std::optional<bool> pooled) = 0;
63
63
  virtual std::optional<bool> getActive() = 0;
64
64
  virtual void setActive(std::optional<bool> active) = 0;
65
+ virtual std::optional<bool> getWebviewDebuggingEnabled() = 0;
66
+ virtual void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) = 0;
65
67
  virtual std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() = 0;
66
68
  virtual void setOnMessage(const std::optional<std::function<void(const std::string& /* message */)>>& onMessage) = 0;
67
69
  virtual std::optional<std::function<void()>> getOnLoadEnd() = 0;
@@ -105,6 +105,16 @@ namespace margelo::nitro::chartwebview::views {
105
105
  throw std::runtime_error(std::string("ChartWebview.active: ") + exc.what());
106
106
  }
107
107
  }()),
108
+ webviewDebuggingEnabled([&]() -> CachedProp<std::optional<bool>> {
109
+ try {
110
+ const react::RawValue* rawValue = rawProps.at("webviewDebuggingEnabled", nullptr, nullptr);
111
+ if (rawValue == nullptr) return sourceProps.webviewDebuggingEnabled;
112
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
113
+ return CachedProp<std::optional<bool>>::fromRawValue(*runtime, value, sourceProps.webviewDebuggingEnabled);
114
+ } catch (const std::exception& exc) {
115
+ throw std::runtime_error(std::string("ChartWebview.webviewDebuggingEnabled: ") + exc.what());
116
+ }
117
+ }()),
108
118
  onMessage([&]() -> CachedProp<std::optional<std::function<void(const std::string& /* message */)>>> {
109
119
  try {
110
120
  const react::RawValue* rawValue = rawProps.at("onMessage", nullptr, nullptr);
@@ -156,6 +166,7 @@ namespace margelo::nitro::chartwebview::views {
156
166
  reuseKey(other.reuseKey),
157
167
  pooled(other.pooled),
158
168
  active(other.active),
169
+ webviewDebuggingEnabled(other.webviewDebuggingEnabled),
159
170
  onMessage(other.onMessage),
160
171
  onLoadEnd(other.onLoadEnd),
161
172
  onError(other.onError),
@@ -171,6 +182,7 @@ namespace margelo::nitro::chartwebview::views {
171
182
  case hashString("reuseKey"): return true;
172
183
  case hashString("pooled"): return true;
173
184
  case hashString("active"): return true;
185
+ case hashString("webviewDebuggingEnabled"): return true;
174
186
  case hashString("onMessage"): return true;
175
187
  case hashString("onLoadEnd"): return true;
176
188
  case hashString("onError"): return true;
@@ -51,6 +51,7 @@ namespace margelo::nitro::chartwebview::views {
51
51
  CachedProp<std::optional<std::string>> reuseKey;
52
52
  CachedProp<std::optional<bool>> pooled;
53
53
  CachedProp<std::optional<bool>> active;
54
+ CachedProp<std::optional<bool>> webviewDebuggingEnabled;
54
55
  CachedProp<std::optional<std::function<void(const std::string& /* message */)>>> onMessage;
55
56
  CachedProp<std::optional<std::function<void()>>> onLoadEnd;
56
57
  CachedProp<std::optional<std::function<void(const std::string& /* message */)>>> onError;
@@ -12,6 +12,7 @@
12
12
  "reuseKey": true,
13
13
  "pooled": true,
14
14
  "active": true,
15
+ "webviewDebuggingEnabled": true,
15
16
  "onMessage": true,
16
17
  "onLoadEnd": true,
17
18
  "onError": true,
@@ -117,6 +117,15 @@ namespace margelo::nitro::chartwebview {
117
117
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* active */)>("setActive");
118
118
  method(_javaPart, active.has_value() ? jni::JBoolean::valueOf(active.value()) : nullptr);
119
119
  }
120
+ std::optional<bool> JHybridChartWebviewSpec::getWebviewDebuggingEnabled() {
121
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JBoolean>()>("getWebviewDebuggingEnabled");
122
+ auto __result = method(_javaPart);
123
+ return __result != nullptr ? std::make_optional(static_cast<bool>(__result->value())) : std::nullopt;
124
+ }
125
+ void JHybridChartWebviewSpec::setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) {
126
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* webviewDebuggingEnabled */)>("setWebviewDebuggingEnabled");
127
+ method(_javaPart, webviewDebuggingEnabled.has_value() ? jni::JBoolean::valueOf(webviewDebuggingEnabled.value()) : nullptr);
128
+ }
120
129
  std::optional<std::function<void(const std::string& /* message */)>> JHybridChartWebviewSpec::getOnMessage() {
121
130
  static const auto method = javaClassStatic()->getMethod<jni::local_ref<JFunc_void_std__string::javaobject>()>("getOnMessage_cxx");
122
131
  auto __result = method(_javaPart);
@@ -66,6 +66,8 @@ namespace margelo::nitro::chartwebview {
66
66
  void setPooled(std::optional<bool> pooled) override;
67
67
  std::optional<bool> getActive() override;
68
68
  void setActive(std::optional<bool> active) override;
69
+ std::optional<bool> getWebviewDebuggingEnabled() override;
70
+ void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) override;
69
71
  std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() override;
70
72
  void setOnMessage(const std::optional<std::function<void(const std::string& /* message */)>>& onMessage) override;
71
73
  std::optional<std::function<void()>> getOnLoadEnd() override;
@@ -68,6 +68,10 @@ void JHybridChartWebviewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass
68
68
  view->setActive(props.active.value);
69
69
  // TODO: Set isDirty = false
70
70
  }
71
+ if (props.webviewDebuggingEnabled.isDirty) {
72
+ view->setWebviewDebuggingEnabled(props.webviewDebuggingEnabled.value);
73
+ // TODO: Set isDirty = false
74
+ }
71
75
  if (props.onMessage.isDirty) {
72
76
  view->setOnMessage(props.onMessage.value);
73
77
  // TODO: Set isDirty = false