@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
@@ -92,9 +92,9 @@ var init_internalLogger = __esm({
92
92
  }
93
93
  /** Set level from the public string name used in config (e.g. 'warn'). */
94
94
  setLevelByName(name) {
95
- const resolved3 = LEVEL_BY_NAME[name];
96
- if (resolved3 !== void 0) {
97
- this.level = resolved3;
95
+ const resolved = LEVEL_BY_NAME[name];
96
+ if (resolved !== void 0) {
97
+ this.level = resolved;
98
98
  }
99
99
  }
100
100
  /** Current numeric level — exposed for callers that branch on dev verbosity. */
@@ -600,7 +600,7 @@ var SDK_VERSION;
600
600
  var init_version = __esm({
601
601
  "lib/module/core/constants/version.js"() {
602
602
  "use strict";
603
- SDK_VERSION = "1.13.0";
603
+ SDK_VERSION = "2.0.1";
604
604
  }
605
605
  });
606
606
 
@@ -1353,6 +1353,15 @@ var init_AutoScreenDetector = __esm({
1353
1353
  _manualScreen = null;
1354
1354
  _manualScreenTs = 0;
1355
1355
  _autoScreen = null;
1356
+ /**
1357
+ * When `_autoScreen` was last written.
1358
+ *
1359
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
1360
+ * Without a timestamp the only comparison available was "is the manual value still
1361
+ * fresh", which is a different question and gave the wrong answer — see the note on
1362
+ * `_currentScreen()`.
1363
+ */
1364
+ _autoScreenTs = 0;
1356
1365
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
1357
1366
  _autoVisitSig = null;
1358
1367
  _navigationRef = null;
@@ -1420,6 +1429,7 @@ var init_AutoScreenDetector = __esm({
1420
1429
  if (this.isManualScreenActive()) return;
1421
1430
  if (name !== this._autoScreen) {
1422
1431
  this._autoScreen = name;
1432
+ this._autoScreenTs = Date.now();
1423
1433
  this._autoVisitSig = name;
1424
1434
  this._emitScreenChange(name, "heuristic");
1425
1435
  }
@@ -1427,8 +1437,19 @@ var init_AutoScreenDetector = __esm({
1427
1437
  }
1428
1438
  }
1429
1439
  /**
1430
- * Check if a manual screen is currently active.
1431
- * Used by auto-detection to yield to manual overrides.
1440
+ * Should a WRITER defer to the manual screen right now?
1441
+ *
1442
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
1443
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
1444
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
1445
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
1446
+ *
1447
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
1448
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
1449
+ * `setManualScreen` in production, so the effect was latent — but the moment the
1450
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
1451
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
1452
+ * fix that introduced that would have been worse than the gap it closed.
1432
1453
  */
1433
1454
  isManualScreenActive() {
1434
1455
  if (!this._manualScreen) return false;
@@ -1474,6 +1495,7 @@ var init_AutoScreenDetector = __esm({
1474
1495
  const visit = this._extractActiveVisit(state);
1475
1496
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
1476
1497
  this._autoScreen = visit.name;
1498
+ this._autoScreenTs = Date.now();
1477
1499
  this._autoVisitSig = this._visitSig(visit);
1478
1500
  this._emitScreenChange(visit.name, "navRef");
1479
1501
  }
@@ -1492,6 +1514,7 @@ var init_AutoScreenDetector = __esm({
1492
1514
  const visit = this._extractActiveVisit(state);
1493
1515
  if (visit) {
1494
1516
  this._autoScreen = visit.name;
1517
+ this._autoScreenTs = Date.now();
1495
1518
  this._autoVisitSig = this._visitSig(visit);
1496
1519
  this._emitScreenChange(visit.name, "navRef");
1497
1520
  }
@@ -1522,6 +1545,7 @@ var init_AutoScreenDetector = __esm({
1522
1545
  const visit = this._extractActiveVisit(state);
1523
1546
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
1524
1547
  this._autoScreen = visit.name;
1548
+ this._autoScreenTs = Date.now();
1525
1549
  this._autoVisitSig = this._visitSig(visit);
1526
1550
  this._emitScreenChange(visit.name, "heuristic");
1527
1551
  }
@@ -1620,7 +1644,7 @@ var init_AutoScreenDetector = __esm({
1620
1644
  if (!reactNav || reactNav.__scalebunAutoHooked) return;
1621
1645
  const detector = this;
1622
1646
  let installed2 = 0;
1623
- const redefine = (key, value) => {
1647
+ const redefine2 = (key, value) => {
1624
1648
  try {
1625
1649
  Object.defineProperty(reactNav, key, {
1626
1650
  value,
@@ -1670,7 +1694,7 @@ var init_AutoScreenDetector = __esm({
1670
1694
  });
1671
1695
  });
1672
1696
  Wrapped.displayName = "ScaleBunNavigationContainer";
1673
- if (redefine("NavigationContainer", Wrapped)) installed2++;
1697
+ if (redefine2("NavigationContainer", Wrapped)) installed2++;
1674
1698
  }
1675
1699
  if (typeof reactNav.createNavigationContainerRef === "function") {
1676
1700
  const originalCreate = reactNav.createNavigationContainerRef;
@@ -1682,7 +1706,7 @@ var init_AutoScreenDetector = __esm({
1682
1706
  }
1683
1707
  return ref;
1684
1708
  };
1685
- if (redefine("createNavigationContainerRef", patchedCreate)) installed2++;
1709
+ if (redefine2("createNavigationContainerRef", patchedCreate)) installed2++;
1686
1710
  }
1687
1711
  if (typeof reactNav.useNavigationContainerRef === "function") {
1688
1712
  const originalHook = reactNav.useNavigationContainerRef;
@@ -1694,7 +1718,7 @@ var init_AutoScreenDetector = __esm({
1694
1718
  }
1695
1719
  return ref;
1696
1720
  };
1697
- if (redefine("useNavigationContainerRef", patchedHook)) installed2++;
1721
+ if (redefine2("useNavigationContainerRef", patchedHook)) installed2++;
1698
1722
  }
1699
1723
  this._autoHookInstalled = installed2 > 0;
1700
1724
  try {
@@ -1743,15 +1767,58 @@ var init_AutoScreenDetector = __esm({
1743
1767
  this._listeners = [];
1744
1768
  this._manualScreen = null;
1745
1769
  this._autoScreen = null;
1770
+ this._autoScreenTs = 0;
1746
1771
  this._navigationRef = null;
1747
1772
  _instance = null;
1748
1773
  }
1749
1774
  /** Get the current best-known screen name */
1750
1775
  getCurrentScreen() {
1751
- if (this._manualScreen && this.isManualScreenActive()) {
1752
- return this._manualScreen;
1776
+ return this._currentScreen().name;
1777
+ }
1778
+ /**
1779
+ * WHERE the current screen name came from, or 'none'.
1780
+ *
1781
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
1782
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
1783
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
1784
+ * causes: the app never instrumented anything, the app instrumented manually and the
1785
+ * value never reached interactions, or auto-detection ran and failed. Those have
1786
+ * different owners and different fixes, and guessing between them is how a P0 stays
1787
+ * open.
1788
+ */
1789
+ getCurrentScreenSource() {
1790
+ return this._currentScreen().source;
1791
+ }
1792
+ /**
1793
+ * LAST WRITER WINS between the manual beacon and auto-detection.
1794
+ *
1795
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
1796
+ * declares a different one — that is what mounting a screen means. It does not
1797
+ * expire, which is the correction here: the previous rule returned the manual name
1798
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
1799
+ * which in a manually-instrumented app is `null`.
1800
+ *
1801
+ * Comparing timestamps rather than hard-coding a priority also handles the real
1802
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
1803
+ * beacon on one modal, and whichever spoke last is the truth.
1804
+ */
1805
+ _currentScreen() {
1806
+ const manual = this._manualScreen;
1807
+ const auto = this._autoScreen;
1808
+ if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
1809
+ return {
1810
+ name: manual,
1811
+ source: "manual"
1812
+ };
1753
1813
  }
1754
- return this._autoScreen;
1814
+ if (auto) return {
1815
+ name: auto,
1816
+ source: "navigation"
1817
+ };
1818
+ return {
1819
+ name: null,
1820
+ source: "none"
1821
+ };
1755
1822
  }
1756
1823
  // ─── Internal ───────────────────────────────────────────────────────
1757
1824
  _emitScreenChange(name, source) {
@@ -3632,17 +3699,54 @@ var init_automaticEvents = __esm({
3632
3699
  function generateInteractionId() {
3633
3700
  return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3634
3701
  }
3635
- function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
3636
- let best;
3637
- let bestDelta = toleranceMs + 1;
3638
- for (const start of starts) {
3702
+ function correlateInteraction(starts, occurredAt, toleranceMs = CORRELATION_TOLERANCE_MS, marginMs = CORRELATION_MARGIN_MS) {
3703
+ const candidates = [];
3704
+ for (let i = 0; i < starts.length; i++) {
3705
+ const start = starts[i];
3706
+ if (!start) continue;
3639
3707
  const delta = Math.abs(start.occurredAt - occurredAt);
3640
- if (delta < bestDelta) {
3641
- best = start;
3642
- bestDelta = delta;
3708
+ if (delta <= toleranceMs) candidates.push({
3709
+ index: i,
3710
+ delta
3711
+ });
3712
+ }
3713
+ if (candidates.length === 0) {
3714
+ return {
3715
+ index: -1,
3716
+ outcome: "none",
3717
+ deltaMs: null,
3718
+ runnerUpDeltaMs: null
3719
+ };
3720
+ }
3721
+ candidates.sort((a, b) => a.delta - b.delta || a.index - b.index);
3722
+ const best = candidates[0];
3723
+ const runnerUp = candidates[1];
3724
+ if (runnerUp && runnerUp.delta - best.delta < marginMs) {
3725
+ return {
3726
+ index: -1,
3727
+ outcome: "ambiguous",
3728
+ deltaMs: best.delta,
3729
+ runnerUpDeltaMs: runnerUp.delta
3730
+ };
3731
+ }
3732
+ return {
3733
+ start: starts[best.index],
3734
+ index: best.index,
3735
+ outcome: "exact",
3736
+ deltaMs: best.delta,
3737
+ runnerUpDeltaMs: runnerUp ? runnerUp.delta : null
3738
+ };
3739
+ }
3740
+ function pruneInteractionStarts(starts, now2, toleranceMs = CORRELATION_TOLERANCE_MS) {
3741
+ let removed = 0;
3742
+ for (let i = starts.length - 1; i >= 0; i--) {
3743
+ const start = starts[i];
3744
+ if (!start || now2 - start.occurredAt > toleranceMs) {
3745
+ starts.splice(i, 1);
3746
+ removed++;
3643
3747
  }
3644
3748
  }
3645
- return bestDelta <= toleranceMs ? best : void 0;
3749
+ return removed;
3646
3750
  }
3647
3751
  function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3648
3752
  return {
@@ -3660,11 +3764,13 @@ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3660
3764
  canonical_mirror: canonicalMirror || void 0
3661
3765
  };
3662
3766
  }
3663
- var INTERACTION_PROTOCOL_VERSION;
3767
+ var INTERACTION_PROTOCOL_VERSION, CORRELATION_TOLERANCE_MS, CORRELATION_MARGIN_MS;
3664
3768
  var init_interactionProtocol = __esm({
3665
3769
  "lib/module/features/journey/interactionProtocol.js"() {
3666
3770
  "use strict";
3667
3771
  INTERACTION_PROTOCOL_VERSION = 1;
3772
+ CORRELATION_TOLERANCE_MS = 1500;
3773
+ CORRELATION_MARGIN_MS = 100;
3668
3774
  }
3669
3775
  });
3670
3776
 
@@ -4407,6 +4513,69 @@ var init_SessionManager = __esm({
4407
4513
  state_status: stateStatus,
4408
4514
  ui: details?.ui,
4409
4515
  target_id: details?.targetId,
4516
+ /**
4517
+ * RN-6 — shipped with the row so the join is auditable instead of assumed.
4518
+ *
4519
+ * Without it, an interaction with no `target_id` is ambiguous in the worst way:
4520
+ * it could mean the control genuinely had no identity, or that the correlation
4521
+ * refused and the identity exists but could not be attributed. Those are
4522
+ * different defects with different owners, and the accounting lane cannot give
4523
+ * an honest unplaceable REASON without knowing which.
4524
+ *
4525
+ * Undefined on the JS-fallback path (no native event, so nothing was joined),
4526
+ * which is itself the correct answer there rather than a fabricated 'exact'.
4527
+ */
4528
+ correlation: details?.correlation,
4529
+ correlation_delta_ms: details?.correlationDeltaMs,
4530
+ correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
4531
+ /**
4532
+ * RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
4533
+ * identifier from a structural inference. 'path' means the app authored no testID
4534
+ * and the identity was derived from component names, which is useful but will
4535
+ * change under refactoring; a dashboard that presents the two identically invites
4536
+ * someone to trust a key that is about to move.
4537
+ */
4538
+ target_source: details?.targetSource,
4539
+ /**
4540
+ * RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
4541
+ * 'none' means the app instrumented nothing the SDK could see; 'manual' means a
4542
+ * <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
4543
+ * different owners behind one number that today has none.
4544
+ */
4545
+ screen_source: details?.screenSource,
4546
+ /**
4547
+ * RN-13 — what makes "the heat is on this control" checkable instead of assumed.
4548
+ * Absent when the control could not be measured synchronously, which is a fact worth
4549
+ * storing: an unmeasurable control and a mis-placed tap are different findings.
4550
+ */
4551
+ target_rect: details?.targetRect,
4552
+ /**
4553
+ * RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
4554
+ * table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
4555
+ * these need no migration. Mobile simply never sent them.
4556
+ *
4557
+ * `scroll_source` says whether an offset was available at all: 'none' means the app
4558
+ * uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
4559
+ * the integration and not a tap at the top of a list.
4560
+ */
4561
+ scrollX: details?.scrollX,
4562
+ scrollY: details?.scrollY,
4563
+ scroll_source: details?.scrollSource,
4564
+ /**
4565
+ * RN-4 — what stops a recycled native view merging two different rows into one
4566
+ * hotspot. The key is the app's own keyExtractor output, so it is stable across
4567
+ * scrolling and across sessions; the index is carried so a recycled slot (same index,
4568
+ * different key) can be proven rather than inferred.
4569
+ */
4570
+ item_key: details?.itemKey,
4571
+ item_index: details?.itemIndex,
4572
+ /**
4573
+ * RN-15 — where in the control, not just which control. Turns "they tapped Add to
4574
+ * Cart" into "they tapped its left edge", which is what tells you whether a hit area
4575
+ * is right. Outside 0..1 is a hitSlop tap and is kept as such.
4576
+ */
4577
+ local_u: details?.localU,
4578
+ local_v: details?.localV,
4410
4579
  gestureType,
4411
4580
  x: details?.x,
4412
4581
  y: details?.y,
@@ -5498,10 +5667,10 @@ function requestExport(transport, enabled, presetOrOptions) {
5498
5667
  return;
5499
5668
  }
5500
5669
  const timer = setTimeout(() => {
5501
- const pending4 = _pendingExports.get(requestId);
5502
- if (pending4) {
5670
+ const pending5 = _pendingExports.get(requestId);
5671
+ if (pending5) {
5503
5672
  _pendingExports.delete(requestId);
5504
- pending4.resolve({
5673
+ pending5.resolve({
5505
5674
  success: false,
5506
5675
  error: `Export timed out after ${EXPORT_TIMEOUT_MS / 1e3}s`
5507
5676
  });
@@ -5543,14 +5712,14 @@ function handleExportResult(args) {
5543
5712
  try {
5544
5713
  const requestId = args.requestId;
5545
5714
  if (!requestId) return;
5546
- const pending4 = _pendingExports.get(requestId);
5547
- if (!pending4) {
5715
+ const pending5 = _pendingExports.get(requestId);
5716
+ if (!pending5) {
5548
5717
  logger.warn(`[ExportBridge] Received result for unknown requestId: ${requestId}`);
5549
5718
  return;
5550
5719
  }
5551
- clearTimeout(pending4.timer);
5720
+ clearTimeout(pending5.timer);
5552
5721
  _pendingExports.delete(requestId);
5553
- pending4.resolve({
5722
+ pending5.resolve({
5554
5723
  success: args.success ?? false,
5555
5724
  filePath: args.filePath,
5556
5725
  error: args.error,
@@ -5561,9 +5730,9 @@ function handleExportResult(args) {
5561
5730
  }
5562
5731
  }
5563
5732
  function teardownExports() {
5564
- for (const [, pending4] of _pendingExports) {
5565
- clearTimeout(pending4.timer);
5566
- pending4.reject(new Error("Debug disabled \u2014 export cancelled"));
5733
+ for (const [, pending5] of _pendingExports) {
5734
+ clearTimeout(pending5.timer);
5735
+ pending5.reject(new Error("Debug disabled \u2014 export cancelled"));
5567
5736
  }
5568
5737
  _pendingExports.clear();
5569
5738
  }
@@ -6785,6 +6954,174 @@ var init_installationId = __esm({
6785
6954
  }
6786
6955
  });
6787
6956
 
6957
+ // lib/module/features/journey/scrollContext.js
6958
+ var scrollContext_exports = {};
6959
+ __export(scrollContext_exports, {
6960
+ SCROLL_OFFSET_TTL_MS: () => SCROLL_OFFSET_TTL_MS,
6961
+ clearScrollOffsets: () => clearScrollOffsets,
6962
+ nativeTagOf: () => nativeTagOf,
6963
+ nearestRecordedOffset: () => nearestRecordedOffset,
6964
+ recordScrollOffset: () => recordScrollOffset,
6965
+ trackedScrollerCount: () => trackedScrollerCount
6966
+ });
6967
+ function nativeTagOf(node) {
6968
+ if (!node || typeof node !== "object") return null;
6969
+ const n = node;
6970
+ const tag = typeof n.__nativeTag === "number" ? n.__nativeTag : n._nativeTag;
6971
+ return typeof tag === "number" && Number.isFinite(tag) ? tag : null;
6972
+ }
6973
+ function recordScrollOffset(tag, x, y, now2 = Date.now()) {
6974
+ if (tag === null || !Number.isFinite(x) || !Number.isFinite(y)) return;
6975
+ offsets.set(tag, {
6976
+ x,
6977
+ y,
6978
+ at: now2
6979
+ });
6980
+ if (offsets.size > MAX_TRACKED_SCROLLERS) {
6981
+ let oldestTag = null;
6982
+ let oldestAt = Infinity;
6983
+ for (const [k, v] of offsets) {
6984
+ if (v.at < oldestAt) {
6985
+ oldestAt = v.at;
6986
+ oldestTag = k;
6987
+ }
6988
+ }
6989
+ if (oldestTag !== null) offsets.delete(oldestTag);
6990
+ }
6991
+ }
6992
+ function nearestRecordedOffset(ancestorTags, now2 = Date.now()) {
6993
+ for (const tag of ancestorTags) {
6994
+ if (tag === null) continue;
6995
+ const hit = offsets.get(tag);
6996
+ if (!hit) continue;
6997
+ if (now2 - hit.at > SCROLL_OFFSET_TTL_MS) {
6998
+ offsets.delete(tag);
6999
+ continue;
7000
+ }
7001
+ return {
7002
+ x: hit.x,
7003
+ y: hit.y,
7004
+ tag
7005
+ };
7006
+ }
7007
+ return null;
7008
+ }
7009
+ function clearScrollOffsets() {
7010
+ offsets.clear();
7011
+ }
7012
+ function trackedScrollerCount() {
7013
+ return offsets.size;
7014
+ }
7015
+ var SCROLL_OFFSET_TTL_MS, MAX_TRACKED_SCROLLERS, offsets;
7016
+ var init_scrollContext = __esm({
7017
+ "lib/module/features/journey/scrollContext.js"() {
7018
+ "use strict";
7019
+ SCROLL_OFFSET_TTL_MS = 5 * 6e4;
7020
+ MAX_TRACKED_SCROLLERS = 32;
7021
+ offsets = /* @__PURE__ */ new Map();
7022
+ }
7023
+ });
7024
+
7025
+ // lib/module/features/journey/autoInstrumentScroll.js
7026
+ var autoInstrumentScroll_exports = {};
7027
+ __export(autoInstrumentScroll_exports, {
7028
+ autoInstrumentScrollViews: () => autoInstrumentScrollViews,
7029
+ resetAutoInstrumentForTests: () => resetAutoInstrumentForTests
7030
+ });
7031
+ function redefine(target, key, value) {
7032
+ try {
7033
+ Object.defineProperty(target, key, {
7034
+ configurable: true,
7035
+ enumerable: true,
7036
+ writable: true,
7037
+ value
7038
+ });
7039
+ return target[key] === value;
7040
+ } catch {
7041
+ return false;
7042
+ }
7043
+ }
7044
+ function instrument(Original, React14) {
7045
+ const Wrapped = React14.forwardRef((props, ref) => {
7046
+ const onScroll = (event) => {
7047
+ try {
7048
+ const target = event?.nativeEvent?.target;
7049
+ const offset = event?.nativeEvent?.contentOffset;
7050
+ if (offset) {
7051
+ recordScrollOffset(typeof target === "number" ? target : nativeTagOf(target), offset.x, offset.y);
7052
+ }
7053
+ } catch {
7054
+ }
7055
+ try {
7056
+ props?.onScroll?.(event);
7057
+ } catch (e) {
7058
+ setTimeout(() => {
7059
+ throw e;
7060
+ }, 0);
7061
+ }
7062
+ };
7063
+ const throttle = props?.scrollEventThrottle ?? 16;
7064
+ return React14.createElement(Original, {
7065
+ ...props,
7066
+ scrollEventThrottle: throttle,
7067
+ onScroll,
7068
+ ref
7069
+ });
7070
+ });
7071
+ Wrapped.displayName = "ScaleBunAutoScrollView";
7072
+ return Wrapped;
7073
+ }
7074
+ function autoInstrumentScrollViews() {
7075
+ if (alreadyPatched) return {
7076
+ installed: true
7077
+ };
7078
+ try {
7079
+ const RN = require("react-native");
7080
+ const React14 = require("react");
7081
+ const Original = RN.ScrollView;
7082
+ if (typeof Original !== "function" && typeof Original !== "object") {
7083
+ return {
7084
+ installed: false,
7085
+ reason: "react-native has no ScrollView export to wrap"
7086
+ };
7087
+ }
7088
+ if (Original.__scalebunInstrumented) {
7089
+ alreadyPatched = true;
7090
+ return {
7091
+ installed: true
7092
+ };
7093
+ }
7094
+ const Wrapped = instrument(Original, React14);
7095
+ Wrapped.__scalebunInstrumented = true;
7096
+ if (!redefine(RN, "ScrollView", Wrapped)) {
7097
+ return {
7098
+ installed: false,
7099
+ 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."
7100
+ };
7101
+ }
7102
+ alreadyPatched = true;
7103
+ return {
7104
+ installed: true
7105
+ };
7106
+ } catch (e) {
7107
+ return {
7108
+ installed: false,
7109
+ reason: e?.message ?? "unknown failure"
7110
+ };
7111
+ }
7112
+ }
7113
+ function resetAutoInstrumentForTests() {
7114
+ alreadyPatched = false;
7115
+ }
7116
+ var alreadyPatched;
7117
+ var init_autoInstrumentScroll = __esm({
7118
+ "lib/module/features/journey/autoInstrumentScroll.js"() {
7119
+ "use strict";
7120
+ init_scrollContext();
7121
+ alreadyPatched = false;
7122
+ }
7123
+ });
7124
+
6788
7125
  // lib/module/features/engage/engageSignals.js
6789
7126
  var EngageSignalBus, engageSignals;
6790
7127
  var init_engageSignals = __esm({
@@ -6993,322 +7330,6 @@ var init_deviceAttributes = __esm({
6993
7330
  }
6994
7331
  });
6995
7332
 
6996
- // lib/module/features/ota/crypto/loadEd25519.js
6997
- function loadEd25519() {
6998
- try {
6999
- const mod = require("@noble/ed25519");
7000
- const ed = mod?.default ?? mod;
7001
- if (typeof ed?.verifyAsync === "function" || typeof ed?.verify === "function") {
7002
- return ed;
7003
- }
7004
- return null;
7005
- } catch {
7006
- return null;
7007
- }
7008
- }
7009
- var init_loadEd25519 = __esm({
7010
- "lib/module/features/ota/crypto/loadEd25519.js"() {
7011
- "use strict";
7012
- }
7013
- });
7014
-
7015
- // lib/module/features/ota/crypto/loadSha512.js
7016
- function loadSha512() {
7017
- try {
7018
- const mod = require("@noble/hashes/sha2.js");
7019
- const sha512 = mod?.sha512 ?? mod?.default?.sha512;
7020
- return typeof sha512 === "function" ? sha512 : null;
7021
- } catch {
7022
- return null;
7023
- }
7024
- }
7025
- var init_loadSha512 = __esm({
7026
- "lib/module/features/ota/crypto/loadSha512.js"() {
7027
- "use strict";
7028
- }
7029
- });
7030
-
7031
- // lib/module/features/ota/crypto/builtinVerifier.js
7032
- var builtinVerifier_exports = {};
7033
- __export(builtinVerifier_exports, {
7034
- _resetBuiltinVerifier: () => _resetBuiltinVerifier,
7035
- decodeSignature: () => decodeSignature,
7036
- getBuiltinVerifier: () => getBuiltinVerifier
7037
- });
7038
- function hexToBytes(hex) {
7039
- const clean2 = hex.trim().toLowerCase().replace(/^0x/, "");
7040
- if (clean2.length === 0 || clean2.length % 2 !== 0 || /[^0-9a-f]/.test(clean2)) return null;
7041
- const out = new Uint8Array(clean2.length / 2);
7042
- for (let i = 0; i < out.length; i++) {
7043
- out[i] = parseInt(clean2.slice(i * 2, i * 2 + 2), 16);
7044
- }
7045
- return out;
7046
- }
7047
- function base64ToBytes2(b64) {
7048
- const s = b64.trim().replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
7049
- if (s.length === 0) return null;
7050
- const out = [];
7051
- let buffer = 0;
7052
- let bits = 0;
7053
- for (const ch of s) {
7054
- const idx = B64_ALPHABET2.indexOf(ch);
7055
- if (idx === -1) return null;
7056
- buffer = buffer << 6 | idx;
7057
- bits += 6;
7058
- if (bits >= 8) {
7059
- bits -= 8;
7060
- out.push(buffer >> bits & 255);
7061
- }
7062
- }
7063
- return Uint8Array.from(out);
7064
- }
7065
- function decodeSignature(sig) {
7066
- const trimmed = sig.trim();
7067
- if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
7068
- const hex2 = hexToBytes(trimmed);
7069
- if (hex2 && hex2.length === ED25519_SIGNATURE_BYTES) return hex2;
7070
- }
7071
- const b64 = base64ToBytes2(trimmed);
7072
- if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
7073
- const hex = hexToBytes(trimmed);
7074
- if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
7075
- return null;
7076
- }
7077
- function wireSha512(ed, sha512) {
7078
- const concat2 = (parts) => {
7079
- if (parts.length === 1) return parts[0];
7080
- let total = 0;
7081
- for (const p of parts) total += p.length;
7082
- const out = new Uint8Array(total);
7083
- let off = 0;
7084
- for (const p of parts) {
7085
- out.set(p, off);
7086
- off += p.length;
7087
- }
7088
- return out;
7089
- };
7090
- const sync = (...msgs) => sha512(concat2(msgs));
7091
- const asyncFn = async (...msgs) => sha512(concat2(msgs));
7092
- let wired = false;
7093
- try {
7094
- if (ed.hashes && typeof ed.hashes === "object") {
7095
- if (typeof ed.hashes.sha512 !== "function") ed.hashes.sha512 = sync;
7096
- if (typeof ed.hashes.sha512Async !== "function") ed.hashes.sha512Async = asyncFn;
7097
- wired = typeof ed.hashes.sha512 === "function";
7098
- }
7099
- } catch {
7100
- }
7101
- try {
7102
- if (ed.etc && typeof ed.etc === "object") {
7103
- if (typeof ed.etc.sha512Sync !== "function") ed.etc.sha512Sync = sync;
7104
- if (typeof ed.etc.sha512Async !== "function") ed.etc.sha512Async = asyncFn;
7105
- wired = wired || typeof ed.etc.sha512Sync === "function";
7106
- }
7107
- } catch {
7108
- }
7109
- return wired;
7110
- }
7111
- function getBuiltinVerifier() {
7112
- if (resolved !== void 0) return resolved;
7113
- const ed = loadEd25519();
7114
- const sha512 = loadSha512();
7115
- if (!ed || !sha512) {
7116
- resolved = null;
7117
- return null;
7118
- }
7119
- if (!wireSha512(ed, sha512)) {
7120
- 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.");
7121
- resolved = null;
7122
- return null;
7123
- }
7124
- resolved = async ({
7125
- messageHex,
7126
- signature,
7127
- publicKey
7128
- }) => {
7129
- const message = hexToBytes(messageHex);
7130
- const pub = hexToBytes(publicKey);
7131
- const sig = decodeSignature(signature);
7132
- if (!message) {
7133
- logger.error("[OTA] Bundle SHA-256 is not valid hex \u2014 refusing to stage.");
7134
- return false;
7135
- }
7136
- if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
7137
- 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"}.`);
7138
- return false;
7139
- }
7140
- if (!sig) {
7141
- logger.error("[OTA] Bundle signature is not a 64-byte hex/base64 value.");
7142
- return false;
7143
- }
7144
- try {
7145
- if (typeof ed.verify === "function") return ed.verify(sig, message, pub);
7146
- if (typeof ed.verifyAsync === "function") return await ed.verifyAsync(sig, message, pub);
7147
- return false;
7148
- } catch {
7149
- return false;
7150
- }
7151
- };
7152
- __DEV__ && logger.debug("[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).");
7153
- return resolved;
7154
- }
7155
- function _resetBuiltinVerifier() {
7156
- resolved = void 0;
7157
- }
7158
- var ED25519_SIGNATURE_BYTES, ED25519_PUBLIC_KEY_BYTES, B64_ALPHABET2, resolved;
7159
- var init_builtinVerifier = __esm({
7160
- "lib/module/features/ota/crypto/builtinVerifier.js"() {
7161
- "use strict";
7162
- init_loadEd25519();
7163
- init_loadSha512();
7164
- init_internalLogger();
7165
- ED25519_SIGNATURE_BYTES = 64;
7166
- ED25519_PUBLIC_KEY_BYTES = 32;
7167
- B64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7168
- }
7169
- });
7170
-
7171
- // lib/module/features/ota/crypto/nativeVerifier.js
7172
- function bytesToHex(bytes) {
7173
- let out = "";
7174
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
7175
- return out;
7176
- }
7177
- function normalizeHex(value, expectedChars) {
7178
- const clean2 = value.trim().toLowerCase().replace(/^0x/, "");
7179
- if (clean2.length !== expectedChars || /[^0-9a-f]/.test(clean2)) return null;
7180
- return clean2;
7181
- }
7182
- function getNativeVerifier() {
7183
- if (resolved2 !== void 0) return resolved2;
7184
- const native = NativeScaleBunOta_default;
7185
- if (!native || typeof native.verifyEd25519 !== "function") {
7186
- resolved2 = null;
7187
- return null;
7188
- }
7189
- resolved2 = async ({
7190
- messageHex,
7191
- signature,
7192
- publicKey
7193
- }) => {
7194
- const message = normalizeHex(messageHex, 64);
7195
- const key = normalizeHex(publicKey, ED25519_PUBLIC_KEY_HEX_CHARS);
7196
- const sigBytes = decodeSignature(signature);
7197
- if (!message || !key || !sigBytes) {
7198
- logger.error("[OTA] Signature inputs malformed (bundle hash, signature, or public key) \u2014 refusing to stage.");
7199
- return false;
7200
- }
7201
- let verdict;
7202
- try {
7203
- verdict = await native.verifyEd25519(message, bytesToHex(sigBytes), key);
7204
- } catch (err) {
7205
- logger.warn(`[OTA] Native ed25519 verify failed to run: ${err?.message ?? err}`);
7206
- verdict = "unavailable";
7207
- }
7208
- if (verdict === "valid") return true;
7209
- if (verdict === "invalid") return false;
7210
- const builtin = getBuiltinVerifier();
7211
- if (builtin) {
7212
- return builtin({
7213
- messageHex,
7214
- signature,
7215
- publicKey
7216
- });
7217
- }
7218
- throw new Error("ed25519 unavailable: this OS has no native implementation (Android < 13) and the optional peers @noble/ed25519 + @noble/hashes are not installed.");
7219
- };
7220
- __DEV__ && logger.debug("[OTA] Using native ed25519 verifier (platform crypto).");
7221
- return resolved2;
7222
- }
7223
- var ED25519_PUBLIC_KEY_HEX_CHARS, resolved2;
7224
- var init_nativeVerifier = __esm({
7225
- "lib/module/features/ota/crypto/nativeVerifier.js"() {
7226
- "use strict";
7227
- init_NativeScaleBunOta();
7228
- init_internalLogger();
7229
- init_builtinVerifier();
7230
- ED25519_PUBLIC_KEY_HEX_CHARS = 64;
7231
- }
7232
- });
7233
-
7234
- // lib/module/features/ota/signature.js
7235
- async function verifyBundleSignature(bundleSha256, signature, config) {
7236
- const rawKey = config?.publicKey;
7237
- const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter((k) => typeof k === "string" && k.trim().length > 0);
7238
- if (rawKey === void 0 || rawKey === "" || rawKey === null) {
7239
- if (!warnedNotConfigured) {
7240
- warnedNotConfigured = true;
7241
- logger.warn("[OTA] No signing public key configured \u2014 bundles are accepted on SHA-256 integrity alone. Configure `ota.publicSigningKey` to enforce authenticity.");
7242
- }
7243
- return {
7244
- ok: true,
7245
- reason: "not_configured"
7246
- };
7247
- }
7248
- if (keys.length === 0) {
7249
- 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.");
7250
- return {
7251
- ok: false,
7252
- reason: "no_keys"
7253
- };
7254
- }
7255
- if (!signature) {
7256
- logger.error("[OTA] Bundle has no signature but a signing key is configured \u2014 refusing to stage.");
7257
- return {
7258
- ok: false,
7259
- reason: "missing_signature"
7260
- };
7261
- }
7262
- const verifier = typeof config?.verifier === "function" ? config.verifier : getNativeVerifier() ?? getBuiltinVerifier();
7263
- if (!verifier) {
7264
- 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`.");
7265
- return {
7266
- ok: false,
7267
- reason: "no_verifier"
7268
- };
7269
- }
7270
- let sawThrow = false;
7271
- let sawCompletion = false;
7272
- for (const key of keys) {
7273
- try {
7274
- const valid = await verifier({
7275
- messageHex: bundleSha256,
7276
- signature,
7277
- publicKey: key
7278
- });
7279
- sawCompletion = true;
7280
- if (valid) return {
7281
- ok: true,
7282
- reason: "verified"
7283
- };
7284
- } catch (err) {
7285
- sawThrow = true;
7286
- logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
7287
- }
7288
- }
7289
- if (sawThrow && !sawCompletion) {
7290
- return {
7291
- ok: false,
7292
- reason: "verifier_threw"
7293
- };
7294
- }
7295
- 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.");
7296
- return {
7297
- ok: false,
7298
- reason: "invalid_signature"
7299
- };
7300
- }
7301
- var warnedNotConfigured;
7302
- var init_signature = __esm({
7303
- "lib/module/features/ota/signature.js"() {
7304
- "use strict";
7305
- init_internalLogger();
7306
- init_builtinVerifier();
7307
- init_nativeVerifier();
7308
- warnedNotConfigured = false;
7309
- }
7310
- });
7311
-
7312
7333
  // lib/module/features/ota/environment.js
7313
7334
  function compare(a, b) {
7314
7335
  if (a.major !== b.major) return a.major - b.major;
@@ -7536,10 +7557,10 @@ async function deliverOtaEvents(params) {
7536
7557
  })
7537
7558
  });
7538
7559
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
7539
- __DEV__ && logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
7560
+ logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
7540
7561
  } catch (err) {
7541
7562
  for (const e of events) otaEventEmitter.emit(e);
7542
- __DEV__ && logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
7563
+ logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
7543
7564
  }
7544
7565
  }
7545
7566
  var import_react_native33, PROGRESS_POST_INTERVAL_MS, DEFAULT_HEALTHY_AFTER_MS, OtaOrchestrator, otaOrchestrator;
@@ -7554,7 +7575,6 @@ var init_OtaOrchestrator = __esm({
7554
7575
  init_geoCountry();
7555
7576
  init_deviceAttributes();
7556
7577
  init_installationId();
7557
- init_signature();
7558
7578
  init_environment();
7559
7579
  init_retry();
7560
7580
  init_StorageBackend();
@@ -7573,6 +7593,8 @@ var init_OtaOrchestrator = __esm({
7573
7593
  return this.currentBundle;
7574
7594
  }
7575
7595
  isRestartRequiredState = false;
7596
+ /** Root/jailbreak indicators observed at init; empty when none found. */
7597
+ integrityIndicators = [];
7576
7598
  healthyTimer = null;
7577
7599
  bootGuardConfig = {};
7578
7600
  environment = null;
@@ -7582,15 +7604,15 @@ var init_OtaOrchestrator = __esm({
7582
7604
  /**
7583
7605
  * Initialize the orchestrator. Checks for boot-guard recovery state.
7584
7606
  *
7585
- * `signature` is optional: an app that has not adopted signing keeps working
7586
- * on SHA-256 integrity alone. Once a public key IS configured, a bundle that
7587
- * cannot be verified is refused rather than installed (OTA-03).
7607
+ * Signature enforcement is no longer a JS concern: signed releases are
7608
+ * verified and staged in NATIVE code against RSA keys embedded in the app
7609
+ * package (SB-OTA-RSA-SHA256-V1), and unsigned releases keep working on
7610
+ * SHA-256 integrity alone. There is nothing for the host to configure here.
7588
7611
  */
7589
7612
  init(config) {
7590
7613
  noThrow(() => {
7591
7614
  if (this.initialized) {
7592
7615
  this.bootGuardConfig = config ?? this.bootGuardConfig;
7593
- this.signatureConfig = config?.signature ?? this.signatureConfig;
7594
7616
  return;
7595
7617
  }
7596
7618
  if (!NativeScaleBunOta_default) {
@@ -7607,18 +7629,27 @@ var init_OtaOrchestrator = __esm({
7607
7629
  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.");
7608
7630
  }
7609
7631
  if (this.environment.requiresReactHostIntegration) {
7610
- __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.");
7632
+ 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.");
7611
7633
  }
7612
7634
  this.initialized = true;
7613
7635
  this.enabled = true;
7636
+ try {
7637
+ const raw = NativeScaleBunOta_default?.getDeviceIntegrity?.();
7638
+ this.integrityIndicators = raw ? JSON.parse(raw) : [];
7639
+ if (this.integrityIndicators.length > 0) {
7640
+ 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.`);
7641
+ }
7642
+ } catch {
7643
+ this.integrityIndicators = [];
7644
+ }
7614
7645
  this.bootGuardConfig = config ?? {};
7615
- this.signatureConfig = config?.signature;
7616
- __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
7646
+ logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
7617
7647
  const slotState = this.readSlotState();
7618
7648
  this.hydrateCurrentBundleFromSlots(slotState);
7619
7649
  void prefetchDeviceCountry();
7620
7650
  this.checkBootGuardRecovery(slotState);
7621
7651
  this.verifyRunningBundleIdentity();
7652
+ this.startBootGuardHeartbeat(slotState);
7622
7653
  });
7623
7654
  }
7624
7655
  /**
@@ -7649,13 +7680,13 @@ var init_OtaOrchestrator = __esm({
7649
7680
  releaseId: record4.releaseId ?? void 0,
7650
7681
  sha256: record4.sha256
7651
7682
  };
7652
- __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
7683
+ logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
7653
7684
  return;
7654
7685
  }
7655
7686
  this.currentBundle = {
7656
7687
  sha256: current.sha256
7657
7688
  };
7658
- __DEV__ && logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
7689
+ logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
7659
7690
  } catch {
7660
7691
  }
7661
7692
  }
@@ -7685,7 +7716,7 @@ var init_OtaOrchestrator = __esm({
7685
7716
  }
7686
7717
  if (!record4.identityToken) return;
7687
7718
  if (running === record4.identityToken) {
7688
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7719
+ logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7689
7720
  this.retireIdentityToken(record4);
7690
7721
  return;
7691
7722
  }
@@ -7788,6 +7819,13 @@ var init_OtaOrchestrator = __esm({
7788
7819
  isEnabled() {
7789
7820
  return this.enabled;
7790
7821
  }
7822
+ /**
7823
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
7824
+ * Empty means nothing was detected — not proof the device is clean.
7825
+ */
7826
+ deviceIntegrityIndicators() {
7827
+ return [...this.integrityIndicators];
7828
+ }
7791
7829
  isRestartRequired() {
7792
7830
  return this.isRestartRequiredState;
7793
7831
  }
@@ -7799,12 +7837,22 @@ var init_OtaOrchestrator = __esm({
7799
7837
  const env = this.environment ?? detectOtaEnvironment();
7800
7838
  const mobileOS = resolveMobileOS();
7801
7839
  if (!mobileOS) throw new Error("OTA is not supported on this platform");
7840
+ let trustedKeyIds;
7841
+ try {
7842
+ const raw = NativeScaleBunOta_default?.getTrustedKeyIds?.();
7843
+ if (raw) {
7844
+ const arr2 = JSON.parse(raw);
7845
+ if (Array.isArray(arr2) && arr2.length) trustedKeyIds = arr2.slice(0, 16);
7846
+ }
7847
+ } catch {
7848
+ }
7802
7849
  const payload = {
7803
7850
  appVersion: requestParams.appVersion,
7804
7851
  platform: mobileOS,
7805
7852
  installationId: requestParams.installationId,
7806
7853
  currentBundleId: this.currentBundle?.id,
7807
7854
  currentBundleHash: this.currentBundle?.sha256,
7855
+ trustedKeyIds,
7808
7856
  channelName: requestParams.channelName,
7809
7857
  // Host-supplied country wins; otherwise the cached edge-Worker lookup
7810
7858
  // fills in (see geoCountry.ts). Undefined when neither knows — the
@@ -7871,7 +7919,7 @@ var init_OtaOrchestrator = __esm({
7871
7919
  error: "OTA native module disabled or missing"
7872
7920
  };
7873
7921
  }
7874
- __DEV__ && logger.debug("[OTA] Sync started\u2026");
7922
+ logger.debug("[OTA] Sync started\u2026");
7875
7923
  if (this.currentBundle?.id) {
7876
7924
  otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
7877
7925
  releaseId: this.currentBundle.releaseId,
@@ -7880,7 +7928,7 @@ var init_OtaOrchestrator = __esm({
7880
7928
  }
7881
7929
  const checkRes = await this.checkForUpdate(params);
7882
7930
  if (checkRes.action === "NONE") {
7883
- __DEV__ && logger.debug("[OTA] App is up to date");
7931
+ logger.debug("[OTA] App is up to date");
7884
7932
  this.startBootGuardHeartbeat();
7885
7933
  return {
7886
7934
  status: "UP_TO_DATE"
@@ -7918,25 +7966,12 @@ var init_OtaOrchestrator = __esm({
7918
7966
  releaseId: bundle.releaseId,
7919
7967
  version: bundle.version
7920
7968
  });
7921
- const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
7922
- if (!signatureOutcome.ok) {
7923
- otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
7924
- releaseId: bundle.releaseId,
7925
- error: `Signature check failed: ${signatureOutcome.reason}`,
7926
- version: bundle.version
7927
- });
7928
- return {
7929
- status: "ERROR",
7930
- error: `Bundle signature check failed (${signatureOutcome.reason})`,
7931
- bundle
7932
- };
7933
- }
7934
7969
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
7935
7970
  releaseId: bundle.releaseId,
7936
7971
  version: bundle.version
7937
7972
  });
7938
7973
  const downloadStart = Date.now();
7939
- __DEV__ && logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
7974
+ logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
7940
7975
  let lastPostAt = 0;
7941
7976
  const postProgress = (bytesRead, status) => {
7942
7977
  const total = bundle.size > 0 ? bundle.size : Math.max(bytesRead, 1);
@@ -7978,39 +8013,75 @@ var init_OtaOrchestrator = __esm({
7978
8013
  }
7979
8014
  });
7980
8015
  let staged = false;
7981
- if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
7982
- __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`);
8016
+ if (bundle.envelope) {
8017
+ const envelopeJson = JSON.stringify({
8018
+ ...bundle.envelope,
8019
+ downloadUrl: bundle.url
8020
+ });
8021
+ let verdict;
7983
8022
  try {
7984
- staged = await NativeScaleBunOta_default.stagePatch(
7985
- bundle.patch.url,
7986
- bundle.patch.sha256,
7987
- bundle.sha256
7988
- // expected full-bundle hash after patching
7989
- );
7990
- if (staged) {
7991
- patchUsed = true;
7992
- __DEV__ && logger.debug("[OTA] Patch applied successfully");
7993
- } else {
7994
- logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
7995
- }
7996
- } catch (patchErr) {
7997
- logger.warn(`[OTA] Patch staging failed: ${patchErr.message} \u2014 falling back to full bundle`);
8023
+ const verdictJson = await NativeScaleBunOta_default.verifyAndStageRelease(envelopeJson);
8024
+ verdict = JSON.parse(verdictJson);
8025
+ } catch (err) {
8026
+ verdict = {
8027
+ ok: false,
8028
+ code: "verify_stage_threw",
8029
+ message: err?.message ?? String(err)
8030
+ };
7998
8031
  }
7999
- }
8000
- if (!staged) {
8001
- try {
8002
- staged = await retryWithBackoff(async () => {
8003
- const ok = await NativeScaleBunOta_default.stageBundle(bundle.url, bundle.sha256);
8004
- if (!ok) {
8005
- throw new Error("stageBundle returned false");
8006
- }
8007
- return ok;
8008
- }, {
8009
- label: `download v${bundle.version}`,
8010
- attempts: 3
8032
+ if (!verdict.ok) {
8033
+ unsubscribeProgress();
8034
+ postProgress(0, "FAILED");
8035
+ logger.error(`[OTA] Native verify/stage refused the release: ${verdict.code ?? "unknown"}${verdict.message ? ` \u2014 ${verdict.message}` : ""}`);
8036
+ otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
8037
+ releaseId: bundle.releaseId,
8038
+ // `error` is mapped onto the wire `errorCode` by deliverOtaEvents —
8039
+ // carry the stable native verdict code there.
8040
+ error: verdict.code ?? "verify_and_stage_release_failed",
8041
+ version: bundle.version
8011
8042
  });
8012
- } catch {
8013
- staged = false;
8043
+ return {
8044
+ status: "ERROR",
8045
+ error: `Release verification failed (${verdict.code ?? "unknown"})`,
8046
+ bundle
8047
+ };
8048
+ }
8049
+ staged = true;
8050
+ } else {
8051
+ if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
8052
+ logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
8053
+ try {
8054
+ staged = await NativeScaleBunOta_default.stagePatch(
8055
+ bundle.patch.url,
8056
+ bundle.patch.sha256,
8057
+ bundle.sha256
8058
+ // expected full-bundle hash after patching
8059
+ );
8060
+ if (staged) {
8061
+ patchUsed = true;
8062
+ logger.debug("[OTA] Patch applied successfully");
8063
+ } else {
8064
+ logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
8065
+ }
8066
+ } catch (patchErr) {
8067
+ logger.warn(`[OTA] Patch staging failed: ${patchErr.message} \u2014 falling back to full bundle`);
8068
+ }
8069
+ }
8070
+ if (!staged) {
8071
+ try {
8072
+ staged = await retryWithBackoff(async () => {
8073
+ const ok = await NativeScaleBunOta_default.stageBundle(bundle.url, bundle.sha256);
8074
+ if (!ok) {
8075
+ throw new Error("stageBundle returned false");
8076
+ }
8077
+ return ok;
8078
+ }, {
8079
+ label: `download v${bundle.version}`,
8080
+ attempts: 3
8081
+ });
8082
+ } catch {
8083
+ staged = false;
8084
+ }
8014
8085
  }
8015
8086
  }
8016
8087
  unsubscribeProgress();
@@ -8057,7 +8128,7 @@ var init_OtaOrchestrator = __esm({
8057
8128
  releaseId: bundle.releaseId,
8058
8129
  version: bundle.version
8059
8130
  });
8060
- __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
8131
+ logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
8061
8132
  await deliverOtaEvents(params);
8062
8133
  const installMode = bundle.installMode ?? "ON_NEXT_RESTART";
8063
8134
  if (params.autoRestart || installMode === "IMMEDIATE") {
@@ -8088,24 +8159,23 @@ var init_OtaOrchestrator = __esm({
8088
8159
  * native boot-guard marker. If JS crashes before the timer fires, the
8089
8160
  * marker stays set and native code reverts on next cold start.
8090
8161
  */
8091
- startBootGuardHeartbeat() {
8162
+ startBootGuardHeartbeat(preRead) {
8092
8163
  if (!NativeScaleBunOta_default) return;
8093
8164
  if (this.healthyTimer) {
8094
8165
  clearTimeout(this.healthyTimer);
8095
8166
  this.healthyTimer = null;
8096
8167
  }
8097
8168
  try {
8098
- const stateJson = NativeScaleBunOta_default.getSlotState();
8099
- const state = JSON.parse(stateJson);
8100
- if (!state.bootMarkerPresent) {
8169
+ const state = preRead !== void 0 ? preRead : JSON.parse(NativeScaleBunOta_default.getSlotState());
8170
+ if (!state?.bootMarkerPresent) {
8101
8171
  return;
8102
8172
  }
8103
8173
  const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
8104
- __DEV__ && logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
8174
+ logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
8105
8175
  this.healthyTimer = setTimeout(() => {
8106
8176
  noThrow(() => {
8107
8177
  NativeScaleBunOta_default?.markHealthy();
8108
- __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
8178
+ logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
8109
8179
  this.healthyTimer = null;
8110
8180
  const running = this.currentBundle;
8111
8181
  if (running?.id) {
@@ -8151,11 +8221,11 @@ var init_OtaOrchestrator = __esm({
8151
8221
  } catch {
8152
8222
  }
8153
8223
  this.resumeRestartArmed = false;
8154
- __DEV__ && logger.info("[OTA] App resumed \u2014 activating the staged update now");
8224
+ logger.info("[OTA] App resumed \u2014 activating the staged update now");
8155
8225
  this.restart();
8156
8226
  }
8157
8227
  });
8158
- __DEV__ && logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
8228
+ logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
8159
8229
  } catch {
8160
8230
  this.resumeRestartArmed = false;
8161
8231
  }
@@ -8172,7 +8242,7 @@ var init_OtaOrchestrator = __esm({
8172
8242
  */
8173
8243
  restart() {
8174
8244
  noThrow(() => {
8175
- __DEV__ && logger.info("[OTA] Triggering app restart\u2026");
8245
+ logger.info("[OTA] Triggering app restart\u2026");
8176
8246
  if (NativeScaleBunOta_default && typeof NativeScaleBunOta_default.restartApp === "function") {
8177
8247
  NativeScaleBunOta_default.restartApp();
8178
8248
  } else if (import_react_native33.DevSettings && typeof import_react_native33.DevSettings.reload === "function") {
@@ -8191,17 +8261,25 @@ var init_OtaOrchestrator = __esm({
8191
8261
  var uiState_exports = {};
8192
8262
  __export(uiState_exports, {
8193
8263
  clearUiState: () => clearUiState,
8264
+ onUiStateChange: () => onUiStateChange,
8265
+ resetUiStateListeners: () => resetUiStateListeners,
8194
8266
  setUiState: () => setUiState,
8267
+ uiStateListenerCount: () => uiStateListenerCount,
8195
8268
  uiStateSignature: () => uiStateSignature
8196
8269
  });
8197
8270
  function setUiState(name, value) {
8198
8271
  const n = clean(name);
8199
8272
  const v = clean(value);
8200
- if (n && v) declared.set(n, v);
8273
+ if (!n || !v) return;
8274
+ if (declared.get(n) === v) return;
8275
+ declared.set(n, v);
8276
+ notifyChanged();
8201
8277
  }
8202
8278
  function clearUiState(name) {
8279
+ const before = declared.size;
8203
8280
  if (name === void 0) declared.clear();
8204
8281
  else declared.delete(clean(name));
8282
+ if (declared.size !== before) notifyChanged();
8205
8283
  }
8206
8284
  function uiStateSignature() {
8207
8285
  if (!declared.size) return void 0;
@@ -8214,12 +8292,42 @@ function uiStateSignature() {
8214
8292
  }
8215
8293
  return signature || void 0;
8216
8294
  }
8217
- var declared, clean;
8295
+ function onUiStateChange(listener) {
8296
+ listeners2.add(listener);
8297
+ return () => {
8298
+ listeners2.delete(listener);
8299
+ };
8300
+ }
8301
+ function resetUiStateListeners() {
8302
+ listeners2.clear();
8303
+ if (pending4 !== null) {
8304
+ clearTimeout(pending4);
8305
+ pending4 = null;
8306
+ }
8307
+ }
8308
+ function uiStateListenerCount() {
8309
+ return listeners2.size;
8310
+ }
8311
+ function notifyChanged() {
8312
+ if (pending4 !== null) return;
8313
+ pending4 = setTimeout(() => {
8314
+ pending4 = null;
8315
+ for (const listener of listeners2) {
8316
+ try {
8317
+ listener();
8318
+ } catch {
8319
+ }
8320
+ }
8321
+ }, 0);
8322
+ }
8323
+ var declared, clean, listeners2, pending4;
8218
8324
  var init_uiState = __esm({
8219
8325
  "lib/module/features/journey/uiState.js"() {
8220
8326
  "use strict";
8221
8327
  declared = /* @__PURE__ */ new Map();
8222
8328
  clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
8329
+ listeners2 = /* @__PURE__ */ new Set();
8330
+ pending4 = null;
8223
8331
  }
8224
8332
  });
8225
8333
 
@@ -10170,6 +10278,22 @@ var SHAPE = {
10170
10278
  }
10171
10279
  },
10172
10280
  captureInteractionHeatmap: bool(true),
10281
+ /**
10282
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
10283
+ * offset, not just ones wrapped in ScaleBunScrollView.
10284
+ *
10285
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
10286
+ * That flag changes what the SDK records about the host; this one changes what the HOST
10287
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
10288
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
10289
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
10290
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
10291
+ * discover afterwards.
10292
+ *
10293
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
10294
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
10295
+ */
10296
+ autoInstrumentScrollViews: bool(false),
10173
10297
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
10174
10298
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
10175
10299
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -10186,18 +10310,9 @@ var SHAPE = {
10186
10310
  kind: "str",
10187
10311
  opt: true
10188
10312
  },
10189
- publicSigningKey: {
10190
- kind: "str",
10191
- opt: true
10192
- },
10193
- // Rotation support: several pinned keys, any of which may verify a
10194
- // bundle. Ship a build pinning [old, new], re-sign server-side with
10195
- // new, drop old next release — no emergency store submission when a
10196
- // key must be replaced. Merged with publicSigningKey by the facade.
10197
- publicSigningKeys: {
10198
- kind: "strArr",
10199
- opt: true
10200
- },
10313
+ // No signing-key fields: signed releases are verified natively against
10314
+ // RSA keys embedded in the app package (SB-OTA-RSA-SHA256-V1). JS must
10315
+ // not supply a trusted key.
10201
10316
  mandatoryBlocksUi: bool(false)
10202
10317
  }
10203
10318
  }
@@ -10999,6 +11114,23 @@ function installNetworkMonitoring(onEvent, options = {}) {
10999
11114
  });
11000
11115
  }
11001
11116
 
11117
+ // lib/module/features/network/thirdParty.js
11118
+ 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";
11119
+ function isThirdPartyBeacon(url, hosts) {
11120
+ if (!url) return false;
11121
+ let host = "";
11122
+ try {
11123
+ host = new URL(url).hostname.toLowerCase();
11124
+ } catch {
11125
+ return false;
11126
+ }
11127
+ if (!host) return false;
11128
+ for (const suffix of (hosts ?? THIRD_PARTY_BEACON_HOSTS).split(",")) {
11129
+ if (host === suffix || host.endsWith("." + suffix)) return true;
11130
+ }
11131
+ return false;
11132
+ }
11133
+
11002
11134
  // lib/module/features/network/NetworkFeature.js
11003
11135
  init_networkAdapter();
11004
11136
  init_SessionManager();
@@ -11014,10 +11146,12 @@ var NetworkFeature = class {
11014
11146
  if (this.active) return;
11015
11147
  installNetworkMonitoring((data) => {
11016
11148
  const isInternal = isSdkInternalUrl(data.url, this.options.sdkBaseUrl);
11017
- if (!isInternal) {
11149
+ const isBeaconOrDenied = this.options.captureThirdParty !== true && isThirdPartyBeacon(data.url, this.options.thirdPartyHosts) || matchesDenyUrl(data.url, this.options.denyUrls);
11150
+ const isExcluded = isInternal || isBeaconOrDenied;
11151
+ if (!isExcluded) {
11018
11152
  this.trackFn("$network_request", data);
11019
11153
  }
11020
- if (data.type === "network") {
11154
+ if (data.type === "network" && !isBeaconOrDenied) {
11021
11155
  reportNetworkRequest({
11022
11156
  method: data.method,
11023
11157
  url: data.url,
@@ -11033,7 +11167,7 @@ var NetworkFeature = class {
11033
11167
  requestBody: data.requestBody,
11034
11168
  responseBody: data.responseBody
11035
11169
  });
11036
- if (!isInternal) {
11170
+ if (!isExcluded) {
11037
11171
  const sessionManager = SessionManager.getExistingInstance();
11038
11172
  if (sessionManager) {
11039
11173
  const backendTransport = sessionManager.getBackendTransport();
@@ -11064,6 +11198,13 @@ var NetworkFeature = class {
11064
11198
  return this.active;
11065
11199
  }
11066
11200
  };
11201
+ function matchesDenyUrl(url, patterns) {
11202
+ if (!patterns || !patterns.length || !url) return false;
11203
+ for (const p of patterns) {
11204
+ if (typeof p === "string" ? url.includes(p) : p.test(url)) return true;
11205
+ }
11206
+ return false;
11207
+ }
11067
11208
  function isSdkInternalUrl(url, sdkBaseUrl) {
11068
11209
  if (!url) return false;
11069
11210
  try {
@@ -12095,8 +12236,8 @@ var PerformanceTransport = class {
12095
12236
  setSink(sink) {
12096
12237
  this.sink = sink;
12097
12238
  if (sink && this.buffer.length > 0) {
12098
- const pending4 = this.buffer.splice(0);
12099
- for (const msg of pending4) {
12239
+ const pending5 = this.buffer.splice(0);
12240
+ for (const msg of pending5) {
12100
12241
  try {
12101
12242
  sink.send(msg);
12102
12243
  } catch {
@@ -13811,22 +13952,22 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
13811
13952
  * lose the snapshot entirely. `imageData.length` is the base64 length, which is within ~1% of the
13812
13953
  * serialized cost and needs no extra pass.
13813
13954
  */
13814
- static _selectFramesToPersist(pending4) {
13955
+ static _selectFramesToPersist(pending5) {
13815
13956
  const isKeyframe = (b) => b.frame.frameType !== "delta";
13816
13957
  const size = (b) => b.imageData?.length ?? 0;
13817
13958
  const keep = [];
13818
13959
  let budget = _BackendSessionAdapter.MAX_PERSIST_BYTES;
13819
13960
  const maxCount = _BackendSessionAdapter.MAX_FRAMES_PER_BATCH;
13820
- for (let i = pending4.length - 1; i >= 0; i--) {
13821
- const b = pending4[i];
13961
+ for (let i = pending5.length - 1; i >= 0; i--) {
13962
+ const b = pending5[i];
13822
13963
  if (!isKeyframe(b)) continue;
13823
13964
  const cost = size(b);
13824
13965
  if (cost > budget || keep.length >= maxCount) continue;
13825
13966
  budget -= cost;
13826
13967
  keep.push(b);
13827
13968
  }
13828
- for (let i = pending4.length - 1; i >= 0; i--) {
13829
- const b = pending4[i];
13969
+ for (let i = pending5.length - 1; i >= 0; i--) {
13970
+ const b = pending5[i];
13830
13971
  if (isKeyframe(b)) continue;
13831
13972
  const cost = size(b);
13832
13973
  if (cost > budget || keep.length >= maxCount) continue;
@@ -14259,6 +14400,10 @@ var SDKBootstrapper = class {
14259
14400
  return null;
14260
14401
  }
14261
14402
  this.config = result.data;
14403
+ const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
14404
+ if (typeof requestedApiBaseUrl === "string" && requestedApiBaseUrl && requestedApiBaseUrl !== DEFAULT_API_BASE_URL) {
14405
+ 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.`);
14406
+ }
14262
14407
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
14263
14408
  if (this.config.logLevel) {
14264
14409
  logger.setLevelByName(this.config.logLevel);
@@ -14423,6 +14568,20 @@ var SDKBootstrapper = class {
14423
14568
  }));
14424
14569
  }
14425
14570
  await this.featureRegistry.initializeAll(featureContext);
14571
+ if (this.config.autoInstrumentScrollViews === true) {
14572
+ try {
14573
+ const {
14574
+ autoInstrumentScrollViews: autoInstrumentScrollViews2
14575
+ } = (init_autoInstrumentScroll(), __toCommonJS(autoInstrumentScroll_exports));
14576
+ const outcome = autoInstrumentScrollViews2();
14577
+ if (!outcome.installed) {
14578
+ logger.warn("[Bootstrap] autoInstrumentScrollViews was requested but could not be installed: " + outcome.reason + " \u2014 taps will carry no content depth.");
14579
+ } else {
14580
+ __DEV__ && logger.debug("[Bootstrap] ScrollView auto-instrumentation installed");
14581
+ }
14582
+ } catch {
14583
+ }
14584
+ }
14426
14585
  if (features.replay !== false) {
14427
14586
  try {
14428
14587
  __DEV__ && logger.debug("[Bootstrap] Initializing unified SessionManager");
@@ -17065,14 +17224,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
17065
17224
  this._apiBaseUrl = result.config.apiBaseUrl;
17066
17225
  this._maybeStartEventTracker(rawConfig);
17067
17226
  if (this._pendingIdentity) {
17068
- const pending4 = this._pendingIdentity;
17227
+ const pending5 = this._pendingIdentity;
17069
17228
  this._pendingIdentity = null;
17070
- this.identify(pending4.userId, pending4.traits);
17229
+ this.identify(pending5.userId, pending5.traits);
17071
17230
  }
17072
17231
  const realQueue = coreContainer.get("PersistentQueue");
17073
17232
  const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
17074
17233
  if (buffered.length > 0) {
17075
- __DEV__ && logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
17234
+ logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
17076
17235
  for (const envelope of buffered) {
17077
17236
  realQueue.add(envelope);
17078
17237
  }
@@ -17100,17 +17259,11 @@ var ScaleBunFacade = class _ScaleBunFacade {
17100
17259
  /**
17101
17260
  * Boot the OTA orchestrator when the init config asks for it.
17102
17261
  *
17103
- * `publicSigningKey` / `publicSigningKeys` are honoured here so signature
17104
- * enforcement is reachable from configuration alone. The list form exists
17105
- * for key ROTATION: a build pinning [old, new] keeps verifying while the
17106
- * server moves to the new key, so replacing a key never needs an emergency
17107
- * store release. Both fields merge (deduplicated) into one pinned set.
17108
- *
17109
- * Verification comes from `ota.verifySignature` when supplied, else the
17110
- * built-in @noble-based verifier (optional peers). Pinning keys with
17111
- * NEITHER available is fail-CLOSED by design (signature.ts) — an update
17112
- * that cannot be verified is not installed. We say that out loud rather
17113
- * than letting the app discover it as a silent no-update condition.
17262
+ * Signature enforcement is no longer configured from JS. Signed releases are
17263
+ * verified and staged in NATIVE code against RSA keys embedded in the app
17264
+ * package (SB-OTA-RSA-SHA256-V1); unsigned releases keep working on SHA-256
17265
+ * integrity. There are no signing-key or `verifySignature` options to pass
17266
+ * through the host embeds its trusted keys natively.
17114
17267
  */
17115
17268
  _maybeStartOta(rawConfig) {
17116
17269
  const ota = rawConfig?.ota;
@@ -17119,29 +17272,8 @@ var ScaleBunFacade = class _ScaleBunFacade {
17119
17272
  const {
17120
17273
  otaOrchestrator: otaOrchestrator2
17121
17274
  } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
17122
- const rawSingle = ota.publicSigningKey;
17123
- const rawList = ota.publicSigningKeys;
17124
- const publicKeys = Array.from(new Set([...typeof rawSingle === "string" && rawSingle ? [rawSingle] : [], ...Array.isArray(rawList) ? rawList : []].filter((k) => typeof k === "string" && k.trim().length > 0)));
17125
- const signingRequested = publicKeys.length > 0 || Array.isArray(rawList);
17126
- const verifier = ota.verifySignature;
17127
- if (signingRequested && typeof verifier !== "function") {
17128
- const {
17129
- getBuiltinVerifier: getBuiltinVerifier2
17130
- } = (init_builtinVerifier(), __toCommonJS(builtinVerifier_exports));
17131
- if (!getBuiltinVerifier2()) {
17132
- logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
17133
- }
17134
- }
17135
17275
  const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
17136
17276
  otaOrchestrator2.init({
17137
- ...signingRequested ? {
17138
- signature: {
17139
- // Single key stays a plain string — the shape every
17140
- // existing consumer and test already handles.
17141
- publicKey: publicKeys.length === 1 ? publicKeys[0] : publicKeys,
17142
- verifier
17143
- }
17144
- } : {},
17145
17277
  ...healthyAfterMs !== void 0 ? {
17146
17278
  healthyAfterMs
17147
17279
  } : {}
@@ -17190,7 +17322,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
17190
17322
  }
17191
17323
  const clientKey = this._clientKey;
17192
17324
  const apiUrl = this._apiBaseUrl;
17193
- if (!clientKey || !apiUrl) return;
17325
+ if (!clientKey || !apiUrl) {
17326
+ return;
17327
+ }
17194
17328
  this._otaCheckInFlight = true;
17195
17329
  try {
17196
17330
  const info = await bridgeAdapter.getDeviceInfo();
@@ -17241,7 +17375,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17241
17375
  if (!dbgConfig) return;
17242
17376
  dbgConfig.__skipDeprecationWarning = true;
17243
17377
  this.enableDebug(dbgConfig);
17244
- __DEV__ && logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
17378
+ logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
17245
17379
  } catch (err) {
17246
17380
  logger.warn("[ScaleBun] Auto debug connection failed:", err?.message);
17247
17381
  }
@@ -17255,7 +17389,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17255
17389
  const apiBaseUrl = this._apiBaseUrl;
17256
17390
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
17257
17391
  if (enabled && !appId) {
17258
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
17392
+ logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
17259
17393
  }
17260
17394
  return;
17261
17395
  }
@@ -17305,7 +17439,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17305
17439
  onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue)
17306
17440
  });
17307
17441
  this._eventTracker.start();
17308
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
17442
+ logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
17309
17443
  this._applyNativeDeviceContext(this._eventTracker);
17310
17444
  this._captureInstallReferrer(this._eventTracker);
17311
17445
  const tracker = this._eventTracker;
@@ -17939,7 +18073,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17939
18073
  const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
17940
18074
  const messages = cfg?.inAppMessages ?? [];
17941
18075
  const previews = messages.filter((m) => m.preview === true || m.isTest === true);
17942
- __DEV__ && logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
18076
+ logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
17943
18077
  return messages;
17944
18078
  },
17945
18079
  /** Upload a survey/NPS/feedback response (idempotent, rides the outbox). */
@@ -18215,7 +18349,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18215
18349
  logger.warn("SDK not initialized. Event dropped:", name);
18216
18350
  return;
18217
18351
  }
18218
- __DEV__ && logger.debug("Tracking event:", name);
18352
+ logger.debug("Tracking event:", name);
18219
18353
  engageSignals.emit(name, payload);
18220
18354
  const adapter = SessionManager.getExistingInstance()?.getBackendTransport();
18221
18355
  if (!isUploadedSessionEvent(name)) {
@@ -18344,7 +18478,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18344
18478
  await Promise.all([this._eventTracker?.flush() ?? Promise.resolve(), adapter?.flushAnalytics() ?? Promise.resolve()]);
18345
18479
  return;
18346
18480
  }
18347
- __DEV__ && logger.debug("Flushing events from persistent queue...");
18481
+ logger.debug("Flushing events from persistent queue...");
18348
18482
  const httpClient = coreContainer.get("HttpClient");
18349
18483
  await flushQueue(this._getQueue(), httpClient, getDeviceId());
18350
18484
  });
@@ -18366,9 +18500,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
18366
18500
  const bootstrapPerfFeature = getPerformanceFeature();
18367
18501
  if (bootstrapPerfFeature) {
18368
18502
  this._performanceFeature = bootstrapPerfFeature;
18369
- __DEV__ && logger.debug("Performance feature linked from bootstrap");
18503
+ logger.debug("Performance feature linked from bootstrap");
18370
18504
  }
18371
- __DEV__ && logger.info("Debug enabled");
18505
+ logger.info("Debug enabled");
18372
18506
  });
18373
18507
  }
18374
18508
  /** Disable the debug subsystem and clean up */
@@ -18511,14 +18645,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
18511
18645
  return false;
18512
18646
  }
18513
18647
  sm.startSession(metadata);
18514
- __DEV__ && logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
18648
+ logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
18515
18649
  return true;
18516
18650
  },
18517
18651
  end: (reason) => {
18518
18652
  const sm = SessionManager.getExistingInstance();
18519
18653
  if (!sm || !sm.isActive) return false;
18520
18654
  sm.endSession(reason);
18521
- __DEV__ && logger.info("session.end() \u2705");
18655
+ logger.info("session.end() \u2705");
18522
18656
  return true;
18523
18657
  },
18524
18658
  emitEvent: (type, data) => {
@@ -18571,7 +18705,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18571
18705
  ...this._privacyConfig,
18572
18706
  ...config
18573
18707
  };
18574
- __DEV__ && logger.debug("[report] Privacy config updated");
18708
+ logger.debug("[report] Privacy config updated");
18575
18709
  },
18576
18710
  /** Set branding for exported reports. */
18577
18711
  setBranding: (config) => {
@@ -18579,7 +18713,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18579
18713
  ...this._brandingConfig,
18580
18714
  ...config
18581
18715
  };
18582
- __DEV__ && logger.debug("[report] Branding config updated");
18716
+ logger.debug("[report] Branding config updated");
18583
18717
  },
18584
18718
  /** Get current privacy config. */
18585
18719
  getPrivacy: () => ({
@@ -18626,9 +18760,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
18626
18760
  AutoScreenDetector: AutoScreenDetector2
18627
18761
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
18628
18762
  AutoScreenDetector2.getInstance().setNavigationRef(ref);
18629
- __DEV__ && logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
18763
+ logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
18630
18764
  } catch (err) {
18631
- __DEV__ && logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
18765
+ logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
18632
18766
  }
18633
18767
  });
18634
18768
  }
@@ -18935,8 +19069,8 @@ function installEngageTriggerEngine(deps) {
18935
19069
  if (!cachedConfig) return;
18936
19070
  configReady = true;
18937
19071
  const now2 = Date.now();
18938
- const pending4 = recent.splice(0);
18939
- for (const e of pending4) {
19072
+ const pending5 = recent.splice(0);
19073
+ for (const e of pending5) {
18940
19074
  const age = now2 - e.at;
18941
19075
  if (age > RECENT_TTL_MS) continue;
18942
19076
  const remaining = delayMsForTrigger(cachedConfig, e.options.trigger) - age;
@@ -20201,11 +20335,11 @@ var keyOf = (value) => value.trim();
20201
20335
  var validRect = (rect) => [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
20202
20336
  function createEngageAnchorRegistry() {
20203
20337
  const entries = /* @__PURE__ */ new Map();
20204
- const listeners2 = /* @__PURE__ */ new Map();
20338
+ const listeners3 = /* @__PURE__ */ new Map();
20205
20339
  const notify = (rawKey) => {
20206
20340
  const key = keyOf(rawKey);
20207
20341
  if (!key) return;
20208
- listeners2.get(key)?.forEach((listener) => listener());
20342
+ listeners3.get(key)?.forEach((listener) => listener());
20209
20343
  };
20210
20344
  return {
20211
20345
  register(rawKey, node) {
@@ -20247,12 +20381,12 @@ function createEngageAnchorRegistry() {
20247
20381
  const key = keyOf(rawKey);
20248
20382
  if (!key) return () => {
20249
20383
  };
20250
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
20384
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
20251
20385
  set.add(listener);
20252
- listeners2.set(key, set);
20386
+ listeners3.set(key, set);
20253
20387
  return () => {
20254
20388
  set.delete(listener);
20255
- if (set.size === 0) listeners2.delete(key);
20389
+ if (set.size === 0) listeners3.delete(key);
20256
20390
  };
20257
20391
  },
20258
20392
  notify
@@ -21099,11 +21233,11 @@ var normalizedWheelValue = (value) => value.toLowerCase().replace(/\s+/g, " ").t
21099
21233
  function resolveWheelOutcome(segments, source) {
21100
21234
  const identity = wheelOutcomeIdentity(source);
21101
21235
  const index = findWheelOutcomeIndex(segments, identity);
21102
- const resolved3 = index >= 0;
21236
+ const resolved = index >= 0;
21103
21237
  return {
21104
- index: resolved3 ? index : 0,
21105
- label: identity.resultLabel || (resolved3 ? segments[index]?.fullLabel ?? "" : ""),
21106
- resolved: resolved3
21238
+ index: resolved ? index : 0,
21239
+ label: identity.resultLabel || (resolved ? segments[index]?.fullLabel ?? "" : ""),
21240
+ resolved
21107
21241
  };
21108
21242
  }
21109
21243
  function SpinWheelInApp(props) {
@@ -29361,10 +29495,10 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
29361
29495
  var keyOf2 = (value) => value.trim();
29362
29496
  function createEngageInlinePlacementRegistry() {
29363
29497
  const slots = /* @__PURE__ */ new Map();
29364
- const pending4 = /* @__PURE__ */ new Map();
29365
- const listeners2 = /* @__PURE__ */ new Map();
29498
+ const pending5 = /* @__PURE__ */ new Map();
29499
+ const listeners3 = /* @__PURE__ */ new Map();
29366
29500
  const notify = (key) => {
29367
- listeners2.get(key)?.forEach((listener) => listener());
29501
+ listeners3.get(key)?.forEach((listener) => listener());
29368
29502
  };
29369
29503
  return {
29370
29504
  register(rawKey, sink) {
@@ -29381,7 +29515,7 @@ function createEngageInlinePlacementRegistry() {
29381
29515
  sink,
29382
29516
  token
29383
29517
  });
29384
- sink(pending4.get(key) ?? null);
29518
+ sink(pending5.get(key) ?? null);
29385
29519
  if (!previous) notify(key);
29386
29520
  return () => {
29387
29521
  if (slots.get(key)?.token !== token) return;
@@ -29392,7 +29526,7 @@ function createEngageInlinePlacementRegistry() {
29392
29526
  deliver(rawKey, delivery) {
29393
29527
  const key = keyOf2(rawKey);
29394
29528
  if (!key) return false;
29395
- pending4.set(key, delivery);
29529
+ pending5.set(key, delivery);
29396
29530
  const slot = slots.get(key);
29397
29531
  slot?.sink(delivery);
29398
29532
  return slot !== void 0;
@@ -29400,9 +29534,9 @@ function createEngageInlinePlacementRegistry() {
29400
29534
  clear(rawKey, messageId) {
29401
29535
  const key = keyOf2(rawKey);
29402
29536
  if (!key) return;
29403
- const current = pending4.get(key);
29537
+ const current = pending5.get(key);
29404
29538
  if (!current || messageId !== void 0 && current.message?.id !== messageId) return;
29405
- pending4.delete(key);
29539
+ pending5.delete(key);
29406
29540
  slots.get(key)?.sink(null);
29407
29541
  },
29408
29542
  has(rawKey) {
@@ -29413,12 +29547,12 @@ function createEngageInlinePlacementRegistry() {
29413
29547
  const key = keyOf2(rawKey);
29414
29548
  if (!key) return () => {
29415
29549
  };
29416
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
29550
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
29417
29551
  set.add(listener);
29418
- listeners2.set(key, set);
29552
+ listeners3.set(key, set);
29419
29553
  return () => {
29420
29554
  set.delete(listener);
29421
- if (set.size === 0) listeners2.delete(key);
29555
+ if (set.size === 0) listeners3.delete(key);
29422
29556
  };
29423
29557
  }
29424
29558
  };
@@ -29962,8 +30096,8 @@ function EngagePromptProvider({
29962
30096
  __DEV__ && logger.debug("[Engage] no eligible in-app message to render");
29963
30097
  return false;
29964
30098
  }
29965
- const resolved3 = selectInAppVariant(message, getDeviceId());
29966
- activeInAppVariantRef.current = message.experiment?.enabled ? resolved3 : null;
30099
+ const resolved = selectInAppVariant(message, getDeviceId());
30100
+ activeInAppVariantRef.current = message.experiment?.enabled ? resolved : null;
29967
30101
  store.recordImpression(message.id, Date.now(), message.revision);
29968
30102
  setActiveInApp(message);
29969
30103
  return true;
@@ -30207,6 +30341,7 @@ var import_react_native42 = require("react-native");
30207
30341
  // lib/module/features/journey/touchTarget.js
30208
30342
  var MAX_DEPTH = 12;
30209
30343
  var MAX_LEN = 60;
30344
+ var MAX_PATH_SEGMENTS = 4;
30210
30345
  function str2(v) {
30211
30346
  if (typeof v !== "string") return void 0;
30212
30347
  const s = v.trim();
@@ -30238,41 +30373,136 @@ function fiberOf(event) {
30238
30373
  const e = event;
30239
30374
  const direct = e._targetInst;
30240
30375
  if (direct && typeof direct === "object") return direct;
30241
- const target = e.target;
30242
- if (target && typeof target === "object") {
30243
- const handle = target._internalFiberInstanceHandleDEV ?? target._internalInstanceHandle;
30376
+ const target = e.target ?? e.nativeEvent?.target;
30377
+ if (target) {
30378
+ const handle = target._internalInstanceHandle ?? target._targetInst;
30244
30379
  if (handle && typeof handle === "object") return handle;
30245
30380
  }
30246
30381
  return null;
30247
30382
  }
30248
- function resolveTouchTarget(event, maxDepth = MAX_DEPTH) {
30383
+ function resolveTouchTarget(event, optsOrMaxDepth = {}) {
30384
+ const opts = typeof optsOrMaxDepth === "number" ? {
30385
+ maxDepth: optsOrMaxDepth
30386
+ } : optsOrMaxDepth;
30387
+ const maxDepth = opts.maxDepth ?? MAX_DEPTH;
30249
30388
  let f = fiberOf(event);
30250
30389
  if (!f) return null;
30251
30390
  const out = {};
30391
+ const segments = [];
30392
+ const ancestors = [];
30252
30393
  let depth = 0;
30253
30394
  try {
30254
30395
  while (f && depth < maxDepth) {
30255
30396
  const props = f.memoizedProps;
30256
30397
  if (props) {
30257
- out.accessibilityLabel ??= str2(props.accessibilityLabel);
30398
+ const beforeTestId = out.testID;
30258
30399
  out.testID ??= str2(props.testID);
30259
- out.text ??= textChild(props);
30400
+ if (!beforeTestId && out.testID) out.node = f.stateNode;
30401
+ if (typeof props.cellKey === "string" && props.cellKey) {
30402
+ out.itemKey ??= str2(props.cellKey);
30403
+ if (typeof props.index === "number" && Number.isFinite(props.index)) {
30404
+ out.itemIndex ??= props.index;
30405
+ }
30406
+ }
30407
+ if (opts.includeText) {
30408
+ out.accessibilityLabel ??= str2(props.accessibilityLabel);
30409
+ out.text ??= textChild(props);
30410
+ }
30411
+ }
30412
+ if (f.stateNode) ancestors.push(f.stateNode);
30413
+ const name = componentName(f);
30414
+ if (name) {
30415
+ out.component ??= name;
30416
+ if (segments[segments.length - 1] !== name && segments.length < MAX_PATH_SEGMENTS) {
30417
+ segments.push(name);
30418
+ }
30260
30419
  }
30261
- out.component ??= componentName(f);
30262
- if (out.accessibilityLabel && out.testID) break;
30420
+ if (out.testID && segments.length >= MAX_PATH_SEGMENTS) break;
30263
30421
  f = f.return ?? null;
30264
30422
  depth++;
30265
30423
  }
30266
30424
  } catch {
30267
30425
  }
30268
- return out.accessibilityLabel || out.testID || out.text || out.component ? out : null;
30426
+ if (segments.length) out.path = segments.join("<");
30427
+ if (!out.node) out.node = fiberOf(event)?.stateNode;
30428
+ if (ancestors.length) out.ancestors = ancestors;
30429
+ return out.testID || out.path || out.component || out.accessibilityLabel || out.text || out.itemKey ? out : null;
30269
30430
  }
30270
30431
  function describeTouchTarget(t) {
30271
30432
  if (!t) return void 0;
30272
- return t.accessibilityLabel || t.testID || t.text || t.component;
30433
+ return t.testID || t.path || t.component;
30434
+ }
30435
+ function targetIdentity(t) {
30436
+ if (!t) return {
30437
+ source: "none"
30438
+ };
30439
+ if (t.testID) return {
30440
+ id: t.testID,
30441
+ source: "testID"
30442
+ };
30443
+ if (t.path) return {
30444
+ id: t.path,
30445
+ source: "path"
30446
+ };
30447
+ if (t.component) return {
30448
+ id: t.component,
30449
+ source: "component"
30450
+ };
30451
+ return {
30452
+ source: "none"
30453
+ };
30454
+ }
30455
+
30456
+ // lib/module/features/journey/targetGeometry.js
30457
+ function readRectSync(node) {
30458
+ if (!node || typeof node !== "object") return null;
30459
+ const n = node;
30460
+ const fn = typeof n.unstable_getBoundingClientRect === "function" ? n.unstable_getBoundingClientRect : typeof n.getBoundingClientRect === "function" ? n.getBoundingClientRect : null;
30461
+ if (!fn) return null;
30462
+ try {
30463
+ const r = fn.call(n);
30464
+ if (!r) return null;
30465
+ const {
30466
+ x,
30467
+ y,
30468
+ width,
30469
+ height
30470
+ } = r;
30471
+ if (![x, y, width, height].every((v) => typeof v === "number" && Number.isFinite(v))) return null;
30472
+ if (width <= 0 || height <= 0) return null;
30473
+ return {
30474
+ x,
30475
+ y,
30476
+ width,
30477
+ height
30478
+ };
30479
+ } catch {
30480
+ return null;
30481
+ }
30482
+ }
30483
+ function normalizeRect(rect, base) {
30484
+ if (!rect || !base || base.w <= 0 || base.h <= 0) return null;
30485
+ const out = {
30486
+ x: rect.x / base.w,
30487
+ y: rect.y / base.h,
30488
+ width: rect.width / base.w,
30489
+ height: rect.height / base.h
30490
+ };
30491
+ if (![out.x, out.y, out.width, out.height].every((v) => Number.isFinite(v))) return null;
30492
+ return out;
30493
+ }
30494
+ function localPoint(rect, nx, ny) {
30495
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
30496
+ if (typeof nx !== "number" || typeof ny !== "number") return null;
30497
+ if (!Number.isFinite(nx) || !Number.isFinite(ny)) return null;
30498
+ return {
30499
+ u: (nx - rect.x) / rect.width,
30500
+ v: (ny - rect.y) / rect.height
30501
+ };
30273
30502
  }
30274
30503
 
30275
30504
  // lib/module/features/journey/ScaleBunDebugRoot.js
30505
+ init_scrollContext();
30276
30506
  init_device();
30277
30507
  init_interactionProtocol();
30278
30508
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -30284,6 +30514,7 @@ function getBootstrap() {
30284
30514
  return _bootstrap;
30285
30515
  }
30286
30516
  var JourneyContext = /* @__PURE__ */ (0, import_react9.createContext)(null);
30517
+ var UI_STATE_CAPTURE_MIN_INTERVAL_MS = 1e3;
30287
30518
  function ScaleBunDebugRoot({
30288
30519
  children,
30289
30520
  navigationRef,
@@ -30462,6 +30693,28 @@ function ScaleBunDebugRoot({
30462
30693
  clearTimeout(timer);
30463
30694
  };
30464
30695
  }, [navigationRef]);
30696
+ (0, import_react9.useEffect)(() => {
30697
+ let lastCaptureTs = 0;
30698
+ const {
30699
+ onUiStateChange: onUiStateChange2
30700
+ } = (init_uiState(), __toCommonJS(uiState_exports));
30701
+ const unsubscribe = onUiStateChange2(() => {
30702
+ const now2 = Date.now();
30703
+ if (now2 - lastCaptureTs < UI_STATE_CAPTURE_MIN_INTERVAL_MS) return;
30704
+ lastCaptureTs = now2;
30705
+ try {
30706
+ const {
30707
+ SessionManager: SessionManager2
30708
+ } = (init_SessionManager(), __toCommonJS(SessionManager_exports));
30709
+ const sm = SessionManager2.getExistingInstance();
30710
+ if (sm && typeof sm.captureManualFrame === "function") {
30711
+ void sm.captureManualFrame();
30712
+ }
30713
+ } catch {
30714
+ }
30715
+ });
30716
+ return unsubscribe;
30717
+ }, []);
30465
30718
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
30466
30719
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
30467
30720
  const touchStartRef = (0, import_react9.useRef)(null);
@@ -30478,10 +30731,15 @@ function ScaleBunDebugRoot({
30478
30731
  lastNativeTouchTsRef.current = Date.now();
30479
30732
  nativeTrackingConfirmedRef.current = true;
30480
30733
  const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
30481
- const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
30734
+ pruneInteractionStarts(interactionStartsRef.current, nativeOccurredAt);
30735
+ const correlation = correlateInteraction(interactionStartsRef.current, nativeOccurredAt);
30736
+ const start = correlation.start;
30737
+ if (correlation.index >= 0) {
30738
+ interactionStartsRef.current.splice(correlation.index, 1);
30739
+ }
30482
30740
  if (start) {
30483
- const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
30484
- if (pending4 !== void 0) clearTimeout(pending4);
30741
+ const pending5 = pendingJsEmitsRef.current.get(start.interactionId);
30742
+ if (pending5 !== void 0) clearTimeout(pending5);
30485
30743
  pendingJsEmitsRef.current.delete(start.interactionId);
30486
30744
  }
30487
30745
  const {
@@ -30504,11 +30762,62 @@ function ScaleBunDebugRoot({
30504
30762
  interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
30505
30763
  interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
30506
30764
  occurredAt: nativeOccurredAt,
30765
+ /**
30766
+ * RN-6 — THESE FIVE FIELDS ARE THE ONLY ONES THE JOIN CARRIES, AND
30767
+ * THEREFORE THE ONLY ONES A BAD JOIN CAN FALSIFY.
30768
+ *
30769
+ * Coordinates below come from native and are right regardless. So a
30770
+ * mispairing does not produce visibly broken data: it produces heat
30771
+ * in the correct position attributed to the wrong control, in the
30772
+ * wrong UI state, on the wrong screen. Every field is individually
30773
+ * well-formed and nothing downstream can tell.
30774
+ *
30775
+ * When the correlation is not 'exact' they are left UNDEFINED rather
30776
+ * than filled from a best guess. An interaction with no target is
30777
+ * honest and visibly incomplete — the accounting lane can count it as
30778
+ * unplaceable and say why. An interaction with the WRONG target is
30779
+ * indistinguishable from a correct one and silently corrupts every
30780
+ * aggregate it enters. `null` is not `0`, and a guess is not a
30781
+ * measurement.
30782
+ */
30507
30783
  ui: start?.ui,
30508
30784
  stateStatus: start?.stateStatus ?? "not_captured",
30509
30785
  target: start?.target,
30510
30786
  targetId: start?.targetId,
30787
+ targetSource: start?.targetSource,
30511
30788
  screenName: start?.screenName,
30789
+ screenSource: start?.screenSource ?? "none",
30790
+ targetRect: start?.targetRect,
30791
+ /**
30792
+ * RN-15 — WHERE IN THE CONTROL, computed where both halves exist: the
30793
+ * point comes from the native event and the rect was measured at
30794
+ * finger-down, in the same capture base. Pairing them here rather than
30795
+ * downstream stops anything later reaching for a different rect.
30796
+ */
30797
+ ...(() => {
30798
+ const lp = localPoint(start?.targetRect ?? null, event.normalizedX, event.normalizedY);
30799
+ return lp ? {
30800
+ localU: lp.u,
30801
+ localV: lp.v
30802
+ } : {};
30803
+ })(),
30804
+ scrollX: start?.scrollX,
30805
+ scrollY: start?.scrollY,
30806
+ scrollSource: start?.scrollSource ?? "none",
30807
+ itemKey: start?.itemKey,
30808
+ itemIndex: start?.itemIndex,
30809
+ /**
30810
+ * The join's own verdict, shipped with the row so it is auditable
30811
+ * rather than a claim made in a comment. 'ambiguous' and 'none' are
30812
+ * deliberately distinguished: 'none' means the JS handler never fired
30813
+ * (a real capture gap on this platform), 'ambiguous' means it fired
30814
+ * more than once too close together to attribute. Those have
30815
+ * different fixes, and collapsing them would hide which one is
30816
+ * actually happening in production.
30817
+ */
30818
+ correlation: correlation.outcome,
30819
+ correlationDeltaMs: correlation.deltaMs,
30820
+ correlationRunnerUpDeltaMs: correlation.runnerUpDeltaMs,
30512
30821
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30513
30822
  x: event.rawX,
30514
30823
  y: event.rawY,
@@ -30580,6 +30889,7 @@ function ScaleBunDebugRoot({
30580
30889
  const target = resolveTouchTarget(e);
30581
30890
  let ui;
30582
30891
  let screenName;
30892
+ let screenSource = "none";
30583
30893
  try {
30584
30894
  const {
30585
30895
  uiStateSignature: uiStateSignature2
@@ -30591,7 +30901,9 @@ function ScaleBunDebugRoot({
30591
30901
  const {
30592
30902
  AutoScreenDetector: AutoScreenDetector2
30593
30903
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30594
- screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30904
+ const detector = AutoScreenDetector2.getInstance();
30905
+ screenName = detector.getCurrentScreen() || void 0;
30906
+ screenSource = screenName ? detector.getCurrentScreenSource() : "none";
30595
30907
  } catch {
30596
30908
  }
30597
30909
  const start = {
@@ -30601,9 +30913,91 @@ function ScaleBunDebugRoot({
30601
30913
  y: touch.pageY,
30602
30914
  locationX: touch.locationX,
30603
30915
  locationY: touch.locationY,
30916
+ /**
30917
+ * RN-1 + RN-2 — the identity that is persisted, and it is now code-derived.
30918
+ *
30919
+ * `describeTouchTarget` returns `testID` -> structural path -> component name,
30920
+ * and never a caption or an accessibilityLabel. Before RN-2 it preferred free
30921
+ * text, so this line was writing user data ("Pay EGP 1,442", an order row
30922
+ * labelled with an email address, a contact's name) into the analytics store —
30923
+ * and, because `target` is an aggregation key, splitting one button into one
30924
+ * bucket per person at the same time.
30925
+ *
30926
+ * `targetId` used to be `target?.testID` alone, which is `undefined` in the
30927
+ * overwhelming majority of apps — no testIDs authored — so the field the
30928
+ * backend keys on was empty almost always. It now falls back to the structural
30929
+ * path, with `targetSource` recording WHICH it got.
30930
+ */
30604
30931
  target: describeTouchTarget(target),
30605
- targetId: target?.testID,
30932
+ targetId: targetIdentity(target).id,
30933
+ /**
30934
+ * WHICH KIND of identity this is, because they are not equally trustworthy: a
30935
+ * `testID` is a promise from the app's authors that this control is this
30936
+ * control; a `path` is an inference that breaks when the tree is refactored.
30937
+ * Without this the accounting lane cannot distinguish "the control had no
30938
+ * identity" from "the identity is a structural guess", and both would render
30939
+ * as an equally confident target.
30940
+ */
30941
+ targetSource: targetIdentity(target).source,
30942
+ /* RN-4 — row identity from the app's own keyExtractor; see touchTarget.ts. */
30943
+ itemKey: target?.itemKey,
30944
+ itemIndex: target?.itemIndex,
30945
+ /**
30946
+ * RN-13 — the control's rectangle, so `heat ∈ target` is decidable.
30947
+ *
30948
+ * Read SYNCHRONOUSLY here, in the same tick as the touch, and normalized against
30949
+ * the SAME measured root the tap's own coordinates use. Both halves matter:
30950
+ *
30951
+ * - A callback measurement (`measureInWindow`) would resolve after this handler
30952
+ * returns, so the rect attached to a tap could be the previous tap's, with
30953
+ * nothing afterwards able to tell. `targetGeometry` refuses to guess: it reads
30954
+ * now or reports null.
30955
+ * - Normalizing against a window size instead of the root view is off by the
30956
+ * status bar on most devices, which would read as "the heat is just outside
30957
+ * the button" everywhere rather than as a measurement bug.
30958
+ *
30959
+ * `measureRoot()` is called first because the cached root rect is populated
30960
+ * asynchronously at mount; without it the first taps of a session normalize
30961
+ * against nothing and get a null rect for a reason that has nothing to do with
30962
+ * the control.
30963
+ */
30964
+ /**
30965
+ * RN-3 — WHERE IN THE CONTENT, not just where on the glass.
30966
+ *
30967
+ * Two taps at the same screen coordinate one viewport apart are two different
30968
+ * controls. Without an offset they merge into one hotspot, and the heatmap shows
30969
+ * heavy engagement at a position where, in content terms, nothing in particular
30970
+ * is. The offset is also a CACHE dimension — two scroll states of one route must
30971
+ * not share a backdrop — so its absence corrupts the picture as well as the count.
30972
+ *
30973
+ * `ancestors` is innermost-first, so this takes the NEAREST scroller that has a
30974
+ * recorded position: a tap inside a horizontal carousel belongs to the carousel's
30975
+ * content, not to how far the page behind it had scrolled.
30976
+ *
30977
+ * Null when no ancestor has one — never zero. Zero is a real scroll position (the
30978
+ * top of a list), and reporting it for "unknown" would place every unmeasured tap
30979
+ * at the top of its content, indistinguishable from a genuine tap there.
30980
+ */
30981
+ ...(() => {
30982
+ const hit = nearestRecordedOffset((target?.ancestors ?? []).map((n) => nativeTagOf(n)));
30983
+ return hit ? {
30984
+ scrollX: hit.x,
30985
+ scrollY: hit.y,
30986
+ scrollSource: "wrapper"
30987
+ } : {
30988
+ scrollSource: "none"
30989
+ };
30990
+ })(),
30991
+ targetRect: (() => {
30992
+ measureRoot();
30993
+ const root = rootRectRef.current;
30994
+ return normalizeRect(readRectSync(target?.node), root ? {
30995
+ w: root.w,
30996
+ h: root.h
30997
+ } : null) ?? void 0;
30998
+ })(),
30606
30999
  screenName,
31000
+ screenSource,
30607
31001
  ui,
30608
31002
  stateStatus: ui ? "captured_nonempty" : "not_instrumented",
30609
31003
  emitAutomaticAnalytics: captureAutomaticInteractions
@@ -30640,7 +31034,12 @@ function ScaleBunDebugRoot({
30640
31034
  }
30641
31035
  measureRoot();
30642
31036
  const rootRect = rootRectRef.current;
30643
- const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
31037
+ const freshTarget = start ? null : resolveTouchTarget(e);
31038
+ const tapped = start?.target ?? describeTouchTarget(freshTarget);
31039
+ const identity = start ? {
31040
+ id: start.targetId,
31041
+ source: start.targetSource
31042
+ } : targetIdentity(freshTarget);
30644
31043
  const gestureDetails = {
30645
31044
  target: tapped,
30646
31045
  interactionId: start?.interactionId ?? generateInteractionId(),
@@ -30648,8 +31047,16 @@ function ScaleBunDebugRoot({
30648
31047
  occurredAt: start?.occurredAt ?? Date.now(),
30649
31048
  ui: start?.ui,
30650
31049
  stateStatus: start?.stateStatus ?? "not_captured",
30651
- targetId: start?.targetId,
31050
+ targetId: identity.id,
31051
+ targetSource: identity.source,
30652
31052
  screenName: start?.screenName,
31053
+ screenSource: start?.screenSource ?? "none",
31054
+ targetRect: start?.targetRect,
31055
+ scrollX: start?.scrollX,
31056
+ scrollY: start?.scrollY,
31057
+ scrollSource: start?.scrollSource ?? "none",
31058
+ itemKey: start?.itemKey,
31059
+ itemIndex: start?.itemIndex,
30653
31060
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30654
31061
  x: start?.x ?? touch.pageX,
30655
31062
  y: start?.y ?? touch.pageY,
@@ -30665,6 +31072,11 @@ function ScaleBunDebugRoot({
30665
31072
  const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
30666
31073
  gestureDetails.normalizedX = nx;
30667
31074
  gestureDetails.normalizedY = ny;
31075
+ const lp = localPoint(start?.targetRect ?? null, nx, ny);
31076
+ if (lp) {
31077
+ gestureDetails.localU = lp.u;
31078
+ gestureDetails.localV = lp.v;
31079
+ }
30668
31080
  gestureDetails.normalizedPrecomputed = true;
30669
31081
  gestureDetails.viewportWidth = rootRect.w;
30670
31082
  gestureDetails.viewportHeight = rootRect.h;
@@ -30793,6 +31205,13 @@ function useScaleBunScreen(name) {
30793
31205
  }
30794
31206
  } catch {
30795
31207
  }
31208
+ try {
31209
+ const {
31210
+ AutoScreenDetector: AutoScreenDetector2
31211
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
31212
+ AutoScreenDetector2.getInstance().setManualScreen(name);
31213
+ } catch {
31214
+ }
30796
31215
  try {
30797
31216
  const bs = getBootstrap();
30798
31217
  const perfFeature = bs?.getPerformanceFeature?.();
@@ -30943,6 +31362,18 @@ var ScaleBunScrollView = /* @__PURE__ */ import_react12.default.forwardRef(funct
30943
31362
  }
30944
31363
  } catch {
30945
31364
  }
31365
+ try {
31366
+ const {
31367
+ recordScrollOffset: recordScrollOffset2,
31368
+ nativeTagOf: nativeTagOf2
31369
+ } = (init_scrollContext(), __toCommonJS(scrollContext_exports));
31370
+ const {
31371
+ contentOffset,
31372
+ target
31373
+ } = event.nativeEvent;
31374
+ recordScrollOffset2(typeof target === "number" ? target : nativeTagOf2(target), contentOffset.x, contentOffset.y);
31375
+ } catch {
31376
+ }
30946
31377
  if (userOnScroll) {
30947
31378
  userOnScroll(event);
30948
31379
  }