@everframe/react-native 1.5.0 → 1.6.1
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 +1 -1
- package/README.md +136 -17
- package/android/build.gradle.kts +12 -19
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +16 -962
- package/dist/NativeEverframe.d.ts +7 -67
- package/dist/index.d.ts +63 -178
- package/dist/index.js +23 -23
- package/dist/index.web.d.ts +2 -2
- package/dist/{launch-C8wbMunK.d.ts → launch-aXtOOfNU.d.ts} +1 -2
- package/ios/Sources/EverframeBridge.swift +16 -456
- package/ios/Sources/EverframeEventEmitter.swift +9 -126
- package/ios/Sources/EverframeModule.mm +2 -52
- package/package.json +4 -4
- package/src/NativeEverframe.ts +11 -107
package/dist/index.web.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as ErrorCaptureStatus, P as PromiseRejectionStatus, R as ReportDeliveryStatus, c as ReporterTrigger, k as PermissionResult, L as LaunchContext, e as InteractionHookOptions, C as ControlChangeInput, S as ScrollInput, d as InteractionTargetAnnotation } from './launch-
|
|
2
|
-
export { f as ErrorCaptureCounters, g as ErrorCaptureOutcome, h as ErrorCapturePath, H as HelperType, I as InteractionPolicyOptions, l as PromiseRejectionCounters, m as RejectionReason, n as ReportCaptureOutcome, o as ReportCapturePath, p as ReportCapturePathStatus, q as ReportDeliveryReason, s as ReportQueueOperation, t as ReportQueueStatus, u as ReportStorageFailure, v as ReportTransportOrigin, w as ReportTransportOutcome, x as ReportTransportStatus, a as ReporterOpenOptions } from './launch-
|
|
1
|
+
import { E as ErrorCaptureStatus, P as PromiseRejectionStatus, R as ReportDeliveryStatus, c as ReporterTrigger, k as PermissionResult, L as LaunchContext, e as InteractionHookOptions, C as ControlChangeInput, S as ScrollInput, d as InteractionTargetAnnotation } from './launch-aXtOOfNU.js';
|
|
2
|
+
export { f as ErrorCaptureCounters, g as ErrorCaptureOutcome, h as ErrorCapturePath, H as HelperType, I as InteractionPolicyOptions, l as PromiseRejectionCounters, m as RejectionReason, n as ReportCaptureOutcome, o as ReportCapturePath, p as ReportCapturePathStatus, q as ReportDeliveryReason, s as ReportQueueOperation, t as ReportQueueStatus, u as ReportStorageFailure, v as ReportTransportOrigin, w as ReportTransportOutcome, x as ReportTransportStatus, a as ReporterOpenOptions } from './launch-aXtOOfNU.js';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
export * from '@everframe/react';
|
|
5
5
|
import 'zod';
|
|
@@ -1013,8 +1013,7 @@ declare class EverframeNotMountedError extends Error {
|
|
|
1013
1013
|
/**
|
|
1014
1014
|
* Rejected by `open()` when the host turned the reporter off
|
|
1015
1015
|
* (`reporter: { disabled: true }`, a crash-only session). The session keeps
|
|
1016
|
-
* this for its whole life
|
|
1017
|
-
* reports then come through the companion, not through `open()`. Same name
|
|
1016
|
+
* this for its whole life. Same name
|
|
1018
1017
|
* as the web SDK's error of the same purpose.
|
|
1019
1018
|
*/
|
|
1020
1019
|
declare class EverframeReporterDisabledError extends Error {
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
// The RN bridge surface is the Phase 06 5-method TurboModule (configure,
|
|
10
10
|
// openReporter, registerSensitiveRect, setExtra). The native
|
|
11
11
|
// EFReporterPresenter owns the entire report UX (capture + annotate + redact +
|
|
12
|
-
// submit) on iOS/iPadOS. On tvOS the on-device modal reporter was removed
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// on tvOS resolves to .cancelled by way of the absent presenter installer.
|
|
12
|
+
// submit) on iOS/iPadOS. On tvOS the on-device modal reporter was removed,
|
|
13
|
+
// so openReporter() on tvOS resolves to .cancelled by way of the absent
|
|
14
|
+
// presenter installer.
|
|
16
15
|
//
|
|
17
16
|
// 1. configure(opts) → start Everframe.shared with EverframeConfig
|
|
18
17
|
// + install EFReporterPresenter resolver
|
|
@@ -55,12 +54,8 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
55
54
|
networkBodiesDisabled: Bool,
|
|
56
55
|
installIdentifierDisabled: Bool,
|
|
57
56
|
crashReportingDisabled: Bool,
|
|
58
|
-
attachPinUi: NSString?,
|
|
59
|
-
companionDeviceId: NSString?,
|
|
60
|
-
companionBadgeEnabled: Bool,
|
|
61
57
|
shakeToReportEnabled: Bool,
|
|
62
58
|
quickCaptureEnabled: Bool,
|
|
63
|
-
companionBadgePosition: NSString?,
|
|
64
59
|
theme: NSDictionary?,
|
|
65
60
|
vitalsEnabled: NSNumber?,
|
|
66
61
|
vitalsSampleRate: NSNumber?,
|
|
@@ -95,26 +90,11 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
95
90
|
// Polarity flips exactly once, here: JS sends the veto
|
|
96
91
|
// (`crashReportingDisabled`), native stores the permission.
|
|
97
92
|
cfg.capture.crash = !crashReportingDisabled
|
|
98
|
-
// Explicit companion device-identity override (naming spec
|
|
99
|
-
// 2026-08-24) — read straight off the flat ConfigOpts field, same
|
|
100
|
-
// shape as attachPinUi below. nil/absent falls through to the
|
|
101
|
-
// Keychain-then-stored-UUID chain inside CompanionDeviceId.resolve.
|
|
102
|
-
cfg.companionDeviceId = companionDeviceId as String?
|
|
103
|
-
// Companion on-screen name badge (naming spec 2026-08-24, controller
|
|
104
|
-
// ruling / Task 6b) — read straight off the flat ConfigOpts fields,
|
|
105
|
-
// same shape as companionDeviceId above. `companionBadgeEnabled` is
|
|
106
|
-
// already defaulted to `true` by the .mm's `.value_or(true)`;
|
|
107
|
-
// `companionBadgePosition` stays the raw string here — parsed into
|
|
108
|
-
// `CompanionBadgePosition` only at `startCompanion()`, since a
|
|
109
|
-
// position can't affect anything before a relay client (and its
|
|
110
|
-
// badge) exist, mirroring `companionAttachPinUi` below.
|
|
111
|
-
cfg.companionBadgeEnabled = companionBadgeEnabled
|
|
112
93
|
cfg.shakeToReportEnabled = shakeToReportEnabled
|
|
113
94
|
cfg.quickCaptureEnabled = quickCaptureEnabled
|
|
114
95
|
// Polarity flips exactly once, here: JS sends the crash-only switch
|
|
115
96
|
// (`reporterDisabled`), native stores the permission.
|
|
116
97
|
cfg.reporterEnabled = !reporterDisabled
|
|
117
|
-
cfg.companionBadgePosition = companionBadgePosition as String?
|
|
118
98
|
// Inline reporter theme (reporter branding spec 2026-08-25, RN
|
|
119
99
|
// slice). The .mm collapses the 8 flat ConfigOpts `theme*` wire
|
|
120
100
|
// fields into one role-keyed dictionary (nil when the host set no
|
|
@@ -135,13 +115,6 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
135
115
|
if let vitalsCaptureSourceQuery { cfg.vitals.captureSourceQuery = vitalsCaptureSourceQuery.boolValue }
|
|
136
116
|
// Absent stays nil, so the native SDK detects a debug build itself; present wins.
|
|
137
117
|
cfg.debugBuild = debugBuild?.boolValue
|
|
138
|
-
// The attach-PIN UI mode for startCompanion() to read (spec 2026-08-19). configure()
|
|
139
|
-
// precedes startCompanion() in every host's lifecycle, so this is the natural place to
|
|
140
|
-
// parse the flat ConfigOpts string into the native enum; startCompanion is where it is
|
|
141
|
-
// actually consumed (RelayWSClient's announce arg + the suppression seam), since a mode
|
|
142
|
-
// can't affect anything before a relay client exists. A changed value forces native
|
|
143
|
-
// reconfiguration even when the SDK configuration itself compares equal.
|
|
144
|
-
let newAttachPinUi = Self.parseAttachPinUi(attachPinUi as String?)
|
|
145
118
|
let health: ReleaseHealthConfiguration?
|
|
146
119
|
if releaseHealthEnabled, let loaded = releaseHealthLoadedBuildId {
|
|
147
120
|
// The SDK reads the app version itself; the bridge carries the loaded bundle, the user ID and
|
|
@@ -158,11 +131,9 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
158
131
|
// the unchanged-configuration comparison.
|
|
159
132
|
cfg.releaseHealth = nil
|
|
160
133
|
// Native code atomically checks installed config and reserves its exact owner.
|
|
161
|
-
// An identical configure preserves the session
|
|
162
|
-
// requires the same full restart as the other native configuration changes.
|
|
134
|
+
// An identical configure preserves the session.
|
|
163
135
|
let applyHealth = try Everframe.shared.configureAndPrepareReleaseHealth(
|
|
164
|
-
config: cfg, health: health, forceRestart:
|
|
165
|
-
companionAttachPinUi = newAttachPinUi
|
|
136
|
+
config: cfg, health: health, forceRestart: false)
|
|
166
137
|
// Reservation is tied to this configure's exact native start generation. Later
|
|
167
138
|
// configure/disable or a native restart invalidates the asynchronous operation.
|
|
168
139
|
if let applyHealth {
|
|
@@ -170,12 +141,9 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
170
141
|
}
|
|
171
142
|
installPresenterResolverIfNeeded()
|
|
172
143
|
// Spec 2026-09-17 setExtra-resolver — installed unconditionally,
|
|
173
|
-
// every configure()
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// funnel through Everframe.shared.report.open() →
|
|
177
|
-
// __consumePendingAttachments() with no companion involvement at
|
|
178
|
-
// all. See `installExtraResolveHook()`'s own doc comment.
|
|
144
|
+
// every configure(): this must cover the plain in-app openReporter()
|
|
145
|
+
// path and native shake, both of which funnel through
|
|
146
|
+
// Everframe.shared.report.open() → __consumePendingAttachments(). See `installExtraResolveHook()`'s own doc comment.
|
|
179
147
|
installExtraResolveHook()
|
|
180
148
|
// Task 13: prime the device-metadata cache off the main queue so a
|
|
181
149
|
// crash arriving shortly after configure() still has device context.
|
|
@@ -203,35 +171,10 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
203
171
|
)
|
|
204
172
|
}
|
|
205
173
|
|
|
206
|
-
/// Coerce the flat `ConfigOpts.attachPinUi` string into `AttachPinUi`.
|
|
207
|
-
/// `nil` (field absent) and any unrecognised value both degrade to
|
|
208
|
-
/// `.builtin` — the safe default matching "host did nothing special".
|
|
209
|
-
private static func parseAttachPinUi(_ raw: String?) -> AttachPinUi {
|
|
210
|
-
guard let raw, let mode = AttachPinUi(rawValue: raw) else { return .builtin }
|
|
211
|
-
return mode
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/// Coerce the flat `ConfigOpts.companionBadgePosition` string into
|
|
215
|
-
/// `CompanionBadgePosition` (naming spec 2026-08-24, controller ruling /
|
|
216
|
-
/// Task 6b). `nil` (field absent) and any unrecognised value both
|
|
217
|
-
/// degrade to `.bottomRight` — matches `CompanionBadgeOptions.position`'s
|
|
218
|
-
/// own default, so an absent field behaves exactly like never touching
|
|
219
|
-
/// the option at all.
|
|
220
|
-
private static func parseCompanionBadgePosition(_ raw: String?) -> CompanionBadgePosition {
|
|
221
|
-
switch raw {
|
|
222
|
-
case "bottom-left": return .bottomLeft
|
|
223
|
-
case "top-right": return .topRight
|
|
224
|
-
case "top-left": return .topLeft
|
|
225
|
-
default: return .bottomRight
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
174
|
/// Wires `EFReporterPresenter` as the resolver for `Everframe.shared.report
|
|
230
175
|
/// .open()`. Called from configure(); the first call installs, subsequent
|
|
231
|
-
/// calls no-op. tvOS has no on-device modal
|
|
232
|
-
///
|
|
233
|
-
/// tvOS `installResolver()` is a no-op. The gate here skips that call and
|
|
234
|
-
/// logs where tvOS reporting goes instead.
|
|
176
|
+
/// calls no-op. tvOS has no on-device modal, so EverframeReporterUI's
|
|
177
|
+
/// tvOS `installResolver()` is a no-op. The gate here skips that call.
|
|
235
178
|
private static func installPresenterResolverIfNeeded() {
|
|
236
179
|
#if !os(tvOS)
|
|
237
180
|
resolverInstallLock.lock()
|
|
@@ -246,7 +189,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
246
189
|
os_log("[everframe] installed EFReporterPresenter resolver", log: log, type: .info)
|
|
247
190
|
}
|
|
248
191
|
#else
|
|
249
|
-
os_log("[everframe] tvOS — EFReporterPresenter not installed
|
|
192
|
+
os_log("[everframe] tvOS — EFReporterPresenter not installed (no on-device reporter)", log: log, type: .info)
|
|
250
193
|
#endif
|
|
251
194
|
}
|
|
252
195
|
|
|
@@ -335,9 +278,8 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
335
278
|
}
|
|
336
279
|
|
|
337
280
|
/// Per-correlation_id continuation map, released by
|
|
338
|
-
/// `signalExtraResolverReady`.
|
|
339
|
-
///
|
|
340
|
-
/// same "the timeout resumes it if JS never calls back" shape).
|
|
281
|
+
/// `signalExtraResolverReady`. A lock-guarded dictionary; the timeout
|
|
282
|
+
/// resumes a continuation if JS never calls back.
|
|
341
283
|
nonisolated(unsafe) private static var pendingExtraResolveContinuations:
|
|
342
284
|
[String: CheckedContinuation<Void, Never>] = [:]
|
|
343
285
|
private static let pendingExtraResolveLock = NSLock()
|
|
@@ -348,8 +290,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
348
290
|
/// string/object-only host's report path exactly as fast as before this
|
|
349
291
|
/// feature landed (requirement: "no behaviour change whatsoever").
|
|
350
292
|
///
|
|
351
|
-
/// Otherwise
|
|
352
|
-
/// exactly: send `EverframeEventEmitter.sendExtraResolveRequested`, then
|
|
293
|
+
/// Otherwise send `EverframeEventEmitter.sendExtraResolveRequested`, then
|
|
353
294
|
/// suspend up to 250ms for JS's `signalExtraResolverReady` ack.
|
|
354
295
|
private static func awaitJsExtraResolve() async {
|
|
355
296
|
extraResolverActiveLock.lock()
|
|
@@ -363,8 +304,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
363
304
|
pendingExtraResolveContinuations[correlationId] = cont
|
|
364
305
|
pendingExtraResolveLock.unlock()
|
|
365
306
|
// Send AFTER registering the continuation so a fast JS callback
|
|
366
|
-
// can't lose the race
|
|
367
|
-
// uses.
|
|
307
|
+
// can't lose the race.
|
|
368
308
|
EverframeEventEmitter.sendExtraResolveRequested(correlationId)
|
|
369
309
|
// 250ms bounded fallback — resume if JS hasn't called back by then.
|
|
370
310
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.25) {
|
|
@@ -376,8 +316,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
376
316
|
/// JS handshake ack: called once the `everframe.extra.resolveRequested`
|
|
377
317
|
/// listener has — if a resolver is registered — pushed a freshly
|
|
378
318
|
/// resolved value through `setExtra` above. Idempotent — a late signal
|
|
379
|
-
/// after the 250ms fallback already fired is a harmless no-op
|
|
380
|
-
/// like `signalCompanionReportRequestReady` below.
|
|
319
|
+
/// after the 250ms fallback already fired is a harmless no-op.
|
|
381
320
|
@objc public static func signalExtraResolverReady(_ correlationId: NSString) {
|
|
382
321
|
resumeAndDropPendingExtraResolve(correlationId: correlationId as String)
|
|
383
322
|
}
|
|
@@ -470,67 +409,6 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
470
409
|
}
|
|
471
410
|
}
|
|
472
411
|
|
|
473
|
-
/// Install-race fix (spec 2026-08-19 review finding 3). `configure()`
|
|
474
|
-
/// (above) queues the PIN presenter install via
|
|
475
|
-
/// `installPresenterResolverIfNeeded()`'s `DispatchQueue.main.async` —
|
|
476
|
-
/// note that call marks `resolverInstalled = true` BEFORE the async block
|
|
477
|
-
/// actually runs, so that flag alone cannot tell a caller here whether
|
|
478
|
-
/// `CompanionPinPresenter.install()` (which sets
|
|
479
|
-
/// `CompanionAPI.__builtinPinUiInstalled`) has genuinely finished.
|
|
480
|
-
///
|
|
481
|
-
/// `startCompanion()` builds `RelayWSClient` and calls `connect()`
|
|
482
|
-
/// synchronously right after this; `connect()`'s announce path reads
|
|
483
|
-
/// `__builtinPinUiInstalled` from a freshly-spawned `Task` that can run
|
|
484
|
-
/// before the main-queue-dispatched install block does, especially early
|
|
485
|
-
/// in a host's launch sequence when the main queue is already busy. A
|
|
486
|
-
/// host that calls `startCompanion()` immediately after `configure()`
|
|
487
|
-
/// resolves — the ordinary RN lifecycle — would then read `false` for
|
|
488
|
-
/// the capability check for the ENTIRE session, so `.builtin` mode would
|
|
489
|
-
/// never advertise `supportsAttachPin` even though the presenter finishes
|
|
490
|
-
/// installing moments later.
|
|
491
|
-
///
|
|
492
|
-
/// Fix: synchronously hop to MainActor and call
|
|
493
|
-
/// `CompanionPinPresenter.install()` directly — the same call
|
|
494
|
-
/// `EFReporterPresenter.installResolver()` itself makes — and WAIT for
|
|
495
|
-
/// it, before `startCompanion()` constructs the client. `install()` is
|
|
496
|
-
/// `@MainActor` and idempotent (its own internal `installed` flag), so
|
|
497
|
-
/// this is cheap on every call after the first: the common case (install
|
|
498
|
-
/// already genuinely finished by the time `startCompanion()` runs) pays
|
|
499
|
-
/// only a MainActor hop, not real work. Scoped to the smallest change
|
|
500
|
-
/// that closes the race — `installPresenterResolverIfNeeded()`'s async
|
|
501
|
-
/// dispatch and its own `resolverInstalled` flag are untouched, since
|
|
502
|
-
/// `EFReporterPresenter.installResolver()` also wires the full modal
|
|
503
|
-
/// reporter (openReporter's resolver), which this call does not need to
|
|
504
|
-
/// duplicate or wait for.
|
|
505
|
-
///
|
|
506
|
-
/// Gate: `#if canImport(UIKit)`, matching `CompanionPinPresenter.swift`'s
|
|
507
|
-
/// own gate — NOT `#if !os(tvOS)`. Round-2 review finding 2: tvOS hosts
|
|
508
|
-
/// are a PRIMARY consumer of companion (see `CompanionPinPresenter.swift`
|
|
509
|
-
/// header + `RelayWSClient`'s), and only `EFReporterPresenter` (the full
|
|
510
|
-
/// modal reporter, iOS-only — tvOS has no on-device modal) needs the
|
|
511
|
-
/// `!os(tvOS)` exclusion `installPresenterResolverIfNeeded()` above
|
|
512
|
-
/// correctly keeps. Gating THIS call the same way silently left RN-tvOS
|
|
513
|
-
/// builtin hosts with `CompanionAPI.__builtinPinUiInstalled` permanently
|
|
514
|
-
/// false, so `startCompanion()`'s announce always reported
|
|
515
|
-
/// `supportsAttachPin: false` and every tvOS host fell back to legacy
|
|
516
|
-
/// one-click attach even though `CompanionPinPresenter` builds and runs
|
|
517
|
-
/// fine there.
|
|
518
|
-
private static func ensurePinPresenterInstalledSync() {
|
|
519
|
-
#if canImport(UIKit)
|
|
520
|
-
if Thread.isMainThread {
|
|
521
|
-
MainActor.assumeIsolated {
|
|
522
|
-
CompanionPinPresenter.install()
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
DispatchQueue.main.sync {
|
|
526
|
-
MainActor.assumeIsolated {
|
|
527
|
-
CompanionPinPresenter.install()
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
#endif
|
|
532
|
-
}
|
|
533
|
-
|
|
534
412
|
// MARK: - openReporter
|
|
535
413
|
|
|
536
414
|
/// Opens the native reporter modal and resolves with the user's outcome.
|
|
@@ -792,322 +670,4 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
|
|
|
792
670
|
}
|
|
793
671
|
markSensitive(v)
|
|
794
672
|
}
|
|
795
|
-
|
|
796
|
-
// MARK: - Companion (Plan 06.2-11)
|
|
797
|
-
//
|
|
798
|
-
// Bridge-side ownership of:
|
|
799
|
-
// • At most ONE live RelayWSClient against the configured ingest endpoint.
|
|
800
|
-
// • Combine subscriptions on `Everframe.shared.companion.$state`,
|
|
801
|
-
// `.$pairUrl`, `.$code` and `.$attachedUserName` that forward into
|
|
802
|
-
// `EverframeEventEmitter`.
|
|
803
|
-
//
|
|
804
|
-
// The Combine subscriptions are installed lazily on first `startCompanion`
|
|
805
|
-
// and kept across stop/start cycles (the companion observable survives the
|
|
806
|
-
// WS lifetime — see CompanionAPI.swift). This avoids missing the initial
|
|
807
|
-
// values that may already be present from a prior start.
|
|
808
|
-
//
|
|
809
|
-
// Concurrency: a single NSLock guards `companionClient` + `companionCancellables`
|
|
810
|
-
// mirrors the RelayWSClient internal locking pattern. NEVER called from
|
|
811
|
-
// RN's JS thread directly — the .mm shim hops to MainActor below to satisfy
|
|
812
|
-
// both `Everframe.shared.start` semantics and the RelayWSClient init/teardown
|
|
813
|
-
// contract.
|
|
814
|
-
|
|
815
|
-
private static let companionLock = NSLock()
|
|
816
|
-
nonisolated(unsafe) private static var companionClient: RelayWSClient?
|
|
817
|
-
/// The host's configured attach-PIN UI mode (spec 2026-08-19), parsed
|
|
818
|
-
/// from `ConfigOpts.attachPinUi` at `configure()` time. Defaults to
|
|
819
|
-
/// `.builtin` for a host that never sets the flag.
|
|
820
|
-
nonisolated(unsafe) private static var companionAttachPinUi: AttachPinUi = .builtin
|
|
821
|
-
// `CompanionCaptureBridge` subscribes to the report.request notification
|
|
822
|
-
// posted by RelayWSClient and runs the capture+send dance. Without an
|
|
823
|
-
// instance alive, report.request fires into the void → phone hangs on
|
|
824
|
-
// "Capturing the TV screen" forever (Plan 06.2-11 bugfix).
|
|
825
|
-
nonisolated(unsafe) private static var companionCaptureBridge: CompanionCaptureBridge?
|
|
826
|
-
nonisolated(unsafe) private static var companionCancellables: Set<AnyCancellable> = []
|
|
827
|
-
nonisolated(unsafe) private static var companionSubscriptionsInstalled = false
|
|
828
|
-
|
|
829
|
-
/// Open a relay WS connection and start forwarding
|
|
830
|
-
/// `Everframe.shared.companion` signals to the JS event emitter. Idempotent:
|
|
831
|
-
/// if a client is already live this is a no-op — the socket is NOT
|
|
832
|
-
/// recreated. Call `stopCompanion()` first to tear the existing connection
|
|
833
|
-
/// down before a subsequent `startCompanion()` opens a fresh one.
|
|
834
|
-
@objc public static func startCompanion(_ endpoint: NSString) {
|
|
835
|
-
// The JS-side `endpoint` arg is ignored — RelayWSClient defaults
|
|
836
|
-
// to the build-time-baked `IngestEndpoint.url`.
|
|
837
|
-
_ = endpoint
|
|
838
|
-
|
|
839
|
-
// MUST run before `RelayWSClient` is constructed below — see
|
|
840
|
-
// `ensurePinPresenterInstalledSync()`'s doc comment for the race this
|
|
841
|
-
// closes (finding 3, spec 2026-08-19 review).
|
|
842
|
-
ensurePinPresenterInstalledSync()
|
|
843
|
-
|
|
844
|
-
// Combine subscriptions install once and survive stop/start cycles;
|
|
845
|
-
// safe to (re-)call on every start (self-guarded). MUST run OUTSIDE
|
|
846
|
-
// `companionLock` — it self-acquires the same non-reentrant lock.
|
|
847
|
-
installCompanionSubscriptionsIfNeeded()
|
|
848
|
-
|
|
849
|
-
companionLock.lock()
|
|
850
|
-
// Idempotent: a live client means a prior startCompanion already
|
|
851
|
-
// opened the socket. Do NOT recreate it — callers must stopCompanion()
|
|
852
|
-
// first. Holding the lock across the check + install keeps two racing
|
|
853
|
-
// starts from both constructing a client (belt-and-suspenders; the
|
|
854
|
-
// .mm shim already serialises this on MainActor).
|
|
855
|
-
if companionClient != nil {
|
|
856
|
-
companionLock.unlock()
|
|
857
|
-
os_log("[everframe] startCompanion ignored — already running; call stopCompanion() first",
|
|
858
|
-
log: log, type: .info)
|
|
859
|
-
return
|
|
860
|
-
}
|
|
861
|
-
os_log("[everframe] startCompanion (endpoint arg ignored)",
|
|
862
|
-
log: log, type: .info)
|
|
863
|
-
// Runtime suppression of the built-in PIN presenter (spec 2026-08-19,
|
|
864
|
-
// controller ruling): a runtime flag, not an install-site gate.
|
|
865
|
-
// `EFReporterPresenter.installResolver()` always installs
|
|
866
|
-
// `CompanionPinPresenter` (see `installPresenterResolverIfNeeded()`
|
|
867
|
-
// above) so `__builtinPinUiInstalled` stays an honest capability
|
|
868
|
-
// signal regardless of the host's chosen mode — only the PRESENTING
|
|
869
|
-
// is silenced, here, at the one site where the mode is already known
|
|
870
|
-
// (configure() precedes startCompanion() in every host's lifecycle).
|
|
871
|
-
Everframe.shared.companion.__setBuiltinPinUiSuppressed(companionAttachPinUi != .builtin)
|
|
872
|
-
// Companion discovery (spec 2026-08-07): announcing is what makes this
|
|
873
|
-
// device appear in the dashboard's Companion tab. It is opt-in on the
|
|
874
|
-
// SDK key the host ALREADY configured via `configure()` — we never ask
|
|
875
|
-
// for a second one. A nil key takes the ticketless path: RelayWSClient
|
|
876
|
-
// builds no announcer and connects to plain `/relay/tv`, byte-identical
|
|
877
|
-
// to pre-companion behaviour. Announce must never be able to break
|
|
878
|
-
// reporting — no key means "no dashboard presence", never "no socket".
|
|
879
|
-
let client = RelayWSClient(
|
|
880
|
-
companion: Everframe.shared.companion,
|
|
881
|
-
sdkKey: configuredSdkKey(),
|
|
882
|
-
deviceLabel: companionDeviceLabel(),
|
|
883
|
-
attachPinUi: companionAttachPinUi,
|
|
884
|
-
// Naming spec 2026-08-24 — read off the same `currentConfig`
|
|
885
|
-
// snapshot `configuredSdkKey()` above already reads, not a
|
|
886
|
-
// second configuration surface. nil falls through to
|
|
887
|
-
// RelayWSClient's own Keychain-then-stored-UUID chain.
|
|
888
|
-
companionDeviceId: Everframe.shared.currentConfig?.companionDeviceId,
|
|
889
|
-
// Companion on-screen name badge (naming spec 2026-08-24,
|
|
890
|
-
// controller ruling / Task 6b) — same `currentConfig` snapshot,
|
|
891
|
-
// read fresh at start time (a mode/toggle can't affect anything
|
|
892
|
-
// before a relay client and its badge exist). `?? true` covers
|
|
893
|
-
// the same "never configured" edge `configuredSdkKey()`'s own
|
|
894
|
-
// optional chain already tolerates.
|
|
895
|
-
companionBadge: CompanionBadgeOptions(
|
|
896
|
-
enabled: Everframe.shared.currentConfig?.companionBadgeEnabled ?? true,
|
|
897
|
-
position: Self.parseCompanionBadgePosition(Everframe.shared.currentConfig?.companionBadgePosition)
|
|
898
|
-
)
|
|
899
|
-
)
|
|
900
|
-
// Wire the capture bridge BEFORE connecting so the report.request
|
|
901
|
-
// notification observer is registered before the first frame can fire.
|
|
902
|
-
let captureBridge = CompanionCaptureBridge(client: client)
|
|
903
|
-
companionClient = client
|
|
904
|
-
companionCaptureBridge = captureBridge
|
|
905
|
-
companionLock.unlock()
|
|
906
|
-
|
|
907
|
-
client.connect()
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/// The SDK key the host already configured through `configure()`, or nil
|
|
911
|
-
/// when there isn't a usable one. Read straight off `Everframe.shared`'s
|
|
912
|
-
/// own config snapshot — deliberately NOT a second place a key can be
|
|
913
|
-
/// configured, and NOT a new public surface (`currentConfig` is already
|
|
914
|
-
/// public and read-only).
|
|
915
|
-
///
|
|
916
|
-
/// Returns nil when `Everframe.start` never ran, and when the host
|
|
917
|
-
/// configured a blank/whitespace-only key. Both are "no dashboard
|
|
918
|
-
/// presence", never "no socket": RelayWSClient skips the announce hop
|
|
919
|
-
/// entirely on a nil key and reporting is unaffected.
|
|
920
|
-
private static func configuredSdkKey() -> String? {
|
|
921
|
-
guard let sdkKey = Everframe.shared.currentConfig?.sdkKey,
|
|
922
|
-
!sdkKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
923
|
-
else { return nil }
|
|
924
|
-
return sdkKey
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/// Non-personal device label for the dashboard's device list — "Apple TV",
|
|
928
|
-
/// "iPhone", "iPad". Deliberately `UIDevice.current.model` and NOT
|
|
929
|
-
/// `UIDevice.current.name`, which is commonly a person's name
|
|
930
|
-
/// ("Aurimas's Apple TV") and would leak into a shared dashboard list as a
|
|
931
|
-
/// side effect of turning companion on. The display code is the real
|
|
932
|
-
/// disambiguator; the label is only a convenience.
|
|
933
|
-
private static func companionDeviceLabel() -> String? {
|
|
934
|
-
let model = UIDevice.current.model
|
|
935
|
-
return model.isEmpty ? nil : model
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/// Test-only — object identity of the live companion client, so tests can
|
|
939
|
-
/// prove a repeat `startCompanion()` is a no-op (same instance, not
|
|
940
|
-
/// recreated). Nil when no client is live.
|
|
941
|
-
static func _companionClientIdentityForTesting() -> ObjectIdentifier? {
|
|
942
|
-
companionLock.lock()
|
|
943
|
-
defer { companionLock.unlock() }
|
|
944
|
-
return companionClient.map(ObjectIdentifier.init)
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/// JS handshake: called from the RN-bridge ObjC shim once the JS-side
|
|
948
|
-
/// `everframe.companion.reportRequested` listener is ready for capture to
|
|
949
|
-
/// proceed. Releases the CompanionCaptureBridge's bounded wait.
|
|
950
|
-
@objc public static func signalCompanionReportRequestReady(_ correlationId: NSString) {
|
|
951
|
-
CompanionCaptureBridge.signalReactTreeAttached(correlationId: correlationId as String)
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/// Disconnect the active companion WS client (if any) and drop the
|
|
955
|
-
/// reference. Combine subscriptions stay alive across stop/start cycles
|
|
956
|
-
/// because the underlying CompanionAPI singleton on `Everframe.shared`
|
|
957
|
-
/// outlives the WS — replaying any state changes that happen between
|
|
958
|
-
/// stop and the next start would otherwise be lost from JS's view.
|
|
959
|
-
@objc public static func stopCompanion() {
|
|
960
|
-
os_log("[everframe] stopCompanion", log: log, type: .info)
|
|
961
|
-
companionLock.lock()
|
|
962
|
-
let client = companionClient
|
|
963
|
-
companionClient = nil
|
|
964
|
-
// Drop the capture bridge so its NotificationCenter observers
|
|
965
|
-
// deregister (CompanionCaptureBridge cleans up in deinit).
|
|
966
|
-
companionCaptureBridge = nil
|
|
967
|
-
companionLock.unlock()
|
|
968
|
-
client?.disconnect()
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/// Install Combine sinks on `Everframe.shared.companion.$state`,
|
|
972
|
-
/// `.$pairUrl`, `.$code` and `.$attachedUserName` that forward to
|
|
973
|
-
/// `EverframeEventEmitter`. Idempotent — the
|
|
974
|
-
/// `companionSubscriptionsInstalled` flag guards re-entry.
|
|
975
|
-
///
|
|
976
|
-
/// `@Published` publishers fire `.sink` synchronously on the writer's
|
|
977
|
-
/// thread (URLSession delegate queue, our `__setState` callsite). The
|
|
978
|
-
/// emitter's `sendEvent(withName:body:)` is thread-safe (RN bridge
|
|
979
|
-
/// enqueues onto the JS thread), so no main-thread hop is needed.
|
|
980
|
-
private static func installCompanionSubscriptionsIfNeeded() {
|
|
981
|
-
companionLock.lock()
|
|
982
|
-
let alreadyInstalled = companionSubscriptionsInstalled
|
|
983
|
-
companionSubscriptionsInstalled = true
|
|
984
|
-
companionLock.unlock()
|
|
985
|
-
guard !alreadyInstalled else { return }
|
|
986
|
-
|
|
987
|
-
let companion = Everframe.shared.companion
|
|
988
|
-
var bag = Set<AnyCancellable>()
|
|
989
|
-
companion.$state
|
|
990
|
-
.sink { state in
|
|
991
|
-
// CompanionState.rawValue is camelCase (Swift's default for
|
|
992
|
-
// String-backed enums) — `.reportInProgress` would cross to
|
|
993
|
-
// JS as "reportInProgress" and silently fail the union match
|
|
994
|
-
// in `src/companion.ts` (CompanionState = 'report_in_progress'
|
|
995
|
-
// | ...). Map to snake_case at the bridge boundary so both
|
|
996
|
-
// platforms emit the same JS-facing strings. Android does the
|
|
997
|
-
// equivalent via `CompanionState.toRnString()` in
|
|
998
|
-
// EverframeModule.kt.
|
|
999
|
-
EverframeEventEmitter.sendState(Self.rnString(for: state))
|
|
1000
|
-
}
|
|
1001
|
-
.store(in: &bag)
|
|
1002
|
-
companion.$pairUrl
|
|
1003
|
-
.sink { url in
|
|
1004
|
-
EverframeEventEmitter.sendPairUrl(url)
|
|
1005
|
-
}
|
|
1006
|
-
.store(in: &bag)
|
|
1007
|
-
|
|
1008
|
-
// Companion discovery (spec 2026-08-07). `CompanionState.swift` also
|
|
1009
|
-
// posts `.everframeCompanionCodeChange` /
|
|
1010
|
-
// `.everframeCompanionAttachedUserNameChange` for UIKit/ObjC hosts, but
|
|
1011
|
-
// we take the `@Published` route here for the same reason `$pairUrl`
|
|
1012
|
-
// does: a `Published.Publisher` replays its CURRENT value to a new
|
|
1013
|
-
// subscriber, so JS still learns a code that was already set by a
|
|
1014
|
-
// prior start. NotificationCenter has no replay and posts only on a
|
|
1015
|
-
// real change — subscribing there would silently lose exactly the
|
|
1016
|
-
// initial values this install-once-and-survive-stop/start design
|
|
1017
|
-
// exists to preserve.
|
|
1018
|
-
companion.$code
|
|
1019
|
-
.sink { code in
|
|
1020
|
-
EverframeEventEmitter.sendCode(code)
|
|
1021
|
-
}
|
|
1022
|
-
.store(in: &bag)
|
|
1023
|
-
companion.$attachedUserName
|
|
1024
|
-
.sink { name in
|
|
1025
|
-
EverframeEventEmitter.sendAttachedUserName(name)
|
|
1026
|
-
}
|
|
1027
|
-
.store(in: &bag)
|
|
1028
|
-
|
|
1029
|
-
// Resolved device display name (naming spec 2026-08-24). Same
|
|
1030
|
-
// replay-on-subscribe rationale as `$code`/`$attachedUserName`
|
|
1031
|
-
// above — a `Published` publisher replays its CURRENT value to a
|
|
1032
|
-
// new subscriber, so JS still learns a name that was already
|
|
1033
|
-
// resolved by a prior announce.
|
|
1034
|
-
companion.$resolvedName
|
|
1035
|
-
.sink { name in
|
|
1036
|
-
EverframeEventEmitter.sendResolvedName(name)
|
|
1037
|
-
}
|
|
1038
|
-
.store(in: &bag)
|
|
1039
|
-
|
|
1040
|
-
// Attach-PIN challenge (spec 2026-08-19). Same replay-on-subscribe
|
|
1041
|
-
// rationale as `$code`/`$attachedUserName` above — a `Published`
|
|
1042
|
-
// publisher replays its CURRENT value to a new subscriber, so a
|
|
1043
|
-
// `'custom'`-mode host that starts JS-side listening after the
|
|
1044
|
-
// challenge already arrived still learns about it.
|
|
1045
|
-
companion.$attachChallenge
|
|
1046
|
-
.sink { challenge in
|
|
1047
|
-
EverframeEventEmitter.sendAttachChallenge(challenge.map {
|
|
1048
|
-
["code": $0.code, "requestedByName": $0.requestedByName, "ttlMs": $0.ttlMs]
|
|
1049
|
-
})
|
|
1050
|
-
}
|
|
1051
|
-
.store(in: &bag)
|
|
1052
|
-
|
|
1053
|
-
// Forward `report.request` arrival to JS so the companion module
|
|
1054
|
-
// can acknowledge before the bridge proceeds with native capture
|
|
1055
|
-
// (see CompanionCaptureBridge.awaitJsReactTreeAttach). The
|
|
1056
|
-
// CompanionCaptureBridge posts this notification at the top of
|
|
1057
|
-
// handleReportRequest and then awaits the JS callback up to
|
|
1058
|
-
// 250ms before falling through.
|
|
1059
|
-
NotificationCenter.default.publisher(
|
|
1060
|
-
for: CompanionCaptureBridge.shouldAttachReactTreeNotification
|
|
1061
|
-
)
|
|
1062
|
-
.sink { note in
|
|
1063
|
-
guard let corrId = note.userInfo?["correlation_id"] as? String else { return }
|
|
1064
|
-
EverframeEventEmitter.sendReportRequested(corrId)
|
|
1065
|
-
}
|
|
1066
|
-
.store(in: &bag)
|
|
1067
|
-
|
|
1068
|
-
companionLock.lock()
|
|
1069
|
-
companionCancellables = bag
|
|
1070
|
-
companionLock.unlock()
|
|
1071
|
-
os_log("[everframe] installed companion Combine subscriptions",
|
|
1072
|
-
log: log, type: .info)
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/// Map `CompanionState` to the snake_case payload string the JS facade
|
|
1076
|
-
/// expects (`'unpaired' | 'paired' | 'report_in_progress' |
|
|
1077
|
-
/// 'phone_disconnected'`). MUST match `EverframeModule.toRnString()` on
|
|
1078
|
-
/// Android. Not `@objc` — `CompanionState` is a Swift enum that does not
|
|
1079
|
-
/// bridge to ObjC; consumers call this from Swift only (the .mm shim sends
|
|
1080
|
-
/// already-stringified payloads to the event emitter).
|
|
1081
|
-
public static func rnString(for state: CompanionState) -> String {
|
|
1082
|
-
switch state {
|
|
1083
|
-
case .unpaired: return "unpaired"
|
|
1084
|
-
case .paired: return "paired"
|
|
1085
|
-
case .reportInProgress: return "report_in_progress"
|
|
1086
|
-
case .phoneDisconnected: return "phone_disconnected"
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// MARK: - Test seams (internal access via @testable import)
|
|
1091
|
-
|
|
1092
|
-
/// Whether a RelayWSClient is currently owned by the bridge. The test
|
|
1093
|
-
/// target uses this to assert that `startCompanion(...)` installs the
|
|
1094
|
-
/// client and `stopCompanion()` drops it.
|
|
1095
|
-
@objc public static func _hasCompanionClientForTesting() -> Bool {
|
|
1096
|
-
companionLock.lock()
|
|
1097
|
-
defer { companionLock.unlock() }
|
|
1098
|
-
return companionClient != nil
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
/// Test-only reset for the Combine subscription latch + cancellables —
|
|
1102
|
-
/// XCTest creates a fresh bridge state per test method.
|
|
1103
|
-
@objc public static func _resetCompanionForTesting() {
|
|
1104
|
-
companionLock.lock()
|
|
1105
|
-
companionClient?.disconnect()
|
|
1106
|
-
companionClient = nil
|
|
1107
|
-
companionCancellables.removeAll()
|
|
1108
|
-
companionSubscriptionsInstalled = false
|
|
1109
|
-
companionAttachPinUi = .builtin
|
|
1110
|
-
companionLock.unlock()
|
|
1111
|
-
Everframe.shared.companion.__setBuiltinPinUiSuppressed(false)
|
|
1112
|
-
}
|
|
1113
673
|
}
|