@scalebun/react-native 1.11.0 → 1.13.0

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.
@@ -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.11.0";
603
+ SDK_VERSION = "1.13.0";
604
604
  }
605
605
  });
606
606
 
@@ -5874,6 +5874,42 @@ var init_bootstrap = __esm({
5874
5874
  }
5875
5875
  });
5876
5876
 
5877
+ // lib/module/core/lifecycle/appLifecycle.js
5878
+ var appLifecycle_exports = {};
5879
+ __export(appLifecycle_exports, {
5880
+ appLifecycle: () => appLifecycle
5881
+ });
5882
+ var import_react_native6, AppLifecycle, appLifecycle;
5883
+ var init_appLifecycle = __esm({
5884
+ "lib/module/core/lifecycle/appLifecycle.js"() {
5885
+ "use strict";
5886
+ import_react_native6 = require("react-native");
5887
+ init_internalLogger();
5888
+ AppLifecycle = class {
5889
+ listeners = [];
5890
+ currentState = import_react_native6.AppState.currentState;
5891
+ constructor() {
5892
+ import_react_native6.AppState.addEventListener("change", this.handleStateChange);
5893
+ }
5894
+ handleStateChange = (nextState) => {
5895
+ __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
5896
+ this.currentState = nextState;
5897
+ this.listeners.forEach((l) => l(nextState));
5898
+ };
5899
+ addListener(listener) {
5900
+ this.listeners.push(listener);
5901
+ }
5902
+ removeListener(listener) {
5903
+ this.listeners = this.listeners.filter((l) => l !== listener);
5904
+ }
5905
+ getCurrentState() {
5906
+ return this.currentState;
5907
+ }
5908
+ };
5909
+ appLifecycle = new AppLifecycle();
5910
+ }
5911
+ });
5912
+
5877
5913
  // lib/module/features/crash/CrashReporter.js
5878
5914
  function getCrashReporter() {
5879
5915
  if (!_reporter) _reporter = new CrashReporter();
@@ -7472,6 +7508,11 @@ async function deliverOtaEvents(params) {
7472
7508
  kind: "ota_event",
7473
7509
  type: e.type,
7474
7510
  bundleId: e.bundleId,
7511
+ // Without this the backend stored a null releaseId on every row it ingested,
7512
+ // while serving the release id on every check — so the delivery funnel could
7513
+ // only ever be grouped by bundle, and a bundle re-promoted under a second
7514
+ // release merged the two into one indistinguishable series.
7515
+ releaseId: e.releaseId,
7475
7516
  installationId: params.installationId,
7476
7517
  // OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
7477
7518
  // resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
@@ -7573,20 +7614,31 @@ var init_OtaOrchestrator = __esm({
7573
7614
  this.bootGuardConfig = config ?? {};
7574
7615
  this.signatureConfig = config?.signature;
7575
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 ?? "?"}` : ""})`);
7576
- this.hydrateCurrentBundleFromSlots();
7617
+ const slotState = this.readSlotState();
7618
+ this.hydrateCurrentBundleFromSlots(slotState);
7577
7619
  void prefetchDeviceCountry();
7620
+ this.checkBootGuardRecovery(slotState);
7578
7621
  this.verifyRunningBundleIdentity();
7579
- this.checkBootGuardRecovery();
7580
7622
  });
7581
7623
  }
7582
7624
  /**
7583
7625
  * Read the active slot back into `currentBundle` so the next check reports
7584
7626
  * what this device is genuinely running.
7585
7627
  */
7586
- hydrateCurrentBundleFromSlots() {
7587
- if (!NativeScaleBunOta_default) return;
7628
+ /**
7629
+ * Parse the native slot state once. Returns null when the module is absent or
7630
+ * the payload is unreadable — every caller treats that as "factory bundle".
7631
+ */
7632
+ readSlotState() {
7633
+ if (!NativeScaleBunOta_default) return null;
7634
+ try {
7635
+ return JSON.parse(NativeScaleBunOta_default.getSlotState());
7636
+ } catch {
7637
+ return null;
7638
+ }
7639
+ }
7640
+ hydrateCurrentBundleFromSlots(state) {
7588
7641
  try {
7589
- const state = JSON.parse(NativeScaleBunOta_default.getSlotState());
7590
7642
  const current = state?.current;
7591
7643
  if (!current?.sha256) return;
7592
7644
  const record4 = this.readInstallRecord();
@@ -7594,6 +7646,7 @@ var init_OtaOrchestrator = __esm({
7594
7646
  this.currentBundle = {
7595
7647
  id: record4.bundleId,
7596
7648
  version: record4.version,
7649
+ releaseId: record4.releaseId ?? void 0,
7597
7650
  sha256: record4.sha256
7598
7651
  };
7599
7652
  __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
@@ -7624,29 +7677,23 @@ var init_OtaOrchestrator = __esm({
7624
7677
  verifyRunningBundleIdentity() {
7625
7678
  if (!this.currentBundle) return;
7626
7679
  const running = readRunningBundleMarker();
7627
- const expected = this.readInstallExpectation();
7628
- if (expected && expected.bundleId !== this.currentBundle.id) {
7680
+ const record4 = this.readInstallRecord();
7681
+ if (!record4) return;
7682
+ if (record4.sha256 !== this.currentBundle.sha256) {
7629
7683
  this.clearInstallExpectation();
7630
7684
  return;
7631
7685
  }
7632
- if (expected) {
7633
- if (running === expected.identityToken) {
7634
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7635
- this.clearInstallExpectation();
7636
- return;
7637
- }
7638
- 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).`);
7639
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7640
- error: `install_not_effective \u2014 expected ${expected.identityToken}, running ${running ?? "none"}`
7641
- });
7686
+ if (!record4.identityToken) return;
7687
+ if (running === record4.identityToken) {
7688
+ __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7689
+ this.retireIdentityToken(record4);
7642
7690
  return;
7643
7691
  }
7644
- if (running && running !== this.currentBundle.id) {
7645
- logger.error(`[OTA] BUNDLE MISMATCH \u2014 slot says ${this.currentBundle.id} is active but the running bundle identifies as ${running}.`);
7646
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7647
- error: `bundle_identity_mismatch \u2014 running ${running}`
7648
- });
7649
- }
7692
+ 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).`);
7693
+ otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
7694
+ releaseId: this.currentBundle.releaseId,
7695
+ error: `install_not_effective \u2014 expected ${record4.identityToken}, running ${running ?? "none"}`
7696
+ });
7650
7697
  }
7651
7698
  // ── Install expectation ────────────────────────────────────────────────────
7652
7699
  // Written just before the restart that activates a bundle, read on the next
@@ -7658,6 +7705,11 @@ var init_OtaOrchestrator = __esm({
7658
7705
  this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
7659
7706
  bundleId: bundle.id,
7660
7707
  version: bundle.version,
7708
+ // Carried so telemetry emitted on a LATER launch (a boot-guard
7709
+ // rollback, an ineffective install) can still be attributed to the
7710
+ // release, not merely the bundle. The check response is long gone by
7711
+ // then; this record is the only thing that remembers.
7712
+ releaseId: bundle.releaseId ?? null,
7661
7713
  // The join key back to the native slot, which records sha256 and
7662
7714
  // nothing else identifying.
7663
7715
  sha256: bundle.sha256,
@@ -7676,12 +7728,26 @@ var init_OtaOrchestrator = __esm({
7676
7728
  return null;
7677
7729
  }
7678
7730
  }
7679
- readInstallExpectation() {
7680
- const record4 = this.readInstallRecord();
7681
- return record4 && record4.identityToken ? {
7682
- bundleId: record4.bundleId,
7683
- identityToken: record4.identityToken
7684
- } : null;
7731
+ /**
7732
+ * Drop the identity token once the install has been proven, keeping the rest
7733
+ * of the record.
7734
+ *
7735
+ * The record does two jobs: it proves an install took effect (once), and it
7736
+ * maps the native slot's sha256 back to a bundle id (for the life of that
7737
+ * bundle). Only the first job is finished after a successful verification, so
7738
+ * only the token is retired.
7739
+ */
7740
+ retireIdentityToken(record4) {
7741
+ try {
7742
+ this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
7743
+ bundleId: record4.bundleId,
7744
+ version: record4.version,
7745
+ releaseId: record4.releaseId ?? null,
7746
+ sha256: record4.sha256,
7747
+ identityToken: null
7748
+ }));
7749
+ } catch {
7750
+ }
7685
7751
  }
7686
7752
  clearInstallExpectation() {
7687
7753
  try {
@@ -7698,17 +7764,24 @@ var init_OtaOrchestrator = __esm({
7698
7764
  * If getSlotState() shows bootMarkerPresent=false but we have a 'previous' slot
7699
7765
  * and no 'current' OTA bundle, the native layer already reverted.
7700
7766
  */
7701
- checkBootGuardRecovery() {
7702
- if (!NativeScaleBunOta_default) return;
7767
+ checkBootGuardRecovery(state) {
7703
7768
  try {
7704
- const stateJson = NativeScaleBunOta_default.getSlotState();
7705
- const state = JSON.parse(stateJson);
7706
- if (state.bootGuardReverted) {
7707
- logger.warn("[OTA] Boot guard fired \u2014 app was reverted to previous bundle");
7708
- otaEventEmitter.emitSimple("AUTO_ROLLBACK", state.previous?.bundleId ?? "unknown", {
7709
- reason: "boot_guard_crash_loop_detected"
7769
+ if (!state?.bootGuardReverted) return;
7770
+ const record4 = this.readInstallRecord();
7771
+ const revertedSha = state.bootGuardRevertedSha256;
7772
+ const matchesRecord = !!record4 && (!revertedSha || record4.sha256 === revertedSha);
7773
+ const reason = state.bootGuardRevertReason || "boot_crash_guard";
7774
+ if (matchesRecord && record4) {
7775
+ logger.warn(`[OTA] Boot guard fired \u2014 reverted away from bundle ${record4.bundleId} (v${record4.version}); reason: ${reason}`);
7776
+ otaEventEmitter.emitSimple("AUTO_ROLLBACK", record4.bundleId, {
7777
+ releaseId: record4.releaseId ?? void 0,
7778
+ version: record4.version,
7779
+ reason
7710
7780
  });
7781
+ this.clearInstallExpectation();
7782
+ return;
7711
7783
  }
7784
+ 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.`);
7712
7785
  } catch {
7713
7786
  }
7714
7787
  }
@@ -7799,6 +7872,12 @@ var init_OtaOrchestrator = __esm({
7799
7872
  };
7800
7873
  }
7801
7874
  __DEV__ && logger.debug("[OTA] Sync started\u2026");
7875
+ if (this.currentBundle?.id) {
7876
+ otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
7877
+ releaseId: this.currentBundle.releaseId,
7878
+ version: this.currentBundle.version
7879
+ });
7880
+ }
7802
7881
  const checkRes = await this.checkForUpdate(params);
7803
7882
  if (checkRes.action === "NONE") {
7804
7883
  __DEV__ && logger.debug("[OTA] App is up to date");
@@ -7809,7 +7888,9 @@ var init_OtaOrchestrator = __esm({
7809
7888
  }
7810
7889
  if (checkRes.action === "ROLLBACK") {
7811
7890
  logger.warn("[OTA] Server requested ROLLBACK \u2014 reverting to previous bundle");
7812
- otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown");
7891
+ otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown", {
7892
+ releaseId: this.currentBundle?.releaseId
7893
+ });
7813
7894
  const reverted = await NativeScaleBunOta_default.revertToPrevious();
7814
7895
  if (reverted) {
7815
7896
  this.currentBundle = null;
@@ -7833,9 +7914,14 @@ var init_OtaOrchestrator = __esm({
7833
7914
  }
7834
7915
  const bundle = checkRes.bundle;
7835
7916
  let patchUsed = false;
7917
+ otaEventEmitter.emitSimple("OFFERED", bundle.id, {
7918
+ releaseId: bundle.releaseId,
7919
+ version: bundle.version
7920
+ });
7836
7921
  const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
7837
7922
  if (!signatureOutcome.ok) {
7838
7923
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
7924
+ releaseId: bundle.releaseId,
7839
7925
  error: `Signature check failed: ${signatureOutcome.reason}`,
7840
7926
  version: bundle.version
7841
7927
  });
@@ -7846,6 +7932,7 @@ var init_OtaOrchestrator = __esm({
7846
7932
  };
7847
7933
  }
7848
7934
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
7935
+ releaseId: bundle.releaseId,
7849
7936
  version: bundle.version
7850
7937
  });
7851
7938
  const downloadStart = Date.now();
@@ -7931,6 +8018,7 @@ var init_OtaOrchestrator = __esm({
7931
8018
  postProgress(0, "FAILED");
7932
8019
  logger.error("[OTA] Staging bundle failed after retries");
7933
8020
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
8021
+ releaseId: bundle.releaseId,
7934
8022
  error: "Staging failed \u2014 SHA-256 mismatch or download error",
7935
8023
  version: bundle.version
7936
8024
  });
@@ -7943,6 +8031,7 @@ var init_OtaOrchestrator = __esm({
7943
8031
  postProgress(bundle.size, "COMPLETED");
7944
8032
  const downloadDuration = Date.now() - downloadStart;
7945
8033
  otaEventEmitter.emitSimple("DOWNLOAD_COMPLETE", bundle.id, {
8034
+ releaseId: bundle.releaseId,
7946
8035
  version: bundle.version,
7947
8036
  durationMs: downloadDuration,
7948
8037
  patchUsed
@@ -7951,6 +8040,7 @@ var init_OtaOrchestrator = __esm({
7951
8040
  if (!applied) {
7952
8041
  logger.error("[OTA] Applying update failed");
7953
8042
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
8043
+ releaseId: bundle.releaseId,
7954
8044
  error: "Atomic slot swap failed",
7955
8045
  version: bundle.version
7956
8046
  });
@@ -7964,6 +8054,7 @@ var init_OtaOrchestrator = __esm({
7964
8054
  this.isRestartRequiredState = true;
7965
8055
  this.recordInstallExpectation(bundle);
7966
8056
  otaEventEmitter.emitSimple("INSTALLED", bundle.id, {
8057
+ releaseId: bundle.releaseId,
7967
8058
  version: bundle.version
7968
8059
  });
7969
8060
  __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
@@ -8016,6 +8107,14 @@ var init_OtaOrchestrator = __esm({
8016
8107
  NativeScaleBunOta_default?.markHealthy();
8017
8108
  __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
8018
8109
  this.healthyTimer = null;
8110
+ const running = this.currentBundle;
8111
+ if (running?.id) {
8112
+ otaEventEmitter.emitSimple("BOOT_SUCCESS", running.id, {
8113
+ releaseId: running.releaseId,
8114
+ version: running.version,
8115
+ durationMs: healthyMs
8116
+ });
8117
+ }
8019
8118
  });
8020
8119
  }, healthyMs);
8021
8120
  } catch {
@@ -10386,31 +10485,8 @@ var FlushScheduler = class {
10386
10485
  }
10387
10486
  };
10388
10487
 
10389
- // lib/module/core/lifecycle/appLifecycle.js
10390
- var import_react_native6 = require("react-native");
10391
- init_internalLogger();
10392
- var AppLifecycle = class {
10393
- listeners = [];
10394
- currentState = import_react_native6.AppState.currentState;
10395
- constructor() {
10396
- import_react_native6.AppState.addEventListener("change", this.handleStateChange);
10397
- }
10398
- handleStateChange = (nextState) => {
10399
- __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
10400
- this.currentState = nextState;
10401
- this.listeners.forEach((l) => l(nextState));
10402
- };
10403
- addListener(listener) {
10404
- this.listeners.push(listener);
10405
- }
10406
- removeListener(listener) {
10407
- this.listeners = this.listeners.filter((l) => l !== listener);
10408
- }
10409
- getCurrentState() {
10410
- return this.currentState;
10411
- }
10412
- };
10413
- var appLifecycle = new AppLifecycle();
10488
+ // lib/module/bootstrap/SDKBootstrapper.js
10489
+ init_appLifecycle();
10414
10490
 
10415
10491
  // lib/module/bootstrap/FeatureRegistry.js
10416
10492
  init_internalLogger();
@@ -16896,7 +16972,7 @@ async function captureInstallReferrerOnce(sink) {
16896
16972
  }
16897
16973
 
16898
16974
  // lib/module/public/ScaleBunFacade.js
16899
- var ScaleBunFacade = class {
16975
+ var ScaleBunFacade = class _ScaleBunFacade {
16900
16976
  initialized = false;
16901
16977
  /** In-flight init promise — guards against a second init() racing before the first resolves. */
16902
16978
  _initInFlight = null;
@@ -17056,6 +17132,7 @@ var ScaleBunFacade = class {
17056
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`.");
17057
17133
  }
17058
17134
  }
17135
+ const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
17059
17136
  otaOrchestrator2.init({
17060
17137
  ...signingRequested ? {
17061
17138
  signature: {
@@ -17065,15 +17142,99 @@ var ScaleBunFacade = class {
17065
17142
  verifier
17066
17143
  }
17067
17144
  } : {},
17068
- ...typeof ota.healthyTimeoutMs === "number" ? {
17069
- healthyTimeoutMs: ota.healthyTimeoutMs
17145
+ ...healthyAfterMs !== void 0 ? {
17146
+ healthyAfterMs
17070
17147
  } : {}
17071
17148
  });
17072
17149
  logger.info("[ScaleBun] OTA enabled from init config.");
17150
+ this._startOtaChecks(ota);
17073
17151
  } catch (err) {
17074
17152
  logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
17075
17153
  }
17076
17154
  }
17155
+ /** Guards against overlapping config-driven OTA checks. */
17156
+ _otaCheckInFlight = false;
17157
+ /** Wall clock of the last config-driven check, for the foreground floor. */
17158
+ _otaLastCheckAt = 0;
17159
+ _otaForegroundListener = null;
17160
+ /**
17161
+ * Minimum gap between config-driven checks.
17162
+ *
17163
+ * A foreground transition is cheap to trigger — app switchers, permission
17164
+ * dialogs and share sheets all produce one — so an unthrottled check would
17165
+ * put a request on the hot path every time the user glanced away. Ten
17166
+ * minutes is well below any realistic release cadence and well above that
17167
+ * noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
17168
+ * which is never throttled.
17169
+ */
17170
+ static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1e3;
17171
+ /**
17172
+ * Drive OTA checks from init config: once at startup, then on each
17173
+ * foreground when `checkOnForeground` is on (the schema default).
17174
+ *
17175
+ * `appVersion` is resolved from the native bridge rather than asked of the
17176
+ * integrator, because it gates the server's `targetAppVersion` semver check
17177
+ * — sending a wrong or invented value is worse than sending none, and there
17178
+ * is no honest default. If it cannot be resolved, the check is skipped with
17179
+ * a warning instead of guessing.
17180
+ */
17181
+ _startOtaChecks(ota) {
17182
+ if (__DEV__) {
17183
+ logger.info("[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).");
17184
+ return;
17185
+ }
17186
+ const runCheck = async (trigger) => {
17187
+ if (this._otaCheckInFlight) return;
17188
+ if (trigger === "foreground" && Date.now() - this._otaLastCheckAt < _ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
17189
+ return;
17190
+ }
17191
+ const clientKey = this._clientKey;
17192
+ const apiUrl = this._apiBaseUrl;
17193
+ if (!clientKey || !apiUrl) return;
17194
+ this._otaCheckInFlight = true;
17195
+ try {
17196
+ const info = await bridgeAdapter.getDeviceInfo();
17197
+ const appVersion = info?.appVersion;
17198
+ if (!appVersion) {
17199
+ 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 }).");
17200
+ return;
17201
+ }
17202
+ this._otaLastCheckAt = Date.now();
17203
+ const {
17204
+ otaOrchestrator: otaOrchestrator2
17205
+ } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
17206
+ await otaOrchestrator2.sync({
17207
+ apiUrl,
17208
+ clientKey,
17209
+ appVersion,
17210
+ // The documented option, finally connected. Omitted means the
17211
+ // server's `default` channel, exactly as before.
17212
+ channelName: typeof ota.channelOverride === "string" ? ota.channelOverride : void 0
17213
+ // Never forced from config: the release's own installMode
17214
+ // decides when the app restarts, and yanking the screen out
17215
+ // from under a user is not a decision this switch should make.
17216
+ });
17217
+ } catch (err) {
17218
+ logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
17219
+ } finally {
17220
+ this._otaCheckInFlight = false;
17221
+ }
17222
+ };
17223
+ void runCheck("startup");
17224
+ if (ota.checkOnForeground === false) return;
17225
+ if (this._otaForegroundListener) return;
17226
+ try {
17227
+ const {
17228
+ appLifecycle: appLifecycle2
17229
+ } = (init_appLifecycle(), __toCommonJS(appLifecycle_exports));
17230
+ this._otaForegroundListener = (state) => {
17231
+ if (state === "active") void runCheck("foreground");
17232
+ };
17233
+ appLifecycle2.addListener(this._otaForegroundListener);
17234
+ } catch {
17235
+ this._otaForegroundListener = null;
17236
+ }
17237
+ }
17077
17238
  _autoEnableDebug(debugConfig) {
17078
17239
  try {
17079
17240
  const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
@@ -18485,6 +18646,7 @@ init_internalLogger();
18485
18646
 
18486
18647
  // lib/module/features/engage/engageTriggerEngine.js
18487
18648
  init_internalLogger();
18649
+ init_appLifecycle();
18488
18650
  init_AutoScreenDetector();
18489
18651
  init_StorageBackend();
18490
18652
  init_engageSignals();
@@ -18792,6 +18954,7 @@ function installEngageTriggerEngine(deps) {
18792
18954
  }
18793
18955
 
18794
18956
  // lib/module/features/engage/EngagePromptProvider.js
18957
+ init_appLifecycle();
18795
18958
  init_deviceId();
18796
18959
  init_api();
18797
18960
  init_SessionManager();
@@ -31025,7 +31188,7 @@ function useOtaUpdate(options) {
31025
31188
  setIsSyncing(true);
31026
31189
  setDownloadProgress(0);
31027
31190
  try {
31028
- const result = await otaOrchestrator.sync(options);
31191
+ const result = await otaOrchestrator.sync(optionsRef.current);
31029
31192
  setSyncResult(result);
31030
31193
  if (result.isMandatory && result.status === "UPDATE_INSTALLED" && optionsRef.current.mandatoryBlocksUi) {
31031
31194
  setMandatoryUpdatePending(true);
@@ -31034,7 +31197,7 @@ function useOtaUpdate(options) {
31034
31197
  } finally {
31035
31198
  setIsSyncing(false);
31036
31199
  }
31037
- }, [options.apiUrl, options.clientKey, options.appVersion, options.installationId, options.autoRestart]);
31200
+ }, []);
31038
31201
  const restart = (0, import_react16.useCallback)(() => {
31039
31202
  setMandatoryUpdatePending(false);
31040
31203
  otaOrchestrator.restart();
@@ -151,10 +151,19 @@ class OtaSlotManager {
151
151
  // it `bootGuardReverted` was read by the orchestrator and written by
152
152
  // nobody on iOS, so an auto-rollback that DID occur reported nothing —
153
153
  // the dashboard counted zero rollbacks while devices were reverting.
154
- let record: [String: Any] = [
154
+ // Stamp WHICH bundle is being reverted away from, read before current/ is
155
+ // removed below. Without it JS had nothing to name: slot meta carries no
156
+ // bundleId and the previous slot is gone by the time JS looks, so every
157
+ // AUTO_ROLLBACK reported bundleId "unknown" and the server dropped it as
158
+ // unresolvable — leaving the crash guard's rollback signal at zero.
159
+ var record: [String: Any] = [
155
160
  "reason": reason,
156
161
  "at": Int(Date().timeIntervalSince1970 * 1000),
157
162
  ]
163
+ if let meta = readMeta(slot: currentSlot),
164
+ let failingSha = meta["sha256"] as? String, !failingSha.isEmpty {
165
+ record["sha256"] = failingSha
166
+ }
158
167
  if let data = try? JSONSerialization.data(withJSONObject: record) {
159
168
  try? data.write(to: revertRecordFile, options: .atomic)
160
169
  }
@@ -195,14 +204,14 @@ class OtaSlotManager {
195
204
  let markerId = (markerJson["sha256"] as? String)
196
205
  ?? (markerJson["bundleId"] as? String) else {
197
206
  NSLog("[ScaleBunOta] Invalid boot marker — reverting")
198
- _ = revert()
207
+ _ = revert(reason: "invalid_marker")
199
208
  return
200
209
  }
201
210
 
202
211
  let metaURL = currentSlot.appendingPathComponent(OtaSlotManager.metaFilename)
203
212
  guard fm.fileExists(atPath: metaURL.path) else {
204
213
  NSLog("[ScaleBunOta] Boot marker present but no current/meta.json — reverting")
205
- _ = revert()
214
+ _ = revert(reason: "missing_meta")
206
215
  return
207
216
  }
208
217
 
@@ -210,7 +219,7 @@ class OtaSlotManager {
210
219
  guard let metaJson = try JSONSerialization.jsonObject(with: metaData) as? [String: Any],
211
220
  let currentId = metaJson["sha256"] as? String else {
212
221
  NSLog("[ScaleBunOta] Invalid current/meta.json — reverting")
213
- _ = revert()
222
+ _ = revert(reason: "invalid_meta")
214
223
  return
215
224
  }
216
225
 
@@ -250,7 +259,7 @@ class OtaSlotManager {
250
259
  }
251
260
  } catch {
252
261
  NSLog("[ScaleBunOta] checkBootGuard() failed — reverting: \(error)")
253
- _ = revert()
262
+ _ = revert(reason: "boot_guard_error")
254
263
  }
255
264
  }
256
265
 
@@ -273,6 +282,11 @@ class OtaSlotManager {
273
282
  state["bootGuardReverted"] = true
274
283
  state["bootGuardRevertReason"] = rec["reason"] as? String ?? "boot_guard"
275
284
  state["bootGuardRevertedAt"] = rec["at"] as? Int ?? 0
285
+ // The bundle reverted AWAY from. JS joins it back to a bundle id
286
+ // through its install record so the rollback can be reported.
287
+ if let sha = rec["sha256"] as? String, !sha.isEmpty {
288
+ state["bootGuardRevertedSha256"] = sha
289
+ }
276
290
  }
277
291
  try? fm.removeItem(at: revertRecordFile)
278
292
  }
@@ -14,5 +14,5 @@ exports.SDK_VERSION = void 0;
14
14
  * value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
15
15
  * version was introduced to solve.
16
16
  */
17
- const SDK_VERSION = exports.SDK_VERSION = '1.11.0';
17
+ const SDK_VERSION = exports.SDK_VERSION = '1.13.0';
18
18
  //# sourceMappingURL=version.js.map