@onekeyfe/react-native-chart-webview 3.0.53 → 3.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChartWebview.podspec +1 -1
- package/README.md +15 -2
- package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +54 -3
- package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebviewPackage.kt +24 -1
- package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +166 -20
- package/ios/ChartWebview.swift +117 -4
- package/lib/module/index.js +50 -5
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +91 -4
package/ChartWebview.podspec
CHANGED
|
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
-
s.source = { :git => "https://github.com/OneKeyHQ/
|
|
14
|
+
s.source = { :git => "https://github.com/OneKeyHQ/native-views/react-native-chart-webview.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
16
|
s.source_files = [
|
|
17
17
|
"ios/**/*.{swift}",
|
package/README.md
CHANGED
|
@@ -10,12 +10,25 @@ yarn add @onekeyfe/react-native-chart-webview
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
```
|
|
13
|
+
```tsx
|
|
14
14
|
import { ChartWebviewView } from '@onekeyfe/react-native-chart-webview';
|
|
15
15
|
|
|
16
16
|
// ...
|
|
17
17
|
|
|
18
|
-
<ChartWebviewView
|
|
18
|
+
<ChartWebviewView
|
|
19
|
+
style={{ flex: 1 }}
|
|
20
|
+
uri="https://tradingview.onekey.so/?theme=dark&symbol=BTC"
|
|
21
|
+
reuseKey="market"
|
|
22
|
+
pooled={true}
|
|
23
|
+
active={true}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<ChartWebviewView
|
|
27
|
+
style={{ flex: 1 }}
|
|
28
|
+
localBundle="tradingview-assets"
|
|
29
|
+
entry="index.html"
|
|
30
|
+
paramsJson={JSON.stringify({ theme: 'dark', symbol: 'BTC' })}
|
|
31
|
+
/>
|
|
19
32
|
```
|
|
20
33
|
|
|
21
34
|
## Contributing
|
|
@@ -36,7 +36,16 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
36
36
|
// render-ready signal. Shared so render-ready (which arrives on whichever host
|
|
37
37
|
// owns the WebView at that instant — not necessarily the awaiting one) reveals
|
|
38
38
|
// the correct host regardless of the owner-handoff timing.
|
|
39
|
-
|
|
39
|
+
//
|
|
40
|
+
// WeakReference (iOS uses a `weak` static for the same reason): a strong static
|
|
41
|
+
// would leak the host — and its ReactContext/Activity — if the host unmounts
|
|
42
|
+
// mid-reveal. dispose() also clears it eagerly on detach.
|
|
43
|
+
private var pendingRevealHostRef: java.lang.ref.WeakReference<HybridChartWebview>? = null
|
|
44
|
+
private var pendingRevealHost: HybridChartWebview?
|
|
45
|
+
get() = pendingRevealHostRef?.get()
|
|
46
|
+
set(value) {
|
|
47
|
+
pendingRevealHostRef = value?.let { java.lang.ref.WeakReference(it) }
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
private val instanceId = instanceIds.incrementAndGet()
|
|
@@ -45,7 +54,11 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
45
54
|
// we add at runtime (the WebView / placeholder). Without forcing a measure +
|
|
46
55
|
// layout pass here, those children stay 0x0 and the slot renders blank. This
|
|
47
56
|
// requestLayout override is the standard RN fix for custom ViewGroups.
|
|
48
|
-
private inner class ChartContainer(ctx: android.content.Context) : FrameLayout(ctx) {
|
|
57
|
+
private inner class ChartContainer(ctx: android.content.Context) : FrameLayout(ctx), HostAware {
|
|
58
|
+
// Lets the view manager reach this host from the dropped View (onDropViewInstance)
|
|
59
|
+
// to run teardown — see TeardownChartWebviewManager.
|
|
60
|
+
override val chartHost: HybridChartWebview get() = this@HybridChartWebview
|
|
61
|
+
|
|
49
62
|
private val relayout = Runnable {
|
|
50
63
|
measure(
|
|
51
64
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
@@ -79,6 +92,9 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
79
92
|
/** The WebView backing this host (a shared pool entry, or a private one). */
|
|
80
93
|
private var backing: PooledChartWebView? = null
|
|
81
94
|
private var attached = false
|
|
95
|
+
// The pooled key this host has refcounted via ChartWebviewPool.adopt (null when
|
|
96
|
+
// not pooled). Makes adopt/release idempotent per host across many reconciles.
|
|
97
|
+
private var adoptedPoolKey: String? = null
|
|
82
98
|
|
|
83
99
|
// --- Source props ---
|
|
84
100
|
|
|
@@ -200,8 +216,16 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
200
216
|
// displays the live WebView; inactive hosts display the cached frame snapshot
|
|
201
217
|
// so their slot isn't blank (and the live WebView reparents on hand-off).
|
|
202
218
|
private fun reconcilePooled() {
|
|
203
|
-
val
|
|
219
|
+
val key = effectiveKey()
|
|
220
|
+
val entry = ChartWebviewPool.acquireShared(key, context)
|
|
204
221
|
backing = entry
|
|
222
|
+
// Refcount the pool entry once per host (reconcile runs many times). Balanced
|
|
223
|
+
// by releaseShared in dispose(). If the reuseKey changed, release the old one.
|
|
224
|
+
if (adoptedPoolKey != key) {
|
|
225
|
+
adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) }
|
|
226
|
+
ChartWebviewPool.adopt(key)
|
|
227
|
+
adoptedPoolKey = key
|
|
228
|
+
}
|
|
205
229
|
if (wantsOwnership()) {
|
|
206
230
|
entry.owner = this
|
|
207
231
|
// Register the document-start bridge before the first load (the prop is set
|
|
@@ -328,4 +352,31 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
328
352
|
placeholder?.let { container.removeView(it) }
|
|
329
353
|
placeholder = null
|
|
330
354
|
}
|
|
355
|
+
|
|
356
|
+
// Host teardown. Called by the view manager's onDropViewInstance (the only
|
|
357
|
+
// reliable "host is gone" signal — onViewDetachedFromWindow fires on every
|
|
358
|
+
// navigation, not just final teardown). Tears down the NON-pooled (private)
|
|
359
|
+
// WebView so it doesn't leak a Chromium renderer + JavascriptInterface per
|
|
360
|
+
// mount/unmount. Pooled (shared) backing is intentionally left alive in the
|
|
361
|
+
// warm pool — other hosts may still share it; its lifetime is the pool's.
|
|
362
|
+
fun dispose() {
|
|
363
|
+
stopOwnCapture()
|
|
364
|
+
container.removeCallbacks(reconcileRunnable)
|
|
365
|
+
container.removeCallbacks(revealFallbackRunnable)
|
|
366
|
+
// Don't leak this host (and its ReactContext/Activity) via the static
|
|
367
|
+
// pendingRevealHost if we unmount mid-reveal.
|
|
368
|
+
if (pendingRevealHost == this) pendingRevealHost = null
|
|
369
|
+
val entry = backing
|
|
370
|
+
if (entry != null && entry.owner == this) entry.owner = null
|
|
371
|
+
// Balance the pool adopt() if this host ever joined a pooled key. Kept warm by
|
|
372
|
+
// default (single-instance cache); the release path makes destroy() reachable.
|
|
373
|
+
adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) }
|
|
374
|
+
adoptedPoolKey = null
|
|
375
|
+
// A private (non-pooled) instance is owned solely by this host — tear it down
|
|
376
|
+
// so it doesn't leak a Chromium renderer + JavascriptInterface.
|
|
377
|
+
if (entry != null && !isPooled()) entry.destroy()
|
|
378
|
+
backing = null
|
|
379
|
+
removePlaceholder()
|
|
380
|
+
ownSnapshot = null
|
|
381
|
+
}
|
|
331
382
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.chartwebview
|
|
2
2
|
|
|
3
|
+
import android.view.View
|
|
3
4
|
import com.facebook.react.BaseReactPackage
|
|
4
5
|
import com.facebook.react.bridge.NativeModule
|
|
5
6
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
@@ -8,6 +9,26 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
8
9
|
|
|
9
10
|
import com.margelo.nitro.chartwebview.views.HybridChartWebviewManager
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* View manager subclass that adds a teardown hook on top of the nitrogen-generated
|
|
14
|
+
* [HybridChartWebviewManager] (which is DO-NOT-MODIFY). The base manager removes
|
|
15
|
+
* the view from its private table on drop but never tears the WebView down, so the
|
|
16
|
+
* non-pooled (private) path would leak a Chromium renderer + JavascriptInterface
|
|
17
|
+
* per mount/unmount. On drop we reach the host via the container ([HostAware]) and
|
|
18
|
+
* call [HybridChartWebview.dispose].
|
|
19
|
+
*/
|
|
20
|
+
class TeardownChartWebviewManager : HybridChartWebviewManager() {
|
|
21
|
+
override fun onDropViewInstance(view: View) {
|
|
22
|
+
(view as? HostAware)?.chartHost?.dispose()
|
|
23
|
+
super.onDropViewInstance(view)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Implemented by the host's container view so the manager can reach the host on drop. */
|
|
28
|
+
interface HostAware {
|
|
29
|
+
val chartHost: HybridChartWebview
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
class ChartWebviewPackage : BaseReactPackage() {
|
|
12
33
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
13
34
|
return null
|
|
@@ -18,7 +39,9 @@ class ChartWebviewPackage : BaseReactPackage() {
|
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
21
|
-
|
|
42
|
+
// Use the teardown-aware subclass so the non-pooled WebView is destroyed on
|
|
43
|
+
// host drop (the base manager never tears it down).
|
|
44
|
+
return listOf(TeardownChartWebviewManager())
|
|
22
45
|
}
|
|
23
46
|
|
|
24
47
|
companion object {
|
|
@@ -18,6 +18,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
18
18
|
import android.webkit.WebResourceRequest
|
|
19
19
|
import android.webkit.WebResourceResponse
|
|
20
20
|
import android.webkit.WebView
|
|
21
|
+
import androidx.webkit.ScriptHandler
|
|
21
22
|
import androidx.webkit.WebResourceErrorCompat
|
|
22
23
|
import androidx.webkit.WebViewAssetLoader
|
|
23
24
|
import androidx.webkit.WebViewClientCompat
|
|
@@ -66,18 +67,83 @@ class PooledChartWebView private constructor(
|
|
|
66
67
|
// Shim (defines __chartNativePost) + the TS bridge that uses it. Set lazily via
|
|
67
68
|
// setBridgeScript once the host has the prop value — registering it in init
|
|
68
69
|
// would capture an empty script, since the first reconcile (window-attach) can
|
|
69
|
-
// run before the bridgeScript prop is applied.
|
|
70
|
+
// run before the bridgeScript prop is applied. The privileged bridge exposes
|
|
71
|
+
// window.$onekey.$private.request etc., so it must only ever be injected into
|
|
72
|
+
// the trusted chart origin(s) we load — NOT cross-origin subframes / arbitrary
|
|
73
|
+
// pages. The actual document-start registration is therefore deferred to
|
|
74
|
+
// registerBridgeForOrigins(), called once the load URL (hence origin) is known.
|
|
70
75
|
private var outboundBridgeJs: String = ""
|
|
71
|
-
private var bridgeRegistered = false
|
|
72
76
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
77
|
+
// Handler for the currently-registered document-start script, kept so it can be
|
|
78
|
+
// removed and re-registered if the bridge script or the trusted origin set
|
|
79
|
+
// changes (instead of silently latching the first one).
|
|
80
|
+
private var bridgeScriptHandler: ScriptHandler? = null
|
|
81
|
+
// The (script, origins) pair currently registered, so we can detect changes and
|
|
82
|
+
// avoid redundant re-registration.
|
|
83
|
+
private var registeredBridgeJs: String? = null
|
|
84
|
+
private var registeredOrigins: Set<String> = emptySet()
|
|
85
|
+
|
|
86
|
+
// True once we have a non-empty bridge script staged; gates loading (the page
|
|
87
|
+
// must not boot before the bridge is registered or its first requests are lost).
|
|
88
|
+
private val bridgeRegistered: Boolean
|
|
89
|
+
get() = outboundBridgeJs.isNotEmpty()
|
|
90
|
+
|
|
91
|
+
// Called by the host before the first load. Stages the document-start bridge
|
|
92
|
+
// (shim + the shared TS bridgeScript) once a non-empty script is available.
|
|
93
|
+
// The script is not injected into the page yet — that happens, scoped to the
|
|
94
|
+
// trusted origin(s), in registerBridgeForOrigins() when the load URL is known.
|
|
95
|
+
// If a second host sharing the reuseKey supplies a DIFFERENT script we update
|
|
96
|
+
// it (and re-register on next load) rather than silently dropping it; in the
|
|
97
|
+
// app's single-reuseKey + constant-bridge reality this branch never fires, but
|
|
98
|
+
// not latching keeps it correct if that ever changes.
|
|
75
99
|
fun setBridgeScript(bridgeScript: String) {
|
|
76
|
-
if (
|
|
100
|
+
if (bridgeScript.isEmpty()) return
|
|
77
101
|
outboundBridgeJs = "$NATIVE_POST_SHIM_JS\n$bridgeScript"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register (or re-register) the document-start bridge scoped to exactly the
|
|
105
|
+
// trusted origins we load. allowedOriginRules of setOf("*") would leak the
|
|
106
|
+
// privileged bridge into every cross-origin subframe; here we pass only the
|
|
107
|
+
// offline asset origin and (in online mode) the configured chart origin.
|
|
108
|
+
private fun registerBridgeForOrigins(origins: Set<String>) {
|
|
109
|
+
if (outboundBridgeJs.isEmpty() || origins.isEmpty()) return
|
|
110
|
+
if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) return
|
|
111
|
+
// No-op if nothing changed (same script + same origin set already registered).
|
|
112
|
+
if (bridgeScriptHandler != null &&
|
|
113
|
+
registeredBridgeJs == outboundBridgeJs &&
|
|
114
|
+
registeredOrigins == origins
|
|
115
|
+
) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
bridgeScriptHandler?.remove()
|
|
119
|
+
bridgeScriptHandler =
|
|
120
|
+
WebViewCompat.addDocumentStartJavaScript(webView, outboundBridgeJs, origins)
|
|
121
|
+
registeredBridgeJs = outboundBridgeJs
|
|
122
|
+
registeredOrigins = origins
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The trusted origin set for the current source: the offline asset origin
|
|
126
|
+
// (https://appassets.androidplatform.net) always, plus the online/fallback
|
|
127
|
+
// `uri` origin when running in online mode. computeTargetUrl serves offline
|
|
128
|
+
// content from ASSET_HOST and online content from `uri`, so these are exactly
|
|
129
|
+
// the origins the privileged bridge legitimately runs in.
|
|
130
|
+
private fun trustedOriginsFor(uri: String?): Set<String> {
|
|
131
|
+
val origins = linkedSetOf("https://$ASSET_HOST")
|
|
132
|
+
originOf(uri)?.let { origins.add(it) }
|
|
133
|
+
return origins
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// scheme://host[:port] of a URL, or null if it can't be parsed / isn't http(s).
|
|
137
|
+
private fun originOf(url: String?): String? {
|
|
138
|
+
if (url.isNullOrEmpty()) return null
|
|
139
|
+
return try {
|
|
140
|
+
val u = java.net.URI(url)
|
|
141
|
+
val scheme = u.scheme?.lowercase() ?: return null
|
|
142
|
+
if (scheme != "https" && scheme != "http") return null
|
|
143
|
+
val host = u.host ?: return null
|
|
144
|
+
if (u.port != -1) "$scheme://$host:${u.port}" else "$scheme://$host"
|
|
145
|
+
} catch (e: Exception) {
|
|
146
|
+
null
|
|
81
147
|
}
|
|
82
148
|
}
|
|
83
149
|
|
|
@@ -93,6 +159,10 @@ class PooledChartWebView private constructor(
|
|
|
93
159
|
// clearSnapshot() or the next capture replaces it.
|
|
94
160
|
private var cachedSnapshot: Bitmap? = null
|
|
95
161
|
private var overlay: ImageView? = null
|
|
162
|
+
// The bitmap currently shown by `overlay` (a snapshot reused directly, not a
|
|
163
|
+
// copy). Tracked so a concurrent capture doesn't recycle a bitmap still on
|
|
164
|
+
// screen — see capturePixelCopy.
|
|
165
|
+
private var overlaySnapshot: Bitmap? = null
|
|
96
166
|
|
|
97
167
|
// How long the snapshot overlay stays up after a reparent, giving the WebView
|
|
98
168
|
// time to draw its first frame in the new container (~5 frames).
|
|
@@ -119,14 +189,12 @@ class PooledChartWebView private constructor(
|
|
|
119
189
|
request: WebResourceRequest,
|
|
120
190
|
): WebResourceResponse? = assetLoader?.shouldInterceptRequest(request.url)
|
|
121
191
|
|
|
122
|
-
override fun onPageStarted(view: WebView, url: String?, favicon: android.graphics.Bitmap?) {
|
|
123
|
-
super.onPageStarted(view, url, favicon)
|
|
124
|
-
if (outboundBridgeJs.isNotEmpty()) view.evaluateJavascript(outboundBridgeJs, null)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
192
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
128
193
|
super.onPageFinished(view, url)
|
|
129
|
-
|
|
194
|
+
// The bridge is delivered exclusively via the origin-scoped document-start
|
|
195
|
+
// script (see registerBridgeForOrigins). We deliberately do NOT re-inject
|
|
196
|
+
// it here via evaluateJavascript — that ran on every page event regardless
|
|
197
|
+
// of URL and would expose the privileged bridge to untrusted pages/frames.
|
|
130
198
|
owner?.dispatchLoadEnd()
|
|
131
199
|
// Prime the snapshot so the first move already has a frame to mask with.
|
|
132
200
|
refreshSnapshotSoon()
|
|
@@ -207,6 +275,9 @@ class PooledChartWebView private constructor(
|
|
|
207
275
|
fun clearSnapshot() {
|
|
208
276
|
runOnUiThread {
|
|
209
277
|
removeOverlay()
|
|
278
|
+
// The pool owns cachedSnapshot outright (hosts get copies via captureNow),
|
|
279
|
+
// so recycle it here instead of leaving a full-size ARGB_8888 bitmap to GC.
|
|
280
|
+
cachedSnapshot?.recycle()
|
|
210
281
|
cachedSnapshot = null
|
|
211
282
|
}
|
|
212
283
|
}
|
|
@@ -220,9 +291,17 @@ class PooledChartWebView private constructor(
|
|
|
220
291
|
}
|
|
221
292
|
|
|
222
293
|
/// Capture now and deliver the fresh frame (e.g. so the host that is losing
|
|
223
|
-
/// ownership can update its placeholder to its very last frame).
|
|
294
|
+
/// ownership can update its placeholder to its very last frame). The host gets
|
|
295
|
+
/// an independent COPY: the pool reuses/recycles its own `cachedSnapshot` on
|
|
296
|
+
/// every internal refresh, so handing out the live buffer would let it recycle
|
|
297
|
+
/// a bitmap still shown in a host placeholder. The copy is the host's to keep.
|
|
224
298
|
fun captureNow(callback: (Bitmap?) -> Unit) {
|
|
225
|
-
capturePixelCopy
|
|
299
|
+
capturePixelCopy { bmp ->
|
|
300
|
+
val copy = bmp?.takeIf { !it.isRecycled }?.let {
|
|
301
|
+
it.copy(it.config ?: Bitmap.Config.ARGB_8888, false)
|
|
302
|
+
}
|
|
303
|
+
callback(copy)
|
|
304
|
+
}
|
|
226
305
|
}
|
|
227
306
|
|
|
228
307
|
// Capture the WebView's REAL on-screen pixels (incl. GPU-rendered chart
|
|
@@ -247,7 +326,20 @@ class PooledChartWebView private constructor(
|
|
|
247
326
|
src,
|
|
248
327
|
bmp,
|
|
249
328
|
{ result ->
|
|
250
|
-
if (result == PixelCopy.SUCCESS)
|
|
329
|
+
if (result == PixelCopy.SUCCESS) {
|
|
330
|
+
// Recycle the frame we're replacing to avoid piling up full-size
|
|
331
|
+
// ARGB_8888 bitmaps. Guard against recycling one still referenced by a
|
|
332
|
+
// live overlay/placeholder ImageView (showSnapshotOverlay reuses
|
|
333
|
+
// cachedSnapshot directly): only the unused old buffer is freed.
|
|
334
|
+
val old = cachedSnapshot
|
|
335
|
+
cachedSnapshot = bmp
|
|
336
|
+
if (old != null && old !== bmp && old !== overlaySnapshot && !old.isRecycled) {
|
|
337
|
+
old.recycle()
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Capture failed: the freshly allocated buffer is unused — free it.
|
|
341
|
+
if (!bmp.isRecycled) bmp.recycle()
|
|
342
|
+
}
|
|
251
343
|
callback?.invoke(cachedSnapshot)
|
|
252
344
|
},
|
|
253
345
|
webView.handler ?: Handler(Looper.getMainLooper()),
|
|
@@ -279,12 +371,17 @@ class PooledChartWebView private constructor(
|
|
|
279
371
|
}
|
|
280
372
|
container.addView(iv) // added last => drawn on top of the WebView
|
|
281
373
|
overlay = iv
|
|
374
|
+
overlaySnapshot = snap
|
|
282
375
|
iv.postDelayed({ removeOverlay() }, overlayHideDelayMs)
|
|
283
376
|
}
|
|
284
377
|
|
|
285
378
|
private fun removeOverlay() {
|
|
286
|
-
overlay?.let {
|
|
379
|
+
overlay?.let {
|
|
380
|
+
(it.parent as? ViewGroup)?.removeView(it)
|
|
381
|
+
it.setImageDrawable(null)
|
|
382
|
+
}
|
|
287
383
|
overlay = null
|
|
384
|
+
overlaySnapshot = null
|
|
288
385
|
}
|
|
289
386
|
|
|
290
387
|
/**
|
|
@@ -301,6 +398,11 @@ class PooledChartWebView private constructor(
|
|
|
301
398
|
lastLocalBundle = localBundle
|
|
302
399
|
rebuildAssetLoader(localBundle)
|
|
303
400
|
}
|
|
401
|
+
// Register the document-start bridge scoped to exactly the origin(s) this load
|
|
402
|
+
// uses (offline asset origin, plus the online `uri` origin when present) before
|
|
403
|
+
// navigating, so the page boots with the bridge but cross-origin subframes /
|
|
404
|
+
// untrusted pages don't receive it.
|
|
405
|
+
registerBridgeForOrigins(trustedOriginsFor(uri))
|
|
304
406
|
val target = computeTargetUrl(uri, localBundle, entry, paramsJson) ?: return
|
|
305
407
|
if (target == lastLoadedUrl) return
|
|
306
408
|
lastLoadedUrl = target
|
|
@@ -319,9 +421,19 @@ class PooledChartWebView private constructor(
|
|
|
319
421
|
runOnUiThread { webView.reload() }
|
|
320
422
|
}
|
|
321
423
|
|
|
322
|
-
/**
|
|
424
|
+
/**
|
|
425
|
+
* Permanently free the WebView and its bridge/snapshot resources. Used for the
|
|
426
|
+
* non-pooled (private) path on host teardown, and reachable for the pool (see
|
|
427
|
+
* ChartWebviewPool.release) so an evicted entry can be torn down rather than
|
|
428
|
+
* leaking a Chromium renderer + JavascriptInterface.
|
|
429
|
+
*/
|
|
323
430
|
fun destroy() {
|
|
324
431
|
runOnUiThread {
|
|
432
|
+
bridgeScriptHandler?.remove()
|
|
433
|
+
bridgeScriptHandler = null
|
|
434
|
+
removeOverlay()
|
|
435
|
+
cachedSnapshot?.recycle()
|
|
436
|
+
cachedSnapshot = null
|
|
325
437
|
(webView.parent as? ViewGroup)?.removeView(webView)
|
|
326
438
|
webView.destroy()
|
|
327
439
|
val n = liveCount.decrementAndGet()
|
|
@@ -405,8 +517,42 @@ class PooledChartWebView private constructor(
|
|
|
405
517
|
*/
|
|
406
518
|
object ChartWebviewPool {
|
|
407
519
|
private val shared = HashMap<String, PooledChartWebView>()
|
|
408
|
-
|
|
520
|
+
// Refcount per key: incremented when a host adopts the entry, decremented when a
|
|
521
|
+
// host releases it. Lets us know when an entry has no live hosts. With the app's
|
|
522
|
+
// single constant reuseKey this stays >= 0 and the entry is kept warm; the count
|
|
523
|
+
// exists so destroy() is *reachable* (see releaseShared) rather than dead code,
|
|
524
|
+
// and so a future multi-key/LRU policy can evict safely.
|
|
525
|
+
private val refCounts = HashMap<String, Int>()
|
|
526
|
+
|
|
527
|
+
/** Get (creating if absent) the entry for [key]. Does not change the refcount —
|
|
528
|
+
* call [adopt] exactly once per host to register a live reference. */
|
|
409
529
|
@Synchronized
|
|
410
530
|
fun acquireShared(key: String, context: Context): PooledChartWebView =
|
|
411
531
|
shared.getOrPut(key) { PooledChartWebView.create(context, key) }
|
|
532
|
+
|
|
533
|
+
/** Register one live host reference to [key] (call once per host; idempotency is
|
|
534
|
+
* the caller's responsibility via adoptedPoolKey). */
|
|
535
|
+
@Synchronized
|
|
536
|
+
fun adopt(key: String) {
|
|
537
|
+
refCounts[key] = (refCounts[key] ?: 0) + 1
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* A host that previously adopted [key] is going away. Decrements the refcount.
|
|
542
|
+
* By default the entry is kept warm even at zero (intentional single-instance
|
|
543
|
+
* cache — see class doc); pass [destroyWhenIdle] = true to actually tear the
|
|
544
|
+
* WebView down when the last host leaves (used for an LRU/eviction policy).
|
|
545
|
+
*/
|
|
546
|
+
@Synchronized
|
|
547
|
+
fun releaseShared(key: String, destroyWhenIdle: Boolean = false) {
|
|
548
|
+
val next = (refCounts[key] ?: 0) - 1
|
|
549
|
+
if (next <= 0) {
|
|
550
|
+
refCounts.remove(key)
|
|
551
|
+
if (destroyWhenIdle) {
|
|
552
|
+
shared.remove(key)?.destroy()
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
refCounts[key] = next
|
|
556
|
+
}
|
|
557
|
+
}
|
|
412
558
|
}
|
package/ios/ChartWebview.swift
CHANGED
|
@@ -63,6 +63,9 @@ class HybridChartWebview: HybridChartWebviewSpec {
|
|
|
63
63
|
/// The WebView backing this host (a shared pool entry, or a private one).
|
|
64
64
|
private var backing: PooledChartWebView?
|
|
65
65
|
private var attached = false
|
|
66
|
+
// The pooled key this host has refcounted via the pool (nil when not pooled).
|
|
67
|
+
// Makes adopt/release idempotent per host across many reconciles.
|
|
68
|
+
private var adoptedPoolKey: String?
|
|
66
69
|
|
|
67
70
|
override init() {
|
|
68
71
|
super.init()
|
|
@@ -72,6 +75,26 @@ class HybridChartWebview: HybridChartWebviewSpec {
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
// Host teardown. ARC deallocates the host when the view manager drops it; this is
|
|
79
|
+
// the reliable "host is gone" signal. Tears down the NON-pooled (private) WebView
|
|
80
|
+
// so it doesn't leak, balances the pool refcount for the pooled path, and clears
|
|
81
|
+
// the static pendingRevealHost if we go away mid-reveal.
|
|
82
|
+
deinit {
|
|
83
|
+
revealFallbackWork?.cancel()
|
|
84
|
+
if HybridChartWebview.pendingRevealHost === self {
|
|
85
|
+
HybridChartWebview.pendingRevealHost = nil
|
|
86
|
+
}
|
|
87
|
+
let entry = backing
|
|
88
|
+
if let key = adoptedPoolKey {
|
|
89
|
+
// Pooled: balance the adopt(). Kept warm by default; release makes the
|
|
90
|
+
// pool's destroy() reachable.
|
|
91
|
+
ChartWebviewPool.shared.releaseShared(key: key)
|
|
92
|
+
} else {
|
|
93
|
+
// Private: this host solely owns the WebView — tear it down.
|
|
94
|
+
entry?.destroy()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
// MARK: - Props (source)
|
|
76
99
|
|
|
77
100
|
var uri: String? { didSet { applySourceIfOwner() } }
|
|
@@ -168,8 +191,16 @@ class HybridChartWebview: HybridChartWebviewSpec {
|
|
|
168
191
|
// displays the live WebView; inactive hosts display the cached frame snapshot
|
|
169
192
|
// so their slot isn't blank (and the live WebView reparents on hand-off).
|
|
170
193
|
private func reconcilePooled() {
|
|
171
|
-
let
|
|
194
|
+
let key = effectiveKey()
|
|
195
|
+
let pooled = ChartWebviewPool.shared.acquireShared(key: key)
|
|
172
196
|
backing = pooled
|
|
197
|
+
// Refcount the entry once per host (reconcile runs many times). Balanced by
|
|
198
|
+
// releaseShared in deinit. If the reuseKey changed, release the old one first.
|
|
199
|
+
if adoptedPoolKey != key {
|
|
200
|
+
if let old = adoptedPoolKey { ChartWebviewPool.shared.releaseShared(key: old) }
|
|
201
|
+
ChartWebviewPool.shared.adopt(key: key)
|
|
202
|
+
adoptedPoolKey = key
|
|
203
|
+
}
|
|
173
204
|
if wantsOwnership() {
|
|
174
205
|
pooled.owner = self
|
|
175
206
|
// Register the document-start bridge before the first load (the prop is set
|
|
@@ -326,6 +357,9 @@ final class PooledChartWebView {
|
|
|
326
357
|
// added lazily (see setBridgeScript) once the host has the prop value.
|
|
327
358
|
private var userContent: WKUserContentController?
|
|
328
359
|
private var bridgeRegistered = false
|
|
360
|
+
// The bridge script currently registered, so a differing script from a second
|
|
361
|
+
// host sharing a reuseKey is re-registered rather than silently dropped (fix #4).
|
|
362
|
+
private var registeredBridgeScript: String?
|
|
329
363
|
|
|
330
364
|
private var webView: WKWebView!
|
|
331
365
|
private var proxy: ChartWebViewProxy!
|
|
@@ -354,15 +388,27 @@ final class PooledChartWebView {
|
|
|
354
388
|
// subsequent calls are no-ops. Done lazily because the first reconcile
|
|
355
389
|
// (window-attach) can run before the bridgeScript prop is applied.
|
|
356
390
|
func setBridgeScript(_ bridgeScript: String) {
|
|
357
|
-
guard !
|
|
391
|
+
guard !bridgeScript.isEmpty, let userContent = userContent else { return }
|
|
392
|
+
// Re-register if a second host sharing the reuseKey supplies a DIFFERENT script
|
|
393
|
+
// instead of silently dropping it (fix #4). In the app's single-reuseKey +
|
|
394
|
+
// constant-bridge reality this never fires; not latching keeps it correct.
|
|
395
|
+
if bridgeRegistered, registeredBridgeScript == bridgeScript { return }
|
|
396
|
+
if bridgeRegistered {
|
|
397
|
+
// WKUserScripts can't be removed individually; drop all and re-add ours.
|
|
398
|
+
userContent.removeAllUserScripts()
|
|
399
|
+
}
|
|
358
400
|
bridgeRegistered = true
|
|
401
|
+
registeredBridgeScript = bridgeScript
|
|
359
402
|
let handlerName = ChartWebviewConst.messageHandlerName
|
|
360
403
|
let shim = "(function(){window.__chartNativePost=function(s){"
|
|
361
404
|
+ "window.webkit.messageHandlers.\(handlerName).postMessage(s);};})();"
|
|
362
405
|
let userScript = WKUserScript(
|
|
363
406
|
source: shim + "\n" + bridgeScript,
|
|
364
407
|
injectionTime: .atDocumentStart,
|
|
365
|
-
|
|
408
|
+
// Main frame only: the privileged bridge (window.$onekey.$private.request etc.)
|
|
409
|
+
// must not be exposed to cross-origin iframes. The chart page runs in the main
|
|
410
|
+
// frame, so it still gets the full bridge (fix #1).
|
|
411
|
+
forMainFrameOnly: true
|
|
366
412
|
)
|
|
367
413
|
userContent.addUserScript(userScript)
|
|
368
414
|
}
|
|
@@ -430,6 +476,32 @@ final class PooledChartWebView {
|
|
|
430
476
|
}
|
|
431
477
|
}
|
|
432
478
|
|
|
479
|
+
/// Permanently tear down the WebView and its bridge. Used for the non-pooled
|
|
480
|
+
/// (private) path on host teardown, and reachable for the pool (see
|
|
481
|
+
/// ChartWebviewPool.releaseShared). Stops loading, detaches the view, and
|
|
482
|
+
/// removes the script message handler so the proxy retain is released.
|
|
483
|
+
func destroy() {
|
|
484
|
+
runOnMain { [weak self] in
|
|
485
|
+
guard let self = self else { return }
|
|
486
|
+
self.removeOverlay()
|
|
487
|
+
self.cachedSnapshot = nil
|
|
488
|
+
if let webView = self.webView {
|
|
489
|
+
webView.stopLoading()
|
|
490
|
+
webView.navigationDelegate = nil
|
|
491
|
+
webView.uiDelegate = nil
|
|
492
|
+
webView.removeFromSuperview()
|
|
493
|
+
}
|
|
494
|
+
// Drop the page -> native handler so the user-content controller stops
|
|
495
|
+
// retaining the proxy (and re-entrancy can't deliver to a torn-down owner).
|
|
496
|
+
self.userContent?.removeScriptMessageHandler(forName: ChartWebviewConst.messageHandlerName)
|
|
497
|
+
self.userContent?.removeAllUserScripts()
|
|
498
|
+
self.userContent = nil
|
|
499
|
+
self.webView = nil
|
|
500
|
+
PooledChartWebView.liveCount -= 1
|
|
501
|
+
NSLog("[ChartWebviewPool] WebView DESTROYED key=\(self.key) liveCount=\(PooledChartWebView.liveCount)")
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
433
505
|
/// Drop the cached snapshot and remove any visible overlay.
|
|
434
506
|
func clearSnapshot() {
|
|
435
507
|
runOnMain { [weak self] in
|
|
@@ -741,11 +813,52 @@ extension ChartWebViewProxy: WKURLSchemeHandler {
|
|
|
741
813
|
final class ChartWebviewPool {
|
|
742
814
|
static let shared = ChartWebviewPool()
|
|
743
815
|
private var entries: [String: PooledChartWebView] = [:]
|
|
744
|
-
|
|
816
|
+
// Refcount per key: incremented on adopt, decremented on release. Lets us know
|
|
817
|
+
// when an entry has no live hosts. With the app's single constant reuseKey this
|
|
818
|
+
// stays warm; the count exists so destroy() is reachable (see releaseShared) and
|
|
819
|
+
// so a future multi-key/LRU policy can evict safely.
|
|
820
|
+
private var refCounts: [String: Int] = [:]
|
|
821
|
+
// Serializes all dictionary access. Android guards the pool with @Synchronized;
|
|
822
|
+
// here concurrent acquire/release could otherwise corrupt the dict or create
|
|
823
|
+
// duplicate WebViews for one key (fix #3).
|
|
824
|
+
private let lock = NSLock()
|
|
825
|
+
|
|
826
|
+
/// Get (creating if absent) the entry for `key`. Does NOT change the refcount —
|
|
827
|
+
/// call `adopt` exactly once per host (idempotency via `adoptedPoolKey`).
|
|
745
828
|
func acquireShared(key: String) -> PooledChartWebView {
|
|
829
|
+
lock.lock()
|
|
830
|
+
defer { lock.unlock() }
|
|
746
831
|
if let existing = entries[key] { return existing }
|
|
747
832
|
let created = PooledChartWebView(key: key)
|
|
748
833
|
entries[key] = created
|
|
749
834
|
return created
|
|
750
835
|
}
|
|
836
|
+
|
|
837
|
+
/// Register one live host reference to `key` (call once per host).
|
|
838
|
+
func adopt(key: String) {
|
|
839
|
+
lock.lock()
|
|
840
|
+
defer { lock.unlock() }
|
|
841
|
+
refCounts[key, default: 0] += 1
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// A host that previously adopted `key` is going away. Decrements the refcount.
|
|
845
|
+
/// Kept warm by default (intentional single-instance cache); pass
|
|
846
|
+
/// `destroyWhenIdle: true` to tear the WebView down when the last host leaves.
|
|
847
|
+
func releaseShared(key: String, destroyWhenIdle: Bool = false) {
|
|
848
|
+
lock.lock()
|
|
849
|
+
let next = (refCounts[key] ?? 0) - 1
|
|
850
|
+
var toDestroy: PooledChartWebView?
|
|
851
|
+
if next <= 0 {
|
|
852
|
+
refCounts[key] = nil
|
|
853
|
+
if destroyWhenIdle {
|
|
854
|
+
toDestroy = entries.removeValue(forKey: key)
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
refCounts[key] = next
|
|
858
|
+
}
|
|
859
|
+
lock.unlock()
|
|
860
|
+
// destroy() hops to main and isn't part of the dict invariant — call outside
|
|
861
|
+
// the lock.
|
|
862
|
+
toDestroy?.destroy()
|
|
863
|
+
}
|
|
751
864
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -1,19 +1,64 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { createElement } from 'react';
|
|
4
|
-
import { getHostComponent } from 'react-native-nitro-modules';
|
|
3
|
+
import { createElement, useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { callback, getHostComponent } from 'react-native-nitro-modules';
|
|
5
5
|
const ChartWebviewConfig = require('../nitrogen/generated/shared/json/ChartWebviewConfig.json');
|
|
6
6
|
import { CHART_BRIDGE_JS } from "./bridge.js";
|
|
7
7
|
export { CHART_BRIDGE_JS } from "./bridge.js";
|
|
8
8
|
const NativeChartWebviewView = getHostComponent('ChartWebview', () => ChartWebviewConfig);
|
|
9
|
+
function unwrapCallback(wrapped) {
|
|
10
|
+
if (typeof wrapped === 'function') {
|
|
11
|
+
return wrapped;
|
|
12
|
+
}
|
|
13
|
+
return wrapped?.f;
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
// Default the document-start bridge script so consumers never have to know about
|
|
11
17
|
// it (and it's never absent — an optional string flipping to null is rejected by
|
|
12
18
|
// the native binding). Callers can still override `bridgeScript` to customize.
|
|
13
|
-
export function ChartWebviewView(
|
|
19
|
+
export function ChartWebviewView({
|
|
20
|
+
bridgeScript,
|
|
21
|
+
hybridRef,
|
|
22
|
+
onMessage,
|
|
23
|
+
onLoadEnd,
|
|
24
|
+
onError,
|
|
25
|
+
...props
|
|
26
|
+
}) {
|
|
27
|
+
const hybridRefRef = useRef(unwrapCallback(hybridRef));
|
|
28
|
+
hybridRefRef.current = unwrapCallback(hybridRef);
|
|
29
|
+
const onMessageRef = useRef(unwrapCallback(onMessage));
|
|
30
|
+
onMessageRef.current = unwrapCallback(onMessage);
|
|
31
|
+
const onLoadEndRef = useRef(unwrapCallback(onLoadEnd));
|
|
32
|
+
onLoadEndRef.current = unwrapCallback(onLoadEnd);
|
|
33
|
+
const onErrorRef = useRef(unwrapCallback(onError));
|
|
34
|
+
onErrorRef.current = unwrapCallback(onError);
|
|
35
|
+
const stableHybridRef = useCallback(ref => {
|
|
36
|
+
hybridRefRef.current?.(ref);
|
|
37
|
+
}, []);
|
|
38
|
+
const stableOnMessage = useCallback(message => {
|
|
39
|
+
onMessageRef.current?.(message);
|
|
40
|
+
}, []);
|
|
41
|
+
const stableOnLoadEnd = useCallback(() => {
|
|
42
|
+
onLoadEndRef.current?.();
|
|
43
|
+
}, []);
|
|
44
|
+
const stableOnError = useCallback(message => {
|
|
45
|
+
onErrorRef.current?.(message);
|
|
46
|
+
}, []);
|
|
47
|
+
const hasHybridRef = hybridRefRef.current != null;
|
|
48
|
+
const hasOnMessage = onMessageRef.current != null;
|
|
49
|
+
const hasOnLoadEnd = onLoadEndRef.current != null;
|
|
50
|
+
const hasOnError = onErrorRef.current != null;
|
|
51
|
+
const hybridRefProp = useMemo(() => hasHybridRef ? callback(stableHybridRef) : undefined, [hasHybridRef, stableHybridRef]);
|
|
52
|
+
const onMessageProp = useMemo(() => hasOnMessage ? callback(stableOnMessage) : undefined, [hasOnMessage, stableOnMessage]);
|
|
53
|
+
const onLoadEndProp = useMemo(() => hasOnLoadEnd ? callback(stableOnLoadEnd) : undefined, [hasOnLoadEnd, stableOnLoadEnd]);
|
|
54
|
+
const onErrorProp = useMemo(() => hasOnError ? callback(stableOnError) : undefined, [hasOnError, stableOnError]);
|
|
14
55
|
return /*#__PURE__*/createElement(NativeChartWebviewView, {
|
|
15
|
-
|
|
16
|
-
|
|
56
|
+
...props,
|
|
57
|
+
hybridRef: hybridRefProp,
|
|
58
|
+
onMessage: onMessageProp,
|
|
59
|
+
onLoadEnd: onLoadEndProp,
|
|
60
|
+
onError: onErrorProp,
|
|
61
|
+
bridgeScript: bridgeScript ?? CHART_BRIDGE_JS
|
|
17
62
|
});
|
|
18
63
|
}
|
|
19
64
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["createElement","getHostComponent","ChartWebviewConfig","require","CHART_BRIDGE_JS","NativeChartWebviewView","ChartWebviewView","props","
|
|
1
|
+
{"version":3,"names":["createElement","useCallback","useMemo","useRef","callback","getHostComponent","ChartWebviewConfig","require","CHART_BRIDGE_JS","NativeChartWebviewView","unwrapCallback","wrapped","f","ChartWebviewView","bridgeScript","hybridRef","onMessage","onLoadEnd","onError","props","hybridRefRef","current","onMessageRef","onLoadEndRef","onErrorRef","stableHybridRef","ref","stableOnMessage","message","stableOnLoadEnd","stableOnError","hasHybridRef","hasOnMessage","hasOnLoadEnd","hasOnError","hybridRefProp","undefined","onMessageProp","onLoadEndProp","onErrorProp"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ViewProps } from 'react-native';
|
|
2
|
+
import { type NitroViewWrappedCallback } from 'react-native-nitro-modules';
|
|
2
3
|
import type { ChartWebviewMethods, ChartWebviewProps } from './ChartWebview.nitro';
|
|
3
4
|
export type { ChartWebviewMethods, ChartWebviewProps } from './ChartWebview.nitro';
|
|
4
5
|
export { CHART_BRIDGE_JS } from './bridge';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
hybridRef?:
|
|
6
|
+
type MaybeWrappedCallback<T extends (...args: any[]) => void> = T | NitroViewWrappedCallback<T>;
|
|
7
|
+
type ChartWebviewViewProps = Omit<ChartWebviewProps, 'onMessage' | 'onLoadEnd' | 'onError'> & ViewProps & {
|
|
8
|
+
hybridRef?: MaybeWrappedCallback<(ref: any) => void>;
|
|
9
|
+
onMessage?: MaybeWrappedCallback<(message: string) => void>;
|
|
10
|
+
onLoadEnd?: MaybeWrappedCallback<() => void>;
|
|
11
|
+
onError?: MaybeWrappedCallback<(message: string) => void>;
|
|
12
|
+
};
|
|
13
|
+
export declare function ChartWebviewView({ bridgeScript, hybridRef, onMessage, onLoadEnd, onError, ...props }: ChartWebviewViewProps): import("react").FunctionComponentElement<Omit<{
|
|
14
|
+
hybridRef?: NitroViewWrappedCallback<((ref: import("react-native-nitro-modules").HybridView<ChartWebviewProps, ChartWebviewMethods>) => void) | undefined> | undefined;
|
|
8
15
|
uri?: string | undefined;
|
|
9
16
|
localBundle?: string | undefined;
|
|
10
17
|
entry?: string | undefined;
|
|
@@ -13,9 +20,9 @@ export declare function ChartWebviewView(props: ComponentProps<typeof NativeChar
|
|
|
13
20
|
reuseKey?: string | undefined;
|
|
14
21
|
pooled?: boolean | undefined;
|
|
15
22
|
active?: boolean | undefined;
|
|
16
|
-
onMessage?:
|
|
17
|
-
onLoadEnd?:
|
|
18
|
-
onError?:
|
|
23
|
+
onMessage?: NitroViewWrappedCallback<((message: string) => void) | undefined> | undefined;
|
|
24
|
+
onLoadEnd?: NitroViewWrappedCallback<(() => void) | undefined> | undefined;
|
|
25
|
+
onError?: NitroViewWrappedCallback<((message: string) => void) | undefined> | undefined;
|
|
19
26
|
} & Readonly<Omit<Readonly<{
|
|
20
27
|
onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => unknown) | undefined;
|
|
21
28
|
onAccessibilityTap?: (() => unknown) | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAGnF,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAO3C,KAAK,oBAAoB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,IACxD,CAAC,GACD,wBAAwB,CAAC,CAAC,CAAC,CAAC;AAEhC,KAAK,qBAAqB,GAAG,IAAI,CAC/B,iBAAiB,EACjB,WAAW,GAAG,WAAW,GAAG,SAAS,CACtC,GACC,SAAS,GAAG;IACV,SAAS,CAAC,EAAE,oBAAoB,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,oBAAoB,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IAC5D,SAAS,CAAC,EAAE,oBAAoB,CAAC,MAAM,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,oBAAoB,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;CAC3D,CAAC;AAcJ,wBAAgB,gBAAgB,CAAC,EAC/B,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDvB"}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { createElement,
|
|
2
|
-
import {
|
|
1
|
+
import { createElement, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { type ViewProps } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
callback,
|
|
5
|
+
getHostComponent,
|
|
6
|
+
type NitroViewWrappedCallback,
|
|
7
|
+
} from 'react-native-nitro-modules';
|
|
3
8
|
const ChartWebviewConfig = require('../nitrogen/generated/shared/json/ChartWebviewConfig.json');
|
|
4
9
|
import type { ChartWebviewMethods, ChartWebviewProps } from './ChartWebview.nitro';
|
|
5
10
|
import { CHART_BRIDGE_JS } from './bridge';
|
|
@@ -12,9 +17,91 @@ const NativeChartWebviewView = getHostComponent<ChartWebviewProps, ChartWebviewM
|
|
|
12
17
|
() => ChartWebviewConfig
|
|
13
18
|
);
|
|
14
19
|
|
|
20
|
+
type MaybeWrappedCallback<T extends (...args: any[]) => void> =
|
|
21
|
+
| T
|
|
22
|
+
| NitroViewWrappedCallback<T>;
|
|
23
|
+
|
|
24
|
+
type ChartWebviewViewProps = Omit<
|
|
25
|
+
ChartWebviewProps,
|
|
26
|
+
'onMessage' | 'onLoadEnd' | 'onError'
|
|
27
|
+
> &
|
|
28
|
+
ViewProps & {
|
|
29
|
+
hybridRef?: MaybeWrappedCallback<(ref: any) => void>;
|
|
30
|
+
onMessage?: MaybeWrappedCallback<(message: string) => void>;
|
|
31
|
+
onLoadEnd?: MaybeWrappedCallback<() => void>;
|
|
32
|
+
onError?: MaybeWrappedCallback<(message: string) => void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function unwrapCallback<T extends (...args: any[]) => void>(
|
|
36
|
+
wrapped: MaybeWrappedCallback<T> | undefined
|
|
37
|
+
): T | undefined {
|
|
38
|
+
if (typeof wrapped === 'function') {
|
|
39
|
+
return wrapped;
|
|
40
|
+
}
|
|
41
|
+
return wrapped?.f;
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
// Default the document-start bridge script so consumers never have to know about
|
|
16
45
|
// it (and it's never absent — an optional string flipping to null is rejected by
|
|
17
46
|
// the native binding). Callers can still override `bridgeScript` to customize.
|
|
18
|
-
export function ChartWebviewView(
|
|
19
|
-
|
|
47
|
+
export function ChartWebviewView({
|
|
48
|
+
bridgeScript,
|
|
49
|
+
hybridRef,
|
|
50
|
+
onMessage,
|
|
51
|
+
onLoadEnd,
|
|
52
|
+
onError,
|
|
53
|
+
...props
|
|
54
|
+
}: ChartWebviewViewProps) {
|
|
55
|
+
const hybridRefRef = useRef(unwrapCallback(hybridRef));
|
|
56
|
+
hybridRefRef.current = unwrapCallback(hybridRef);
|
|
57
|
+
const onMessageRef = useRef(unwrapCallback(onMessage));
|
|
58
|
+
onMessageRef.current = unwrapCallback(onMessage);
|
|
59
|
+
const onLoadEndRef = useRef(unwrapCallback(onLoadEnd));
|
|
60
|
+
onLoadEndRef.current = unwrapCallback(onLoadEnd);
|
|
61
|
+
const onErrorRef = useRef(unwrapCallback(onError));
|
|
62
|
+
onErrorRef.current = unwrapCallback(onError);
|
|
63
|
+
|
|
64
|
+
const stableHybridRef = useCallback((ref: any) => {
|
|
65
|
+
hybridRefRef.current?.(ref);
|
|
66
|
+
}, []);
|
|
67
|
+
const stableOnMessage = useCallback((message: string) => {
|
|
68
|
+
onMessageRef.current?.(message);
|
|
69
|
+
}, []);
|
|
70
|
+
const stableOnLoadEnd = useCallback(() => {
|
|
71
|
+
onLoadEndRef.current?.();
|
|
72
|
+
}, []);
|
|
73
|
+
const stableOnError = useCallback((message: string) => {
|
|
74
|
+
onErrorRef.current?.(message);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const hasHybridRef = hybridRefRef.current != null;
|
|
78
|
+
const hasOnMessage = onMessageRef.current != null;
|
|
79
|
+
const hasOnLoadEnd = onLoadEndRef.current != null;
|
|
80
|
+
const hasOnError = onErrorRef.current != null;
|
|
81
|
+
|
|
82
|
+
const hybridRefProp = useMemo(
|
|
83
|
+
() => (hasHybridRef ? callback(stableHybridRef) : undefined),
|
|
84
|
+
[hasHybridRef, stableHybridRef]
|
|
85
|
+
);
|
|
86
|
+
const onMessageProp = useMemo(
|
|
87
|
+
() => (hasOnMessage ? callback(stableOnMessage) : undefined),
|
|
88
|
+
[hasOnMessage, stableOnMessage]
|
|
89
|
+
);
|
|
90
|
+
const onLoadEndProp = useMemo(
|
|
91
|
+
() => (hasOnLoadEnd ? callback(stableOnLoadEnd) : undefined),
|
|
92
|
+
[hasOnLoadEnd, stableOnLoadEnd]
|
|
93
|
+
);
|
|
94
|
+
const onErrorProp = useMemo(
|
|
95
|
+
() => (hasOnError ? callback(stableOnError) : undefined),
|
|
96
|
+
[hasOnError, stableOnError]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return createElement(NativeChartWebviewView, {
|
|
100
|
+
...props,
|
|
101
|
+
hybridRef: hybridRefProp,
|
|
102
|
+
onMessage: onMessageProp,
|
|
103
|
+
onLoadEnd: onLoadEndProp,
|
|
104
|
+
onError: onErrorProp,
|
|
105
|
+
bridgeScript: bridgeScript ?? CHART_BRIDGE_JS,
|
|
106
|
+
});
|
|
20
107
|
}
|