@scalebun/react-native 1.10.7 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
- package/dist/scalebun.full.js +467 -255
- package/dist/scalebun.slim.js +466 -254
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/ios/Ota/OtaSlotManager.swift +19 -5
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +8 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
- package/lib/commonjs/features/ota/OtaTypes.js +4 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/commonjs/public/ScaleBunFacade.js +115 -2
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +8 -1
- package/lib/module/features/ota/OtaOrchestrator.js +174 -48
- package/lib/module/features/ota/OtaTypes.js +1 -1
- package/lib/module/features/ota/useOtaUpdate.js +11 -2
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/module/public/ScaleBunFacade.js +115 -2
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
- package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
- package/lib/typescript/features/session/SessionManager.d.ts +15 -10
- package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
- package/package.json +4 -3
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +9 -4
- package/src/features/ota/OtaEventEmitter.ts +12 -0
- package/src/features/ota/OtaOrchestrator.ts +209 -62
- package/src/features/ota/OtaTypes.ts +37 -39
- package/src/features/ota/useOtaUpdate.ts +11 -2
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
- package/src/public/ScaleBunFacade.ts +127 -3
|
@@ -41,16 +41,16 @@ import { bridgeAdapter } from '../replay/bridge/adapters/bridgeAdapter';
|
|
|
41
41
|
import { redactBody } from '../../debug/redaction';
|
|
42
42
|
import { consumeCalibrationTarget } from '../journey/calibrationContext';
|
|
43
43
|
import { resolvePlatformOS } from '../../core/context/device';
|
|
44
|
+
import { emitAutomaticEvent } from '../../analytics/automaticEvents';
|
|
45
|
+
import {
|
|
46
|
+
INTERACTION_PROTOCOL_VERSION,
|
|
47
|
+
automaticInteractionProperties,
|
|
48
|
+
generateInteractionId,
|
|
49
|
+
type InteractionStateStatus,
|
|
50
|
+
} from '../journey/interactionProtocol';
|
|
44
51
|
|
|
45
52
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
46
53
|
|
|
47
|
-
/**
|
|
48
|
-
* Max analytics-lane heatmap interactions emitted per foreground (analytics
|
|
49
|
-
* session) window. Bounds ingest volume now that capture defaults ON; enough to
|
|
50
|
-
* resolve hotspot density, the long tail is dropped (drop-newest beyond cap).
|
|
51
|
-
*/
|
|
52
|
-
const HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
|
|
53
|
-
|
|
54
54
|
export interface SessionManagerConfig {
|
|
55
55
|
sessionReplay?: Partial<SessionReplayConfig>;
|
|
56
56
|
syncPolicy?: SyncDecisionCallback;
|
|
@@ -84,16 +84,6 @@ export class SessionManager {
|
|
|
84
84
|
* lane even when no replay recording is active. Additive, opt-in (default off).
|
|
85
85
|
*/
|
|
86
86
|
private _captureInteractionHeatmap = false;
|
|
87
|
-
/**
|
|
88
|
-
* Sampling cap for the analytics-lane heatmap emission. Now that capture is
|
|
89
|
-
* ON by default, an unbounded one-event-per-gesture stream could materially
|
|
90
|
-
* inflate ingest volume. We cap emitted interactions per analytics-session
|
|
91
|
-
* window (finalize-scoped per foreground): the first N gestures define the
|
|
92
|
-
* hotspot shape; the long tail is dropped. Resets when the window changes.
|
|
93
|
-
*/
|
|
94
|
-
private _heatmapWindowSessionId: string | null = null;
|
|
95
|
-
private _heatmapWindowCount = 0;
|
|
96
|
-
|
|
97
87
|
private session: Session | null = null;
|
|
98
88
|
private active = false;
|
|
99
89
|
private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -740,21 +730,16 @@ export class SessionManager {
|
|
|
740
730
|
* injected sensitive keys (e.g. a label) are stripped before transport.
|
|
741
731
|
* - Never throws.
|
|
742
732
|
*/
|
|
743
|
-
private _emitInteractionToAnalytics(
|
|
744
|
-
|
|
745
|
-
|
|
733
|
+
private _emitInteractionToAnalytics(
|
|
734
|
+
gestureType: string,
|
|
735
|
+
payload: Record<string, unknown>,
|
|
736
|
+
occurredAt: number,
|
|
737
|
+
screenName?: string,
|
|
738
|
+
): boolean {
|
|
739
|
+
if (!this._captureInteractionHeatmap) return false;
|
|
740
|
+
if (this.active) return false; // recording lane already carries this tap
|
|
746
741
|
const adapter = this._backendTransport;
|
|
747
|
-
if (!adapter) return;
|
|
748
|
-
|
|
749
|
-
// Per-foreground sampling cap. Reset the counter when the analytics
|
|
750
|
-
// session window rolls over, then drop anything past the cap.
|
|
751
|
-
const windowId = adapter.analyticsSessionId ?? '';
|
|
752
|
-
if (windowId !== this._heatmapWindowSessionId) {
|
|
753
|
-
this._heatmapWindowSessionId = windowId;
|
|
754
|
-
this._heatmapWindowCount = 0;
|
|
755
|
-
}
|
|
756
|
-
if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
|
|
757
|
-
this._heatmapWindowCount++;
|
|
742
|
+
if (!adapter) return false;
|
|
758
743
|
|
|
759
744
|
try {
|
|
760
745
|
// Normalize coords in place (same logic as the recording lane).
|
|
@@ -782,16 +767,18 @@ export class SessionManager {
|
|
|
782
767
|
eventId: generateEventId(),
|
|
783
768
|
// sessionId is (re)stamped by the analytics lane at flush time.
|
|
784
769
|
sessionId: adapter.analyticsSessionId ?? '',
|
|
785
|
-
ts:
|
|
770
|
+
ts: occurredAt,
|
|
786
771
|
type: 'USER_ACTION',
|
|
787
772
|
subtype: `gesture:${gestureType}`,
|
|
788
|
-
screen: this._lastKnownScreen ?? undefined,
|
|
773
|
+
screen: screenName ?? this._lastKnownScreen ?? undefined,
|
|
789
774
|
payload: safePayload,
|
|
790
775
|
source: 'user',
|
|
791
776
|
};
|
|
792
777
|
adapter.trackEvent(event);
|
|
778
|
+
return true;
|
|
793
779
|
} catch (err) {
|
|
794
780
|
logger.error('[SessionManager] heatmap analytics emit failed:', err);
|
|
781
|
+
return false;
|
|
795
782
|
}
|
|
796
783
|
}
|
|
797
784
|
|
|
@@ -810,6 +797,8 @@ export class SessionManager {
|
|
|
810
797
|
traceId?: string;
|
|
811
798
|
source?: JourneyEventSource;
|
|
812
799
|
journeyId?: string;
|
|
800
|
+
/** Original observation time; interaction capture may resolve asynchronously. */
|
|
801
|
+
timestamp?: number;
|
|
813
802
|
},
|
|
814
803
|
): void {
|
|
815
804
|
if (!this.active) return;
|
|
@@ -1046,13 +1035,18 @@ export class SessionManager {
|
|
|
1046
1035
|
* Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
|
|
1047
1036
|
* Also triggers frame capture for desktop-initiated recordings.
|
|
1048
1037
|
*/
|
|
1049
|
-
onUserAction(
|
|
1038
|
+
onUserAction(
|
|
1039
|
+
subtype: string,
|
|
1040
|
+
payload?: Record<string, unknown>,
|
|
1041
|
+
context?: { screen?: string; timestamp?: number },
|
|
1042
|
+
): void {
|
|
1050
1043
|
if (!this.active) return; // Defense-in-depth: no emission when recording is OFF
|
|
1051
1044
|
this.emitEvent('USER_ACTION', {
|
|
1052
1045
|
subtype,
|
|
1053
|
-
screen: this._lastKnownScreen ?? undefined,
|
|
1046
|
+
screen: context?.screen ?? this._lastKnownScreen ?? undefined,
|
|
1054
1047
|
payload,
|
|
1055
1048
|
source: 'user',
|
|
1049
|
+
timestamp: context?.timestamp,
|
|
1056
1050
|
});
|
|
1057
1051
|
// NOTE: Do NOT call captureManager.onInteraction() here.
|
|
1058
1052
|
// emitEvent() already triggers onInteraction() for USER_ACTION events (line ~405).
|
|
@@ -1103,11 +1097,22 @@ export class SessionManager {
|
|
|
1103
1097
|
screenWidth?: number;
|
|
1104
1098
|
screenHeight?: number;
|
|
1105
1099
|
platform?: string;
|
|
1100
|
+
interactionId?: string;
|
|
1101
|
+
interactionProtocol?: number;
|
|
1102
|
+
occurredAt?: number;
|
|
1103
|
+
ui?: string;
|
|
1104
|
+
stateStatus?: InteractionStateStatus;
|
|
1105
|
+
targetId?: string;
|
|
1106
|
+
screenName?: string;
|
|
1107
|
+
emitAutomaticAnalytics?: boolean;
|
|
1106
1108
|
}): void {
|
|
1107
1109
|
// Exact-duplicate suppression — see _lastGestureSig. Signature is the gesture type plus
|
|
1108
1110
|
// raw coordinates verbatim; String() keeps undefined coords distinct from 0 (a payload
|
|
1109
1111
|
// with no coords never collides with a real origin tap).
|
|
1110
|
-
const
|
|
1112
|
+
const suppliedInteractionId = details?.interactionId;
|
|
1113
|
+
const dedupSig = suppliedInteractionId
|
|
1114
|
+
? `id:${suppliedInteractionId}`
|
|
1115
|
+
: `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
|
|
1111
1116
|
const nowTs = Date.now();
|
|
1112
1117
|
if (
|
|
1113
1118
|
dedupSig === this._lastGestureSig &&
|
|
@@ -1122,7 +1127,15 @@ export class SessionManager {
|
|
|
1122
1127
|
// Build the canonical gesture payload ONCE so the recording lane and the
|
|
1123
1128
|
// (additive) analytics lane carry byte-identical keys (normalizedX/Y,
|
|
1124
1129
|
// gestureType, etc.). Same object shape that was previously inlined.
|
|
1130
|
+
const interactionId = suppliedInteractionId ?? generateInteractionId();
|
|
1131
|
+
const occurredAt = details?.occurredAt ?? Date.now();
|
|
1132
|
+
const stateStatus = details?.stateStatus ?? (details?.ui ? 'captured_nonempty' : 'not_captured');
|
|
1125
1133
|
const payload: Record<string, unknown> = {
|
|
1134
|
+
interaction_id: interactionId,
|
|
1135
|
+
interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
|
|
1136
|
+
state_status: stateStatus,
|
|
1137
|
+
ui: details?.ui,
|
|
1138
|
+
target_id: details?.targetId,
|
|
1126
1139
|
gestureType,
|
|
1127
1140
|
x: details?.x,
|
|
1128
1141
|
y: details?.y,
|
|
@@ -1167,7 +1180,27 @@ export class SessionManager {
|
|
|
1167
1180
|
// when the flag is OFF, no backend transport is attached, or recording is
|
|
1168
1181
|
// active (the recording path below already carries this tap). Uses a fresh
|
|
1169
1182
|
// payload clone so analytics normalization never mutates the recording one.
|
|
1170
|
-
this._emitInteractionToAnalytics(
|
|
1183
|
+
const analyticsReplayCarrier = this._emitInteractionToAnalytics(
|
|
1184
|
+
gestureType,
|
|
1185
|
+
{ ...payload },
|
|
1186
|
+
occurredAt,
|
|
1187
|
+
details?.screenName,
|
|
1188
|
+
);
|
|
1189
|
+
const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
|
|
1190
|
+
if (details?.emitAutomaticAnalytics) {
|
|
1191
|
+
try {
|
|
1192
|
+
const safePayload = redactBody(payload) as Record<string, unknown>;
|
|
1193
|
+
emitAutomaticEvent(
|
|
1194
|
+
'element_interacted',
|
|
1195
|
+
automaticInteractionProperties(
|
|
1196
|
+
safePayload,
|
|
1197
|
+
details?.screenName ?? this._lastKnownScreen ?? undefined,
|
|
1198
|
+
replayCarrier,
|
|
1199
|
+
),
|
|
1200
|
+
occurredAt,
|
|
1201
|
+
);
|
|
1202
|
+
} catch { /* automatic projection must never affect interaction capture */ }
|
|
1203
|
+
}
|
|
1171
1204
|
|
|
1172
1205
|
if (!this.active) return; // Defense-in-depth: no emission when recording is OFF
|
|
1173
1206
|
// The subtype becomes `ReplayEvent.label`, which is what the Events explorer GROUPS BY. Naming
|
|
@@ -1178,7 +1211,11 @@ export class SessionManager {
|
|
|
1178
1211
|
// ⚠️ `gestureType` also travels in the payload, and the backend's `resolveGesture` reads THAT
|
|
1179
1212
|
// first — so enriching the label cannot change heatmap gesture classification.
|
|
1180
1213
|
const target = typeof details?.target === 'string' ? details.target.trim() : '';
|
|
1181
|
-
this.onUserAction(
|
|
1214
|
+
this.onUserAction(
|
|
1215
|
+
target ? `gesture:${gestureType} · ${target}` : `gesture:${gestureType}`,
|
|
1216
|
+
payload,
|
|
1217
|
+
{ screen: details?.screenName, timestamp: occurredAt },
|
|
1218
|
+
);
|
|
1182
1219
|
}
|
|
1183
1220
|
|
|
1184
1221
|
/**
|
|
@@ -327,6 +327,17 @@ class ScaleBunFacade {
|
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
+
// `healthyAfterMs` is the field BootGuardConfig actually declares.
|
|
331
|
+
// This passed `healthyTimeoutMs`, which nothing reads, so a host that
|
|
332
|
+
// tuned the boot-guard window silently got the 10s default instead.
|
|
333
|
+
// Both spellings are accepted so the older one keeps working.
|
|
334
|
+
const healthyAfterMs =
|
|
335
|
+
typeof ota.healthyAfterMs === 'number'
|
|
336
|
+
? ota.healthyAfterMs
|
|
337
|
+
: typeof ota.healthyTimeoutMs === 'number'
|
|
338
|
+
? ota.healthyTimeoutMs
|
|
339
|
+
: undefined;
|
|
340
|
+
|
|
330
341
|
otaOrchestrator.init({
|
|
331
342
|
...(signingRequested
|
|
332
343
|
? {
|
|
@@ -338,16 +349,129 @@ class ScaleBunFacade {
|
|
|
338
349
|
},
|
|
339
350
|
}
|
|
340
351
|
: {}),
|
|
341
|
-
...(
|
|
342
|
-
? { healthyTimeoutMs: ota.healthyTimeoutMs }
|
|
343
|
-
: {}),
|
|
352
|
+
...(healthyAfterMs !== undefined ? { healthyAfterMs } : {}),
|
|
344
353
|
});
|
|
345
354
|
logger.info('[ScaleBun] OTA enabled from init config.');
|
|
355
|
+
|
|
356
|
+
// Run the checks the config asked for. Until this existed, `ota.enabled`
|
|
357
|
+
// initialised the orchestrator and then never checked anything: the
|
|
358
|
+
// documented `checkOnForeground` and `channelOverride` options were read
|
|
359
|
+
// by no code at all, and an app following the documented config received
|
|
360
|
+
// updates only if it ALSO drove `useOtaUpdate` or the CodePush shim by
|
|
361
|
+
// hand. Nothing logged, because "no update available" and "never asked"
|
|
362
|
+
// look identical from the outside.
|
|
363
|
+
this._startOtaChecks(ota);
|
|
346
364
|
} catch (err: any) {
|
|
347
365
|
logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
|
|
348
366
|
}
|
|
349
367
|
}
|
|
350
368
|
|
|
369
|
+
/** Guards against overlapping config-driven OTA checks. */
|
|
370
|
+
private _otaCheckInFlight = false;
|
|
371
|
+
/** Wall clock of the last config-driven check, for the foreground floor. */
|
|
372
|
+
private _otaLastCheckAt = 0;
|
|
373
|
+
private _otaForegroundListener: ((state: string) => void) | null = null;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Minimum gap between config-driven checks.
|
|
377
|
+
*
|
|
378
|
+
* A foreground transition is cheap to trigger — app switchers, permission
|
|
379
|
+
* dialogs and share sheets all produce one — so an unthrottled check would
|
|
380
|
+
* put a request on the hot path every time the user glanced away. Ten
|
|
381
|
+
* minutes is well below any realistic release cadence and well above that
|
|
382
|
+
* noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
|
|
383
|
+
* which is never throttled.
|
|
384
|
+
*/
|
|
385
|
+
private static readonly OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Drive OTA checks from init config: once at startup, then on each
|
|
389
|
+
* foreground when `checkOnForeground` is on (the schema default).
|
|
390
|
+
*
|
|
391
|
+
* `appVersion` is resolved from the native bridge rather than asked of the
|
|
392
|
+
* integrator, because it gates the server's `targetAppVersion` semver check
|
|
393
|
+
* — sending a wrong or invented value is worse than sending none, and there
|
|
394
|
+
* is no honest default. If it cannot be resolved, the check is skipped with
|
|
395
|
+
* a warning instead of guessing.
|
|
396
|
+
*/
|
|
397
|
+
private _startOtaChecks(ota: any): void {
|
|
398
|
+
// Never in a debug build. `ScaleBunOtaModule.getJSBundleFile()` returns null
|
|
399
|
+
// there on purpose so Metro keeps ownership of the bundle — so a bundle
|
|
400
|
+
// downloaded in dev is installed into a slot that will never be loaded, and
|
|
401
|
+
// the identity check on the next launch then correctly observes that the
|
|
402
|
+
// running code is not what was installed and reports APPLY_FAILED. Checking
|
|
403
|
+
// at all in dev buys nothing and manufactures that false alarm. A developer
|
|
404
|
+
// testing the OTA path drives `useOtaUpdate().sync()` explicitly.
|
|
405
|
+
if (__DEV__) {
|
|
406
|
+
logger.info('[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const runCheck = async (trigger: 'startup' | 'foreground'): Promise<void> => {
|
|
411
|
+
if (this._otaCheckInFlight) return;
|
|
412
|
+
if (
|
|
413
|
+
trigger === 'foreground' &&
|
|
414
|
+
Date.now() - this._otaLastCheckAt < ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS
|
|
415
|
+
) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const clientKey = this._clientKey;
|
|
419
|
+
const apiUrl = this._apiBaseUrl;
|
|
420
|
+
if (!clientKey || !apiUrl) return;
|
|
421
|
+
|
|
422
|
+
this._otaCheckInFlight = true;
|
|
423
|
+
try {
|
|
424
|
+
const info = await bridgeAdapter.getDeviceInfo();
|
|
425
|
+
const appVersion = (info as { appVersion?: string } | null)?.appVersion;
|
|
426
|
+
if (!appVersion) {
|
|
427
|
+
logger.warn(
|
|
428
|
+
'[ScaleBun] OTA check skipped — the app version could not be read from the ' +
|
|
429
|
+
'native bridge. Rebuild the native app, or drive checks yourself with ' +
|
|
430
|
+
'useOtaUpdate({ appVersion }).',
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this._otaLastCheckAt = Date.now();
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
436
|
+
const { otaOrchestrator } = require('../features/ota/OtaOrchestrator');
|
|
437
|
+
await otaOrchestrator.sync({
|
|
438
|
+
apiUrl,
|
|
439
|
+
clientKey,
|
|
440
|
+
appVersion,
|
|
441
|
+
// The documented option, finally connected. Omitted means the
|
|
442
|
+
// server's `default` channel, exactly as before.
|
|
443
|
+
channelName: typeof ota.channelOverride === 'string' ? ota.channelOverride : undefined,
|
|
444
|
+
// Never forced from config: the release's own installMode
|
|
445
|
+
// decides when the app restarts, and yanking the screen out
|
|
446
|
+
// from under a user is not a decision this switch should make.
|
|
447
|
+
});
|
|
448
|
+
} catch (err: any) {
|
|
449
|
+
logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
|
|
450
|
+
} finally {
|
|
451
|
+
this._otaCheckInFlight = false;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
void runCheck('startup');
|
|
456
|
+
|
|
457
|
+
if (ota.checkOnForeground === false) return;
|
|
458
|
+
if (this._otaForegroundListener) return; // idempotent across repeated init()
|
|
459
|
+
try {
|
|
460
|
+
// Through `appLifecycle`, not a second AppState subscription: the SDK
|
|
461
|
+
// already owns one and fanning out from it keeps every consumer on the
|
|
462
|
+
// same transition sequence.
|
|
463
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
464
|
+
const { appLifecycle } = require('../core/lifecycle/appLifecycle');
|
|
465
|
+
this._otaForegroundListener = (state: string) => {
|
|
466
|
+
if (state === 'active') void runCheck('foreground');
|
|
467
|
+
};
|
|
468
|
+
appLifecycle.addListener(this._otaForegroundListener);
|
|
469
|
+
} catch {
|
|
470
|
+
// Lifecycle unavailable (tests, exotic hosts) — the startup check stands.
|
|
471
|
+
this._otaForegroundListener = null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
351
475
|
private _autoEnableDebug(debugConfig: DebugConnectionConfig): void {
|
|
352
476
|
try {
|
|
353
477
|
const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
|