@mentra/engine 3.1.0-dev.20 → 3.1.0-dev.22

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 (69) 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 +2 -2
  7. package/build/facades/ota.d.ts +4 -4
  8. package/build/facades/ota.d.ts.map +1 -1
  9. package/build/facades/ota.js +3 -4
  10. package/build/facades/ota.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +4 -4
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/react/MentraLiveOtaFlow.d.ts +3 -2
  14. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  15. package/build/react/MentraLiveOtaFlow.js +68 -345
  16. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  17. package/build/react/index.d.ts +1 -0
  18. package/build/react/index.d.ts.map +1 -1
  19. package/build/react/index.js +1 -0
  20. package/build/react/index.js.map +1 -1
  21. package/build/react/useMentraLiveOta.d.ts +69 -0
  22. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  23. package/build/react/useMentraLiveOta.js +485 -0
  24. package/build/react/useMentraLiveOta.js.map +1 -0
  25. package/build/services/GlassesStatusProjection.d.ts +15 -2
  26. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  27. package/build/services/GlassesStatusProjection.js +3 -3
  28. package/build/services/GlassesStatusProjection.js.map +1 -1
  29. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  30. package/build/services/HotspotOtaTransport.js +8 -7
  31. package/build/services/HotspotOtaTransport.js.map +1 -1
  32. package/build/services/OtaInstallCoordinator.d.ts +24 -3
  33. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  34. package/build/services/OtaInstallCoordinator.js +14 -12
  35. package/build/services/OtaInstallCoordinator.js.map +1 -1
  36. package/build/services/OtaService.d.ts.map +1 -1
  37. package/build/services/OtaService.js +1 -1
  38. package/build/services/OtaService.js.map +1 -1
  39. package/build/services/OtaUpdateCheckService.d.ts +1 -1
  40. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  41. package/build/services/OtaUpdateCheckService.js +6 -2
  42. package/build/services/OtaUpdateCheckService.js.map +1 -1
  43. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  44. package/build/services/asg/localNetworkTransport.js +13 -12
  45. package/build/services/asg/localNetworkTransport.js.map +1 -1
  46. package/build/services/glassesClockSync.js +1 -1
  47. package/build/services/glassesClockSync.js.map +1 -1
  48. package/build/stores/core.d.ts +3 -3
  49. package/build/stores/core.d.ts.map +1 -1
  50. package/build/stores/core.js.map +1 -1
  51. package/build/stores/glasses.d.ts +10 -4
  52. package/build/stores/glasses.d.ts.map +1 -1
  53. package/build/stores/glasses.js.map +1 -1
  54. package/package.json +12 -6
  55. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  56. package/src/facades/ota.ts +3 -4
  57. package/src/generated/releaseMetadata.ts +4 -4
  58. package/src/react/MentraLiveOtaFlow.tsx +116 -466
  59. package/src/react/index.ts +13 -0
  60. package/src/react/useMentraLiveOta.ts +619 -0
  61. package/src/services/GlassesStatusProjection.ts +7 -6
  62. package/src/services/HotspotOtaTransport.ts +8 -7
  63. package/src/services/OtaInstallCoordinator.ts +16 -13
  64. package/src/services/OtaService.ts +1 -2
  65. package/src/services/OtaUpdateCheckService.ts +8 -5
  66. package/src/services/asg/localNetworkTransport.ts +20 -19
  67. package/src/services/glassesClockSync.ts +1 -1
  68. package/src/stores/core.ts +4 -4
  69. package/src/stores/glasses.ts +13 -6
@@ -1,27 +1,12 @@
1
- import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"
1
+ /* eslint-disable react-native/no-raw-text -- BodyText and PercentText are local Text wrappers. */
2
+ import React, {useMemo} from "react"
2
3
  import {ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle} from "react-native"
3
4
  import {SafeAreaView} from "react-native-safe-area-context"
4
5
  import Svg, {Path, Rect} from "react-native-svg"
5
6
 
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"
23
-
24
- export type MentraLiveOtaFlowPage = "check" | "progress"
7
+ import {useMentraLiveOta, type MentraLiveOtaController, type MentraLiveOtaFlowPage} from "./useMentraLiveOta"
8
+
9
+ export type {MentraLiveOtaFlowPage} from "./useMentraLiveOta"
25
10
 
26
11
  export type MentraLiveOtaFlowTheme = {
27
12
  background: string
@@ -40,7 +25,7 @@ export type MentraLiveOtaFlowProps = {
40
25
  deviceName?: string
41
26
  /** Entry page. `progress` exists for recovery/deep-link compatibility. */
42
27
  initialPage?: MentraLiveOtaFlowPage
43
- /** Start the OTA-only projections. Full engine hosts should pass false. */
28
+ /** Start the OTA-only projections. Full Engine hosts should pass false. */
44
29
  initializeRuntime?: boolean
45
30
  /** Called after the final check or when the user leaves an optional update. */
46
31
  onFinished: () => void
@@ -59,11 +44,6 @@ export type MentraLiveOtaFlowProps = {
59
44
  style?: StyleProp<ViewStyle>
60
45
  }
61
46
 
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
47
  const DEFAULT_THEME: MentraLiveOtaFlowTheme = {
68
48
  background: "#FFFFFF",
69
49
  border: "#D7DFDA",
@@ -133,29 +113,13 @@ export function MentraLiveOtaFlow({
133
113
  translate = defaultTranslate,
134
114
  }: MentraLiveOtaFlowProps) {
135
115
  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
- }, [])
116
+ const controller = useMentraLiveOta({
117
+ initialPage,
118
+ initializeRuntime,
119
+ onFinished,
120
+ onFirmwareRestartingChange,
121
+ onOpenWifiSetup,
122
+ })
159
123
 
160
124
  return (
161
125
  <SafeAreaView style={[styles.safeArea, {backgroundColor: colors.background}, style]}>
@@ -163,257 +127,44 @@ export function MentraLiveOtaFlow({
163
127
  <View />
164
128
  <MentraMark color={colors.primary} />
165
129
  </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
- )}
130
+ <OtaFlowContent
131
+ allowDevSkip={allowDevSkip}
132
+ colors={colors}
133
+ controller={controller}
134
+ deviceName={deviceName}
135
+ superMode={superMode}
136
+ translate={translate}
137
+ />
191
138
  </SafeAreaView>
192
139
  )
193
140
  }
194
141
 
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({
142
+ function OtaFlowContent({
207
143
  allowDevSkip,
208
144
  colors,
145
+ controller,
209
146
  deviceName,
210
- generation,
211
- onFinished,
212
- onOpenWifiSetup,
213
- onStartProgress,
147
+ superMode,
214
148
  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
- }
149
+ }: {
150
+ allowDevSkip: boolean
151
+ colors: MentraLiveOtaFlowTheme
152
+ controller: MentraLiveOtaController
153
+ deviceName: string
154
+ superMode: boolean
155
+ translate: MentraLiveOtaFlowTranslate
156
+ }) {
157
+ const {state} = controller
395
158
 
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()
159
+ if (state.screen === "initializing") {
160
+ return (
161
+ <FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
162
+ <ActivityIndicator size="large" color={colors.foreground} />
163
+ </FlowPage>
164
+ )
414
165
  }
415
166
 
416
- if (checkState === "checking") {
167
+ if (state.screen === "checking") {
417
168
  return (
418
169
  <FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
419
170
  <BodyText colors={colors}>{translate("ota:checkingForUpdatesMessage")}</BodyText>
@@ -422,41 +173,41 @@ function OtaCheckPage({
422
173
  )
423
174
  }
424
175
 
425
- if (checkState === "update_available") {
176
+ if (state.screen === "update_available" || state.screen === "wifi_required") {
426
177
  return (
427
178
  <FlowPage
428
179
  colors={colors}
429
180
  icon="download"
430
- title={translate(isDowngradeUpdate ? "ota:downgradeAvailable" : "ota:updateAvailable", {deviceName})}
181
+ title={translate(state.versionChange ? "ota:downgradeAvailable" : "ota:updateAvailable", {deviceName})}
431
182
  actions={
432
183
  <>
433
184
  <FlowButton
434
185
  colors={colors}
435
- disabled={!glassesWifiStatusKnown}
436
- label={translate(!glassesWifiStatusKnown || canInstallUpdate ? "ota:updateNow" : "ota:setupWifi")}
437
- onPress={updateNow}
186
+ disabled={!state.wifiStatusKnown}
187
+ label={translate(state.screen === "wifi_required" ? "ota:setupWifi" : "ota:updateNow")}
188
+ onPress={state.screen === "wifi_required" ? controller.openWifiSetup : controller.install}
438
189
  />
439
- {!isUpdateRequired ? (
440
- <FlowButton colors={colors} label={translate("ota:updateLater")} onPress={onFinished} secondary />
190
+ {state.canDismiss ? (
191
+ <FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary />
441
192
  ) : null}
442
- {allowDevSkip && isUpdateRequired ? (
443
- <FlowButton colors={colors} label="Skip (dev only)" onPress={onFinished} secondary />
193
+ {allowDevSkip && state.updateRequired ? (
194
+ <FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary />
444
195
  ) : null}
445
196
  </>
446
197
  }>
447
198
  <BodyText colors={colors}>
448
- {canInstallUpdate
449
- ? translate(isDowngradeUpdate ? "ota:downgradeDescription" : "ota:updateDescription")
450
- : translate("ota:updateConnectWifi", {deviceName})}
199
+ {state.screen === "wifi_required"
200
+ ? translate("ota:updateConnectWifi", {deviceName})
201
+ : translate(state.versionChange ? "ota:downgradeDescription" : "ota:updateDescription")}
451
202
  </BodyText>
452
203
  </FlowPage>
453
204
  )
454
205
  }
455
206
 
456
- if (checkState === "dev_build") {
207
+ if (state.screen === "dev_build") {
457
208
  return (
458
209
  <FlowPage
459
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
210
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
460
211
  colors={colors}
461
212
  icon="settings"
462
213
  title={translate("ota:devBuild")}>
@@ -465,10 +216,10 @@ function OtaCheckPage({
465
216
  )
466
217
  }
467
218
 
468
- if (checkState === "no_update") {
219
+ if (state.screen === "up_to_date") {
469
220
  return (
470
221
  <FlowPage
471
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
222
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
472
223
  colors={colors}
473
224
  icon="check"
474
225
  title={translate("ota:upToDate")}>
@@ -477,10 +228,10 @@ function OtaCheckPage({
477
228
  )
478
229
  }
479
230
 
480
- if (errorKind === "pin_unavailable") {
231
+ if (state.screen === "update_info_unavailable") {
481
232
  return (
482
233
  <FlowPage
483
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished} />}
234
+ actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
484
235
  colors={colors}
485
236
  icon="alert"
486
237
  title={translate("ota:updateInfoUnavailable")}>
@@ -489,104 +240,32 @@ function OtaCheckPage({
489
240
  )
490
241
  }
491
242
 
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()
243
+ if (state.screen === "check_failed") {
244
+ return (
245
+ <FlowPage
246
+ actions={
247
+ <>
248
+ <FlowButton colors={colors} label="Retry" onPress={controller.retryCheck} />
249
+ {allowDevSkip ? (
250
+ <FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary />
251
+ ) : null}
252
+ </>
253
+ }
254
+ colors={colors}
255
+ icon="alert"
256
+ title={translate("ota:checkFailed")}>
257
+ <BodyText colors={colors}>{translate("ota:checkFailedMessage")}</BodyText>
258
+ </FlowPage>
259
+ )
581
260
  }
582
261
 
583
- if (versionChangePhase === "restarting" || versionChangePhase === "verifying") {
262
+ if (state.versionChangePhase === "restarting" || state.versionChangePhase === "verifying") {
584
263
  return (
585
264
  <FlowPage
586
265
  colors={colors}
587
266
  icon="download"
588
267
  title={translate(
589
- versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting",
268
+ state.versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting",
590
269
  )}>
591
270
  <ActivityIndicator size="large" color={colors.foreground} />
592
271
  <BodyText colors={colors}>{translate("ota:versionChangeKeepNearby")}</BodyText>
@@ -595,19 +274,19 @@ function OtaProgressPage({
595
274
  )
596
275
  }
597
276
 
598
- if (displayState === "starting") {
599
- const hotspotTitle =
600
- hotspotPhase === "downloading"
277
+ if (state.screen === "starting" || state.screen === "preparing_hotspot") {
278
+ const title =
279
+ state.hotspotPhase === "downloading"
601
280
  ? "Downloading update to phone..."
602
- : hotspotPhase === "starting_hotspot"
603
- ? "Starting glasses hotspot..."
604
- : hotspotPhase === "joining_hotspot"
605
- ? "Connecting phone to glasses..."
606
- : "Starting update..."
281
+ : state.hotspotPhase === "starting_hotspot"
282
+ ? "Starting glasses hotspot..."
283
+ : state.hotspotPhase === "joining_hotspot"
284
+ ? "Connecting phone to glasses..."
285
+ : "Starting update..."
607
286
  return (
608
- <FlowPage colors={colors} icon="download" title={hotspotTitle}>
609
- {hotspotPhase === "downloading" && hotspotArtifactPercent !== null ? (
610
- <PercentText colors={colors} percent={hotspotArtifactPercent} />
287
+ <FlowPage colors={colors} icon="download" title={title}>
288
+ {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
289
+ <PercentText colors={colors} percent={state.hotspotArtifactPercent} />
611
290
  ) : null}
612
291
  <ActivityIndicator size="large" color={colors.foreground} />
613
292
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
@@ -615,41 +294,34 @@ function OtaProgressPage({
615
294
  )
616
295
  }
617
296
 
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)
297
+ if (state.screen === "updating") {
628
298
  return (
629
- <FlowPage colors={colors} icon="download" title={isDownload ? "Downloading..." : "Installing..."}>
630
- {isApkOnlyInstalling ? (
299
+ <FlowPage colors={colors} icon="download" title={state.phase === "download" ? "Downloading..." : "Installing..."}>
300
+ {state.installingApkOnly ? (
631
301
  <ActivityIndicator size="large" color={colors.foreground} />
632
302
  ) : (
633
303
  <>
634
- <PercentText colors={colors} percent={percent} />
304
+ <PercentText colors={colors} percent={state.progress ?? 0} />
635
305
  <View style={[styles.progressTrack, {backgroundColor: colors.border}]}>
636
- <View style={[styles.progressFill, {backgroundColor: colors.primary, width: `${percent}%`}]} />
306
+ <View
307
+ style={[styles.progressFill, {backgroundColor: colors.primary, width: `${state.progress ?? 0}%`}]}
308
+ />
637
309
  </View>
638
310
  </>
639
311
  )}
640
312
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
641
- {isVersionChange && otaStatus?.phase === "install" ? (
313
+ {state.versionChange && state.phase === "install" ? (
642
314
  <BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>
643
315
  ) : null}
644
316
  </FlowPage>
645
317
  )
646
318
  }
647
319
 
648
- if (displayState === "restarting") {
320
+ if (state.screen === "restarting") {
649
321
  return (
650
322
  <FlowPage
651
323
  actions={
652
- <FlowButton colors={colors} disabled={continueButtonDisabled} label="Continue" onPress={stopAndCheck} />
324
+ <FlowButton colors={colors} disabled={state.continueDisabled} label="Continue" onPress={controller.finish} />
653
325
  }
654
326
  colors={colors}
655
327
  icon="check"
@@ -658,24 +330,24 @@ function OtaProgressPage({
658
330
  )
659
331
  }
660
332
 
661
- if (displayState === "complete") {
662
- const title = versionChangeConverged
333
+ if (state.screen === "complete") {
334
+ const title = state.versionChangeConverged
663
335
  ? translate("ota:versionChangeComplete")
664
- : isVersionChange
665
- ? translate("ota:versionChangeFirmwarePassComplete")
666
- : "Update complete!"
667
- const message = versionChangeConverged
336
+ : state.versionChange
337
+ ? translate("ota:versionChangeFirmwarePassComplete")
338
+ : "Update complete!"
339
+ const message = state.versionChangeConverged
668
340
  ? translate("ota:versionChangeCompleteMessage")
669
- : isVersionChange
670
- ? translate("ota:versionChangeFirmwarePassCompleteMessage")
671
- : "Your glasses are up to date."
341
+ : state.versionChange
342
+ ? translate("ota:versionChangeFirmwarePassCompleteMessage")
343
+ : "Your glasses are up to date."
672
344
  return (
673
345
  <FlowPage
674
346
  actions={
675
347
  <FlowButton
676
348
  colors={colors}
677
- label={isVersionChange && !versionChangeConverged ? "Continue" : "Done"}
678
- onPress={finishAndCheck}
349
+ label={state.versionChange && !state.versionChangeConverged ? "Continue" : "Done"}
350
+ onPress={controller.finish}
679
351
  />
680
352
  }
681
353
  colors={colors}
@@ -686,38 +358,25 @@ function OtaProgressPage({
686
358
  )
687
359
  }
688
360
 
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)
361
+ if (state.screen === "failed") {
695
362
  return (
696
363
  <FlowPage
697
364
  actions={
698
365
  <>
699
366
  <FlowButton
700
367
  colors={colors}
701
- label={requiresGlassesReboot ? "Done" : "Retry"}
702
- onPress={requiresGlassesReboot ? stopAndCheck : retryInstall}
368
+ label={state.canRetry ? "Retry" : "Done"}
369
+ onPress={state.canRetry ? controller.retryInstall : controller.finish}
703
370
  />
704
- {showChangeWifi ? (
705
- <FlowButton
706
- colors={colors}
707
- label="Change WiFi"
708
- onPress={() => {
709
- stopOtaAutoChain()
710
- onOpenWifiSetup()
711
- }}
712
- secondary
713
- />
371
+ {state.canOpenWifiSetup ? (
372
+ <FlowButton colors={colors} label="Change WiFi" onPress={controller.openWifiSetup} secondary />
714
373
  ) : null}
715
374
  </>
716
375
  }
717
376
  colors={colors}
718
377
  icon="alert"
719
378
  title="Update Failed">
720
- <BodyText colors={colors}>{displayedError}</BodyText>
379
+ <BodyText colors={colors}>{state.error?.message}</BodyText>
721
380
  </FlowPage>
722
381
  )
723
382
  }
@@ -726,16 +385,7 @@ function OtaProgressPage({
726
385
  <FlowPage
727
386
  actions={
728
387
  superMode ? (
729
- <FlowButton
730
- colors={colors}
731
- label="Skip (super)"
732
- onPress={() => {
733
- stopOtaAutoChain()
734
- ota.installSession.discard()
735
- onReturnToCheck()
736
- }}
737
- secondary
738
- />
388
+ <FlowButton colors={colors} label="Skip (super)" onPress={controller.discard} secondary />
739
389
  ) : undefined
740
390
  }
741
391
  colors={colors}