@everframe/react-native 0.9.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.
Files changed (38) hide show
  1. package/EverframeRN.podspec +88 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +8 -0
  4. package/README.md +505 -0
  5. package/android/build.gradle.kts +209 -0
  6. package/android/settings.gradle.kts +8 -0
  7. package/android/src/main/AndroidManifest.xml +9 -0
  8. package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +1670 -0
  9. package/android/src/main/java/dev/everframe/rn/EverframePackage.kt +40 -0
  10. package/android/src/main/java/dev/everframe/rn/EverframeSensitiveViewManager.kt +20 -0
  11. package/android/src/main/java/dev/everframe/rn/RnVideoPrivacyAdapter.kt +65 -0
  12. package/dist/NativeEverframe.d.ts +308 -0
  13. package/dist/NativeEverframe.js +1 -0
  14. package/dist/browser-ZQAY6B4M.js +1 -0
  15. package/dist/chunk-LPIKPZNX.js +1 -0
  16. package/dist/chunk-QRPINMCR.js +1 -0
  17. package/dist/chunk-QZRTTQRS.js +1 -0
  18. package/dist/chunk-XFLFA52R.js +1 -0
  19. package/dist/chunk-XUSV3KOD.js +1 -0
  20. package/dist/index.d.ts +1006 -0
  21. package/dist/index.js +51 -0
  22. package/dist/integrations/console.d.ts +8 -0
  23. package/dist/integrations/console.js +1 -0
  24. package/dist/integrations/react-native-video.d.ts +44 -0
  25. package/dist/integrations/react-native-video.js +1 -0
  26. package/dist/integrations/react-navigation.d.ts +21 -0
  27. package/dist/integrations/react-navigation.js +1 -0
  28. package/dist/integrations/theoplayer.d.ts +37 -0
  29. package/dist/integrations/theoplayer.js +1 -0
  30. package/dist/types-D5e4kWaw.d.ts +12 -0
  31. package/ios/Sources/EverframeBridge.swift +1043 -0
  32. package/ios/Sources/EverframeEventEmitter.swift +204 -0
  33. package/ios/Sources/EverframeModule.h +36 -0
  34. package/ios/Sources/EverframeModule.mm +489 -0
  35. package/ios/Sources/RemoteVitalsBridge.swift +99 -0
  36. package/package.json +121 -0
  37. package/src/NativeEverframe.ts +439 -0
  38. package/src/codegen-types.ts +11 -0
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2026 ScriptX
3
+ require 'json'
4
+
5
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
6
+
7
+ # Derive the native-SDK range from this package's own version, so the JS half
8
+ # and the native half always agree. `~> X.Y.Z` means `>= X.Y.Z, < X.(Y+1).0`:
9
+ # it blocks cross-minor drift, and it puts a floor at this package's exact
10
+ # version.
11
+ #
12
+ # The floor matters. A range of `~> X.Y.0` is satisfied by X.Y.0 forever, and
13
+ # CocoaPods only re-resolves a pod whose `Podfile.lock` entry no longer fits its
14
+ # constraint — so upgrading the npm package alone would leave `pod install`
15
+ # returning the previously locked native SDK, with nothing in the project
16
+ # reporting a mismatch. The floor makes the stale entry unsatisfiable, so the
17
+ # ordinary `pod install` picks up the matching native version without needing
18
+ # `pod update`.
19
+ #
20
+ # The Android build (`android/build.gradle.kts`) carries the same floor.
21
+ #
22
+ # Every release publishes the native SDKs before the npm packages and verifies
23
+ # they are fetchable first, so a published `@everframe/react-native@X.Y.Z` always
24
+ # has a `Everframe@X.Y.Z` to resolve against.
25
+ #
26
+ # Local/CI override, mirroring `everframeNativeVersion` on the Android side
27
+ # (packages/sdk-react-native/android/build.gradle.kts). It exists for the window
28
+ # between a native minor landing and changesets publishing the JS bump that
29
+ # matches it — which is EVERY native minor, not an edge case: this package's
30
+ # version only moves at release time, so until then the derived range asks for a
31
+ # native pod that the workspace no longer contains, and `pod install` cannot
32
+ # resolve at all.
33
+ #
34
+ # Deliberately an env var rather than a file lookup: this podspec is also
35
+ # evaluated from inside the published npm tarball at a consumer's install, where
36
+ # no workspace path exists. Unset — every consumer install — the derivation
37
+ # below is the only source of truth, unchanged.
38
+ native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
39
+ native_minor_range = native_version_override.empty? ? '~> 0.9.0' : native_version_override
40
+
41
+ Pod::Spec.new do |s|
42
+ s.name = 'EverframeRN'
43
+ s.version = package['version']
44
+ s.summary = 'React Native bridge for the Everframe iOS SDK.'
45
+ s.description = <<~DESC
46
+ iOS half of @everframe/react-native — a thin TurboModule bridge over
47
+ the Everframe iOS SDK. Composes capture from public primitives and delegates
48
+ submit to the SDK.
49
+ DESC
50
+ s.homepage = 'https://everframe.dev'
51
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
52
+ s.authors = { 'ScriptX' => 'eng@scriptx.com' }
53
+ s.source = { :git => 'https://github.com/scriptx-com/everframe.git', :tag => "v#{s.version}" }
54
+
55
+ s.platforms = { :ios => '15.0', :tvos => '15.0' }
56
+ s.swift_versions = ['5.10']
57
+
58
+ s.frameworks = ['UIKit']
59
+ s.requires_arc = true
60
+
61
+ s.source_files = 'ios/Sources/**/*.{h,m,mm,swift}'
62
+ s.public_header_files = 'ios/Sources/EverframeModule.h'
63
+
64
+ s.pod_target_xcconfig = {
65
+ 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
66
+ 'CLANG_CXX_LIBRARY' => 'libc++',
67
+ 'DEFINES_MODULE' => 'YES',
68
+ }
69
+
70
+ if defined?(install_modules_dependencies)
71
+ install_modules_dependencies(s)
72
+ else
73
+ s.dependency 'React-Core'
74
+ end
75
+
76
+ # Published Everframe iOS SDK (CocoaPods Trunk). Range derived from the
77
+ # JS package version (see `native_minor_range` above) so JS and native
78
+ # always agree on the minor.
79
+ s.dependency 'Everframe/Core', native_minor_range
80
+ s.dependency 'Everframe/ReporterUI', native_minor_range
81
+
82
+ s.dependency 'React-Core'
83
+
84
+ s.test_spec 'Tests' do |t|
85
+ t.source_files = 'ios/Tests/**/*.swift'
86
+ t.frameworks = ['XCTest', 'UIKit']
87
+ end
88
+ end
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScriptX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ SPDX-License-Identifier: MIT
2
+ SPDX-FileCopyrightText: 2026 ScriptX
3
+
4
+ Everframe
5
+ SPDX-FileCopyrightText: 2026 ScriptX
6
+
7
+ This product includes software developed by ScriptX
8
+ (https://github.com/scriptx-com/everframe).
package/README.md ADDED
@@ -0,0 +1,505 @@
1
+ <!-- SPDX-License-Identifier: MIT -->
2
+ <!-- SPDX-FileCopyrightText: 2026 ScriptX -->
3
+
4
+ # @everframe/react-native
5
+
6
+ Everframe React Native bridge — exposes the native iOS and Android reporter
7
+ modal to RN host apps via a TurboModule + a thin React provider/hook API.
8
+
9
+ > Phone, Apple TV, and Android TV are all supported when the host app is
10
+ > built against `react-native-tvos` (see [TV Host Integration](#tv-host-integration-apple-tv--android-tv)).
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @everframe/react-native
18
+ ```
19
+
20
+ Peer dependencies: `react` ≥ 19, `react-native` (or `react-native-tvos` for
21
+ TV targets) ≥ 0.85.
22
+
23
+ Then install pods (iOS) / sync Gradle (Android) as usual:
24
+
25
+ ```bash
26
+ cd ios && pod install
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Wrap your app with `<EverframeProvider>` at the highest practical level
32
+ (above any navigator / focus engine root):
33
+
34
+ ```tsx
35
+ import { EverframeProvider, useEverframe } from '@everframe/react-native';
36
+
37
+ export default function App() {
38
+ return (
39
+ // `apiKey` is the only required field. The ingest endpoint is a
40
+ // compile-time constant in the SDK and is not configurable in v1.
41
+ <EverframeProvider
42
+ config={{
43
+ apiKey: 'txx_live_xxxxxxxxxxxxxxxx',
44
+ // Recommended in RN/Expo development: shake also opens the dev menu.
45
+ shakeToReport: { enabled: !__DEV__ },
46
+ }}
47
+ >
48
+ <RootNavigator />
49
+ </EverframeProvider>
50
+ );
51
+ }
52
+ ```
53
+
54
+ Trigger the reporter from any host UI:
55
+
56
+ ```tsx
57
+ function HelpButton() {
58
+ const { open } = useEverframe();
59
+ return <Button title="Report a bug" onPress={() => open()} />;
60
+ }
61
+ ```
62
+
63
+ For non-component contexts, use the top-level `open` re-export
64
+ (throws `EverframeNotMountedError` if the provider is not yet mounted):
65
+
66
+ ```ts
67
+ import { open } from '@everframe/react-native';
68
+ await open();
69
+ ```
70
+
71
+ `open()` returns a `ReporterResult` with `{ status: 'submitted' |
72
+ 'queued' | 'cancelled', ... }`.
73
+
74
+ For handled JavaScript exceptions, call `captureException(error)` from the
75
+ provider context and attach any non-sensitive context needed for diagnosis.
76
+ The source API requires matching rebuilt native components; published `0.7.0`
77
+ artifacts are not evidence of support.
78
+
79
+ ## Network body capture (client veto)
80
+
81
+ ```tsx
82
+ <EverframeProvider config={{ apiKey: '…', networkBodies: { disabled: true } }}>
83
+ ```
84
+
85
+ `networkBodies.disabled: true` is a **client veto** — it can only turn body
86
+ capture off locally; it can never turn it on. The server's per-app
87
+ `captureBodies` gate is still authoritative, and native network capture must
88
+ still be wired up on each platform (see below) before any body is ever
89
+ recorded.
90
+
91
+ **This option does not, by itself, make React Native capture network
92
+ bodies.** RN does not currently attach Everframe's network capture to its own
93
+ HTTP clients (`fetch`, Axios, etc.) — that wiring is tracked separately and
94
+ is not part of this SDK yet. RN apps do still inherit the native SDK's own
95
+ auto-capture running underneath them, so `networkBodies` controls that
96
+ native capture — and even that requires the native side to actually be
97
+ capturing network traffic in the first place:
98
+
99
+ - **Android**: bodies are captured only for OkHttpClient instances the host
100
+ app explicitly builds with `addEverframeInterceptor()` (see
101
+ `packages/sdk-android/android/README.md`). Without that interceptor, no
102
+ network capture — and therefore no bodies — happens regardless of this
103
+ option.
104
+ - **iOS**: equivalent native network capture wiring, if and when the host
105
+ app uses it.
106
+
107
+ Do not enable this option expecting RN's own network calls to show up in
108
+ reports — that capability doesn't exist yet.
109
+
110
+ ## Navigation breadcrumbs
111
+
112
+ Mark screens with one line; the native SDK derives `from → to` from a global
113
+ chain shared with native auto-capture. Works with any navigation approach.
114
+
115
+ react-navigation (screens stay mounted — pass focus):
116
+
117
+ ```tsx
118
+ import { useEverframeScreen } from '@everframe/react-native';
119
+ import { useIsFocused, useRoute } from '@react-navigation/native';
120
+
121
+ function DetailScreen() {
122
+ useEverframeScreen(useRoute().name, { focused: useIsFocused() });
123
+ ...
124
+ }
125
+ ```
126
+
127
+ …or whole-app in one place:
128
+
129
+ ```tsx
130
+ <NavigationContainer ref={navRef}
131
+ onStateChange={() => recordScreen(navRef.getCurrentRoute()?.name ?? '')}>
132
+ ```
133
+
134
+ Wix react-native-navigation:
135
+
136
+ ```tsx
137
+ componentDidAppear() { recordScreen(this.props.screenName); }
138
+ ```
139
+
140
+ Hand-rolled (conditional-render tabs, custom switchers):
141
+
142
+ ```tsx
143
+ function DeskTab() {
144
+ useEverframeScreen('Desk');
145
+ ...
146
+ }
147
+ ```
148
+
149
+ Screen names should be route identifiers, never user content.
150
+
151
+ ## Opt-in JS integrations
152
+
153
+ Native capture (taps, screens, lifecycle, errors) is automatic. Two things
154
+ live only in JS — console logs and navigator state — and are captured ONLY
155
+ when you opt in:
156
+
157
+ ```tsx
158
+ import { consoleIntegration } from '@everframe/react-native/integrations/console';
159
+ import { reactNavigationIntegration } from '@everframe/react-native/integrations/react-navigation';
160
+ import { createNavigationContainerRef } from '@react-navigation/native';
161
+
162
+ const navigationRef = createNavigationContainerRef();
163
+ const txNav = reactNavigationIntegration({ navigationRef });
164
+
165
+ <EverframeProvider config={{ apiKey, integrations: [consoleIntegration(), txNav] }}>
166
+ <NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
167
+ ...
168
+ ```
169
+
170
+ - `consoleIntegration({ levels? })` — forwards `console.log/info/warn/error`
171
+ (configurable) to the breadcrumb trail with real severity. Originals always
172
+ run first.
173
+ - `reactNavigationIntegration({ navigationRef })` — records every route
174
+ change via `recordScreen`; also covers expo-router. Pass `txNav.onReady`
175
+ to the container so the initial route is recorded.
176
+
177
+ Using another navigator? Any stack is a ~5-line adapter over
178
+ `recordScreen` — the `useEverframeScreen` recipes above cover Wix RNN and
179
+ hand-rolled navigation, and a custom integration is just
180
+ `{ name, setup() { ...subscribe...; return unsubscribe } }`.
181
+
182
+ ## Session Vitals
183
+
184
+ Native capture again: every RN app automatically gets background CPU/memory
185
+ sampling and a session id stamped on reports and crashes as soon as the host
186
+ app's project has vitals enabled on the dashboard — no JS change required.
187
+
188
+ What JS adds is **player tracking**, because in RN the player instance lives
189
+ inside a native view and never reaches JS.
190
+
191
+ ### Config
192
+
193
+ ```tsx
194
+ <EverframeProvider config={{ apiKey, vitals: { enabled: true, sampleRate: 0.5, captureSourceQuery: false } }}>
195
+ ```
196
+
197
+ - `enabled` — absent follows the dashboard toggle; `false` opts out locally;
198
+ `true` never forces vitals on if the dashboard has them off.
199
+ - `sampleRate` — `[0, 1]`, `min()`'d with the server-configured rate.
200
+ - `captureSourceQuery` — keep the query string on `source_change.src`
201
+ (default `false`: signed CDN URLs carry tokens in the query).
202
+
203
+ **Reconfiguring.** A Provider remount with the *same* config does not restart
204
+ the SDK: both native sides compare the incoming config against the *installed*
205
+ one and skip the start entirely. A **changed** config does restart it — and a
206
+ start supersedes the running SDK, detaching every player integration it had
207
+ announced. **Limitation:** players tracked before that restart stay untracked
208
+ until their screens remount; nothing re-registers them automatically. Two React
209
+ instances in one process are *not* coordinated either: if a second one starts
210
+ the SDK under the first, the first instance's players stay untracked until its
211
+ own screens remount.
212
+
213
+ ### react-native-video (v7)
214
+
215
+ ```tsx
216
+ import { useVideoPlayer, VideoView } from 'react-native-video';
217
+ import { useVideoPlayerVitals } from '@everframe/react-native/integrations/react-native-video';
218
+
219
+ function Player() {
220
+ const player = useVideoPlayer(source);
221
+ useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });
222
+ return <VideoView player={player} />;
223
+ }
224
+ ```
225
+
226
+ `libraryVersion` is a host-supplied option — react-native-video does not
227
+ expose it at runtime.
228
+
229
+ **To measure INITIAL startup, defer the load:**
230
+
231
+ ```tsx
232
+ // Module scope: the latch belongs to the PLAYER, not to the component. A
233
+ // replacement instance (StrictMode's double-mount, a source change) is a
234
+ // different object and gets its own initialize; the same instance never
235
+ // gets a second one, which would restart playback under the user.
236
+ const initialised = new WeakSet<object>();
237
+
238
+ const player = useVideoPlayer({ uri: SOURCE, initializeOnCreation: false });
239
+ useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });
240
+
241
+ useEffect(() => {
242
+ if (initialised.has(player)) return;
243
+ initialised.add(player);
244
+ void player.initialize();
245
+ }, [player]);
246
+ ```
247
+
248
+ Why: on Android react-native-video v7 emits `onLoadStart` synchronously from
249
+ inside the native player constructor — before any effect runs, so before the
250
+ adapter has subscribed — and `onLoadStart` is what arms the adapter's startup
251
+ clock, so the first source's startup latency is simply never seen. Deferring
252
+ the load until after the adapter subscribed puts the first `onLoadStart` back
253
+ where it can be observed. Without this, everything else still works; only the
254
+ *first* `startup` measurement is missing (later source changes are fine).
255
+
256
+ Options: `name`, `libraryVersion`, and `captureErrors` (default `false` — see
257
+ Limitations). `onBandwidthUpdate` is read per platform: on iOS its `bitrate`
258
+ is the rendition's declared bitrate and drives `bitrate_change` plus the
259
+ `bitrate` stat; on Android it is ExoPlayer's bandwidth estimate, reported as
260
+ the `bandwidthEstimate` stat (with `quality_change` for the accompanying
261
+ rendition size) and never as a bitrate.
262
+
263
+ `onProgress` is read per platform too. The `bufferAheadMs` stat is always the
264
+ media time buffered *ahead of the playhead*: on Android `bufferDuration` is
265
+ already that, on iOS it is the buffered range's absolute end position, so the
266
+ adapter subtracts `currentTime` from it.
267
+
268
+ ### THEOplayer
269
+
270
+ ```tsx
271
+ import { attachTheoPlayerVitals } from '@everframe/react-native/integrations/theoplayer';
272
+
273
+ <THEOplayerView onPlayerReady={(player) => attachTheoPlayerVitals(player, { name: 'main' })} />
274
+ ```
275
+
276
+ Both adapters are pure translators: neither library is imported at runtime
277
+ or added as a dependency of this package.
278
+
279
+ ### Custom log lines
280
+
281
+ ```ts
282
+ import { trackVitals } from '@everframe/react-native';
283
+
284
+ trackVitals('ad_break', { pod: 1 });
285
+ ```
286
+
287
+ ### Any other player: `trackPlayer`
288
+
289
+ Libraries without an adapter above (or a player object outside a component
290
+ tree) can drive the same pipeline directly:
291
+
292
+ ```ts
293
+ import { trackPlayer } from '@everframe/react-native';
294
+
295
+ const handle = trackPlayer({ library: 'my-player', libraryVersion: '1.2.0', name: 'main' });
296
+ handle.emit('play');
297
+ handle.emit('buffer_start');
298
+ handle.emit('buffer_end', { durationMs: 800 });
299
+ handle.updateStats({ bufferAheadMs: 4200, bitrate: 2_500_000 });
300
+ handle.track('ad_break', { pod: 1 });
301
+ handle.detach();
302
+ ```
303
+
304
+ `useTrackPlayer(opts)` is the hook form — tracks on mount, detaches on
305
+ unmount, mints a fresh token on remount (Fast Refresh-safe).
306
+
307
+ Event vocabulary for `emit`: `source_change`, `drm`, `startup`, `play`,
308
+ `pause`, `buffer_start`, `buffer_end`, `seek`, `rate_change`,
309
+ `bitrate_change`, `quality_change`, `dropped_frames`, `error`, `stats`.
310
+ `player_attach` and `player_detach` are reserved for the native lifecycle
311
+ markers the registry emits itself — passing either to `emit` is silently
312
+ dropped, not forwarded.
313
+
314
+ ### Limitations
315
+
316
+ - **JS-thread stalls pause player events.** Startup timing, span
317
+ derivation and stats all happen in JS, so a wedged JS thread stalls the
318
+ player's vitals while it is wedged. Native CPU/memory sampling keeps
319
+ running regardless — it does not go through JS.
320
+ - **Dropped frames** are reported only where the underlying library exposes
321
+ them; the vocabulary has the slot, adapters fill it when they can.
322
+ - **The ten-per-minute non-fatal error budget counts forwarding attempts,
323
+ including ones native drops** — e.g. before vitals start, or against a
324
+ token whose registration was refused. `PlayerHandle.emit` is void
325
+ (fire-and-forget across the bridge), so JS cannot know whether native
326
+ admitted an entry; an error refused over there still spends a slot.
327
+ - **`captureErrors` changes react-native-video's own behaviour, so it is
328
+ off by default.** With it on, errors carry the library's `code`
329
+ (`'source/invalid-uri'`, …). But v7 only throws synchronously from
330
+ `play()` / `pause()` / `seekBy()` / `seekTo()` / `selectTextTrack()` /
331
+ `getAvailableTextTracks()` when NO `onError` listener is registered — so
332
+ merely subscribing makes those calls stop throwing for the whole app, and
333
+ a host relying on `try { player.play() } catch` would silently lose its
334
+ errors. Off by default, fatal errors are still reported (via
335
+ `onStatusChange('error')`), just without a `code`.
336
+ - **`captureErrors` keeps swallowing after detach.** react-native-video's
337
+ listener removal leaves an EMPTY `onError` listener set rather than no set
338
+ at all, and the library's "throw synchronously when nobody is listening"
339
+ check only asks whether the set exists. So once `captureErrors: true` has
340
+ been attached even once, that player keeps suppressing the synchronous
341
+ `play()` / `seek*()` throws for the rest of its lifetime — detaching the
342
+ vitals adapter does not give them back. A library bug, not an adapter one;
343
+ there is nothing to fix on our side, which is the second reason
344
+ `captureErrors` is off by default.
345
+ - **`rate_change` was not observed from react-native-video v7 on Android** in
346
+ our Android emulator smoke — `onPlaybackRateChange` never arrived there.
347
+ Note that the library *does* forward ExoPlayer's
348
+ `onPlaybackParametersChanged`, so this is a "not seen in our smoke run"
349
+ result, not a proven library gap; a host that changes playback speed
350
+ explicitly may well see it. Nothing on the adapter side gates it either way.
351
+ - **react-native-video v7 does not support tvOS.** As of `7.0.0-beta.11`
352
+ there is no tvOS podspec/target, so it cannot be used on Apple TV at all.
353
+ Use THEOplayer, or a custom `trackPlayer`/`PlayerHandle` adapter, on Apple
354
+ TV hosts.
355
+ - **A native react-native-video plugin** (attaching the real
356
+ ExoPlayer/AVPlayer instance directly, for full fidelity including dropped
357
+ frames) is a follow-up once this JS-forwarding seam has proven itself in
358
+ the field. Every library ends on ExoPlayer/AVPlayer, but JS event
359
+ forwarding is what works universally today.
360
+
361
+ ## TV Host Integration (Apple TV + Android TV)
362
+
363
+ `@everframe/react-native` supports Apple TV + Android TV when consumed
364
+ from a host app built against `react-native-tvos`. This support is developed
365
+ against RN-tvos `0.85.3-0`, Expo SDK `56.0.0-preview.7`, and React `19.2.5`;
366
+ the canonical example app below is kept building on that matrix.
367
+
368
+ ### Provider placement
369
+
370
+ `<EverframeProvider>` MUST sit ABOVE the TV focus engine's root in the
371
+ component tree. The reporter is presented imperatively by the native side
372
+ (via `TXTVReporterViewController` on iOS / `:everframe-tv` `ReporterActivity`
373
+ on Android), so React Native's focus engine never sees it — the native VC
374
+ manages focus on its own UIWindow / Activity.
375
+
376
+ ```tsx
377
+ <EverframeProvider config={{ apiKey }}>
378
+ <NavigationContainer>{/* focus engine root */}</NavigationContainer>
379
+ </EverframeProvider>
380
+ ```
381
+
382
+ ### Triggers are the host app's concern
383
+
384
+ Everframe installs shake-to-report on Android and iOS phones/tablets through the
385
+ native SDKs. It is enabled by default, requires no permission, safely no-ops on
386
+ Android devices without an accelerometer, and never runs on Android TV or tvOS.
387
+ The dashboard is authoritative: local `enabled: true` cannot override a
388
+ dashboard disable.
389
+
390
+ In React Native and Expo development builds, use
391
+ `shakeToReport: { enabled: !__DEV__ }` because the React Native development
392
+ menu also uses shake. Production remains enabled by default.
393
+
394
+ All other gestures and keys are host-owned. The host app
395
+ calls `useEverframe().open()` (or the top-level `open()`)
396
+ from whatever trigger makes sense for the target form factor.
397
+
398
+ For TV, the canonical pattern uses `TVEventHandler` (exposed by
399
+ `react-native-tvos`).
400
+
401
+ #### API shape (react-native-tvos@0.85.3-0)
402
+
403
+ ```ts
404
+ import { TVEventHandler } from 'react-native';
405
+
406
+ type HWEvent = {
407
+ eventType:
408
+ | 'menu' | 'playPause' | 'longPlayPause' | 'select'
409
+ | 'up' | 'down' | 'left' | 'right'
410
+ | 'longUp' | 'longDown' | 'longLeft' | 'longRight'
411
+ | 'pan' | string;
412
+ eventKeyAction?: -1 | 0 | 1 | number; // 0 = down, 1 = up, -1 = unknown
413
+ tag?: number;
414
+ body?: { state: 'Began' | 'Changed' | 'Ended'; x: number; y: number; velocityX: number; velocityY: number };
415
+ };
416
+
417
+ const subscription = TVEventHandler.addListener((evt: HWEvent) => { /* ... */ });
418
+ subscription?.remove();
419
+ ```
420
+
421
+ > If you have seen the older `new TVEventHandler(); handler.enable(cmp, cb);
422
+ > handler.disable()` shape elsewhere — that class-based API was removed in
423
+ > RN-tvos 0.85.x. Always read the version installed in your `node_modules`.
424
+
425
+ #### Apple TV: long-press Play/Pause
426
+
427
+ ```tsx
428
+ import { Platform, TVEventHandler } from 'react-native';
429
+ import { useEverframe } from '@everframe/react-native';
430
+ import { useEffect } from 'react';
431
+
432
+ function useAppleTVReporterTrigger() {
433
+ const { open } = useEverframe();
434
+ useEffect(() => {
435
+ if (!(Platform.isTV && Platform.OS === 'ios')) return;
436
+ const sub = TVEventHandler.addListener((evt) => {
437
+ if (evt.eventType !== 'longPlayPause') return;
438
+ if (Number(evt.eventKeyAction) !== 1) return; // key-up only
439
+ void open();
440
+ });
441
+ return () => sub?.remove();
442
+ }, [open]);
443
+ }
444
+ ```
445
+
446
+ > **Why `longPlayPause` instead of `menu`?** `.menu` is reserved by Apple as
447
+ > the system back-navigation gesture on the Siri Remote — apps that bind it
448
+ > for a non-navigation purpose are App Store-rejected. Everframe's iOS SDK
449
+ > enforces this via `ReservedKeysValidator`; the same constraint applies on
450
+ > RN. `longPlayPause` is a native long-press event emitted by the OS
451
+ > (no JS timing required).
452
+
453
+ #### Android TV: KEYCODE_MENU
454
+
455
+ ```tsx
456
+ import { Platform, TVEventHandler } from 'react-native';
457
+ import { useEverframe } from '@everframe/react-native';
458
+ import { useEffect } from 'react';
459
+
460
+ function useAndroidTVReporterTrigger() {
461
+ const { open } = useEverframe();
462
+ useEffect(() => {
463
+ if (!(Platform.isTV && Platform.OS === 'android')) return;
464
+ const sub = TVEventHandler.addListener((evt) => {
465
+ if (evt.eventType !== 'menu') return;
466
+ if (Number(evt.eventKeyAction) !== 1) return; // key-up only
467
+ void open();
468
+ });
469
+ return () => sub?.remove();
470
+ }, [open]);
471
+ }
472
+ ```
473
+
474
+ `KEYCODE_MENU` is mapped to `eventType === 'menu'` by
475
+ `ReactAndroidHWInputDeviceHelper` (RN-tvos). It is the conventional
476
+ "settings / debug menu" key on Android TV remotes.
477
+
478
+ ### Canonical example
479
+
480
+ The dogfood sample app at
481
+ [`examples/react-native/src/screens/Home.tsx`](https://github.com/scriptx-com/everframe/tree/main/examples/react-native/src/screens/Home.tsx)
482
+ implements both recipes side-by-side, gated by `Platform.isTV` +
483
+ `Platform.OS`. Cloned from the repo, build it with:
484
+
485
+ ```bash
486
+ pnpm --filter examples-react-native ios:tv # Apple TV simulator
487
+ pnpm --filter examples-react-native android:tv # Android TV emulator
488
+ ```
489
+
490
+ That example's own README covers prebuild details and the known pitfalls of
491
+ the TV targets.
492
+
493
+ ### What about the floating bubble?
494
+
495
+ The iPhone floating-bubble overlay shipped in Phase 04 is NOT exposed via
496
+ the RN bridge today (and would not make sense on TV anyway — the focus
497
+ engine is the input model, not a touch-positioned overlay). On phone RN
498
+ hosts, add your own host-level button. On TV hosts, use the remote
499
+ recipes above.
500
+
501
+ ---
502
+
503
+ ## License
504
+
505
+ MIT