@scalebun/react-native 2.0.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 (70) hide show
  1. package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
  2. package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +18 -3
  4. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +62 -1
  5. package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
  6. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +2 -0
  7. package/dist/scalebun.full.js +840 -114
  8. package/dist/scalebun.slim.js +838 -112
  9. package/ios/ReplaySdk.swift +65 -2
  10. package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
  11. package/lib/commonjs/core/config/schema.js +16 -0
  12. package/lib/commonjs/core/constants/version.js +1 -1
  13. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
  14. package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
  15. package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
  16. package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
  17. package/lib/commonjs/features/journey/scrollContext.js +146 -0
  18. package/lib/commonjs/features/journey/targetGeometry.js +164 -0
  19. package/lib/commonjs/features/journey/touchTarget.js +152 -24
  20. package/lib/commonjs/features/journey/uiState.js +81 -1
  21. package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
  22. package/lib/commonjs/features/ota/OtaOrchestrator.js +69 -22
  23. package/lib/commonjs/features/session/SessionManager.js +63 -0
  24. package/lib/commonjs/public/ScaleBunFacade.js +18 -16
  25. package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
  26. package/lib/module/core/config/schema.js +16 -0
  27. package/lib/module/core/constants/version.js +1 -1
  28. package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
  29. package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
  30. package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
  31. package/lib/module/features/journey/interactionProtocol.js +128 -8
  32. package/lib/module/features/journey/scrollContext.js +135 -0
  33. package/lib/module/features/journey/targetGeometry.js +155 -0
  34. package/lib/module/features/journey/touchTarget.js +151 -24
  35. package/lib/module/features/journey/uiState.js +78 -1
  36. package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
  37. package/lib/module/features/ota/OtaOrchestrator.js +69 -22
  38. package/lib/module/features/session/SessionManager.js +63 -0
  39. package/lib/module/public/ScaleBunFacade.js +18 -16
  40. package/lib/typescript/core/config/schema.d.ts +2 -0
  41. package/lib/typescript/core/constants/version.d.ts +1 -1
  42. package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
  43. package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
  44. package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
  45. package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
  46. package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
  47. package/lib/typescript/features/journey/uiState.d.ts +39 -0
  48. package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
  49. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +7 -0
  50. package/lib/typescript/features/session/SessionManager.d.ts +55 -0
  51. package/lib/typescript/public/types.d.ts +10 -0
  52. package/lib/typescript/specs/NativeScaleBunOta.d.ts +2 -0
  53. package/package.json +2 -2
  54. package/src/bootstrap/SDKBootstrapper.ts +40 -0
  55. package/src/core/config/schema.ts +18 -0
  56. package/src/core/constants/version.ts +1 -1
  57. package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
  58. package/src/features/journey/ScaleBunScrollView.tsx +29 -0
  59. package/src/features/journey/autoInstrumentScroll.ts +155 -0
  60. package/src/features/journey/interactionProtocol.ts +188 -10
  61. package/src/features/journey/scrollContext.ts +137 -0
  62. package/src/features/journey/targetGeometry.ts +176 -0
  63. package/src/features/journey/touchTarget.ts +237 -26
  64. package/src/features/journey/uiState.ts +80 -1
  65. package/src/features/navigation/AutoScreenDetector.ts +65 -5
  66. package/src/features/ota/OtaOrchestrator.ts +76 -22
  67. package/src/features/session/SessionManager.ts +113 -0
  68. package/src/public/ScaleBunFacade.ts +18 -16
  69. package/src/public/types.ts +10 -0
  70. package/src/specs/NativeScaleBunOta.ts +3 -0
@@ -30,6 +30,15 @@ export declare class AutoScreenDetector {
30
30
  private _manualScreen;
31
31
  private _manualScreenTs;
32
32
  private _autoScreen;
33
+ /**
34
+ * When `_autoScreen` was last written.
35
+ *
36
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
37
+ * Without a timestamp the only comparison available was "is the manual value still
38
+ * fresh", which is a different question and gave the wrong answer — see the note on
39
+ * `_currentScreen()`.
40
+ */
41
+ private _autoScreenTs;
33
42
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
34
43
  private _autoVisitSig;
35
44
  private _navigationRef;
@@ -72,8 +81,19 @@ export declare class AutoScreenDetector {
72
81
  */
73
82
  setNativeScreen(name: string): void;
74
83
  /**
75
- * Check if a manual screen is currently active.
76
- * Used by auto-detection to yield to manual overrides.
84
+ * Should a WRITER defer to the manual screen right now?
85
+ *
86
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
87
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
88
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
89
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
90
+ *
91
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
92
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
93
+ * `setManualScreen` in production, so the effect was latent — but the moment the
94
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
95
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
96
+ * fix that introduced that would have been worse than the gap it closed.
77
97
  */
78
98
  isManualScreenActive(): boolean;
79
99
  /**
@@ -144,6 +164,32 @@ export declare class AutoScreenDetector {
144
164
  teardown(): void;
145
165
  /** Get the current best-known screen name */
146
166
  getCurrentScreen(): string | null;
167
+ /**
168
+ * WHERE the current screen name came from, or 'none'.
169
+ *
170
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
171
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
172
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
173
+ * causes: the app never instrumented anything, the app instrumented manually and the
174
+ * value never reached interactions, or auto-detection ran and failed. Those have
175
+ * different owners and different fixes, and guessing between them is how a P0 stays
176
+ * open.
177
+ */
178
+ getCurrentScreenSource(): 'manual' | 'navigation' | 'none';
179
+ /**
180
+ * LAST WRITER WINS between the manual beacon and auto-detection.
181
+ *
182
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
183
+ * declares a different one — that is what mounting a screen means. It does not
184
+ * expire, which is the correction here: the previous rule returned the manual name
185
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
186
+ * which in a manually-instrumented app is `null`.
187
+ *
188
+ * Comparing timestamps rather than hard-coding a priority also handles the real
189
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
190
+ * beacon on one modal, and whichever spoke last is the truth.
191
+ */
192
+ private _currentScreen;
147
193
  private _emitScreenChange;
148
194
  }
149
195
  export {};
@@ -34,6 +34,8 @@ export declare class OtaOrchestrator {
34
34
  */
35
35
  getCurrentBundle(): OtaBundlePayload | null;
36
36
  private isRestartRequiredState;
37
+ /** Root/jailbreak indicators observed at init; empty when none found. */
38
+ private integrityIndicators;
37
39
  private healthyTimer;
38
40
  private bootGuardConfig;
39
41
  private environment;
@@ -97,6 +99,11 @@ export declare class OtaOrchestrator {
97
99
  */
98
100
  private checkBootGuardRecovery;
99
101
  isEnabled(): boolean;
102
+ /**
103
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
104
+ * Empty means nothing was detected — not proof the device is clean.
105
+ */
106
+ deviceIntegrityIndicators(): string[];
100
107
  isRestartRequired(): boolean;
101
108
  /**
102
109
  * Perform an OTA check against the backend endpoint.
@@ -268,6 +268,61 @@ export declare class SessionManager {
268
268
  targetId?: string;
269
269
  screenName?: string;
270
270
  emitAutomaticAnalytics?: boolean;
271
+ /**
272
+ * RN-6 — the native↔JS join's own verdict, and it MUST be declared here.
273
+ *
274
+ * This signature ENUMERATES its fields rather than spreading, and the only caller
275
+ * (ScaleBunDebugRoot's native listener) invokes it through `(sm as any)`. So a field
276
+ * added at the call site and not added here is dropped in silence, with no type
277
+ * error anywhere — which is precisely how these three nearly shipped as a no-op.
278
+ *
279
+ * 'exact' = one JS start was clearly closest. 'ambiguous' = two were within
280
+ * CORRELATION_MARGIN_MS of each other, so target/ui/screenName were deliberately
281
+ * withheld rather than guessed. 'none' = no start within tolerance, meaning the JS
282
+ * handler never fired. The last two are kept distinct because they have different
283
+ * fixes and collapsing them would hide which is happening in production.
284
+ */
285
+ correlation?: 'exact' | 'ambiguous' | 'none';
286
+ correlationDeltaMs?: number | null;
287
+ correlationRunnerUpDeltaMs?: number | null;
288
+ /**
289
+ * RN-1/RN-2 — which kind of identity `targetId` is. Declared here for the same reason as
290
+ * the correlation fields above: this signature enumerates, and its only caller goes
291
+ * through `(sm as any)`, so an undeclared field is dropped in silence.
292
+ */
293
+ targetSource?: 'testID' | 'path' | 'component' | 'none';
294
+ /** RN-8: where the screen name came from. Declared here because this signature enumerates. */
295
+ screenSource?: 'manual' | 'navigation' | 'none';
296
+ /**
297
+ * RN-13: the target control's rectangle in capture-base fractions. Declared here because
298
+ * this signature ENUMERATES — the sixth field to need it, after three correlation fields,
299
+ * targetSource and screenSource.
300
+ */
301
+ targetRect?: {
302
+ x: number;
303
+ y: number;
304
+ width: number;
305
+ height: number;
306
+ };
307
+ /**
308
+ * RN-3: content offset of the nearest ancestor scroller. Declared here because this
309
+ * signature ENUMERATES — the seventh field to need it.
310
+ */
311
+ scrollX?: number;
312
+ scrollY?: number;
313
+ scrollSource?: 'wrapper' | 'none';
314
+ /**
315
+ * RN-4: list row identity. Declared here because this signature ENUMERATES — the eighth
316
+ * and ninth fields to need it.
317
+ */
318
+ itemKey?: string;
319
+ itemIndex?: number;
320
+ /**
321
+ * RN-15: tap position within the target control. Declared here because this signature
322
+ * ENUMERATES — the tenth and eleventh fields to need it.
323
+ */
324
+ localU?: number;
325
+ localV?: number;
271
326
  }): void;
272
327
  /**
273
328
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -287,6 +287,16 @@ export interface SimplifiedInitConfig {
287
287
  appId?: string;
288
288
  /** SDK client key (SaaS mode). Also used to authenticate the envelope lane. */
289
289
  clientKey?: string;
290
+ /**
291
+ * Patch React Native's ScrollView export so every scroll view reports its scroll offset,
292
+ * giving taps their content depth without the app swapping to ScaleBunScrollView.
293
+ *
294
+ * OFF BY DEFAULT because it changes what the host renders, not merely what the SDK records:
295
+ * every scroll view in the app — including ones inside libraries you did not write — routes
296
+ * through an SDK wrapper. Turn it on deliberately. With it off, taps on a plain ScrollView
297
+ * report no content depth and say so, rather than reporting a fabricated offset of 0.
298
+ */
299
+ autoInstrumentScrollViews?: boolean;
290
300
  /** Platform OVERRIDE — auto-detected from Platform.OS when omitted (integrations should omit it). */
291
301
  platform?: 'ios' | 'android' | 'web' | 'react_native';
292
302
  /**
@@ -30,6 +30,8 @@ export interface Spec extends TurboModule {
30
30
  * Empty array when no keys are embedded.
31
31
  */
32
32
  getTrustedKeyIds(): string;
33
+ /** JSON array of root/jailbreak indicator strings; [] when none found. */
34
+ getDeviceIntegrity(): string;
33
35
  /**
34
36
  * Returns JSON with current slot state:
35
37
  * { current: { bundleId, version, sha256, installedAt } | null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalebun/react-native",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "React Native SDK for ScaleBun",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -111,7 +111,7 @@
111
111
  "@babel/runtime": "^7.25.0",
112
112
  "@jridgewell/sourcemap-codec": "1.5.5",
113
113
  "@jridgewell/trace-mapping": "0.3.31",
114
- "@scalebun/cli": "^2.0.0"
114
+ "@scalebun/cli": "^2.0.1"
115
115
  },
116
116
  "codegenConfig": {
117
117
  "name": "ScaleBunSpec",
@@ -91,6 +91,24 @@ export class SDKBootstrapper {
91
91
  // apiBaseUrl is LOCKED: whatever an integrator passed (or the schema default) is
92
92
  // discarded here so the SDK can never be repointed away from the ScaleBun cloud.
93
93
  // Change DEFAULT_API_BASE_URL at build time to target a different backend.
94
+ // Say so when we discard it. The value was accepted by the schema, silently
95
+ // overwritten here, and the SDK then talked to a different server than the
96
+ // integrator configured — with no log, no warning, and no error. Someone
97
+ // pointing the SDK at a staging or local backend would watch requests arrive
98
+ // at production and have nothing to go on.
99
+ const requestedApiBaseUrl = (rawConfig as any)?.apiBaseUrl;
100
+ if (
101
+ typeof requestedApiBaseUrl === 'string' &&
102
+ requestedApiBaseUrl &&
103
+ requestedApiBaseUrl !== DEFAULT_API_BASE_URL
104
+ ) {
105
+ logger.warn(
106
+ `[ScaleBun] Ignoring apiBaseUrl '${requestedApiBaseUrl}' — the endpoint is ` +
107
+ `locked at build time and this build targets ${DEFAULT_API_BASE_URL}. To ` +
108
+ `target another backend, change DEFAULT_API_BASE_URL in the SDK source and ` +
109
+ `rebuild.`,
110
+ );
111
+ }
94
112
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
95
113
  // Explicit logLevel wins; otherwise debug:true implies verbose 'debug'.
96
114
  if (this.config.logLevel) {
@@ -340,6 +358,28 @@ export class SDKBootstrapper {
340
358
  await this.featureRegistry.initializeAll(featureContext);
341
359
 
342
360
  // ── Session Replay Initialization (Core) ───────────────
361
+ /**
362
+ * RN-3 zero-config scroll instrumentation, opt-in.
363
+ *
364
+ * Placed before replay init because it patches a module export and must be in place before the
365
+ * host renders its first scroll view. Reports rather than assumes: the equivalent React
366
+ * Navigation hook once claimed success it had not verified and left a whole pipeline silently
367
+ * empty, so a failure here is said out loud with a reason an integrator can act on.
368
+ */
369
+ if (this.config.autoInstrumentScrollViews === true) {
370
+ try {
371
+ const { autoInstrumentScrollViews } = require('../features/journey/autoInstrumentScroll');
372
+ const outcome = autoInstrumentScrollViews();
373
+ if (!outcome.installed) {
374
+ logger.warn(
375
+ '[Bootstrap] autoInstrumentScrollViews was requested but could not be ' +
376
+ 'installed: ' + outcome.reason + ' — taps will carry no content depth.',
377
+ );
378
+ } else {
379
+ __DEV__ && logger.debug('[Bootstrap] ScrollView auto-instrumentation installed');
380
+ }
381
+ } catch { /* no-throw */ }
382
+ }
343
383
  if (features.replay !== false) {
344
384
  try {
345
385
  __DEV__ && logger.debug('[Bootstrap] Initializing unified SessionManager');
@@ -188,6 +188,22 @@ const SHAPE: Record<string, Field> = {
188
188
  },
189
189
  },
190
190
  captureInteractionHeatmap: bool(true),
191
+ /**
192
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
193
+ * offset, not just ones wrapped in ScaleBunScrollView.
194
+ *
195
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
196
+ * That flag changes what the SDK records about the host; this one changes what the HOST
197
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
198
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
199
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
200
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
201
+ * discover afterwards.
202
+ *
203
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
204
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
205
+ */
206
+ autoInstrumentScrollViews: bool(false),
191
207
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
192
208
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
193
209
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -396,6 +412,8 @@ export interface ScaleBunConfig {
396
412
  redactBodies?: boolean;
397
413
  };
398
414
  captureInteractionHeatmap: boolean;
415
+ /** RN-3 zero-config scroll instrumentation. OFF by default — it changes what the HOST renders. */
416
+ autoInstrumentScrollViews: boolean;
399
417
  /** Opt-in: capture console.* into the Logs lane in SaaS mode. Default false. */
400
418
  captureConsoleLogs: boolean;
401
419
  ota?: {
@@ -8,4 +8,4 @@
8
8
  * value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
9
9
  * version was introduced to solve.
10
10
  */
11
- export const SDK_VERSION = '2.0.0';
11
+ export const SDK_VERSION = '2.0.1';