@mentra/engine 3.1.0-dev.4 → 3.1.0-dev.40

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 (103) hide show
  1. package/README.md +19 -5
  2. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  3. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/ota-transport.js +2 -0
  5. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  6. package/build/engine.d.ts +5 -2
  7. package/build/engine.d.ts.map +1 -1
  8. package/build/facades/ota.d.ts +9 -6
  9. package/build/facades/ota.d.ts.map +1 -1
  10. package/build/facades/ota.js +6 -4
  11. package/build/facades/ota.js.map +1 -1
  12. package/build/facades/pairing.d.ts +4 -0
  13. package/build/facades/pairing.d.ts.map +1 -1
  14. package/build/facades/pairing.js +81 -7
  15. package/build/facades/pairing.js.map +1 -1
  16. package/build/generated/releaseMetadata.js +5 -5
  17. package/build/generated/releaseMetadata.js.map +1 -1
  18. package/build/index.d.ts +1 -1
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js.map +1 -1
  21. package/build/react/MentraLiveOtaFlow.d.ts +3 -2
  22. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  23. package/build/react/MentraLiveOtaFlow.js +113 -352
  24. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  25. package/build/react/index.d.ts +1 -0
  26. package/build/react/index.d.ts.map +1 -1
  27. package/build/react/index.js +1 -0
  28. package/build/react/index.js.map +1 -1
  29. package/build/react/useMentraLiveOta.d.ts +80 -0
  30. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  31. package/build/react/useMentraLiveOta.js +550 -0
  32. package/build/react/useMentraLiveOta.js.map +1 -0
  33. package/build/services/BlobStore.d.ts +2 -0
  34. package/build/services/BlobStore.d.ts.map +1 -1
  35. package/build/services/BlobStore.js +63 -10
  36. package/build/services/BlobStore.js.map +1 -1
  37. package/build/services/GlassesStatusProjection.d.ts +15 -2
  38. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  39. package/build/services/GlassesStatusProjection.js +3 -3
  40. package/build/services/GlassesStatusProjection.js.map +1 -1
  41. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  42. package/build/services/HotspotOtaTransport.js +8 -7
  43. package/build/services/HotspotOtaTransport.js.map +1 -1
  44. package/build/services/OtaAutoChain.d.ts +9 -2
  45. package/build/services/OtaAutoChain.d.ts.map +1 -1
  46. package/build/services/OtaAutoChain.js +10 -2
  47. package/build/services/OtaAutoChain.js.map +1 -1
  48. package/build/services/OtaInstallCoordinator.d.ts +24 -3
  49. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  50. package/build/services/OtaInstallCoordinator.js +14 -12
  51. package/build/services/OtaInstallCoordinator.js.map +1 -1
  52. package/build/services/OtaService.d.ts.map +1 -1
  53. package/build/services/OtaService.js +1 -1
  54. package/build/services/OtaService.js.map +1 -1
  55. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  56. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  57. package/build/services/OtaUpdateCheckService.js +21 -5
  58. package/build/services/OtaUpdateCheckService.js.map +1 -1
  59. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  60. package/build/services/asg/localNetworkTransport.js +13 -12
  61. package/build/services/asg/localNetworkTransport.js.map +1 -1
  62. package/build/services/glassesClockSync.js +1 -1
  63. package/build/services/glassesClockSync.js.map +1 -1
  64. package/build/services/otaReleaseVersion.d.ts +10 -0
  65. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  66. package/build/services/otaReleaseVersion.js +14 -0
  67. package/build/services/otaReleaseVersion.js.map +1 -0
  68. package/build/stores/appVisibility.d.ts +3 -0
  69. package/build/stores/appVisibility.d.ts.map +1 -0
  70. package/build/stores/appVisibility.js +3 -0
  71. package/build/stores/appVisibility.js.map +1 -0
  72. package/build/stores/apps.d.ts.map +1 -1
  73. package/build/stores/apps.js +7 -1
  74. package/build/stores/apps.js.map +1 -1
  75. package/build/stores/core.d.ts +3 -3
  76. package/build/stores/core.d.ts.map +1 -1
  77. package/build/stores/core.js.map +1 -1
  78. package/build/stores/glasses.d.ts +10 -4
  79. package/build/stores/glasses.d.ts.map +1 -1
  80. package/build/stores/glasses.js.map +1 -1
  81. package/package.json +12 -6
  82. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  83. package/src/facades/ota.ts +16 -5
  84. package/src/facades/pairing.ts +88 -12
  85. package/src/generated/releaseMetadata.ts +5 -5
  86. package/src/index.ts +1 -0
  87. package/src/react/MentraLiveOtaFlow.tsx +178 -465
  88. package/src/react/index.ts +14 -0
  89. package/src/react/useMentraLiveOta.ts +707 -0
  90. package/src/services/BlobStore.ts +57 -10
  91. package/src/services/GlassesStatusProjection.ts +7 -6
  92. package/src/services/HotspotOtaTransport.ts +8 -7
  93. package/src/services/OtaAutoChain.ts +26 -2
  94. package/src/services/OtaInstallCoordinator.ts +16 -13
  95. package/src/services/OtaService.ts +1 -2
  96. package/src/services/OtaUpdateCheckService.ts +26 -8
  97. package/src/services/asg/localNetworkTransport.ts +20 -19
  98. package/src/services/glassesClockSync.ts +1 -1
  99. package/src/services/otaReleaseVersion.ts +22 -0
  100. package/src/stores/appVisibility.ts +3 -0
  101. package/src/stores/apps.ts +7 -1
  102. package/src/stores/core.ts +4 -4
  103. package/src/stores/glasses.ts +13 -6
@@ -1,27 +1,21 @@
1
- import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"
2
- import {ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle} from "react-native"
1
+ /* eslint-disable react-native/no-raw-text -- BodyText and PercentText are local Text wrappers. */
2
+ import React, {useMemo} from "react"
3
+ import {
4
+ ActivityIndicator,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ type StyleProp,
11
+ type ViewStyle,
12
+ } from "react-native"
3
13
  import {SafeAreaView} from "react-native-safe-area-context"
4
14
  import Svg, {Path, Rect} from "react-native-svg"
5
15
 
6
- import {ota} from "../facades/ota"
7
- import {
8
- beginOtaAutoChain,
9
- clearOtaAutoChainReconnectWait,
10
- isOtaAutoChainActive,
11
- otaAutoChainFingerprint,
12
- otaAutoChainReconnectWaitRemaining,
13
- stopOtaAutoChain,
14
- tryAdvanceOtaAutoChain,
15
- } from "../services/OtaAutoChain"
16
- import {
17
- BES_INSTALL_RESTART_MESSAGE,
18
- getOtaErrorMessage,
19
- shouldRequireGlassesRebootForBesFailure,
20
- shouldShowChangeWifiForOtaDownloadFailure,
21
- } from "../services/OtaErrorMapping"
22
- import {useEngineSnapshot} from "./useEngineSnapshot"
16
+ import {useMentraLiveOta, type MentraLiveOtaController, type MentraLiveOtaFlowPage} from "./useMentraLiveOta"
23
17
 
24
- export type MentraLiveOtaFlowPage = "check" | "progress"
18
+ export type {MentraLiveOtaFlowPage} from "./useMentraLiveOta"
25
19
 
26
20
  export type MentraLiveOtaFlowTheme = {
27
21
  background: string
@@ -40,7 +34,7 @@ export type MentraLiveOtaFlowProps = {
40
34
  deviceName?: string
41
35
  /** Entry page. `progress` exists for recovery/deep-link compatibility. */
42
36
  initialPage?: MentraLiveOtaFlowPage
43
- /** Start the OTA-only projections. Full engine hosts should pass false. */
37
+ /** Start the OTA-only projections. Full Engine hosts should pass false. */
44
38
  initializeRuntime?: boolean
45
39
  /** Called after the final check or when the user leaves an optional update. */
46
40
  onFinished: () => void
@@ -59,11 +53,6 @@ export type MentraLiveOtaFlowProps = {
59
53
  style?: StyleProp<ViewStyle>
60
54
  }
61
55
 
62
- type CheckState = "checking" | "update_available" | "no_update" | "dev_build" | "error"
63
-
64
- const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
65
- const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
66
-
67
56
  const DEFAULT_THEME: MentraLiveOtaFlowTheme = {
68
57
  background: "#FFFFFF",
69
58
  border: "#D7DFDA",
@@ -83,6 +72,9 @@ const ENGLISH_COPY: Record<string, string> = {
83
72
  "ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
84
73
  "ota:updateDescription":
85
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}}",
86
78
  "ota:downgradeAvailable": "{{deviceName}} Version Change Required",
87
79
  "ota:downgradeDescription":
88
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.",
@@ -133,29 +125,13 @@ export function MentraLiveOtaFlow({
133
125
  translate = defaultTranslate,
134
126
  }: MentraLiveOtaFlowProps) {
135
127
  const colors = useMemo(() => ({...DEFAULT_THEME, ...theme}), [theme])
136
- const [page, setPage] = useState<MentraLiveOtaFlowPage>(initialPage)
137
- const [checkGeneration, setCheckGeneration] = useState(0)
138
- const [runtimeReady, setRuntimeReady] = useState(!initializeRuntime)
139
-
140
- useEffect(() => {
141
- if (!initializeRuntime) {
142
- setRuntimeReady(true)
143
- return
144
- }
145
- let cancelled = false
146
- setRuntimeReady(false)
147
- void ota.initialize().finally(() => {
148
- if (!cancelled) setRuntimeReady(true)
149
- })
150
- return () => {
151
- cancelled = true
152
- }
153
- }, [initializeRuntime])
154
-
155
- const showCheck = useCallback(() => {
156
- setPage("check")
157
- setCheckGeneration((generation) => generation + 1)
158
- }, [])
128
+ const controller = useMentraLiveOta({
129
+ initialPage,
130
+ initializeRuntime,
131
+ onFinished,
132
+ onFirmwareRestartingChange,
133
+ onOpenWifiSetup,
134
+ })
159
135
 
160
136
  return (
161
137
  <SafeAreaView style={[styles.safeArea, {backgroundColor: colors.background}, style]}>
@@ -163,257 +139,44 @@ export function MentraLiveOtaFlow({
163
139
  <View />
164
140
  <MentraMark color={colors.primary} />
165
141
  </View>
166
- {!runtimeReady ? (
167
- <FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
168
- <ActivityIndicator size="large" color={colors.foreground} />
169
- </FlowPage>
170
- ) : page === "check" ? (
171
- <OtaCheckPage
172
- allowDevSkip={allowDevSkip}
173
- colors={colors}
174
- deviceName={deviceName}
175
- generation={checkGeneration}
176
- onFinished={onFinished}
177
- onOpenWifiSetup={onOpenWifiSetup}
178
- onStartProgress={() => setPage("progress")}
179
- translate={translate}
180
- />
181
- ) : (
182
- <OtaProgressPage
183
- colors={colors}
184
- onFirmwareRestartingChange={onFirmwareRestartingChange}
185
- onOpenWifiSetup={onOpenWifiSetup}
186
- onReturnToCheck={showCheck}
187
- superMode={superMode}
188
- translate={translate}
189
- />
190
- )}
142
+ <OtaFlowContent
143
+ allowDevSkip={allowDevSkip}
144
+ colors={colors}
145
+ controller={controller}
146
+ deviceName={deviceName}
147
+ superMode={superMode}
148
+ translate={translate}
149
+ />
191
150
  </SafeAreaView>
192
151
  )
193
152
  }
194
153
 
195
- type CheckPageProps = {
196
- allowDevSkip: boolean
197
- colors: MentraLiveOtaFlowTheme
198
- deviceName: string
199
- generation: number
200
- onFinished: () => void
201
- onOpenWifiSetup: () => void
202
- onStartProgress: () => void
203
- translate: MentraLiveOtaFlowTranslate
204
- }
205
-
206
- function OtaCheckPage({
154
+ function OtaFlowContent({
207
155
  allowDevSkip,
208
156
  colors,
157
+ controller,
209
158
  deviceName,
210
- generation,
211
- onFinished,
212
- onOpenWifiSetup,
213
- onStartProgress,
159
+ superMode,
214
160
  translate,
215
- }: CheckPageProps) {
216
- const otaSnapshot = useEngineSnapshot(ota.snapshot, ota.onSnapshot)
217
- const [checkState, setCheckState] = useState<CheckState>("checking")
218
- const [isUpdateRequired, setIsUpdateRequired] = useState(true)
219
- const [isDowngradeUpdate, setIsDowngradeUpdate] = useState(false)
220
- const [errorKind, setErrorKind] = useState<"network" | "pin_unavailable">("network")
221
- const [updateFingerprint, setUpdateFingerprint] = useState<string | null>(null)
222
- const [retryGeneration, setRetryGeneration] = useState(0)
223
- const performCheckGenerationRef = useRef(0)
224
- const activeCheckKeyRef = useRef<string | null>(null)
225
- const checkStartedRef = useRef(false)
226
- const checkCompletedRef = useRef(false)
227
- const selectedCheckResultRef = useRef<Awaited<ReturnType<typeof ota.checkForUpdates>> | null>(null)
228
- const onFinishedRef = useRef(onFinished)
229
- const onStartProgressRef = useRef(onStartProgress)
230
- onFinishedRef.current = onFinished
231
- onStartProgressRef.current = onStartProgress
232
-
233
- const glassesWifiConnected = otaSnapshot.wifiConnected
234
- const glassesWifiStatusKnown = otaSnapshot.wifiStatusKnown
235
- const hotspotOtaSupported = otaSnapshot.hotspotOtaVersion === 1
236
- const canInstallUpdate = glassesWifiStatusKnown && (glassesWifiConnected || hotspotOtaSupported)
237
- const checkKey = `${generation}:${retryGeneration}`
238
-
239
- const navigateToProgress = useCallback(() => {
240
- ota.clearProgress()
241
- onStartProgressRef.current()
242
- }, [])
243
-
244
- useEffect(() => {
245
- const MIN_DISPLAY_TIME_MS = 1100
246
- const MAX_WAIT_FOR_VERSION_INFO_MS = 10_000
247
- if (activeCheckKeyRef.current !== checkKey) {
248
- activeCheckKeyRef.current = checkKey
249
- checkStartedRef.current = false
250
- checkCompletedRef.current = false
251
- setCheckState("checking")
252
- }
253
-
254
- const myGeneration = ++performCheckGenerationRef.current
255
- let cancelled = false
256
- let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
257
-
258
- const performCheck = async () => {
259
- if (checkCompletedRef.current) return
260
- if (!otaSnapshot.connected) {
261
- if (isOtaAutoChainActive()) {
262
- const remainingMs = otaAutoChainReconnectWaitRemaining()
263
- if (remainingMs === null) return
264
- reconnectTimeout = setTimeout(() => {
265
- if (cancelled || myGeneration !== performCheckGenerationRef.current || ota.snapshot().connected) return
266
- stopOtaAutoChain()
267
- checkCompletedRef.current = true
268
- setCheckState("error")
269
- }, remainingMs)
270
- return
271
- }
272
- if (checkStartedRef.current) {
273
- stopOtaAutoChain()
274
- checkCompletedRef.current = true
275
- setCheckState("error")
276
- return
277
- }
278
- checkCompletedRef.current = true
279
- onFinishedRef.current()
280
- return
281
- }
282
-
283
- clearOtaAutoChainReconnectWait()
284
- checkStartedRef.current = true
285
- const startTime = Date.now()
286
- try {
287
- let result = await ota.checkForUpdates({
288
- waitForBuildNumberMs: MAX_WAIT_FOR_VERSION_INFO_MS,
289
- waitForBesVersionMs: 5000,
290
- waitForMtkVersionMs: 2000,
291
- refreshVersionInfo: true,
292
- fixClockBeforeCheck: false,
293
- })
294
- if (cancelled || myGeneration !== performCheckGenerationRef.current) return
295
- if (!result.hasCheckCompleted && result.checkFailureReason === "network" && isOtaAutoChainActive()) {
296
- await new Promise((resolve) => setTimeout(resolve, AUTO_CHAIN_NETWORK_RETRY_DELAY_MS))
297
- if (cancelled || myGeneration !== performCheckGenerationRef.current) return
298
- result = await ota.checkForUpdates({
299
- waitForBuildNumberMs: MAX_WAIT_FOR_VERSION_INFO_MS,
300
- waitForBesVersionMs: 5000,
301
- waitForMtkVersionMs: 2000,
302
- refreshVersionInfo: true,
303
- fixClockBeforeCheck: false,
304
- })
305
- }
306
- if (cancelled || myGeneration !== performCheckGenerationRef.current) return
307
- selectedCheckResultRef.current = result
308
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))))
309
- if (cancelled || myGeneration !== performCheckGenerationRef.current) return
310
-
311
- if (result.skippedReason === "disconnected") {
312
- stopOtaAutoChain()
313
- checkCompletedRef.current = true
314
- setCheckState("error")
315
- return
316
- }
317
- if (result.skippedReason === "missing_build") {
318
- checkCompletedRef.current = true
319
- if (isOtaAutoChainActive()) {
320
- stopOtaAutoChain()
321
- setCheckState("error")
322
- } else {
323
- onFinishedRef.current()
324
- }
325
- return
326
- }
327
- if (result.skippedReason === "dev_build") {
328
- stopOtaAutoChain()
329
- checkCompletedRef.current = true
330
- ota.clearUpdateAvailable()
331
- setCheckState("dev_build")
332
- return
333
- }
334
- if (!result.hasCheckCompleted) {
335
- stopOtaAutoChain()
336
- checkCompletedRef.current = true
337
- setErrorKind(result.checkFailureReason === "pin_unavailable" ? "pin_unavailable" : "network")
338
- setCheckState("error")
339
- return
340
- }
341
- if (result.updateAvailable && result.updateInfo) {
342
- setIsUpdateRequired(result.isRequired)
343
- setIsDowngradeUpdate(result.updateInfo.isDowngrade === true)
344
- const fingerprint = otaAutoChainFingerprint(result)
345
- setUpdateFingerprint(fingerprint)
346
- if (isOtaAutoChainActive()) {
347
- const snapshot = ota.snapshot()
348
- if (!snapshot.wifiStatusKnown) return
349
- if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
350
- stopOtaAutoChain()
351
- } else {
352
- const admission = tryAdvanceOtaAutoChain(fingerprint, result.updateInfo.isDowngrade === true)
353
- if (admission.advance) {
354
- checkCompletedRef.current = true
355
- ota.installSession.prepare(result)
356
- navigateToProgress()
357
- return
358
- }
359
- }
360
- }
361
- checkCompletedRef.current = true
362
- setCheckState("update_available")
363
- return
364
- }
365
- stopOtaAutoChain()
366
- checkCompletedRef.current = true
367
- ota.clearUpdateAvailable()
368
- setCheckState("no_update")
369
- } catch (error) {
370
- console.error("OTA check failed:", error)
371
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))))
372
- if (cancelled || myGeneration !== performCheckGenerationRef.current) return
373
- stopOtaAutoChain()
374
- checkCompletedRef.current = true
375
- setCheckState("error")
376
- }
377
- }
378
-
379
- void performCheck()
380
- return () => {
381
- cancelled = true
382
- if (reconnectTimeout) clearTimeout(reconnectTimeout)
383
- }
384
- }, [
385
- checkKey,
386
- otaSnapshot.connected,
387
- otaSnapshot.wifiStatusKnown,
388
- navigateToProgress,
389
- ])
390
-
391
- const retry = () => {
392
- setCheckState("checking")
393
- setRetryGeneration((value) => value + 1)
394
- }
161
+ }: {
162
+ allowDevSkip: boolean
163
+ colors: MentraLiveOtaFlowTheme
164
+ controller: MentraLiveOtaController
165
+ deviceName: string
166
+ superMode: boolean
167
+ translate: MentraLiveOtaFlowTranslate
168
+ }) {
169
+ const {state} = controller
395
170
 
396
- const updateNow = () => {
397
- const result = selectedCheckResultRef.current
398
- if (!result) {
399
- setCheckState("error")
400
- return
401
- }
402
- const snapshot = ota.snapshot()
403
- if (!snapshot.wifiStatusKnown) {
404
- retry()
405
- return
406
- }
407
- if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
408
- onOpenWifiSetup()
409
- return
410
- }
411
- ota.installSession.prepare(result)
412
- if (updateFingerprint) beginOtaAutoChain(updateFingerprint, isDowngradeUpdate)
413
- navigateToProgress()
171
+ if (state.screen === "initializing") {
172
+ return (
173
+ <FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
174
+ <ActivityIndicator size="large" color={colors.foreground} />
175
+ </FlowPage>
176
+ )
414
177
  }
415
178
 
416
- if (checkState === "checking") {
179
+ if (state.screen === "checking") {
417
180
  return (
418
181
  <FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
419
182
  <BodyText colors={colors}>{translate("ota:checkingForUpdatesMessage")}</BodyText>
@@ -422,41 +185,51 @@ function OtaCheckPage({
422
185
  )
423
186
  }
424
187
 
425
- if (checkState === "update_available") {
188
+ if (state.screen === "update_available" || state.screen === "wifi_required") {
426
189
  return (
427
190
  <FlowPage
428
191
  colors={colors}
429
192
  icon="download"
430
- title={translate(isDowngradeUpdate ? "ota:downgradeAvailable" : "ota:updateAvailable", {deviceName})}
193
+ title={translate(state.versionChange ? "ota:downgradeAvailable" : "ota:updateAvailable", {deviceName})}
431
194
  actions={
432
195
  <>
433
196
  <FlowButton
434
197
  colors={colors}
435
- disabled={!glassesWifiStatusKnown}
436
- label={translate(!glassesWifiStatusKnown || canInstallUpdate ? "ota:updateNow" : "ota:setupWifi")}
437
- onPress={updateNow}
198
+ disabled={!state.wifiStatusKnown}
199
+ label={translate(state.screen === "wifi_required" ? "ota:setupWifi" : "ota:updateNow")}
200
+ onPress={state.screen === "wifi_required" ? controller.openWifiSetup : controller.install}
438
201
  />
439
- {!isUpdateRequired ? (
440
- <FlowButton colors={colors} label={translate("ota:updateLater")} onPress={onFinished} secondary />
202
+ {state.canDismiss ? (
203
+ <FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary />
441
204
  ) : null}
442
- {allowDevSkip && isUpdateRequired ? (
443
- <FlowButton colors={colors} label="Skip (dev only)" onPress={onFinished} secondary />
205
+ {allowDevSkip && state.updateRequired ? (
206
+ <FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary />
444
207
  ) : null}
445
208
  </>
446
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}
447
220
  <BodyText colors={colors}>
448
- {canInstallUpdate
449
- ? translate(isDowngradeUpdate ? "ota:downgradeDescription" : "ota:updateDescription")
450
- : translate("ota:updateConnectWifi", {deviceName})}
221
+ {state.screen === "wifi_required"
222
+ ? translate("ota:updateConnectWifi", {deviceName})
223
+ : translate(state.versionChange ? "ota:downgradeDescription" : "ota:updateDescription")}
451
224
  </BodyText>
452
225
  </FlowPage>
453
226
  )
454
227
  }
455
228
 
456
- if (checkState === "dev_build") {
229
+ if (state.screen === "dev_build") {
457
230
  return (
458
231
  <FlowPage
459
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
232
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
460
233
  colors={colors}
461
234
  icon="settings"
462
235
  title={translate("ota:devBuild")}>
@@ -465,22 +238,28 @@ function OtaCheckPage({
465
238
  )
466
239
  }
467
240
 
468
- if (checkState === "no_update") {
241
+ if (state.screen === "up_to_date") {
469
242
  return (
470
243
  <FlowPage
471
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
244
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
472
245
  colors={colors}
473
246
  icon="check"
474
247
  title={translate("ota:upToDate")}>
475
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} />
476
255
  </FlowPage>
477
256
  )
478
257
  }
479
258
 
480
- if (errorKind === "pin_unavailable") {
259
+ if (state.screen === "update_info_unavailable") {
481
260
  return (
482
261
  <FlowPage
483
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
262
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
484
263
  colors={colors}
485
264
  icon="alert"
486
265
  title={translate("ota:updateInfoUnavailable")}>
@@ -489,104 +268,32 @@ function OtaCheckPage({
489
268
  )
490
269
  }
491
270
 
492
- return (
493
- <FlowPage
494
- actions={
495
- <>
496
- <FlowButton colors={colors} label="Retry" onPress={retry} />
497
- {allowDevSkip ? <FlowButton colors={colors} label="Skip (dev only)" onPress={onFinished} secondary /> : null}
498
- </>
499
- }
500
- colors={colors}
501
- icon="alert"
502
- title={translate("ota:checkFailed")}>
503
- <BodyText colors={colors}>{translate("ota:checkFailedMessage")}</BodyText>
504
- </FlowPage>
505
- )
506
- }
507
-
508
- type ProgressPageProps = {
509
- colors: MentraLiveOtaFlowTheme
510
- onFirmwareRestartingChange?: (restarting: boolean, progressActive: boolean) => void
511
- onOpenWifiSetup: () => void
512
- onReturnToCheck: () => void
513
- superMode: boolean
514
- translate: MentraLiveOtaFlowTranslate
515
- }
516
-
517
- function OtaProgressPage({
518
- colors,
519
- onFirmwareRestartingChange,
520
- onOpenWifiSetup,
521
- onReturnToCheck,
522
- superMode,
523
- translate,
524
- }: ProgressPageProps) {
525
- const install = useEngineSnapshot(ota.installSession.snapshot, ota.installSession.onSnapshot)
526
- const autoChainAdvancedRef = useRef(false)
527
- const {
528
- connected,
529
- continueButtonDisabled,
530
- displayState,
531
- errorMsg,
532
- hotspotArtifactPercent,
533
- hotspotPhase,
534
- isVersionChange,
535
- mtkInstallStallSimulatedPercent,
536
- otaProgress,
537
- otaStatus,
538
- versionChangeConverged,
539
- versionChangePhase,
540
- } = install
541
- const firmwareRestarting = (!connected && displayState === "restarting") || versionChangePhase === "restarting"
542
-
543
- useEffect(() => {
544
- onFirmwareRestartingChange?.(firmwareRestarting, true)
545
- }, [firmwareRestarting, onFirmwareRestartingChange])
546
-
547
- useEffect(
548
- () => () => {
549
- onFirmwareRestartingChange?.(false, false)
550
- },
551
- [onFirmwareRestartingChange],
552
- )
553
-
554
- useEffect(() => {
555
- ota.installSession.attach()
556
- return () => ota.installSession.detach()
557
- }, [])
558
-
559
- useEffect(() => {
560
- if (displayState !== "complete" || !connected || !isOtaAutoChainActive() || autoChainAdvancedRef.current) return
561
- const timeout = setTimeout(() => {
562
- if (!isOtaAutoChainActive()) return
563
- autoChainAdvancedRef.current = true
564
- ota.installSession.finish()
565
- onReturnToCheck()
566
- }, AUTO_CHAIN_COMPLETE_DELAY_MS)
567
- return () => clearTimeout(timeout)
568
- }, [connected, displayState, onReturnToCheck])
569
-
570
- const finishAndCheck = () => {
571
- ota.installSession.finish()
572
- onReturnToCheck()
573
- }
574
- const stopAndCheck = () => {
575
- stopOtaAutoChain()
576
- finishAndCheck()
577
- }
578
- const retryInstall = () => {
579
- onFirmwareRestartingChange?.(false, true)
580
- ota.installSession.retry()
271
+ if (state.screen === "check_failed") {
272
+ return (
273
+ <FlowPage
274
+ actions={
275
+ <>
276
+ <FlowButton colors={colors} label="Retry" onPress={controller.retryCheck} />
277
+ {allowDevSkip ? (
278
+ <FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary />
279
+ ) : null}
280
+ </>
281
+ }
282
+ colors={colors}
283
+ icon="alert"
284
+ title={translate("ota:checkFailed")}>
285
+ <BodyText colors={colors}>{translate("ota:checkFailedMessage")}</BodyText>
286
+ </FlowPage>
287
+ )
581
288
  }
582
289
 
583
- if (versionChangePhase === "restarting" || versionChangePhase === "verifying") {
290
+ if (state.versionChangePhase === "restarting" || state.versionChangePhase === "verifying") {
584
291
  return (
585
292
  <FlowPage
586
293
  colors={colors}
587
294
  icon="download"
588
295
  title={translate(
589
- versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting",
296
+ state.versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting",
590
297
  )}>
591
298
  <ActivityIndicator size="large" color={colors.foreground} />
592
299
  <BodyText colors={colors}>{translate("ota:versionChangeKeepNearby")}</BodyText>
@@ -595,19 +302,19 @@ function OtaProgressPage({
595
302
  )
596
303
  }
597
304
 
598
- if (displayState === "starting") {
599
- const hotspotTitle =
600
- hotspotPhase === "downloading"
305
+ if (state.screen === "starting" || state.screen === "preparing_hotspot") {
306
+ const title =
307
+ state.hotspotPhase === "downloading"
601
308
  ? "Downloading update to phone..."
602
- : hotspotPhase === "starting_hotspot"
309
+ : state.hotspotPhase === "starting_hotspot"
603
310
  ? "Starting glasses hotspot..."
604
- : hotspotPhase === "joining_hotspot"
311
+ : state.hotspotPhase === "joining_hotspot"
605
312
  ? "Connecting phone to glasses..."
606
313
  : "Starting update..."
607
314
  return (
608
- <FlowPage colors={colors} icon="download" title={hotspotTitle}>
609
- {hotspotPhase === "downloading" && hotspotArtifactPercent !== null ? (
610
- <PercentText colors={colors} percent={hotspotArtifactPercent} />
315
+ <FlowPage colors={colors} icon="download" title={title}>
316
+ {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
317
+ <PercentText colors={colors} percent={state.hotspotArtifactPercent} />
611
318
  ) : null}
612
319
  <ActivityIndicator size="large" color={colors.foreground} />
613
320
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
@@ -615,41 +322,34 @@ function OtaProgressPage({
615
322
  )
616
323
  }
617
324
 
618
- if (displayState === "updating") {
619
- const isDownload = otaStatus?.phase === "download"
620
- const totalSteps = otaStatus?.totalSteps ?? 1
621
- const isApkOnlyInstalling = otaStatus?.stepType === "apk" && otaStatus.phase === "install" && totalSteps === 1
622
- const rawPercent = isDownload
623
- ? otaStatus?.stepPercent ?? 0
624
- : totalSteps >= 2
625
- ? otaStatus?.overallPercent ?? 0
626
- : otaStatus?.stepPercent ?? 0
627
- const percent = Math.min(Math.max(rawPercent, mtkInstallStallSimulatedPercent ?? 0, 0), 100)
325
+ if (state.screen === "updating") {
628
326
  return (
629
- <FlowPage colors={colors} icon="download" title={isDownload ? "Downloading..." : "Installing..."}>
630
- {isApkOnlyInstalling ? (
327
+ <FlowPage colors={colors} icon="download" title={state.phase === "download" ? "Downloading..." : "Installing..."}>
328
+ {state.installingApkOnly ? (
631
329
  <ActivityIndicator size="large" color={colors.foreground} />
632
330
  ) : (
633
331
  <>
634
- <PercentText colors={colors} percent={percent} />
332
+ <PercentText colors={colors} percent={state.progress ?? 0} />
635
333
  <View style={[styles.progressTrack, {backgroundColor: colors.border}]}>
636
- <View style={[styles.progressFill, {backgroundColor: colors.primary, width: `${percent}%`}]} />
334
+ <View
335
+ style={[styles.progressFill, {backgroundColor: colors.primary, width: `${state.progress ?? 0}%`}]}
336
+ />
637
337
  </View>
638
338
  </>
639
339
  )}
640
340
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
641
- {isVersionChange && otaStatus?.phase === "install" ? (
341
+ {state.versionChange && state.phase === "install" ? (
642
342
  <BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>
643
343
  ) : null}
644
344
  </FlowPage>
645
345
  )
646
346
  }
647
347
 
648
- if (displayState === "restarting") {
348
+ if (state.screen === "restarting") {
649
349
  return (
650
350
  <FlowPage
651
351
  actions={
652
- <FlowButton colors={colors} disabled={continueButtonDisabled} label="Continue" onPress={stopAndCheck} />
352
+ <FlowButton colors={colors} disabled={state.continueDisabled} label="Continue" onPress={controller.finish} />
653
353
  }
654
354
  colors={colors}
655
355
  icon="check"
@@ -658,15 +358,15 @@ function OtaProgressPage({
658
358
  )
659
359
  }
660
360
 
661
- if (displayState === "complete") {
662
- const title = versionChangeConverged
361
+ if (state.screen === "complete") {
362
+ const title = state.versionChangeConverged
663
363
  ? translate("ota:versionChangeComplete")
664
- : isVersionChange
364
+ : state.versionChange
665
365
  ? translate("ota:versionChangeFirmwarePassComplete")
666
366
  : "Update complete!"
667
- const message = versionChangeConverged
367
+ const message = state.versionChangeConverged
668
368
  ? translate("ota:versionChangeCompleteMessage")
669
- : isVersionChange
369
+ : state.versionChange
670
370
  ? translate("ota:versionChangeFirmwarePassCompleteMessage")
671
371
  : "Your glasses are up to date."
672
372
  return (
@@ -674,50 +374,43 @@ function OtaProgressPage({
674
374
  actions={
675
375
  <FlowButton
676
376
  colors={colors}
677
- label={isVersionChange && !versionChangeConverged ? "Continue" : "Done"}
678
- onPress={finishAndCheck}
377
+ label={state.versionChange && !state.versionChangeConverged ? "Continue" : "Done"}
378
+ onPress={controller.finish}
679
379
  />
680
380
  }
681
381
  colors={colors}
682
382
  icon="check"
683
383
  title={title}>
684
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} />
685
391
  </FlowPage>
686
392
  )
687
393
  }
688
394
 
689
- if (displayState === "failed") {
690
- const requiresGlassesReboot = shouldRequireGlassesRebootForBesFailure(otaStatus, otaProgress, errorMsg)
691
- const displayedError = requiresGlassesReboot
692
- ? BES_INSTALL_RESTART_MESSAGE
693
- : errorMsg || getOtaErrorMessage(otaStatus?.error)
694
- const showChangeWifi = shouldShowChangeWifiForOtaDownloadFailure(otaStatus, otaProgress, errorMsg)
395
+ if (state.screen === "failed") {
695
396
  return (
696
397
  <FlowPage
697
398
  actions={
698
399
  <>
699
400
  <FlowButton
700
401
  colors={colors}
701
- label={requiresGlassesReboot ? "Done" : "Retry"}
702
- onPress={requiresGlassesReboot ? stopAndCheck : retryInstall}
402
+ label={state.canRetry ? "Retry" : "Done"}
403
+ onPress={state.canRetry ? controller.retryInstall : controller.finish}
703
404
  />
704
- {showChangeWifi ? (
705
- <FlowButton
706
- colors={colors}
707
- label="Change WiFi"
708
- onPress={() => {
709
- stopOtaAutoChain()
710
- onOpenWifiSetup()
711
- }}
712
- secondary
713
- />
405
+ {state.canOpenWifiSetup ? (
406
+ <FlowButton colors={colors} label="Change WiFi" onPress={controller.openWifiSetup} secondary />
714
407
  ) : null}
715
408
  </>
716
409
  }
717
410
  colors={colors}
718
411
  icon="alert"
719
412
  title="Update Failed">
720
- <BodyText colors={colors}>{displayedError}</BodyText>
413
+ <BodyText colors={colors}>{state.error?.message}</BodyText>
721
414
  </FlowPage>
722
415
  )
723
416
  }
@@ -726,16 +419,7 @@ function OtaProgressPage({
726
419
  <FlowPage
727
420
  actions={
728
421
  superMode ? (
729
- <FlowButton
730
- colors={colors}
731
- label="Skip (super)"
732
- onPress={() => {
733
- stopOtaAutoChain()
734
- ota.installSession.discard()
735
- onReturnToCheck()
736
- }}
737
- secondary
738
- />
422
+ <FlowButton colors={colors} label="Skip (super)" onPress={controller.discard} secondary />
739
423
  ) : undefined
740
424
  }
741
425
  colors={colors}
@@ -776,6 +460,30 @@ function PercentText({colors, percent}: {colors: MentraLiveOtaFlowTheme; percent
776
460
  return <Text style={[styles.percent, {color: colors.primary}]}>{Math.round(percent)}%</Text>
777
461
  }
778
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
+
779
487
  function FlowButton({
780
488
  colors,
781
489
  disabled = false,
@@ -797,13 +505,13 @@ function FlowButton({
797
505
  style={({pressed}) => [
798
506
  styles.button,
799
507
  {
800
- backgroundColor: secondary ? colors.background : colors.primary,
801
- borderColor: secondary ? colors.border : colors.primary,
508
+ backgroundColor: secondary ? colors.background : colors.foreground,
509
+ borderColor: secondary ? colors.border : colors.foreground,
802
510
  opacity: disabled ? 0.45 : pressed ? 0.75 : 1,
803
511
  },
804
512
  ]}
805
513
  testID={`button-${label}`}>
806
- <Text style={[styles.buttonText, {color: secondary ? colors.foreground : colors.primaryText}]}>{label}</Text>
514
+ <Text style={[styles.buttonText, {color: secondary ? colors.foreground : colors.background}]}>{label}</Text>
807
515
  </Pressable>
808
516
  )
809
517
  }
@@ -844,13 +552,18 @@ const styles = StyleSheet.create({
844
552
  percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
845
553
  progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
846
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},
847
560
  button: {
848
561
  alignItems: "center",
849
- borderRadius: 10,
562
+ borderRadius: 50,
850
563
  borderWidth: 1,
851
564
  justifyContent: "center",
852
- minHeight: 48,
565
+ minHeight: 44,
853
566
  paddingHorizontal: 20,
854
567
  },
855
- buttonText: {fontSize: 15, fontWeight: "700"},
568
+ buttonText: {fontSize: 14, fontWeight: "500"},
856
569
  })