@everframe/react-native 1.1.0 → 1.2.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.
@@ -1,5 +1,5 @@
1
- import { E as ErrorCaptureStatus, P as PromiseRejectionStatus, R as ReportDeliveryStatus } from './promise-rejection-types-C-lxCdDl.js';
2
- export { a as ErrorCaptureCounters, b as ErrorCaptureOutcome, c as ErrorCapturePath, d as PromiseRejectionCounters, e as RejectionReason, f as ReportCaptureOutcome, g as ReportCapturePath, h as ReportCapturePathStatus, i as ReportDeliveryReason, j as ReportQueueOperation, k as ReportQueueStatus, l as ReportStorageFailure, m as ReportTransportOrigin, n as ReportTransportOutcome, o as ReportTransportStatus } from './promise-rejection-types-C-lxCdDl.js';
1
+ import { E as ErrorCaptureStatus, P as PromiseRejectionStatus, R as ReportDeliveryStatus } from './promise-rejection-types-Bf5iLv8n.js';
2
+ export { a as ErrorCaptureCounters, b as ErrorCaptureOutcome, c as ErrorCapturePath, d as PromiseRejectionCounters, e as RejectionReason, f as ReportCaptureOutcome, g as ReportCapturePath, h as ReportCapturePathStatus, i as ReportDeliveryReason, j as ReportQueueOperation, k as ReportQueueStatus, l as ReportStorageFailure, m as ReportTransportOrigin, n as ReportTransportOutcome, o as ReportTransportStatus } from './promise-rejection-types-Bf5iLv8n.js';
3
3
  export * from '@everframe/react';
4
4
 
5
5
  /** Hermes observation is separate from the browser SDK's own rejection handling. */
package/dist/index.web.js CHANGED
@@ -1 +1 @@
1
- import{a as r,c as e,e as o}from"./chunk-ENPQ66EV.js";import{a as t}from"./chunk-XUSV3KOD.js";export*from"@everframe/react";function a(){return o("unsupported","platform")}t(a,"getPromiseRejectionStatus");function s(){return e("unsupported","platform")}t(s,"getErrorCaptureStatus");function n(){return r("unsupported","platform")}t(n,"getReportDeliveryStatus");export{s as getErrorCaptureStatus,a as getPromiseRejectionStatus,n as getReportDeliveryStatus};
1
+ import{a as r,c as e,e as o}from"./chunk-UN3Q4UGZ.js";import{a as t}from"./chunk-XUSV3KOD.js";export*from"@everframe/react";function a(){return o("unsupported","platform")}t(a,"getPromiseRejectionStatus");function s(){return e("unsupported","platform")}t(s,"getErrorCaptureStatus");function n(){return r("unsupported","platform")}t(n,"getReportDeliveryStatus");export{s as getErrorCaptureStatus,a as getPromiseRejectionStatus,n as getReportDeliveryStatus};
@@ -1 +1 @@
1
- import{h as c}from"../chunk-POE5CSO5.js";import"../chunk-ENPQ66EV.js";import{a as i}from"../chunk-XUSV3KOD.js";var g={log:"info",info:"info",warn:"warn",error:"error",debug:"debug"},a=2048;function p(t){let l=[...new Set(t?.levels??["log","info","warn","error"])];return{name:"console",setup(){let n=!1,e=[];for(let o of l){let r=console[o];if(typeof r!="function")continue;e.push({level:o,fn:r});let u=r.bind(console);console[o]=(...f)=>{if(u(...f),!n){n=!0;try{let s=v(f);s&&!s.startsWith("[everframe]")&&c({message:s,kind:"console",level:g[o]})}catch{}finally{n=!1}}}}return()=>{for(let{level:o,fn:r}of e)console[o]=r}}}}i(p,"consoleIntegration");function v(t){let n=t.map(e=>{if(typeof e=="string")return e;if(e instanceof Error)return e.message;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}).join(" ");return n.length>a?n.slice(0,a):n}i(v,"serializeArgs");export{p as consoleIntegration};
1
+ import{j as c}from"../chunk-MIWWEGLO.js";import"../chunk-UN3Q4UGZ.js";import{a as i}from"../chunk-XUSV3KOD.js";var g={log:"info",info:"info",warn:"warn",error:"error",debug:"debug"},a=2048;function p(t){let l=[...new Set(t?.levels??["log","info","warn","error"])];return{name:"console",setup(){let n=!1,e=[];for(let o of l){let r=console[o];if(typeof r!="function")continue;e.push({level:o,fn:r});let u=r.bind(console);console[o]=(...f)=>{if(u(...f),!n){n=!0;try{let s=v(f);s&&!s.startsWith("[everframe]")&&c({message:s,kind:"console",level:g[o]})}catch{}finally{n=!1}}}}return()=>{for(let{level:o,fn:r}of e)console[o]=r}}}}i(p,"consoleIntegration");function v(t){let n=t.map(e=>{if(typeof e=="string")return e;if(e instanceof Error)return e.message;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}).join(" ");return n.length>a?n.slice(0,a):n}i(v,"serializeArgs");export{p as consoleIntegration};
@@ -1 +1 @@
1
- import{i}from"../chunk-POE5CSO5.js";import"../chunk-ENPQ66EV.js";import{a as n}from"../chunk-XUSV3KOD.js";function o(t){let e=n(()=>{try{let a=t.navigationRef.getCurrentRoute?.()?.name;a&&i(a)}catch{}},"emit");return{name:"react-navigation",onReady:e,onStateChange:e,setup(){try{t.navigationRef.isReady()&&e()}catch{}try{return t.navigationRef.addListener("state",e)}catch{return}}}}n(o,"reactNavigationIntegration");export{o as reactNavigationIntegration};
1
+ import{k as i}from"../chunk-MIWWEGLO.js";import"../chunk-UN3Q4UGZ.js";import{a as n}from"../chunk-XUSV3KOD.js";function o(t){let e=n(()=>{try{let a=t.navigationRef.getCurrentRoute?.()?.name;a&&i(a)}catch{}},"emit");return{name:"react-navigation",onReady:e,onStateChange:e,setup(){try{t.navigationRef.isReady()&&e()}catch{}try{return t.navigationRef.addListener("state",e)}catch{return}}}}n(o,"reactNavigationIntegration");export{o as reactNavigationIntegration};
@@ -1 +1 @@
1
- import{b as n,e as a}from"../chunk-POE5CSO5.js";import"../chunk-ENPQ66EV.js";import{a as c}from"../chunk-XUSV3KOD.js";var d=Reflect.apply,u;function l(t,i){let r=a();if(!r){s();return}let p=n(),o;try{if(i!=null){let e=Object.getOwnPropertyDescriptor(i,"componentStack");e&&"value"in e&&typeof e.value=="string"&&(o=e.value)}}catch{}try{let e=r.captureException;if(a()!==r||n()!==p)return;d(e,r,[t,{context:"react.error-boundary",...o!==void 0?{metadata:{componentStack:o}}:{}}])}catch{}}c(l,"captureReactError");function s(){try{let t=n();if(u===t||typeof __DEV__>"u"||!__DEV__)return;u=t,console.warn("[everframe] captureReactError: no EverframeProvider is mounted, so this error was not captured. Render the error boundary inside <EverframeProvider>; errors caught during its first commit are not captured.")}catch{}}c(s,"warnNotMounted");export{l as captureReactError};
1
+ import{d as n,g as a}from"../chunk-MIWWEGLO.js";import"../chunk-UN3Q4UGZ.js";import{a as c}from"../chunk-XUSV3KOD.js";var d=Reflect.apply,u;function l(t,i){let r=a();if(!r){s();return}let p=n(),o;try{if(i!=null){let e=Object.getOwnPropertyDescriptor(i,"componentStack");e&&"value"in e&&typeof e.value=="string"&&(o=e.value)}}catch{}try{let e=r.captureException;if(a()!==r||n()!==p)return;d(e,r,[t,{context:"react.error-boundary",...o!==void 0?{metadata:{componentStack:o}}:{}}])}catch{}}c(l,"captureReactError");function s(){try{let t=n();if(u===t||typeof __DEV__>"u"||!__DEV__)return;u=t,console.warn("[everframe] captureReactError: no EverframeProvider is mounted, so this error was not captured. Render the error boundary inside <EverframeProvider>; errors caught during its first commit are not captured.")}catch{}}c(s,"warnNotMounted");export{l as captureReactError};
@@ -10,7 +10,7 @@ type ReportQueueOperation = typeof queueOperations[number];
10
10
  type ReportStorageFailure = typeof storageFailures[number];
11
11
  type ReportTransportOrigin = typeof transportOrigins[number];
12
12
  type ReportTransportOutcome = typeof transportOutcomes[number];
13
- type ReportDeliveryReason = 'none' | 'no-start' | 'capture-disabled' | 'no-mount' | 'platform' | 'native-method-missing' | 'native-call-failed' | 'invalid-native-snapshot' | 'snapshot-busy';
13
+ type ReportDeliveryReason = 'none' | 'no-start' | 'capture-disabled' | 'no-mount' | 'platform' | 'native-call-failed' | 'invalid-native-snapshot' | 'snapshot-busy';
14
14
  interface ReportCapturePathStatus {
15
15
  supported: boolean;
16
16
  settledAttempts: number;
@@ -26,7 +26,6 @@ interface ReportQueueStatus {
26
26
  terminalHttpPolicy: 'retain' | 'attempt-remove' | 'unknown';
27
27
  operations: Record<ReportQueueOperation, number>;
28
28
  lastFailure?: ReportStorageFailure;
29
- migration: 'not-observed' | 'clear' | 'blocked' | 'unknown';
30
29
  }
31
30
  interface ReportTransportStatus {
32
31
  settledAttempts: number;
@@ -61,7 +60,6 @@ interface ErrorCaptureCounters {
61
60
  captureFailed: number;
62
61
  reentrantSuppressed: number;
63
62
  inactiveAborted: number;
64
- legacyAttempted: number;
65
63
  }
66
64
  type ErrorCaptureOutcome = Exclude<keyof ErrorCaptureCounters, 'attempted'>;
67
65
  interface ErrorCaptureStatus {
@@ -7,7 +7,7 @@
7
7
  // new D-decision):
8
8
  //
9
9
  // The RN bridge surface is the Phase 06 5-method TurboModule (configure,
10
- // openReporter, registerSensitiveRect, setExtra, attachReactTree). The native
10
+ // openReporter, registerSensitiveRect, setExtra). The native
11
11
  // EFReporterPresenter owns the entire report UX (capture + annotate + redact +
12
12
  // submit) on iOS/iPadOS. On tvOS the on-device modal reporter was removed —
13
13
  // tvOS hosts call the companion start API and route reporting through the
@@ -26,6 +26,7 @@
26
26
  // • The presenter resolver is installed exactly once per process (idempotent
27
27
  // installer + a local flag for defense-in-depth).
28
28
  import Foundation
29
+ import CoreFoundation
29
30
  import Combine
30
31
  import os.log
31
32
  import UIKit
@@ -44,11 +45,15 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
44
45
 
45
46
  // MARK: - configure
46
47
 
48
+ // Serialize the bridge's start/idempotence decision with synchronous health reservation.
49
+ private static let configureLock = NSRecursiveLock()
50
+
47
51
  @objc public static func configure(
48
- appId: NSString,
52
+ sdkKey: NSString,
49
53
  endpoint: NSString,
50
54
  networkBodiesDisabled: Bool,
51
55
  installIdentifierDisabled: Bool,
56
+ crashReportingDisabled: Bool,
52
57
  attachPinUi: NSString?,
53
58
  companionDeviceId: NSString?,
54
59
  companionBadgeEnabled: Bool,
@@ -57,21 +62,32 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
57
62
  theme: NSDictionary?,
58
63
  vitalsEnabled: NSNumber?,
59
64
  vitalsSampleRate: NSNumber?,
60
- vitalsCaptureSourceQuery: NSNumber?
65
+ vitalsCaptureSourceQuery: NSNumber?,
66
+ debugBuild: NSNumber?,
67
+ releaseHealthEnabled: Bool,
68
+ releaseHealthLoadedBuildId: NSString?,
69
+ releaseHealthUserId: NSString?,
70
+ releaseHealthIncludeDebugBuilds: Bool,
71
+ reporterDisabled: Bool = false
61
72
  ) throws {
73
+ configureLock.lock()
74
+ defer { configureLock.unlock() }
62
75
  // `endpoint` arg is retained for ObjC ABI compatibility with the
63
76
  // codegen .mm — value is ignored. The SDK bakes the ingest URL at
64
77
  // compile time (sdk-ios IngestEndpoint).
65
78
  _ = endpoint
66
- os_log("[everframe] configure appId=%{public}@", log: log, type: .info, appId as String)
79
+ os_log("[everframe] configure sdkKey=%{public}@", log: log, type: .info, sdkKey as String)
67
80
  // Polarity flips exactly once, here: JS sends the veto
68
81
  // (`networkBodiesDisabled`), native stores the permission.
69
- var cfg = EverframeConfig(appId: appId as String)
82
+ var cfg = EverframeConfig(sdkKey: sdkKey as String)
70
83
  cfg.capture.networkBodies = !networkBodiesDisabled
71
84
  // Polarity flips exactly once, here: JS sends the veto
72
85
  // (`installIdentifierDisabled`), native stores the permission —
73
86
  // identical to `networkBodies` above.
74
87
  cfg.installIdentifierEnabled = !installIdentifierDisabled
88
+ // Polarity flips exactly once, here: JS sends the veto
89
+ // (`crashReportingDisabled`), native stores the permission.
90
+ cfg.capture.crash = !crashReportingDisabled
75
91
  // Explicit companion device-identity override (naming spec
76
92
  // 2026-08-24) — read straight off the flat ConfigOpts field, same
77
93
  // shape as attachPinUi below. nil/absent falls through to the
@@ -87,6 +103,9 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
87
103
  // badge) exist, mirroring `companionAttachPinUi` below.
88
104
  cfg.companionBadgeEnabled = companionBadgeEnabled
89
105
  cfg.shakeToReportEnabled = shakeToReportEnabled
106
+ // Polarity flips exactly once, here: JS sends the crash-only switch
107
+ // (`reporterDisabled`), native stores the permission.
108
+ cfg.reporterEnabled = !reporterDisabled
90
109
  cfg.companionBadgePosition = companionBadgePosition as String?
91
110
  // Inline reporter theme (reporter branding spec 2026-08-25, RN
92
111
  // slice). The .mm collapses the 8 flat ConfigOpts `theme*` wire
@@ -106,57 +125,40 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
106
125
  if let vitalsEnabled { cfg.vitals.enabled = vitalsEnabled.boolValue }
107
126
  if let vitalsSampleRate { cfg.vitals.sampleRate = vitalsSampleRate.doubleValue }
108
127
  if let vitalsCaptureSourceQuery { cfg.vitals.captureSourceQuery = vitalsCaptureSourceQuery.boolValue }
128
+ // Absent stays nil, so the native SDK detects a debug build itself; present wins.
129
+ cfg.debugBuild = debugBuild?.boolValue
109
130
  // The attach-PIN UI mode for startCompanion() to read (spec 2026-08-19). configure()
110
131
  // precedes startCompanion() in every host's lifecycle, so this is the natural place to
111
132
  // parse the flat ConfigOpts string into the native enum; startCompanion is where it is
112
133
  // actually consumed (RelayWSClient's announce arg + the suppression seam), since a mode
113
- // can't affect anything before a relay client exists. Parsed into a LOCAL: the gate
114
- // below compares it against the stashed value, and only the start path adopts it.
134
+ // can't affect anything before a relay client exists. A changed value forces native
135
+ // reconfiguration even when the SDK configuration itself compares equal.
115
136
  let newAttachPinUi = Self.parseAttachPinUi(attachPinUi as String?)
116
- // Codex round-6, H1 — IDEMPOTENT configure, twin of `EverframeModule.configure`'s gate on
117
- // Android, and decided against the INSTALLED config rather than a cache of the last
118
- // options this bridge started with.
119
- //
120
- // `Everframe.shared.start(...)` SUPERSEDES the running SDK: the outgoing controller's
121
- // shutdown detaches every integration it had announced, so a Provider that merely
122
- // REMOUNTS with the same config used to kill every live player registration for
123
- // nothing. An identical configuration installs identical state, so the cheapest correct
124
- // answer is not to start at all.
125
- //
126
- // Round-5 kept a cached snapshot of the options it last started with, and a cache can
127
- // disagree with the singleton it claims to describe: two overlapping configures (A then
128
- // B) can interleave so the cache says A while the SDK runs B, and a native host calling
129
- // `Everframe.shared.start(configB)` between two bridge configures of A reproduces it with
130
- // no concurrency at all — the second A matches the stale cached A, skips, and leaves B
131
- // installed under a JS host that believes it configured A. `currentConfig` cannot drift:
132
- // it IS the installed state.
133
- //
134
- // `captureGate` is the second term: a killed SDK must be restarted even by an identical
135
- // config, or the host is left with a dead SDK and no way to revive it.
136
- //
137
- // Codex round-7, I3 — the comparison is the WHOLE config (`EverframeConfig` is now
138
- // `Equatable`, matching Android's `data class`), not a rendering of the fields this
139
- // bridge happens to set. Round-6 hand-built a string snapshot here, which meant any
140
- // config field the snapshot's author had not listed — a new one, or one a native host
141
- // set directly — read as "unchanged" and silently suppressed a start that was needed.
142
- // `attachPinUi` is the one term that is NOT part of the config (it lands in module
143
- // state), so it is compared separately.
144
- //
145
- // Everything else this method does still runs on the skip path — the presenter-resolver
146
- // install and the crash-metadata prime — because both are idempotent and neither
147
- // depends on a fresh start. The attach-PIN stash is the exception, and only nominally:
148
- // on the skip path it would write the value it already holds.
149
- let unchanged = Everframe.shared.captureGate
150
- && Everframe.shared.currentConfig == cfg
151
- && companionAttachPinUi == newAttachPinUi
152
- if unchanged {
153
- os_log("[everframe] configure: SDK already running this exact config — skipping start()",
154
- log: log, type: .debug)
137
+ let health: ReleaseHealthConfiguration?
138
+ if releaseHealthEnabled, let loaded = releaseHealthLoadedBuildId {
139
+ // The SDK reads the app version itself; the bridge carries the loaded bundle, the user ID and
140
+ // whether debug builds count. A nil value (disabled, no loaded bundle, or an invalid identity) is off.
141
+ health = try? ReleaseHealthConfiguration(userId: releaseHealthUserId as String?,
142
+ loadedBuildId: loaded as String, loadedBundleStatus: .known,
143
+ includeDebugBuilds: releaseHealthIncludeDebugBuilds)
155
144
  } else {
156
- try Everframe.shared.start(config: cfg)
157
- // Adopted only on the start path — the skip path has already established that the
158
- // stashed value equals this one.
159
- companionAttachPinUi = newAttachPinUi
145
+ health = nil
146
+ }
147
+ // The bridge owns release health through `health` alone: it needs the loaded Hermes bundle, and a new
148
+ // user ID must reconfigure health without restarting the SDK. The native default
149
+ // (`EverframeConfig.releaseHealth`, on) must not arm anonymous sessions at start or take part in
150
+ // the unchanged-configuration comparison.
151
+ cfg.releaseHealth = nil
152
+ // Native code atomically checks installed config and reserves its exact owner.
153
+ // An identical configure preserves the session; a changed attach-PIN mode still
154
+ // requires the same full restart as the other native configuration changes.
155
+ let applyHealth = try Everframe.shared.configureAndPrepareReleaseHealth(
156
+ config: cfg, health: health, forceRestart: companionAttachPinUi != newAttachPinUi)
157
+ companionAttachPinUi = newAttachPinUi
158
+ // Reservation is tied to this configure's exact native start generation. Later
159
+ // configure/disable or a native restart invalidates the asynchronous operation.
160
+ if let applyHealth {
161
+ Task { _ = await applyHealth() }
160
162
  }
161
163
  installPresenterResolverIfNeeded()
162
164
  // Spec 2026-09-17 setExtra-resolver — installed unconditionally,
@@ -219,9 +221,9 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
219
221
  /// Wires `EFReporterPresenter` as the resolver for `Everframe.shared.report
220
222
  /// .open()`. Called from configure(); the first call installs, subsequent
221
223
  /// calls no-op. tvOS has no on-device modal — reporting goes through the
222
- /// phone-companion flow (QR → phone browser) — so the presenter type
223
- /// itself is `#if !os(tvOS)`-gated in EverframeReporterUI. Mirror that
224
- /// gate here so the bridge compiles for the tvOS slice.
224
+ /// phone-companion flow (QR → phone browser) — so EverframeReporterUI's
225
+ /// tvOS `installResolver()` is a no-op. The gate here skips that call and
226
+ /// logs where tvOS reporting goes instead.
225
227
  private static func installPresenterResolverIfNeeded() {
226
228
  #if !os(tvOS)
227
229
  resolverInstallLock.lock()
@@ -278,7 +280,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
278
280
  /// every `configure()` is cheap, harmless churn, same as Android already
279
281
  /// accepts on every module construction.
280
282
  ///
281
- /// Returns the fresh generation so `EverframeModule.mm`'s `-configure:`
283
+ /// Returns the fresh generation so `EverframeModule.mm`'s `-configureSync:`
282
284
  /// can capture it (via `currentExtraResolveHookGeneration()` below) and
283
285
  /// hand it back to `resetExtraResolverForTeardown(_:)` at `-invalidate`
284
286
  /// time. `@discardableResult` because `configure()` itself has no use
@@ -295,8 +297,8 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
295
297
 
296
298
  /// ObjC-visible read of whichever generation is live RIGHT NOW — i.e.
297
299
  /// whatever the most recent `installExtraResolveHook()` call minted.
298
- /// `EverframeModule.mm`'s `-configure:` calls this immediately after
299
- /// `configureWithAppId:...:error:` (which, on success or on the
300
+ /// `EverframeModule.mm`'s `-configureSync:` calls this immediately after
301
+ /// `configureWithSdkKey:...:error:` (which, on success or on the
300
302
  /// already-running-unchanged skip path, calls `installExtraResolveHook()`
301
303
  /// internally) and stashes the result on its own instance ivar for
302
304
  /// `-invalidate` to hand back later. 0 means no `configure()` call has
@@ -400,7 +402,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
400
402
  /// `RemoteVitalsBridge.reset()`'s own lifetime doc comment.
401
403
  ///
402
404
  /// IDENTITY GUARD (D1 fix — stale-teardown race). `generation` is the
403
- /// value `EverframeModule.mm`'s `-configure:` captured via
405
+ /// value `EverframeModule.mm`'s `-configureSync:` captured via
404
406
  /// `currentExtraResolveHookGeneration()` right after ITS OWN
405
407
  /// `configure()` call, stashed on that ObjC instance and handed back
406
408
  /// here at `-invalidate` time. RN dispatches `-invalidate` on a
@@ -562,12 +564,29 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
562
564
  dict = ["status": "cancelled"]
563
565
  }
564
566
  completion(dict, nil)
567
+ } catch EverframeReporterError.reporterDisabled {
568
+ completion(nil, NSError(
569
+ domain: "EverframeBridge", code: reporterDisabledErrorCode,
570
+ userInfo: [NSLocalizedDescriptionKey: EverframeReporterError.reporterDisabled.localizedDescription]
571
+ ))
565
572
  } catch {
566
573
  completion(nil, error as NSError)
567
574
  }
568
575
  }
569
576
  }
570
577
 
578
+ /// `openReporter`'s error for a crash-only session (`reporterDisabled: true`).
579
+ static let reporterDisabledErrorCode = 1101
580
+
581
+ /// The promise rejection code for an `openReporter` failure. A crash-only session gets
582
+ /// `EVERFRAME_REPORTER_DISABLED`, which the JS facade maps to
583
+ /// `EverframeReporterDisabledError`; anything else keeps `open_reporter_failed`.
584
+ @objc(openReporterRejectCodeFor:)
585
+ public static func openReporterRejectCode(for error: NSError) -> String {
586
+ error.domain == "EverframeBridge" && error.code == reporterDisabledErrorCode
587
+ ? "EVERFRAME_REPORTER_DISABLED" : "open_reporter_failed"
588
+ }
589
+
571
590
  // MARK: - registerSensitiveRect
572
591
 
573
592
  /// Mark a view as sensitive. Routed through MainActor.assumeIsolated so the
@@ -604,12 +623,31 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
604
623
  /// `everframe-react-native` (distinct from `CrashReporter.captureFacts`'s
605
624
  /// `"everframe-ios"` default) so envelopes are attributable to the RN SDK.
606
625
  @objc public static func reportCrash(_ crashJson: NSString) -> Bool {
607
- CrashReporter.captureFacts(json: crashJson as String, sdkName: "everframe-react-native")
626
+ let facts = crashJson as String
627
+ // Replays on errors (spec 2026-10-11): only an error the app survives asks for a replay of the seconds
628
+ // before it. A fatal JavaScript error ends the app, so it is a crash and never carries one.
629
+ guard appSurvivesError(facts) else {
630
+ return CrashReporter.captureFacts(json: facts, sdkName: "everframe-react-native", attachReplay: false)
631
+ }
632
+ return CrashReporter.captureFacts(json: facts, sdkName: "everframe-react-native", attachReplay: true)
608
633
  }
609
634
 
610
- /// Explicit handled capture acknowledges the core's completed encrypted enqueue.
635
+ /// Explicit handled capture acknowledges the core's completed encrypted enqueue. It is always an error the
636
+ /// app survives, so it asks for a replay; the core decides whether the session qualifies.
611
637
  @objc public static func captureHandledException(_ crashJson: NSString) -> Bool {
612
- CrashReporter.captureHandledFacts(json: crashJson as String, sdkName: "everframe-react-native")
638
+ CrashReporter.captureHandledFacts(json: crashJson as String, sdkName: "everframe-react-native", attachReplay: true)
639
+ }
640
+
641
+ /// True when the facts describe an error the app survives: `fatal` is absent or the JSON boolean `false`,
642
+ /// which is how `CrashReporter` classifies facts that are not marked fatal. Anything unreadable counts as
643
+ /// fatal here, so it can only lose a replay, never ask for one on a crash.
644
+ static func appSurvivesError(_ facts: String) -> Bool {
645
+ guard let data = facts.data(using: .utf8),
646
+ let object = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else { return false }
647
+ guard let flag = object["fatal"] else { return true }
648
+ guard let boolean = flag as? NSNumber,
649
+ CFGetTypeID(boolean) == CFBooleanGetTypeID() else { return false }
650
+ return !boolean.boolValue
613
651
  }
614
652
 
615
653
  // MARK: - setExtra
@@ -631,7 +669,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
631
669
  /// `Everframe.shared.addBreadcrumb(message:kind:level:data:)` (Task 5) —
632
670
  /// NO validation/coercion happens here. That singleton already owns
633
671
  /// kind/level/data coercion AND gating (no-op pre-start / while killed),
634
- /// exactly like `setExtra`/`attachReactTree` above never re-check
672
+ /// exactly like `setExtra` above never re-checks
635
673
  /// `captureGate` themselves. `data` crosses as an `NSDictionary?` — the
636
674
  /// codegen-typed `UnsafeObject` boundary for freeform objects; a
637
675
  /// non-dictionary payload (shouldn't happen via codegen, but fail-soft
@@ -655,8 +693,8 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
655
693
 
656
694
  /// Forward a host-declared user from JS into `Everframe.shared.setUser`.
657
695
  /// Pure pass-through: the native singleton owns storage — NO
658
- /// validation/coercion happens here, same contract as `setExtra`/
659
- /// `attachReactTree` above. `nil` clears the active user (the bridge
696
+ /// validation/coercion happens here, same contract as `setExtra`
697
+ /// above. `nil` clears the active user (the bridge
660
698
  /// forbids `T | null`, so JS expresses "clear" by calling with no
661
699
  /// argument, which crosses as `nil` here). A missing key on the incoming
662
700
  /// dictionary must become `nil`, never an empty string — that's what
@@ -749,9 +787,7 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
749
787
  nonisolated(unsafe) private static var companionClient: RelayWSClient?
750
788
  /// The host's configured attach-PIN UI mode (spec 2026-08-19), parsed
751
789
  /// from `ConfigOpts.attachPinUi` at `configure()` time. Defaults to
752
- /// `.builtin` — the pre-existing-behaviour default for a host that never
753
- /// set the flag (including every host on an older RN SDK version whose
754
- /// `configure()` call carries no such field at all).
790
+ /// `.builtin` for a host that never sets the flag.
755
791
  nonisolated(unsafe) private static var companionAttachPinUi: AttachPinUi = .builtin
756
792
  // `CompanionCaptureBridge` subscribes to the report.request notification
757
793
  // posted by RelayWSClient and runs the capture+send dance. Without an
@@ -853,10 +889,10 @@ import EverframeReporterUI // tvOS slice present per Package.swift:12 (TV-03
853
889
  /// presence", never "no socket": RelayWSClient skips the announce hop
854
890
  /// entirely on a nil key and reporting is unaffected.
855
891
  private static func configuredSdkKey() -> String? {
856
- guard let appId = Everframe.shared.currentConfig?.appId,
857
- !appId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
892
+ guard let sdkKey = Everframe.shared.currentConfig?.sdkKey,
893
+ !sdkKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
858
894
  else { return nil }
859
- return appId
895
+ return sdkKey
860
896
  }
861
897
 
862
898
  /// Non-personal device label for the dashboard's device list — "Apple TV",
@@ -70,11 +70,9 @@ public final class EverframeEventEmitter: RCTEventEmitter {
70
70
  /// Payload: `{ code, requestedByName, ttlMs }` | NSNull.
71
71
  public static let attachChallengeEvent = "everframe.companion.attachChallenge"
72
72
 
73
- /// Fired when a `report.request` arrives from the paired phone. This
74
- /// used to give JS a window to walk the React fiber tree and call
75
- /// `attachReactTree`; both are gone (spec 2026-08-29). The event and
76
- /// the bridge's brief wait survive as the companion's JS-ready
77
- /// handshake (see CompanionCaptureBridge.awaitJsReactTreeAttach).
73
+ /// Fired when a `report.request` arrives from the paired phone. The event
74
+ /// and the bridge's brief wait are the companion's JS-ready handshake
75
+ /// (see CompanionCaptureBridge.awaitJsReactTreeAttach).
78
76
  /// Payload: String — the correlation_id of the inbound request.
79
77
  public static let reportRequestedEvent = "everframe.companion.reportRequested"
80
78
 
@@ -6,7 +6,7 @@
6
6
  // to the Swift glue layer (`EverframeBridge` in `EverframeBridge.swift`).
7
7
  //
8
8
  // Threading (RESEARCH §2.5):
9
- // • configure — JS thread (synchronous; <5ms per sdk-ios contract)
9
+ // • configureSync — JS thread (synchronous; <5ms per sdk-ios contract)
10
10
  // • captureNow — main queue (UIKit reads, capture, accessibility walk)
11
11
  // • registerSensitiveRect — main queue (RCTUIManager view lookup)
12
12
  // • submit — background OK (no UIKit reads); bridge runs it
@@ -75,11 +75,11 @@
75
75
  RemoteVitalsBridge *_vitals;
76
76
  // D1 fix (stale-teardown identity guard, spec 2026-09-17 setExtra-resolver).
77
77
  // Snapshot of `[EverframeBridge currentExtraResolveHookGeneration]` taken
78
- // right after THIS instance's most recent `-configure:` call — see
78
+ // right after THIS instance's most recent `-configureSync:` call — see
79
79
  // `EverframeBridge.resetExtraResolverForTeardown(_:)`'s doc comment for why
80
80
  // `-invalidate` must hand this back rather than tearing down unconditionally.
81
81
  // Zero-initialized by ObjC, matching `resetExtraResolverForTeardown(_:)`'s
82
- // "0 = never installed" no-op case for an instance whose `-configure:` was
82
+ // "0 = never installed" no-op case for an instance whose `-configureSync:` was
83
83
  // never called (or never reached the install site) before `-invalidate`.
84
84
  NSInteger _extraResolveHookGeneration;
85
85
  }
@@ -106,11 +106,13 @@ RCT_EXPORT_MODULE(Everframe)
106
106
  _bridge = bridge;
107
107
  }
108
108
 
109
- // MARK: - configure(opts)
109
+ // MARK: - configureSync(opts)
110
110
 
111
111
  // Signature MUST match the codegen-emitted protocol in EverframeSpec.h:
112
- // - (void)configure:(JS::NativeEverframe::ConfigOpts &)opts;
113
- // Field accessors (.apiKey(), .sdkVersion(), .captureScreenshot(),
112
+ // - (NSNumber *)configureSync:(JS::NativeEverframe::ConfigOpts &)opts;
113
+ // Blocking rather than void: React Native dispatches void TurboModule methods
114
+ // asynchronously, and JS's first capture after mount must not overtake it.
115
+ // Field accessors (.sdkKey(), .sdkVersion(), .captureScreenshot(),
114
116
  // .networkBodiesDisabled()) are inline methods on the JS::NativeEverframe::
115
117
  // ConfigOpts struct, generated from the TS spec in src/NativeEverframe.ts.
116
118
  // The ingest URL is no longer JS-supplied — the native SDK bakes it at
@@ -120,16 +122,18 @@ RCT_EXPORT_MODULE(Everframe)
120
122
  // into ConfigOpts by extractBridgeConfig() but is never read below, so it is
121
123
  // silently dropped on iOS today. Task 9 (spec 2026-08-12) only wires
122
124
  // `networkBodiesDisabled` through; see task-9-report.md.
123
- - (void)configure:(JS::NativeEverframe::ConfigOpts &)opts {
124
- NSString *appId = opts.apiKey() ?: @"";
125
+ - (NSNumber *)configureSync:(JS::NativeEverframe::ConfigOpts &)opts {
126
+ NSString *sdkKey = opts.sdkKey() ?: @"";
125
127
  BOOL networkBodiesDisabled = opts.networkBodiesDisabled().value_or(false);
126
128
  // `installIdentifierDisabled` (MAI meter spec 2026-08-27) — `std::optional
127
129
  // <bool>`, same shape as `networkBodiesDisabled()` above. Absent means the
128
130
  // host said nothing, which is "send it": the SDK default is ON.
129
131
  BOOL installIdentifierDisabled = opts.installIdentifierDisabled().value_or(false);
132
+ // `crashReportingDisabled` — same `std::optional<bool>` shape. Absent keeps capture on.
133
+ BOOL crashReportingDisabled = opts.crashReportingDisabled().value_or(false);
130
134
  // `attachPinUi` (spec 2026-08-19) is a plain optional STRING field —
131
135
  // `opts.attachPinUi()` returns a nullable NSString*, same shape as
132
- // `opts.apiKey()` above, NOT the `std::optional<bool>` shape
136
+ // `opts.sdkKey()` above, NOT the `std::optional<bool>` shape
133
137
  // `networkBodiesDisabled()` uses. `EverframeBridge.configure` coerces a
134
138
  // nil/unrecognised value to `.builtin`.
135
139
  NSString *attachPinUi = opts.attachPinUi();
@@ -149,6 +153,9 @@ RCT_EXPORT_MODULE(Everframe)
149
153
  // above; `EverframeBridge` coerces nil/unrecognised to `.bottomRight`.
150
154
  BOOL companionBadgeEnabled = opts.companionBadgeEnabled().value_or(true);
151
155
  BOOL shakeToReportEnabled = opts.shakeToReportEnabled().value_or(true);
156
+ // Crash-only session switch (spec 2026-10-12) — `std::optional<bool>`, same shape as
157
+ // `networkBodiesDisabled()`. Absent means a normal report session.
158
+ BOOL reporterDisabled = opts.reporterDisabled().value_or(false);
152
159
  NSString *companionBadgePosition = opts.companionBadgePosition();
153
160
  // Inline reporter theme (reporter branding spec 2026-08-25, RN slice) —
154
161
  // 8 flat optional STRING fields on ConfigOpts (RN codegen cannot express
@@ -179,11 +186,17 @@ RCT_EXPORT_MODULE(Everframe)
179
186
  NSNumber *vitalsEnabled = opts.vitalsEnabled().has_value() ? @(opts.vitalsEnabled().value()) : nil;
180
187
  NSNumber *vitalsSampleRate = opts.vitalsSampleRate().has_value() ? @(opts.vitalsSampleRate().value()) : nil;
181
188
  NSNumber *vitalsCaptureSourceQuery = opts.vitalsCaptureSourceQuery().has_value() ? @(opts.vitalsCaptureSourceQuery().value()) : nil;
189
+ // `debugBuild` stays nil when absent so the native SDK decides for itself; the JS facade always sends it.
190
+ NSNumber *debugBuild = opts.debugBuild().has_value() ? @(opts.debugBuild().value()) : nil;
191
+ // Release health is on by default: a missing `releaseHealthEnabled` is on and only an explicit NO is
192
+ // off. The JS facade always sends the flag, so a missing value can only come from another caller.
193
+ // `releaseHealthIncludeDebugBuilds` is false when missing, matching the native SDK default.
182
194
  NSError *err = nil;
183
- [EverframeBridge configureWithAppId:appId
195
+ [EverframeBridge configureWithSdkKey:sdkKey
184
196
  endpoint:@""
185
197
  networkBodiesDisabled:networkBodiesDisabled
186
198
  installIdentifierDisabled:installIdentifierDisabled
199
+ crashReportingDisabled:crashReportingDisabled
187
200
  attachPinUi:attachPinUi
188
201
  companionDeviceId:companionDeviceId
189
202
  companionBadgeEnabled:companionBadgeEnabled
@@ -193,9 +206,15 @@ RCT_EXPORT_MODULE(Everframe)
193
206
  vitalsEnabled:vitalsEnabled
194
207
  vitalsSampleRate:vitalsSampleRate
195
208
  vitalsCaptureSourceQuery:vitalsCaptureSourceQuery
209
+ debugBuild:debugBuild
210
+ releaseHealthEnabled:opts.releaseHealthEnabled().value_or(true)
211
+ releaseHealthLoadedBuildId:opts.releaseHealthLoadedBuildId()
212
+ releaseHealthUserId:opts.releaseHealthUserId()
213
+ releaseHealthIncludeDebugBuilds:opts.releaseHealthIncludeDebugBuilds().value_or(false)
214
+ reporterDisabled:reporterDisabled
196
215
  error:&err];
197
216
  if (err != nil) {
198
- // Configure is void-typed at the spec; we can't reject a promise here.
217
+ // A refused configuration is logged; JS keeps its mount either way.
199
218
  RCTLogWarn(@"[everframe] configure failed: %@", err.localizedDescription);
200
219
  }
201
220
  // D1 fix (stale-teardown identity guard, spec 2026-09-17 setExtra-resolver).
@@ -211,13 +230,6 @@ RCT_EXPORT_MODULE(Everframe)
211
230
  // the effective options are unchanged and the SDK is already started, and when it does
212
231
  // restart, the JS `configureEpoch` bump re-runs every vitals hook effect — each one
213
232
  // 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
233
  return @YES;
222
234
  }
223
235
 
@@ -229,7 +241,9 @@ RCT_EXPORT_MODULE(Everframe)
229
241
  reject:(RCTPromiseRejectBlock)reject {
230
242
  [EverframeBridge openReporterWithCompletion:^(NSDictionary *result, NSError *err) {
231
243
  if (err) {
232
- reject(@"open_reporter_failed", err.localizedDescription, err);
244
+ // `EVERFRAME_REPORTER_DISABLED` for a crash-only session (the JS facade maps it to
245
+ // `EverframeReporterDisabledError`); `open_reporter_failed` for every other failure.
246
+ reject([EverframeBridge openReporterRejectCodeFor:err], err.localizedDescription, err);
233
247
  } else {
234
248
  resolve(result ?: @{});
235
249
  }
@@ -390,7 +404,7 @@ RCT_EXPORT_MODULE(Everframe)
390
404
  // full 250ms ask-and-wait for an ack nobody will ever send.
391
405
  //
392
406
  // D1 fix: `_extraResolveHookGeneration` (captured at the end of
393
- // `-configure:`) travels with this instance so the Swift side can tell a
407
+ // `-configureSync:`) travels with this instance so the Swift side can tell a
394
408
  // STALE instance's `-invalidate` — delivered late, on this module's
395
409
  // method queue, after a reloaded bundle's fresh instance already
396
410
  // reconfigured — from the CURRENT one, and no-op the former. See
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everframe/react-native",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "React Native SDK for Everframe — AI-ready in-app bug reporting, bridging the native iOS and Android reporter to your RN app. Supports phone, Apple TV, and Android TV.",
5
5
  "author": {
6
6
  "name": "Scriptx, UAB",
@@ -100,7 +100,7 @@
100
100
  "dependencies": {
101
101
  "bippy": "^0.5.39",
102
102
  "zod": "^4.3.0",
103
- "@everframe/react": "^1.1.0"
103
+ "@everframe/react": "^1.2.1"
104
104
  },
105
105
  "devDependencies": {
106
106
  "@arethetypeswrong/cli": "^0.18.0",
@@ -112,8 +112,8 @@
112
112
  "publint": "^0.3.0",
113
113
  "tsup": "^8.0.0",
114
114
  "vitest": "^4.0.0",
115
- "@everframe/sdk-core": "0.5.0",
116
- "@everframe/protocol": "0.6.0"
115
+ "@everframe/sdk-core": "0.6.0",
116
+ "@everframe/protocol": "0.7.0"
117
117
  },
118
118
  "codegenConfig": {
119
119
  "name": "EverframeSpec",