@scalebun/react-native 1.13.0 → 2.0.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 (115) hide show
  1. package/android/build.gradle +8 -0
  2. package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
  4. package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
  5. package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
  6. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
  7. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +132 -62
  8. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
  9. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +80 -1
  10. package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
  11. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +6 -8
  12. package/dist/scalebun.full.js +964 -533
  13. package/dist/scalebun.slim.js +962 -531
  14. package/ios/Ota/OtaProtocol.swift +242 -0
  15. package/ios/Ota/OtaSlotManager.swift +26 -0
  16. package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
  17. package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
  18. package/ios/Ota/ScaleBunOtaModule.swift +125 -53
  19. package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
  20. package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
  21. package/ios/ReplaySdk.swift +65 -2
  22. package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
  23. package/lib/commonjs/core/config/schema.js +19 -12
  24. package/lib/commonjs/core/constants/version.js +1 -1
  25. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
  26. package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
  27. package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
  28. package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
  29. package/lib/commonjs/features/journey/scrollContext.js +146 -0
  30. package/lib/commonjs/features/journey/targetGeometry.js +164 -0
  31. package/lib/commonjs/features/journey/touchTarget.js +152 -24
  32. package/lib/commonjs/features/journey/uiState.js +81 -1
  33. package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
  34. package/lib/commonjs/features/network/NetworkFeature.js +39 -5
  35. package/lib/commonjs/features/network/thirdParty.js +90 -0
  36. package/lib/commonjs/features/ota/OtaOrchestrator.js +175 -80
  37. package/lib/commonjs/features/session/SessionManager.js +63 -0
  38. package/lib/commonjs/public/ScaleBunFacade.js +23 -57
  39. package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
  40. package/lib/module/core/config/schema.js +19 -12
  41. package/lib/module/core/constants/version.js +1 -1
  42. package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
  43. package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
  44. package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
  45. package/lib/module/features/journey/interactionProtocol.js +128 -8
  46. package/lib/module/features/journey/scrollContext.js +135 -0
  47. package/lib/module/features/journey/targetGeometry.js +155 -0
  48. package/lib/module/features/journey/touchTarget.js +151 -24
  49. package/lib/module/features/journey/uiState.js +78 -1
  50. package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
  51. package/lib/module/features/network/NetworkFeature.js +38 -4
  52. package/lib/module/features/network/thirdParty.js +83 -0
  53. package/lib/module/features/ota/OtaOrchestrator.js +175 -80
  54. package/lib/module/features/session/SessionManager.js +63 -0
  55. package/lib/module/public/ScaleBunFacade.js +23 -57
  56. package/lib/typescript/core/config/schema.d.ts +2 -3
  57. package/lib/typescript/core/constants/version.d.ts +1 -1
  58. package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
  59. package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
  60. package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
  61. package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
  62. package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
  63. package/lib/typescript/features/journey/uiState.d.ts +39 -0
  64. package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
  65. package/lib/typescript/features/network/index.d.ts +18 -0
  66. package/lib/typescript/features/network/thirdParty.d.ts +65 -0
  67. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +12 -8
  68. package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
  69. package/lib/typescript/features/session/SessionManager.d.ts +55 -0
  70. package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
  71. package/lib/typescript/public/types.d.ts +10 -0
  72. package/lib/typescript/specs/NativeScaleBunOta.d.ts +32 -20
  73. package/package.json +3 -13
  74. package/scalebun-react-native.podspec +4 -0
  75. package/src/bootstrap/SDKBootstrapper.ts +40 -0
  76. package/src/core/config/schema.ts +21 -9
  77. package/src/core/constants/version.ts +1 -1
  78. package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
  79. package/src/features/journey/ScaleBunScrollView.tsx +29 -0
  80. package/src/features/journey/autoInstrumentScroll.ts +155 -0
  81. package/src/features/journey/interactionProtocol.ts +188 -10
  82. package/src/features/journey/scrollContext.ts +137 -0
  83. package/src/features/journey/targetGeometry.ts +176 -0
  84. package/src/features/journey/touchTarget.ts +237 -26
  85. package/src/features/journey/uiState.ts +80 -1
  86. package/src/features/navigation/AutoScreenDetector.ts +65 -5
  87. package/src/features/network/NetworkFeature.ts +41 -4
  88. package/src/features/network/index.ts +18 -0
  89. package/src/features/network/thirdParty.ts +92 -0
  90. package/src/features/ota/OtaOrchestrator.ts +190 -90
  91. package/src/features/ota/OtaTypes.ts +23 -1
  92. package/src/features/session/SessionManager.ts +113 -0
  93. package/src/public/ScaleBunFacade.ts +23 -69
  94. package/src/public/types.ts +10 -0
  95. package/src/specs/NativeScaleBunOta.ts +35 -20
  96. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
  97. package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
  98. package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
  99. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
  100. package/lib/commonjs/features/ota/signature.js +0 -175
  101. package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
  102. package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
  103. package/lib/module/features/ota/crypto/loadSha512.js +0 -34
  104. package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
  105. package/lib/module/features/ota/signature.js +0 -169
  106. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
  107. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
  108. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
  109. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
  110. package/lib/typescript/features/ota/signature.d.ts +0 -81
  111. package/src/features/ota/crypto/builtinVerifier.ts +0 -257
  112. package/src/features/ota/crypto/loadEd25519.ts +0 -41
  113. package/src/features/ota/crypto/loadSha512.ts +0 -35
  114. package/src/features/ota/crypto/nativeVerifier.ts +0 -117
  115. package/src/features/ota/signature.ts +0 -206
@@ -89,9 +89,9 @@ var init_internalLogger = __esm({
89
89
  }
90
90
  /** Set level from the public string name used in config (e.g. 'warn'). */
91
91
  setLevelByName(name) {
92
- const resolved3 = LEVEL_BY_NAME[name];
93
- if (resolved3 !== void 0) {
94
- this.level = resolved3;
92
+ const resolved = LEVEL_BY_NAME[name];
93
+ if (resolved !== void 0) {
94
+ this.level = resolved;
95
95
  }
96
96
  }
97
97
  /** Current numeric level — exposed for callers that branch on dev verbosity. */
@@ -605,7 +605,7 @@ var SDK_VERSION;
605
605
  var init_version = __esm({
606
606
  "lib/module/core/constants/version.js"() {
607
607
  "use strict";
608
- SDK_VERSION = "1.13.0";
608
+ SDK_VERSION = "2.0.1";
609
609
  }
610
610
  });
611
611
 
@@ -3461,6 +3461,15 @@ var init_AutoScreenDetector = __esm({
3461
3461
  _manualScreen = null;
3462
3462
  _manualScreenTs = 0;
3463
3463
  _autoScreen = null;
3464
+ /**
3465
+ * When `_autoScreen` was last written.
3466
+ *
3467
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
3468
+ * Without a timestamp the only comparison available was "is the manual value still
3469
+ * fresh", which is a different question and gave the wrong answer — see the note on
3470
+ * `_currentScreen()`.
3471
+ */
3472
+ _autoScreenTs = 0;
3464
3473
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
3465
3474
  _autoVisitSig = null;
3466
3475
  _navigationRef = null;
@@ -3528,6 +3537,7 @@ var init_AutoScreenDetector = __esm({
3528
3537
  if (this.isManualScreenActive()) return;
3529
3538
  if (name !== this._autoScreen) {
3530
3539
  this._autoScreen = name;
3540
+ this._autoScreenTs = Date.now();
3531
3541
  this._autoVisitSig = name;
3532
3542
  this._emitScreenChange(name, "heuristic");
3533
3543
  }
@@ -3535,8 +3545,19 @@ var init_AutoScreenDetector = __esm({
3535
3545
  }
3536
3546
  }
3537
3547
  /**
3538
- * Check if a manual screen is currently active.
3539
- * Used by auto-detection to yield to manual overrides.
3548
+ * Should a WRITER defer to the manual screen right now?
3549
+ *
3550
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
3551
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
3552
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
3553
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
3554
+ *
3555
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
3556
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
3557
+ * `setManualScreen` in production, so the effect was latent — but the moment the
3558
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
3559
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
3560
+ * fix that introduced that would have been worse than the gap it closed.
3540
3561
  */
3541
3562
  isManualScreenActive() {
3542
3563
  if (!this._manualScreen) return false;
@@ -3582,6 +3603,7 @@ var init_AutoScreenDetector = __esm({
3582
3603
  const visit = this._extractActiveVisit(state);
3583
3604
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
3584
3605
  this._autoScreen = visit.name;
3606
+ this._autoScreenTs = Date.now();
3585
3607
  this._autoVisitSig = this._visitSig(visit);
3586
3608
  this._emitScreenChange(visit.name, "navRef");
3587
3609
  }
@@ -3600,6 +3622,7 @@ var init_AutoScreenDetector = __esm({
3600
3622
  const visit = this._extractActiveVisit(state);
3601
3623
  if (visit) {
3602
3624
  this._autoScreen = visit.name;
3625
+ this._autoScreenTs = Date.now();
3603
3626
  this._autoVisitSig = this._visitSig(visit);
3604
3627
  this._emitScreenChange(visit.name, "navRef");
3605
3628
  }
@@ -3630,6 +3653,7 @@ var init_AutoScreenDetector = __esm({
3630
3653
  const visit = this._extractActiveVisit(state);
3631
3654
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
3632
3655
  this._autoScreen = visit.name;
3656
+ this._autoScreenTs = Date.now();
3633
3657
  this._autoVisitSig = this._visitSig(visit);
3634
3658
  this._emitScreenChange(visit.name, "heuristic");
3635
3659
  }
@@ -3728,7 +3752,7 @@ var init_AutoScreenDetector = __esm({
3728
3752
  if (!reactNav || reactNav.__scalebunAutoHooked) return;
3729
3753
  const detector = this;
3730
3754
  let installed2 = 0;
3731
- const redefine = (key, value) => {
3755
+ const redefine2 = (key, value) => {
3732
3756
  try {
3733
3757
  Object.defineProperty(reactNav, key, {
3734
3758
  value,
@@ -3778,7 +3802,7 @@ var init_AutoScreenDetector = __esm({
3778
3802
  });
3779
3803
  });
3780
3804
  Wrapped.displayName = "ScaleBunNavigationContainer";
3781
- if (redefine("NavigationContainer", Wrapped)) installed2++;
3805
+ if (redefine2("NavigationContainer", Wrapped)) installed2++;
3782
3806
  }
3783
3807
  if (typeof reactNav.createNavigationContainerRef === "function") {
3784
3808
  const originalCreate = reactNav.createNavigationContainerRef;
@@ -3790,7 +3814,7 @@ var init_AutoScreenDetector = __esm({
3790
3814
  }
3791
3815
  return ref;
3792
3816
  };
3793
- if (redefine("createNavigationContainerRef", patchedCreate)) installed2++;
3817
+ if (redefine2("createNavigationContainerRef", patchedCreate)) installed2++;
3794
3818
  }
3795
3819
  if (typeof reactNav.useNavigationContainerRef === "function") {
3796
3820
  const originalHook = reactNav.useNavigationContainerRef;
@@ -3802,7 +3826,7 @@ var init_AutoScreenDetector = __esm({
3802
3826
  }
3803
3827
  return ref;
3804
3828
  };
3805
- if (redefine("useNavigationContainerRef", patchedHook)) installed2++;
3829
+ if (redefine2("useNavigationContainerRef", patchedHook)) installed2++;
3806
3830
  }
3807
3831
  this._autoHookInstalled = installed2 > 0;
3808
3832
  try {
@@ -3851,15 +3875,58 @@ var init_AutoScreenDetector = __esm({
3851
3875
  this._listeners = [];
3852
3876
  this._manualScreen = null;
3853
3877
  this._autoScreen = null;
3878
+ this._autoScreenTs = 0;
3854
3879
  this._navigationRef = null;
3855
3880
  _instance = null;
3856
3881
  }
3857
3882
  /** Get the current best-known screen name */
3858
3883
  getCurrentScreen() {
3859
- if (this._manualScreen && this.isManualScreenActive()) {
3860
- return this._manualScreen;
3884
+ return this._currentScreen().name;
3885
+ }
3886
+ /**
3887
+ * WHERE the current screen name came from, or 'none'.
3888
+ *
3889
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
3890
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
3891
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
3892
+ * causes: the app never instrumented anything, the app instrumented manually and the
3893
+ * value never reached interactions, or auto-detection ran and failed. Those have
3894
+ * different owners and different fixes, and guessing between them is how a P0 stays
3895
+ * open.
3896
+ */
3897
+ getCurrentScreenSource() {
3898
+ return this._currentScreen().source;
3899
+ }
3900
+ /**
3901
+ * LAST WRITER WINS between the manual beacon and auto-detection.
3902
+ *
3903
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
3904
+ * declares a different one — that is what mounting a screen means. It does not
3905
+ * expire, which is the correction here: the previous rule returned the manual name
3906
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
3907
+ * which in a manually-instrumented app is `null`.
3908
+ *
3909
+ * Comparing timestamps rather than hard-coding a priority also handles the real
3910
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
3911
+ * beacon on one modal, and whichever spoke last is the truth.
3912
+ */
3913
+ _currentScreen() {
3914
+ const manual = this._manualScreen;
3915
+ const auto = this._autoScreen;
3916
+ if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
3917
+ return {
3918
+ name: manual,
3919
+ source: "manual"
3920
+ };
3861
3921
  }
3862
- return this._autoScreen;
3922
+ if (auto) return {
3923
+ name: auto,
3924
+ source: "navigation"
3925
+ };
3926
+ return {
3927
+ name: null,
3928
+ source: "none"
3929
+ };
3863
3930
  }
3864
3931
  // ─── Internal ───────────────────────────────────────────────────────
3865
3932
  _emitScreenChange(name, source) {
@@ -5740,17 +5807,54 @@ var init_automaticEvents = __esm({
5740
5807
  function generateInteractionId() {
5741
5808
  return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
5742
5809
  }
5743
- function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
5744
- let best;
5745
- let bestDelta = toleranceMs + 1;
5746
- for (const start of starts) {
5810
+ function correlateInteraction(starts, occurredAt, toleranceMs = CORRELATION_TOLERANCE_MS, marginMs = CORRELATION_MARGIN_MS) {
5811
+ const candidates = [];
5812
+ for (let i = 0; i < starts.length; i++) {
5813
+ const start = starts[i];
5814
+ if (!start) continue;
5747
5815
  const delta = Math.abs(start.occurredAt - occurredAt);
5748
- if (delta < bestDelta) {
5749
- best = start;
5750
- bestDelta = delta;
5816
+ if (delta <= toleranceMs) candidates.push({
5817
+ index: i,
5818
+ delta
5819
+ });
5820
+ }
5821
+ if (candidates.length === 0) {
5822
+ return {
5823
+ index: -1,
5824
+ outcome: "none",
5825
+ deltaMs: null,
5826
+ runnerUpDeltaMs: null
5827
+ };
5828
+ }
5829
+ candidates.sort((a, b) => a.delta - b.delta || a.index - b.index);
5830
+ const best = candidates[0];
5831
+ const runnerUp = candidates[1];
5832
+ if (runnerUp && runnerUp.delta - best.delta < marginMs) {
5833
+ return {
5834
+ index: -1,
5835
+ outcome: "ambiguous",
5836
+ deltaMs: best.delta,
5837
+ runnerUpDeltaMs: runnerUp.delta
5838
+ };
5839
+ }
5840
+ return {
5841
+ start: starts[best.index],
5842
+ index: best.index,
5843
+ outcome: "exact",
5844
+ deltaMs: best.delta,
5845
+ runnerUpDeltaMs: runnerUp ? runnerUp.delta : null
5846
+ };
5847
+ }
5848
+ function pruneInteractionStarts(starts, now2, toleranceMs = CORRELATION_TOLERANCE_MS) {
5849
+ let removed = 0;
5850
+ for (let i = starts.length - 1; i >= 0; i--) {
5851
+ const start = starts[i];
5852
+ if (!start || now2 - start.occurredAt > toleranceMs) {
5853
+ starts.splice(i, 1);
5854
+ removed++;
5751
5855
  }
5752
5856
  }
5753
- return bestDelta <= toleranceMs ? best : void 0;
5857
+ return removed;
5754
5858
  }
5755
5859
  function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5756
5860
  return {
@@ -5768,11 +5872,13 @@ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5768
5872
  canonical_mirror: canonicalMirror || void 0
5769
5873
  };
5770
5874
  }
5771
- var INTERACTION_PROTOCOL_VERSION;
5875
+ var INTERACTION_PROTOCOL_VERSION, CORRELATION_TOLERANCE_MS, CORRELATION_MARGIN_MS;
5772
5876
  var init_interactionProtocol = __esm({
5773
5877
  "lib/module/features/journey/interactionProtocol.js"() {
5774
5878
  "use strict";
5775
5879
  INTERACTION_PROTOCOL_VERSION = 1;
5880
+ CORRELATION_TOLERANCE_MS = 1500;
5881
+ CORRELATION_MARGIN_MS = 100;
5776
5882
  }
5777
5883
  });
5778
5884
 
@@ -6515,6 +6621,69 @@ var init_SessionManager = __esm({
6515
6621
  state_status: stateStatus,
6516
6622
  ui: details?.ui,
6517
6623
  target_id: details?.targetId,
6624
+ /**
6625
+ * RN-6 — shipped with the row so the join is auditable instead of assumed.
6626
+ *
6627
+ * Without it, an interaction with no `target_id` is ambiguous in the worst way:
6628
+ * it could mean the control genuinely had no identity, or that the correlation
6629
+ * refused and the identity exists but could not be attributed. Those are
6630
+ * different defects with different owners, and the accounting lane cannot give
6631
+ * an honest unplaceable REASON without knowing which.
6632
+ *
6633
+ * Undefined on the JS-fallback path (no native event, so nothing was joined),
6634
+ * which is itself the correct answer there rather than a fabricated 'exact'.
6635
+ */
6636
+ correlation: details?.correlation,
6637
+ correlation_delta_ms: details?.correlationDeltaMs,
6638
+ correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
6639
+ /**
6640
+ * RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
6641
+ * identifier from a structural inference. 'path' means the app authored no testID
6642
+ * and the identity was derived from component names, which is useful but will
6643
+ * change under refactoring; a dashboard that presents the two identically invites
6644
+ * someone to trust a key that is about to move.
6645
+ */
6646
+ target_source: details?.targetSource,
6647
+ /**
6648
+ * RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
6649
+ * 'none' means the app instrumented nothing the SDK could see; 'manual' means a
6650
+ * <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
6651
+ * different owners behind one number that today has none.
6652
+ */
6653
+ screen_source: details?.screenSource,
6654
+ /**
6655
+ * RN-13 — what makes "the heat is on this control" checkable instead of assumed.
6656
+ * Absent when the control could not be measured synchronously, which is a fact worth
6657
+ * storing: an unmeasurable control and a mis-placed tap are different findings.
6658
+ */
6659
+ target_rect: details?.targetRect,
6660
+ /**
6661
+ * RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
6662
+ * table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
6663
+ * these need no migration. Mobile simply never sent them.
6664
+ *
6665
+ * `scroll_source` says whether an offset was available at all: 'none' means the app
6666
+ * uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
6667
+ * the integration and not a tap at the top of a list.
6668
+ */
6669
+ scrollX: details?.scrollX,
6670
+ scrollY: details?.scrollY,
6671
+ scroll_source: details?.scrollSource,
6672
+ /**
6673
+ * RN-4 — what stops a recycled native view merging two different rows into one
6674
+ * hotspot. The key is the app's own keyExtractor output, so it is stable across
6675
+ * scrolling and across sessions; the index is carried so a recycled slot (same index,
6676
+ * different key) can be proven rather than inferred.
6677
+ */
6678
+ item_key: details?.itemKey,
6679
+ item_index: details?.itemIndex,
6680
+ /**
6681
+ * RN-15 — where in the control, not just which control. Turns "they tapped Add to
6682
+ * Cart" into "they tapped its left edge", which is what tells you whether a hit area
6683
+ * is right. Outside 0..1 is a hitSlop tap and is kept as such.
6684
+ */
6685
+ local_u: details?.localU,
6686
+ local_v: details?.localV,
6518
6687
  gestureType,
6519
6688
  x: details?.x,
6520
6689
  y: details?.y,
@@ -8553,8 +8722,8 @@ var init_PerformanceTransport = __esm({
8553
8722
  setSink(sink) {
8554
8723
  this.sink = sink;
8555
8724
  if (sink && this.buffer.length > 0) {
8556
- const pending4 = this.buffer.splice(0);
8557
- for (const msg of pending4) {
8725
+ const pending5 = this.buffer.splice(0);
8726
+ for (const msg of pending5) {
8558
8727
  try {
8559
8728
  sink.send(msg);
8560
8729
  } catch {
@@ -9327,8 +9496,8 @@ var init_ProfilerTransport = __esm({
9327
9496
  setSink(sink) {
9328
9497
  this.sink = sink;
9329
9498
  if (sink && this.buffer.length > 0) {
9330
- const pending4 = this.buffer.splice(0);
9331
- for (const msg of pending4) {
9499
+ const pending5 = this.buffer.splice(0);
9500
+ for (const msg of pending5) {
9332
9501
  try {
9333
9502
  sink.send(msg);
9334
9503
  } catch {
@@ -10216,10 +10385,10 @@ function requestExport(transport, enabled, presetOrOptions) {
10216
10385
  return;
10217
10386
  }
10218
10387
  const timer = setTimeout(() => {
10219
- const pending4 = _pendingExports.get(requestId);
10220
- if (pending4) {
10388
+ const pending5 = _pendingExports.get(requestId);
10389
+ if (pending5) {
10221
10390
  _pendingExports.delete(requestId);
10222
- pending4.resolve({
10391
+ pending5.resolve({
10223
10392
  success: false,
10224
10393
  error: `Export timed out after ${EXPORT_TIMEOUT_MS / 1e3}s`
10225
10394
  });
@@ -10261,14 +10430,14 @@ function handleExportResult(args) {
10261
10430
  try {
10262
10431
  const requestId = args.requestId;
10263
10432
  if (!requestId) return;
10264
- const pending4 = _pendingExports.get(requestId);
10265
- if (!pending4) {
10433
+ const pending5 = _pendingExports.get(requestId);
10434
+ if (!pending5) {
10266
10435
  logger.warn(`[ExportBridge] Received result for unknown requestId: ${requestId}`);
10267
10436
  return;
10268
10437
  }
10269
- clearTimeout(pending4.timer);
10438
+ clearTimeout(pending5.timer);
10270
10439
  _pendingExports.delete(requestId);
10271
- pending4.resolve({
10440
+ pending5.resolve({
10272
10441
  success: args.success ?? false,
10273
10442
  filePath: args.filePath,
10274
10443
  error: args.error,
@@ -10279,9 +10448,9 @@ function handleExportResult(args) {
10279
10448
  }
10280
10449
  }
10281
10450
  function teardownExports() {
10282
- for (const [, pending4] of _pendingExports) {
10283
- clearTimeout(pending4.timer);
10284
- pending4.reject(new Error("Debug disabled \u2014 export cancelled"));
10451
+ for (const [, pending5] of _pendingExports) {
10452
+ clearTimeout(pending5.timer);
10453
+ pending5.reject(new Error("Debug disabled \u2014 export cancelled"));
10285
10454
  }
10286
10455
  _pendingExports.clear();
10287
10456
  }
@@ -11489,6 +11658,174 @@ var init_installationId = __esm({
11489
11658
  }
11490
11659
  });
11491
11660
 
11661
+ // lib/module/features/journey/scrollContext.js
11662
+ var scrollContext_exports = {};
11663
+ __export(scrollContext_exports, {
11664
+ SCROLL_OFFSET_TTL_MS: () => SCROLL_OFFSET_TTL_MS,
11665
+ clearScrollOffsets: () => clearScrollOffsets,
11666
+ nativeTagOf: () => nativeTagOf,
11667
+ nearestRecordedOffset: () => nearestRecordedOffset,
11668
+ recordScrollOffset: () => recordScrollOffset,
11669
+ trackedScrollerCount: () => trackedScrollerCount
11670
+ });
11671
+ function nativeTagOf(node) {
11672
+ if (!node || typeof node !== "object") return null;
11673
+ const n = node;
11674
+ const tag = typeof n.__nativeTag === "number" ? n.__nativeTag : n._nativeTag;
11675
+ return typeof tag === "number" && Number.isFinite(tag) ? tag : null;
11676
+ }
11677
+ function recordScrollOffset(tag, x, y, now2 = Date.now()) {
11678
+ if (tag === null || !Number.isFinite(x) || !Number.isFinite(y)) return;
11679
+ offsets.set(tag, {
11680
+ x,
11681
+ y,
11682
+ at: now2
11683
+ });
11684
+ if (offsets.size > MAX_TRACKED_SCROLLERS) {
11685
+ let oldestTag = null;
11686
+ let oldestAt = Infinity;
11687
+ for (const [k, v] of offsets) {
11688
+ if (v.at < oldestAt) {
11689
+ oldestAt = v.at;
11690
+ oldestTag = k;
11691
+ }
11692
+ }
11693
+ if (oldestTag !== null) offsets.delete(oldestTag);
11694
+ }
11695
+ }
11696
+ function nearestRecordedOffset(ancestorTags, now2 = Date.now()) {
11697
+ for (const tag of ancestorTags) {
11698
+ if (tag === null) continue;
11699
+ const hit = offsets.get(tag);
11700
+ if (!hit) continue;
11701
+ if (now2 - hit.at > SCROLL_OFFSET_TTL_MS) {
11702
+ offsets.delete(tag);
11703
+ continue;
11704
+ }
11705
+ return {
11706
+ x: hit.x,
11707
+ y: hit.y,
11708
+ tag
11709
+ };
11710
+ }
11711
+ return null;
11712
+ }
11713
+ function clearScrollOffsets() {
11714
+ offsets.clear();
11715
+ }
11716
+ function trackedScrollerCount() {
11717
+ return offsets.size;
11718
+ }
11719
+ var SCROLL_OFFSET_TTL_MS, MAX_TRACKED_SCROLLERS, offsets;
11720
+ var init_scrollContext = __esm({
11721
+ "lib/module/features/journey/scrollContext.js"() {
11722
+ "use strict";
11723
+ SCROLL_OFFSET_TTL_MS = 5 * 6e4;
11724
+ MAX_TRACKED_SCROLLERS = 32;
11725
+ offsets = /* @__PURE__ */ new Map();
11726
+ }
11727
+ });
11728
+
11729
+ // lib/module/features/journey/autoInstrumentScroll.js
11730
+ var autoInstrumentScroll_exports = {};
11731
+ __export(autoInstrumentScroll_exports, {
11732
+ autoInstrumentScrollViews: () => autoInstrumentScrollViews,
11733
+ resetAutoInstrumentForTests: () => resetAutoInstrumentForTests
11734
+ });
11735
+ function redefine(target, key, value) {
11736
+ try {
11737
+ Object.defineProperty(target, key, {
11738
+ configurable: true,
11739
+ enumerable: true,
11740
+ writable: true,
11741
+ value
11742
+ });
11743
+ return target[key] === value;
11744
+ } catch {
11745
+ return false;
11746
+ }
11747
+ }
11748
+ function instrument(Original, React14) {
11749
+ const Wrapped = React14.forwardRef((props, ref) => {
11750
+ const onScroll = (event) => {
11751
+ try {
11752
+ const target = event?.nativeEvent?.target;
11753
+ const offset = event?.nativeEvent?.contentOffset;
11754
+ if (offset) {
11755
+ recordScrollOffset(typeof target === "number" ? target : nativeTagOf(target), offset.x, offset.y);
11756
+ }
11757
+ } catch {
11758
+ }
11759
+ try {
11760
+ props?.onScroll?.(event);
11761
+ } catch (e) {
11762
+ setTimeout(() => {
11763
+ throw e;
11764
+ }, 0);
11765
+ }
11766
+ };
11767
+ const throttle = props?.scrollEventThrottle ?? 16;
11768
+ return React14.createElement(Original, {
11769
+ ...props,
11770
+ scrollEventThrottle: throttle,
11771
+ onScroll,
11772
+ ref
11773
+ });
11774
+ });
11775
+ Wrapped.displayName = "ScaleBunAutoScrollView";
11776
+ return Wrapped;
11777
+ }
11778
+ function autoInstrumentScrollViews() {
11779
+ if (alreadyPatched) return {
11780
+ installed: true
11781
+ };
11782
+ try {
11783
+ const RN = require("react-native");
11784
+ const React14 = require("react");
11785
+ const Original = RN.ScrollView;
11786
+ if (typeof Original !== "function" && typeof Original !== "object") {
11787
+ return {
11788
+ installed: false,
11789
+ reason: "react-native has no ScrollView export to wrap"
11790
+ };
11791
+ }
11792
+ if (Original.__scalebunInstrumented) {
11793
+ alreadyPatched = true;
11794
+ return {
11795
+ installed: true
11796
+ };
11797
+ }
11798
+ const Wrapped = instrument(Original, React14);
11799
+ Wrapped.__scalebunInstrumented = true;
11800
+ if (!redefine(RN, "ScrollView", Wrapped)) {
11801
+ return {
11802
+ installed: false,
11803
+ reason: "react-native's ScrollView export could not be replaced \u2014 it is a getter with no configurable descriptor on this bundler. Use ScaleBunScrollView explicitly instead."
11804
+ };
11805
+ }
11806
+ alreadyPatched = true;
11807
+ return {
11808
+ installed: true
11809
+ };
11810
+ } catch (e) {
11811
+ return {
11812
+ installed: false,
11813
+ reason: e?.message ?? "unknown failure"
11814
+ };
11815
+ }
11816
+ }
11817
+ function resetAutoInstrumentForTests() {
11818
+ alreadyPatched = false;
11819
+ }
11820
+ var alreadyPatched;
11821
+ var init_autoInstrumentScroll = __esm({
11822
+ "lib/module/features/journey/autoInstrumentScroll.js"() {
11823
+ "use strict";
11824
+ init_scrollContext();
11825
+ alreadyPatched = false;
11826
+ }
11827
+ });
11828
+
11492
11829
  // lib/module/features/engage/engageSignals.js
11493
11830
  var EngageSignalBus, engageSignals;
11494
11831
  var init_engageSignals = __esm({
@@ -11697,322 +12034,6 @@ var init_deviceAttributes = __esm({
11697
12034
  }
11698
12035
  });
11699
12036
 
11700
- // lib/module/features/ota/crypto/loadEd25519.js
11701
- function loadEd25519() {
11702
- try {
11703
- const mod = require("@noble/ed25519");
11704
- const ed = mod?.default ?? mod;
11705
- if (typeof ed?.verifyAsync === "function" || typeof ed?.verify === "function") {
11706
- return ed;
11707
- }
11708
- return null;
11709
- } catch {
11710
- return null;
11711
- }
11712
- }
11713
- var init_loadEd25519 = __esm({
11714
- "lib/module/features/ota/crypto/loadEd25519.js"() {
11715
- "use strict";
11716
- }
11717
- });
11718
-
11719
- // lib/module/features/ota/crypto/loadSha512.js
11720
- function loadSha512() {
11721
- try {
11722
- const mod = require("@noble/hashes/sha2.js");
11723
- const sha512 = mod?.sha512 ?? mod?.default?.sha512;
11724
- return typeof sha512 === "function" ? sha512 : null;
11725
- } catch {
11726
- return null;
11727
- }
11728
- }
11729
- var init_loadSha512 = __esm({
11730
- "lib/module/features/ota/crypto/loadSha512.js"() {
11731
- "use strict";
11732
- }
11733
- });
11734
-
11735
- // lib/module/features/ota/crypto/builtinVerifier.js
11736
- var builtinVerifier_exports = {};
11737
- __export(builtinVerifier_exports, {
11738
- _resetBuiltinVerifier: () => _resetBuiltinVerifier,
11739
- decodeSignature: () => decodeSignature,
11740
- getBuiltinVerifier: () => getBuiltinVerifier
11741
- });
11742
- function hexToBytes(hex) {
11743
- const clean2 = hex.trim().toLowerCase().replace(/^0x/, "");
11744
- if (clean2.length === 0 || clean2.length % 2 !== 0 || /[^0-9a-f]/.test(clean2)) return null;
11745
- const out = new Uint8Array(clean2.length / 2);
11746
- for (let i = 0; i < out.length; i++) {
11747
- out[i] = parseInt(clean2.slice(i * 2, i * 2 + 2), 16);
11748
- }
11749
- return out;
11750
- }
11751
- function base64ToBytes2(b64) {
11752
- const s = b64.trim().replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
11753
- if (s.length === 0) return null;
11754
- const out = [];
11755
- let buffer = 0;
11756
- let bits = 0;
11757
- for (const ch of s) {
11758
- const idx = B64_ALPHABET2.indexOf(ch);
11759
- if (idx === -1) return null;
11760
- buffer = buffer << 6 | idx;
11761
- bits += 6;
11762
- if (bits >= 8) {
11763
- bits -= 8;
11764
- out.push(buffer >> bits & 255);
11765
- }
11766
- }
11767
- return Uint8Array.from(out);
11768
- }
11769
- function decodeSignature(sig) {
11770
- const trimmed = sig.trim();
11771
- if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
11772
- const hex2 = hexToBytes(trimmed);
11773
- if (hex2 && hex2.length === ED25519_SIGNATURE_BYTES) return hex2;
11774
- }
11775
- const b64 = base64ToBytes2(trimmed);
11776
- if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
11777
- const hex = hexToBytes(trimmed);
11778
- if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
11779
- return null;
11780
- }
11781
- function wireSha512(ed, sha512) {
11782
- const concat2 = (parts) => {
11783
- if (parts.length === 1) return parts[0];
11784
- let total = 0;
11785
- for (const p of parts) total += p.length;
11786
- const out = new Uint8Array(total);
11787
- let off = 0;
11788
- for (const p of parts) {
11789
- out.set(p, off);
11790
- off += p.length;
11791
- }
11792
- return out;
11793
- };
11794
- const sync = (...msgs) => sha512(concat2(msgs));
11795
- const asyncFn = async (...msgs) => sha512(concat2(msgs));
11796
- let wired = false;
11797
- try {
11798
- if (ed.hashes && typeof ed.hashes === "object") {
11799
- if (typeof ed.hashes.sha512 !== "function") ed.hashes.sha512 = sync;
11800
- if (typeof ed.hashes.sha512Async !== "function") ed.hashes.sha512Async = asyncFn;
11801
- wired = typeof ed.hashes.sha512 === "function";
11802
- }
11803
- } catch {
11804
- }
11805
- try {
11806
- if (ed.etc && typeof ed.etc === "object") {
11807
- if (typeof ed.etc.sha512Sync !== "function") ed.etc.sha512Sync = sync;
11808
- if (typeof ed.etc.sha512Async !== "function") ed.etc.sha512Async = asyncFn;
11809
- wired = wired || typeof ed.etc.sha512Sync === "function";
11810
- }
11811
- } catch {
11812
- }
11813
- return wired;
11814
- }
11815
- function getBuiltinVerifier() {
11816
- if (resolved !== void 0) return resolved;
11817
- const ed = loadEd25519();
11818
- const sha512 = loadSha512();
11819
- if (!ed || !sha512) {
11820
- resolved = null;
11821
- return null;
11822
- }
11823
- if (!wireSha512(ed, sha512)) {
11824
- logger.error("[OTA] @noble/ed25519 is installed but its SHA-512 provider could not be wired (unrecognised version shape). Supply `ota.verifySignature` yourself, or pin a @noble/ed25519 version this SDK supports.");
11825
- resolved = null;
11826
- return null;
11827
- }
11828
- resolved = async ({
11829
- messageHex,
11830
- signature,
11831
- publicKey
11832
- }) => {
11833
- const message = hexToBytes(messageHex);
11834
- const pub = hexToBytes(publicKey);
11835
- const sig = decodeSignature(signature);
11836
- if (!message) {
11837
- logger.error("[OTA] Bundle SHA-256 is not valid hex \u2014 refusing to stage.");
11838
- return false;
11839
- }
11840
- if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
11841
- logger.error(`[OTA] ota.publicSigningKey must be ${ED25519_PUBLIC_KEY_BYTES} bytes of hex (${ED25519_PUBLIC_KEY_BYTES * 2} characters) \u2014 got ${pub ? `${pub.length} bytes` : "a value that is not hex"}.`);
11842
- return false;
11843
- }
11844
- if (!sig) {
11845
- logger.error("[OTA] Bundle signature is not a 64-byte hex/base64 value.");
11846
- return false;
11847
- }
11848
- try {
11849
- if (typeof ed.verify === "function") return ed.verify(sig, message, pub);
11850
- if (typeof ed.verifyAsync === "function") return await ed.verifyAsync(sig, message, pub);
11851
- return false;
11852
- } catch {
11853
- return false;
11854
- }
11855
- };
11856
- __DEV__ && logger.debug("[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).");
11857
- return resolved;
11858
- }
11859
- function _resetBuiltinVerifier() {
11860
- resolved = void 0;
11861
- }
11862
- var ED25519_SIGNATURE_BYTES, ED25519_PUBLIC_KEY_BYTES, B64_ALPHABET2, resolved;
11863
- var init_builtinVerifier = __esm({
11864
- "lib/module/features/ota/crypto/builtinVerifier.js"() {
11865
- "use strict";
11866
- init_loadEd25519();
11867
- init_loadSha512();
11868
- init_internalLogger();
11869
- ED25519_SIGNATURE_BYTES = 64;
11870
- ED25519_PUBLIC_KEY_BYTES = 32;
11871
- B64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
11872
- }
11873
- });
11874
-
11875
- // lib/module/features/ota/crypto/nativeVerifier.js
11876
- function bytesToHex(bytes) {
11877
- let out = "";
11878
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
11879
- return out;
11880
- }
11881
- function normalizeHex(value, expectedChars) {
11882
- const clean2 = value.trim().toLowerCase().replace(/^0x/, "");
11883
- if (clean2.length !== expectedChars || /[^0-9a-f]/.test(clean2)) return null;
11884
- return clean2;
11885
- }
11886
- function getNativeVerifier() {
11887
- if (resolved2 !== void 0) return resolved2;
11888
- const native = NativeScaleBunOta_default;
11889
- if (!native || typeof native.verifyEd25519 !== "function") {
11890
- resolved2 = null;
11891
- return null;
11892
- }
11893
- resolved2 = async ({
11894
- messageHex,
11895
- signature,
11896
- publicKey
11897
- }) => {
11898
- const message = normalizeHex(messageHex, 64);
11899
- const key = normalizeHex(publicKey, ED25519_PUBLIC_KEY_HEX_CHARS);
11900
- const sigBytes = decodeSignature(signature);
11901
- if (!message || !key || !sigBytes) {
11902
- logger.error("[OTA] Signature inputs malformed (bundle hash, signature, or public key) \u2014 refusing to stage.");
11903
- return false;
11904
- }
11905
- let verdict;
11906
- try {
11907
- verdict = await native.verifyEd25519(message, bytesToHex(sigBytes), key);
11908
- } catch (err) {
11909
- logger.warn(`[OTA] Native ed25519 verify failed to run: ${err?.message ?? err}`);
11910
- verdict = "unavailable";
11911
- }
11912
- if (verdict === "valid") return true;
11913
- if (verdict === "invalid") return false;
11914
- const builtin = getBuiltinVerifier();
11915
- if (builtin) {
11916
- return builtin({
11917
- messageHex,
11918
- signature,
11919
- publicKey
11920
- });
11921
- }
11922
- throw new Error("ed25519 unavailable: this OS has no native implementation (Android < 13) and the optional peers @noble/ed25519 + @noble/hashes are not installed.");
11923
- };
11924
- __DEV__ && logger.debug("[OTA] Using native ed25519 verifier (platform crypto).");
11925
- return resolved2;
11926
- }
11927
- var ED25519_PUBLIC_KEY_HEX_CHARS, resolved2;
11928
- var init_nativeVerifier = __esm({
11929
- "lib/module/features/ota/crypto/nativeVerifier.js"() {
11930
- "use strict";
11931
- init_NativeScaleBunOta();
11932
- init_internalLogger();
11933
- init_builtinVerifier();
11934
- ED25519_PUBLIC_KEY_HEX_CHARS = 64;
11935
- }
11936
- });
11937
-
11938
- // lib/module/features/ota/signature.js
11939
- async function verifyBundleSignature(bundleSha256, signature, config) {
11940
- const rawKey = config?.publicKey;
11941
- const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter((k) => typeof k === "string" && k.trim().length > 0);
11942
- if (rawKey === void 0 || rawKey === "" || rawKey === null) {
11943
- if (!warnedNotConfigured) {
11944
- warnedNotConfigured = true;
11945
- logger.warn("[OTA] No signing public key configured \u2014 bundles are accepted on SHA-256 integrity alone. Configure `ota.publicSigningKey` to enforce authenticity.");
11946
- }
11947
- return {
11948
- ok: true,
11949
- reason: "not_configured"
11950
- };
11951
- }
11952
- if (keys.length === 0) {
11953
- logger.error("[OTA] Signing key config resolves to zero usable keys (empty array or blank strings) \u2014 refusing to stage. Remove the config to disable signing, or pin at least one real key.");
11954
- return {
11955
- ok: false,
11956
- reason: "no_keys"
11957
- };
11958
- }
11959
- if (!signature) {
11960
- logger.error("[OTA] Bundle has no signature but a signing key is configured \u2014 refusing to stage.");
11961
- return {
11962
- ok: false,
11963
- reason: "missing_signature"
11964
- };
11965
- }
11966
- const verifier = typeof config?.verifier === "function" ? config.verifier : getNativeVerifier() ?? getBuiltinVerifier();
11967
- if (!verifier) {
11968
- logger.error("[OTA] A signing key is configured but no signature verifier is available \u2014 refusing to stage. Either install the optional peers `@noble/ed25519` and `@noble/hashes` (the SDK then verifies with no further code), or provide your own `ota.verifySignature`.");
11969
- return {
11970
- ok: false,
11971
- reason: "no_verifier"
11972
- };
11973
- }
11974
- let sawThrow = false;
11975
- let sawCompletion = false;
11976
- for (const key of keys) {
11977
- try {
11978
- const valid = await verifier({
11979
- messageHex: bundleSha256,
11980
- signature,
11981
- publicKey: key
11982
- });
11983
- sawCompletion = true;
11984
- if (valid) return {
11985
- ok: true,
11986
- reason: "verified"
11987
- };
11988
- } catch (err) {
11989
- sawThrow = true;
11990
- logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
11991
- }
11992
- }
11993
- if (sawThrow && !sawCompletion) {
11994
- return {
11995
- ok: false,
11996
- reason: "verifier_threw"
11997
- };
11998
- }
11999
- logger.error(keys.length > 1 ? `[OTA] Bundle signature is INVALID under all ${keys.length} pinned keys \u2014 refusing to stage.` : "[OTA] Bundle signature is INVALID \u2014 refusing to stage.");
12000
- return {
12001
- ok: false,
12002
- reason: "invalid_signature"
12003
- };
12004
- }
12005
- var warnedNotConfigured;
12006
- var init_signature = __esm({
12007
- "lib/module/features/ota/signature.js"() {
12008
- "use strict";
12009
- init_internalLogger();
12010
- init_builtinVerifier();
12011
- init_nativeVerifier();
12012
- warnedNotConfigured = false;
12013
- }
12014
- });
12015
-
12016
12037
  // lib/module/features/ota/environment.js
12017
12038
  function compare(a, b) {
12018
12039
  if (a.major !== b.major) return a.major - b.major;
@@ -12240,10 +12261,10 @@ async function deliverOtaEvents(params) {
12240
12261
  })
12241
12262
  });
12242
12263
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
12243
- __DEV__ && logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
12264
+ logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
12244
12265
  } catch (err) {
12245
12266
  for (const e of events) otaEventEmitter.emit(e);
12246
- __DEV__ && logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
12267
+ logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
12247
12268
  }
12248
12269
  }
12249
12270
  var import_react_native34, PROGRESS_POST_INTERVAL_MS, DEFAULT_HEALTHY_AFTER_MS, OtaOrchestrator, otaOrchestrator;
@@ -12258,7 +12279,6 @@ var init_OtaOrchestrator = __esm({
12258
12279
  init_geoCountry();
12259
12280
  init_deviceAttributes();
12260
12281
  init_installationId();
12261
- init_signature();
12262
12282
  init_environment();
12263
12283
  init_retry();
12264
12284
  init_StorageBackend();
@@ -12277,6 +12297,8 @@ var init_OtaOrchestrator = __esm({
12277
12297
  return this.currentBundle;
12278
12298
  }
12279
12299
  isRestartRequiredState = false;
12300
+ /** Root/jailbreak indicators observed at init; empty when none found. */
12301
+ integrityIndicators = [];
12280
12302
  healthyTimer = null;
12281
12303
  bootGuardConfig = {};
12282
12304
  environment = null;
@@ -12286,15 +12308,15 @@ var init_OtaOrchestrator = __esm({
12286
12308
  /**
12287
12309
  * Initialize the orchestrator. Checks for boot-guard recovery state.
12288
12310
  *
12289
- * `signature` is optional: an app that has not adopted signing keeps working
12290
- * on SHA-256 integrity alone. Once a public key IS configured, a bundle that
12291
- * cannot be verified is refused rather than installed (OTA-03).
12311
+ * Signature enforcement is no longer a JS concern: signed releases are
12312
+ * verified and staged in NATIVE code against RSA keys embedded in the app
12313
+ * package (SB-OTA-RSA-SHA256-V1), and unsigned releases keep working on
12314
+ * SHA-256 integrity alone. There is nothing for the host to configure here.
12292
12315
  */
12293
12316
  init(config) {
12294
12317
  noThrow(() => {
12295
12318
  if (this.initialized) {
12296
12319
  this.bootGuardConfig = config ?? this.bootGuardConfig;
12297
- this.signatureConfig = config?.signature ?? this.signatureConfig;
12298
12320
  return;
12299
12321
  }
12300
12322
  if (!NativeScaleBunOta_default) {
@@ -12311,18 +12333,27 @@ var init_OtaOrchestrator = __esm({
12311
12333
  logger.warn("[OTA] Running bridgeless but the React Native version could not be determined. If this app is below RN 0.76.1, OTA updates will install and never load.");
12312
12334
  }
12313
12335
  if (this.environment.requiresReactHostIntegration) {
12314
- __DEV__ && logger.debug("[OTA] React Native >= 0.82 detected. The only valid Android integration is getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) \u2014 ReactNativeHost and its getJSBundleFile() override no longer exist.");
12336
+ logger.debug("[OTA] React Native >= 0.82 detected. The only valid Android integration is getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) \u2014 ReactNativeHost and its getJSBundleFile() override no longer exist.");
12315
12337
  }
12316
12338
  this.initialized = true;
12317
12339
  this.enabled = true;
12340
+ try {
12341
+ const raw = NativeScaleBunOta_default?.getDeviceIntegrity?.();
12342
+ this.integrityIndicators = raw ? JSON.parse(raw) : [];
12343
+ if (this.integrityIndicators.length > 0) {
12344
+ logger.warn(`[OTA] Device integrity indicators present: ${this.integrityIndicators.join(", ")}. Updates still install and are still signature-verified, but an installed bundle on this device can be modified by other privileged processes.`);
12345
+ }
12346
+ } catch {
12347
+ this.integrityIndicators = [];
12348
+ }
12318
12349
  this.bootGuardConfig = config ?? {};
12319
- this.signatureConfig = config?.signature;
12320
- __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12350
+ logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12321
12351
  const slotState = this.readSlotState();
12322
12352
  this.hydrateCurrentBundleFromSlots(slotState);
12323
12353
  void prefetchDeviceCountry();
12324
12354
  this.checkBootGuardRecovery(slotState);
12325
12355
  this.verifyRunningBundleIdentity();
12356
+ this.startBootGuardHeartbeat(slotState);
12326
12357
  });
12327
12358
  }
12328
12359
  /**
@@ -12353,13 +12384,13 @@ var init_OtaOrchestrator = __esm({
12353
12384
  releaseId: record4.releaseId ?? void 0,
12354
12385
  sha256: record4.sha256
12355
12386
  };
12356
- __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
12387
+ logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
12357
12388
  return;
12358
12389
  }
12359
12390
  this.currentBundle = {
12360
12391
  sha256: current.sha256
12361
12392
  };
12362
- __DEV__ && logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
12393
+ logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
12363
12394
  } catch {
12364
12395
  }
12365
12396
  }
@@ -12389,7 +12420,7 @@ var init_OtaOrchestrator = __esm({
12389
12420
  }
12390
12421
  if (!record4.identityToken) return;
12391
12422
  if (running === record4.identityToken) {
12392
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12423
+ logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12393
12424
  this.retireIdentityToken(record4);
12394
12425
  return;
12395
12426
  }
@@ -12492,6 +12523,13 @@ var init_OtaOrchestrator = __esm({
12492
12523
  isEnabled() {
12493
12524
  return this.enabled;
12494
12525
  }
12526
+ /**
12527
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
12528
+ * Empty means nothing was detected — not proof the device is clean.
12529
+ */
12530
+ deviceIntegrityIndicators() {
12531
+ return [...this.integrityIndicators];
12532
+ }
12495
12533
  isRestartRequired() {
12496
12534
  return this.isRestartRequiredState;
12497
12535
  }
@@ -12503,12 +12541,22 @@ var init_OtaOrchestrator = __esm({
12503
12541
  const env = this.environment ?? detectOtaEnvironment();
12504
12542
  const mobileOS = resolveMobileOS();
12505
12543
  if (!mobileOS) throw new Error("OTA is not supported on this platform");
12544
+ let trustedKeyIds;
12545
+ try {
12546
+ const raw = NativeScaleBunOta_default?.getTrustedKeyIds?.();
12547
+ if (raw) {
12548
+ const arr2 = JSON.parse(raw);
12549
+ if (Array.isArray(arr2) && arr2.length) trustedKeyIds = arr2.slice(0, 16);
12550
+ }
12551
+ } catch {
12552
+ }
12506
12553
  const payload = {
12507
12554
  appVersion: requestParams.appVersion,
12508
12555
  platform: mobileOS,
12509
12556
  installationId: requestParams.installationId,
12510
12557
  currentBundleId: this.currentBundle?.id,
12511
12558
  currentBundleHash: this.currentBundle?.sha256,
12559
+ trustedKeyIds,
12512
12560
  channelName: requestParams.channelName,
12513
12561
  // Host-supplied country wins; otherwise the cached edge-Worker lookup
12514
12562
  // fills in (see geoCountry.ts). Undefined when neither knows — the
@@ -12575,7 +12623,7 @@ var init_OtaOrchestrator = __esm({
12575
12623
  error: "OTA native module disabled or missing"
12576
12624
  };
12577
12625
  }
12578
- __DEV__ && logger.debug("[OTA] Sync started\u2026");
12626
+ logger.debug("[OTA] Sync started\u2026");
12579
12627
  if (this.currentBundle?.id) {
12580
12628
  otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
12581
12629
  releaseId: this.currentBundle.releaseId,
@@ -12584,7 +12632,7 @@ var init_OtaOrchestrator = __esm({
12584
12632
  }
12585
12633
  const checkRes = await this.checkForUpdate(params);
12586
12634
  if (checkRes.action === "NONE") {
12587
- __DEV__ && logger.debug("[OTA] App is up to date");
12635
+ logger.debug("[OTA] App is up to date");
12588
12636
  this.startBootGuardHeartbeat();
12589
12637
  return {
12590
12638
  status: "UP_TO_DATE"
@@ -12622,25 +12670,12 @@ var init_OtaOrchestrator = __esm({
12622
12670
  releaseId: bundle.releaseId,
12623
12671
  version: bundle.version
12624
12672
  });
12625
- const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
12626
- if (!signatureOutcome.ok) {
12627
- otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12628
- releaseId: bundle.releaseId,
12629
- error: `Signature check failed: ${signatureOutcome.reason}`,
12630
- version: bundle.version
12631
- });
12632
- return {
12633
- status: "ERROR",
12634
- error: `Bundle signature check failed (${signatureOutcome.reason})`,
12635
- bundle
12636
- };
12637
- }
12638
12673
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
12639
12674
  releaseId: bundle.releaseId,
12640
12675
  version: bundle.version
12641
12676
  });
12642
12677
  const downloadStart = Date.now();
12643
- __DEV__ && logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
12678
+ logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
12644
12679
  let lastPostAt = 0;
12645
12680
  const postProgress = (bytesRead, status) => {
12646
12681
  const total = bundle.size > 0 ? bundle.size : Math.max(bytesRead, 1);
@@ -12682,39 +12717,75 @@ var init_OtaOrchestrator = __esm({
12682
12717
  }
12683
12718
  });
12684
12719
  let staged = false;
12685
- if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
12686
- __DEV__ && logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
12720
+ if (bundle.envelope) {
12721
+ const envelopeJson = JSON.stringify({
12722
+ ...bundle.envelope,
12723
+ downloadUrl: bundle.url
12724
+ });
12725
+ let verdict;
12687
12726
  try {
12688
- staged = await NativeScaleBunOta_default.stagePatch(
12689
- bundle.patch.url,
12690
- bundle.patch.sha256,
12691
- bundle.sha256
12692
- // expected full-bundle hash after patching
12693
- );
12694
- if (staged) {
12695
- patchUsed = true;
12696
- __DEV__ && logger.debug("[OTA] Patch applied successfully");
12697
- } else {
12698
- logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
12699
- }
12700
- } catch (patchErr) {
12701
- logger.warn(`[OTA] Patch staging failed: ${patchErr.message} \u2014 falling back to full bundle`);
12727
+ const verdictJson = await NativeScaleBunOta_default.verifyAndStageRelease(envelopeJson);
12728
+ verdict = JSON.parse(verdictJson);
12729
+ } catch (err) {
12730
+ verdict = {
12731
+ ok: false,
12732
+ code: "verify_stage_threw",
12733
+ message: err?.message ?? String(err)
12734
+ };
12702
12735
  }
12703
- }
12704
- if (!staged) {
12705
- try {
12706
- staged = await retryWithBackoff(async () => {
12707
- const ok = await NativeScaleBunOta_default.stageBundle(bundle.url, bundle.sha256);
12708
- if (!ok) {
12709
- throw new Error("stageBundle returned false");
12710
- }
12711
- return ok;
12712
- }, {
12713
- label: `download v${bundle.version}`,
12714
- attempts: 3
12736
+ if (!verdict.ok) {
12737
+ unsubscribeProgress();
12738
+ postProgress(0, "FAILED");
12739
+ logger.error(`[OTA] Native verify/stage refused the release: ${verdict.code ?? "unknown"}${verdict.message ? ` \u2014 ${verdict.message}` : ""}`);
12740
+ otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12741
+ releaseId: bundle.releaseId,
12742
+ // `error` is mapped onto the wire `errorCode` by deliverOtaEvents —
12743
+ // carry the stable native verdict code there.
12744
+ error: verdict.code ?? "verify_and_stage_release_failed",
12745
+ version: bundle.version
12715
12746
  });
12716
- } catch {
12717
- staged = false;
12747
+ return {
12748
+ status: "ERROR",
12749
+ error: `Release verification failed (${verdict.code ?? "unknown"})`,
12750
+ bundle
12751
+ };
12752
+ }
12753
+ staged = true;
12754
+ } else {
12755
+ if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
12756
+ logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
12757
+ try {
12758
+ staged = await NativeScaleBunOta_default.stagePatch(
12759
+ bundle.patch.url,
12760
+ bundle.patch.sha256,
12761
+ bundle.sha256
12762
+ // expected full-bundle hash after patching
12763
+ );
12764
+ if (staged) {
12765
+ patchUsed = true;
12766
+ logger.debug("[OTA] Patch applied successfully");
12767
+ } else {
12768
+ logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
12769
+ }
12770
+ } catch (patchErr) {
12771
+ logger.warn(`[OTA] Patch staging failed: ${patchErr.message} \u2014 falling back to full bundle`);
12772
+ }
12773
+ }
12774
+ if (!staged) {
12775
+ try {
12776
+ staged = await retryWithBackoff(async () => {
12777
+ const ok = await NativeScaleBunOta_default.stageBundle(bundle.url, bundle.sha256);
12778
+ if (!ok) {
12779
+ throw new Error("stageBundle returned false");
12780
+ }
12781
+ return ok;
12782
+ }, {
12783
+ label: `download v${bundle.version}`,
12784
+ attempts: 3
12785
+ });
12786
+ } catch {
12787
+ staged = false;
12788
+ }
12718
12789
  }
12719
12790
  }
12720
12791
  unsubscribeProgress();
@@ -12761,7 +12832,7 @@ var init_OtaOrchestrator = __esm({
12761
12832
  releaseId: bundle.releaseId,
12762
12833
  version: bundle.version
12763
12834
  });
12764
- __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
12835
+ logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
12765
12836
  await deliverOtaEvents(params);
12766
12837
  const installMode = bundle.installMode ?? "ON_NEXT_RESTART";
12767
12838
  if (params.autoRestart || installMode === "IMMEDIATE") {
@@ -12792,24 +12863,23 @@ var init_OtaOrchestrator = __esm({
12792
12863
  * native boot-guard marker. If JS crashes before the timer fires, the
12793
12864
  * marker stays set and native code reverts on next cold start.
12794
12865
  */
12795
- startBootGuardHeartbeat() {
12866
+ startBootGuardHeartbeat(preRead) {
12796
12867
  if (!NativeScaleBunOta_default) return;
12797
12868
  if (this.healthyTimer) {
12798
12869
  clearTimeout(this.healthyTimer);
12799
12870
  this.healthyTimer = null;
12800
12871
  }
12801
12872
  try {
12802
- const stateJson = NativeScaleBunOta_default.getSlotState();
12803
- const state = JSON.parse(stateJson);
12804
- if (!state.bootMarkerPresent) {
12873
+ const state = preRead !== void 0 ? preRead : JSON.parse(NativeScaleBunOta_default.getSlotState());
12874
+ if (!state?.bootMarkerPresent) {
12805
12875
  return;
12806
12876
  }
12807
12877
  const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
12808
- __DEV__ && logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
12878
+ logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
12809
12879
  this.healthyTimer = setTimeout(() => {
12810
12880
  noThrow(() => {
12811
12881
  NativeScaleBunOta_default?.markHealthy();
12812
- __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12882
+ logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12813
12883
  this.healthyTimer = null;
12814
12884
  const running = this.currentBundle;
12815
12885
  if (running?.id) {
@@ -12855,11 +12925,11 @@ var init_OtaOrchestrator = __esm({
12855
12925
  } catch {
12856
12926
  }
12857
12927
  this.resumeRestartArmed = false;
12858
- __DEV__ && logger.info("[OTA] App resumed \u2014 activating the staged update now");
12928
+ logger.info("[OTA] App resumed \u2014 activating the staged update now");
12859
12929
  this.restart();
12860
12930
  }
12861
12931
  });
12862
- __DEV__ && logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
12932
+ logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
12863
12933
  } catch {
12864
12934
  this.resumeRestartArmed = false;
12865
12935
  }
@@ -12876,7 +12946,7 @@ var init_OtaOrchestrator = __esm({
12876
12946
  */
12877
12947
  restart() {
12878
12948
  noThrow(() => {
12879
- __DEV__ && logger.info("[OTA] Triggering app restart\u2026");
12949
+ logger.info("[OTA] Triggering app restart\u2026");
12880
12950
  if (NativeScaleBunOta_default && typeof NativeScaleBunOta_default.restartApp === "function") {
12881
12951
  NativeScaleBunOta_default.restartApp();
12882
12952
  } else if (import_react_native34.DevSettings && typeof import_react_native34.DevSettings.reload === "function") {
@@ -12895,17 +12965,25 @@ var init_OtaOrchestrator = __esm({
12895
12965
  var uiState_exports = {};
12896
12966
  __export(uiState_exports, {
12897
12967
  clearUiState: () => clearUiState,
12968
+ onUiStateChange: () => onUiStateChange,
12969
+ resetUiStateListeners: () => resetUiStateListeners,
12898
12970
  setUiState: () => setUiState,
12971
+ uiStateListenerCount: () => uiStateListenerCount,
12899
12972
  uiStateSignature: () => uiStateSignature
12900
12973
  });
12901
12974
  function setUiState(name, value) {
12902
12975
  const n = clean(name);
12903
12976
  const v = clean(value);
12904
- if (n && v) declared.set(n, v);
12977
+ if (!n || !v) return;
12978
+ if (declared.get(n) === v) return;
12979
+ declared.set(n, v);
12980
+ notifyChanged();
12905
12981
  }
12906
12982
  function clearUiState(name) {
12983
+ const before = declared.size;
12907
12984
  if (name === void 0) declared.clear();
12908
12985
  else declared.delete(clean(name));
12986
+ if (declared.size !== before) notifyChanged();
12909
12987
  }
12910
12988
  function uiStateSignature() {
12911
12989
  if (!declared.size) return void 0;
@@ -12918,12 +12996,42 @@ function uiStateSignature() {
12918
12996
  }
12919
12997
  return signature || void 0;
12920
12998
  }
12921
- var declared, clean;
12999
+ function onUiStateChange(listener) {
13000
+ listeners2.add(listener);
13001
+ return () => {
13002
+ listeners2.delete(listener);
13003
+ };
13004
+ }
13005
+ function resetUiStateListeners() {
13006
+ listeners2.clear();
13007
+ if (pending4 !== null) {
13008
+ clearTimeout(pending4);
13009
+ pending4 = null;
13010
+ }
13011
+ }
13012
+ function uiStateListenerCount() {
13013
+ return listeners2.size;
13014
+ }
13015
+ function notifyChanged() {
13016
+ if (pending4 !== null) return;
13017
+ pending4 = setTimeout(() => {
13018
+ pending4 = null;
13019
+ for (const listener of listeners2) {
13020
+ try {
13021
+ listener();
13022
+ } catch {
13023
+ }
13024
+ }
13025
+ }, 0);
13026
+ }
13027
+ var declared, clean, listeners2, pending4;
12922
13028
  var init_uiState = __esm({
12923
13029
  "lib/module/features/journey/uiState.js"() {
12924
13030
  "use strict";
12925
13031
  declared = /* @__PURE__ */ new Map();
12926
13032
  clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
13033
+ listeners2 = /* @__PURE__ */ new Set();
13034
+ pending4 = null;
12927
13035
  }
12928
13036
  });
12929
13037
 
@@ -14824,6 +14932,22 @@ var SHAPE = {
14824
14932
  }
14825
14933
  },
14826
14934
  captureInteractionHeatmap: bool(true),
14935
+ /**
14936
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
14937
+ * offset, not just ones wrapped in ScaleBunScrollView.
14938
+ *
14939
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
14940
+ * That flag changes what the SDK records about the host; this one changes what the HOST
14941
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
14942
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
14943
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
14944
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
14945
+ * discover afterwards.
14946
+ *
14947
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
14948
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
14949
+ */
14950
+ autoInstrumentScrollViews: bool(false),
14827
14951
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
14828
14952
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
14829
14953
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -14840,18 +14964,9 @@ var SHAPE = {
14840
14964
  kind: "str",
14841
14965
  opt: true
14842
14966
  },
14843
- publicSigningKey: {
14844
- kind: "str",
14845
- opt: true
14846
- },
14847
- // Rotation support: several pinned keys, any of which may verify a
14848
- // bundle. Ship a build pinning [old, new], re-sign server-side with
14849
- // new, drop old next release — no emergency store submission when a
14850
- // key must be replaced. Merged with publicSigningKey by the facade.
14851
- publicSigningKeys: {
14852
- kind: "strArr",
14853
- opt: true
14854
- },
14967
+ // No signing-key fields: signed releases are verified natively against
14968
+ // RSA keys embedded in the app package (SB-OTA-RSA-SHA256-V1). JS must
14969
+ // not supply a trusted key.
14855
14970
  mandatoryBlocksUi: bool(false)
14856
14971
  }
14857
14972
  }
@@ -15654,6 +15769,23 @@ function installNetworkMonitoring(onEvent, options = {}) {
15654
15769
  });
15655
15770
  }
15656
15771
 
15772
+ // lib/module/features/network/thirdParty.js
15773
+ var THIRD_PARTY_BEACON_HOSTS = "google-analytics.com,googletagmanager.com,analytics.google.com,doubleclick.net,googlesyndication.com,googleadservices.com,app-measurement.com,facebook.net,analytics.tiktok.com,ads-twitter.com,analytics.twitter.com,ct.pinterest.com,bat.bing.com,px.ads.linkedin.com,tr.snapchat.com,criteo.com,criteo.net,taboola.com,outbrain.com,adroll.com,adsrvr.org,quantserve.com,scorecardresearch.com,hotjar.com,hotjar.io,clarity.ms,fullstory.com,mouseflow.com,crazyegg.com,luckyorange.com,inspectlet.com,segment.io,mixpanel.com,amplitude.com,heap.io,heapanalytics.com,rudderstack.com,posthog.com,matomo.cloud,plausible.io,simpleanalytics.com,statcounter.com,appsflyer.com,adjust.com,branch.io,ipify.org,ipinfo.io,ipapi.co,ip-api.com,geolocation-db.com,cloudflareinsights.com,nr-data.net,browser-intake-datadoghq.com,sentry.io,bugsnag.com,rollbar.com";
15774
+ function isThirdPartyBeacon(url, hosts) {
15775
+ if (!url) return false;
15776
+ let host = "";
15777
+ try {
15778
+ host = new URL(url).hostname.toLowerCase();
15779
+ } catch {
15780
+ return false;
15781
+ }
15782
+ if (!host) return false;
15783
+ for (const suffix of (hosts ?? THIRD_PARTY_BEACON_HOSTS).split(",")) {
15784
+ if (host === suffix || host.endsWith("." + suffix)) return true;
15785
+ }
15786
+ return false;
15787
+ }
15788
+
15657
15789
  // lib/module/features/network/NetworkFeature.js
15658
15790
  init_networkAdapter();
15659
15791
  init_SessionManager();
@@ -15669,10 +15801,12 @@ var NetworkFeature = class {
15669
15801
  if (this.active) return;
15670
15802
  installNetworkMonitoring((data) => {
15671
15803
  const isInternal = isSdkInternalUrl(data.url, this.options.sdkBaseUrl);
15672
- if (!isInternal) {
15804
+ const isBeaconOrDenied = this.options.captureThirdParty !== true && isThirdPartyBeacon(data.url, this.options.thirdPartyHosts) || matchesDenyUrl(data.url, this.options.denyUrls);
15805
+ const isExcluded = isInternal || isBeaconOrDenied;
15806
+ if (!isExcluded) {
15673
15807
  this.trackFn("$network_request", data);
15674
15808
  }
15675
- if (data.type === "network") {
15809
+ if (data.type === "network" && !isBeaconOrDenied) {
15676
15810
  reportNetworkRequest({
15677
15811
  method: data.method,
15678
15812
  url: data.url,
@@ -15688,7 +15822,7 @@ var NetworkFeature = class {
15688
15822
  requestBody: data.requestBody,
15689
15823
  responseBody: data.responseBody
15690
15824
  });
15691
- if (!isInternal) {
15825
+ if (!isExcluded) {
15692
15826
  const sessionManager = SessionManager.getExistingInstance();
15693
15827
  if (sessionManager) {
15694
15828
  const backendTransport = sessionManager.getBackendTransport();
@@ -15719,6 +15853,13 @@ var NetworkFeature = class {
15719
15853
  return this.active;
15720
15854
  }
15721
15855
  };
15856
+ function matchesDenyUrl(url, patterns) {
15857
+ if (!patterns || !patterns.length || !url) return false;
15858
+ for (const p of patterns) {
15859
+ if (typeof p === "string" ? url.includes(p) : p.test(url)) return true;
15860
+ }
15861
+ return false;
15862
+ }
15722
15863
  function isSdkInternalUrl(url, sdkBaseUrl) {
15723
15864
  if (!url) return false;
15724
15865
  try {
@@ -17021,22 +17162,22 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
17021
17162
  * lose the snapshot entirely. `imageData.length` is the base64 length, which is within ~1% of the
17022
17163
  * serialized cost and needs no extra pass.
17023
17164
  */
17024
- static _selectFramesToPersist(pending4) {
17165
+ static _selectFramesToPersist(pending5) {
17025
17166
  const isKeyframe = (b) => b.frame.frameType !== "delta";
17026
17167
  const size = (b) => b.imageData?.length ?? 0;
17027
17168
  const keep = [];
17028
17169
  let budget = _BackendSessionAdapter.MAX_PERSIST_BYTES;
17029
17170
  const maxCount = _BackendSessionAdapter.MAX_FRAMES_PER_BATCH;
17030
- for (let i = pending4.length - 1; i >= 0; i--) {
17031
- const b = pending4[i];
17171
+ for (let i = pending5.length - 1; i >= 0; i--) {
17172
+ const b = pending5[i];
17032
17173
  if (!isKeyframe(b)) continue;
17033
17174
  const cost = size(b);
17034
17175
  if (cost > budget || keep.length >= maxCount) continue;
17035
17176
  budget -= cost;
17036
17177
  keep.push(b);
17037
17178
  }
17038
- for (let i = pending4.length - 1; i >= 0; i--) {
17039
- const b = pending4[i];
17179
+ for (let i = pending5.length - 1; i >= 0; i--) {
17180
+ const b = pending5[i];
17040
17181
  if (isKeyframe(b)) continue;
17041
17182
  const cost = size(b);
17042
17183
  if (cost > budget || keep.length >= maxCount) continue;
@@ -17469,6 +17610,10 @@ var SDKBootstrapper = class {
17469
17610
  return null;
17470
17611
  }
17471
17612
  this.config = result.data;
17613
+ const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
17614
+ if (typeof requestedApiBaseUrl === "string" && requestedApiBaseUrl && requestedApiBaseUrl !== DEFAULT_API_BASE_URL) {
17615
+ logger.warn(`[ScaleBun] Ignoring apiBaseUrl '${requestedApiBaseUrl}' \u2014 the endpoint is locked at build time and this build targets ${DEFAULT_API_BASE_URL}. To target another backend, change DEFAULT_API_BASE_URL in the SDK source and rebuild.`);
17616
+ }
17472
17617
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
17473
17618
  if (this.config.logLevel) {
17474
17619
  logger.setLevelByName(this.config.logLevel);
@@ -17633,6 +17778,20 @@ var SDKBootstrapper = class {
17633
17778
  }));
17634
17779
  }
17635
17780
  await this.featureRegistry.initializeAll(featureContext);
17781
+ if (this.config.autoInstrumentScrollViews === true) {
17782
+ try {
17783
+ const {
17784
+ autoInstrumentScrollViews: autoInstrumentScrollViews2
17785
+ } = (init_autoInstrumentScroll(), __toCommonJS(autoInstrumentScroll_exports));
17786
+ const outcome = autoInstrumentScrollViews2();
17787
+ if (!outcome.installed) {
17788
+ logger.warn("[Bootstrap] autoInstrumentScrollViews was requested but could not be installed: " + outcome.reason + " \u2014 taps will carry no content depth.");
17789
+ } else {
17790
+ __DEV__ && logger.debug("[Bootstrap] ScrollView auto-instrumentation installed");
17791
+ }
17792
+ } catch {
17793
+ }
17794
+ }
17636
17795
  if (features.replay !== false) {
17637
17796
  try {
17638
17797
  __DEV__ && logger.debug("[Bootstrap] Initializing unified SessionManager");
@@ -20276,14 +20435,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
20276
20435
  this._apiBaseUrl = result.config.apiBaseUrl;
20277
20436
  this._maybeStartEventTracker(rawConfig);
20278
20437
  if (this._pendingIdentity) {
20279
- const pending4 = this._pendingIdentity;
20438
+ const pending5 = this._pendingIdentity;
20280
20439
  this._pendingIdentity = null;
20281
- this.identify(pending4.userId, pending4.traits);
20440
+ this.identify(pending5.userId, pending5.traits);
20282
20441
  }
20283
20442
  const realQueue = coreContainer.get("PersistentQueue");
20284
20443
  const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
20285
20444
  if (buffered.length > 0) {
20286
- __DEV__ && logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
20445
+ logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
20287
20446
  for (const envelope of buffered) {
20288
20447
  realQueue.add(envelope);
20289
20448
  }
@@ -20311,17 +20470,11 @@ var ScaleBunFacade = class _ScaleBunFacade {
20311
20470
  /**
20312
20471
  * Boot the OTA orchestrator when the init config asks for it.
20313
20472
  *
20314
- * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
20315
- * enforcement is reachable from configuration alone. The list form exists
20316
- * for key ROTATION: a build pinning [old, new] keeps verifying while the
20317
- * server moves to the new key, so replacing a key never needs an emergency
20318
- * store release. Both fields merge (deduplicated) into one pinned set.
20319
- *
20320
- * Verification comes from `ota.verifySignature` when supplied, else the
20321
- * built-in @noble-based verifier (optional peers). Pinning keys with
20322
- * NEITHER available is fail-CLOSED by design (signature.ts) — an update
20323
- * that cannot be verified is not installed. We say that out loud rather
20324
- * than letting the app discover it as a silent no-update condition.
20473
+ * Signature enforcement is no longer configured from JS. Signed releases are
20474
+ * verified and staged in NATIVE code against RSA keys embedded in the app
20475
+ * package (SB-OTA-RSA-SHA256-V1); unsigned releases keep working on SHA-256
20476
+ * integrity. There are no signing-key or `verifySignature` options to pass
20477
+ * through the host embeds its trusted keys natively.
20325
20478
  */
20326
20479
  _maybeStartOta(rawConfig) {
20327
20480
  const ota = rawConfig?.ota;
@@ -20330,29 +20483,8 @@ var ScaleBunFacade = class _ScaleBunFacade {
20330
20483
  const {
20331
20484
  otaOrchestrator: otaOrchestrator2
20332
20485
  } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
20333
- const rawSingle = ota.publicSigningKey;
20334
- const rawList = ota.publicSigningKeys;
20335
- const publicKeys = Array.from(new Set([...typeof rawSingle === "string" && rawSingle ? [rawSingle] : [], ...Array.isArray(rawList) ? rawList : []].filter((k) => typeof k === "string" && k.trim().length > 0)));
20336
- const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
20337
- const verifier = ota.verifySignature;
20338
- if (signingRequested && typeof verifier !== "function") {
20339
- const {
20340
- getBuiltinVerifier: getBuiltinVerifier2
20341
- } = (init_builtinVerifier(), __toCommonJS(builtinVerifier_exports));
20342
- if (!getBuiltinVerifier2()) {
20343
- 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`.");
20344
- }
20345
- }
20346
20486
  const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
20347
20487
  otaOrchestrator2.init({
20348
- ...signingRequested ? {
20349
- signature: {
20350
- // Single key stays a plain string — the shape every
20351
- // existing consumer and test already handles.
20352
- publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
20353
- verifier
20354
- }
20355
- } : {},
20356
20488
  ...healthyAfterMs !== void 0 ? {
20357
20489
  healthyAfterMs
20358
20490
  } : {}
@@ -20401,7 +20533,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
20401
20533
  }
20402
20534
  const clientKey = this._clientKey;
20403
20535
  const apiUrl = this._apiBaseUrl;
20404
- if (!clientKey || !apiUrl) return;
20536
+ if (!clientKey || !apiUrl) {
20537
+ return;
20538
+ }
20405
20539
  this._otaCheckInFlight = true;
20406
20540
  try {
20407
20541
  const info = await bridgeAdapter.getDeviceInfo();
@@ -20452,7 +20586,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20452
20586
  if (!dbgConfig) return;
20453
20587
  dbgConfig.__skipDeprecationWarning = true;
20454
20588
  this.enableDebug(dbgConfig);
20455
- __DEV__ && logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
20589
+ logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
20456
20590
  } catch (err) {
20457
20591
  logger.warn("[ScaleBun] Auto debug connection failed:", err?.message);
20458
20592
  }
@@ -20466,7 +20600,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20466
20600
  const apiBaseUrl = this._apiBaseUrl;
20467
20601
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
20468
20602
  if (enabled && !appId) {
20469
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
20603
+ logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
20470
20604
  }
20471
20605
  return;
20472
20606
  }
@@ -20516,7 +20650,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20516
20650
  onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue)
20517
20651
  });
20518
20652
  this._eventTracker.start();
20519
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
20653
+ logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
20520
20654
  this._applyNativeDeviceContext(this._eventTracker);
20521
20655
  this._captureInstallReferrer(this._eventTracker);
20522
20656
  const tracker = this._eventTracker;
@@ -21150,7 +21284,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21150
21284
  const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
21151
21285
  const messages = cfg?.inAppMessages ?? [];
21152
21286
  const previews = messages.filter((m) => m.preview === true || m.isTest === true);
21153
- __DEV__ && logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
21287
+ logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
21154
21288
  return messages;
21155
21289
  },
21156
21290
  /** Upload a survey/NPS/feedback response (idempotent, rides the outbox). */
@@ -21426,7 +21560,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21426
21560
  logger.warn("SDK not initialized. Event dropped:", name);
21427
21561
  return;
21428
21562
  }
21429
- __DEV__ && logger.debug("Tracking event:", name);
21563
+ logger.debug("Tracking event:", name);
21430
21564
  engageSignals.emit(name, payload);
21431
21565
  const adapter = SessionManager.getExistingInstance()?.getBackendTransport();
21432
21566
  if (!isUploadedSessionEvent(name)) {
@@ -21555,7 +21689,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21555
21689
  await Promise.all([this._eventTracker?.flush() ?? Promise.resolve(), adapter?.flushAnalytics() ?? Promise.resolve()]);
21556
21690
  return;
21557
21691
  }
21558
- __DEV__ && logger.debug("Flushing events from persistent queue...");
21692
+ logger.debug("Flushing events from persistent queue...");
21559
21693
  const httpClient = coreContainer.get("HttpClient");
21560
21694
  await flushQueue(this._getQueue(), httpClient, getDeviceId());
21561
21695
  });
@@ -21577,9 +21711,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
21577
21711
  const bootstrapPerfFeature = getPerformanceFeature();
21578
21712
  if (bootstrapPerfFeature) {
21579
21713
  this._performanceFeature = bootstrapPerfFeature;
21580
- __DEV__ && logger.debug("Performance feature linked from bootstrap");
21714
+ logger.debug("Performance feature linked from bootstrap");
21581
21715
  }
21582
- __DEV__ && logger.info("Debug enabled");
21716
+ logger.info("Debug enabled");
21583
21717
  });
21584
21718
  }
21585
21719
  /** Disable the debug subsystem and clean up */
@@ -21722,14 +21856,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
21722
21856
  return false;
21723
21857
  }
21724
21858
  sm.startSession(metadata);
21725
- __DEV__ && logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
21859
+ logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
21726
21860
  return true;
21727
21861
  },
21728
21862
  end: (reason) => {
21729
21863
  const sm = SessionManager.getExistingInstance();
21730
21864
  if (!sm || !sm.isActive) return false;
21731
21865
  sm.endSession(reason);
21732
- __DEV__ && logger.info("session.end() \u2705");
21866
+ logger.info("session.end() \u2705");
21733
21867
  return true;
21734
21868
  },
21735
21869
  emitEvent: (type, data) => {
@@ -21782,7 +21916,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21782
21916
  ...this._privacyConfig,
21783
21917
  ...config
21784
21918
  };
21785
- __DEV__ && logger.debug("[report] Privacy config updated");
21919
+ logger.debug("[report] Privacy config updated");
21786
21920
  },
21787
21921
  /** Set branding for exported reports. */
21788
21922
  setBranding: (config) => {
@@ -21790,7 +21924,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21790
21924
  ...this._brandingConfig,
21791
21925
  ...config
21792
21926
  };
21793
- __DEV__ && logger.debug("[report] Branding config updated");
21927
+ logger.debug("[report] Branding config updated");
21794
21928
  },
21795
21929
  /** Get current privacy config. */
21796
21930
  getPrivacy: () => ({
@@ -21837,9 +21971,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
21837
21971
  AutoScreenDetector: AutoScreenDetector2
21838
21972
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
21839
21973
  AutoScreenDetector2.getInstance().setNavigationRef(ref);
21840
- __DEV__ && logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
21974
+ logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
21841
21975
  } catch (err) {
21842
- __DEV__ && logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
21976
+ logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
21843
21977
  }
21844
21978
  });
21845
21979
  }
@@ -22146,8 +22280,8 @@ function installEngageTriggerEngine(deps) {
22146
22280
  if (!cachedConfig) return;
22147
22281
  configReady = true;
22148
22282
  const now2 = Date.now();
22149
- const pending4 = recent.splice(0);
22150
- for (const e of pending4) {
22283
+ const pending5 = recent.splice(0);
22284
+ for (const e of pending5) {
22151
22285
  const age = now2 - e.at;
22152
22286
  if (age > RECENT_TTL_MS) continue;
22153
22287
  const remaining = delayMsForTrigger(cachedConfig, e.options.trigger) - age;
@@ -23412,11 +23546,11 @@ var keyOf = (value) => value.trim();
23412
23546
  var validRect = (rect) => [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
23413
23547
  function createEngageAnchorRegistry() {
23414
23548
  const entries = /* @__PURE__ */ new Map();
23415
- const listeners2 = /* @__PURE__ */ new Map();
23549
+ const listeners3 = /* @__PURE__ */ new Map();
23416
23550
  const notify = (rawKey) => {
23417
23551
  const key = keyOf(rawKey);
23418
23552
  if (!key) return;
23419
- listeners2.get(key)?.forEach((listener) => listener());
23553
+ listeners3.get(key)?.forEach((listener) => listener());
23420
23554
  };
23421
23555
  return {
23422
23556
  register(rawKey, node) {
@@ -23458,12 +23592,12 @@ function createEngageAnchorRegistry() {
23458
23592
  const key = keyOf(rawKey);
23459
23593
  if (!key) return () => {
23460
23594
  };
23461
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
23595
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
23462
23596
  set.add(listener);
23463
- listeners2.set(key, set);
23597
+ listeners3.set(key, set);
23464
23598
  return () => {
23465
23599
  set.delete(listener);
23466
- if (set.size === 0) listeners2.delete(key);
23600
+ if (set.size === 0) listeners3.delete(key);
23467
23601
  };
23468
23602
  },
23469
23603
  notify
@@ -24310,11 +24444,11 @@ var normalizedWheelValue = (value) => value.toLowerCase().replace(/\s+/g, " ").t
24310
24444
  function resolveWheelOutcome(segments, source) {
24311
24445
  const identity = wheelOutcomeIdentity(source);
24312
24446
  const index = findWheelOutcomeIndex(segments, identity);
24313
- const resolved3 = index >= 0;
24447
+ const resolved = index >= 0;
24314
24448
  return {
24315
- index: resolved3 ? index : 0,
24316
- label: identity.resultLabel || (resolved3 ? segments[index]?.fullLabel ?? "" : ""),
24317
- resolved: resolved3
24449
+ index: resolved ? index : 0,
24450
+ label: identity.resultLabel || (resolved ? segments[index]?.fullLabel ?? "" : ""),
24451
+ resolved
24318
24452
  };
24319
24453
  }
24320
24454
  function SpinWheelInApp(props) {
@@ -32572,10 +32706,10 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
32572
32706
  var keyOf2 = (value) => value.trim();
32573
32707
  function createEngageInlinePlacementRegistry() {
32574
32708
  const slots = /* @__PURE__ */ new Map();
32575
- const pending4 = /* @__PURE__ */ new Map();
32576
- const listeners2 = /* @__PURE__ */ new Map();
32709
+ const pending5 = /* @__PURE__ */ new Map();
32710
+ const listeners3 = /* @__PURE__ */ new Map();
32577
32711
  const notify = (key) => {
32578
- listeners2.get(key)?.forEach((listener) => listener());
32712
+ listeners3.get(key)?.forEach((listener) => listener());
32579
32713
  };
32580
32714
  return {
32581
32715
  register(rawKey, sink) {
@@ -32592,7 +32726,7 @@ function createEngageInlinePlacementRegistry() {
32592
32726
  sink,
32593
32727
  token
32594
32728
  });
32595
- sink(pending4.get(key) ?? null);
32729
+ sink(pending5.get(key) ?? null);
32596
32730
  if (!previous) notify(key);
32597
32731
  return () => {
32598
32732
  if (slots.get(key)?.token !== token) return;
@@ -32603,7 +32737,7 @@ function createEngageInlinePlacementRegistry() {
32603
32737
  deliver(rawKey, delivery) {
32604
32738
  const key = keyOf2(rawKey);
32605
32739
  if (!key) return false;
32606
- pending4.set(key, delivery);
32740
+ pending5.set(key, delivery);
32607
32741
  const slot = slots.get(key);
32608
32742
  slot?.sink(delivery);
32609
32743
  return slot !== void 0;
@@ -32611,9 +32745,9 @@ function createEngageInlinePlacementRegistry() {
32611
32745
  clear(rawKey, messageId) {
32612
32746
  const key = keyOf2(rawKey);
32613
32747
  if (!key) return;
32614
- const current = pending4.get(key);
32748
+ const current = pending5.get(key);
32615
32749
  if (!current || messageId !== void 0 && current.message?.id !== messageId) return;
32616
- pending4.delete(key);
32750
+ pending5.delete(key);
32617
32751
  slots.get(key)?.sink(null);
32618
32752
  },
32619
32753
  has(rawKey) {
@@ -32624,12 +32758,12 @@ function createEngageInlinePlacementRegistry() {
32624
32758
  const key = keyOf2(rawKey);
32625
32759
  if (!key) return () => {
32626
32760
  };
32627
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
32761
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
32628
32762
  set.add(listener);
32629
- listeners2.set(key, set);
32763
+ listeners3.set(key, set);
32630
32764
  return () => {
32631
32765
  set.delete(listener);
32632
- if (set.size === 0) listeners2.delete(key);
32766
+ if (set.size === 0) listeners3.delete(key);
32633
32767
  };
32634
32768
  }
32635
32769
  };
@@ -33173,8 +33307,8 @@ function EngagePromptProvider({
33173
33307
  __DEV__ && logger.debug("[Engage] no eligible in-app message to render");
33174
33308
  return false;
33175
33309
  }
33176
- const resolved3 = selectInAppVariant(message, getDeviceId());
33177
- activeInAppVariantRef.current = message.experiment?.enabled ? resolved3 : null;
33310
+ const resolved = selectInAppVariant(message, getDeviceId());
33311
+ activeInAppVariantRef.current = message.experiment?.enabled ? resolved : null;
33178
33312
  store.recordImpression(message.id, Date.now(), message.revision);
33179
33313
  setActiveInApp(message);
33180
33314
  return true;
@@ -33418,6 +33552,7 @@ var import_react_native43 = require("react-native");
33418
33552
  // lib/module/features/journey/touchTarget.js
33419
33553
  var MAX_DEPTH = 12;
33420
33554
  var MAX_LEN = 60;
33555
+ var MAX_PATH_SEGMENTS = 4;
33421
33556
  function str2(v) {
33422
33557
  if (typeof v !== "string") return void 0;
33423
33558
  const s = v.trim();
@@ -33449,41 +33584,136 @@ function fiberOf(event) {
33449
33584
  const e = event;
33450
33585
  const direct = e._targetInst;
33451
33586
  if (direct && typeof direct === "object") return direct;
33452
- const target = e.target;
33453
- if (target && typeof target === "object") {
33454
- const handle = target._internalFiberInstanceHandleDEV ?? target._internalInstanceHandle;
33587
+ const target = e.target ?? e.nativeEvent?.target;
33588
+ if (target) {
33589
+ const handle = target._internalInstanceHandle ?? target._targetInst;
33455
33590
  if (handle && typeof handle === "object") return handle;
33456
33591
  }
33457
33592
  return null;
33458
33593
  }
33459
- function resolveTouchTarget(event, maxDepth = MAX_DEPTH) {
33594
+ function resolveTouchTarget(event, optsOrMaxDepth = {}) {
33595
+ const opts = typeof optsOrMaxDepth === "number" ? {
33596
+ maxDepth: optsOrMaxDepth
33597
+ } : optsOrMaxDepth;
33598
+ const maxDepth = opts.maxDepth ?? MAX_DEPTH;
33460
33599
  let f = fiberOf(event);
33461
33600
  if (!f) return null;
33462
33601
  const out = {};
33602
+ const segments = [];
33603
+ const ancestors = [];
33463
33604
  let depth = 0;
33464
33605
  try {
33465
33606
  while (f && depth < maxDepth) {
33466
33607
  const props = f.memoizedProps;
33467
33608
  if (props) {
33468
- out.accessibilityLabel ??= str2(props.accessibilityLabel);
33609
+ const beforeTestId = out.testID;
33469
33610
  out.testID ??= str2(props.testID);
33470
- out.text ??= textChild(props);
33611
+ if (!beforeTestId && out.testID) out.node = f.stateNode;
33612
+ if (typeof props.cellKey === "string" && props.cellKey) {
33613
+ out.itemKey ??= str2(props.cellKey);
33614
+ if (typeof props.index === "number" && Number.isFinite(props.index)) {
33615
+ out.itemIndex ??= props.index;
33616
+ }
33617
+ }
33618
+ if (opts.includeText) {
33619
+ out.accessibilityLabel ??= str2(props.accessibilityLabel);
33620
+ out.text ??= textChild(props);
33621
+ }
33471
33622
  }
33472
- out.component ??= componentName(f);
33473
- if (out.accessibilityLabel && out.testID) break;
33623
+ if (f.stateNode) ancestors.push(f.stateNode);
33624
+ const name = componentName(f);
33625
+ if (name) {
33626
+ out.component ??= name;
33627
+ if (segments[segments.length - 1] !== name && segments.length < MAX_PATH_SEGMENTS) {
33628
+ segments.push(name);
33629
+ }
33630
+ }
33631
+ if (out.testID && segments.length >= MAX_PATH_SEGMENTS) break;
33474
33632
  f = f.return ?? null;
33475
33633
  depth++;
33476
33634
  }
33477
33635
  } catch {
33478
33636
  }
33479
- return out.accessibilityLabel || out.testID || out.text || out.component ? out : null;
33637
+ if (segments.length) out.path = segments.join("<");
33638
+ if (!out.node) out.node = fiberOf(event)?.stateNode;
33639
+ if (ancestors.length) out.ancestors = ancestors;
33640
+ return out.testID || out.path || out.component || out.accessibilityLabel || out.text || out.itemKey ? out : null;
33480
33641
  }
33481
33642
  function describeTouchTarget(t) {
33482
33643
  if (!t) return void 0;
33483
- return t.accessibilityLabel || t.testID || t.text || t.component;
33644
+ return t.testID || t.path || t.component;
33645
+ }
33646
+ function targetIdentity(t) {
33647
+ if (!t) return {
33648
+ source: "none"
33649
+ };
33650
+ if (t.testID) return {
33651
+ id: t.testID,
33652
+ source: "testID"
33653
+ };
33654
+ if (t.path) return {
33655
+ id: t.path,
33656
+ source: "path"
33657
+ };
33658
+ if (t.component) return {
33659
+ id: t.component,
33660
+ source: "component"
33661
+ };
33662
+ return {
33663
+ source: "none"
33664
+ };
33665
+ }
33666
+
33667
+ // lib/module/features/journey/targetGeometry.js
33668
+ function readRectSync(node) {
33669
+ if (!node || typeof node !== "object") return null;
33670
+ const n = node;
33671
+ const fn = typeof n.unstable_getBoundingClientRect === "function" ? n.unstable_getBoundingClientRect : typeof n.getBoundingClientRect === "function" ? n.getBoundingClientRect : null;
33672
+ if (!fn) return null;
33673
+ try {
33674
+ const r = fn.call(n);
33675
+ if (!r) return null;
33676
+ const {
33677
+ x,
33678
+ y,
33679
+ width,
33680
+ height
33681
+ } = r;
33682
+ if (![x, y, width, height].every((v) => typeof v === "number" && Number.isFinite(v))) return null;
33683
+ if (width <= 0 || height <= 0) return null;
33684
+ return {
33685
+ x,
33686
+ y,
33687
+ width,
33688
+ height
33689
+ };
33690
+ } catch {
33691
+ return null;
33692
+ }
33693
+ }
33694
+ function normalizeRect(rect, base) {
33695
+ if (!rect || !base || base.w <= 0 || base.h <= 0) return null;
33696
+ const out = {
33697
+ x: rect.x / base.w,
33698
+ y: rect.y / base.h,
33699
+ width: rect.width / base.w,
33700
+ height: rect.height / base.h
33701
+ };
33702
+ if (![out.x, out.y, out.width, out.height].every((v) => Number.isFinite(v))) return null;
33703
+ return out;
33704
+ }
33705
+ function localPoint(rect, nx, ny) {
33706
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
33707
+ if (typeof nx !== "number" || typeof ny !== "number") return null;
33708
+ if (!Number.isFinite(nx) || !Number.isFinite(ny)) return null;
33709
+ return {
33710
+ u: (nx - rect.x) / rect.width,
33711
+ v: (ny - rect.y) / rect.height
33712
+ };
33484
33713
  }
33485
33714
 
33486
33715
  // lib/module/features/journey/ScaleBunDebugRoot.js
33716
+ init_scrollContext();
33487
33717
  init_device();
33488
33718
  init_interactionProtocol();
33489
33719
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -33495,6 +33725,7 @@ function getBootstrap() {
33495
33725
  return _bootstrap;
33496
33726
  }
33497
33727
  var JourneyContext = /* @__PURE__ */ (0, import_react9.createContext)(null);
33728
+ var UI_STATE_CAPTURE_MIN_INTERVAL_MS = 1e3;
33498
33729
  function ScaleBunDebugRoot({
33499
33730
  children,
33500
33731
  navigationRef,
@@ -33673,6 +33904,28 @@ function ScaleBunDebugRoot({
33673
33904
  clearTimeout(timer);
33674
33905
  };
33675
33906
  }, [navigationRef]);
33907
+ (0, import_react9.useEffect)(() => {
33908
+ let lastCaptureTs = 0;
33909
+ const {
33910
+ onUiStateChange: onUiStateChange2
33911
+ } = (init_uiState(), __toCommonJS(uiState_exports));
33912
+ const unsubscribe = onUiStateChange2(() => {
33913
+ const now2 = Date.now();
33914
+ if (now2 - lastCaptureTs < UI_STATE_CAPTURE_MIN_INTERVAL_MS) return;
33915
+ lastCaptureTs = now2;
33916
+ try {
33917
+ const {
33918
+ SessionManager: SessionManager2
33919
+ } = (init_SessionManager(), __toCommonJS(SessionManager_exports));
33920
+ const sm = SessionManager2.getExistingInstance();
33921
+ if (sm && typeof sm.captureManualFrame === "function") {
33922
+ void sm.captureManualFrame();
33923
+ }
33924
+ } catch {
33925
+ }
33926
+ });
33927
+ return unsubscribe;
33928
+ }, []);
33676
33929
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
33677
33930
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
33678
33931
  const touchStartRef = (0, import_react9.useRef)(null);
@@ -33689,10 +33942,15 @@ function ScaleBunDebugRoot({
33689
33942
  lastNativeTouchTsRef.current = Date.now();
33690
33943
  nativeTrackingConfirmedRef.current = true;
33691
33944
  const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
33692
- const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
33945
+ pruneInteractionStarts(interactionStartsRef.current, nativeOccurredAt);
33946
+ const correlation = correlateInteraction(interactionStartsRef.current, nativeOccurredAt);
33947
+ const start = correlation.start;
33948
+ if (correlation.index >= 0) {
33949
+ interactionStartsRef.current.splice(correlation.index, 1);
33950
+ }
33693
33951
  if (start) {
33694
- const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
33695
- if (pending4 !== void 0) clearTimeout(pending4);
33952
+ const pending5 = pendingJsEmitsRef.current.get(start.interactionId);
33953
+ if (pending5 !== void 0) clearTimeout(pending5);
33696
33954
  pendingJsEmitsRef.current.delete(start.interactionId);
33697
33955
  }
33698
33956
  const {
@@ -33715,11 +33973,62 @@ function ScaleBunDebugRoot({
33715
33973
  interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
33716
33974
  interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
33717
33975
  occurredAt: nativeOccurredAt,
33976
+ /**
33977
+ * RN-6 — THESE FIVE FIELDS ARE THE ONLY ONES THE JOIN CARRIES, AND
33978
+ * THEREFORE THE ONLY ONES A BAD JOIN CAN FALSIFY.
33979
+ *
33980
+ * Coordinates below come from native and are right regardless. So a
33981
+ * mispairing does not produce visibly broken data: it produces heat
33982
+ * in the correct position attributed to the wrong control, in the
33983
+ * wrong UI state, on the wrong screen. Every field is individually
33984
+ * well-formed and nothing downstream can tell.
33985
+ *
33986
+ * When the correlation is not 'exact' they are left UNDEFINED rather
33987
+ * than filled from a best guess. An interaction with no target is
33988
+ * honest and visibly incomplete — the accounting lane can count it as
33989
+ * unplaceable and say why. An interaction with the WRONG target is
33990
+ * indistinguishable from a correct one and silently corrupts every
33991
+ * aggregate it enters. `null` is not `0`, and a guess is not a
33992
+ * measurement.
33993
+ */
33718
33994
  ui: start?.ui,
33719
33995
  stateStatus: start?.stateStatus ?? "not_captured",
33720
33996
  target: start?.target,
33721
33997
  targetId: start?.targetId,
33998
+ targetSource: start?.targetSource,
33722
33999
  screenName: start?.screenName,
34000
+ screenSource: start?.screenSource ?? "none",
34001
+ targetRect: start?.targetRect,
34002
+ /**
34003
+ * RN-15 — WHERE IN THE CONTROL, computed where both halves exist: the
34004
+ * point comes from the native event and the rect was measured at
34005
+ * finger-down, in the same capture base. Pairing them here rather than
34006
+ * downstream stops anything later reaching for a different rect.
34007
+ */
34008
+ ...(() => {
34009
+ const lp = localPoint(start?.targetRect ?? null, event.normalizedX, event.normalizedY);
34010
+ return lp ? {
34011
+ localU: lp.u,
34012
+ localV: lp.v
34013
+ } : {};
34014
+ })(),
34015
+ scrollX: start?.scrollX,
34016
+ scrollY: start?.scrollY,
34017
+ scrollSource: start?.scrollSource ?? "none",
34018
+ itemKey: start?.itemKey,
34019
+ itemIndex: start?.itemIndex,
34020
+ /**
34021
+ * The join's own verdict, shipped with the row so it is auditable
34022
+ * rather than a claim made in a comment. 'ambiguous' and 'none' are
34023
+ * deliberately distinguished: 'none' means the JS handler never fired
34024
+ * (a real capture gap on this platform), 'ambiguous' means it fired
34025
+ * more than once too close together to attribute. Those have
34026
+ * different fixes, and collapsing them would hide which one is
34027
+ * actually happening in production.
34028
+ */
34029
+ correlation: correlation.outcome,
34030
+ correlationDeltaMs: correlation.deltaMs,
34031
+ correlationRunnerUpDeltaMs: correlation.runnerUpDeltaMs,
33723
34032
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33724
34033
  x: event.rawX,
33725
34034
  y: event.rawY,
@@ -33791,6 +34100,7 @@ function ScaleBunDebugRoot({
33791
34100
  const target = resolveTouchTarget(e);
33792
34101
  let ui;
33793
34102
  let screenName;
34103
+ let screenSource = "none";
33794
34104
  try {
33795
34105
  const {
33796
34106
  uiStateSignature: uiStateSignature2
@@ -33802,7 +34112,9 @@ function ScaleBunDebugRoot({
33802
34112
  const {
33803
34113
  AutoScreenDetector: AutoScreenDetector2
33804
34114
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33805
- screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
34115
+ const detector = AutoScreenDetector2.getInstance();
34116
+ screenName = detector.getCurrentScreen() || void 0;
34117
+ screenSource = screenName ? detector.getCurrentScreenSource() : "none";
33806
34118
  } catch {
33807
34119
  }
33808
34120
  const start = {
@@ -33812,9 +34124,91 @@ function ScaleBunDebugRoot({
33812
34124
  y: touch.pageY,
33813
34125
  locationX: touch.locationX,
33814
34126
  locationY: touch.locationY,
34127
+ /**
34128
+ * RN-1 + RN-2 — the identity that is persisted, and it is now code-derived.
34129
+ *
34130
+ * `describeTouchTarget` returns `testID` -> structural path -> component name,
34131
+ * and never a caption or an accessibilityLabel. Before RN-2 it preferred free
34132
+ * text, so this line was writing user data ("Pay EGP 1,442", an order row
34133
+ * labelled with an email address, a contact's name) into the analytics store —
34134
+ * and, because `target` is an aggregation key, splitting one button into one
34135
+ * bucket per person at the same time.
34136
+ *
34137
+ * `targetId` used to be `target?.testID` alone, which is `undefined` in the
34138
+ * overwhelming majority of apps — no testIDs authored — so the field the
34139
+ * backend keys on was empty almost always. It now falls back to the structural
34140
+ * path, with `targetSource` recording WHICH it got.
34141
+ */
33815
34142
  target: describeTouchTarget(target),
33816
- targetId: target?.testID,
34143
+ targetId: targetIdentity(target).id,
34144
+ /**
34145
+ * WHICH KIND of identity this is, because they are not equally trustworthy: a
34146
+ * `testID` is a promise from the app's authors that this control is this
34147
+ * control; a `path` is an inference that breaks when the tree is refactored.
34148
+ * Without this the accounting lane cannot distinguish "the control had no
34149
+ * identity" from "the identity is a structural guess", and both would render
34150
+ * as an equally confident target.
34151
+ */
34152
+ targetSource: targetIdentity(target).source,
34153
+ /* RN-4 — row identity from the app's own keyExtractor; see touchTarget.ts. */
34154
+ itemKey: target?.itemKey,
34155
+ itemIndex: target?.itemIndex,
34156
+ /**
34157
+ * RN-13 — the control's rectangle, so `heat ∈ target` is decidable.
34158
+ *
34159
+ * Read SYNCHRONOUSLY here, in the same tick as the touch, and normalized against
34160
+ * the SAME measured root the tap's own coordinates use. Both halves matter:
34161
+ *
34162
+ * - A callback measurement (`measureInWindow`) would resolve after this handler
34163
+ * returns, so the rect attached to a tap could be the previous tap's, with
34164
+ * nothing afterwards able to tell. `targetGeometry` refuses to guess: it reads
34165
+ * now or reports null.
34166
+ * - Normalizing against a window size instead of the root view is off by the
34167
+ * status bar on most devices, which would read as "the heat is just outside
34168
+ * the button" everywhere rather than as a measurement bug.
34169
+ *
34170
+ * `measureRoot()` is called first because the cached root rect is populated
34171
+ * asynchronously at mount; without it the first taps of a session normalize
34172
+ * against nothing and get a null rect for a reason that has nothing to do with
34173
+ * the control.
34174
+ */
34175
+ /**
34176
+ * RN-3 — WHERE IN THE CONTENT, not just where on the glass.
34177
+ *
34178
+ * Two taps at the same screen coordinate one viewport apart are two different
34179
+ * controls. Without an offset they merge into one hotspot, and the heatmap shows
34180
+ * heavy engagement at a position where, in content terms, nothing in particular
34181
+ * is. The offset is also a CACHE dimension — two scroll states of one route must
34182
+ * not share a backdrop — so its absence corrupts the picture as well as the count.
34183
+ *
34184
+ * `ancestors` is innermost-first, so this takes the NEAREST scroller that has a
34185
+ * recorded position: a tap inside a horizontal carousel belongs to the carousel's
34186
+ * content, not to how far the page behind it had scrolled.
34187
+ *
34188
+ * Null when no ancestor has one — never zero. Zero is a real scroll position (the
34189
+ * top of a list), and reporting it for "unknown" would place every unmeasured tap
34190
+ * at the top of its content, indistinguishable from a genuine tap there.
34191
+ */
34192
+ ...(() => {
34193
+ const hit = nearestRecordedOffset((target?.ancestors ?? []).map((n) => nativeTagOf(n)));
34194
+ return hit ? {
34195
+ scrollX: hit.x,
34196
+ scrollY: hit.y,
34197
+ scrollSource: "wrapper"
34198
+ } : {
34199
+ scrollSource: "none"
34200
+ };
34201
+ })(),
34202
+ targetRect: (() => {
34203
+ measureRoot();
34204
+ const root = rootRectRef.current;
34205
+ return normalizeRect(readRectSync(target?.node), root ? {
34206
+ w: root.w,
34207
+ h: root.h
34208
+ } : null) ?? void 0;
34209
+ })(),
33817
34210
  screenName,
34211
+ screenSource,
33818
34212
  ui,
33819
34213
  stateStatus: ui ? "captured_nonempty" : "not_instrumented",
33820
34214
  emitAutomaticAnalytics: captureAutomaticInteractions
@@ -33851,7 +34245,12 @@ function ScaleBunDebugRoot({
33851
34245
  }
33852
34246
  measureRoot();
33853
34247
  const rootRect = rootRectRef.current;
33854
- const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
34248
+ const freshTarget = start ? null : resolveTouchTarget(e);
34249
+ const tapped = start?.target ?? describeTouchTarget(freshTarget);
34250
+ const identity = start ? {
34251
+ id: start.targetId,
34252
+ source: start.targetSource
34253
+ } : targetIdentity(freshTarget);
33855
34254
  const gestureDetails = {
33856
34255
  target: tapped,
33857
34256
  interactionId: start?.interactionId ?? generateInteractionId(),
@@ -33859,8 +34258,16 @@ function ScaleBunDebugRoot({
33859
34258
  occurredAt: start?.occurredAt ?? Date.now(),
33860
34259
  ui: start?.ui,
33861
34260
  stateStatus: start?.stateStatus ?? "not_captured",
33862
- targetId: start?.targetId,
34261
+ targetId: identity.id,
34262
+ targetSource: identity.source,
33863
34263
  screenName: start?.screenName,
34264
+ screenSource: start?.screenSource ?? "none",
34265
+ targetRect: start?.targetRect,
34266
+ scrollX: start?.scrollX,
34267
+ scrollY: start?.scrollY,
34268
+ scrollSource: start?.scrollSource ?? "none",
34269
+ itemKey: start?.itemKey,
34270
+ itemIndex: start?.itemIndex,
33864
34271
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33865
34272
  x: start?.x ?? touch.pageX,
33866
34273
  y: start?.y ?? touch.pageY,
@@ -33876,6 +34283,11 @@ function ScaleBunDebugRoot({
33876
34283
  const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
33877
34284
  gestureDetails.normalizedX = nx;
33878
34285
  gestureDetails.normalizedY = ny;
34286
+ const lp = localPoint(start?.targetRect ?? null, nx, ny);
34287
+ if (lp) {
34288
+ gestureDetails.localU = lp.u;
34289
+ gestureDetails.localV = lp.v;
34290
+ }
33879
34291
  gestureDetails.normalizedPrecomputed = true;
33880
34292
  gestureDetails.viewportWidth = rootRect.w;
33881
34293
  gestureDetails.viewportHeight = rootRect.h;
@@ -34004,6 +34416,13 @@ function useScaleBunScreen(name) {
34004
34416
  }
34005
34417
  } catch {
34006
34418
  }
34419
+ try {
34420
+ const {
34421
+ AutoScreenDetector: AutoScreenDetector2
34422
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
34423
+ AutoScreenDetector2.getInstance().setManualScreen(name);
34424
+ } catch {
34425
+ }
34007
34426
  try {
34008
34427
  const bs = getBootstrap();
34009
34428
  const perfFeature = bs?.getPerformanceFeature?.();
@@ -34154,6 +34573,18 @@ var ScaleBunScrollView = /* @__PURE__ */ import_react12.default.forwardRef(funct
34154
34573
  }
34155
34574
  } catch {
34156
34575
  }
34576
+ try {
34577
+ const {
34578
+ recordScrollOffset: recordScrollOffset2,
34579
+ nativeTagOf: nativeTagOf2
34580
+ } = (init_scrollContext(), __toCommonJS(scrollContext_exports));
34581
+ const {
34582
+ contentOffset,
34583
+ target
34584
+ } = event.nativeEvent;
34585
+ recordScrollOffset2(typeof target === "number" ? target : nativeTagOf2(target), contentOffset.x, contentOffset.y);
34586
+ } catch {
34587
+ }
34157
34588
  if (userOnScroll) {
34158
34589
  userOnScroll(event);
34159
34590
  }