@onekeyfe/react-native-chart-webview 3.0.63 → 3.0.65

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 +85 -18
  3. package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +253 -26
  4. package/ios/ChartWebview.swift +190 -31
  5. package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
  6. package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
  7. package/lib/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
  8. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
  9. package/lib/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
  10. package/lib/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
  11. package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
  12. package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
  13. package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
  14. package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
  15. package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
  16. package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
  17. package/lib/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
  18. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
  19. package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
  20. package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
  21. package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
  22. package/lib/nitrogen/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
  23. package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
  24. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
  25. package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
  26. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
  27. package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
  28. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
  29. package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
  30. package/lib/nitrogen/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
  31. package/lib/typescript/src/ChartWebview.nitro.d.ts +2 -0
  32. package/lib/typescript/src/ChartWebview.nitro.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +2 -0
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +18 -0
  36. package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +4 -0
  37. package/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +8 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +12 -0
  39. package/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +14 -0
  40. package/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +10 -0
  41. package/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +2 -0
  42. package/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +48 -0
  43. package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +4 -0
  44. package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +4 -0
  45. package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +24 -0
  46. package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +2 -0
  47. package/nitrogen/generated/shared/json/ChartWebviewConfig.json +2 -0
  48. package/package.json +1 -1
  49. package/src/ChartWebview.nitro.ts +18 -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,24 @@ 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() } }
132
+ // ANDROID ONLY. iOS serves offline content from the onekey-chart:// custom
133
+ // scheme (https can't be intercepted here), so this host stores the prop to
134
+ // satisfy the generated spec but never reads it — offline origin/behavior is
135
+ // unchanged on iOS.
136
+ var assetHost: String?
104
137
 
105
138
  // 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() } }
139
+ // the pooled WebView when we claim it, before its first load.
140
+ var bridgeScript: String? { didSet { applySource() } }
109
141
 
110
142
  // MARK: - Props (singleton)
111
143
 
@@ -132,6 +164,20 @@ class HybridChartWebview: HybridChartWebviewSpec {
132
164
  }
133
165
  }
134
166
 
167
+ // MARK: - Props (debugging)
168
+
169
+ // Make the backing WKWebView inspectable (Safari Web Inspector). Driven by the
170
+ // app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main
171
+ // react-native-webview (which sets WKWebView.isInspectable). nil => default to
172
+ // the DEBUG build behavior (see PooledChartWebView.applyInspectable). Applied to
173
+ // the backing WebView both here (on prop change) and at WebView creation, since
174
+ // `backing` is nil until the first reconcile assigns it.
175
+ var webviewDebuggingEnabled: Bool? {
176
+ didSet {
177
+ backing?.setInspectable(webviewDebuggingEnabled)
178
+ }
179
+ }
180
+
135
181
  // MARK: - Props (events)
136
182
 
137
183
  var onMessage: ((_ message: String) -> Void)?
@@ -140,6 +186,10 @@ class HybridChartWebview: HybridChartWebviewSpec {
140
186
 
141
187
  // Called by the backing PooledChartWebView while this host is the owner.
142
188
  func handleMessage(_ message: String) {
189
+ // page -> native. Truncated; OneKeyLog rate-limits/dedups repeats so streaming
190
+ // data won't flood. Key for Q2 (market chart no data): shows the page's
191
+ // $private kline requests and whether replies come back.
192
+ ChartWVLog.i("msg.in id=\(instanceId) \(ChartWVLog.clip(message, 200))")
143
193
  // The chart reports it has painted the new symbol after a switch; that's our
144
194
  // cue to drop the snapshot we held over the switch and reveal the live chart.
145
195
  if message.contains(HybridChartWebview.renderReadyMarker) { onContentRendered() }
@@ -167,6 +217,25 @@ class HybridChartWebview: HybridChartWebviewSpec {
167
217
 
168
218
  private func wantsOwnership() -> Bool { attached && (active != false) }
169
219
 
220
+ // Apply the source synchronously on a source/bridge prop change. For a pooled
221
+ // host this ALSO registers it as the warmDriver and warm-boots the page even
222
+ // when it is not the visible owner, so page->native callbacks (bars-state /
223
+ // load-end) fall back to it when no host owns the pool. No scheduleReconcile —
224
+ // that loops. `backing` is nil until the first reconcile assigns it (reuseKey /
225
+ // pooled / active setters still scheduleReconcile), so warmDriver is set on the
226
+ // first prop change after the pool entry is acquired.
227
+ private func applySource() {
228
+ guard let pooled = backing else { return }
229
+ if isPooled(), let bs = bridgeScript, !bs.isEmpty {
230
+ pooled.setBridgeScript(bs)
231
+ pooled.warmDriver = self
232
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
233
+ } else if pooled.owner === self {
234
+ pooled.setBridgeScript(bridgeScript ?? "")
235
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
236
+ }
237
+ }
238
+
170
239
  // A single React commit applies props one at a time, so the intermediate
171
240
  // states are inconsistent (e.g. `pooled` re-applied while `active` is still
172
241
  // stale). Reacting to each setter synchronously makes the wrong host claim
@@ -194,6 +263,9 @@ class HybridChartWebview: HybridChartWebviewSpec {
194
263
  let key = effectiveKey()
195
264
  let pooled = ChartWebviewPool.shared.acquireShared(key: key)
196
265
  backing = pooled
266
+ // Apply this host's debug preference to the (possibly freshly created) backing
267
+ // WebView. Mirrors the main react-native-webview toggling WKWebView.isInspectable.
268
+ pooled.setInspectable(webviewDebuggingEnabled)
197
269
  // Refcount the entry once per host (reconcile runs many times). Balanced by
198
270
  // releaseShared in deinit. If the reuseKey changed, release the old one first.
199
271
  if adoptedPoolKey != key {
@@ -201,13 +273,36 @@ class HybridChartWebview: HybridChartWebviewSpec {
201
273
  ChartWebviewPool.shared.adopt(key: key)
202
274
  adoptedPoolKey = key
203
275
  }
276
+ ChartWVLog.i("reconcilePooled id=\(instanceId) key=\(key) wantsOwnership=\(wantsOwnership()) (attached=\(attached) active=\(String(describing: active))) bridgeLen=\((bridgeScript ?? "").count) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil")")
277
+
278
+ // Q1 FIX: WARM-BOOT the shared offline page as soon as ANY referencing host
279
+ // has the document-start bridge + a source — even when this host is NOT the
280
+ // visible owner (the offscreen prewarm runs with attached=false/active=false,
281
+ // so the old code, which only loaded inside `wantsOwnership()`, never booted
282
+ // the page until a real chart screen attached+focused). LOAD is now decoupled
283
+ // from view ownership: ownership below still governs attach / reveal / snapshot.
284
+ // Idempotent across hosts — the unified URL is constant (setSource logs
285
+ // SAME_URL), and setBridgeScript no-ops once registered.
286
+ if let bs = bridgeScript, !bs.isEmpty {
287
+ ChartWVLog.i("reconcilePooled.WARM id=\(instanceId) key=\(key) (owner-independent boot; wantsOwnership=\(wantsOwnership()))")
288
+ pooled.setBridgeScript(bs)
289
+ // Q1 FIX (data): register as the warm DRIVER (NOT owner — the YIELD branch
290
+ // below clears `owner`, which is why the previous provisional-owner attempt
291
+ // was wiped in the same reconcile). didFinish / page messages fall back to
292
+ // warmDriver when there's no visible owner, so the page is driven its symbol
293
+ // the instant it loads instead of waiting ~5s for a focused host to claim.
294
+ pooled.warmDriver = self
295
+ ChartWVLog.i("reconcilePooled.WARM_DRIVER id=\(instanceId) key=\(key)")
296
+ // setSource internally guards (bridgeRegistered / SAME_URL / NO_URL), so this
297
+ // is idempotent and a no-op when there is nothing new to load.
298
+ pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
299
+ }
300
+
204
301
  if wantsOwnership() {
302
+ let wasOwner = pooled.owner === self
205
303
  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 ?? "")
304
+ ChartWVLog.i("reconcilePooled.CLAIM id=\(instanceId) key=\(key) wasOwner=\(wasOwner)")
209
305
  pooled.attach(to: container)
210
- pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
211
306
  // Keep a fresh frame of OUR content while we own the WebView, so that when
212
307
  // we later go inactive (and the shared WebView reloads the other slot's
213
308
  // chart) our slot freezes to its own last frame, not the other content.
@@ -224,6 +319,7 @@ class HybridChartWebview: HybridChartWebviewSpec {
224
319
  // Inactive: give up ownership only if we still hold it, and freeze to our
225
320
  // own last captured frame. We do NOT detach (that races a rapid re-claim).
226
321
  let wasOwner = pooled.owner === self
322
+ ChartWVLog.i("reconcilePooled.YIELD id=\(instanceId) key=\(key) wasOwner=\(wasOwner) (attached=\(attached) active=\(String(describing: active)))")
227
323
  if wasOwner { pooled.owner = nil }
228
324
  stopOwnCapture()
229
325
  showPlaceholder(ownSnapshot)
@@ -262,19 +358,13 @@ class HybridChartWebview: HybridChartWebviewSpec {
262
358
  guard wantsOwnership() else { return }
263
359
  let pooled = backing ?? PooledChartWebView(key: effectiveKey())
264
360
  backing = pooled
361
+ pooled.setInspectable(webviewDebuggingEnabled)
265
362
  pooled.owner = self
266
363
  pooled.setBridgeScript(bridgeScript ?? "")
267
364
  pooled.attach(to: container)
268
365
  pooled.setSource(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson)
269
366
  }
270
367
 
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
368
 
279
369
  // MARK: - Snapshot placeholder (shown while this host is inactive)
280
370
 
@@ -352,6 +442,13 @@ final class PooledChartWebView {
352
442
 
353
443
  let key: String
354
444
  weak var owner: HybridChartWebview?
445
+ // Q1 FIX (data): the host that warm-booted the page and can drive its symbol /
446
+ // service its callbacks while there is no VISIBLE owner yet. Separate from
447
+ // `owner` because the reconcile YIELD branch clears `owner` to nil (so a
448
+ // provisional owner never survived to nav.didFinish). Callbacks fall back to
449
+ // this when `owner` is nil, so the page is told its symbol the instant it loads
450
+ // instead of waiting ~5s for a focused host to attach/claim.
451
+ weak var warmDriver: HybridChartWebview?
355
452
 
356
453
  // The page's user-content controller, kept so the document-start bridge can be
357
454
  // added lazily (see setBridgeScript) once the host has the prop value.
@@ -366,6 +463,11 @@ final class PooledChartWebView {
366
463
  private var lastLoadedUrl: String?
367
464
  private var attachGeneration = 0
368
465
 
466
+ // Whether the backing WKWebView should be inspectable (Safari Web Inspector).
467
+ // nil until a host applies its debug preference; defaults to the DEBUG build
468
+ // behavior (see resolvedInspectable), mirroring the main react-native-webview.
469
+ private var inspectablePreference: Bool?
470
+
369
471
  /// Read by the scheme handler to resolve offline files.
370
472
  fileprivate var currentLocalBundle: String?
371
473
 
@@ -379,7 +481,7 @@ final class PooledChartWebView {
379
481
  init(key: String) {
380
482
  self.key = key
381
483
  PooledChartWebView.liveCount += 1
382
- NSLog("[ChartWebviewPool] WebView CREATED key=\(key) liveCount=\(PooledChartWebView.liveCount)")
484
+ ChartWVLog.i("pool.CREATE key=\(key) liveCount=\(PooledChartWebView.liveCount)")
383
485
  setupWebView()
384
486
  }
385
487
 
@@ -388,7 +490,10 @@ final class PooledChartWebView {
388
490
  // subsequent calls are no-ops. Done lazily because the first reconcile
389
491
  // (window-attach) can run before the bridgeScript prop is applied.
390
492
  func setBridgeScript(_ bridgeScript: String) {
391
- guard !bridgeScript.isEmpty, let userContent = userContent else { return }
493
+ guard !bridgeScript.isEmpty, let userContent = userContent else {
494
+ ChartWVLog.w("setBridgeScript.SKIP key=\(key) empty=\(bridgeScript.isEmpty) hasUserContent=\(userContent != nil) -> bridgeRegistered stays \(bridgeRegistered)")
495
+ return
496
+ }
392
497
  // Re-register if a second host sharing the reuseKey supplies a DIFFERENT script
393
498
  // instead of silently dropping it (fix #4). In the app's single-reuseKey +
394
499
  // constant-bridge reality this never fires; not latching keeps it correct.
@@ -399,6 +504,7 @@ final class PooledChartWebView {
399
504
  }
400
505
  bridgeRegistered = true
401
506
  registeredBridgeScript = bridgeScript
507
+ ChartWVLog.i("setBridgeScript.REGISTERED key=\(key) len=\(bridgeScript.count)")
402
508
  let handlerName = ChartWebviewConst.messageHandlerName
403
509
  let shim = "(function(){window.__chartNativePost=function(s){"
404
510
  + "window.webkit.messageHandlers.\(handlerName).postMessage(s);};})();"
@@ -442,10 +548,37 @@ final class PooledChartWebView {
442
548
  webView.navigationDelegate = proxy
443
549
  webView.uiDelegate = proxy
444
550
  webView.scrollView.bounces = false
551
+ self.webView = webView
552
+ // Honor the app's dev-mode webview-debug toggle instead of an unconditional
553
+ // `true`. nil (no preference applied yet) falls back to the DEBUG build default.
554
+ applyInspectable()
555
+ }
556
+
557
+ // Set the host-driven debug preference and apply it to the live WebView. Called
558
+ // both on the prop change and at host claim, so the (possibly newly created)
559
+ // WebView always reflects the latest toggle value.
560
+ func setInspectable(_ enabled: Bool?) {
561
+ inspectablePreference = enabled
562
+ applyInspectable()
563
+ }
564
+
565
+ // Resolve the effective inspectable value: explicit preference if set, otherwise
566
+ // the DEBUG build default (mirrors the main react-native-webview, which is
567
+ // inspectable in dev builds even without the toggle).
568
+ private func resolvedInspectable() -> Bool {
569
+ if let pref = inspectablePreference { return pref }
570
+ #if DEBUG
571
+ return true
572
+ #else
573
+ return false
574
+ #endif
575
+ }
576
+
577
+ private func applyInspectable() {
578
+ guard let webView = webView else { return }
445
579
  if #available(iOS 16.4, *) {
446
- webView.isInspectable = true
580
+ webView.isInspectable = resolvedInspectable()
447
581
  }
448
- self.webView = webView
449
582
  }
450
583
 
451
584
  // MARK: - Reparenting
@@ -498,7 +631,7 @@ final class PooledChartWebView {
498
631
  self.userContent = nil
499
632
  self.webView = nil
500
633
  PooledChartWebView.liveCount -= 1
501
- NSLog("[ChartWebviewPool] WebView DESTROYED key=\(self.key) liveCount=\(PooledChartWebView.liveCount)")
634
+ ChartWVLog.i("pool.DESTROY key=\(self.key) liveCount=\(PooledChartWebView.liveCount)")
502
635
  }
503
636
  }
504
637
 
@@ -570,15 +703,26 @@ final class PooledChartWebView {
570
703
  // Never load before the document-start bridge is registered — otherwise the
571
704
  // page boots without the bridge and its first $private requests are lost. The
572
705
  // host re-calls setSource once the bridgeScript prop arrives.
573
- guard bridgeRegistered else { return }
706
+ guard bridgeRegistered else {
707
+ ChartWVLog.w("setSource.BLOCKED key=\(key) bridgeRegistered=false -> NOT loading (will retry when bridgeScript arrives). uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil")")
708
+ return
709
+ }
574
710
  currentLocalBundle = localBundle
575
- guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else { return }
576
- guard urlString != lastLoadedUrl else { return }
711
+ guard let urlString = computeTargetUrl(uri: uri, localBundle: localBundle, entry: entry, paramsJson: paramsJson) else {
712
+ ChartWVLog.w("setSource.NO_URL key=\(key) uri=\(ChartWVLog.clip(uri)) localBundle=\(localBundle ?? "nil") entry=\(entry ?? "nil") -> nothing to load")
713
+ return
714
+ }
715
+ guard urlString != lastLoadedUrl else {
716
+ ChartWVLog.i("setSource.SAME_URL key=\(key) skip reload url=\(ChartWVLog.clip(urlString))")
717
+ return
718
+ }
577
719
  guard let url = URL(string: urlString) else {
720
+ ChartWVLog.e("setSource.INVALID_URL key=\(key) url=\(ChartWVLog.clip(urlString))")
578
721
  owner?.handleError("Invalid url: \(urlString)")
579
722
  return
580
723
  }
581
724
  lastLoadedUrl = urlString
725
+ ChartWVLog.i("setSource.LOAD key=\(key) url=\(ChartWVLog.clip(urlString))")
582
726
  runOnMain { [weak self] in self?.webView?.load(URLRequest(url: url)) }
583
727
  }
584
728
 
@@ -622,8 +766,12 @@ final class PooledChartWebView {
622
766
  // MARK: - Bridge methods
623
767
 
624
768
  func postMessage(_ message: String) {
769
+ ChartWVLog.i("msg.out key=\(key) \(ChartWVLog.clip(message, 200))")
625
770
  runOnMain { [weak self] in
626
- guard let self = self, let webView = self.webView else { return }
771
+ guard let self = self, let webView = self.webView else {
772
+ ChartWVLog.w("msg.out.DROPPED no webView")
773
+ return
774
+ }
627
775
  let jsStringLiteral = self.jsStringLiteral(from: message)
628
776
  let js = "window.postMessage(JSON.parse(\(jsStringLiteral)), '*')"
629
777
  webView.evaluateJavaScript(js, completionHandler: nil)
@@ -677,7 +825,9 @@ extension ChartWebViewProxy: WKScriptMessageHandler {
677
825
  didReceive message: WKScriptMessage
678
826
  ) {
679
827
  guard message.name == ChartWebviewConst.messageHandlerName else { return }
680
- let owner = pooled?.owner
828
+ // Q1 FIX: route page->native messages ($private data requests) to the warm
829
+ // driver when there's no visible owner, so they aren't dropped during warm.
830
+ let owner = pooled?.owner ?? pooled?.warmDriver
681
831
  if let body = message.body as? String {
682
832
  owner?.handleMessage(body)
683
833
  } else if let data = try? JSONSerialization.data(withJSONObject: message.body, options: []),
@@ -693,16 +843,22 @@ extension ChartWebViewProxy: WKScriptMessageHandler {
693
843
 
694
844
  extension ChartWebViewProxy: WKNavigationDelegate {
695
845
  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
696
- pooled?.owner?.handleLoadEnd()
846
+ // Q1 FIX: fall back to warmDriver when there's no visible owner yet, so the
847
+ // page's load-complete callback (-> JS onLoadEnd -> SYMBOL_CHANGE) fires now.
848
+ let target = pooled?.owner ?? pooled?.warmDriver
849
+ ChartWVLog.i("nav.didFinish url=\(ChartWVLog.clip(webView.url?.absoluteString)) hasOwner=\(pooled?.owner != nil) viaWarmDriver=\(pooled?.owner == nil && pooled?.warmDriver != nil)")
850
+ target?.handleLoadEnd()
697
851
  // Prime the snapshot so the first move already has a frame to mask with.
698
852
  pooled?.refreshSnapshotSoon()
699
853
  }
700
854
 
701
855
  func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
856
+ ChartWVLog.e("nav.didFail url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)")
702
857
  pooled?.owner?.handleError(error.localizedDescription)
703
858
  }
704
859
 
705
860
  func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
861
+ ChartWVLog.e("nav.didFailProvisional url=\(ChartWVLog.clip(webView.url?.absoluteString)) error=\(error.localizedDescription)")
706
862
  pooled?.owner?.handleError(error.localizedDescription)
707
863
  }
708
864
  }
@@ -723,6 +879,7 @@ extension ChartWebViewProxy: WKURLSchemeHandler {
723
879
  }
724
880
 
725
881
  guard let localBundle = pooled?.currentLocalBundle, !localBundle.isEmpty else {
882
+ ChartWVLog.e("scheme.NO_BUNDLE url=\(ChartWVLog.clip(url.absoluteString)) -> 404 (currentLocalBundle empty)")
726
883
  respondNotFound(url: url, task: urlSchemeTask)
727
884
  return
728
885
  }
@@ -733,9 +890,11 @@ extension ChartWebViewProxy: WKURLSchemeHandler {
733
890
 
734
891
  guard let fileURL = resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath),
735
892
  let data = try? Data(contentsOf: fileURL) else {
893
+ ChartWVLog.e("scheme.404 bundle=\(localBundle) path=\(relativePath) (resolved=\(resolveBundleFileURL(localBundle: localBundle, relativePath: relativePath)?.path ?? "nil")) -> Not Found")
736
894
  respondNotFound(url: url, task: urlSchemeTask)
737
895
  return
738
896
  }
897
+ ChartWVLog.i("scheme.serve bundle=\(localBundle) path=\(relativePath) bytes=\(data.count)")
739
898
 
740
899
  let headers: [String: String] = [
741
900
  "Content-Type": mimeTypeForPath(fileURL.pathExtension),
@@ -81,6 +81,15 @@ namespace margelo::nitro::chartwebview {
81
81
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* paramsJson */)>("setParamsJson");
82
82
  method(_javaPart, paramsJson.has_value() ? jni::make_jstring(paramsJson.value()) : nullptr);
83
83
  }
84
+ std::optional<std::string> JHybridChartWebviewSpec::getAssetHost() {
85
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>()>("getAssetHost");
86
+ auto __result = method(_javaPart);
87
+ return __result != nullptr ? std::make_optional(__result->toStdString()) : std::nullopt;
88
+ }
89
+ void JHybridChartWebviewSpec::setAssetHost(const std::optional<std::string>& assetHost) {
90
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* assetHost */)>("setAssetHost");
91
+ method(_javaPart, assetHost.has_value() ? jni::make_jstring(assetHost.value()) : nullptr);
92
+ }
84
93
  std::optional<std::string> JHybridChartWebviewSpec::getBridgeScript() {
85
94
  static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>()>("getBridgeScript");
86
95
  auto __result = method(_javaPart);
@@ -117,6 +126,15 @@ namespace margelo::nitro::chartwebview {
117
126
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* active */)>("setActive");
118
127
  method(_javaPart, active.has_value() ? jni::JBoolean::valueOf(active.value()) : nullptr);
119
128
  }
129
+ std::optional<bool> JHybridChartWebviewSpec::getWebviewDebuggingEnabled() {
130
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JBoolean>()>("getWebviewDebuggingEnabled");
131
+ auto __result = method(_javaPart);
132
+ return __result != nullptr ? std::make_optional(static_cast<bool>(__result->value())) : std::nullopt;
133
+ }
134
+ void JHybridChartWebviewSpec::setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) {
135
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* webviewDebuggingEnabled */)>("setWebviewDebuggingEnabled");
136
+ method(_javaPart, webviewDebuggingEnabled.has_value() ? jni::JBoolean::valueOf(webviewDebuggingEnabled.value()) : nullptr);
137
+ }
120
138
  std::optional<std::function<void(const std::string& /* message */)>> JHybridChartWebviewSpec::getOnMessage() {
121
139
  static const auto method = javaClassStatic()->getMethod<jni::local_ref<JFunc_void_std__string::javaobject>()>("getOnMessage_cxx");
122
140
  auto __result = method(_javaPart);
@@ -58,6 +58,8 @@ namespace margelo::nitro::chartwebview {
58
58
  void setEntry(const std::optional<std::string>& entry) override;
59
59
  std::optional<std::string> getParamsJson() override;
60
60
  void setParamsJson(const std::optional<std::string>& paramsJson) override;
61
+ std::optional<std::string> getAssetHost() override;
62
+ void setAssetHost(const std::optional<std::string>& assetHost) override;
61
63
  std::optional<std::string> getBridgeScript() override;
62
64
  void setBridgeScript(const std::optional<std::string>& bridgeScript) override;
63
65
  std::optional<std::string> getReuseKey() override;
@@ -66,6 +68,8 @@ namespace margelo::nitro::chartwebview {
66
68
  void setPooled(std::optional<bool> pooled) override;
67
69
  std::optional<bool> getActive() override;
68
70
  void setActive(std::optional<bool> active) override;
71
+ std::optional<bool> getWebviewDebuggingEnabled() override;
72
+ void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) override;
69
73
  std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() override;
70
74
  void setOnMessage(const std::optional<std::function<void(const std::string& /* message */)>>& onMessage) override;
71
75
  std::optional<std::function<void()>> getOnLoadEnd() override;
@@ -52,6 +52,10 @@ void JHybridChartWebviewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass
52
52
  view->setParamsJson(props.paramsJson.value);
53
53
  // TODO: Set isDirty = false
54
54
  }
55
+ if (props.assetHost.isDirty) {
56
+ view->setAssetHost(props.assetHost.value);
57
+ // TODO: Set isDirty = false
58
+ }
55
59
  if (props.bridgeScript.isDirty) {
56
60
  view->setBridgeScript(props.bridgeScript.value);
57
61
  // TODO: Set isDirty = false
@@ -68,6 +72,10 @@ void JHybridChartWebviewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass
68
72
  view->setActive(props.active.value);
69
73
  // TODO: Set isDirty = false
70
74
  }
75
+ if (props.webviewDebuggingEnabled.isDirty) {
76
+ view->setWebviewDebuggingEnabled(props.webviewDebuggingEnabled.value);
77
+ // TODO: Set isDirty = false
78
+ }
71
79
  if (props.onMessage.isDirty) {
72
80
  view->setOnMessage(props.onMessage.value);
73
81
  // TODO: Set isDirty = false
@@ -66,6 +66,12 @@ abstract class HybridChartWebviewSpec: HybridView() {
66
66
  @set:Keep
67
67
  abstract var paramsJson: String?
68
68
 
69
+ @get:DoNotStrip
70
+ @get:Keep
71
+ @set:DoNotStrip
72
+ @set:Keep
73
+ abstract var assetHost: String?
74
+
69
75
  @get:DoNotStrip
70
76
  @get:Keep
71
77
  @set:DoNotStrip
@@ -90,6 +96,12 @@ abstract class HybridChartWebviewSpec: HybridView() {
90
96
  @set:Keep
91
97
  abstract var active: Boolean?
92
98
 
99
+ @get:DoNotStrip
100
+ @get:Keep
101
+ @set:DoNotStrip
102
+ @set:Keep
103
+ abstract var webviewDebuggingEnabled: Boolean?
104
+
93
105
  abstract var onMessage: ((message: String) -> Unit)?
94
106
 
95
107
  private var onMessage_cxx: Func_void_std__string?
@@ -86,6 +86,13 @@ namespace margelo::nitro::chartwebview {
86
86
  inline void setParamsJson(const std::optional<std::string>& paramsJson) noexcept override {
87
87
  _swiftPart.setParamsJson(paramsJson);
88
88
  }
89
+ inline std::optional<std::string> getAssetHost() noexcept override {
90
+ auto __result = _swiftPart.getAssetHost();
91
+ return __result;
92
+ }
93
+ inline void setAssetHost(const std::optional<std::string>& assetHost) noexcept override {
94
+ _swiftPart.setAssetHost(assetHost);
95
+ }
89
96
  inline std::optional<std::string> getBridgeScript() noexcept override {
90
97
  auto __result = _swiftPart.getBridgeScript();
91
98
  return __result;
@@ -114,6 +121,13 @@ namespace margelo::nitro::chartwebview {
114
121
  inline void setActive(std::optional<bool> active) noexcept override {
115
122
  _swiftPart.setActive(active);
116
123
  }
124
+ inline std::optional<bool> getWebviewDebuggingEnabled() noexcept override {
125
+ auto __result = _swiftPart.getWebviewDebuggingEnabled();
126
+ return __result;
127
+ }
128
+ inline void setWebviewDebuggingEnabled(std::optional<bool> webviewDebuggingEnabled) noexcept override {
129
+ _swiftPart.setWebviewDebuggingEnabled(webviewDebuggingEnabled);
130
+ }
117
131
  inline std::optional<std::function<void(const std::string& /* message */)>> getOnMessage() noexcept override {
118
132
  auto __result = _swiftPart.getOnMessage();
119
133
  return __result;
@@ -91,6 +91,11 @@ using namespace margelo::nitro::chartwebview::views;
91
91
  swiftPart.setParamsJson(newViewProps.paramsJson.value);
92
92
  newViewProps.paramsJson.isDirty = false;
93
93
  }
94
+ // assetHost: optional
95
+ if (newViewProps.assetHost.isDirty) {
96
+ swiftPart.setAssetHost(newViewProps.assetHost.value);
97
+ newViewProps.assetHost.isDirty = false;
98
+ }
94
99
  // bridgeScript: optional
95
100
  if (newViewProps.bridgeScript.isDirty) {
96
101
  swiftPart.setBridgeScript(newViewProps.bridgeScript.value);
@@ -111,6 +116,11 @@ using namespace margelo::nitro::chartwebview::views;
111
116
  swiftPart.setActive(newViewProps.active.value);
112
117
  newViewProps.active.isDirty = false;
113
118
  }
119
+ // webviewDebuggingEnabled: optional
120
+ if (newViewProps.webviewDebuggingEnabled.isDirty) {
121
+ swiftPart.setWebviewDebuggingEnabled(newViewProps.webviewDebuggingEnabled.value);
122
+ newViewProps.webviewDebuggingEnabled.isDirty = false;
123
+ }
114
124
  // onMessage: optional
115
125
  if (newViewProps.onMessage.isDirty) {
116
126
  swiftPart.setOnMessage(newViewProps.onMessage.value);
@@ -15,10 +15,12 @@ public protocol HybridChartWebviewSpec_protocol: HybridObject, HybridView {
15
15
  var localBundle: String? { get set }
16
16
  var entry: String? { get set }
17
17
  var paramsJson: String? { get set }
18
+ var assetHost: String? { get set }
18
19
  var bridgeScript: String? { get set }
19
20
  var reuseKey: String? { get set }
20
21
  var pooled: Bool? { get set }
21
22
  var active: Bool? { get set }
23
+ var webviewDebuggingEnabled: Bool? { get set }
22
24
  var onMessage: ((_ message: String) -> Void)? { get set }
23
25
  var onLoadEnd: (() -> Void)? { get set }
24
26
  var onError: ((_ message: String) -> Void)? { get set }
@@ -210,6 +210,30 @@ open class HybridChartWebviewSpec_cxx {
210
210
  }
211
211
  }
212
212
 
213
+ public final var assetHost: bridge.std__optional_std__string_ {
214
+ @inline(__always)
215
+ get {
216
+ return { () -> bridge.std__optional_std__string_ in
217
+ if let __unwrappedValue = self.__implementation.assetHost {
218
+ return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
219
+ } else {
220
+ return .init()
221
+ }
222
+ }()
223
+ }
224
+ @inline(__always)
225
+ set {
226
+ self.__implementation.assetHost = { () -> String? in
227
+ if bridge.has_value_std__optional_std__string_(newValue) {
228
+ let __unwrapped = bridge.get_std__optional_std__string_(newValue)
229
+ return String(__unwrapped)
230
+ } else {
231
+ return nil
232
+ }
233
+ }()
234
+ }
235
+ }
236
+
213
237
  public final var bridgeScript: bridge.std__optional_std__string_ {
214
238
  @inline(__always)
215
239
  get {
@@ -306,6 +330,30 @@ open class HybridChartWebviewSpec_cxx {
306
330
  }
307
331
  }
308
332
 
333
+ public final var webviewDebuggingEnabled: bridge.std__optional_bool_ {
334
+ @inline(__always)
335
+ get {
336
+ return { () -> bridge.std__optional_bool_ in
337
+ if let __unwrappedValue = self.__implementation.webviewDebuggingEnabled {
338
+ return bridge.create_std__optional_bool_(__unwrappedValue)
339
+ } else {
340
+ return .init()
341
+ }
342
+ }()
343
+ }
344
+ @inline(__always)
345
+ set {
346
+ self.__implementation.webviewDebuggingEnabled = { () -> Bool? in
347
+ if bridge.has_value_std__optional_bool_(newValue) {
348
+ let __unwrapped = bridge.get_std__optional_bool_(newValue)
349
+ return __unwrapped
350
+ } else {
351
+ return nil
352
+ }
353
+ }()
354
+ }
355
+ }
356
+
309
357
  public final var onMessage: bridge.std__optional_std__function_void_const_std__string_____message______ {
310
358
  @inline(__always)
311
359
  get {
@@ -22,6 +22,8 @@ namespace margelo::nitro::chartwebview {
22
22
  prototype.registerHybridSetter("entry", &HybridChartWebviewSpec::setEntry);
23
23
  prototype.registerHybridGetter("paramsJson", &HybridChartWebviewSpec::getParamsJson);
24
24
  prototype.registerHybridSetter("paramsJson", &HybridChartWebviewSpec::setParamsJson);
25
+ prototype.registerHybridGetter("assetHost", &HybridChartWebviewSpec::getAssetHost);
26
+ prototype.registerHybridSetter("assetHost", &HybridChartWebviewSpec::setAssetHost);
25
27
  prototype.registerHybridGetter("bridgeScript", &HybridChartWebviewSpec::getBridgeScript);
26
28
  prototype.registerHybridSetter("bridgeScript", &HybridChartWebviewSpec::setBridgeScript);
27
29
  prototype.registerHybridGetter("reuseKey", &HybridChartWebviewSpec::getReuseKey);
@@ -30,6 +32,8 @@ namespace margelo::nitro::chartwebview {
30
32
  prototype.registerHybridSetter("pooled", &HybridChartWebviewSpec::setPooled);
31
33
  prototype.registerHybridGetter("active", &HybridChartWebviewSpec::getActive);
32
34
  prototype.registerHybridSetter("active", &HybridChartWebviewSpec::setActive);
35
+ prototype.registerHybridGetter("webviewDebuggingEnabled", &HybridChartWebviewSpec::getWebviewDebuggingEnabled);
36
+ prototype.registerHybridSetter("webviewDebuggingEnabled", &HybridChartWebviewSpec::setWebviewDebuggingEnabled);
33
37
  prototype.registerHybridGetter("onMessage", &HybridChartWebviewSpec::getOnMessage);
34
38
  prototype.registerHybridSetter("onMessage", &HybridChartWebviewSpec::setOnMessage);
35
39
  prototype.registerHybridGetter("onLoadEnd", &HybridChartWebviewSpec::getOnLoadEnd);