@scalebun/react-native 1.10.7 → 1.11.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.
Files changed (54) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
  2. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
  3. package/dist/scalebun.full.js +467 -255
  4. package/dist/scalebun.slim.js +466 -254
  5. package/ios/Capture/InteractionTracker.swift +8 -4
  6. package/ios/Ota/OtaSlotManager.swift +19 -5
  7. package/lib/commonjs/analytics/EventTracker.js +5 -5
  8. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  9. package/lib/commonjs/core/constants/version.js +7 -2
  10. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
  11. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  12. package/lib/commonjs/features/journey/uiState.js +8 -1
  13. package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
  14. package/lib/commonjs/features/ota/OtaTypes.js +4 -0
  15. package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
  16. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  17. package/lib/commonjs/features/session/SessionManager.js +37 -38
  18. package/lib/commonjs/public/ScaleBunFacade.js +115 -2
  19. package/lib/module/analytics/EventTracker.js +5 -5
  20. package/lib/module/analytics/automaticEvents.js +3 -2
  21. package/lib/module/core/constants/version.js +7 -2
  22. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
  23. package/lib/module/features/journey/interactionProtocol.js +38 -0
  24. package/lib/module/features/journey/uiState.js +8 -1
  25. package/lib/module/features/ota/OtaOrchestrator.js +174 -48
  26. package/lib/module/features/ota/OtaTypes.js +1 -1
  27. package/lib/module/features/ota/useOtaUpdate.js +11 -2
  28. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  29. package/lib/module/features/session/SessionManager.js +37 -38
  30. package/lib/module/public/ScaleBunFacade.js +115 -2
  31. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  32. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  33. package/lib/typescript/core/constants/version.d.ts +7 -2
  34. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  35. package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
  36. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
  37. package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
  38. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  39. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  40. package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
  41. package/package.json +4 -3
  42. package/src/analytics/EventTracker.ts +5 -5
  43. package/src/analytics/automaticEvents.ts +4 -0
  44. package/src/core/constants/version.ts +7 -2
  45. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
  46. package/src/features/journey/interactionProtocol.ts +65 -0
  47. package/src/features/journey/uiState.ts +9 -4
  48. package/src/features/ota/OtaEventEmitter.ts +12 -0
  49. package/src/features/ota/OtaOrchestrator.ts +209 -62
  50. package/src/features/ota/OtaTypes.ts +37 -39
  51. package/src/features/ota/useOtaUpdate.ts +11 -2
  52. package/src/features/session/JourneyEventPipeline.ts +7 -5
  53. package/src/features/session/SessionManager.ts +75 -38
  54. package/src/public/ScaleBunFacade.ts +127 -3
@@ -600,7 +600,7 @@ var SDK_VERSION;
600
600
  var init_version = __esm({
601
601
  "lib/module/core/constants/version.js"() {
602
602
  "use strict";
603
- SDK_VERSION = "1.10.7";
603
+ SDK_VERSION = "1.11.1";
604
604
  }
605
605
  });
606
606
 
@@ -2459,22 +2459,23 @@ var init_JourneyEventPipeline = __esm({
2459
2459
  emit(type, opts) {
2460
2460
  try {
2461
2461
  const key = `${type}:${opts?.subtype ?? ""}`;
2462
- const now2 = Date.now();
2463
- if (key === this.lastEventKey && now2 - this.lastEventTs < this.config.dedupeWindowMs) {
2462
+ const receivedAt = Date.now();
2463
+ const occurredAt = opts?.timestamp ?? receivedAt;
2464
+ if (key === this.lastEventKey && receivedAt - this.lastEventTs < this.config.dedupeWindowMs) {
2464
2465
  const incomingConfidence = opts?.payload?.confidence;
2465
2466
  if (incomingConfidence === "high" && this.lastEventConfidence !== "high") {
2466
- this._replaceLastEvent(key, now2, opts);
2467
+ this._replaceLastEvent(key, receivedAt, opts);
2467
2468
  }
2468
2469
  return null;
2469
2470
  }
2470
2471
  this.lastEventKey = key;
2471
- this.lastEventTs = now2;
2472
+ this.lastEventTs = receivedAt;
2472
2473
  this.lastEventConfidence = opts?.payload?.confidence ?? null;
2473
2474
  const event = {
2474
2475
  eventId: generateEventId2(),
2475
2476
  sessionId: this.sessionId,
2476
2477
  journeyId: opts?.journeyId,
2477
- ts: now2,
2478
+ ts: occurredAt,
2478
2479
  type,
2479
2480
  subtype: opts?.subtype,
2480
2481
  severity: opts?.severity ?? inferSeverity(type),
@@ -3574,6 +3575,99 @@ var init_calibrationContext = __esm({
3574
3575
  }
3575
3576
  });
3576
3577
 
3578
+ // lib/module/analytics/automaticEvents.js
3579
+ function compactProperties(properties) {
3580
+ const out = {
3581
+ capture_source: "automatic"
3582
+ };
3583
+ for (const [key, value] of Object.entries(properties ?? {})) {
3584
+ if (value !== void 0) out[key] = value;
3585
+ }
3586
+ return out;
3587
+ }
3588
+ function emitAutomaticEvent(name, properties, timestamp) {
3589
+ const event = {
3590
+ name,
3591
+ properties: compactProperties(properties),
3592
+ timestamp
3593
+ };
3594
+ if (listeners.size === 0) {
3595
+ pending2.push(event);
3596
+ if (pending2.length > MAX_PENDING) pending2.shift();
3597
+ return;
3598
+ }
3599
+ for (const listener of listeners) {
3600
+ try {
3601
+ listener(event);
3602
+ } catch {
3603
+ }
3604
+ }
3605
+ }
3606
+ function subscribeAutomaticEvents(listener) {
3607
+ listeners.add(listener);
3608
+ if (pending2.length > 0) {
3609
+ const buffered = pending2.splice(0, pending2.length);
3610
+ for (const event of buffered) {
3611
+ try {
3612
+ listener(event);
3613
+ } catch {
3614
+ }
3615
+ }
3616
+ }
3617
+ return () => {
3618
+ listeners.delete(listener);
3619
+ };
3620
+ }
3621
+ var listeners, pending2, MAX_PENDING;
3622
+ var init_automaticEvents = __esm({
3623
+ "lib/module/analytics/automaticEvents.js"() {
3624
+ "use strict";
3625
+ listeners = /* @__PURE__ */ new Set();
3626
+ pending2 = [];
3627
+ MAX_PENDING = 50;
3628
+ }
3629
+ });
3630
+
3631
+ // lib/module/features/journey/interactionProtocol.js
3632
+ function generateInteractionId() {
3633
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3634
+ }
3635
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
3636
+ let best;
3637
+ let bestDelta = toleranceMs + 1;
3638
+ for (const start of starts) {
3639
+ const delta = Math.abs(start.occurredAt - occurredAt);
3640
+ if (delta < bestDelta) {
3641
+ best = start;
3642
+ bestDelta = delta;
3643
+ }
3644
+ }
3645
+ return bestDelta <= toleranceMs ? best : void 0;
3646
+ }
3647
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3648
+ return {
3649
+ gesture_type: payload.gestureType,
3650
+ screen_name: screenName,
3651
+ interaction_id: payload.interaction_id,
3652
+ interaction_protocol: payload.interaction_protocol,
3653
+ state_status: payload.state_status,
3654
+ ui: payload.ui,
3655
+ target_id: payload.target_id,
3656
+ normalized_x: payload.normalizedX,
3657
+ normalized_y: payload.normalizedY,
3658
+ direction: payload.direction,
3659
+ duration_ms: payload.durationMs,
3660
+ canonical_mirror: canonicalMirror || void 0
3661
+ };
3662
+ }
3663
+ var INTERACTION_PROTOCOL_VERSION;
3664
+ var init_interactionProtocol = __esm({
3665
+ "lib/module/features/journey/interactionProtocol.js"() {
3666
+ "use strict";
3667
+ INTERACTION_PROTOCOL_VERSION = 1;
3668
+ }
3669
+ });
3670
+
3577
3671
  // lib/module/features/session/SessionManager.js
3578
3672
  var SessionManager_exports = {};
3579
3673
  __export(SessionManager_exports, {
@@ -3582,7 +3676,7 @@ __export(SessionManager_exports, {
3582
3676
  function generateSessionId2() {
3583
3677
  return `ses-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
3584
3678
  }
3585
- var HEATMAP_MAX_INTERACTIONS_PER_WINDOW, _instance2, SessionManager;
3679
+ var _instance2, SessionManager;
3586
3680
  var init_SessionManager = __esm({
3587
3681
  "lib/module/features/session/SessionManager.js"() {
3588
3682
  "use strict";
@@ -3599,7 +3693,8 @@ var init_SessionManager = __esm({
3599
3693
  init_redaction();
3600
3694
  init_calibrationContext();
3601
3695
  init_device();
3602
- HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
3696
+ init_automaticEvents();
3697
+ init_interactionProtocol();
3603
3698
  _instance2 = null;
3604
3699
  SessionManager = class _SessionManager {
3605
3700
  desktopTransport = null;
@@ -3610,15 +3705,6 @@ var init_SessionManager = __esm({
3610
3705
  * lane even when no replay recording is active. Additive, opt-in (default off).
3611
3706
  */
3612
3707
  _captureInteractionHeatmap = false;
3613
- /**
3614
- * Sampling cap for the analytics-lane heatmap emission. Now that capture is
3615
- * ON by default, an unbounded one-event-per-gesture stream could materially
3616
- * inflate ingest volume. We cap emitted interactions per analytics-session
3617
- * window (finalize-scoped per foreground): the first N gestures define the
3618
- * hotspot shape; the long tail is dropped. Resets when the window changes.
3619
- */
3620
- _heatmapWindowSessionId = null;
3621
- _heatmapWindowCount = 0;
3622
3708
  session = null;
3623
3709
  active = false;
3624
3710
  timeoutTimer = null;
@@ -4099,18 +4185,11 @@ var init_SessionManager = __esm({
4099
4185
  * injected sensitive keys (e.g. a label) are stripped before transport.
4100
4186
  * - Never throws.
4101
4187
  */
4102
- _emitInteractionToAnalytics(gestureType, payload) {
4103
- if (!this._captureInteractionHeatmap) return;
4104
- if (this.active) return;
4188
+ _emitInteractionToAnalytics(gestureType, payload, occurredAt, screenName) {
4189
+ if (!this._captureInteractionHeatmap) return false;
4190
+ if (this.active) return false;
4105
4191
  const adapter = this._backendTransport;
4106
- if (!adapter) return;
4107
- const windowId = adapter.analyticsSessionId ?? "";
4108
- if (windowId !== this._heatmapWindowSessionId) {
4109
- this._heatmapWindowSessionId = windowId;
4110
- this._heatmapWindowCount = 0;
4111
- }
4112
- if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
4113
- this._heatmapWindowCount++;
4192
+ if (!adapter) return false;
4114
4193
  try {
4115
4194
  this._normalizeInteractionPayload(payload);
4116
4195
  const safePayload = redactBody(payload);
@@ -4118,16 +4197,18 @@ var init_SessionManager = __esm({
4118
4197
  eventId: generateEventId(),
4119
4198
  // sessionId is (re)stamped by the analytics lane at flush time.
4120
4199
  sessionId: adapter.analyticsSessionId ?? "",
4121
- ts: Date.now(),
4200
+ ts: occurredAt,
4122
4201
  type: "USER_ACTION",
4123
4202
  subtype: `gesture:${gestureType}`,
4124
- screen: this._lastKnownScreen ?? void 0,
4203
+ screen: screenName ?? this._lastKnownScreen ?? void 0,
4125
4204
  payload: safePayload,
4126
4205
  source: "user"
4127
4206
  };
4128
4207
  adapter.trackEvent(event);
4208
+ return true;
4129
4209
  } catch (err) {
4130
4210
  logger.error("[SessionManager] heatmap analytics emit failed:", err);
4211
+ return false;
4131
4212
  }
4132
4213
  }
4133
4214
  /**
@@ -4293,13 +4374,14 @@ var init_SessionManager = __esm({
4293
4374
  * Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
4294
4375
  * Also triggers frame capture for desktop-initiated recordings.
4295
4376
  */
4296
- onUserAction(subtype, payload) {
4377
+ onUserAction(subtype, payload, context) {
4297
4378
  if (!this.active) return;
4298
4379
  this.emitEvent("USER_ACTION", {
4299
4380
  subtype,
4300
- screen: this._lastKnownScreen ?? void 0,
4381
+ screen: context?.screen ?? this._lastKnownScreen ?? void 0,
4301
4382
  payload,
4302
- source: "user"
4383
+ source: "user",
4384
+ timestamp: context?.timestamp
4303
4385
  });
4304
4386
  }
4305
4387
  /**
@@ -4307,7 +4389,8 @@ var init_SessionManager = __esm({
4307
4389
  * Emits a USER_ACTION event with gesture-specific subtype and payload.
4308
4390
  */
4309
4391
  onGestureDetected(gestureType, details) {
4310
- const dedupSig = `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4392
+ const suppliedInteractionId = details?.interactionId;
4393
+ const dedupSig = suppliedInteractionId ? `id:${suppliedInteractionId}` : `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4311
4394
  const nowTs = Date.now();
4312
4395
  if (dedupSig === this._lastGestureSig && nowTs - this._lastGestureTs <= _SessionManager.GESTURE_DEDUP_WINDOW_MS) {
4313
4396
  this._lastGestureTs = nowTs;
@@ -4315,7 +4398,15 @@ var init_SessionManager = __esm({
4315
4398
  }
4316
4399
  this._lastGestureSig = dedupSig;
4317
4400
  this._lastGestureTs = nowTs;
4401
+ const interactionId = suppliedInteractionId ?? generateInteractionId();
4402
+ const occurredAt = details?.occurredAt ?? Date.now();
4403
+ const stateStatus = details?.stateStatus ?? (details?.ui ? "captured_nonempty" : "not_captured");
4318
4404
  const payload = {
4405
+ interaction_id: interactionId,
4406
+ interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
4407
+ state_status: stateStatus,
4408
+ ui: details?.ui,
4409
+ target_id: details?.targetId,
4319
4410
  gestureType,
4320
4411
  x: details?.x,
4321
4412
  y: details?.y,
@@ -4353,12 +4444,23 @@ var init_SessionManager = __esm({
4353
4444
  screenHeight: details?.screenHeight,
4354
4445
  platform: details?.platform
4355
4446
  };
4356
- this._emitInteractionToAnalytics(gestureType, {
4447
+ const analyticsReplayCarrier = this._emitInteractionToAnalytics(gestureType, {
4357
4448
  ...payload
4358
- });
4449
+ }, occurredAt, details?.screenName);
4450
+ const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
4451
+ if (details?.emitAutomaticAnalytics) {
4452
+ try {
4453
+ const safePayload = redactBody(payload);
4454
+ emitAutomaticEvent("element_interacted", automaticInteractionProperties(safePayload, details?.screenName ?? this._lastKnownScreen ?? void 0, replayCarrier), occurredAt);
4455
+ } catch {
4456
+ }
4457
+ }
4359
4458
  if (!this.active) return;
4360
4459
  const target = typeof details?.target === "string" ? details.target.trim() : "";
4361
- this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload);
4460
+ this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload, {
4461
+ screen: details?.screenName,
4462
+ timestamp: occurredAt
4463
+ });
4362
4464
  }
4363
4465
  /**
4364
4466
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -5772,6 +5874,42 @@ var init_bootstrap = __esm({
5772
5874
  }
5773
5875
  });
5774
5876
 
5877
+ // lib/module/core/lifecycle/appLifecycle.js
5878
+ var appLifecycle_exports = {};
5879
+ __export(appLifecycle_exports, {
5880
+ appLifecycle: () => appLifecycle
5881
+ });
5882
+ var import_react_native6, AppLifecycle, appLifecycle;
5883
+ var init_appLifecycle = __esm({
5884
+ "lib/module/core/lifecycle/appLifecycle.js"() {
5885
+ "use strict";
5886
+ import_react_native6 = require("react-native");
5887
+ init_internalLogger();
5888
+ AppLifecycle = class {
5889
+ listeners = [];
5890
+ currentState = import_react_native6.AppState.currentState;
5891
+ constructor() {
5892
+ import_react_native6.AppState.addEventListener("change", this.handleStateChange);
5893
+ }
5894
+ handleStateChange = (nextState) => {
5895
+ __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
5896
+ this.currentState = nextState;
5897
+ this.listeners.forEach((l) => l(nextState));
5898
+ };
5899
+ addListener(listener) {
5900
+ this.listeners.push(listener);
5901
+ }
5902
+ removeListener(listener) {
5903
+ this.listeners = this.listeners.filter((l) => l !== listener);
5904
+ }
5905
+ getCurrentState() {
5906
+ return this.currentState;
5907
+ }
5908
+ };
5909
+ appLifecycle = new AppLifecycle();
5910
+ }
5911
+ });
5912
+
5775
5913
  // lib/module/features/crash/CrashReporter.js
5776
5914
  function getCrashReporter() {
5777
5915
  if (!_reporter) _reporter = new CrashReporter();
@@ -6229,58 +6367,6 @@ var init_batching = __esm({
6229
6367
  }
6230
6368
  });
6231
6369
 
6232
- // lib/module/analytics/automaticEvents.js
6233
- function compactProperties(properties) {
6234
- const out = {
6235
- capture_source: "automatic"
6236
- };
6237
- for (const [key, value] of Object.entries(properties ?? {})) {
6238
- if (value !== void 0) out[key] = value;
6239
- }
6240
- return out;
6241
- }
6242
- function emitAutomaticEvent(name, properties) {
6243
- const event = {
6244
- name,
6245
- properties: compactProperties(properties)
6246
- };
6247
- if (listeners.size === 0) {
6248
- pending3.push(event);
6249
- if (pending3.length > MAX_PENDING2) pending3.shift();
6250
- return;
6251
- }
6252
- for (const listener of listeners) {
6253
- try {
6254
- listener(event);
6255
- } catch {
6256
- }
6257
- }
6258
- }
6259
- function subscribeAutomaticEvents(listener) {
6260
- listeners.add(listener);
6261
- if (pending3.length > 0) {
6262
- const buffered = pending3.splice(0, pending3.length);
6263
- for (const event of buffered) {
6264
- try {
6265
- listener(event);
6266
- } catch {
6267
- }
6268
- }
6269
- }
6270
- return () => {
6271
- listeners.delete(listener);
6272
- };
6273
- }
6274
- var listeners, pending3, MAX_PENDING2;
6275
- var init_automaticEvents = __esm({
6276
- "lib/module/analytics/automaticEvents.js"() {
6277
- "use strict";
6278
- listeners = /* @__PURE__ */ new Set();
6279
- pending3 = [];
6280
- MAX_PENDING2 = 50;
6281
- }
6282
- });
6283
-
6284
6370
  // lib/module/analytics/EventTracker.js
6285
6371
  function uuid() {
6286
6372
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -6349,7 +6435,7 @@ var init_EventTracker = __esm({
6349
6435
  this.started = true;
6350
6436
  if (this.cfg.automaticEventTracking) {
6351
6437
  this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
6352
- this.track(event.name, event.properties);
6438
+ this.track(event.name, event.properties, event.timestamp);
6353
6439
  });
6354
6440
  }
6355
6441
  if (this.cfg.autoLifecycleEvents) {
@@ -6453,9 +6539,9 @@ var init_EventTracker = __esm({
6453
6539
  }
6454
6540
  }
6455
6541
  // ─── tracking ────────────────────────────────────────────────────────────
6456
- track(eventName, properties) {
6542
+ track(eventName, properties, timestamp) {
6457
6543
  try {
6458
- this.enqueue(this.buildEnvelope(eventName, properties));
6544
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
6459
6545
  try {
6460
6546
  this.cfg.onEvent?.(eventName);
6461
6547
  } catch {
@@ -6559,7 +6645,7 @@ var init_EventTracker = __esm({
6559
6645
  };
6560
6646
  }
6561
6647
  // ─── internals ─────────────────────────────────────────────────────────────
6562
- buildEnvelope(eventName, properties) {
6648
+ buildEnvelope(eventName, properties, timestamp) {
6563
6649
  const ctx = this.cfg.context ?? {};
6564
6650
  let canonicalSessionId;
6565
6651
  try {
@@ -6570,7 +6656,7 @@ var init_EventTracker = __esm({
6570
6656
  const env = {
6571
6657
  event_id: uuid(),
6572
6658
  event_name: eventName,
6573
- event_time: Date.now(),
6659
+ event_time: timestamp ?? Date.now(),
6574
6660
  app_id: this.cfg.appId,
6575
6661
  platform: this.cfg.platform ?? resolveEventPlatform(),
6576
6662
  installation_id: this.installationId,
@@ -7422,6 +7508,11 @@ async function deliverOtaEvents(params) {
7422
7508
  kind: "ota_event",
7423
7509
  type: e.type,
7424
7510
  bundleId: e.bundleId,
7511
+ // Without this the backend stored a null releaseId on every row it ingested,
7512
+ // while serving the release id on every check — so the delivery funnel could
7513
+ // only ever be grouped by bundle, and a bundle re-promoted under a second
7514
+ // release merged the two into one indistinguishable series.
7515
+ releaseId: e.releaseId,
7425
7516
  installationId: params.installationId,
7426
7517
  // OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
7427
7518
  // resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
@@ -7523,20 +7614,31 @@ var init_OtaOrchestrator = __esm({
7523
7614
  this.bootGuardConfig = config ?? {};
7524
7615
  this.signatureConfig = config?.signature;
7525
7616
  __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
7526
- this.hydrateCurrentBundleFromSlots();
7617
+ const slotState = this.readSlotState();
7618
+ this.hydrateCurrentBundleFromSlots(slotState);
7527
7619
  void prefetchDeviceCountry();
7620
+ this.checkBootGuardRecovery(slotState);
7528
7621
  this.verifyRunningBundleIdentity();
7529
- this.checkBootGuardRecovery();
7530
7622
  });
7531
7623
  }
7532
7624
  /**
7533
7625
  * Read the active slot back into `currentBundle` so the next check reports
7534
7626
  * what this device is genuinely running.
7535
7627
  */
7536
- hydrateCurrentBundleFromSlots() {
7537
- if (!NativeScaleBunOta_default) return;
7628
+ /**
7629
+ * Parse the native slot state once. Returns null when the module is absent or
7630
+ * the payload is unreadable — every caller treats that as "factory bundle".
7631
+ */
7632
+ readSlotState() {
7633
+ if (!NativeScaleBunOta_default) return null;
7634
+ try {
7635
+ return JSON.parse(NativeScaleBunOta_default.getSlotState());
7636
+ } catch {
7637
+ return null;
7638
+ }
7639
+ }
7640
+ hydrateCurrentBundleFromSlots(state) {
7538
7641
  try {
7539
- const state = JSON.parse(NativeScaleBunOta_default.getSlotState());
7540
7642
  const current = state?.current;
7541
7643
  if (!current?.sha256) return;
7542
7644
  const record4 = this.readInstallRecord();
@@ -7544,6 +7646,7 @@ var init_OtaOrchestrator = __esm({
7544
7646
  this.currentBundle = {
7545
7647
  id: record4.bundleId,
7546
7648
  version: record4.version,
7649
+ releaseId: record4.releaseId ?? void 0,
7547
7650
  sha256: record4.sha256
7548
7651
  };
7549
7652
  __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
@@ -7574,29 +7677,23 @@ var init_OtaOrchestrator = __esm({
7574
7677
  verifyRunningBundleIdentity() {
7575
7678
  if (!this.currentBundle) return;
7576
7679
  const running = readRunningBundleMarker();
7577
- const expected = this.readInstallExpectation();
7578
- if (expected && expected.bundleId !== this.currentBundle.id) {
7680
+ const record4 = this.readInstallRecord();
7681
+ if (!record4) return;
7682
+ if (record4.sha256 !== this.currentBundle.sha256) {
7579
7683
  this.clearInstallExpectation();
7580
7684
  return;
7581
7685
  }
7582
- if (expected) {
7583
- if (running === expected.identityToken) {
7584
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7585
- this.clearInstallExpectation();
7586
- return;
7587
- }
7588
- logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
7589
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7590
- error: `install_not_effective \u2014 expected ${expected.identityToken}, running ${running ?? "none"}`
7591
- });
7686
+ if (!record4.identityToken) return;
7687
+ if (running === record4.identityToken) {
7688
+ __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7689
+ this.retireIdentityToken(record4);
7592
7690
  return;
7593
7691
  }
7594
- if (running && running !== this.currentBundle.id) {
7595
- logger.error(`[OTA] BUNDLE MISMATCH \u2014 slot says ${this.currentBundle.id} is active but the running bundle identifies as ${running}.`);
7596
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7597
- error: `bundle_identity_mismatch \u2014 running ${running}`
7598
- });
7599
- }
7692
+ logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
7693
+ otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7694
+ releaseId: this.currentBundle.releaseId,
7695
+ error: `install_not_effective \u2014 expected ${record4.identityToken}, running ${running ?? "none"}`
7696
+ });
7600
7697
  }
7601
7698
  // ── Install expectation ────────────────────────────────────────────────────
7602
7699
  // Written just before the restart that activates a bundle, read on the next
@@ -7608,6 +7705,11 @@ var init_OtaOrchestrator = __esm({
7608
7705
  this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
7609
7706
  bundleId: bundle.id,
7610
7707
  version: bundle.version,
7708
+ // Carried so telemetry emitted on a LATER launch (a boot-guard
7709
+ // rollback, an ineffective install) can still be attributed to the
7710
+ // release, not merely the bundle. The check response is long gone by
7711
+ // then; this record is the only thing that remembers.
7712
+ releaseId: bundle.releaseId ?? null,
7611
7713
  // The join key back to the native slot, which records sha256 and
7612
7714
  // nothing else identifying.
7613
7715
  sha256: bundle.sha256,
@@ -7626,12 +7728,26 @@ var init_OtaOrchestrator = __esm({
7626
7728
  return null;
7627
7729
  }
7628
7730
  }
7629
- readInstallExpectation() {
7630
- const record4 = this.readInstallRecord();
7631
- return record4 && record4.identityToken ? {
7632
- bundleId: record4.bundleId,
7633
- identityToken: record4.identityToken
7634
- } : null;
7731
+ /**
7732
+ * Drop the identity token once the install has been proven, keeping the rest
7733
+ * of the record.
7734
+ *
7735
+ * The record does two jobs: it proves an install took effect (once), and it
7736
+ * maps the native slot's sha256 back to a bundle id (for the life of that
7737
+ * bundle). Only the first job is finished after a successful verification, so
7738
+ * only the token is retired.
7739
+ */
7740
+ retireIdentityToken(record4) {
7741
+ try {
7742
+ this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
7743
+ bundleId: record4.bundleId,
7744
+ version: record4.version,
7745
+ releaseId: record4.releaseId ?? null,
7746
+ sha256: record4.sha256,
7747
+ identityToken: null
7748
+ }));
7749
+ } catch {
7750
+ }
7635
7751
  }
7636
7752
  clearInstallExpectation() {
7637
7753
  try {
@@ -7648,17 +7764,24 @@ var init_OtaOrchestrator = __esm({
7648
7764
  * If getSlotState() shows bootMarkerPresent=false but we have a 'previous' slot
7649
7765
  * and no 'current' OTA bundle, the native layer already reverted.
7650
7766
  */
7651
- checkBootGuardRecovery() {
7652
- if (!NativeScaleBunOta_default) return;
7767
+ checkBootGuardRecovery(state) {
7653
7768
  try {
7654
- const stateJson = NativeScaleBunOta_default.getSlotState();
7655
- const state = JSON.parse(stateJson);
7656
- if (state.bootGuardReverted) {
7657
- logger.warn("[OTA] Boot guard fired \u2014 app was reverted to previous bundle");
7658
- otaEventEmitter.emitSimple("AUTO_ROLLBACK", state.previous?.bundleId ?? "unknown", {
7659
- reason: "boot_guard_crash_loop_detected"
7769
+ if (!state?.bootGuardReverted) return;
7770
+ const record4 = this.readInstallRecord();
7771
+ const revertedSha = state.bootGuardRevertedSha256;
7772
+ const matchesRecord = !!record4 && (!revertedSha || record4.sha256 === revertedSha);
7773
+ const reason = state.bootGuardRevertReason || "boot_crash_guard";
7774
+ if (matchesRecord && record4) {
7775
+ logger.warn(`[OTA] Boot guard fired \u2014 reverted away from bundle ${record4.bundleId} (v${record4.version}); reason: ${reason}`);
7776
+ otaEventEmitter.emitSimple("AUTO_ROLLBACK", record4.bundleId, {
7777
+ releaseId: record4.releaseId ?? void 0,
7778
+ version: record4.version,
7779
+ reason
7660
7780
  });
7781
+ this.clearInstallExpectation();
7782
+ return;
7661
7783
  }
7784
+ logger.warn(`[OTA] Boot guard fired (reason: ${reason}) but the rolled-back bundle could not be identified locally \u2014 no install record. The rollback is not reported to the server.`);
7662
7785
  } catch {
7663
7786
  }
7664
7787
  }
@@ -7749,6 +7872,12 @@ var init_OtaOrchestrator = __esm({
7749
7872
  };
7750
7873
  }
7751
7874
  __DEV__ && logger.debug("[OTA] Sync started\u2026");
7875
+ if (this.currentBundle?.id) {
7876
+ otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
7877
+ releaseId: this.currentBundle.releaseId,
7878
+ version: this.currentBundle.version
7879
+ });
7880
+ }
7752
7881
  const checkRes = await this.checkForUpdate(params);
7753
7882
  if (checkRes.action === "NONE") {
7754
7883
  __DEV__ && logger.debug("[OTA] App is up to date");
@@ -7759,7 +7888,9 @@ var init_OtaOrchestrator = __esm({
7759
7888
  }
7760
7889
  if (checkRes.action === "ROLLBACK") {
7761
7890
  logger.warn("[OTA] Server requested ROLLBACK \u2014 reverting to previous bundle");
7762
- otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown");
7891
+ otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown", {
7892
+ releaseId: this.currentBundle?.releaseId
7893
+ });
7763
7894
  const reverted = await NativeScaleBunOta_default.revertToPrevious();
7764
7895
  if (reverted) {
7765
7896
  this.currentBundle = null;
@@ -7783,9 +7914,14 @@ var init_OtaOrchestrator = __esm({
7783
7914
  }
7784
7915
  const bundle = checkRes.bundle;
7785
7916
  let patchUsed = false;
7917
+ otaEventEmitter.emitSimple("OFFERED", bundle.id, {
7918
+ releaseId: bundle.releaseId,
7919
+ version: bundle.version
7920
+ });
7786
7921
  const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
7787
7922
  if (!signatureOutcome.ok) {
7788
7923
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
7924
+ releaseId: bundle.releaseId,
7789
7925
  error: `Signature check failed: ${signatureOutcome.reason}`,
7790
7926
  version: bundle.version
7791
7927
  });
@@ -7796,6 +7932,7 @@ var init_OtaOrchestrator = __esm({
7796
7932
  };
7797
7933
  }
7798
7934
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
7935
+ releaseId: bundle.releaseId,
7799
7936
  version: bundle.version
7800
7937
  });
7801
7938
  const downloadStart = Date.now();
@@ -7881,6 +8018,7 @@ var init_OtaOrchestrator = __esm({
7881
8018
  postProgress(0, "FAILED");
7882
8019
  logger.error("[OTA] Staging bundle failed after retries");
7883
8020
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
8021
+ releaseId: bundle.releaseId,
7884
8022
  error: "Staging failed \u2014 SHA-256 mismatch or download error",
7885
8023
  version: bundle.version
7886
8024
  });
@@ -7893,6 +8031,7 @@ var init_OtaOrchestrator = __esm({
7893
8031
  postProgress(bundle.size, "COMPLETED");
7894
8032
  const downloadDuration = Date.now() - downloadStart;
7895
8033
  otaEventEmitter.emitSimple("DOWNLOAD_COMPLETE", bundle.id, {
8034
+ releaseId: bundle.releaseId,
7896
8035
  version: bundle.version,
7897
8036
  durationMs: downloadDuration,
7898
8037
  patchUsed
@@ -7901,6 +8040,7 @@ var init_OtaOrchestrator = __esm({
7901
8040
  if (!applied) {
7902
8041
  logger.error("[OTA] Applying update failed");
7903
8042
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
8043
+ releaseId: bundle.releaseId,
7904
8044
  error: "Atomic slot swap failed",
7905
8045
  version: bundle.version
7906
8046
  });
@@ -7914,6 +8054,7 @@ var init_OtaOrchestrator = __esm({
7914
8054
  this.isRestartRequiredState = true;
7915
8055
  this.recordInstallExpectation(bundle);
7916
8056
  otaEventEmitter.emitSimple("INSTALLED", bundle.id, {
8057
+ releaseId: bundle.releaseId,
7917
8058
  version: bundle.version
7918
8059
  });
7919
8060
  __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
@@ -7966,6 +8107,14 @@ var init_OtaOrchestrator = __esm({
7966
8107
  NativeScaleBunOta_default?.markHealthy();
7967
8108
  __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
7968
8109
  this.healthyTimer = null;
8110
+ const running = this.currentBundle;
8111
+ if (running?.id) {
8112
+ otaEventEmitter.emitSimple("BOOT_SUCCESS", running.id, {
8113
+ releaseId: running.releaseId,
8114
+ version: running.version,
8115
+ durationMs: healthyMs
8116
+ });
8117
+ }
7969
8118
  });
7970
8119
  }, healthyMs);
7971
8120
  } catch {
@@ -8056,7 +8205,14 @@ function clearUiState(name) {
8056
8205
  }
8057
8206
  function uiStateSignature() {
8058
8207
  if (!declared.size) return void 0;
8059
- return [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`).join(";").slice(0, 96);
8208
+ const pairs = [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`);
8209
+ let signature = "";
8210
+ for (const pair of pairs) {
8211
+ const next = signature ? `${signature};${pair}` : pair;
8212
+ if (next.length > 96) break;
8213
+ signature = next;
8214
+ }
8215
+ return signature || void 0;
8060
8216
  }
8061
8217
  var declared, clean;
8062
8218
  var init_uiState = __esm({
@@ -10329,31 +10485,8 @@ var FlushScheduler = class {
10329
10485
  }
10330
10486
  };
10331
10487
 
10332
- // lib/module/core/lifecycle/appLifecycle.js
10333
- var import_react_native6 = require("react-native");
10334
- init_internalLogger();
10335
- var AppLifecycle = class {
10336
- listeners = [];
10337
- currentState = import_react_native6.AppState.currentState;
10338
- constructor() {
10339
- import_react_native6.AppState.addEventListener("change", this.handleStateChange);
10340
- }
10341
- handleStateChange = (nextState) => {
10342
- __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
10343
- this.currentState = nextState;
10344
- this.listeners.forEach((l) => l(nextState));
10345
- };
10346
- addListener(listener) {
10347
- this.listeners.push(listener);
10348
- }
10349
- removeListener(listener) {
10350
- this.listeners = this.listeners.filter((l) => l !== listener);
10351
- }
10352
- getCurrentState() {
10353
- return this.currentState;
10354
- }
10355
- };
10356
- var appLifecycle = new AppLifecycle();
10488
+ // lib/module/bootstrap/SDKBootstrapper.js
10489
+ init_appLifecycle();
10357
10490
 
10358
10491
  // lib/module/bootstrap/FeatureRegistry.js
10359
10492
  init_internalLogger();
@@ -11013,14 +11146,14 @@ function installRejectionHandler(onRejection, deps) {
11013
11146
  allRejections: true,
11014
11147
  onUnhandled: (id, error) => {
11015
11148
  const err = toError(error);
11016
- pending2.set(id, err);
11149
+ pending3.set(id, err);
11017
11150
  onRejection(err);
11018
11151
  if (isDev) {
11019
11152
  console.warn(`Possible unhandled promise rejection (id: ${id}):`, err?.message ?? err);
11020
11153
  }
11021
11154
  },
11022
11155
  onHandled: (id) => {
11023
- pending2.delete(id);
11156
+ pending3.delete(id);
11024
11157
  if (isDev) {
11025
11158
  console.warn(`Promise rejection handled late (id: ${id}) \u2014 it was already reported.`);
11026
11159
  }
@@ -11030,7 +11163,7 @@ function installRejectionHandler(onRejection, deps) {
11030
11163
  return true;
11031
11164
  }) ?? false;
11032
11165
  }
11033
- var pending2 = /* @__PURE__ */ new Map();
11166
+ var pending3 = /* @__PURE__ */ new Map();
11034
11167
 
11035
11168
  // lib/module/features/crash/CrashFeature.js
11036
11169
  init_CrashReporter();
@@ -11347,7 +11480,7 @@ var AppLaunchCollector = class {
11347
11480
  };
11348
11481
 
11349
11482
  // lib/module/features/performance/collectors/ScreenLoadCollector.js
11350
- var MAX_PENDING = 20;
11483
+ var MAX_PENDING2 = 20;
11351
11484
  var ScreenLoadCollector = class {
11352
11485
  pending = /* @__PURE__ */ new Map();
11353
11486
  lastScreen = null;
@@ -11362,7 +11495,7 @@ var ScreenLoadCollector = class {
11362
11495
  */
11363
11496
  markStart(screenName) {
11364
11497
  if (!this.config.screenLoad) return;
11365
- if (this.pending.size >= MAX_PENDING) {
11498
+ if (this.pending.size >= MAX_PENDING2) {
11366
11499
  const oldest = this.pending.keys().next().value;
11367
11500
  if (oldest) this.pending.delete(oldest);
11368
11501
  }
@@ -16839,7 +16972,7 @@ async function captureInstallReferrerOnce(sink) {
16839
16972
  }
16840
16973
 
16841
16974
  // lib/module/public/ScaleBunFacade.js
16842
- var ScaleBunFacade = class {
16975
+ var ScaleBunFacade = class _ScaleBunFacade {
16843
16976
  initialized = false;
16844
16977
  /** In-flight init promise — guards against a second init() racing before the first resolves. */
16845
16978
  _initInFlight = null;
@@ -16999,6 +17132,7 @@ var ScaleBunFacade = class {
16999
17132
  logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
17000
17133
  }
17001
17134
  }
17135
+ const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
17002
17136
  otaOrchestrator2.init({
17003
17137
  ...signingRequested ? {
17004
17138
  signature: {
@@ -17008,15 +17142,99 @@ var ScaleBunFacade = class {
17008
17142
  verifier
17009
17143
  }
17010
17144
  } : {},
17011
- ...typeof ota.healthyTimeoutMs === "number" ? {
17012
- healthyTimeoutMs: ota.healthyTimeoutMs
17145
+ ...healthyAfterMs !== void 0 ? {
17146
+ healthyAfterMs
17013
17147
  } : {}
17014
17148
  });
17015
17149
  logger.info("[ScaleBun] OTA enabled from init config.");
17150
+ this._startOtaChecks(ota);
17016
17151
  } catch (err) {
17017
17152
  logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
17018
17153
  }
17019
17154
  }
17155
+ /** Guards against overlapping config-driven OTA checks. */
17156
+ _otaCheckInFlight = false;
17157
+ /** Wall clock of the last config-driven check, for the foreground floor. */
17158
+ _otaLastCheckAt = 0;
17159
+ _otaForegroundListener = null;
17160
+ /**
17161
+ * Minimum gap between config-driven checks.
17162
+ *
17163
+ * A foreground transition is cheap to trigger — app switchers, permission
17164
+ * dialogs and share sheets all produce one — so an unthrottled check would
17165
+ * put a request on the hot path every time the user glanced away. Ten
17166
+ * minutes is well below any realistic release cadence and well above that
17167
+ * noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
17168
+ * which is never throttled.
17169
+ */
17170
+ static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1e3;
17171
+ /**
17172
+ * Drive OTA checks from init config: once at startup, then on each
17173
+ * foreground when `checkOnForeground` is on (the schema default).
17174
+ *
17175
+ * `appVersion` is resolved from the native bridge rather than asked of the
17176
+ * integrator, because it gates the server's `targetAppVersion` semver check
17177
+ * — sending a wrong or invented value is worse than sending none, and there
17178
+ * is no honest default. If it cannot be resolved, the check is skipped with
17179
+ * a warning instead of guessing.
17180
+ */
17181
+ _startOtaChecks(ota) {
17182
+ if (__DEV__) {
17183
+ logger.info("[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).");
17184
+ return;
17185
+ }
17186
+ const runCheck = async (trigger) => {
17187
+ if (this._otaCheckInFlight) return;
17188
+ if (trigger === "foreground" && Date.now() - this._otaLastCheckAt < _ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
17189
+ return;
17190
+ }
17191
+ const clientKey = this._clientKey;
17192
+ const apiUrl = this._apiBaseUrl;
17193
+ if (!clientKey || !apiUrl) return;
17194
+ this._otaCheckInFlight = true;
17195
+ try {
17196
+ const info = await bridgeAdapter.getDeviceInfo();
17197
+ const appVersion = info?.appVersion;
17198
+ if (!appVersion) {
17199
+ logger.warn("[ScaleBun] OTA check skipped \u2014 the app version could not be read from the native bridge. Rebuild the native app, or drive checks yourself with useOtaUpdate({ appVersion }).");
17200
+ return;
17201
+ }
17202
+ this._otaLastCheckAt = Date.now();
17203
+ const {
17204
+ otaOrchestrator: otaOrchestrator2
17205
+ } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
17206
+ await otaOrchestrator2.sync({
17207
+ apiUrl,
17208
+ clientKey,
17209
+ appVersion,
17210
+ // The documented option, finally connected. Omitted means the
17211
+ // server's `default` channel, exactly as before.
17212
+ channelName: typeof ota.channelOverride === "string" ? ota.channelOverride : void 0
17213
+ // Never forced from config: the release's own installMode
17214
+ // decides when the app restarts, and yanking the screen out
17215
+ // from under a user is not a decision this switch should make.
17216
+ });
17217
+ } catch (err) {
17218
+ logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
17219
+ } finally {
17220
+ this._otaCheckInFlight = false;
17221
+ }
17222
+ };
17223
+ void runCheck("startup");
17224
+ if (ota.checkOnForeground === false) return;
17225
+ if (this._otaForegroundListener) return;
17226
+ try {
17227
+ const {
17228
+ appLifecycle: appLifecycle2
17229
+ } = (init_appLifecycle(), __toCommonJS(appLifecycle_exports));
17230
+ this._otaForegroundListener = (state) => {
17231
+ if (state === "active") void runCheck("foreground");
17232
+ };
17233
+ appLifecycle2.addListener(this._otaForegroundListener);
17234
+ } catch {
17235
+ this._otaForegroundListener = null;
17236
+ }
17237
+ }
17020
17238
  _autoEnableDebug(debugConfig) {
17021
17239
  try {
17022
17240
  const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
@@ -18428,6 +18646,7 @@ init_internalLogger();
18428
18646
 
18429
18647
  // lib/module/features/engage/engageTriggerEngine.js
18430
18648
  init_internalLogger();
18649
+ init_appLifecycle();
18431
18650
  init_AutoScreenDetector();
18432
18651
  init_StorageBackend();
18433
18652
  init_engageSignals();
@@ -18735,6 +18954,7 @@ function installEngageTriggerEngine(deps) {
18735
18954
  }
18736
18955
 
18737
18956
  // lib/module/features/engage/EngagePromptProvider.js
18957
+ init_appLifecycle();
18738
18958
  init_deviceId();
18739
18959
  init_api();
18740
18960
  init_SessionManager();
@@ -29983,7 +30203,6 @@ function useEngagePrompt() {
29983
30203
  // lib/module/features/journey/ScaleBunDebugRoot.js
29984
30204
  var import_react9 = __toESM(require("react"));
29985
30205
  var import_react_native42 = require("react-native");
29986
- init_automaticEvents();
29987
30206
 
29988
30207
  // lib/module/features/journey/touchTarget.js
29989
30208
  var MAX_DEPTH = 12;
@@ -30055,6 +30274,7 @@ function describeTouchTarget(t) {
30055
30274
 
30056
30275
  // lib/module/features/journey/ScaleBunDebugRoot.js
30057
30276
  init_device();
30277
+ init_interactionProtocol();
30058
30278
  var import_jsx_runtime8 = require("react/jsx-runtime");
30059
30279
  var _bootstrap = null;
30060
30280
  function getBootstrap() {
@@ -30244,7 +30464,9 @@ function ScaleBunDebugRoot({
30244
30464
  }, [navigationRef]);
30245
30465
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
30246
30466
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
30247
- const pendingJsEmitRef = (0, import_react9.useRef)(null);
30467
+ const touchStartRef = (0, import_react9.useRef)(null);
30468
+ const interactionStartsRef = (0, import_react9.useRef)([]);
30469
+ const pendingJsEmitsRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
30248
30470
  (0, import_react9.useEffect)(() => {
30249
30471
  let subscription = null;
30250
30472
  try {
@@ -30255,9 +30477,12 @@ function ScaleBunDebugRoot({
30255
30477
  try {
30256
30478
  lastNativeTouchTsRef.current = Date.now();
30257
30479
  nativeTrackingConfirmedRef.current = true;
30258
- if (pendingJsEmitRef.current !== null) {
30259
- clearTimeout(pendingJsEmitRef.current);
30260
- pendingJsEmitRef.current = null;
30480
+ const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
30481
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
30482
+ if (start) {
30483
+ const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
30484
+ if (pending4 !== void 0) clearTimeout(pending4);
30485
+ pendingJsEmitsRef.current.delete(start.interactionId);
30261
30486
  }
30262
30487
  const {
30263
30488
  SessionManager: SessionManager2
@@ -30276,6 +30501,15 @@ function ScaleBunDebugRoot({
30276
30501
  } catch {
30277
30502
  }
30278
30503
  sm.onGestureDetected(event.gestureType || "tap", {
30504
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
30505
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
30506
+ occurredAt: nativeOccurredAt,
30507
+ ui: start?.ui,
30508
+ stateStatus: start?.stateStatus ?? "not_captured",
30509
+ target: start?.target,
30510
+ targetId: start?.targetId,
30511
+ screenName: start?.screenName,
30512
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30279
30513
  x: event.rawX,
30280
30514
  y: event.rawY,
30281
30515
  // Use the actual native end coords when present, not the down
@@ -30308,9 +30542,10 @@ function ScaleBunDebugRoot({
30308
30542
  subscription?.remove();
30309
30543
  } catch {
30310
30544
  }
30545
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
30546
+ pendingJsEmitsRef.current.clear();
30311
30547
  };
30312
- }, []);
30313
- const touchStartRef = (0, import_react9.useRef)(null);
30548
+ }, [captureAutomaticInteractions]);
30314
30549
  const rootRectRef = (0, import_react9.useRef)(null);
30315
30550
  const measureRoot = () => {
30316
30551
  try {
@@ -30342,11 +30577,39 @@ function ScaleBunDebugRoot({
30342
30577
  const handleTouchStart = (e) => {
30343
30578
  try {
30344
30579
  const touch = e.nativeEvent;
30345
- touchStartRef.current = {
30580
+ const target = resolveTouchTarget(e);
30581
+ let ui;
30582
+ let screenName;
30583
+ try {
30584
+ const {
30585
+ uiStateSignature: uiStateSignature2
30586
+ } = (init_uiState(), __toCommonJS(uiState_exports));
30587
+ ui = uiStateSignature2();
30588
+ } catch {
30589
+ }
30590
+ try {
30591
+ const {
30592
+ AutoScreenDetector: AutoScreenDetector2
30593
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30594
+ screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30595
+ } catch {
30596
+ }
30597
+ const start = {
30598
+ interactionId: generateInteractionId(),
30599
+ occurredAt: Date.now(),
30346
30600
  x: touch.pageX,
30347
30601
  y: touch.pageY,
30348
- ts: Date.now()
30602
+ locationX: touch.locationX,
30603
+ locationY: touch.locationY,
30604
+ target: describeTouchTarget(target),
30605
+ targetId: target?.testID,
30606
+ screenName,
30607
+ ui,
30608
+ stateStatus: ui ? "captured_nonempty" : "not_instrumented",
30609
+ emitAutomaticAnalytics: captureAutomaticInteractions
30349
30610
  };
30611
+ touchStartRef.current = start;
30612
+ interactionStartsRef.current = interactionStartsRef.current.filter((candidate) => start.occurredAt - candidate.occurredAt < 5e3).concat(start).slice(-8);
30350
30613
  } catch {
30351
30614
  }
30352
30615
  };
@@ -30377,21 +30640,29 @@ function ScaleBunDebugRoot({
30377
30640
  }
30378
30641
  measureRoot();
30379
30642
  const rootRect = rootRectRef.current;
30380
- const tapped = describeTouchTarget(resolveTouchTarget(e));
30643
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
30381
30644
  const gestureDetails = {
30382
30645
  target: tapped,
30383
- x: touch.pageX,
30384
- y: touch.pageY,
30385
- pageX: touch.pageX,
30386
- pageY: touch.pageY,
30387
- locationX: touch.locationX,
30388
- locationY: touch.locationY,
30646
+ interactionId: start?.interactionId ?? generateInteractionId(),
30647
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
30648
+ occurredAt: start?.occurredAt ?? Date.now(),
30649
+ ui: start?.ui,
30650
+ stateStatus: start?.stateStatus ?? "not_captured",
30651
+ targetId: start?.targetId,
30652
+ screenName: start?.screenName,
30653
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30654
+ x: start?.x ?? touch.pageX,
30655
+ y: start?.y ?? touch.pageY,
30656
+ pageX: start?.x ?? touch.pageX,
30657
+ pageY: start?.y ?? touch.pageY,
30658
+ locationX: start?.locationX ?? touch.locationX,
30659
+ locationY: start?.locationY ?? touch.locationY,
30389
30660
  viewportWidth: vpW > 0 ? vpW : void 0,
30390
30661
  viewportHeight: vpH > 0 ? vpH : void 0
30391
30662
  };
30392
30663
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
30393
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
30394
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
30664
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
30665
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
30395
30666
  gestureDetails.normalizedX = nx;
30396
30667
  gestureDetails.normalizedY = ny;
30397
30668
  gestureDetails.normalizedPrecomputed = true;
@@ -30422,7 +30693,7 @@ function ScaleBunDebugRoot({
30422
30693
  const dx = touch.pageX - start.x;
30423
30694
  const dy = touch.pageY - start.y;
30424
30695
  const dist = Math.sqrt(dx * dx + dy * dy);
30425
- const duration = Date.now() - start.ts;
30696
+ const duration = Date.now() - start.occurredAt;
30426
30697
  gestureDetails.duration = duration;
30427
30698
  if (dist >= 15) {
30428
30699
  gestureDetails.endX = touch.pageX;
@@ -30440,60 +30711,16 @@ function ScaleBunDebugRoot({
30440
30711
  gestureType = "long_press";
30441
30712
  }
30442
30713
  }
30443
- const targetInfo = _extractTarget(e);
30444
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
30445
- if (captureAutomaticInteractions) {
30446
- let screen;
30447
- try {
30448
- const {
30449
- AutoScreenDetector: AutoScreenDetector2
30450
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30451
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30452
- } catch {
30453
- }
30454
- let ui;
30455
- try {
30456
- const {
30457
- uiStateSignature: uiStateSignature2
30458
- } = (init_uiState(), __toCommonJS(uiState_exports));
30459
- ui = uiStateSignature2();
30460
- } catch {
30461
- }
30462
- emitAutomaticEvent("element_interacted", {
30463
- gesture_type: gestureType,
30464
- screen_name: screen,
30465
- /* THIS MAP ENUMERATES. A field added to the payload and forgotten here reaches the
30466
- backend as undefined with no error anywhere — the recurring defect class in this
30467
- codebase. `ui` is the key the grid aggregate reads for state, byte-identical to
30468
- the web SDK's, so one dashboard control queries both platforms. */
30469
- ui,
30470
- // testID/nativeID is an author-controlled stable identifier.
30471
- // Accessibility labels and rendered text are deliberately omitted.
30472
- target_id: targetInfo?.testId,
30473
- normalized_x: gestureDetails.normalizedX,
30474
- normalized_y: gestureDetails.normalizedY,
30475
- direction: gestureDetails.direction,
30476
- duration_ms: gestureDetails.duration
30477
- });
30478
- }
30479
30714
  if (gestureType === "tap") {
30480
30715
  try {
30481
30716
  const {
30482
30717
  gestureTriggerDetector: gestureTriggerDetector2
30483
30718
  } = (init_gestureTriggerDetector(), __toCommonJS(gestureTriggerDetector_exports));
30484
- let screen;
30485
- try {
30486
- const {
30487
- AutoScreenDetector: AutoScreenDetector2
30488
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30489
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30490
- } catch {
30491
- }
30492
30719
  gestureTriggerDetector2.recordTap({
30493
- x: touch.pageX,
30494
- y: touch.pageY,
30495
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
30496
- screen
30720
+ x: start?.x ?? touch.pageX,
30721
+ y: start?.y ?? touch.pageY,
30722
+ hasTarget: !!(start?.targetId || start?.target),
30723
+ screen: start?.screenName
30497
30724
  });
30498
30725
  } catch {
30499
30726
  }
@@ -30506,11 +30733,9 @@ function ScaleBunDebugRoot({
30506
30733
  const capturedDetails = {
30507
30734
  ...gestureDetails
30508
30735
  };
30509
- if (pendingJsEmitRef.current !== null) {
30510
- clearTimeout(pendingJsEmitRef.current);
30511
- }
30512
- pendingJsEmitRef.current = setTimeout(() => {
30513
- pendingJsEmitRef.current = null;
30736
+ const interactionId = capturedDetails.interactionId;
30737
+ const timer = setTimeout(() => {
30738
+ pendingJsEmitsRef.current.delete(interactionId);
30514
30739
  if (nativeTrackingConfirmedRef.current) return;
30515
30740
  try {
30516
30741
  const {
@@ -30527,6 +30752,7 @@ function ScaleBunDebugRoot({
30527
30752
  } catch {
30528
30753
  }
30529
30754
  }, 600);
30755
+ pendingJsEmitsRef.current.set(interactionId, timer);
30530
30756
  }
30531
30757
  touchStartRef.current = null;
30532
30758
  } catch {
@@ -30602,20 +30828,6 @@ function useScaleBunScreen(name) {
30602
30828
  }
30603
30829
  }, [name, manager]);
30604
30830
  }
30605
- function _extractTarget(e) {
30606
- try {
30607
- const target = e?.target;
30608
- if (!target) return void 0;
30609
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
30610
- return {
30611
- testId: props.testID || props.nativeID || void 0,
30612
- accessibilityLabel: props.accessibilityLabel || void 0,
30613
- text: void 0
30614
- };
30615
- } catch {
30616
- return void 0;
30617
- }
30618
- }
30619
30831
  var styles4 = {
30620
30832
  root: {
30621
30833
  flex: 1
@@ -30976,7 +31188,7 @@ function useOtaUpdate(options) {
30976
31188
  setIsSyncing(true);
30977
31189
  setDownloadProgress(0);
30978
31190
  try {
30979
- const result = await otaOrchestrator.sync(options);
31191
+ const result = await otaOrchestrator.sync(optionsRef.current);
30980
31192
  setSyncResult(result);
30981
31193
  if (result.isMandatory && result.status === "UPDATE_INSTALLED" && optionsRef.current.mandatoryBlocksUi) {
30982
31194
  setMandatoryUpdatePending(true);
@@ -30985,7 +31197,7 @@ function useOtaUpdate(options) {
30985
31197
  } finally {
30986
31198
  setIsSyncing(false);
30987
31199
  }
30988
- }, [options.apiUrl, options.clientKey, options.appVersion, options.installationId, options.autoRestart]);
31200
+ }, []);
30989
31201
  const restart = (0, import_react16.useCallback)(() => {
30990
31202
  setMandatoryUpdatePending(false);
30991
31203
  otaOrchestrator.restart();