@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.
- package/EverframeRN.podspec +88 -0
- package/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +505 -0
- package/android/build.gradle.kts +209 -0
- package/android/settings.gradle.kts +8 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +1670 -0
- package/android/src/main/java/dev/everframe/rn/EverframePackage.kt +40 -0
- package/android/src/main/java/dev/everframe/rn/EverframeSensitiveViewManager.kt +20 -0
- package/android/src/main/java/dev/everframe/rn/RnVideoPrivacyAdapter.kt +65 -0
- package/dist/NativeEverframe.d.ts +308 -0
- package/dist/NativeEverframe.js +1 -0
- package/dist/browser-ZQAY6B4M.js +1 -0
- package/dist/chunk-LPIKPZNX.js +1 -0
- package/dist/chunk-QRPINMCR.js +1 -0
- package/dist/chunk-QZRTTQRS.js +1 -0
- package/dist/chunk-XFLFA52R.js +1 -0
- package/dist/chunk-XUSV3KOD.js +1 -0
- package/dist/index.d.ts +1006 -0
- package/dist/index.js +51 -0
- package/dist/integrations/console.d.ts +8 -0
- package/dist/integrations/console.js +1 -0
- package/dist/integrations/react-native-video.d.ts +44 -0
- package/dist/integrations/react-native-video.js +1 -0
- package/dist/integrations/react-navigation.d.ts +21 -0
- package/dist/integrations/react-navigation.js +1 -0
- package/dist/integrations/theoplayer.d.ts +37 -0
- package/dist/integrations/theoplayer.js +1 -0
- package/dist/types-D5e4kWaw.d.ts +12 -0
- package/ios/Sources/EverframeBridge.swift +1043 -0
- package/ios/Sources/EverframeEventEmitter.swift +204 -0
- package/ios/Sources/EverframeModule.h +36 -0
- package/ios/Sources/EverframeModule.mm +489 -0
- package/ios/Sources/RemoteVitalsBridge.swift +99 -0
- package/package.json +121 -0
- package/src/NativeEverframe.ts +439 -0
- 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
|
+
}
|