@mentra/engine 3.1.0-dev.31 → 3.1.0-dev.37

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 (48) hide show
  1. package/build/engine.d.ts +3 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/ota.d.ts +5 -2
  4. package/build/facades/ota.d.ts.map +1 -1
  5. package/build/facades/ota.js +3 -0
  6. package/build/facades/ota.js.map +1 -1
  7. package/build/facades/pairing.d.ts +4 -0
  8. package/build/facades/pairing.d.ts.map +1 -1
  9. package/build/facades/pairing.js +81 -7
  10. package/build/facades/pairing.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +5 -5
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/index.d.ts +1 -1
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js.map +1 -1
  16. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  17. package/build/react/MentraLiveOtaFlow.js +39 -1
  18. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  19. package/build/react/index.d.ts +1 -1
  20. package/build/react/index.d.ts.map +1 -1
  21. package/build/react/index.js.map +1 -1
  22. package/build/react/useMentraLiveOta.d.ts +11 -0
  23. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  24. package/build/react/useMentraLiveOta.js +73 -8
  25. package/build/react/useMentraLiveOta.js.map +1 -1
  26. package/build/services/OtaAutoChain.d.ts +9 -2
  27. package/build/services/OtaAutoChain.d.ts.map +1 -1
  28. package/build/services/OtaAutoChain.js +10 -2
  29. package/build/services/OtaAutoChain.js.map +1 -1
  30. package/build/services/OtaUpdateCheckService.d.ts +3 -0
  31. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  32. package/build/services/OtaUpdateCheckService.js +12 -0
  33. package/build/services/OtaUpdateCheckService.js.map +1 -1
  34. package/build/services/otaReleaseVersion.d.ts +10 -0
  35. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  36. package/build/services/otaReleaseVersion.js +14 -0
  37. package/build/services/otaReleaseVersion.js.map +1 -0
  38. package/package.json +6 -6
  39. package/src/facades/ota.ts +13 -1
  40. package/src/facades/pairing.ts +88 -12
  41. package/src/generated/releaseMetadata.ts +5 -5
  42. package/src/index.ts +1 -0
  43. package/src/react/MentraLiveOtaFlow.tsx +72 -9
  44. package/src/react/index.ts +1 -0
  45. package/src/react/useMentraLiveOta.ts +106 -18
  46. package/src/services/OtaAutoChain.ts +26 -2
  47. package/src/services/OtaUpdateCheckService.ts +15 -0
  48. package/src/services/otaReleaseVersion.ts +22 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otaReleaseVersion.js","sourceRoot":"","sources":["../../src/services/otaReleaseVersion.ts"],"names":[],"mappings":"AAAA,MAAM,2BAA2B,GAAG,6CAA6C,CAAA;AASjF,SAAS,mBAAmB,CAAC,OAAkC;IAC7D,MAAM,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,CAAA;IAC/B,OAAO,OAAO,IAAI,2BAA2B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9E,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,wBAAwB,CAAC,KAA6B;IACpE,MAAM,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC1E,IAAI,eAAe,IAAI,KAAK,CAAC,mBAAmB,EAAE,IAAI,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QACtF,OAAO,eAAe,CAAA;IACxB,CAAC;IACD,OAAO,mBAAmB,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAA;AAC1D,CAAC","sourcesContent":["const COORDINATED_RELEASE_VERSION = /^\\d+\\.\\d+\\.\\d+(?:-(?:dev|beta)\\.[1-9]\\d*)?$/\n\ntype OtaReleaseVersionInput = {\n manifestReleaseVersion?: string | null\n manifestUrl: string\n packagedManifestUrl?: string | null\n packagedReleaseIdentity?: string | null\n}\n\nfunction validReleaseVersion(version: string | null | undefined): string | null {\n const trimmed = version?.trim()\n return trimmed && COORDINATED_RELEASE_VERSION.test(trimmed) ? trimmed : null\n}\n\n/** Resolve the label for the selected OTA pin, including stable promotion of exact beta OTA bytes. */\nexport function resolveOtaReleaseVersion(input: OtaReleaseVersionInput): string | null {\n const packagedRelease = validReleaseVersion(input.packagedReleaseIdentity)\n if (packagedRelease && input.packagedManifestUrl?.trim() === input.manifestUrl.trim()) {\n return packagedRelease\n }\n return validReleaseVersion(input.manifestReleaseVersion)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.1.0-dev.31",
3
+ "version": "3.1.0-dev.37",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,11 +97,11 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/bluetooth-sdk": "3.1.0-dev.31",
101
- "@mentra/cloud-client": "3.1.0-dev.31",
102
- "@mentra/cloud-protocol": "3.1.0-dev.31",
103
- "@mentra/crust": "3.1.0-dev.31",
104
- "@mentra/miniapp": "3.1.0-dev.31",
100
+ "@mentra/bluetooth-sdk": "3.1.0-dev.37",
101
+ "@mentra/cloud-client": "3.1.0-dev.37",
102
+ "@mentra/cloud-protocol": "3.1.0-dev.37",
103
+ "@mentra/crust": "3.1.0-dev.37",
104
+ "@mentra/miniapp": "3.1.0-dev.37",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
107
  "semver": "^7.7.4",
@@ -9,7 +9,14 @@
9
9
  * progress screen is a pure renderer over its snapshot + attach/detach/retry/finish.
10
10
  */
11
11
  import BluetoothSdk from "@mentra/bluetooth-sdk"
12
- import type {OtaProgress, OtaProgressStatus, OtaStartAckEvent, OtaStatus, OtaUpdateInfo} from "@mentra/bluetooth-sdk"
12
+ import type {
13
+ OtaProgress,
14
+ OtaProgressStatus,
15
+ OtaStartAckEvent,
16
+ OtaStatus,
17
+ OtaUpdateInfo,
18
+ ReleaseChangelog,
19
+ } from "@mentra/bluetooth-sdk"
13
20
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
14
21
  import {resolveOtaManifestUrl} from "../services/otaManifestUrl"
15
22
  import {otaInstallCoordinator, type OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
@@ -26,6 +33,7 @@ function projectSnapshot() {
26
33
  return {
27
34
  connected: isGlassesConnected(s.connection),
28
35
  buildNumber: s.buildNumber || null,
36
+ appVersion: s.appVersion || null,
29
37
  mtkFirmwareVersion: s.mtkFirmwareVersion || null,
30
38
  besFirmwareVersion: s.besFirmwareVersion || null,
31
39
  hotspotOtaVersion: s.hotspotOtaVersion ?? 0,
@@ -46,6 +54,7 @@ export type {
46
54
  OtaProgressStatus,
47
55
  OtaStatus,
48
56
  OtaUpdateInfo,
57
+ ReleaseChangelog,
49
58
  OtaInstallSnapshot,
50
59
  OtaCheckCurrentGlassesOptions,
51
60
  OtaCheckCurrentGlassesResult,
@@ -84,6 +93,9 @@ export const ota = {
84
93
  ping: () => BluetoothSdk.ping(),
85
94
  /** Resolve and compare the current glasses against the OTA manifest, then update the OTA snapshot. */
86
95
  checkForUpdates: (options?: OtaCheckCurrentGlassesOptions) => checkCurrentGlassesForUpdate(options),
96
+ /** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
97
+ getReleaseChangelogs: (fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[] =>
98
+ BluetoothSdk.getReleaseChangelogs(fromVersion, toVersion),
87
99
  /** Clear the available-update prompt state. */
88
100
  clearUpdateAvailable: () => useGlassesStore.getState().setOtaUpdateAvailable(null),
89
101
  /** Clear active progress/status before entering a fresh install flow. */
@@ -13,6 +13,7 @@ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
13
13
  import type {ConnectOptions, Device, DeviceModel, PairFailureEvent, GlassesNotReadyEvent} from "@mentra/bluetooth-sdk"
14
14
  import {useCoreStore} from "../stores/core"
15
15
  import {useGlassesStore} from "../stores/glasses"
16
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
16
17
  import {hasDefaultDevice} from "../services/DeviceStoreHydration"
17
18
  import {
18
19
  markPendingSelection,
@@ -20,18 +21,14 @@ import {
20
21
  subscribePairingIdentity,
21
22
  type IdentitySnapshot,
22
23
  } from "../services/PairingIdentity"
23
- import {
24
- isGlassesConnected,
25
- isGlassesLinkLayerBusy,
26
- isGlassesReady,
27
- waitForGlassesReady,
28
- } from "../services/GlassesReadiness"
24
+ import {isGlassesConnected, isGlassesLinkLayerBusy, isGlassesReady} from "../services/GlassesReadiness"
29
25
  import {
30
26
  logAutomaticReportSubmissionStatus,
31
27
  toAutomaticReportSubmissionStatus,
32
28
  type AutomaticReportSubmissionStatus,
33
29
  } from "../services/AutomaticReportResult"
34
30
  import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
31
+ import {ControllerTypes} from "../types"
35
32
  import {submitAutomaticReport} from "./reports"
36
33
 
37
34
  export type {IdentitySnapshot} from "../services/PairingIdentity"
@@ -60,6 +57,82 @@ function projectReadiness() {
60
57
  }
61
58
  }
62
59
 
60
+ function projectTargetReady(options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">): boolean {
61
+ const {deviceModel, deviceName} = options
62
+ const glasses = useGlassesStore.getState()
63
+
64
+ if (deviceModel === ControllerTypes.R1) {
65
+ if (!glasses.controllerConnected || !glasses.controllerFullyBooted) return false
66
+ const settings = useSettingsStore.getState()
67
+ const pairedModel = settings.getSetting(SETTINGS.default_controller.key)
68
+ const pairedName = settings.getSetting(SETTINGS.controller_device_name.key)
69
+ return (!deviceModel || pairedModel === deviceModel) && (!deviceName || pairedName === deviceName)
70
+ }
71
+
72
+ if (!isGlassesReady(glasses.connection)) return false
73
+ const identity = projectPairingIdentity()
74
+ if (!deviceModel && !deviceName) return true
75
+ return (
76
+ identity.kind === "paired" &&
77
+ (!deviceModel || identity.model === deviceModel) &&
78
+ (!deviceName || identity.name === deviceName)
79
+ )
80
+ }
81
+
82
+ function subscribeTargetReady(
83
+ options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">,
84
+ cb: (ready: boolean) => void,
85
+ ): () => void {
86
+ let last = projectTargetReady(options)
87
+ const notify = () => {
88
+ const ready = projectTargetReady(options)
89
+ if (ready === last) return
90
+ last = ready
91
+ cb(ready)
92
+ }
93
+ const unsubscribeGlasses = useGlassesStore.subscribe(notify)
94
+ const unsubscribeSettings = useSettingsStore.subscribe(notify)
95
+ return () => {
96
+ unsubscribeGlasses()
97
+ unsubscribeSettings()
98
+ }
99
+ }
100
+
101
+ async function waitForTargetReady(options: PairingReadyWaitOptions, timeoutMs: number): Promise<boolean> {
102
+ return new Promise((resolve) => {
103
+ if (options.signal?.aborted) {
104
+ resolve(false)
105
+ return
106
+ }
107
+ if (projectTargetReady(options)) {
108
+ resolve(true)
109
+ return
110
+ }
111
+
112
+ let settled = false
113
+ let timeout: ReturnType<typeof setTimeout> | null = null
114
+ let unsubscribe: (() => void) | null = null
115
+ let onAbort: (() => void) | null = null
116
+ const finish = (ready: boolean) => {
117
+ if (settled) return
118
+ settled = true
119
+ if (timeout) clearTimeout(timeout)
120
+ if (unsubscribe) unsubscribe()
121
+ if (onAbort && options.signal) options.signal.removeEventListener("abort", onAbort)
122
+ resolve(ready)
123
+ }
124
+
125
+ unsubscribe = subscribeTargetReady(options, (ready) => {
126
+ if (ready) finish(true)
127
+ })
128
+ if (options.signal) {
129
+ onAbort = () => finish(false)
130
+ options.signal.addEventListener("abort", onAbort)
131
+ }
132
+ timeout = setTimeout(() => finish(projectTargetReady(options)), timeoutMs)
133
+ })
134
+ }
135
+
63
136
  export type PairingReadinessSnapshot = ReturnType<typeof projectReadiness>
64
137
 
65
138
  export async function submitPairingBootTimeoutReport(params: {
@@ -108,12 +181,7 @@ async function waitForReadyDuringPairing(options: PairingReadyWaitOptions = {}):
108
181
  })
109
182
 
110
183
  try {
111
- const ready = await waitForGlassesReady({
112
- getConnection: () => useGlassesStore.getState().connection,
113
- subscribe: (listener) => useGlassesStore.subscribe((state) => state.connection, listener),
114
- timeoutMs,
115
- signal: options.signal,
116
- })
184
+ const ready = await waitForTargetReady(options, timeoutMs)
117
185
 
118
186
  if (!ready && !options.signal?.aborted) {
119
187
  void submitPairingBootTimeoutReport({
@@ -193,6 +261,14 @@ export const pairing = {
193
261
  cb(snap)
194
262
  })
195
263
  },
264
+ /** Whether the exact wearable or controller selected on the scan screen is ready. */
265
+ targetReady: (options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">): boolean =>
266
+ projectTargetReady(options),
267
+ /** Subscribe to readiness for the exact selected target, excluding stale devices. */
268
+ onTargetReady: (
269
+ options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">,
270
+ cb: (ready: boolean) => void,
271
+ ): (() => void) => subscribeTargetReady(options, cb),
196
272
 
197
273
  // --- pairing-identity lifecycle (the PairingIdentity read-model + the JS-owned
198
274
  // identity writes; promotion to `paired` only ever happens natively) ---
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.1.0",
15
- "releaseIdentity": "3.1.0-dev.31",
16
- "releaseSetId": "mentra-3.1.0-dev.31",
17
- "sourceCommit": "b9f6530ff3757fc1e42134ce3fb26b84636d6b84",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.31.json",
19
- "otaManifestSha256": "007e585f7e5806c6e74186a9d0249ce0f859fdf6d18adca16f8037359edfdb53"
15
+ "releaseIdentity": "3.1.0-dev.37",
16
+ "releaseSetId": "mentra-3.1.0-dev.37",
17
+ "sourceCommit": "86e6323ec7ce7730aa33b55479c40b02c4af9bc4",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.37.json",
19
+ "otaManifestSha256": "f496445881297a4be07bb1533cb9ac49dfbaea6fb5aa9d25605550406d045279"
20
20
  })
package/src/index.ts CHANGED
@@ -98,6 +98,7 @@ export type {
98
98
  OtaProgressStatus,
99
99
  OtaStatus,
100
100
  OtaUpdateInfo,
101
+ ReleaseChangelog,
101
102
  OtaSnapshot,
102
103
  OtaInstallSnapshot,
103
104
  } from "./facades/ota"
@@ -1,6 +1,15 @@
1
1
  /* eslint-disable react-native/no-raw-text -- BodyText and PercentText are local Text wrappers. */
2
2
  import React, {useMemo} from "react"
3
- import {ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle} from "react-native"
3
+ import {
4
+ ActivityIndicator,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ type StyleProp,
11
+ type ViewStyle,
12
+ } from "react-native"
4
13
  import {SafeAreaView} from "react-native-safe-area-context"
5
14
  import Svg, {Path, Rect} from "react-native-svg"
6
15
 
@@ -63,6 +72,9 @@ const ENGLISH_COPY: Record<string, string> = {
63
72
  "ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
64
73
  "ota:updateDescription":
65
74
  "A new update is available for your glasses. We recommend updating now for the best experience.",
75
+ "ota:releaseTransition": "{{fromVersion}} → {{toVersion}}",
76
+ "ota:releaseTransitionUnknown": "Current version unknown → {{toVersion}}",
77
+ "ota:updatedToVersion": "Updated to {{version}}",
66
78
  "ota:downgradeAvailable": "{{deviceName}} Version Change Required",
67
79
  "ota:downgradeDescription":
68
80
  "This app requires an earlier glasses software version. Your photos and videos will be preserved, but glasses settings will be reset and restored automatically after the change.",
@@ -195,6 +207,16 @@ function OtaFlowContent({
195
207
  ) : null}
196
208
  </>
197
209
  }>
210
+ {state.releaseTransition ? (
211
+ <BodyText colors={colors}>
212
+ {state.releaseTransition.fromVersion
213
+ ? translate("ota:releaseTransition", {
214
+ fromVersion: state.releaseTransition.fromVersion,
215
+ toVersion: state.releaseTransition.toVersion,
216
+ })
217
+ : translate("ota:releaseTransitionUnknown", {toVersion: state.releaseTransition.toVersion})}
218
+ </BodyText>
219
+ ) : null}
198
220
  <BodyText colors={colors}>
199
221
  {state.screen === "wifi_required"
200
222
  ? translate("ota:updateConnectWifi", {deviceName})
@@ -224,6 +246,12 @@ function OtaFlowContent({
224
246
  icon="check"
225
247
  title={translate("ota:upToDate")}>
226
248
  <BodyText colors={colors}>{translate("ota:noUpdatesAvailable")}</BodyText>
249
+ {state.releaseTransition ? (
250
+ <BodyText colors={colors}>
251
+ {translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
252
+ </BodyText>
253
+ ) : null}
254
+ <ChangelogList changelogs={state.changelogs} colors={colors} />
227
255
  </FlowPage>
228
256
  )
229
257
  }
@@ -279,10 +307,10 @@ function OtaFlowContent({
279
307
  state.hotspotPhase === "downloading"
280
308
  ? "Downloading update to phone..."
281
309
  : state.hotspotPhase === "starting_hotspot"
282
- ? "Starting glasses hotspot..."
283
- : state.hotspotPhase === "joining_hotspot"
284
- ? "Connecting phone to glasses..."
285
- : "Starting update..."
310
+ ? "Starting glasses hotspot..."
311
+ : state.hotspotPhase === "joining_hotspot"
312
+ ? "Connecting phone to glasses..."
313
+ : "Starting update..."
286
314
  return (
287
315
  <FlowPage colors={colors} icon="download" title={title}>
288
316
  {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
@@ -334,13 +362,13 @@ function OtaFlowContent({
334
362
  const title = state.versionChangeConverged
335
363
  ? translate("ota:versionChangeComplete")
336
364
  : state.versionChange
337
- ? translate("ota:versionChangeFirmwarePassComplete")
338
- : "Update complete!"
365
+ ? translate("ota:versionChangeFirmwarePassComplete")
366
+ : "Update complete!"
339
367
  const message = state.versionChangeConverged
340
368
  ? translate("ota:versionChangeCompleteMessage")
341
369
  : state.versionChange
342
- ? translate("ota:versionChangeFirmwarePassCompleteMessage")
343
- : "Your glasses are up to date."
370
+ ? translate("ota:versionChangeFirmwarePassCompleteMessage")
371
+ : "Your glasses are up to date."
344
372
  return (
345
373
  <FlowPage
346
374
  actions={
@@ -354,6 +382,12 @@ function OtaFlowContent({
354
382
  icon="check"
355
383
  title={title}>
356
384
  <BodyText colors={colors}>{message}</BodyText>
385
+ {state.releaseTransition ? (
386
+ <BodyText colors={colors}>
387
+ {translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
388
+ </BodyText>
389
+ ) : null}
390
+ <ChangelogList changelogs={state.changelogs} colors={colors} />
357
391
  </FlowPage>
358
392
  )
359
393
  }
@@ -426,6 +460,30 @@ function PercentText({colors, percent}: {colors: MentraLiveOtaFlowTheme; percent
426
460
  return <Text style={[styles.percent, {color: colors.primary}]}>{Math.round(percent)}%</Text>
427
461
  }
428
462
 
463
+ function ChangelogList({
464
+ changelogs,
465
+ colors,
466
+ }: {
467
+ changelogs: MentraLiveOtaController["state"]["changelogs"]
468
+ colors: MentraLiveOtaFlowTheme
469
+ }) {
470
+ if (changelogs.length === 0) return null
471
+ return (
472
+ <ScrollView contentContainerStyle={styles.changelogContent} style={styles.changelogList}>
473
+ {changelogs.map((entry) => (
474
+ <View key={entry.version} style={styles.changelogEntry}>
475
+ <Text selectable style={[styles.changelogVersion, {color: colors.foreground}]}>
476
+ {entry.version}
477
+ </Text>
478
+ <Text selectable style={[styles.changelogMarkdown, {color: colors.textDim}]}>
479
+ {entry.markdown}
480
+ </Text>
481
+ </View>
482
+ ))}
483
+ </ScrollView>
484
+ )
485
+ }
486
+
429
487
  function FlowButton({
430
488
  colors,
431
489
  disabled = false,
@@ -494,6 +552,11 @@ const styles = StyleSheet.create({
494
552
  percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
495
553
  progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
496
554
  progressFill: {borderRadius: 4, height: 8},
555
+ changelogList: {flexShrink: 1, maxHeight: 260, maxWidth: 420, width: "100%"},
556
+ changelogContent: {gap: 18, paddingVertical: 4},
557
+ changelogEntry: {gap: 8},
558
+ changelogVersion: {fontSize: 16, fontWeight: "600"},
559
+ changelogMarkdown: {fontSize: 14, lineHeight: 20},
497
560
  button: {
498
561
  alignItems: "center",
499
562
  borderRadius: 50,
@@ -12,6 +12,7 @@ export {
12
12
  type MentraLiveOtaErrorCode,
13
13
  type MentraLiveOtaHotspotPhase,
14
14
  type MentraLiveOtaInstallPhase,
15
+ type MentraLiveOtaReleaseTransition,
15
16
  type MentraLiveOtaScreen,
16
17
  type MentraLiveOtaState,
17
18
  type MentraLiveOtaStep,
@@ -6,9 +6,11 @@ import {
6
6
  clearOtaAutoChainReconnectWait,
7
7
  isOtaAutoChainActive,
8
8
  otaAutoChainFingerprint,
9
+ otaAutoChainReleaseRange,
9
10
  otaAutoChainReconnectWaitRemaining,
10
11
  stopOtaAutoChain,
11
12
  tryAdvanceOtaAutoChain,
13
+ type OtaAutoChainReleaseRange,
12
14
  } from "../services/OtaAutoChain"
13
15
  import {
14
16
  BES_INSTALL_RESTART_MESSAGE,
@@ -18,6 +20,7 @@ import {
18
20
  } from "../services/OtaErrorMapping"
19
21
  import type {OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
20
22
  import type {OtaCheckCurrentGlassesResult} from "../services/OtaUpdateCheckService"
23
+ import type {ReleaseChangelog} from "../facades/ota"
21
24
  import {useEngineSnapshot} from "./useEngineSnapshot"
22
25
 
23
26
  export type MentraLiveOtaFlowPage = "check" | "progress"
@@ -56,6 +59,13 @@ export type MentraLiveOtaHotspotPhase = "idle" | "downloading" | "starting_hotsp
56
59
  export type MentraLiveOtaInstallPhase = "download" | "install"
57
60
  export type MentraLiveOtaStep = "apk" | "mtk" | "bes"
58
61
 
62
+ export type MentraLiveOtaReleaseTransition = {
63
+ /** Current glasses software label. Temporarily backed by the reported ASG app version. */
64
+ fromVersion: string | null
65
+ /** Exact coordinated release identity for the selected OTA pin. */
66
+ toVersion: string
67
+ }
68
+
59
69
  export type MentraLiveOtaState = {
60
70
  screen: MentraLiveOtaScreen
61
71
  connected: boolean
@@ -84,6 +94,10 @@ export type MentraLiveOtaState = {
84
94
  canDiscard: boolean
85
95
  canOpenWifiSetup: boolean
86
96
  continueDisabled: boolean
97
+ /** Release labels for the offered or just-completed coordinated update. */
98
+ releaseTransition: MentraLiveOtaReleaseTransition | null
99
+ /** Release notes crossed by this update, newest first. Populated on completion. */
100
+ changelogs: ReleaseChangelog[]
87
101
  }
88
102
 
89
103
  export type UseMentraLiveOtaOptions = {
@@ -115,16 +129,44 @@ type CheckState = "checking" | "update_available" | "no_update" | "dev_build" |
115
129
  const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
116
130
  const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
117
131
 
132
+ function releaseRangeTargetVersion(result: OtaCheckCurrentGlassesResult): string | null {
133
+ return result.releaseVersion ?? result.updateInfo?.versionName ?? result.latestVersionInfo?.versionName ?? null
134
+ }
135
+
136
+ function currentReleaseVersion(appVersion: string | null): string | null {
137
+ const version = appVersion?.trim()
138
+ return version || null
139
+ }
140
+
141
+ function releaseTransitionFromRange(range: OtaAutoChainReleaseRange | null): MentraLiveOtaReleaseTransition | null {
142
+ if (!range?.releaseVersion) return null
143
+ return {fromVersion: range.fromVersion, toVersion: range.releaseVersion}
144
+ }
145
+
146
+ function releaseChangelogsForActiveChain(): ReleaseChangelog[] {
147
+ const range = otaAutoChainReleaseRange()
148
+ if (!range?.toVersion) return []
149
+ try {
150
+ return ota.getReleaseChangelogs(range.fromVersion, range.toVersion)
151
+ } catch {
152
+ try {
153
+ return ota.getReleaseChangelogs(null, range.toVersion)
154
+ } catch {
155
+ return []
156
+ }
157
+ }
158
+ }
159
+
118
160
  function installProgress(snapshot: OtaInstallSnapshot): number | null {
119
161
  if (snapshot.displayState !== "updating") return null
120
162
  const {otaStatus} = snapshot
121
163
  const isDownload = otaStatus?.phase === "download"
122
164
  const totalSteps = otaStatus?.totalSteps ?? 1
123
165
  const rawPercent = isDownload
124
- ? (otaStatus?.stepPercent ?? 0)
166
+ ? otaStatus?.stepPercent ?? 0
125
167
  : totalSteps >= 2
126
- ? (otaStatus?.overallPercent ?? 0)
127
- : (otaStatus?.stepPercent ?? 0)
168
+ ? otaStatus?.overallPercent ?? 0
169
+ : otaStatus?.stepPercent ?? 0
128
170
  return Math.min(Math.max(rawPercent, snapshot.mtkInstallStallSimulatedPercent ?? 0, 0), 100)
129
171
  }
130
172
 
@@ -169,6 +211,11 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
169
211
  const [isVersionChange, setIsVersionChange] = useState(false)
170
212
  const [errorKind, setErrorKind] = useState<"network" | "pin_unavailable">("network")
171
213
  const [updateFingerprint, setUpdateFingerprint] = useState<string | null>(null)
214
+ const [offeredReleaseTransition, setOfferedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(null)
215
+ const [completedReleaseTransition, setCompletedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(
216
+ null,
217
+ )
218
+ const [completedChangelogs, setCompletedChangelogs] = useState<ReleaseChangelog[]>([])
172
219
  const [checkGeneration, setCheckGeneration] = useState(0)
173
220
  const performCheckGenerationRef = useRef(0)
174
221
  const activeCheckKeyRef = useRef<string | null>(null)
@@ -313,13 +360,22 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
313
360
  setIsVersionChange(result.updateInfo.isDowngrade === true)
314
361
  const fingerprint = otaAutoChainFingerprint(result)
315
362
  setUpdateFingerprint(fingerprint)
363
+ if (!isOtaAutoChainActive()) {
364
+ const fromVersion = currentReleaseVersion(ota.snapshot().appVersion)
365
+ setOfferedReleaseTransition(result.releaseVersion ? {fromVersion, toVersion: result.releaseVersion} : null)
366
+ }
316
367
  if (isOtaAutoChainActive()) {
317
368
  const snapshot = ota.snapshot()
318
369
  if (!snapshot.wifiStatusKnown) return
319
370
  if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
320
371
  stopOtaAutoChain()
321
372
  } else {
322
- const admission = tryAdvanceOtaAutoChain(fingerprint, result.updateInfo.isDowngrade === true)
373
+ const admission = tryAdvanceOtaAutoChain(
374
+ fingerprint,
375
+ result.updateInfo.isDowngrade === true,
376
+ releaseRangeTargetVersion(result),
377
+ result.releaseVersion,
378
+ )
323
379
  if (admission.advance) {
324
380
  checkCompletedRef.current = true
325
381
  ota.installSession.prepare(result)
@@ -332,6 +388,10 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
332
388
  setCheckState("update_available")
333
389
  return
334
390
  }
391
+ if (isOtaAutoChainActive()) {
392
+ setCompletedChangelogs(releaseChangelogsForActiveChain())
393
+ setCompletedReleaseTransition(releaseTransitionFromRange(otaAutoChainReleaseRange()))
394
+ }
335
395
  stopOtaAutoChain()
336
396
  checkCompletedRef.current = true
337
397
  ota.clearUpdateAvailable()
@@ -397,6 +457,9 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
397
457
  }, [installSnapshot.connected, installSnapshot.displayState, page, returnToCheck])
398
458
 
399
459
  const check = useCallback(() => {
460
+ setOfferedReleaseTransition(null)
461
+ setCompletedReleaseTransition(null)
462
+ setCompletedChangelogs([])
400
463
  setPage("check")
401
464
  setCheckState("checking")
402
465
  setCheckGeneration((generation) => generation + 1)
@@ -420,9 +483,19 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
420
483
  return
421
484
  }
422
485
  ota.installSession.prepare(result)
423
- if (updateFingerprint) beginOtaAutoChain(updateFingerprint, isVersionChange)
486
+ setCompletedChangelogs([])
487
+ if (updateFingerprint) {
488
+ const fromVersion = offeredReleaseTransition
489
+ ? offeredReleaseTransition.fromVersion
490
+ : currentReleaseVersion(snapshot.appVersion)
491
+ beginOtaAutoChain(updateFingerprint, isVersionChange, {
492
+ fromVersion,
493
+ toVersion: releaseRangeTargetVersion(result),
494
+ releaseVersion: result.releaseVersion,
495
+ })
496
+ }
424
497
  navigateToProgress()
425
- }, [check, isVersionChange, navigateToProgress, page, updateFingerprint])
498
+ }, [check, isVersionChange, navigateToProgress, offeredReleaseTransition, page, updateFingerprint])
426
499
 
427
500
  const retryInstall = useCallback(() => {
428
501
  if (page !== "progress") return
@@ -490,6 +563,8 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
490
563
  canDiscard: false,
491
564
  canOpenWifiSetup: false,
492
565
  continueDisabled: false,
566
+ releaseTransition: null,
567
+ changelogs: [],
493
568
  }
494
569
  }
495
570
 
@@ -499,22 +574,22 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
499
574
  checkState === "checking"
500
575
  ? "checking"
501
576
  : checkState === "update_available"
502
- ? wifiRequired
503
- ? "wifi_required"
504
- : "update_available"
505
- : checkState === "no_update"
506
- ? "up_to_date"
507
- : checkState === "dev_build"
508
- ? "dev_build"
509
- : errorKind === "pin_unavailable"
510
- ? "update_info_unavailable"
511
- : "check_failed"
577
+ ? wifiRequired
578
+ ? "wifi_required"
579
+ : "update_available"
580
+ : checkState === "no_update"
581
+ ? "up_to_date"
582
+ : checkState === "dev_build"
583
+ ? "dev_build"
584
+ : errorKind === "pin_unavailable"
585
+ ? "update_info_unavailable"
586
+ : "check_failed"
512
587
  const error: MentraLiveOtaError | null =
513
588
  screen === "check_failed"
514
589
  ? {code: "check_failed", message: "Couldn't check for updates. Please check your connection and try again."}
515
590
  : screen === "update_info_unavailable"
516
- ? {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
517
- : null
591
+ ? {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
592
+ : null
518
593
  return {
519
594
  screen,
520
595
  connected: otaSnapshot.connected,
@@ -543,6 +618,13 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
543
618
  canDiscard: false,
544
619
  canOpenWifiSetup: screen === "wifi_required",
545
620
  continueDisabled: false,
621
+ releaseTransition:
622
+ screen === "update_available" || screen === "wifi_required"
623
+ ? offeredReleaseTransition
624
+ : screen === "up_to_date"
625
+ ? completedReleaseTransition
626
+ : null,
627
+ changelogs: screen === "up_to_date" ? completedChangelogs : [],
546
628
  }
547
629
  }
548
630
 
@@ -568,6 +650,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
568
650
  }
569
651
  : null
570
652
  const totalSteps = installSnapshot.otaStatus?.totalSteps ?? null
653
+ const changelogs = screen === "complete" ? releaseChangelogsForActiveChain() : []
571
654
  return {
572
655
  screen,
573
656
  connected: installSnapshot.connected,
@@ -599,15 +682,20 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
599
682
  canDiscard: screen === "disconnected",
600
683
  canOpenWifiSetup: screen === "failed" && showChangeWifi,
601
684
  continueDisabled: installSnapshot.continueButtonDisabled,
685
+ releaseTransition: releaseTransitionFromRange(otaAutoChainReleaseRange()),
686
+ changelogs,
602
687
  }
603
688
  }, [
604
689
  checkState,
690
+ completedChangelogs,
691
+ completedReleaseTransition,
605
692
  errorKind,
606
693
  firmwareRestarting,
607
694
  installSnapshot,
608
695
  isUpdateRequired,
609
696
  isVersionChange,
610
697
  otaSnapshot,
698
+ offeredReleaseTransition,
611
699
  page,
612
700
  runtimeReady,
613
701
  ])