@everframe/react-native 1.2.1 → 1.4.0
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/EverframeRN.podspec +2 -2
- package/README.md +193 -10
- package/android/build.gradle.kts +1 -1
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +122 -1
- package/android/src/main/java/dev/everframe/rn/RnInteractionEventObserver.kt +69 -0
- package/android/src/main/java/dev/everframe/rn/RnInteractionEventRegistration.kt +127 -0
- package/android/src/main/java/dev/everframe/rn/RnInteractionTargetAdapter.kt +33 -0
- package/android/src/main/java/dev/everframe/rn/RnInteractionTargetRegistration.kt +72 -0
- package/dist/NativeEverframe.d.ts +18 -0
- package/dist/chunk-FZHLCD3Z.js +1 -0
- package/dist/index.d.ts +37 -734
- package/dist/index.js +30 -30
- package/dist/index.web.d.ts +26 -3
- package/dist/index.web.js +1 -1
- package/dist/integrations/console.js +1 -1
- package/dist/integrations/react-navigation.js +1 -1
- package/dist/integrations/react.js +1 -1
- package/dist/launch-BvE4IjRk.d.ts +1090 -0
- package/ios/Sources/EverframeBridge.swift +29 -2
- package/ios/Sources/EverframeModule.mm +115 -6
- package/ios/Sources/RnDevtoolsOrigin.h +7 -0
- package/ios/Sources/RnDevtoolsOrigin.mm +36 -0
- package/ios/Sources/RnInteractionEventRegistration.swift +141 -0
- package/ios/Sources/RnInteractionTargetRegistration.swift +107 -0
- package/package.json +6 -5
- package/src/NativeEverframe.ts +38 -22
- package/src/codegen-types.ts +0 -1
- package/dist/chunk-MIWWEGLO.js +0 -1
- package/dist/promise-rejection-types-Bf5iLv8n.d.ts +0 -99
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 ScriptX
|
|
3
|
+
package dev.everframe.rn
|
|
4
|
+
|
|
5
|
+
import android.view.View
|
|
6
|
+
import dev.everframe.capture.InteractionEvents
|
|
7
|
+
import dev.everframe.capture.PreparedInteraction
|
|
8
|
+
import java.lang.ref.WeakReference
|
|
9
|
+
import kotlinx.serialization.json.*
|
|
10
|
+
|
|
11
|
+
/** Queued work owns the original lease. A new runtime cannot renew old refs or callbacks. */
|
|
12
|
+
internal class RnInteractionEventRegistration(
|
|
13
|
+
private val resolve: (Int) -> View?,
|
|
14
|
+
private val dispatch: (() -> Unit) -> Unit,
|
|
15
|
+
) : AutoCloseable {
|
|
16
|
+
private class Request(val tag: Int, val generation: Long, val lease: String, val types: Set<String>) {
|
|
17
|
+
var view: WeakReference<View>? = null
|
|
18
|
+
var resolved = false
|
|
19
|
+
var listener: View.OnAttachStateChangeListener? = null
|
|
20
|
+
}
|
|
21
|
+
val runtime = InteractionEvents.openRuntime()
|
|
22
|
+
private val namespace = "$runtime:"
|
|
23
|
+
private val requests = mutableMapOf<String, Request>()
|
|
24
|
+
private class PendingRecord(val item: PreparedInteraction, val id: String, val generation: Long)
|
|
25
|
+
private val records = ArrayDeque<PendingRecord>()
|
|
26
|
+
private var recordDrainScheduled = false
|
|
27
|
+
@Volatile private var closed = false
|
|
28
|
+
fun active(): Boolean = !closed && InteractionEvents.ownsRuntime(runtime)
|
|
29
|
+
fun lease(): String = if (active()) InteractionEvents.lease() ?: "" else ""
|
|
30
|
+
private fun integer(value: Double, max: Double) = value.isFinite() && value >= 0 && value <= max && value == value.toLong().toDouble()
|
|
31
|
+
private fun validId(id: String) = id.isNotEmpty() && id.length <= 128 && id.none { it.code < 32 || it.code == 127 }
|
|
32
|
+
@Synchronized fun set(tag: Double, id: String, generation: Double, lease: String, json: String): Boolean {
|
|
33
|
+
if (!active() || !validId(id) || tag <= 0 || !integer(tag,Int.MAX_VALUE.toDouble()) || !integer(generation,9007199254740990.0) || lease.isEmpty() || lease != lease()) return false
|
|
34
|
+
if (json.toByteArray(Charsets.UTF_8).size > 8192) return false
|
|
35
|
+
val options = runCatching { Json.parseToJsonElement(json) as? JsonObject }.getOrNull() ?: return false
|
|
36
|
+
val list = options["helperTypes"] as? JsonArray ?: return false
|
|
37
|
+
if (list.size > 3) return false
|
|
38
|
+
val types = list.map { (it as? JsonPrimitive)?.takeIf { it.isString }?.content ?: return false }.toSet()
|
|
39
|
+
if (!setOf("text_edit","control_change","scroll").containsAll(types)) return false
|
|
40
|
+
// JS callbacks have no immutable native privacy proof at observation.
|
|
41
|
+
// Keep annotations while leaving all native automatic observation active.
|
|
42
|
+
requests.filterValues { it.lease != lease }.values.forEach(::detachListener)
|
|
43
|
+
requests.entries.removeAll { it.value.lease != lease }
|
|
44
|
+
if (requests.size >= 4096 && !requests.containsKey(id)) return false
|
|
45
|
+
if (!InteractionEvents.beginRegistration(namespace+id,generation.toLong(),lease,json)) return false
|
|
46
|
+
val request = Request(tag.toInt(),generation.toLong(),lease,emptySet())
|
|
47
|
+
requests.put(id,request)?.let(::detachListener); enqueue(id,request)
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
private fun enqueue(id: String, request: Request) {
|
|
51
|
+
dispatch {
|
|
52
|
+
synchronized(this) {
|
|
53
|
+
if (!active() || request.lease != lease() || requests[id] !== request || request.resolved) return@dispatch
|
|
54
|
+
val view = runCatching { resolve(request.tag) }.getOrNull() ?: return@dispatch
|
|
55
|
+
if (!view.isAttachedToWindow || view.id != request.tag) return@dispatch
|
|
56
|
+
request.view = WeakReference(view); request.resolved = true
|
|
57
|
+
if (InteractionEvents.completeRegistration(view,namespace+id,request.generation,request.lease,request.types)) {
|
|
58
|
+
val owner = WeakReference(this)
|
|
59
|
+
val nativeId = namespace+id
|
|
60
|
+
val listener = object : View.OnAttachStateChangeListener {
|
|
61
|
+
override fun onViewAttachedToWindow(view: View) = Unit
|
|
62
|
+
override fun onViewDetachedFromWindow(view: View) {
|
|
63
|
+
view.removeOnAttachStateChangeListener(this)
|
|
64
|
+
InteractionEvents.cancelRegistration(nativeId,request.generation)
|
|
65
|
+
owner.get()?.let { bridge -> synchronized(bridge) { if (bridge.requests[id] === request) bridge.requests.remove(id) } }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
request.listener = listener; view.addOnAttachStateChangeListener(listener)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
private fun detachListener(request: Request) {
|
|
74
|
+
dispatch { request.listener?.let { request.view?.get()?.removeOnAttachStateChangeListener(it) }; request.listener = null }
|
|
75
|
+
}
|
|
76
|
+
@Synchronized fun onMount() {
|
|
77
|
+
if (!active()) return
|
|
78
|
+
requests.filterValues { it.lease != lease() }.values.forEach(::detachListener)
|
|
79
|
+
requests.entries.removeAll { it.value.lease != lease() }
|
|
80
|
+
requests.filterValues { !it.resolved }.forEach { (id,request) -> enqueue(id,request) }
|
|
81
|
+
}
|
|
82
|
+
@Synchronized fun clear(id: String, generation: Double): Boolean {
|
|
83
|
+
if (!active() || !validId(id) || !integer(generation,9007199254740991.0)) return false
|
|
84
|
+
if (!InteractionEvents.clearRegistration(namespace+id,generation.toLong())) return false
|
|
85
|
+
requests.remove(id)?.let(::detachListener); return true
|
|
86
|
+
}
|
|
87
|
+
@Synchronized fun record(lease: String, id: String, generation: Double, json: String): Boolean {
|
|
88
|
+
if (!active() || !integer(generation,9007199254740991.0) || (id.isNotEmpty() && !validId(id))) return false
|
|
89
|
+
val prepared = InteractionEvents.prepare(lease,if (id.isEmpty()) "" else namespace+id,generation.toLong(),json) ?: return false
|
|
90
|
+
// Native coalescing/revocation can empty a prepared cell before main runs.
|
|
91
|
+
// Bound those cells too, and enqueue only one callback for the mailbox.
|
|
92
|
+
if (records.size == 64) InteractionEvents.discardPrepared(records.removeFirst().item)
|
|
93
|
+
records.addLast(PendingRecord(prepared,id,generation.toLong()))
|
|
94
|
+
if (!recordDrainScheduled) { recordDrainScheduled = true; scheduleRecordDrain() }
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
private fun scheduleRecordDrain() {
|
|
98
|
+
val owner = WeakReference(this)
|
|
99
|
+
dispatch { owner.get()?.drainRecords() }
|
|
100
|
+
}
|
|
101
|
+
private fun drainRecords() {
|
|
102
|
+
// Yield after a bounded batch even if producers keep refilling the mailbox.
|
|
103
|
+
repeat(64) {
|
|
104
|
+
synchronized(this) {
|
|
105
|
+
if (records.isEmpty()) return@repeat
|
|
106
|
+
val pending = records.removeFirst()
|
|
107
|
+
try {
|
|
108
|
+
if (!active()) return@repeat
|
|
109
|
+
if (pending.id.isNotEmpty()) {
|
|
110
|
+
val request = requests[pending.id] ?: return@repeat
|
|
111
|
+
val view = request.view?.get() ?: return@repeat
|
|
112
|
+
if (request.generation != pending.generation || resolve(request.tag) !== view) return@repeat
|
|
113
|
+
}
|
|
114
|
+
InteractionEvents.recordPrepared(pending.item)
|
|
115
|
+
} finally { InteractionEvents.discardPrepared(pending.item) }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
synchronized(this) {
|
|
119
|
+
if (records.isEmpty()) recordDrainScheduled = false else scheduleRecordDrain()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
@Synchronized override fun close() {
|
|
123
|
+
closed = true
|
|
124
|
+
records.forEach { InteractionEvents.discardPrepared(it.item) }; records.clear()
|
|
125
|
+
requests.values.forEach(::detachListener); requests.clear(); InteractionEvents.closeRuntime(runtime)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 ScriptX
|
|
3
|
+
package dev.everframe.rn
|
|
4
|
+
|
|
5
|
+
import android.view.View
|
|
6
|
+
import com.facebook.react.ReactRootView
|
|
7
|
+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole
|
|
8
|
+
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role
|
|
9
|
+
import dev.everframe.capture.InteractionTargetViewAdapter
|
|
10
|
+
import dev.everframe.capture.InteractionTargetViewHints
|
|
11
|
+
import java.util.Locale
|
|
12
|
+
|
|
13
|
+
internal object RnInteractionTargetAdapter : InteractionTargetViewAdapter {
|
|
14
|
+
override fun describe(view: View): InteractionTargetViewHints? {
|
|
15
|
+
val testId = view.getTag(com.facebook.react.R.id.react_test_id) as? String
|
|
16
|
+
val rawRole = ((view.getTag(com.facebook.react.R.id.role) as? Role)?.name
|
|
17
|
+
?: (view.getTag(com.facebook.react.R.id.accessibility_role) as? AccessibilityRole)?.name)?.lowercase(Locale.ROOT)
|
|
18
|
+
val role = when (rawRole) {
|
|
19
|
+
"imagebutton" -> "button"
|
|
20
|
+
"adjustable" -> "slider"
|
|
21
|
+
"edittext", "search", "searchbox", "textbox" -> "textfield"
|
|
22
|
+
"heading" -> "header"
|
|
23
|
+
"img" -> "image"
|
|
24
|
+
"none", null -> null
|
|
25
|
+
else -> rawRole
|
|
26
|
+
}
|
|
27
|
+
return InteractionTargetViewHints(testId = testId, role = role,
|
|
28
|
+
// ReactSurfaceView (Fabric) also extends ReactRootView. The module
|
|
29
|
+
// identity is assigned by the framework, independent of rendered labels.
|
|
30
|
+
devtools = view is ReactRootView && runCatching { view.jsModuleName in setOf("LogBox", "DevMenu") }.getOrDefault(false),
|
|
31
|
+
actionable = role in setOf("button", "tab", "link", "switch", "checkbox", "radio", "slider", "menuitem", "textfield"))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 ScriptX
|
|
3
|
+
package dev.everframe.rn
|
|
4
|
+
|
|
5
|
+
import android.view.View
|
|
6
|
+
import dev.everframe.capture.InteractionTargetRegistry
|
|
7
|
+
import java.lang.ref.WeakReference
|
|
8
|
+
import java.util.UUID
|
|
9
|
+
|
|
10
|
+
/** Synchronous ownership cancellation, UI-only host resolution. Never waits for the UI thread. */
|
|
11
|
+
internal class RnInteractionTargetRegistration(
|
|
12
|
+
private val resolve: (Int) -> View?,
|
|
13
|
+
private val dispatch: (() -> Unit) -> Unit,
|
|
14
|
+
) : AutoCloseable {
|
|
15
|
+
private class Request(val tag: Int, val generation: Long, val annotation: Map<String, Any?>) {
|
|
16
|
+
var view: WeakReference<View>? = null
|
|
17
|
+
}
|
|
18
|
+
private val namespace = UUID.randomUUID().toString() + ":"
|
|
19
|
+
private val requests = mutableMapOf<String, Request>()
|
|
20
|
+
private var destroyed = false
|
|
21
|
+
private val pending = mutableMapOf<String, Request>()
|
|
22
|
+
private fun integer(value: Double, maximum: Double) = value.isFinite() && value >= 0 && value <= maximum && value == value.toLong().toDouble()
|
|
23
|
+
|
|
24
|
+
@Synchronized fun set(tag: Double, id: String, generation: Double, annotation: Map<String, Any?>): Boolean {
|
|
25
|
+
if (destroyed || tag <= 0 || !integer(tag, Int.MAX_VALUE.toDouble()) || !integer(generation, 9007199254740990.0)) return false
|
|
26
|
+
val key = namespace + id
|
|
27
|
+
if (!InteractionTargetRegistry.beginSet(key, generation.toLong())) return false
|
|
28
|
+
val request = Request(tag.toInt(), generation.toLong(), annotation)
|
|
29
|
+
requests[id] = request
|
|
30
|
+
pending[id] = request
|
|
31
|
+
enqueue(id, request)
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
private fun enqueue(id: String, request: Request) {
|
|
35
|
+
try { dispatch {
|
|
36
|
+
synchronized(this) {
|
|
37
|
+
if (!destroyed && requests[id] === request) {
|
|
38
|
+
val view = runCatching { resolve(request.tag) }.getOrNull()
|
|
39
|
+
// A resolved host cannot migrate to a replacement using the same numeric tag.
|
|
40
|
+
if (view != null && (request.view == null || request.view?.get() === view)) {
|
|
41
|
+
pending.remove(id)
|
|
42
|
+
request.view = WeakReference(view)
|
|
43
|
+
InteractionTargetRegistry.completeSet(view, namespace + id, request.generation, request.annotation)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} } catch (_: Throwable) { InteractionTargetRegistry.clear(namespace + id, request.generation + 1) }
|
|
48
|
+
}
|
|
49
|
+
/** Fabric can deliver the ref before the native mount. Retry only live unresolved owners. */
|
|
50
|
+
@Synchronized fun onMount() {
|
|
51
|
+
if (!destroyed && pending.isNotEmpty()) pending.toList().forEach { (id, request) -> enqueue(id, request) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Synchronized fun clear(id: String, generation: Double): Boolean {
|
|
55
|
+
if (destroyed || !integer(generation, 9007199254740991.0)) return false
|
|
56
|
+
val current = requests[id]
|
|
57
|
+
if (current == null || generation > current.generation) { requests.remove(id); pending.remove(id) }
|
|
58
|
+
InteractionTargetRegistry.clear(namespace + id, generation.toLong())
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
/** Provider configure may run after child ref attachment. Core start clears its registry. */
|
|
62
|
+
@Synchronized fun onStarted() {
|
|
63
|
+
if (!destroyed) requests.forEach { (id, request) ->
|
|
64
|
+
if (InteractionTargetRegistry.beginSet(namespace + id, request.generation)) { pending[id] = request; enqueue(id, request) }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
@Synchronized override fun close() {
|
|
68
|
+
destroyed = true
|
|
69
|
+
requests.forEach { (id, request) -> InteractionTargetRegistry.clear(namespace + id, request.generation + 1) }
|
|
70
|
+
requests.clear(); pending.clear()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -23,6 +23,8 @@ type Rect = {
|
|
|
23
23
|
* Rich variants (e.g., callbacks, function refs) MUST stay on the JS side.
|
|
24
24
|
*/
|
|
25
25
|
type ConfigOpts = {
|
|
26
|
+
/** Bounded local interaction policy; native validation remains authoritative. */
|
|
27
|
+
interactionsJson?: string;
|
|
26
28
|
sdkKey?: string;
|
|
27
29
|
sdkVersion?: string;
|
|
28
30
|
/**
|
|
@@ -148,6 +150,9 @@ type ConfigOpts = {
|
|
|
148
150
|
* sends it (the config's `debugBuild`, else `__DEV__`). Absent means the native SDK decides for itself.
|
|
149
151
|
*/
|
|
150
152
|
debugBuild?: boolean;
|
|
153
|
+
/** Loaded Hermes identity for ordinary reports, independent of release health. */
|
|
154
|
+
jsBundleBuildId?: string;
|
|
155
|
+
jsBundleName?: string;
|
|
151
156
|
/**
|
|
152
157
|
* Native foreground monitoring (release health). The JS facade always sends this flag: `true` with a
|
|
153
158
|
* validated loaded bundle, or `false` to revoke monitoring. A bridge that finds it missing treats it as
|
|
@@ -192,6 +197,16 @@ interface Spec extends TurboModule {
|
|
|
192
197
|
* Typed `UnsafeObject` at codegen boundary; facade casts to `ReporterResult`.
|
|
193
198
|
*/
|
|
194
199
|
openReporter(): Promise<UnsafeObject>;
|
|
200
|
+
openReporterWithTrigger(trigger: string): Promise<UnsafeObject>;
|
|
201
|
+
markReporterOpened(trigger: string): Promise<void>;
|
|
202
|
+
markReporterClosed(): Promise<void>;
|
|
203
|
+
setInteractionTarget(viewTag: number, registrationId: string, generation: number, annotation: UnsafeObject): boolean;
|
|
204
|
+
clearInteractionTarget(registrationId: string, generation: number): boolean;
|
|
205
|
+
/** Revocable semantic observation ownership; unrelated to replay controls. */
|
|
206
|
+
getInteractionLease(): string;
|
|
207
|
+
setInteractionObserver(viewTag: number, id: string, generation: number, lease: string, optionsJson: string): boolean;
|
|
208
|
+
clearInteractionObserver(id: string, generation: number): boolean;
|
|
209
|
+
recordInteraction(lease: string, id: string, generation: number, eventJson: string): boolean;
|
|
195
210
|
registerSensitiveRect(viewTag: number, rect: Rect): void;
|
|
196
211
|
/**
|
|
197
212
|
* Set host-supplied free-form metadata for the next report. Single opaque
|
|
@@ -342,6 +357,9 @@ interface Spec extends TurboModule {
|
|
|
342
357
|
* credential. It groups reports in the dashboard and does nothing else.
|
|
343
358
|
*/
|
|
344
359
|
setUser(user?: UnsafeObject): void;
|
|
360
|
+
/** Synchronous acknowledgement preserves setState → fatal capture ordering. */
|
|
361
|
+
setStateSync(key: string, valueJson: string): boolean;
|
|
362
|
+
clearStateSync(key: string): boolean;
|
|
345
363
|
}
|
|
346
364
|
declare const _default: Spec;
|
|
347
365
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as i,c as s,e as a}from"./chunk-UN3Q4UGZ.js";import{a as t}from"./chunk-XUSV3KOD.js";var n=class extends Error{static{t(this,"EverframeNotMountedError")}name="EverframeNotMountedError";constructor(o){super(o??"EverframeProvider is not mounted; wrap your app root.")}},c="EVERFRAME_REPORTER_DISABLED",p=class extends Error{static{t(this,"EverframeReporterDisabledError")}name="EverframeReporterDisabledError";constructor(o){super(o??"The Everframe reporter is disabled in this session (reporter.disabled).")}};function d(e){return e==="button"||e==="shake"||e==="menu"||e==="companion"}t(d,"isReporterTrigger");var r=null,u={};function R(){return u}t(R,"__getContextEpoch");function f(){try{return r?.getPromiseRejectionStatus()??a("not-mounted","no-mount")}catch{return a("install-failed","hook-install")}}t(f,"getPromiseRejectionStatus");function S(e){r=e,u={}}t(S,"__setCurrentContext");function y(){return r}t(y,"__getCurrentContext");function C(e){let o=r;return o?o.open(e):Promise.reject(new n("EverframeProvider not mounted; wrap your root with <EverframeProvider>."))}t(C,"open");function k(e){r?.setExtra(e)}t(k,"setExtra");function b(e){r?.addBreadcrumb(e)}t(b,"addBreadcrumb");function P(e,o){o!==void 0?r?.recordScreen(e,o):r?.recordScreen(e)}t(P,"recordScreen");function O(e){r?.setUser(e)}t(O,"setUser");function w(e,o){r?.captureException(e,o)}t(w,"captureException");function h(){try{return r?.getErrorCaptureStatus()??s("not-mounted","no-mount")}catch{return s("not-mounted","no-mount")}}t(h,"getErrorCaptureStatus");function j(){let e=r;try{let o=e?.getReportDeliveryStatus();return e===r&&o?o:i("not-mounted","no-mount")}catch{return i("unavailable","native-call-failed")}}t(j,"getReportDeliveryStatus");function _(e,o){r?.setState(e,o)}t(_,"setState");function D(e){r?.clearState(e)}t(D,"clearState");async function T(e){await r?.markReporterOpened(e)}t(T,"markReporterOpened");async function V(){await r?.markReporterClosed()}t(V,"markReporterClosed");export{n as a,c as b,p as c,d,R as e,f,S as g,y as h,C as i,k as j,b as k,P as l,O as m,w as n,h as o,j as p,_ as q,D as r,T as s,V as t};
|