@scalebun/react-native 1.11.0 → 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.
@@ -276,7 +276,6 @@ class InteractionTracker(private val reactContext: ReactApplicationContext) {
276
276
 
277
277
  fun reset() {
278
278
  lastInteractionMs = 0
279
- lastEmitTs = 0
280
279
  touchInteractionId = ""
281
280
  lastEmittedInteractionId = ""
282
281
  touchDownTs = 0
@@ -179,10 +179,18 @@ class SlotManager(private val filesDir: File) {
179
179
  if (reason != null) {
180
180
  runCatching {
181
181
  otaRoot.mkdirs()
182
+ // Stamp WHICH bundle is being reverted away from, read before
183
+ // current/ is deleted below. Without it JS had nothing to name:
184
+ // slot meta carries no bundleId and the previous slot is gone by
185
+ // the time JS looks, so every AUTO_ROLLBACK reported bundleId
186
+ // "unknown" and the server dropped it as unresolvable — leaving
187
+ // the crash guard's own rollback signal permanently at zero.
188
+ val failingSha = readMeta(currentSlot)?.optString("sha256", "").orEmpty()
182
189
  revertRecord.writeText(
183
190
  JSONObject()
184
191
  .put("reason", reason)
185
192
  .put("at", System.currentTimeMillis())
193
+ .apply { if (failingSha.isNotEmpty()) put("sha256", failingSha) }
186
194
  .toString(),
187
195
  )
188
196
  }
@@ -237,7 +245,7 @@ class SlotManager(private val filesDir: File) {
237
245
  val metaFile = File(currentSlot, META_FILENAME)
238
246
  if (!metaFile.exists()) {
239
247
  Log.w(TAG, "Boot marker present but no current/meta.json — auto-reverting")
240
- revert()
248
+ revert(reason = "missing_meta")
241
249
  return
242
250
  }
243
251
 
@@ -259,7 +267,7 @@ class SlotManager(private val filesDir: File) {
259
267
  }
260
268
  if (currentId != markerId) {
261
269
  Log.w(TAG, "Boot marker sha256 mismatch — auto-reverting")
262
- revert()
270
+ revert(reason = "hash_mismatch")
263
271
  return
264
272
  }
265
273
 
@@ -290,7 +298,7 @@ class SlotManager(private val filesDir: File) {
290
298
  Log.i(TAG, "Boot guard: attempt $attempts/$MAX_BOOT_ATTEMPTS for this bundle")
291
299
  } catch (e: Exception) {
292
300
  Log.e(TAG, "checkBootGuard() failed — reverting to be safe", e)
293
- revert()
301
+ revert(reason = "boot_guard_error")
294
302
  }
295
303
  }
296
304
 
@@ -312,6 +320,10 @@ class SlotManager(private val filesDir: File) {
312
320
  state.put("bootGuardReverted", true)
313
321
  state.put("bootGuardRevertReason", rec.optString("reason", "boot_crash_guard"))
314
322
  state.put("bootGuardRevertedAt", rec.optLong("at", 0L))
323
+ // The bundle reverted AWAY from. JS joins it back to a bundle id
324
+ // through its install record so the rollback can be reported.
325
+ val sha = rec.optString("sha256", "")
326
+ if (sha.isNotEmpty()) state.put("bootGuardRevertedSha256", sha)
315
327
  }
316
328
  revertRecord.delete()
317
329
  }
@@ -605,7 +605,7 @@ var SDK_VERSION;
605
605
  var init_version = __esm({
606
606
  "lib/module/core/constants/version.js"() {
607
607
  "use strict";
608
- SDK_VERSION = "1.11.0";
608
+ SDK_VERSION = "1.11.1";
609
609
  }
610
610
  });
611
611
 
@@ -10592,6 +10592,42 @@ var init_bootstrap = __esm({
10592
10592
  }
10593
10593
  });
10594
10594
 
10595
+ // lib/module/core/lifecycle/appLifecycle.js
10596
+ var appLifecycle_exports = {};
10597
+ __export(appLifecycle_exports, {
10598
+ appLifecycle: () => appLifecycle
10599
+ });
10600
+ var import_react_native13, AppLifecycle, appLifecycle;
10601
+ var init_appLifecycle = __esm({
10602
+ "lib/module/core/lifecycle/appLifecycle.js"() {
10603
+ "use strict";
10604
+ import_react_native13 = require("react-native");
10605
+ init_internalLogger();
10606
+ AppLifecycle = class {
10607
+ listeners = [];
10608
+ currentState = import_react_native13.AppState.currentState;
10609
+ constructor() {
10610
+ import_react_native13.AppState.addEventListener("change", this.handleStateChange);
10611
+ }
10612
+ handleStateChange = (nextState) => {
10613
+ __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
10614
+ this.currentState = nextState;
10615
+ this.listeners.forEach((l) => l(nextState));
10616
+ };
10617
+ addListener(listener) {
10618
+ this.listeners.push(listener);
10619
+ }
10620
+ removeListener(listener) {
10621
+ this.listeners = this.listeners.filter((l) => l !== listener);
10622
+ }
10623
+ getCurrentState() {
10624
+ return this.currentState;
10625
+ }
10626
+ };
10627
+ appLifecycle = new AppLifecycle();
10628
+ }
10629
+ });
10630
+
10595
10631
  // lib/module/features/crash/CrashReporter.js
10596
10632
  function getCrashReporter() {
10597
10633
  if (!_reporter) _reporter = new CrashReporter();
@@ -12176,6 +12212,11 @@ async function deliverOtaEvents(params) {
12176
12212
  kind: "ota_event",
12177
12213
  type: e.type,
12178
12214
  bundleId: e.bundleId,
12215
+ // Without this the backend stored a null releaseId on every row it ingested,
12216
+ // while serving the release id on every check — so the delivery funnel could
12217
+ // only ever be grouped by bundle, and a bundle re-promoted under a second
12218
+ // release merged the two into one indistinguishable series.
12219
+ releaseId: e.releaseId,
12179
12220
  installationId: params.installationId,
12180
12221
  // OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
12181
12222
  // resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
@@ -12277,20 +12318,31 @@ var init_OtaOrchestrator = __esm({
12277
12318
  this.bootGuardConfig = config ?? {};
12278
12319
  this.signatureConfig = config?.signature;
12279
12320
  __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12280
- this.hydrateCurrentBundleFromSlots();
12321
+ const slotState = this.readSlotState();
12322
+ this.hydrateCurrentBundleFromSlots(slotState);
12281
12323
  void prefetchDeviceCountry();
12324
+ this.checkBootGuardRecovery(slotState);
12282
12325
  this.verifyRunningBundleIdentity();
12283
- this.checkBootGuardRecovery();
12284
12326
  });
12285
12327
  }
12286
12328
  /**
12287
12329
  * Read the active slot back into `currentBundle` so the next check reports
12288
12330
  * what this device is genuinely running.
12289
12331
  */
12290
- hydrateCurrentBundleFromSlots() {
12291
- if (!NativeScaleBunOta_default) return;
12332
+ /**
12333
+ * Parse the native slot state once. Returns null when the module is absent or
12334
+ * the payload is unreadable — every caller treats that as "factory bundle".
12335
+ */
12336
+ readSlotState() {
12337
+ if (!NativeScaleBunOta_default) return null;
12338
+ try {
12339
+ return JSON.parse(NativeScaleBunOta_default.getSlotState());
12340
+ } catch {
12341
+ return null;
12342
+ }
12343
+ }
12344
+ hydrateCurrentBundleFromSlots(state) {
12292
12345
  try {
12293
- const state = JSON.parse(NativeScaleBunOta_default.getSlotState());
12294
12346
  const current = state?.current;
12295
12347
  if (!current?.sha256) return;
12296
12348
  const record4 = this.readInstallRecord();
@@ -12298,6 +12350,7 @@ var init_OtaOrchestrator = __esm({
12298
12350
  this.currentBundle = {
12299
12351
  id: record4.bundleId,
12300
12352
  version: record4.version,
12353
+ releaseId: record4.releaseId ?? void 0,
12301
12354
  sha256: record4.sha256
12302
12355
  };
12303
12356
  __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
@@ -12328,29 +12381,23 @@ var init_OtaOrchestrator = __esm({
12328
12381
  verifyRunningBundleIdentity() {
12329
12382
  if (!this.currentBundle) return;
12330
12383
  const running = readRunningBundleMarker();
12331
- const expected = this.readInstallExpectation();
12332
- if (expected && expected.bundleId !== this.currentBundle.id) {
12384
+ const record4 = this.readInstallRecord();
12385
+ if (!record4) return;
12386
+ if (record4.sha256 !== this.currentBundle.sha256) {
12333
12387
  this.clearInstallExpectation();
12334
12388
  return;
12335
12389
  }
12336
- if (expected) {
12337
- if (running === expected.identityToken) {
12338
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12339
- this.clearInstallExpectation();
12340
- return;
12341
- }
12342
- logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
12343
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12344
- error: `install_not_effective \u2014 expected ${expected.identityToken}, running ${running ?? "none"}`
12345
- });
12390
+ if (!record4.identityToken) return;
12391
+ if (running === record4.identityToken) {
12392
+ __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12393
+ this.retireIdentityToken(record4);
12346
12394
  return;
12347
12395
  }
12348
- if (running && running !== this.currentBundle.id) {
12349
- logger.error(`[OTA] BUNDLE MISMATCH \u2014 slot says ${this.currentBundle.id} is active but the running bundle identifies as ${running}.`);
12350
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12351
- error: `bundle_identity_mismatch \u2014 running ${running}`
12352
- });
12353
- }
12396
+ logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
12397
+ otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12398
+ releaseId: this.currentBundle.releaseId,
12399
+ error: `install_not_effective \u2014 expected ${record4.identityToken}, running ${running ?? "none"}`
12400
+ });
12354
12401
  }
12355
12402
  // ── Install expectation ────────────────────────────────────────────────────
12356
12403
  // Written just before the restart that activates a bundle, read on the next
@@ -12362,6 +12409,11 @@ var init_OtaOrchestrator = __esm({
12362
12409
  this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
12363
12410
  bundleId: bundle.id,
12364
12411
  version: bundle.version,
12412
+ // Carried so telemetry emitted on a LATER launch (a boot-guard
12413
+ // rollback, an ineffective install) can still be attributed to the
12414
+ // release, not merely the bundle. The check response is long gone by
12415
+ // then; this record is the only thing that remembers.
12416
+ releaseId: bundle.releaseId ?? null,
12365
12417
  // The join key back to the native slot, which records sha256 and
12366
12418
  // nothing else identifying.
12367
12419
  sha256: bundle.sha256,
@@ -12380,12 +12432,26 @@ var init_OtaOrchestrator = __esm({
12380
12432
  return null;
12381
12433
  }
12382
12434
  }
12383
- readInstallExpectation() {
12384
- const record4 = this.readInstallRecord();
12385
- return record4 && record4.identityToken ? {
12386
- bundleId: record4.bundleId,
12387
- identityToken: record4.identityToken
12388
- } : null;
12435
+ /**
12436
+ * Drop the identity token once the install has been proven, keeping the rest
12437
+ * of the record.
12438
+ *
12439
+ * The record does two jobs: it proves an install took effect (once), and it
12440
+ * maps the native slot's sha256 back to a bundle id (for the life of that
12441
+ * bundle). Only the first job is finished after a successful verification, so
12442
+ * only the token is retired.
12443
+ */
12444
+ retireIdentityToken(record4) {
12445
+ try {
12446
+ this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
12447
+ bundleId: record4.bundleId,
12448
+ version: record4.version,
12449
+ releaseId: record4.releaseId ?? null,
12450
+ sha256: record4.sha256,
12451
+ identityToken: null
12452
+ }));
12453
+ } catch {
12454
+ }
12389
12455
  }
12390
12456
  clearInstallExpectation() {
12391
12457
  try {
@@ -12402,17 +12468,24 @@ var init_OtaOrchestrator = __esm({
12402
12468
  * If getSlotState() shows bootMarkerPresent=false but we have a 'previous' slot
12403
12469
  * and no 'current' OTA bundle, the native layer already reverted.
12404
12470
  */
12405
- checkBootGuardRecovery() {
12406
- if (!NativeScaleBunOta_default) return;
12471
+ checkBootGuardRecovery(state) {
12407
12472
  try {
12408
- const stateJson = NativeScaleBunOta_default.getSlotState();
12409
- const state = JSON.parse(stateJson);
12410
- if (state.bootGuardReverted) {
12411
- logger.warn("[OTA] Boot guard fired \u2014 app was reverted to previous bundle");
12412
- otaEventEmitter.emitSimple("AUTO_ROLLBACK", state.previous?.bundleId ?? "unknown", {
12413
- reason: "boot_guard_crash_loop_detected"
12473
+ if (!state?.bootGuardReverted) return;
12474
+ const record4 = this.readInstallRecord();
12475
+ const revertedSha = state.bootGuardRevertedSha256;
12476
+ const matchesRecord = !!record4 && (!revertedSha || record4.sha256 === revertedSha);
12477
+ const reason = state.bootGuardRevertReason || "boot_crash_guard";
12478
+ if (matchesRecord && record4) {
12479
+ logger.warn(`[OTA] Boot guard fired \u2014 reverted away from bundle ${record4.bundleId} (v${record4.version}); reason: ${reason}`);
12480
+ otaEventEmitter.emitSimple("AUTO_ROLLBACK", record4.bundleId, {
12481
+ releaseId: record4.releaseId ?? void 0,
12482
+ version: record4.version,
12483
+ reason
12414
12484
  });
12485
+ this.clearInstallExpectation();
12486
+ return;
12415
12487
  }
12488
+ logger.warn(`[OTA] Boot guard fired (reason: ${reason}) but the rolled-back bundle could not be identified locally \u2014 no install record. The rollback is not reported to the server.`);
12416
12489
  } catch {
12417
12490
  }
12418
12491
  }
@@ -12503,6 +12576,12 @@ var init_OtaOrchestrator = __esm({
12503
12576
  };
12504
12577
  }
12505
12578
  __DEV__ && logger.debug("[OTA] Sync started\u2026");
12579
+ if (this.currentBundle?.id) {
12580
+ otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
12581
+ releaseId: this.currentBundle.releaseId,
12582
+ version: this.currentBundle.version
12583
+ });
12584
+ }
12506
12585
  const checkRes = await this.checkForUpdate(params);
12507
12586
  if (checkRes.action === "NONE") {
12508
12587
  __DEV__ && logger.debug("[OTA] App is up to date");
@@ -12513,7 +12592,9 @@ var init_OtaOrchestrator = __esm({
12513
12592
  }
12514
12593
  if (checkRes.action === "ROLLBACK") {
12515
12594
  logger.warn("[OTA] Server requested ROLLBACK \u2014 reverting to previous bundle");
12516
- otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown");
12595
+ otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown", {
12596
+ releaseId: this.currentBundle?.releaseId
12597
+ });
12517
12598
  const reverted = await NativeScaleBunOta_default.revertToPrevious();
12518
12599
  if (reverted) {
12519
12600
  this.currentBundle = null;
@@ -12537,9 +12618,14 @@ var init_OtaOrchestrator = __esm({
12537
12618
  }
12538
12619
  const bundle = checkRes.bundle;
12539
12620
  let patchUsed = false;
12621
+ otaEventEmitter.emitSimple("OFFERED", bundle.id, {
12622
+ releaseId: bundle.releaseId,
12623
+ version: bundle.version
12624
+ });
12540
12625
  const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
12541
12626
  if (!signatureOutcome.ok) {
12542
12627
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12628
+ releaseId: bundle.releaseId,
12543
12629
  error: `Signature check failed: ${signatureOutcome.reason}`,
12544
12630
  version: bundle.version
12545
12631
  });
@@ -12550,6 +12636,7 @@ var init_OtaOrchestrator = __esm({
12550
12636
  };
12551
12637
  }
12552
12638
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
12639
+ releaseId: bundle.releaseId,
12553
12640
  version: bundle.version
12554
12641
  });
12555
12642
  const downloadStart = Date.now();
@@ -12635,6 +12722,7 @@ var init_OtaOrchestrator = __esm({
12635
12722
  postProgress(0, "FAILED");
12636
12723
  logger.error("[OTA] Staging bundle failed after retries");
12637
12724
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12725
+ releaseId: bundle.releaseId,
12638
12726
  error: "Staging failed \u2014 SHA-256 mismatch or download error",
12639
12727
  version: bundle.version
12640
12728
  });
@@ -12647,6 +12735,7 @@ var init_OtaOrchestrator = __esm({
12647
12735
  postProgress(bundle.size, "COMPLETED");
12648
12736
  const downloadDuration = Date.now() - downloadStart;
12649
12737
  otaEventEmitter.emitSimple("DOWNLOAD_COMPLETE", bundle.id, {
12738
+ releaseId: bundle.releaseId,
12650
12739
  version: bundle.version,
12651
12740
  durationMs: downloadDuration,
12652
12741
  patchUsed
@@ -12655,6 +12744,7 @@ var init_OtaOrchestrator = __esm({
12655
12744
  if (!applied) {
12656
12745
  logger.error("[OTA] Applying update failed");
12657
12746
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12747
+ releaseId: bundle.releaseId,
12658
12748
  error: "Atomic slot swap failed",
12659
12749
  version: bundle.version
12660
12750
  });
@@ -12668,6 +12758,7 @@ var init_OtaOrchestrator = __esm({
12668
12758
  this.isRestartRequiredState = true;
12669
12759
  this.recordInstallExpectation(bundle);
12670
12760
  otaEventEmitter.emitSimple("INSTALLED", bundle.id, {
12761
+ releaseId: bundle.releaseId,
12671
12762
  version: bundle.version
12672
12763
  });
12673
12764
  __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
@@ -12720,6 +12811,14 @@ var init_OtaOrchestrator = __esm({
12720
12811
  NativeScaleBunOta_default?.markHealthy();
12721
12812
  __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12722
12813
  this.healthyTimer = null;
12814
+ const running = this.currentBundle;
12815
+ if (running?.id) {
12816
+ otaEventEmitter.emitSimple("BOOT_SUCCESS", running.id, {
12817
+ releaseId: running.releaseId,
12818
+ version: running.version,
12819
+ durationMs: healthyMs
12820
+ });
12821
+ }
12723
12822
  });
12724
12823
  }, healthyMs);
12725
12824
  } catch {
@@ -15041,31 +15140,8 @@ var FlushScheduler = class {
15041
15140
  }
15042
15141
  };
15043
15142
 
15044
- // lib/module/core/lifecycle/appLifecycle.js
15045
- var import_react_native13 = require("react-native");
15046
- init_internalLogger();
15047
- var AppLifecycle = class {
15048
- listeners = [];
15049
- currentState = import_react_native13.AppState.currentState;
15050
- constructor() {
15051
- import_react_native13.AppState.addEventListener("change", this.handleStateChange);
15052
- }
15053
- handleStateChange = (nextState) => {
15054
- __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
15055
- this.currentState = nextState;
15056
- this.listeners.forEach((l) => l(nextState));
15057
- };
15058
- addListener(listener) {
15059
- this.listeners.push(listener);
15060
- }
15061
- removeListener(listener) {
15062
- this.listeners = this.listeners.filter((l) => l !== listener);
15063
- }
15064
- getCurrentState() {
15065
- return this.currentState;
15066
- }
15067
- };
15068
- var appLifecycle = new AppLifecycle();
15143
+ // lib/module/bootstrap/SDKBootstrapper.js
15144
+ init_appLifecycle();
15069
15145
 
15070
15146
  // lib/module/bootstrap/FeatureRegistry.js
15071
15147
  init_internalLogger();
@@ -20107,7 +20183,7 @@ async function captureInstallReferrerOnce(sink) {
20107
20183
  }
20108
20184
 
20109
20185
  // lib/module/public/ScaleBunFacade.js
20110
- var ScaleBunFacade = class {
20186
+ var ScaleBunFacade = class _ScaleBunFacade {
20111
20187
  initialized = false;
20112
20188
  /** In-flight init promise — guards against a second init() racing before the first resolves. */
20113
20189
  _initInFlight = null;
@@ -20267,6 +20343,7 @@ var ScaleBunFacade = class {
20267
20343
  logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
20268
20344
  }
20269
20345
  }
20346
+ const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
20270
20347
  otaOrchestrator2.init({
20271
20348
  ...signingRequested ? {
20272
20349
  signature: {
@@ -20276,15 +20353,99 @@ var ScaleBunFacade = class {
20276
20353
  verifier
20277
20354
  }
20278
20355
  } : {},
20279
- ...typeof ota.healthyTimeoutMs === "number" ? {
20280
- healthyTimeoutMs: ota.healthyTimeoutMs
20356
+ ...healthyAfterMs !== void 0 ? {
20357
+ healthyAfterMs
20281
20358
  } : {}
20282
20359
  });
20283
20360
  logger.info("[ScaleBun] OTA enabled from init config.");
20361
+ this._startOtaChecks(ota);
20284
20362
  } catch (err) {
20285
20363
  logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
20286
20364
  }
20287
20365
  }
20366
+ /** Guards against overlapping config-driven OTA checks. */
20367
+ _otaCheckInFlight = false;
20368
+ /** Wall clock of the last config-driven check, for the foreground floor. */
20369
+ _otaLastCheckAt = 0;
20370
+ _otaForegroundListener = null;
20371
+ /**
20372
+ * Minimum gap between config-driven checks.
20373
+ *
20374
+ * A foreground transition is cheap to trigger — app switchers, permission
20375
+ * dialogs and share sheets all produce one — so an unthrottled check would
20376
+ * put a request on the hot path every time the user glanced away. Ten
20377
+ * minutes is well below any realistic release cadence and well above that
20378
+ * noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
20379
+ * which is never throttled.
20380
+ */
20381
+ static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1e3;
20382
+ /**
20383
+ * Drive OTA checks from init config: once at startup, then on each
20384
+ * foreground when `checkOnForeground` is on (the schema default).
20385
+ *
20386
+ * `appVersion` is resolved from the native bridge rather than asked of the
20387
+ * integrator, because it gates the server's `targetAppVersion` semver check
20388
+ * — sending a wrong or invented value is worse than sending none, and there
20389
+ * is no honest default. If it cannot be resolved, the check is skipped with
20390
+ * a warning instead of guessing.
20391
+ */
20392
+ _startOtaChecks(ota) {
20393
+ if (__DEV__) {
20394
+ logger.info("[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).");
20395
+ return;
20396
+ }
20397
+ const runCheck = async (trigger) => {
20398
+ if (this._otaCheckInFlight) return;
20399
+ if (trigger === "foreground" && Date.now() - this._otaLastCheckAt < _ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
20400
+ return;
20401
+ }
20402
+ const clientKey = this._clientKey;
20403
+ const apiUrl = this._apiBaseUrl;
20404
+ if (!clientKey || !apiUrl) return;
20405
+ this._otaCheckInFlight = true;
20406
+ try {
20407
+ const info = await bridgeAdapter.getDeviceInfo();
20408
+ const appVersion = info?.appVersion;
20409
+ if (!appVersion) {
20410
+ logger.warn("[ScaleBun] OTA check skipped \u2014 the app version could not be read from the native bridge. Rebuild the native app, or drive checks yourself with useOtaUpdate({ appVersion }).");
20411
+ return;
20412
+ }
20413
+ this._otaLastCheckAt = Date.now();
20414
+ const {
20415
+ otaOrchestrator: otaOrchestrator2
20416
+ } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
20417
+ await otaOrchestrator2.sync({
20418
+ apiUrl,
20419
+ clientKey,
20420
+ appVersion,
20421
+ // The documented option, finally connected. Omitted means the
20422
+ // server's `default` channel, exactly as before.
20423
+ channelName: typeof ota.channelOverride === "string" ? ota.channelOverride : void 0
20424
+ // Never forced from config: the release's own installMode
20425
+ // decides when the app restarts, and yanking the screen out
20426
+ // from under a user is not a decision this switch should make.
20427
+ });
20428
+ } catch (err) {
20429
+ logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
20430
+ } finally {
20431
+ this._otaCheckInFlight = false;
20432
+ }
20433
+ };
20434
+ void runCheck("startup");
20435
+ if (ota.checkOnForeground === false) return;
20436
+ if (this._otaForegroundListener) return;
20437
+ try {
20438
+ const {
20439
+ appLifecycle: appLifecycle2
20440
+ } = (init_appLifecycle(), __toCommonJS(appLifecycle_exports));
20441
+ this._otaForegroundListener = (state) => {
20442
+ if (state === "active") void runCheck("foreground");
20443
+ };
20444
+ appLifecycle2.addListener(this._otaForegroundListener);
20445
+ } catch {
20446
+ this._otaForegroundListener = null;
20447
+ }
20448
+ }
20288
20449
  _autoEnableDebug(debugConfig) {
20289
20450
  try {
20290
20451
  const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
@@ -21696,6 +21857,7 @@ init_internalLogger();
21696
21857
 
21697
21858
  // lib/module/features/engage/engageTriggerEngine.js
21698
21859
  init_internalLogger();
21860
+ init_appLifecycle();
21699
21861
  init_AutoScreenDetector();
21700
21862
  init_StorageBackend();
21701
21863
  init_engageSignals();
@@ -22003,6 +22165,7 @@ function installEngageTriggerEngine(deps) {
22003
22165
  }
22004
22166
 
22005
22167
  // lib/module/features/engage/EngagePromptProvider.js
22168
+ init_appLifecycle();
22006
22169
  init_deviceId();
22007
22170
  init_api();
22008
22171
  init_SessionManager();
@@ -34236,7 +34399,7 @@ function useOtaUpdate(options) {
34236
34399
  setIsSyncing(true);
34237
34400
  setDownloadProgress(0);
34238
34401
  try {
34239
- const result = await otaOrchestrator.sync(options);
34402
+ const result = await otaOrchestrator.sync(optionsRef.current);
34240
34403
  setSyncResult(result);
34241
34404
  if (result.isMandatory && result.status === "UPDATE_INSTALLED" && optionsRef.current.mandatoryBlocksUi) {
34242
34405
  setMandatoryUpdatePending(true);
@@ -34245,7 +34408,7 @@ function useOtaUpdate(options) {
34245
34408
  } finally {
34246
34409
  setIsSyncing(false);
34247
34410
  }
34248
- }, [options.apiUrl, options.clientKey, options.appVersion, options.installationId, options.autoRestart]);
34411
+ }, []);
34249
34412
  const restart = (0, import_react16.useCallback)(() => {
34250
34413
  setMandatoryUpdatePending(false);
34251
34414
  otaOrchestrator.restart();