@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
package/EverframeRN.podspec
CHANGED
|
@@ -40,7 +40,7 @@ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
|
40
40
|
# 1.2.0, so the range already asks for 1.2 while package.json is still 1.1.0.
|
|
41
41
|
# The 1.2.0 release moves package.json to match.
|
|
42
42
|
native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
|
|
43
|
-
native_minor_range = native_version_override.empty? ? '~> 1.
|
|
43
|
+
native_minor_range = native_version_override.empty? ? '~> 1.4.0' : native_version_override
|
|
44
44
|
|
|
45
45
|
Pod::Spec.new do |s|
|
|
46
46
|
s.name = 'EverframeRN'
|
|
@@ -86,7 +86,7 @@ Pod::Spec.new do |s|
|
|
|
86
86
|
s.dependency 'React-Core'
|
|
87
87
|
|
|
88
88
|
s.test_spec 'Tests' do |t|
|
|
89
|
-
t.source_files = 'ios/Tests/**/*.swift'
|
|
89
|
+
t.source_files = 'ios/Tests/**/*.{swift,m,mm}'
|
|
90
90
|
t.frameworks = ['XCTest', 'UIKit']
|
|
91
91
|
end
|
|
92
92
|
end
|
package/README.md
CHANGED
|
@@ -42,9 +42,9 @@ npx @everframe/cli setup react-native --app-id "$EVERFRAME_APP_ID"
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Either way, Android gets `apply plugin: "dev.everframe"` and
|
|
45
|
-
`everframe { appId = "…" }` plus one classpath line, and Xcode gets a
|
|
46
|
-
Script phase that runs `
|
|
47
|
-
the Gradle plugin and the CLI. Set `EVERFRAME_API_TOKEN` in release builds;
|
|
45
|
+
`everframe { appId = "…" }` plus one classpath line, and Xcode gets a
|
|
46
|
+
one-line Run Script phase that runs `scripts/xcode-upload.sh` from the installed
|
|
47
|
+
`@everframe/cli`. The upload logic lives in the Gradle plugin and the CLI. Set `EVERFRAME_API_TOKEN` in release builds;
|
|
48
48
|
without it the build warns and continues. See the
|
|
49
49
|
[CLI guide](../cli/README.md#react-native-and-expo).
|
|
50
50
|
|
|
@@ -517,15 +517,15 @@ inside a native view and never reaches JS.
|
|
|
517
517
|
- `captureSourceQuery` — keep the query string on `source_change.src`
|
|
518
518
|
(default `false`: signed CDN URLs carry tokens in the query).
|
|
519
519
|
|
|
520
|
-
**Reconfiguring.** A Provider remount with the
|
|
521
|
-
the SDK: both native sides compare the incoming config against the
|
|
520
|
+
**Reconfiguring.** A Provider remount with the _same_ config does not restart
|
|
521
|
+
the SDK: both native sides compare the incoming config against the _installed_
|
|
522
522
|
one and skip the start entirely. A **changed** config does restart it (on iOS,
|
|
523
523
|
except a change confined to `releaseHealth` or the loaded bundle; see
|
|
524
524
|
[Release health](#release-health)) — and a
|
|
525
525
|
start supersedes the running SDK, detaching every player integration it had
|
|
526
526
|
announced. **Limitation:** players tracked before that restart stay untracked
|
|
527
527
|
until their screens remount; nothing re-registers them automatically. Two React
|
|
528
|
-
instances in one process are
|
|
528
|
+
instances in one process are _not_ coordinated either: if a second one starts
|
|
529
529
|
the SDK under the first, the first instance's players stay untracked until its
|
|
530
530
|
own screens remount.
|
|
531
531
|
|
|
@@ -570,7 +570,7 @@ adapter has subscribed — and `onLoadStart` is what arms the adapter's startup
|
|
|
570
570
|
clock, so the first source's startup latency is simply never seen. Deferring
|
|
571
571
|
the load until after the adapter subscribed puts the first `onLoadStart` back
|
|
572
572
|
where it can be observed. Without this, everything else still works; only the
|
|
573
|
-
|
|
573
|
+
_first_ `startup` measurement is missing (later source changes are fine).
|
|
574
574
|
|
|
575
575
|
Options: `name`, `libraryVersion`, and `captureErrors` (default `false` — see
|
|
576
576
|
Limitations). `onBandwidthUpdate` is read per platform: on iOS its `bitrate`
|
|
@@ -580,7 +580,7 @@ the `bandwidthEstimate` stat (with `quality_change` for the accompanying
|
|
|
580
580
|
rendition size) and never as a bitrate.
|
|
581
581
|
|
|
582
582
|
`onProgress` is read per platform too. The `bufferAheadMs` stat is always the
|
|
583
|
-
media time buffered
|
|
583
|
+
media time buffered _ahead of the playhead_: on Android `bufferDuration` is
|
|
584
584
|
already that, on iOS it is the buffered range's absolute end position, so the
|
|
585
585
|
adapter subtracts `currentTime` from it.
|
|
586
586
|
|
|
@@ -663,7 +663,7 @@ dropped, not forwarded.
|
|
|
663
663
|
`captureErrors` is off by default.
|
|
664
664
|
- **`rate_change` was not observed from react-native-video v7 on Android** in
|
|
665
665
|
our Android emulator smoke — `onPlaybackRateChange` never arrived there.
|
|
666
|
-
Note that the library
|
|
666
|
+
Note that the library _does_ forward ExoPlayer's
|
|
667
667
|
`onPlaybackParametersChanged`, so this is a "not seen in our smoke run"
|
|
668
668
|
result, not a proven library gap; a host that changes playback speed
|
|
669
669
|
explicitly may well see it. Nothing on the adapter side gates it either way.
|
|
@@ -738,7 +738,7 @@ subscription?.remove();
|
|
|
738
738
|
```
|
|
739
739
|
|
|
740
740
|
> If you have seen the older `new TVEventHandler(); handler.enable(cmp, cb);
|
|
741
|
-
|
|
741
|
+
handler.disable()` shape elsewhere — that class-based API was removed in
|
|
742
742
|
> RN-tvos 0.85.x. Always read the version installed in your `node_modules`.
|
|
743
743
|
|
|
744
744
|
#### Apple TV: long-press Play/Pause
|
|
@@ -840,3 +840,186 @@ activity recreation.
|
|
|
840
840
|
## License
|
|
841
841
|
|
|
842
842
|
MIT
|
|
843
|
+
|
|
844
|
+
### Optional interaction target annotations
|
|
845
|
+
|
|
846
|
+
Phone taps and TV remote interactions automatically use the available native
|
|
847
|
+
control role, accessibility label, test ID, visible text, and screen marker.
|
|
848
|
+
Reporting and automatic breadcrumbs work without app annotations. Logical
|
|
849
|
+
collection positions are included only when known; mounted children are never
|
|
850
|
+
used as the total item count.
|
|
851
|
+
|
|
852
|
+
Use `useEverframeTarget` on an existing host to add product names or logical
|
|
853
|
+
collection metadata:
|
|
854
|
+
|
|
855
|
+
```tsx
|
|
856
|
+
import { useEverframeTarget } from '@everframe/react-native';
|
|
857
|
+
|
|
858
|
+
const targetRef = useEverframeTarget({
|
|
859
|
+
testId: 'poster-low-orbit', role: 'button', name: 'Low Orbit',
|
|
860
|
+
container: { testId: 'home-originals', name: 'Originals', role: 'list', index: 0, count: 12 },
|
|
861
|
+
});
|
|
862
|
+
return <Pressable ref={targetRef} onPress={openTitle}><Text>Low Orbit</Text></Pressable>;
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
Indices are zero-based logical positions in the collection. An explicit
|
|
866
|
+
container replaces the automatic container as a unit. Updating the annotation
|
|
867
|
+
updates the existing host without waiting for `onLayout`; passing `undefined`
|
|
868
|
+
removes it. Strings are bounded to 256 UTF-16 units (roles: 64). Do not put
|
|
869
|
+
secrets or user-entered values into annotations. Existing sensitive-view masking
|
|
870
|
+
takes precedence and suppresses names, IDs, containers, and screen metadata.
|
|
871
|
+
|
|
872
|
+
Compose with an existing ref using a stable callback (return no value):
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
const ref = React.useCallback((node: React.ElementRef<typeof View> | null) => {
|
|
876
|
+
existingRef.current = node;
|
|
877
|
+
targetRef(node);
|
|
878
|
+
}, [targetRef]);
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
Custom components must forward the ref to their native host. For a layout-only
|
|
882
|
+
`View`, use `collapsable={false}` so a host exists. Unsupported custom refs,
|
|
883
|
+
missing providers, and older native binaries fail softly. This feature requires
|
|
884
|
+
a native rebuild to enable annotation registration; it cannot be enabled by a
|
|
885
|
+
JavaScript-only update. The web export accepts the same type and is a no-op.
|
|
886
|
+
No wrapper, `onPress` replacement, or manual breadcrumb is needed.
|
|
887
|
+
|
|
888
|
+
Native registrations use weak host references and cancel stale ref generations.
|
|
889
|
+
After 4096 distinct registrations in one native session, optional annotations
|
|
890
|
+
fail closed until SDK restart; automatic target extraction continues. This
|
|
891
|
+
bound prevents delayed work from resurrecting evicted registration ownership.
|
|
892
|
+
|
|
893
|
+
Native target qualification uses the installed Fabric fixture under
|
|
894
|
+
`__tests__/fixtures/interaction-targets`. Rebuild native binaries after upgrading.
|
|
895
|
+
The iOS sensitive wrapper retains its native host and resolves it through the
|
|
896
|
+
Fabric view registry, so masked descendants stay excluded from target labels.
|
|
897
|
+
|
|
898
|
+
### State labels (iOS and Android)
|
|
899
|
+
|
|
900
|
+
`setState(key, value)` records a string, finite number, or boolean diagnostic label;
|
|
901
|
+
`clearState(key)` removes it. Both are also available from `useEverframe()`.
|
|
902
|
+
Call after the provider mounts. State is redacted, bounded, and frozen with report
|
|
903
|
+
evidence. See [State API](../../docs/state.md) for lifecycle, privacy, limits and
|
|
904
|
+
native recovery freshness.
|
|
905
|
+
|
|
906
|
+
## Semantic interaction evidence
|
|
907
|
+
|
|
908
|
+
On iOS, Android and their TV variants, supported native controls record text edits,
|
|
909
|
+
control changes and completed scroll gestures automatically. Existing taps and
|
|
910
|
+
reporting continue to work without hooks. Programmatic text/control updates do not
|
|
911
|
+
represent user interaction. TV focus movement only counts as scrolling when the
|
|
912
|
+
native content offset changes.
|
|
913
|
+
|
|
914
|
+
Text evidence contains its UTF-16 length by default. Secure or sensitive fields
|
|
915
|
+
produce no text evidence, including length. To permit a value locally, configure
|
|
916
|
+
an exact native `testID`:
|
|
917
|
+
|
|
918
|
+
```tsx
|
|
919
|
+
<EverframeProvider config={{
|
|
920
|
+
sdkKey: 'txx_…',
|
|
921
|
+
interactions: { valueTestIds: ['search-input'] },
|
|
922
|
+
}}>
|
|
923
|
+
<App />
|
|
924
|
+
</EverframeProvider>
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Optional values are redacted and limited to 256 UTF-16 units. Allowlisting a name
|
|
928
|
+
only in hook metadata never grants permission: the live native host must carry
|
|
929
|
+
that exact `testID`. Setting `interactions: { disabled: true }` disables these
|
|
930
|
+
semantic events without changing existing tap/error capture.
|
|
931
|
+
|
|
932
|
+
Use `useEverframeInteraction` to add target metadata to an existing native host.
|
|
933
|
+
Its ref adds no wrapper or app handler. Compose it with any existing app ref:
|
|
934
|
+
|
|
935
|
+
```tsx
|
|
936
|
+
import { useEverframeInteraction } from '@everframe/react-native';
|
|
937
|
+
import type { ElementRef } from 'react';
|
|
938
|
+
import { TextInput } from 'react-native';
|
|
939
|
+
|
|
940
|
+
function SearchField() {
|
|
941
|
+
const interaction = useEverframeInteraction<ElementRef<typeof TextInput>>({
|
|
942
|
+
target: { testId: 'search-input', name: 'Search' },
|
|
943
|
+
});
|
|
944
|
+
return <TextInput testID="search-input" ref={interaction.ref} />;
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Native observation owns text, control and scroll evidence. `helperTypes` is
|
|
949
|
+
retained for source compatibility and always registers an empty list, including
|
|
950
|
+
legacy declarations. Each native bridge also strips helper ownership from older
|
|
951
|
+
JavaScript callers, so these declarations never suppress automatic observation.
|
|
952
|
+
Existing tap evidence is preserved. Missing providers, unsupported refs, older
|
|
953
|
+
native binaries and the web entry safely leave metadata registration inactive.
|
|
954
|
+
|
|
955
|
+
`textChanged(text, options?)`, `controlChanged(change)` and
|
|
956
|
+
`scrollSettled(change)` are compatibility no-ops. They never inspect their
|
|
957
|
+
payloads or send values, lengths or state across the bridge. JavaScript callbacks
|
|
958
|
+
cannot prove whether a host was sensitive when the observation originated;
|
|
959
|
+
checking after it is unmasked would be too late. Custom JavaScript controls
|
|
960
|
+
without a supported native observation point produce missing semantic evidence.
|
|
961
|
+
|
|
962
|
+
Clearing evidence, signing out or changing native capture configuration revokes
|
|
963
|
+
pending work. A queued callback keeps its original ownership and cannot renew it.
|
|
964
|
+
An unchanged mounted hook does not replay old metadata; a fresh committed host
|
|
965
|
+
attachment or metadata change can register again.
|
|
966
|
+
|
|
967
|
+
Text coalesces after 750 ms of inactivity, editing end, submission or safe report
|
|
968
|
+
freeze. An unfinished native composition retains only the last observed user-edit
|
|
969
|
+
length, without its value. If the keyboard ends composition without another text
|
|
970
|
+
change after the idle check, that summary waits for editing end or safe report
|
|
971
|
+
freeze. Its length describes the observed edit, including when the app later
|
|
972
|
+
replaces the text; it is not a newly sampled final value.
|
|
973
|
+
Scrolls coalesce at gesture completion or after 200 ms of inactivity.
|
|
974
|
+
Semantic events share a 20-per-10-second budget; text and scroll also share a
|
|
975
|
+
one-per-second budget per host. Values are bounded again after final redaction.
|
|
976
|
+
|
|
977
|
+
Incoming links are observed through React Native's existing `Linking` channel while the provider is mounted. The SDK does not route links. By default, evidence contains only scheme and host; valid opaque or hostless links retain only their scheme (for example `myapp:`). Parsing works in bare React Native without a global URL patch. Declare known route templates (for example `/watch/:id`) and exact query keys in `interactions.urlRules` to include more. Credentials, fragments and actual dynamic path segments are omitted; ambiguous encoded separators and dot segments are rejected. The initial URL is considered once per JS process. If capture is disabled at that first mount, it is omitted permanently; later mounts and re-enabling capture observe only fresh URL callbacks. The mounted subscription stays active while local policy enables it, and each new callback checks the current native capture lease. Distinct URL callbacks remain distinct observations even when their URLs match, because React Native supplies no delivery ID.
|
|
978
|
+
|
|
979
|
+
Observe an existing permission operation without changing its result:
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
import { observePermissionRequest, recordLaunchContext } from '@everframe/react-native';
|
|
983
|
+
import { PermissionsAndroid } from 'react-native';
|
|
984
|
+
|
|
985
|
+
const result = await observePermissionRequest('android.permission.CAMERA', () =>
|
|
986
|
+
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
// For an existing notification integration; call during the matching foreground entry.
|
|
990
|
+
recordLaunchContext({ trigger: 'notification', arguments: { campaign: 'autumn' } });
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Without a mapper, the permission observer recognizes Android's `granted`, `denied` and `never_ask_again` results. Supply a mapper returning `granted`, `denied`, `blocked`, `limited`, `unavailable` or `unknown` for iOS or third-party APIs. The operation runs exactly once; its original value or rejection is preserved. A mapper failure or rejected request is recorded as `unknown`, never inferred as denial. A result does not prove a system dialog was shown. TV APIs that do not support a permission should map their actual result to `unavailable`.
|
|
994
|
+
|
|
995
|
+
Launch context enriches only a native foreground entry that is still pending (up to 250 ms, ending earlier on report freeze or background). It cannot create a launch, change cold/warm classification, or alter a frozen report. Late context is omitted. Only exact `interactions.launchArgumentKeys` allowlisted primitive values are accepted, at most 16 entries and 128 UTF-16 units per string. URLs belong to link evidence and are omitted from launch arguments. No arguments are captured by default.
|
|
996
|
+
|
|
997
|
+
Permission operations still run without a provider, with older native binaries and on web. Launch context is a no-op in those cases. A clear, sign-out or native configuration change revokes in-flight observations; it does not cancel the app's operation.
|
|
998
|
+
|
|
999
|
+
Target annotations enrich supported automatic native observations, including IME composition and optional allowlisted values. Native integrations that observe controls directly on the UI thread can supply native helpers with original privacy proof; the JavaScript compatibility methods cannot provide that proof.
|
|
1000
|
+
|
|
1001
|
+
### Reporter boundaries and developer tools
|
|
1002
|
+
|
|
1003
|
+
Native phone reporter opens and companion report requests add this custom breadcrumb before freezing evidence:
|
|
1004
|
+
|
|
1005
|
+
```json
|
|
1006
|
+
{"kind":"custom","message":"reporter_opened","data":{"source":"everframe_reporter","trigger":"button"}}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
`trigger` is `button`, `shake`, `menu`, or `companion`. Pending text/scroll summaries are flushed before this boundary. Native shake uses `shake`; companion report requests use `companion`; existing `open()` calls use `button`. To identify a host menu entry explicitly, call `open({ trigger: 'menu' })`. Closing or cancelling the native flow adds `reporter_closed` with `data.source: 'everframe_reporter'` to the live trail. Both events retain breadcrumb capture/config gates.
|
|
1010
|
+
|
|
1011
|
+
A reporter call crosses the React Native bridge after its native input can already have been recorded. It cannot prove that the previous tap or remote key opened the reporter. For a button, annotate its native host ref before interaction:
|
|
1012
|
+
|
|
1013
|
+
```tsx
|
|
1014
|
+
const reporterRef = useEverframeTarget({ reporterTrigger: 'button' });
|
|
1015
|
+
const { open } = useEverframe();
|
|
1016
|
+
return <Pressable ref={reporterRef} onPress={() => void open()}><Text>Report</Text></Pressable>;
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
That target's tap/activation carries `data.origin: 'reporter'` and `data.trigger: 'button'`. On a remote, only Select/Enter activation is attributed to a `button` target; arrows, Menu and Play/Pause retain their ordinary meaning. An explicitly annotated `reporterTrigger: 'menu'` target attributes only its Menu activation. Attribution survives masking without retaining the target's text. Removing/recycling the bound ref removes attribution.
|
|
1020
|
+
|
|
1021
|
+
Hosts owning a reporting flow outside `open()` can `await markReporterOpened('menu')` before showing their reporter UI and `await markReporterClosed()` when it closes. These promises resolve after the native UI-thread boundary work; await them before adding subsequent host breadcrumbs or changing reporter UI. These explicit markers do not claim or remove earlier native keys: a TV event callback may run after native key capture. Use companion report-request boundaries when a host cannot attribute the original remote trigger. Never infer reporter intent from a button label, a Menu/Play/Pause key, or proximity to `open()`.
|
|
1022
|
+
|
|
1023
|
+
Native target capture adds `data.origin: 'devtools'` to supported taps and structured text/control/scroll observations whose native ancestry belongs to React Native LogBox, the native development menu, or Expo development-client UI. This comes from framework view/window/controller identity, an RN-owned development-menu controller, or a framework root module identity. In a development host, the inline LogBox notification/inspector is also registered by the exact React Native component exports through the existing React DevTools commit hook; application components with the same name are not attributed. This observer reads component identity and native refs, never warning text or component props, and removes registrations when their committed hosts disappear or the provider unmounts. Production JavaScript leaves this observer inert. A development JavaScript host can still attribute LogBox when its native SDK was built in Release mode. Display text such as “Dismiss” never grants an origin. A missing React DevTools hook, unavailable RN private module exports at runtime, initial asynchronous module loading, unknown framework versions and surfaces outside the native capture adapters leave attribution unavailable; origin tagging does not install a new dialog capture mechanism. The observer depends on the LogBox module paths in supported React Native versions; a future version removing those paths requires an SDK compatibility update before Metro can bundle it.
|
|
1024
|
+
|
|
1025
|
+
Consumers should use the latest active reporter interval (matching `reporter_opened`/`reporter_closed` markers by ordered trail), cut before its explicitly attributed triggering interaction when present, and otherwise before its opened marker. Closed reporter intervals can also be excluded while preserving application actions after their closing marker. Developer-tool observations can be excluded by their explicit `data.origin`. A cancelled earlier report must not become the boundary for a later report. Legacy trails lacking these fields remain valid and supply no new attribution evidence.
|
package/android/build.gradle.kts
CHANGED
|
@@ -153,7 +153,7 @@ val rnSdkPkg = groovy.json.JsonSlurper()
|
|
|
153
153
|
val rnSdkVersion = rnSdkPkg["version"] as String
|
|
154
154
|
// Native 1.2.0 is the release that renamed the SDK key to `sdkKey`; the
|
|
155
155
|
// bridge moves with it, ahead of package.json, which the 1.2.0 release bumps.
|
|
156
|
-
val nativeMinorRange: String = nativeVersionOverride ?: "[1.
|
|
156
|
+
val nativeMinorRange: String = nativeVersionOverride ?: "[1.4.0,1.5.0)"
|
|
157
157
|
if (nativeVersionOverride != null) {
|
|
158
158
|
logger.lifecycle(
|
|
159
159
|
"[everframe-rn] native AAR pinned to $nativeVersionOverride via everframeNativeVersion " +
|
|
@@ -106,6 +106,44 @@ class EverframeModule(
|
|
|
106
106
|
private val reactContext: ReactApplicationContext,
|
|
107
107
|
) : NativeEverframeSpec(reactContext) {
|
|
108
108
|
|
|
109
|
+
private val interactionTargetAdapter = dev.everframe.capture.InteractionTargetViewAdapters.register(RnInteractionTargetAdapter)
|
|
110
|
+
private val interactionEvents = RnInteractionEventRegistration(
|
|
111
|
+
{ tag -> com.facebook.react.uimanager.UIManagerHelper.getUIManagerForReactTag(reactContext, tag)?.resolveView(tag) },
|
|
112
|
+
{ action -> com.facebook.react.bridge.UiThreadUtil.runOnUiThread { action() } },
|
|
113
|
+
)
|
|
114
|
+
private val interactionEventObserver = RnInteractionEventObserver.install(reactContext) { interactionEvents.active() }
|
|
115
|
+
private val interactionTargets = RnInteractionTargetRegistration(
|
|
116
|
+
{ tag -> UIManagerHelper.getUIManagerForReactTag(reactContext, tag)?.resolveView(tag)?.takeIf { it.isAttachedToWindow } },
|
|
117
|
+
{ block -> UiThreadUtil.runOnUiThread { block() } },
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
private var interactionMountsClosed = false
|
|
121
|
+
private var interactionTargetUIManager: com.facebook.react.bridge.UIManager? = null
|
|
122
|
+
@OptIn(com.facebook.react.common.annotations.UnstableReactNativeAPI::class)
|
|
123
|
+
private val interactionMountListener = object : com.facebook.react.bridge.UIManagerListener {
|
|
124
|
+
override fun willDispatchViewUpdates(uiManager: com.facebook.react.bridge.UIManager) = Unit
|
|
125
|
+
override fun willMountItems(uiManager: com.facebook.react.bridge.UIManager) = Unit
|
|
126
|
+
override fun didMountItems(uiManager: com.facebook.react.bridge.UIManager) { interactionTargets.onMount(); interactionEvents.onMount() }
|
|
127
|
+
override fun didDispatchMountItems(uiManager: com.facebook.react.bridge.UIManager) { interactionTargets.onMount(); interactionEvents.onMount() }
|
|
128
|
+
override fun didScheduleMountItems(uiManager: com.facebook.react.bridge.UIManager) = Unit
|
|
129
|
+
}
|
|
130
|
+
@OptIn(com.facebook.react.common.annotations.UnstableReactNativeAPI::class)
|
|
131
|
+
@Synchronized private fun observeInteractionMounts(tag: Int) {
|
|
132
|
+
if (interactionMountsClosed) return
|
|
133
|
+
val manager = UIManagerHelper.getUIManagerForReactTag(reactContext, tag) ?: return
|
|
134
|
+
if (interactionTargetUIManager === manager) return
|
|
135
|
+
interactionTargetUIManager?.removeUIManagerEventListener(interactionMountListener)
|
|
136
|
+
interactionTargetUIManager = manager
|
|
137
|
+
manager.addUIManagerEventListener(interactionMountListener)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@OptIn(com.facebook.react.common.annotations.UnstableReactNativeAPI::class)
|
|
141
|
+
@Synchronized private fun stopObservingInteractionMounts() {
|
|
142
|
+
interactionMountsClosed = true
|
|
143
|
+
interactionTargetUIManager?.removeUIManagerEventListener(interactionMountListener)
|
|
144
|
+
interactionTargetUIManager = null
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
private val reporterBridge = ReporterBridge()
|
|
110
148
|
|
|
111
149
|
private val videoPrivacyAdapter = Everframe.__registerVideoPrivacyAdapter(RnVideoPrivacyAdapter)
|
|
@@ -290,6 +328,9 @@ class EverframeModule(
|
|
|
290
328
|
release = sdkVersion,
|
|
291
329
|
capture = CaptureConfig(networkBodies = !networkBodiesDisabled, crash = !crashReportingDisabled),
|
|
292
330
|
installIdentifierEnabled = !installIdentifierDisabled,
|
|
331
|
+
interactions = dev.everframe.capture.InteractionEventPolicy.parse(opts.takeIfHasString("interactionsJson")),
|
|
332
|
+
loadedJsBundle = dev.everframe.config.LoadedJsBundle.create(
|
|
333
|
+
opts.takeIfHasString("jsBundleBuildId"), opts.takeIfHasString("jsBundleName")),
|
|
293
334
|
releaseHealth = parseReleaseHealth(opts),
|
|
294
335
|
// The JS facade always sends React Native's `__DEV__` (or the host's override). A missing
|
|
295
336
|
// or null key stays null so the native SDK decides for itself, as for any other caller.
|
|
@@ -369,6 +410,7 @@ class EverframeModule(
|
|
|
369
410
|
// or the host is left with a dead SDK and no way to revive it.
|
|
370
411
|
// • `attachPinUi` — the one bridge option that lands in module state instead of
|
|
371
412
|
// in `EverframeConfig`, so config equality alone cannot see it change.
|
|
413
|
+
interactionEventObserver.refresh(reactContext)
|
|
372
414
|
if (Everframe.captureGate &&
|
|
373
415
|
Everframe.currentConfig == config &&
|
|
374
416
|
companionAttachPinUi == attachPinUi
|
|
@@ -392,6 +434,7 @@ class EverframeModule(
|
|
|
392
434
|
// Window.Callback wrapper is never installed on the current screen
|
|
393
435
|
// and the session ships zero tap crumbs (field bug 2026-07-10).
|
|
394
436
|
Everframe.start(reactContext.applicationContext, config, reactContext.currentActivity)
|
|
437
|
+
interactionTargets.onStarted()
|
|
395
438
|
|
|
396
439
|
// Codex round-5, G1 — the registry is NOT renewed here any more. The live players
|
|
397
440
|
// this start just orphaned are re-registered from JS instead: `mount()` bumps the
|
|
@@ -427,6 +470,45 @@ class EverframeModule(
|
|
|
427
470
|
*/
|
|
428
471
|
@ReactMethod
|
|
429
472
|
override fun openReporter(promise: Promise) {
|
|
473
|
+
openReporterInternal(null,promise)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@ReactMethod
|
|
477
|
+
override fun openReporterWithTrigger(trigger: String, promise: Promise) {
|
|
478
|
+
val value = dev.everframe.config.ReporterTrigger.fromValue(trigger)
|
|
479
|
+
if (value == null) {
|
|
480
|
+
promise.reject(REJECT_OPEN_REPORTER_FAILED,"Invalid reporter trigger")
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
openReporterInternal(value,promise)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@ReactMethod
|
|
487
|
+
override fun markReporterOpened(trigger: String, promise: Promise) {
|
|
488
|
+
runReporterMarker(Everframe.__prepareReporterOpened(trigger),promise)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@ReactMethod
|
|
492
|
+
override fun markReporterClosed(promise: Promise) {
|
|
493
|
+
runReporterMarker(Everframe.__prepareReporterClosed(),promise)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private fun runReporterMarker(action: Runnable, promise: Promise) {
|
|
497
|
+
try {
|
|
498
|
+
UiThreadUtil.runOnUiThread {
|
|
499
|
+
try {
|
|
500
|
+
action.run()
|
|
501
|
+
promise.resolve(null)
|
|
502
|
+
} catch (_: Throwable) {
|
|
503
|
+
promise.reject("reporter_marker_failed","Could not record reporter marker")
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch (_: Throwable) {
|
|
507
|
+
promise.reject("reporter_marker_failed","Could not schedule reporter marker")
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private fun openReporterInternal(trigger: dev.everframe.config.ReporterTrigger?, promise: Promise) {
|
|
430
512
|
if (!Everframe.captureGate) {
|
|
431
513
|
promise.reject(REJECT_CAPTURE_DISABLED, "Everframe not started (captureGate=false). Call configure() first.")
|
|
432
514
|
return
|
|
@@ -439,7 +521,10 @@ class EverframeModule(
|
|
|
439
521
|
}
|
|
440
522
|
MainScope().launch {
|
|
441
523
|
try {
|
|
442
|
-
val result: ReportResult = reporterBridge.open(reactContext)
|
|
524
|
+
val result: ReportResult = if (trigger == null) reporterBridge.open(reactContext)
|
|
525
|
+
else kotlinx.coroutines.withContext(dev.everframe.config.ReporterTriggerContext(trigger)) {
|
|
526
|
+
reporterBridge.open(reactContext)
|
|
527
|
+
}
|
|
443
528
|
val dict = Arguments.createMap().apply {
|
|
444
529
|
when (result) {
|
|
445
530
|
is ReportResult.Submitted -> {
|
|
@@ -602,7 +687,13 @@ class EverframeModule(
|
|
|
602
687
|
* BEFORE the Provider's configure-on-mount, so clearing there would drop
|
|
603
688
|
* legitimate registrations that had already landed.
|
|
604
689
|
*/
|
|
690
|
+
@OptIn(com.facebook.react.common.annotations.UnstableReactNativeAPI::class)
|
|
605
691
|
override fun invalidate() {
|
|
692
|
+
interactionEventObserver.close()
|
|
693
|
+
interactionEvents.close()
|
|
694
|
+
stopObservingInteractionMounts()
|
|
695
|
+
interactionTargets.close()
|
|
696
|
+
interactionTargetAdapter.close()
|
|
606
697
|
sensitiveRegistration.close()
|
|
607
698
|
videoPrivacyAdapter.close()
|
|
608
699
|
super.invalidate()
|
|
@@ -623,6 +714,16 @@ class EverframeModule(
|
|
|
623
714
|
extraResolverActive = false
|
|
624
715
|
}
|
|
625
716
|
|
|
717
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
718
|
+
override fun setStateSync(key: String, valueJson: String): Boolean {
|
|
719
|
+
if (valueJson.length > 2048) return false
|
|
720
|
+
val value = try { kotlinx.serialization.json.Json.decodeFromString(dev.everframe.protocol.generated.StateValue.serializer(), valueJson) }
|
|
721
|
+
catch (_: Exception) { return false }
|
|
722
|
+
return Everframe.setState(key, value)
|
|
723
|
+
}
|
|
724
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
725
|
+
override fun clearStateSync(key: String): Boolean = Everframe.clearState(key)
|
|
726
|
+
|
|
626
727
|
// ---------------- setUser (self-declared identity — spec 2026-08-12) ----------------
|
|
627
728
|
|
|
628
729
|
/**
|
|
@@ -760,6 +861,26 @@ class EverframeModule(
|
|
|
760
861
|
*/
|
|
761
862
|
internal var __drainRequester: () -> Unit = { Everframe.requestOutboxDrain() }
|
|
762
863
|
|
|
864
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
865
|
+
override fun setInteractionTarget(viewTag: Double, registrationId: String, generation: Double, annotation: ReadableMap): Boolean =
|
|
866
|
+
runCatching {
|
|
867
|
+
if (viewTag.isFinite() && viewTag > 0 && viewTag <= Int.MAX_VALUE && viewTag == viewTag.toInt().toDouble()) observeInteractionMounts(viewTag.toInt())
|
|
868
|
+
interactionTargets.set(viewTag, registrationId, generation, annotation.toHashMap())
|
|
869
|
+
}.getOrDefault(false)
|
|
870
|
+
|
|
871
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
872
|
+
override fun getInteractionLease(): String = interactionEvents.lease()
|
|
873
|
+
override fun setInteractionObserver(viewTag: Double, id: String, generation: Double, lease: String, optionsJson: String): Boolean =
|
|
874
|
+
runCatching {
|
|
875
|
+
if (viewTag.isFinite() && viewTag > 0 && viewTag <= Int.MAX_VALUE && viewTag == viewTag.toInt().toDouble()) observeInteractionMounts(viewTag.toInt())
|
|
876
|
+
interactionEvents.set(viewTag,id,generation,lease,optionsJson)
|
|
877
|
+
}.getOrDefault(false)
|
|
878
|
+
override fun clearInteractionObserver(id: String, generation: Double): Boolean = runCatching { interactionEvents.clear(id,generation) }.getOrDefault(false)
|
|
879
|
+
override fun recordInteraction(lease: String, id: String, generation: Double, eventJson: String): Boolean = runCatching { interactionEvents.record(lease,id,generation,eventJson) }.getOrDefault(false)
|
|
880
|
+
|
|
881
|
+
override fun clearInteractionTarget(registrationId: String, generation: Double): Boolean =
|
|
882
|
+
runCatching { interactionTargets.clear(registrationId, generation) }.getOrDefault(false)
|
|
883
|
+
|
|
763
884
|
// ---------------- registerSensitiveRect ----------------
|
|
764
885
|
|
|
765
886
|
@ReactMethod
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 ScriptX
|
|
3
|
+
package dev.everframe.rn
|
|
4
|
+
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.CompoundButton
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
10
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
11
|
+
import com.facebook.react.uimanager.events.Event
|
|
12
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
13
|
+
import com.facebook.react.uimanager.events.EventDispatcherListener
|
|
14
|
+
import com.facebook.react.views.textinput.ReactEditText
|
|
15
|
+
import dev.everframe.capture.InteractionTextAdapter
|
|
16
|
+
import dev.everframe.capture.InteractionControlAdapter
|
|
17
|
+
import dev.everframe.capture.InteractionScrollAdapter
|
|
18
|
+
|
|
19
|
+
/** Observes before JS dispatch. Never reads protected event payloads or replaces app callbacks. */
|
|
20
|
+
internal class RnInteractionEventObserver(private val resolve: (Int, Int) -> View?, private val active: () -> Boolean = { true }) : EventDispatcherListener, AutoCloseable {
|
|
21
|
+
private var dispatcher: EventDispatcher? = null
|
|
22
|
+
@Volatile private var closed = false
|
|
23
|
+
fun attach(value: EventDispatcher) {
|
|
24
|
+
if (closed || dispatcher === value) return
|
|
25
|
+
dispatcher?.removeListener(this); dispatcher = value; value.addListener(this)
|
|
26
|
+
}
|
|
27
|
+
fun refresh(context: ReactApplicationContext) {
|
|
28
|
+
UiThreadUtil.runOnUiThread { runCatching { UIManagerHelper.getEventDispatcher(context)?.let(::attach) } }
|
|
29
|
+
}
|
|
30
|
+
override fun onEventDispatch(event: Event<*>) {
|
|
31
|
+
// Queuing an event would acquire ownership after a possible clear and retain raw payload.
|
|
32
|
+
if (closed || !active() || Looper.myLooper() != Looper.getMainLooper()) return
|
|
33
|
+
runCatching {
|
|
34
|
+
val name = event.getEventName()
|
|
35
|
+
if (name !in names) return
|
|
36
|
+
val view = resolve(event.viewTag, event.surfaceId) ?: return
|
|
37
|
+
if (!view.isAttachedToWindow || UIManagerHelper.getSurfaceId(view) != event.surfaceId) return
|
|
38
|
+
when {
|
|
39
|
+
view is ReactEditText -> when (name) {
|
|
40
|
+
"topChange" -> InteractionTextAdapter.shared.changed(view)
|
|
41
|
+
"topEndEditing" -> InteractionTextAdapter.shared.ended(view)
|
|
42
|
+
"topSubmitEditing" -> InteractionTextAdapter.shared.ended(view, submit = true)
|
|
43
|
+
else -> Unit
|
|
44
|
+
}
|
|
45
|
+
view is CompoundButton && view.javaClass.name == "com.facebook.react.views.switchview.ReactSwitch" && name == "topChange" -> InteractionControlAdapter.shared.changed(view)
|
|
46
|
+
view.javaClass.name in scrollHosts -> when (name) {
|
|
47
|
+
"topScrollBeginDrag" -> InteractionScrollAdapter.shared.began(view, if (view is android.widget.HorizontalScrollView) "x" else "y")
|
|
48
|
+
"topScroll" -> InteractionScrollAdapter.shared.moved(view)
|
|
49
|
+
"topScrollEndDrag" -> InteractionScrollAdapter.shared.endDrag(view)
|
|
50
|
+
"topMomentumScrollEnd" -> InteractionScrollAdapter.shared.finished(view)
|
|
51
|
+
else -> Unit
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
override fun close() {
|
|
57
|
+
closed = true
|
|
58
|
+
dispatcher?.removeListener(this); dispatcher = null
|
|
59
|
+
}
|
|
60
|
+
companion object {
|
|
61
|
+
private val names = setOf("topChange", "topEndEditing", "topSubmitEditing", "topScrollBeginDrag", "topScroll", "topScrollEndDrag", "topMomentumScrollEnd")
|
|
62
|
+
private val scrollHosts = setOf("com.facebook.react.views.scroll.ReactScrollView", "com.facebook.react.views.scroll.ReactHorizontalScrollView")
|
|
63
|
+
fun install(context: ReactApplicationContext, active: () -> Boolean): RnInteractionEventObserver {
|
|
64
|
+
val observer = RnInteractionEventObserver({ tag, _ -> UIManagerHelper.getUIManagerForReactTag(context, tag)?.resolveView(tag) }, active)
|
|
65
|
+
observer.refresh(context)
|
|
66
|
+
return observer
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|