@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,489 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 ScriptX
3
+ //
4
+ // ObjC++ TurboModule for @everframe/react-native (iOS). Conforms to the
5
+ // codegen-generated `NativeEverframeSpec` protocol; all method bodies dispatch
6
+ // to the Swift glue layer (`EverframeBridge` in `EverframeBridge.swift`).
7
+ //
8
+ // Threading (RESEARCH §2.5):
9
+ // • configure — JS thread (synchronous; <5ms per sdk-ios contract)
10
+ // • captureNow — main queue (UIKit reads, capture, accessibility walk)
11
+ // • registerSensitiveRect — main queue (RCTUIManager view lookup)
12
+ // • submit — background OK (no UIKit reads); bridge runs it
13
+ // on a detached Task internally
14
+ // • openAnnotationOverlay — main queue (UI presentation)
15
+ //
16
+ // JS-side name binding: `RCT_EXPORT_MODULE(Everframe)` registers under the
17
+ // name 'Everframe', matching `TurboModuleRegistry.getEnforcing<Spec>('Everframe')`
18
+ // in `src/NativeEverframe.ts`.
19
+ #import "EverframeModule.h"
20
+
21
+ #import <React/RCTBridge.h>
22
+ #import <React/RCTInvalidating.h>
23
+ #import <React/RCTLog.h>
24
+ #import <React/RCTUIManager.h>
25
+ #import <React/RCTUtils.h>
26
+
27
+ // `<RCTInvalidating>` is declared HERE, in the private class extension below,
28
+ // not in EverframeModule.h — same reason `<NativeEverframeSpec>` is: the public
29
+ // header is re-exported through the framework umbrella and we keep its import
30
+ // surface minimal. RCTTurboModuleManager's `_invalidateModules` dispatches on
31
+ // `respondsToSelector:@selector(invalidate)` and only then casts to
32
+ // `id<RCTInvalidating>`, so a class-extension conformance (which is merged
33
+ // into the class's protocol list at runtime) is exactly what it needs.
34
+
35
+ // Codegen-generated `<NativeEverframeSpec>` protocol. Importing this header
36
+ // requires ObjC++ (the file itself has `#error This file must be compiled
37
+ // as Obj-C++`). It lives ONLY in this .mm — never in EverframeModule.h —
38
+ // because the public header is re-exported through the framework umbrella
39
+ // which is processed in plain-ObjC context (see EverframeModule.h header
40
+ // comment for the full rationale).
41
+ #if __has_include(<EverframeSpec/EverframeSpec.h>)
42
+ #import <EverframeSpec/EverframeSpec.h>
43
+ @interface EverframeModule () <NativeEverframeSpec, RCTInvalidating>
44
+ @end
45
+ #else
46
+ @interface EverframeModule () <RCTInvalidating>
47
+ @end
48
+ #endif
49
+
50
+ // Generated by Xcode from the Swift module name (set via podspec `s.name`).
51
+ // We expose the Swift `EverframeBridge` class to ObjC++ via @objc public.
52
+ // Module name is `EverframeRN` (NOT `Everframe`) — see EverframeRN.podspec
53
+ // header comment for the rationale (collision with sdk-ios SwiftPM product).
54
+ //
55
+ // Use the framework form `<EverframeRN/EverframeRN-Swift.h>` when the pod is
56
+ // built as a framework (use_frameworks! :linkage => :dynamic — our case),
57
+ // and fall back to the same-module quoted form for static-lib builds. The
58
+ // `__has_include` guard means a single source file works under either
59
+ // linkage mode, mirroring react-native-screens' Swift-Bridging.h pattern.
60
+ #if __has_include(<EverframeRN/EverframeRN-Swift.h>)
61
+ #import <EverframeRN/EverframeRN-Swift.h>
62
+ #else
63
+ #import "EverframeRN-Swift.h"
64
+ #endif
65
+
66
+ @implementation EverframeModule {
67
+ // Strong ref to the bridge so we can look up RCTUIManager later.
68
+ __weak RCTBridge *_bridge;
69
+ // Session Vitals, owned PER MODULE INSTANCE (codex round-1, C4). It was a
70
+ // process-wide `static` registry on the Swift `EverframeBridge`; React Native
71
+ // does not guarantee one module instance, so that made two React instances
72
+ // share one token space (the second instance's `rp1` was silently refused)
73
+ // and let either one's `-invalidate` tear down the other's players. Android's
74
+ // module has always held its registry as an instance field; this matches it.
75
+ RemoteVitalsBridge *_vitals;
76
+ // D1 fix (stale-teardown identity guard, spec 2026-09-17 setExtra-resolver).
77
+ // Snapshot of `[EverframeBridge currentExtraResolveHookGeneration]` taken
78
+ // right after THIS instance's most recent `-configure:` call — see
79
+ // `EverframeBridge.resetExtraResolverForTeardown(_:)`'s doc comment for why
80
+ // `-invalidate` must hand this back rather than tearing down unconditionally.
81
+ // Zero-initialized by ObjC, matching `resetExtraResolverForTeardown(_:)`'s
82
+ // "0 = never installed" no-op case for an instance whose `-configure:` was
83
+ // never called (or never reached the install site) before `-invalidate`.
84
+ NSInteger _extraResolveHookGeneration;
85
+ }
86
+
87
+ RCT_EXPORT_MODULE(Everframe)
88
+
89
+ - (instancetype)init {
90
+ if (self = [super init]) {
91
+ // Cheap: the registry allocates one lock and an empty map, nothing more —
92
+ // which is also why `+requiresMainQueueSetup` can stay NO.
93
+ _vitals = [RemoteVitalsBridge new];
94
+ }
95
+ return self;
96
+ }
97
+
98
+ + (BOOL)requiresMainQueueSetup {
99
+ // Module construction is cheap (Swift Bridge has no expensive init);
100
+ // returning NO keeps RN's TurboModule cold-start fast. The methods that
101
+ // need the main thread dispatch explicitly below.
102
+ return NO;
103
+ }
104
+
105
+ - (void)setBridge:(RCTBridge *)bridge {
106
+ _bridge = bridge;
107
+ }
108
+
109
+ // MARK: - configure(opts)
110
+
111
+ // Signature MUST match the codegen-emitted protocol in EverframeSpec.h:
112
+ // - (void)configure:(JS::NativeEverframe::ConfigOpts &)opts;
113
+ // Field accessors (.apiKey(), .sdkVersion(), .captureScreenshot(),
114
+ // .networkBodiesDisabled()) are inline methods on the JS::NativeEverframe::
115
+ // ConfigOpts struct, generated from the TS spec in src/NativeEverframe.ts.
116
+ // The ingest URL is no longer JS-supplied — the native SDK bakes it at
117
+ // compile time via IngestEndpoint.url.
118
+ //
119
+ // NOTE (pre-existing, out of scope here): `captureScreenshot` is populated
120
+ // into ConfigOpts by extractBridgeConfig() but is never read below, so it is
121
+ // silently dropped on iOS today. Task 9 (spec 2026-08-12) only wires
122
+ // `networkBodiesDisabled` through; see task-9-report.md.
123
+ - (void)configure:(JS::NativeEverframe::ConfigOpts &)opts {
124
+ NSString *appId = opts.apiKey() ?: @"";
125
+ BOOL networkBodiesDisabled = opts.networkBodiesDisabled().value_or(false);
126
+ // `installIdentifierDisabled` (MAI meter spec 2026-08-27) — `std::optional
127
+ // <bool>`, same shape as `networkBodiesDisabled()` above. Absent means the
128
+ // host said nothing, which is "send it": the SDK default is ON.
129
+ BOOL installIdentifierDisabled = opts.installIdentifierDisabled().value_or(false);
130
+ // `attachPinUi` (spec 2026-08-19) is a plain optional STRING field —
131
+ // `opts.attachPinUi()` returns a nullable NSString*, same shape as
132
+ // `opts.apiKey()` above, NOT the `std::optional<bool>` shape
133
+ // `networkBodiesDisabled()` uses. `EverframeBridge.configure` coerces a
134
+ // nil/unrecognised value to `.builtin`.
135
+ NSString *attachPinUi = opts.attachPinUi();
136
+ // `companionDeviceId` (naming spec 2026-08-24) is a plain optional STRING
137
+ // field — same nullable-NSString* shape as `attachPinUi` above, not the
138
+ // `std::optional<bool>` shape `networkBodiesDisabled()` uses.
139
+ NSString *companionDeviceId = opts.companionDeviceId();
140
+ // `companionBadgeEnabled`/`companionBadgePosition` (naming spec
141
+ // 2026-08-24, controller ruling / Task 6b) — flat fields on ConfigOpts;
142
+ // RN codegen cannot express the nested `{enabled, position}` shape the JS
143
+ // facade's `RuntimeConfig.companionBadge` uses, so `extractBridgeConfig`
144
+ // flattens it before it crosses. `companionBadgeEnabled()` is
145
+ // `std::optional<bool>`, same shape as `networkBodiesDisabled()` above;
146
+ // absent defaults to `true` (badge shown), matching
147
+ // `CompanionBadgeOptions.enabled`'s own default. `companionBadgePosition()`
148
+ // is a nullable NSString*, same shape as `attachPinUi`/`companionDeviceId`
149
+ // above; `EverframeBridge` coerces nil/unrecognised to `.bottomRight`.
150
+ BOOL companionBadgeEnabled = opts.companionBadgeEnabled().value_or(true);
151
+ BOOL shakeToReportEnabled = opts.shakeToReportEnabled().value_or(true);
152
+ NSString *companionBadgePosition = opts.companionBadgePosition();
153
+ // Inline reporter theme (reporter branding spec 2026-08-25, RN slice) —
154
+ // 8 flat optional STRING fields on ConfigOpts (RN codegen cannot express
155
+ // the nested `theme: { … }` shape the JS facade's `RuntimeConfig.theme`
156
+ // uses; `extractBridgeConfig` flattens it before it crosses). Collapsed
157
+ // here into ONE role-keyed dictionary so the Swift seam gains a single
158
+ // `theme:` parameter instead of eight — nil when the host set no theme,
159
+ // preserving "host did nothing" through to `EverframeConfig.theme`.
160
+ // Values cross unvalidated; the sdk-ios ThemeResolver revalidates hex
161
+ // per-field at presentation time.
162
+ NSMutableDictionary<NSString *, NSString *> *theme = [NSMutableDictionary new];
163
+ if (opts.themeBackground()) theme[@"background"] = opts.themeBackground();
164
+ if (opts.themeSurface()) theme[@"surface"] = opts.themeSurface();
165
+ if (opts.themeBorder()) theme[@"border"] = opts.themeBorder();
166
+ if (opts.themeText()) theme[@"text"] = opts.themeText();
167
+ if (opts.themeTextMuted()) theme[@"textMuted"] = opts.themeTextMuted();
168
+ if (opts.themeAccent()) theme[@"accent"] = opts.themeAccent();
169
+ if (opts.themeAccentForeground()) theme[@"accentForeground"] = opts.themeAccentForeground();
170
+ if (opts.themeDestructive()) theme[@"destructive"] = opts.themeDestructive();
171
+ // Session Vitals (spec 2026-09-06) — three flat optional fields on
172
+ // ConfigOpts (RN codegen cannot express the nested `vitals` object the JS
173
+ // facade uses; `extractBridgeConfig` flattens it before it crosses).
174
+ // `std::optional<bool>` / `std::optional<double>` shapes, same as
175
+ // `networkBodiesDisabled()` above — but forwarded as NSNumber* (nil =
176
+ // absent) rather than collapsed with `.value_or(...)`, because the Swift
177
+ // seam MUST be able to tell "absent" from "false": absent `vitalsEnabled`
178
+ // means "follow the dashboard toggle", while `false` is a local opt-out.
179
+ NSNumber *vitalsEnabled = opts.vitalsEnabled().has_value() ? @(opts.vitalsEnabled().value()) : nil;
180
+ NSNumber *vitalsSampleRate = opts.vitalsSampleRate().has_value() ? @(opts.vitalsSampleRate().value()) : nil;
181
+ NSNumber *vitalsCaptureSourceQuery = opts.vitalsCaptureSourceQuery().has_value() ? @(opts.vitalsCaptureSourceQuery().value()) : nil;
182
+ NSError *err = nil;
183
+ [EverframeBridge configureWithAppId:appId
184
+ endpoint:@""
185
+ networkBodiesDisabled:networkBodiesDisabled
186
+ installIdentifierDisabled:installIdentifierDisabled
187
+ attachPinUi:attachPinUi
188
+ companionDeviceId:companionDeviceId
189
+ companionBadgeEnabled:companionBadgeEnabled
190
+ shakeToReportEnabled:shakeToReportEnabled
191
+ companionBadgePosition:companionBadgePosition
192
+ theme:(theme.count > 0 ? theme : nil)
193
+ vitalsEnabled:vitalsEnabled
194
+ vitalsSampleRate:vitalsSampleRate
195
+ vitalsCaptureSourceQuery:vitalsCaptureSourceQuery
196
+ error:&err];
197
+ if (err != nil) {
198
+ // Configure is void-typed at the spec; we can't reject a promise here.
199
+ RCTLogWarn(@"[everframe] configure failed: %@", err.localizedDescription);
200
+ }
201
+ // D1 fix (stale-teardown identity guard, spec 2026-09-17 setExtra-resolver).
202
+ // Capture whichever installation is live right now — regardless of whether
203
+ // `err` fired above — and stash it on this instance for `-invalidate` to
204
+ // hand back to `resetExtraResolverForTeardown:`. See that method's doc
205
+ // comment on EverframeBridge.swift for the race this closes.
206
+ _extraResolveHookGeneration = [EverframeBridge currentExtraResolveHookGeneration];
207
+ // Codex round-5 — nothing re-registers the live players here any more. `renewAll()` raced
208
+ // the runtime's deferred-registration drain (it could detach the replacement it had just
209
+ // installed) and only ever reached THIS module instance's registry. The replacement is two
210
+ // parts, neither of them native: `EverframeBridge.configure` SKIPS the restart entirely when
211
+ // the effective options are unchanged and the SDK is already started, and when it does
212
+ // restart, the JS `configureEpoch` bump re-runs every vitals hook effect — each one
213
+ // detaching its old token and tracking a fresh one against the SDK that is now current.
214
+ }
215
+
216
+ // Additive synchronous startup entry. The legacy void selector above stays
217
+ // ABI-compatible for already-compiled callers; current JS uses this boolean
218
+ // method so its first synchronous capture cannot overtake configure dispatch.
219
+ - (NSNumber *)configureSync:(JS::NativeEverframe::ConfigOpts &)opts {
220
+ [self configure:opts];
221
+ return @YES;
222
+ }
223
+
224
+ // MARK: - openReporter() -> Promise<object>
225
+
226
+ // Opens the native TXReporterPresenter modal. Resolves with
227
+ // { status: "submitted" | "queued" | "cancelled", reportId?: <uuid> }
228
+ - (void)openReporter:(RCTPromiseResolveBlock)resolve
229
+ reject:(RCTPromiseRejectBlock)reject {
230
+ [EverframeBridge openReporterWithCompletion:^(NSDictionary *result, NSError *err) {
231
+ if (err) {
232
+ reject(@"open_reporter_failed", err.localizedDescription, err);
233
+ } else {
234
+ resolve(result ?: @{});
235
+ }
236
+ }];
237
+ }
238
+
239
+ // MARK: - registerSensitiveRect(viewTag, rect)
240
+
241
+ // `viewTag` is `double` per RN codegen rules (numbers cross as double).
242
+ // `rect` is the codegen-typed JS::NativeEverframe::Rect — informational only;
243
+ // the native registry resolves the view's actual frame at capture time.
244
+ // We intentionally don't read .x()/.y()/.width()/.height() here — they're
245
+ // available if a future iteration needs hint-based registration.
246
+ - (void)registerSensitiveRect:(double)viewTag
247
+ rect:(JS::NativeEverframe::Rect &)rect {
248
+ __weak RCTBridge *weakBridge = _bridge;
249
+ NSNumber *tag = @((NSInteger)viewTag);
250
+ dispatch_async(dispatch_get_main_queue(), ^{
251
+ RCTBridge *strongBridge = weakBridge;
252
+ UIView *view = nil;
253
+ if (strongBridge != nil) {
254
+ view = [strongBridge.uiManager viewForReactTag:tag];
255
+ }
256
+ // Bridge does a nil-check + warn-once (Pitfall P5 / T-06-02-03).
257
+ [EverframeBridge markSensitiveViewOrWarn:view];
258
+ });
259
+ }
260
+
261
+ // MARK: - setExtra(map)
262
+
263
+ - (void)setExtra:(NSString *)value {
264
+ [EverframeBridge setExtra:(value ?: @"")];
265
+ }
266
+
267
+ // MARK: - setExtraResolverActive / signalExtraResolverReady (spec 2026-09-17 setExtra-resolver)
268
+
269
+ // `active` is a REQUIRED boolean at the spec (not `active?: boolean`) —
270
+ // codegen maps that to a plain `BOOL` parameter, same shape as
271
+ // `recordPlayerEvent`'s required `t: number` → `double t` below. See
272
+ // `NativeEverframe.ts`'s `Spec.setExtraResolverActive` doc comment for the
273
+ // full contract: a presence bit only, never the resolved value itself.
274
+ - (void)setExtraResolverActive:(BOOL)active {
275
+ [EverframeBridge setExtraResolverActive:active];
276
+ }
277
+
278
+ // JS handshake ack — mirrors `signalCompanionReportRequestReady` below
279
+ // exactly. Releases `EverframeBridge.awaitJsExtraResolve`'s bounded wait for
280
+ // this correlation id.
281
+ - (void)signalExtraResolverReady:(NSString *)correlationId {
282
+ [EverframeBridge signalExtraResolverReady:(correlationId ?: @"")];
283
+ }
284
+
285
+ // MARK: - addBreadcrumb(message, kind?, level?, data?) — Plan 4 / Task 14
286
+
287
+ // `kind`/`level`/`data` are the codegen-optional (`?`) params — nullable at
288
+ // the ObjC boundary, forwarded as-is (no coercion here; EverframeBridge
289
+ // forwards straight to Everframe.shared.addBreadcrumb, which owns all
290
+ // coercion — Task 5). `data` is the codegen `UnsafeObject` freeform-object
291
+ // escape hatch, crossing as `id`.
292
+ - (void)addBreadcrumb:(NSString *)message
293
+ kind:(NSString *)kind
294
+ level:(NSString *)level
295
+ data:(id)data {
296
+ NSDictionary *dataDict = [data isKindOfClass:[NSDictionary class]] ? (NSDictionary *)data : nil;
297
+ [EverframeBridge addBreadcrumb:(message ?: @"")
298
+ kind:kind
299
+ level:level
300
+ data:dataDict];
301
+ }
302
+
303
+ // MARK: - recordScreen(name, data?) — screen markers (spec 2026-07-14)
304
+
305
+ // Navigation screen marker. Forwarded as-is; EverframeBridge → Everframe
306
+ // .shared.recordScreen owns from→to derivation (global chain shared with
307
+ // the viewDidAppear auto-capture), gating, and coercion. `data` is the
308
+ // codegen `UnsafeObject` escape hatch, crossing as `id`.
309
+ - (void)recordScreen:(NSString *)name data:(id)data {
310
+ NSDictionary *dataDict = [data isKindOfClass:[NSDictionary class]] ? (NSDictionary *)data : nil;
311
+ [EverframeBridge recordScreen:(name ?: @"") data:dataDict];
312
+ }
313
+
314
+ // MARK: - Session Vitals (spec 2026-09-06) — five void, fire-and-forget methods
315
+
316
+ // Pure forwarding, no validation: `_vitals`'s `RemotePlayerRegistry` (one per
317
+ // module instance since codex round-1 C4) owns the token map, the 32-token cap and the epoch-ms sanity check, and
318
+ // every one of its methods is a silent no-op for an unknown token — a bridge
319
+ // must never throw into a host. `data`/`stats` are the spec's `UnsafeObject`
320
+ // escape hatch; codegen declares them `NSDictionary *`, but we take `id` and
321
+ // class-check — exactly what `addBreadcrumb`/`recordScreen` above do, since
322
+ // nothing enforces the declared type at the JSI boundary and a non-dictionary
323
+ // payload must be treated as absent rather than crash on a bad selector. `t`
324
+ // is a REQUIRED double (the spec types it `t: number`, not `t?: number` — an
325
+ // event with no timestamp is unorderable, so JS always supplies one).
326
+
327
+ - (void)trackPlayer:(NSString *)token
328
+ library:(NSString *)library
329
+ name:(NSString *)name
330
+ libraryVersion:(NSString *)libraryVersion {
331
+ [_vitals trackPlayer:(token ?: @"")
332
+ library:(library ?: @"")
333
+ name:name
334
+ libraryVersion:libraryVersion];
335
+ }
336
+
337
+ - (void)detachPlayer:(NSString *)token {
338
+ [_vitals detachPlayer:(token ?: @"")];
339
+ }
340
+
341
+ - (void)recordPlayerEvent:(NSString *)token
342
+ type:(NSString *)type
343
+ t:(double)t
344
+ data:(id)data {
345
+ NSDictionary *dataDict = [data isKindOfClass:[NSDictionary class]] ? (NSDictionary *)data : nil;
346
+ [_vitals recordPlayerEvent:(token ?: @"") type:(type ?: @"") t:t data:dataDict];
347
+ }
348
+
349
+ // `stats` is REQUIRED at the spec (`stats: UnsafeObject`, no `?`), but the
350
+ // class-check still matters: a non-dictionary value is DROPPED here rather
351
+ // than passed on as an empty snapshot, which would clobber the player's live
352
+ // stats with zeros.
353
+ - (void)updatePlayerStats:(NSString *)token stats:(id)stats {
354
+ NSDictionary *statsDict = [stats isKindOfClass:[NSDictionary class]] ? (NSDictionary *)stats : nil;
355
+ if (statsDict == nil) return;
356
+ [_vitals updatePlayerStats:(token ?: @"") stats:statsDict];
357
+ }
358
+
359
+ // `dataJson` is a pre-serialised JSON STRING, not UnsafeObject — custom vitals
360
+ // lines carry scalars and arrays that a codegen object type cannot express.
361
+ // Unparseable JSON is recorded WITHOUT data by the registry, never dropped.
362
+ - (void)trackVitals:(NSString *)name dataJson:(NSString *)dataJson token:(NSString *)token {
363
+ [_vitals trackVitals:(name ?: @"") dataJson:dataJson token:token];
364
+ }
365
+
366
+ // Module-instance teardown (RN tears the instance down on a Metro/OTA reload,
367
+ // and on host shutdown). Detach every player registered through THIS INSTANCE
368
+ // so their play/buffer spans close natively — since codex round-1 C1 the
369
+ // integration's own `detach()` emits the closing `buffer_end`/`pause`, so the
370
+ // spans really close instead of accruing to the end of the session — and so
371
+ // the reloaded bundle's re-minted `rp1` is not shadowed by the dead one.
372
+ //
373
+ // LIFETIME (rewritten for C4): the registry is now PER MODULE INSTANCE, exactly
374
+ // like Android's, so this teardown touches only the players this instance
375
+ // registered. It no longer reaches a process-wide map, which is what let one
376
+ // React instance detach another's players. The teardown is terminal — the
377
+ // reload builds a NEW module instance with a NEW registry for the new bundle,
378
+ // so nothing legitimate can still be calling the closed one.
379
+ //
380
+ // NOT done in `configure:`: a screen's `useTrackPlayer` effect runs BEFORE the
381
+ // Provider's configure-on-mount, so clearing there would drop legitimate
382
+ // registrations that had already landed.
383
+ - (void)invalidate {
384
+ [_vitals reset];
385
+ // Review finding F4 (spec 2026-09-17 setExtra-resolver) — see
386
+ // `EverframeBridge.resetExtraResolverForTeardown`'s doc comment for why
387
+ // this must run here: without it, a JS-context teardown that skips
388
+ // `runtime.unmount()` leaves the extra-resolver presence flag stuck
389
+ // true, so every later report (including pure-native ones) pays the
390
+ // full 250ms ask-and-wait for an ack nobody will ever send.
391
+ //
392
+ // D1 fix: `_extraResolveHookGeneration` (captured at the end of
393
+ // `-configure:`) travels with this instance so the Swift side can tell a
394
+ // STALE instance's `-invalidate` — delivered late, on this module's
395
+ // method queue, after a reloaded bundle's fresh instance already
396
+ // reconfigured — from the CURRENT one, and no-op the former. See
397
+ // `EverframeBridge.resetExtraResolverForTeardown(_:)`'s doc comment for
398
+ // the full race this closes.
399
+ [EverframeBridge resetExtraResolverForTeardown:_extraResolveHookGeneration];
400
+ }
401
+
402
+ // MARK: - setUser(user?) — self-declared identity (spec 2026-08-12)
403
+
404
+ // The spec types this bridge parameter `UnsafeObject`, NOT the named
405
+ // `EverframeUserSpec` alias — codegen's ObjC generator ignores `?` optionality on a
406
+ // named struct-alias param (TypeAliasTypeAnnotation) and always emits a
407
+ // REQUIRED `JS::NativeEverframe::EverframeUserSpec &` reference, which would make
408
+ // "no argument = clear" unrepresentable. `UnsafeObject` resolves to
409
+ // GenericObjectTypeAnnotation, which DOES honour `?` and generates the
410
+ // nullable `(NSDictionary *)user` protocol signature — verified against
411
+ // this repo's installed `@react-native/codegen` (see NativeEverframe.ts's
412
+ // file header for the full writeup). `(id)` here matches `addBreadcrumb`'s
413
+ // `data` param below, which crosses the same GenericObjectTypeAnnotation
414
+ // shape; `id` is compatible with the generated `NSDictionary *` protocol
415
+ // type. `EverframeBridge` re-validates and pulls out id/email/displayName
416
+ // defensively.
417
+ - (void)setUser:(id)user {
418
+ NSDictionary *dict = [user isKindOfClass:[NSDictionary class]] ? (NSDictionary *)user : nil;
419
+ [EverframeBridge setUser:dict];
420
+ }
421
+
422
+ // MARK: - reportCrash(crashJson) — Task 13 (spec 2026-07-18)
423
+
424
+ // Sync spec method: codegen emits a non-void, non-promise boolean-returning
425
+ // signature as `NSNumber *` (RN's ObjC bridge convention for a boxed bool
426
+ // return). The RN fatal path calls this synchronously before RN's default
427
+ // handler aborts the process, so persistence must complete before returning.
428
+ - (NSNumber *)reportCrash:(NSString *)crashJson {
429
+ return @([EverframeBridge reportCrash:(crashJson ?: @"")]);
430
+ }
431
+
432
+ - (NSNumber *)captureHandledException:(NSString *)crashJson {
433
+ return @([EverframeBridge captureHandledException:(crashJson ?: @"")]);
434
+ }
435
+
436
+ // MARK: - startCompanion / stopCompanion (Plan 06.2-11)
437
+
438
+ // Opens a relay WS connection against the build-time-baked ingest URL
439
+ // (sdk-ios IngestEndpoint.url) and starts forwarding `Everframe.shared
440
+ // .companion` signals (state + pairUrl) to JS via the
441
+ // `EverframeEventEmitter` RCTEventEmitter subclass.
442
+ - (void)startCompanion {
443
+ [EverframeBridge startCompanion:@""];
444
+ }
445
+
446
+ - (void)stopCompanion {
447
+ [EverframeBridge stopCompanion];
448
+ }
449
+
450
+ // MARK: - signalCompanionReportRequestReady(correlationId)
451
+
452
+ // Companion handshake: JS calls this when it is ready for capture to
453
+ // proceed, releasing the native bridge's bounded wait
454
+ // (CompanionCaptureBridge.awaitJsReactTreeAttach).
455
+ - (void)signalCompanionReportRequestReady:(NSString *)correlationId {
456
+ [EverframeBridge signalCompanionReportRequestReady:(correlationId ?: @"")];
457
+ }
458
+
459
+ // MARK: - addListener / removeListeners (RN NativeEventEmitter contract)
460
+ //
461
+ // These are intentional no-ops on `EverframeModule` — the JS facade
462
+ // (`src/companion.ts`) constructs `NativeEventEmitter(NativeModules
463
+ // .EverframeEventEmitter)`, NOT NativeModules.Everframe, so the real
464
+ // addListener/removeListeners traffic flows to the `EverframeEventEmitter`
465
+ // subclass via RCTEventEmitter's built-in implementations. We still need
466
+ // these stubs on `Everframe` because RN's TurboModule codegen requires every
467
+ // declared spec method to have a backing implementation, and the JS facade
468
+ // keeps `addListener`/`removeListeners` on the Spec so a future migration
469
+ // to a single-module emitter (Android already does this) wouldn't require
470
+ // a spec change.
471
+
472
+ - (void)addListener:(NSString *)eventName {
473
+ // no-op — see comment above.
474
+ }
475
+
476
+ - (void)removeListeners:(double)count {
477
+ // no-op — see comment above.
478
+ }
479
+
480
+ #if __has_include(<EverframeSpec/EverframeSpec.h>)
481
+ // TurboModule JSI factory — registers the C++ spec impl with the JS runtime.
482
+ // Available only once codegen has materialized `EverframeSpec`.
483
+ - (std::shared_ptr<facebook::react::TurboModule>)
484
+ getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
485
+ return std::make_shared<facebook::react::NativeEverframeSpecJSI>(params);
486
+ }
487
+ #endif
488
+
489
+ @end
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 ScriptX
3
+ //
4
+ // Per-module-instance Session Vitals bridge (spec 2026-09-06; codex round-1, C4).
5
+ //
6
+ // WHY THIS EXISTS. The five vitals shims used to hang off `EverframeBridge` as
7
+ // `static` funcs over a `private static let remotePlayers` registry — one map
8
+ // for the whole PROCESS. React Native does not guarantee one module instance:
9
+ // a host can run several React instances (a brownfield app embedding two
10
+ // surfaces, and RN's own reload path, which builds the new instance BEFORE it
11
+ // invalidates the old one). With a process-wide registry those instances share
12
+ // one token space, so instance B's `rp1` collides with instance A's still-live
13
+ // `rp1` (the second `trackPlayer` is silently refused, and that player's whole
14
+ // session goes unmeasured), and — worse — B's `invalidate` runs `detachAll()`
15
+ // over A's registrations, tearing down players A is still using.
16
+ //
17
+ // The Android module has never had this problem: its registry is a plain
18
+ // instance field on the module (`private val remotePlayers`), so each instance
19
+ // owns its own token space and its own teardown. This is the same shape.
20
+ //
21
+ // LIFETIME: one `RemoteVitalsBridge` per `EverframeModule` instance, created at
22
+ // module init and released with it. `reset()` runs from `-[EverframeModule
23
+ // invalidate]`, i.e. on module-instance teardown, which is exactly what a
24
+ // Metro/OTA reload does; it detaches ONLY this instance's players. That
25
+ // teardown is terminal — see `RemotePlayerRegistry.detachAll()` — because the
26
+ // reload constructs a NEW module instance with a NEW registry for the new
27
+ // bundle. A bare `Everframe.shared` restart does NOT reach here: the module
28
+ // instance survives it, and the JS hooks still own detach for every player
29
+ // they registered.
30
+ //
31
+ // This class is GLUE — no validation lives here: unknown tokens, out-of-range
32
+ // timestamps and the 32-token cap are all the registry's business, and every
33
+ // one of its methods is a silent no-op rather than a throw.
34
+ import Foundation
35
+ import EverframeKit
36
+
37
+ @objc public final class RemoteVitalsBridge: NSObject {
38
+
39
+ /// `captureSourceQuery` is a PROVIDER closure resolved at attach time (I13), never a
40
+ /// value read once at `configure()` — a host that reconfigures mid-session must get the
41
+ /// new answer for players attached afterwards.
42
+ private let remotePlayers = RemotePlayerRegistry(
43
+ register: { integration, name in Everframe.shared.trackPlayer(integration, name: name) },
44
+ sessionTrack: { name, data in Everframe.shared.trackVitals(name, data: data, player: nil) },
45
+ captureSourceQuery: { Everframe.shared.currentConfig?.vitals.captureSourceQuery ?? false })
46
+
47
+ @objc public func trackPlayer(
48
+ _ token: NSString,
49
+ library: NSString,
50
+ name: NSString?,
51
+ libraryVersion: NSString?
52
+ ) {
53
+ remotePlayers.track(
54
+ token: token as String,
55
+ library: library as String,
56
+ name: name as String?,
57
+ libraryVersion: libraryVersion as String?
58
+ )
59
+ }
60
+
61
+ @objc public func detachPlayer(_ token: NSString) {
62
+ remotePlayers.detach(token: token as String)
63
+ }
64
+
65
+ @objc public func recordPlayerEvent(
66
+ _ token: NSString,
67
+ type: NSString,
68
+ t: Double,
69
+ data: NSDictionary?
70
+ ) {
71
+ remotePlayers.record(
72
+ token: token as String,
73
+ type: type as String,
74
+ t: t,
75
+ data: data as? [String: Any]
76
+ )
77
+ }
78
+
79
+ @objc public func updatePlayerStats(_ token: NSString, stats: NSDictionary) {
80
+ remotePlayers.updateStats(
81
+ token: token as String,
82
+ stats: (stats as? [String: Any]) ?? [:]
83
+ )
84
+ }
85
+
86
+ @objc public func trackVitals(_ name: NSString, dataJson: NSString?, token: NSString?) {
87
+ remotePlayers.trackVitals(
88
+ name: name as String,
89
+ dataJson: dataJson as String?,
90
+ token: token as String?
91
+ )
92
+ }
93
+
94
+ /// Detach every player registered through THIS module instance and close the registry.
95
+ /// Called from `-[EverframeModule invalidate]`. Idempotent.
96
+ @objc public func reset() {
97
+ remotePlayers.detachAll()
98
+ }
99
+ }