@mentra/engine 3.1.0-dev.7 → 3.1.0-dev.70

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 +23 -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 +10 -6
  9. package/build/facades/ota.d.ts.map +1 -1
  10. package/build/facades/ota.js +7 -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 +8 -2
  22. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  23. package/build/react/MentraLiveOtaFlow.js +249 -354
  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 +84 -0
  30. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  31. package/build/react/useMentraLiveOta.js +616 -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 +13 -6
  82. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  83. package/src/facades/ota.ts +17 -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 +370 -473
  88. package/src/react/index.ts +14 -0
  89. package/src/react/useMentraLiveOta.ts +781 -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,13 +1,10 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { ActivityIndicator, Pressable, StyleSheet, Text, View } 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 { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native";
4
+ import { useMarkdown } from "react-native-marked";
3
5
  import { SafeAreaView } from "react-native-safe-area-context";
4
6
  import Svg, { Path, Rect } from "react-native-svg";
5
- import { ota } from "../facades/ota";
6
- import { beginOtaAutoChain, clearOtaAutoChainReconnectWait, isOtaAutoChainActive, otaAutoChainFingerprint, otaAutoChainReconnectWaitRemaining, stopOtaAutoChain, tryAdvanceOtaAutoChain, } from "../services/OtaAutoChain";
7
- import { BES_INSTALL_RESTART_MESSAGE, getOtaErrorMessage, shouldRequireGlassesRebootForBesFailure, shouldShowChangeWifiForOtaDownloadFailure, } from "../services/OtaErrorMapping";
8
- import { useEngineSnapshot } from "./useEngineSnapshot";
9
- const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000;
10
- const AUTO_CHAIN_COMPLETE_DELAY_MS = 750;
7
+ import { MINIMUM_OTA_BATTERY_LEVEL, useMentraLiveOta, } from "./useMentraLiveOta";
11
8
  const DEFAULT_THEME = {
12
9
  background: "#FFFFFF",
13
10
  border: "#D7DFDA",
@@ -19,16 +16,29 @@ const DEFAULT_THEME = {
19
16
  };
20
17
  const ENGLISH_COPY = {
21
18
  "common:continue": "Continue",
19
+ "common:done": "Done",
22
20
  "ota:checkingForUpdates": "Checking for updates",
23
21
  "ota:checkingForUpdatesMessage": "Connected devices will perform automatic updates. Automatic updates can be disabled in Device Settings",
22
+ "ota:finishingUpdate": "Finishing your update",
23
+ "ota:checkingAdditionalUpdates": "Checking whether your glasses need any additional updates.",
24
24
  "ota:updateAvailable": "{{deviceName}} Update Available",
25
+ "ota:batteryRequiredTitle": "Charge {{deviceName}} to Update",
26
+ "ota:batteryRequiredMessage": "{{deviceName}} is currently at {{batteryLevel}}%. Charge it to at least {{minimumBatteryLevel}}% before updating.",
27
+ "ota:batteryRequiredLiveUpdate": "This screen will update automatically as the battery charges.",
25
28
  "ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
29
+ "ota:wifiRequiredTitle": "WiFi Needed for Update",
26
30
  "ota:updateDescription": "A new update is available for your glasses. We recommend updating now for the best experience.",
31
+ "ota:updateSequenceMessage": "Your glasses may install more than one update and restart several times. Keep them nearby until finished.",
32
+ "ota:releaseTransition": "{{fromVersion}} → {{toVersion}}",
33
+ "ota:releaseTransitionUnknown": "Current version unknown → {{toVersion}}",
34
+ "ota:updatedToVersion": "Updated to {{version}}",
27
35
  "ota:downgradeAvailable": "{{deviceName}} Version Change Required",
28
36
  "ota:downgradeDescription": "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.",
29
37
  "ota:updateNow": "Update Now",
30
38
  "ota:setupWifi": "Setup WiFi",
31
39
  "ota:updateLater": "Later",
40
+ "ota:updateComplete": "Update complete",
41
+ "ota:whatsNew": "What's new",
32
42
  "ota:upToDate": "Up To Date",
33
43
  "ota:devBuild": "Development Build",
34
44
  "ota:devBuildNoOta": "This mobile app is a development build, so automatic glasses updates are disabled. Use the developer settings manifest override to update them manually.",
@@ -55,410 +65,180 @@ function defaultTranslate(key, options) {
55
65
  }
56
66
  export function MentraLiveOtaFlow({ allowDevSkip = typeof __DEV__ !== "undefined" && __DEV__, deviceName = "Mentra Live", initialPage = "check", initializeRuntime = true, onFinished, onFirmwareRestartingChange, onOpenWifiSetup, style, superMode = false, theme, translate = defaultTranslate, }) {
57
67
  const colors = useMemo(() => ({ ...DEFAULT_THEME, ...theme }), [theme]);
58
- const [page, setPage] = useState(initialPage);
59
- const [checkGeneration, setCheckGeneration] = useState(0);
60
- const [runtimeReady, setRuntimeReady] = useState(!initializeRuntime);
61
- useEffect(() => {
62
- if (!initializeRuntime) {
63
- setRuntimeReady(true);
64
- return;
65
- }
66
- let cancelled = false;
67
- setRuntimeReady(false);
68
- void ota.initialize().finally(() => {
69
- if (!cancelled)
70
- setRuntimeReady(true);
71
- });
72
- return () => {
73
- cancelled = true;
74
- };
75
- }, [initializeRuntime]);
76
- const showCheck = useCallback(() => {
77
- setPage("check");
78
- setCheckGeneration((generation) => generation + 1);
79
- }, []);
68
+ const controller = useMentraLiveOta({
69
+ initialPage,
70
+ initializeRuntime,
71
+ onFinished,
72
+ onFirmwareRestartingChange,
73
+ onOpenWifiSetup,
74
+ });
80
75
  return (<SafeAreaView style={[styles.safeArea, { backgroundColor: colors.background }, style]}>
81
76
  <View style={styles.header}>
82
77
  <View />
83
78
  <MentraMark color={colors.primary}/>
84
79
  </View>
85
- {!runtimeReady ? (<FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
86
- <ActivityIndicator size="large" color={colors.foreground}/>
87
- </FlowPage>) : page === "check" ? (<OtaCheckPage allowDevSkip={allowDevSkip} colors={colors} deviceName={deviceName} generation={checkGeneration} onFinished={onFinished} onOpenWifiSetup={onOpenWifiSetup} onStartProgress={() => setPage("progress")} translate={translate}/>) : (<OtaProgressPage colors={colors} onFirmwareRestartingChange={onFirmwareRestartingChange} onOpenWifiSetup={onOpenWifiSetup} onReturnToCheck={showCheck} superMode={superMode} translate={translate}/>)}
80
+ <OtaFlowContent allowDevSkip={allowDevSkip} colors={colors} controller={controller} deviceName={deviceName} superMode={superMode} translate={translate}/>
88
81
  </SafeAreaView>);
89
82
  }
90
- function OtaCheckPage({ allowDevSkip, colors, deviceName, generation, onFinished, onOpenWifiSetup, onStartProgress, translate, }) {
91
- const otaSnapshot = useEngineSnapshot(ota.snapshot, ota.onSnapshot);
92
- const [checkState, setCheckState] = useState("checking");
93
- const [isUpdateRequired, setIsUpdateRequired] = useState(true);
94
- const [isDowngradeUpdate, setIsDowngradeUpdate] = useState(false);
95
- const [errorKind, setErrorKind] = useState("network");
96
- const [updateFingerprint, setUpdateFingerprint] = useState(null);
97
- const [retryGeneration, setRetryGeneration] = useState(0);
98
- const performCheckGenerationRef = useRef(0);
99
- const activeCheckKeyRef = useRef(null);
100
- const checkStartedRef = useRef(false);
101
- const checkCompletedRef = useRef(false);
102
- const selectedCheckResultRef = useRef(null);
103
- const onFinishedRef = useRef(onFinished);
104
- const onStartProgressRef = useRef(onStartProgress);
105
- onFinishedRef.current = onFinished;
106
- onStartProgressRef.current = onStartProgress;
107
- const glassesWifiConnected = otaSnapshot.wifiConnected;
108
- const glassesWifiStatusKnown = otaSnapshot.wifiStatusKnown;
109
- const hotspotOtaSupported = otaSnapshot.hotspotOtaVersion === 1;
110
- const canInstallUpdate = glassesWifiStatusKnown && (glassesWifiConnected || hotspotOtaSupported);
111
- const checkKey = `${generation}:${retryGeneration}`;
112
- const navigateToProgress = useCallback(() => {
113
- ota.clearProgress();
114
- onStartProgressRef.current();
115
- }, []);
116
- useEffect(() => {
117
- const MIN_DISPLAY_TIME_MS = 1100;
118
- const MAX_WAIT_FOR_VERSION_INFO_MS = 10_000;
119
- if (activeCheckKeyRef.current !== checkKey) {
120
- activeCheckKeyRef.current = checkKey;
121
- checkStartedRef.current = false;
122
- checkCompletedRef.current = false;
123
- setCheckState("checking");
124
- }
125
- const myGeneration = ++performCheckGenerationRef.current;
126
- let cancelled = false;
127
- let reconnectTimeout = null;
128
- const performCheck = async () => {
129
- if (checkCompletedRef.current)
130
- return;
131
- if (!otaSnapshot.connected) {
132
- if (isOtaAutoChainActive()) {
133
- const remainingMs = otaAutoChainReconnectWaitRemaining();
134
- if (remainingMs === null)
135
- return;
136
- reconnectTimeout = setTimeout(() => {
137
- if (cancelled || myGeneration !== performCheckGenerationRef.current || ota.snapshot().connected)
138
- return;
139
- stopOtaAutoChain();
140
- checkCompletedRef.current = true;
141
- setCheckState("error");
142
- }, remainingMs);
143
- return;
144
- }
145
- if (checkStartedRef.current) {
146
- stopOtaAutoChain();
147
- checkCompletedRef.current = true;
148
- setCheckState("error");
149
- return;
150
- }
151
- checkCompletedRef.current = true;
152
- onFinishedRef.current();
153
- return;
154
- }
155
- clearOtaAutoChainReconnectWait();
156
- checkStartedRef.current = true;
157
- const startTime = Date.now();
158
- try {
159
- let result = await ota.checkForUpdates({
160
- waitForBuildNumberMs: MAX_WAIT_FOR_VERSION_INFO_MS,
161
- waitForBesVersionMs: 5000,
162
- waitForMtkVersionMs: 2000,
163
- refreshVersionInfo: true,
164
- fixClockBeforeCheck: false,
165
- });
166
- if (cancelled || myGeneration !== performCheckGenerationRef.current)
167
- return;
168
- if (!result.hasCheckCompleted && result.checkFailureReason === "network" && isOtaAutoChainActive()) {
169
- await new Promise((resolve) => setTimeout(resolve, AUTO_CHAIN_NETWORK_RETRY_DELAY_MS));
170
- if (cancelled || myGeneration !== performCheckGenerationRef.current)
171
- return;
172
- result = await ota.checkForUpdates({
173
- waitForBuildNumberMs: MAX_WAIT_FOR_VERSION_INFO_MS,
174
- waitForBesVersionMs: 5000,
175
- waitForMtkVersionMs: 2000,
176
- refreshVersionInfo: true,
177
- fixClockBeforeCheck: false,
178
- });
179
- }
180
- if (cancelled || myGeneration !== performCheckGenerationRef.current)
181
- return;
182
- selectedCheckResultRef.current = result;
183
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))));
184
- if (cancelled || myGeneration !== performCheckGenerationRef.current)
185
- return;
186
- if (result.skippedReason === "disconnected") {
187
- stopOtaAutoChain();
188
- checkCompletedRef.current = true;
189
- setCheckState("error");
190
- return;
191
- }
192
- if (result.skippedReason === "missing_build") {
193
- checkCompletedRef.current = true;
194
- if (isOtaAutoChainActive()) {
195
- stopOtaAutoChain();
196
- setCheckState("error");
197
- }
198
- else {
199
- onFinishedRef.current();
200
- }
201
- return;
202
- }
203
- if (result.skippedReason === "dev_build") {
204
- stopOtaAutoChain();
205
- checkCompletedRef.current = true;
206
- ota.clearUpdateAvailable();
207
- setCheckState("dev_build");
208
- return;
209
- }
210
- if (!result.hasCheckCompleted) {
211
- stopOtaAutoChain();
212
- checkCompletedRef.current = true;
213
- setErrorKind(result.checkFailureReason === "pin_unavailable" ? "pin_unavailable" : "network");
214
- setCheckState("error");
215
- return;
216
- }
217
- if (result.updateAvailable && result.updateInfo) {
218
- setIsUpdateRequired(result.isRequired);
219
- setIsDowngradeUpdate(result.updateInfo.isDowngrade === true);
220
- const fingerprint = otaAutoChainFingerprint(result);
221
- setUpdateFingerprint(fingerprint);
222
- if (isOtaAutoChainActive()) {
223
- const snapshot = ota.snapshot();
224
- if (!snapshot.wifiStatusKnown)
225
- return;
226
- if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
227
- stopOtaAutoChain();
228
- }
229
- else {
230
- const admission = tryAdvanceOtaAutoChain(fingerprint, result.updateInfo.isDowngrade === true);
231
- if (admission.advance) {
232
- checkCompletedRef.current = true;
233
- ota.installSession.prepare(result);
234
- navigateToProgress();
235
- return;
236
- }
237
- }
238
- }
239
- checkCompletedRef.current = true;
240
- setCheckState("update_available");
241
- return;
242
- }
243
- stopOtaAutoChain();
244
- checkCompletedRef.current = true;
245
- ota.clearUpdateAvailable();
246
- setCheckState("no_update");
247
- }
248
- catch (error) {
249
- console.error("OTA check failed:", error);
250
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))));
251
- if (cancelled || myGeneration !== performCheckGenerationRef.current)
252
- return;
253
- stopOtaAutoChain();
254
- checkCompletedRef.current = true;
255
- setCheckState("error");
256
- }
257
- };
258
- void performCheck();
259
- return () => {
260
- cancelled = true;
261
- if (reconnectTimeout)
262
- clearTimeout(reconnectTimeout);
263
- };
264
- }, [
265
- checkKey,
266
- otaSnapshot.connected,
267
- otaSnapshot.wifiStatusKnown,
268
- navigateToProgress,
269
- ]);
270
- const retry = () => {
271
- setCheckState("checking");
272
- setRetryGeneration((value) => value + 1);
273
- };
274
- const updateNow = () => {
275
- const result = selectedCheckResultRef.current;
276
- if (!result) {
277
- setCheckState("error");
278
- return;
279
- }
280
- const snapshot = ota.snapshot();
281
- if (!snapshot.wifiStatusKnown) {
282
- retry();
283
- return;
284
- }
285
- if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
286
- onOpenWifiSetup();
287
- return;
288
- }
289
- ota.installSession.prepare(result);
290
- if (updateFingerprint)
291
- beginOtaAutoChain(updateFingerprint, isDowngradeUpdate);
292
- navigateToProgress();
293
- };
294
- if (checkState === "checking") {
83
+ function OtaFlowContent({ allowDevSkip, colors, controller, deviceName, superMode, translate, }) {
84
+ const { state } = controller;
85
+ if (state.screen === "initializing") {
86
+ return (<FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
87
+ <ActivityIndicator size="large" color={colors.foreground}/>
88
+ </FlowPage>);
89
+ }
90
+ if (state.screen === "checking") {
295
91
  return (<FlowPage colors={colors} icon="download" title={translate("ota:checkingForUpdates")}>
296
92
  <BodyText colors={colors}>{translate("ota:checkingForUpdatesMessage")}</BodyText>
297
93
  <ActivityIndicator size="large" color={colors.foreground}/>
298
94
  </FlowPage>);
299
95
  }
300
- if (checkState === "update_available") {
301
- return (<FlowPage colors={colors} icon="download" title={translate(isDowngradeUpdate ? "ota:downgradeAvailable" : "ota:updateAvailable", { deviceName })} actions={<>
302
- <FlowButton colors={colors} disabled={!glassesWifiStatusKnown} label={translate(!glassesWifiStatusKnown || canInstallUpdate ? "ota:updateNow" : "ota:setupWifi")} onPress={updateNow}/>
303
- {!isUpdateRequired ? (<FlowButton colors={colors} label={translate("ota:updateLater")} onPress={onFinished} secondary/>) : null}
304
- {allowDevSkip && isUpdateRequired ? (<FlowButton colors={colors} label="Skip (dev only)" onPress={onFinished} secondary/>) : null}
96
+ if (state.screen === "battery_required") {
97
+ return (<FlowPage actions={<>
98
+ <FlowButton colors={colors} disabled label={translate("ota:updateNow")} onPress={controller.install}/>
99
+ {state.canDismiss ? (<FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary/>) : null}
100
+ </>} colors={colors} icon="alert" title={translate("ota:batteryRequiredTitle", { deviceName })}>
101
+ <BodyText colors={colors}>
102
+ {translate("ota:batteryRequiredMessage", {
103
+ batteryLevel: String(state.batteryLevel),
104
+ deviceName,
105
+ minimumBatteryLevel: String(MINIMUM_OTA_BATTERY_LEVEL),
106
+ })}
107
+ </BodyText>
108
+ <BodyText colors={colors}>{translate("ota:batteryRequiredLiveUpdate")}</BodyText>
109
+ </FlowPage>);
110
+ }
111
+ if (state.screen === "finishing") {
112
+ return (<FlowPage colors={colors} icon="download" title={translate("ota:finishingUpdate")}>
113
+ <BodyText colors={colors}>{translate("ota:checkingAdditionalUpdates")}</BodyText>
114
+ <ActivityIndicator size="large" color={colors.foreground}/>
115
+ </FlowPage>);
116
+ }
117
+ if (state.screen === "update_available" || state.screen === "wifi_required") {
118
+ const titleKey = state.screen === "wifi_required"
119
+ ? "ota:wifiRequiredTitle"
120
+ : state.versionChange
121
+ ? "ota:downgradeAvailable"
122
+ : "ota:updateAvailable";
123
+ return (<FlowPage colors={colors} icon="download" title={translate(titleKey, { deviceName })} actions={<>
124
+ <FlowButton colors={colors} disabled={!state.wifiStatusKnown} label={translate(state.screen === "wifi_required" ? "ota:setupWifi" : "ota:updateNow")} onPress={state.screen === "wifi_required" ? controller.openWifiSetup : controller.install}/>
125
+ {state.canDismiss ? (<FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary/>) : null}
126
+ {allowDevSkip && state.updateRequired ? (<FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary/>) : null}
305
127
  </>}>
128
+ {state.releaseTransition ? (<BodyText colors={colors}>
129
+ {state.releaseTransition.fromVersion
130
+ ? translate("ota:releaseTransition", {
131
+ fromVersion: state.releaseTransition.fromVersion,
132
+ toVersion: state.releaseTransition.toVersion,
133
+ })
134
+ : translate("ota:releaseTransitionUnknown", { toVersion: state.releaseTransition.toVersion })}
135
+ </BodyText>) : null}
306
136
  <BodyText colors={colors}>
307
- {canInstallUpdate
308
- ? translate(isDowngradeUpdate ? "ota:downgradeDescription" : "ota:updateDescription")
309
- : translate("ota:updateConnectWifi", { deviceName })}
137
+ {state.screen === "wifi_required"
138
+ ? translate("ota:updateConnectWifi", { deviceName })
139
+ : translate(state.versionChange ? "ota:downgradeDescription" : "ota:updateDescription")}
310
140
  </BodyText>
141
+ <BodyText colors={colors}>{translate("ota:updateSequenceMessage")}</BodyText>
311
142
  </FlowPage>);
312
143
  }
313
- if (checkState === "dev_build") {
314
- return (<FlowPage actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished}/>} colors={colors} icon="settings" title={translate("ota:devBuild")}>
144
+ if (state.screen === "dev_build") {
145
+ return (<FlowPage actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish}/>} colors={colors} icon="settings" title={translate("ota:devBuild")}>
315
146
  <BodyText colors={colors}>{translate("ota:devBuildNoOta")}</BodyText>
316
147
  </FlowPage>);
317
148
  }
318
- if (checkState === "no_update") {
319
- return (<FlowPage actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished}/>} colors={colors} icon="check" title={translate("ota:upToDate")}>
149
+ if (state.screen === "up_to_date") {
150
+ return (<FlowPage actions={<FlowButton colors={colors} label={translate(state.completedUpdate ? "common:done" : "common:continue")} onPress={controller.finish}/>} colors={colors} contentAlignment={state.changelogs.length > 0 ? "top" : "center"} icon="check" title={translate(state.completedUpdate ? "ota:updateComplete" : "ota:upToDate")}>
320
151
  <BodyText colors={colors}>{translate("ota:noUpdatesAvailable")}</BodyText>
152
+ {state.releaseTransition ? (<BodyText colors={colors}>
153
+ {translate("ota:updatedToVersion", { version: state.releaseTransition.toVersion })}
154
+ </BodyText>) : null}
155
+ <ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")}/>
321
156
  </FlowPage>);
322
157
  }
323
- if (errorKind === "pin_unavailable") {
324
- return (<FlowPage actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={onFinished}/>} colors={colors} icon="alert" title={translate("ota:updateInfoUnavailable")}>
158
+ if (state.screen === "update_info_unavailable") {
159
+ return (<FlowPage actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish}/>} colors={colors} icon="alert" title={translate("ota:updateInfoUnavailable")}>
325
160
  <BodyText colors={colors}>{translate("ota:updateInfoUnavailableMessage")}</BodyText>
326
161
  </FlowPage>);
327
162
  }
328
- return (<FlowPage actions={<>
329
- <FlowButton colors={colors} label="Retry" onPress={retry}/>
330
- {allowDevSkip ? <FlowButton colors={colors} label="Skip (dev only)" onPress={onFinished} secondary/> : null}
331
- </>} colors={colors} icon="alert" title={translate("ota:checkFailed")}>
332
- <BodyText colors={colors}>{translate("ota:checkFailedMessage")}</BodyText>
333
- </FlowPage>);
334
- }
335
- function OtaProgressPage({ colors, onFirmwareRestartingChange, onOpenWifiSetup, onReturnToCheck, superMode, translate, }) {
336
- const install = useEngineSnapshot(ota.installSession.snapshot, ota.installSession.onSnapshot);
337
- const autoChainAdvancedRef = useRef(false);
338
- const { connected, continueButtonDisabled, displayState, errorMsg, hotspotArtifactPercent, hotspotPhase, isVersionChange, mtkInstallStallSimulatedPercent, otaProgress, otaStatus, versionChangeConverged, versionChangePhase, } = install;
339
- const firmwareRestarting = (!connected && displayState === "restarting") || versionChangePhase === "restarting";
340
- useEffect(() => {
341
- onFirmwareRestartingChange?.(firmwareRestarting, true);
342
- }, [firmwareRestarting, onFirmwareRestartingChange]);
343
- useEffect(() => () => {
344
- onFirmwareRestartingChange?.(false, false);
345
- }, [onFirmwareRestartingChange]);
346
- useEffect(() => {
347
- ota.installSession.attach();
348
- return () => ota.installSession.detach();
349
- }, []);
350
- useEffect(() => {
351
- if (displayState !== "complete" || !connected || !isOtaAutoChainActive() || autoChainAdvancedRef.current)
352
- return;
353
- const timeout = setTimeout(() => {
354
- if (!isOtaAutoChainActive())
355
- return;
356
- autoChainAdvancedRef.current = true;
357
- ota.installSession.finish();
358
- onReturnToCheck();
359
- }, AUTO_CHAIN_COMPLETE_DELAY_MS);
360
- return () => clearTimeout(timeout);
361
- }, [connected, displayState, onReturnToCheck]);
362
- const finishAndCheck = () => {
363
- ota.installSession.finish();
364
- onReturnToCheck();
365
- };
366
- const stopAndCheck = () => {
367
- stopOtaAutoChain();
368
- finishAndCheck();
369
- };
370
- const retryInstall = () => {
371
- onFirmwareRestartingChange?.(false, true);
372
- ota.installSession.retry();
373
- };
374
- if (versionChangePhase === "restarting" || versionChangePhase === "verifying") {
375
- return (<FlowPage colors={colors} icon="download" title={translate(versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting")}>
163
+ if (state.screen === "check_failed") {
164
+ return (<FlowPage actions={<>
165
+ <FlowButton colors={colors} label="Retry" onPress={controller.retryCheck}/>
166
+ {allowDevSkip ? (<FlowButton colors={colors} label="Skip (dev only)" onPress={controller.finish} secondary/>) : null}
167
+ </>} colors={colors} icon="alert" title={translate("ota:checkFailed")}>
168
+ <BodyText colors={colors}>{translate("ota:checkFailedMessage")}</BodyText>
169
+ </FlowPage>);
170
+ }
171
+ if (state.versionChangePhase === "restarting" || state.versionChangePhase === "verifying") {
172
+ return (<FlowPage colors={colors} icon="download" title={translate(state.versionChangePhase === "verifying" ? "ota:versionChangeVerifying" : "ota:versionChangeRestarting")}>
376
173
  <ActivityIndicator size="large" color={colors.foreground}/>
377
174
  <BodyText colors={colors}>{translate("ota:versionChangeKeepNearby")}</BodyText>
378
175
  <BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>
379
176
  </FlowPage>);
380
177
  }
381
- if (displayState === "starting") {
382
- const hotspotTitle = hotspotPhase === "downloading"
178
+ if (state.screen === "starting" || state.screen === "preparing_hotspot") {
179
+ const title = state.hotspotPhase === "downloading"
383
180
  ? "Downloading update to phone..."
384
- : hotspotPhase === "starting_hotspot"
181
+ : state.hotspotPhase === "starting_hotspot"
385
182
  ? "Starting glasses hotspot..."
386
- : hotspotPhase === "joining_hotspot"
183
+ : state.hotspotPhase === "joining_hotspot"
387
184
  ? "Connecting phone to glasses..."
388
185
  : "Starting update...";
389
- return (<FlowPage colors={colors} icon="download" title={hotspotTitle}>
390
- {hotspotPhase === "downloading" && hotspotArtifactPercent !== null ? (<PercentText colors={colors} percent={hotspotArtifactPercent}/>) : null}
186
+ return (<FlowPage colors={colors} icon="download" title={title}>
187
+ {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (<PercentText colors={colors} percent={state.hotspotArtifactPercent}/>) : null}
391
188
  <ActivityIndicator size="large" color={colors.foreground}/>
392
189
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
393
190
  </FlowPage>);
394
191
  }
395
- if (displayState === "updating") {
396
- const isDownload = otaStatus?.phase === "download";
397
- const totalSteps = otaStatus?.totalSteps ?? 1;
398
- const isApkOnlyInstalling = otaStatus?.stepType === "apk" && otaStatus.phase === "install" && totalSteps === 1;
399
- const rawPercent = isDownload
400
- ? otaStatus?.stepPercent ?? 0
401
- : totalSteps >= 2
402
- ? otaStatus?.overallPercent ?? 0
403
- : otaStatus?.stepPercent ?? 0;
404
- const percent = Math.min(Math.max(rawPercent, mtkInstallStallSimulatedPercent ?? 0, 0), 100);
405
- return (<FlowPage colors={colors} icon="download" title={isDownload ? "Downloading..." : "Installing..."}>
406
- {isApkOnlyInstalling ? (<ActivityIndicator size="large" color={colors.foreground}/>) : (<>
407
- <PercentText colors={colors} percent={percent}/>
192
+ if (state.screen === "updating") {
193
+ return (<FlowPage colors={colors} icon="download" title={state.phase === "download" ? "Downloading..." : "Installing..."}>
194
+ {state.installingApkOnly ? (<ActivityIndicator size="large" color={colors.foreground}/>) : (<>
195
+ <PercentText colors={colors} percent={state.progress ?? 0}/>
408
196
  <View style={[styles.progressTrack, { backgroundColor: colors.border }]}>
409
- <View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${percent}%` }]}/>
197
+ <View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${state.progress ?? 0}%` }]}/>
410
198
  </View>
411
199
  </>)}
412
200
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
413
- {isVersionChange && otaStatus?.phase === "install" ? (<BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>) : null}
201
+ {state.versionChange && state.phase === "install" ? (<BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>) : null}
414
202
  </FlowPage>);
415
203
  }
416
- if (displayState === "restarting") {
417
- return (<FlowPage actions={<FlowButton colors={colors} disabled={continueButtonDisabled} label="Continue" onPress={stopAndCheck}/>} colors={colors} icon="check" title="Update Installed"/>);
204
+ if (state.screen === "restarting") {
205
+ return (<FlowPage actions={<FlowButton colors={colors} disabled={state.continueDisabled} label="Continue" onPress={controller.finish}/>} colors={colors} icon="check" title="Update Installed"/>);
418
206
  }
419
- if (displayState === "complete") {
420
- const title = versionChangeConverged
207
+ if (state.screen === "complete") {
208
+ const title = state.versionChangeConverged
421
209
  ? translate("ota:versionChangeComplete")
422
- : isVersionChange
210
+ : state.versionChange
423
211
  ? translate("ota:versionChangeFirmwarePassComplete")
424
212
  : "Update complete!";
425
- const message = versionChangeConverged
213
+ const message = state.versionChangeConverged
426
214
  ? translate("ota:versionChangeCompleteMessage")
427
- : isVersionChange
215
+ : state.versionChange
428
216
  ? translate("ota:versionChangeFirmwarePassCompleteMessage")
429
217
  : "Your glasses are up to date.";
430
- return (<FlowPage actions={<FlowButton colors={colors} label={isVersionChange && !versionChangeConverged ? "Continue" : "Done"} onPress={finishAndCheck}/>} colors={colors} icon="check" title={title}>
218
+ return (<FlowPage actions={<FlowButton colors={colors} label={state.versionChange && !state.versionChangeConverged ? "Continue" : "Done"} onPress={controller.finish}/>} colors={colors} contentAlignment={state.changelogs.length > 0 ? "top" : "center"} icon="check" title={title}>
431
219
  <BodyText colors={colors}>{message}</BodyText>
220
+ {state.releaseTransition ? (<BodyText colors={colors}>
221
+ {translate("ota:updatedToVersion", { version: state.releaseTransition.toVersion })}
222
+ </BodyText>) : null}
223
+ <ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")}/>
432
224
  </FlowPage>);
433
225
  }
434
- if (displayState === "failed") {
435
- const requiresGlassesReboot = shouldRequireGlassesRebootForBesFailure(otaStatus, otaProgress, errorMsg);
436
- const displayedError = requiresGlassesReboot
437
- ? BES_INSTALL_RESTART_MESSAGE
438
- : errorMsg || getOtaErrorMessage(otaStatus?.error);
439
- const showChangeWifi = shouldShowChangeWifiForOtaDownloadFailure(otaStatus, otaProgress, errorMsg);
226
+ if (state.screen === "failed") {
440
227
  return (<FlowPage actions={<>
441
- <FlowButton colors={colors} label={requiresGlassesReboot ? "Done" : "Retry"} onPress={requiresGlassesReboot ? stopAndCheck : retryInstall}/>
442
- {showChangeWifi ? (<FlowButton colors={colors} label="Change WiFi" onPress={() => {
443
- stopOtaAutoChain();
444
- onOpenWifiSetup();
445
- }} secondary/>) : null}
228
+ <FlowButton colors={colors} label={state.canRetry ? "Retry" : "Done"} onPress={state.canRetry ? controller.retryInstall : controller.finish}/>
229
+ {state.canOpenWifiSetup ? (<FlowButton colors={colors} label="Change WiFi" onPress={controller.openWifiSetup} secondary/>) : null}
446
230
  </>} colors={colors} icon="alert" title="Update Failed">
447
- <BodyText colors={colors}>{displayedError}</BodyText>
231
+ <BodyText colors={colors}>{state.error?.message}</BodyText>
448
232
  </FlowPage>);
449
233
  }
450
- return (<FlowPage actions={superMode ? (<FlowButton colors={colors} label="Skip (super)" onPress={() => {
451
- stopOtaAutoChain();
452
- ota.installSession.discard();
453
- onReturnToCheck();
454
- }} secondary/>) : undefined} colors={colors} icon="bluetooth" title="Glasses disconnected">
234
+ return (<FlowPage actions={superMode ? (<FlowButton colors={colors} label="Skip (super)" onPress={controller.discard} secondary/>) : undefined} colors={colors} icon="bluetooth" title="Glasses disconnected">
455
235
  <BodyText colors={colors}>Reconnecting...</BodyText>
456
236
  <ActivityIndicator size="large" color={colors.foreground}/>
457
237
  </FlowPage>);
458
238
  }
459
- function FlowPage({ actions, children, colors, icon, title }) {
239
+ function FlowPage({ actions, children, colors, contentAlignment = "center", icon, title }) {
460
240
  return (<View style={styles.page} testID="mentra-live-ota-flow">
461
- <View style={styles.centerContent}>
241
+ <View style={[styles.centerContent, contentAlignment === "top" && styles.topContent]}>
462
242
  <FlowIcon colors={colors} name={icon}/>
463
243
  <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
464
244
  {children}
@@ -472,16 +252,113 @@ function BodyText({ children, colors }) {
472
252
  function PercentText({ colors, percent }) {
473
253
  return <Text style={[styles.percent, { color: colors.primary }]}>{Math.round(percent)}%</Text>;
474
254
  }
255
+ function ChangelogMarkdown({ colors, markdown }) {
256
+ const markdownStyles = useMemo(() => ({
257
+ blockquote: {
258
+ borderLeftColor: colors.primary,
259
+ borderLeftWidth: 3,
260
+ marginVertical: 2,
261
+ opacity: 1,
262
+ paddingLeft: 12,
263
+ },
264
+ code: {
265
+ backgroundColor: colors.background,
266
+ borderColor: colors.border,
267
+ borderRadius: 8,
268
+ borderWidth: 1,
269
+ padding: 12,
270
+ },
271
+ codespan: {
272
+ backgroundColor: colors.border,
273
+ color: colors.foreground,
274
+ fontFamily: "monospace",
275
+ fontSize: 13,
276
+ fontStyle: "normal",
277
+ fontWeight: "400",
278
+ },
279
+ em: { color: colors.textDim, fontSize: 14, lineHeight: 20 },
280
+ h1: {
281
+ borderBottomWidth: 0,
282
+ color: colors.foreground,
283
+ fontSize: 18,
284
+ fontWeight: "700",
285
+ lineHeight: 24,
286
+ marginVertical: 0,
287
+ paddingBottom: 0,
288
+ },
289
+ h2: {
290
+ borderBottomWidth: 0,
291
+ color: colors.foreground,
292
+ fontSize: 17,
293
+ fontWeight: "700",
294
+ lineHeight: 23,
295
+ marginVertical: 0,
296
+ paddingBottom: 0,
297
+ },
298
+ h3: { color: colors.foreground, fontSize: 16, fontWeight: "700", lineHeight: 22, marginVertical: 0 },
299
+ h4: { color: colors.foreground, fontSize: 15, fontWeight: "700", lineHeight: 21, marginVertical: 0 },
300
+ h5: { color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20, marginVertical: 0 },
301
+ h6: { color: colors.textDim, fontSize: 14, fontWeight: "600", lineHeight: 20, marginVertical: 0 },
302
+ hr: { borderBottomColor: colors.border, marginVertical: 2 },
303
+ image: { borderRadius: 8 },
304
+ li: { color: colors.textDim, fontSize: 14, lineHeight: 20 },
305
+ link: {
306
+ color: colors.primary,
307
+ fontSize: 14,
308
+ fontStyle: "normal",
309
+ lineHeight: 20,
310
+ textDecorationLine: "underline",
311
+ },
312
+ paragraph: { paddingVertical: 0 },
313
+ strikethrough: { color: colors.textDim, fontSize: 14, lineHeight: 20 },
314
+ strong: { color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20 },
315
+ table: { borderColor: colors.border, borderRadius: 8 },
316
+ tableCell: { padding: 8 },
317
+ text: { color: colors.textDim, fontSize: 14, lineHeight: 20 },
318
+ }), [colors]);
319
+ const markdownTheme = useMemo(() => ({
320
+ colors: {
321
+ background: colors.background,
322
+ border: colors.border,
323
+ code: colors.border,
324
+ link: colors.primary,
325
+ text: colors.textDim,
326
+ },
327
+ }), [colors]);
328
+ const elements = useMarkdown(markdown, { colorScheme: "light", styles: markdownStyles, theme: markdownTheme });
329
+ return (<View style={styles.changelogMarkdownContent} testID="ota-changelog-markdown">
330
+ {elements}
331
+ </View>);
332
+ }
333
+ export function ChangelogList({ changelogs, colors, title, }) {
334
+ if (changelogs.length === 0)
335
+ return null;
336
+ return (<View style={[styles.changelogCard, { borderColor: colors.border }]} testID="ota-changelog-card">
337
+ <Text style={[styles.changelogTitle, { color: colors.foreground }]}>{title}</Text>
338
+ <ScrollView contentContainerStyle={styles.changelogContent} nestedScrollEnabled showsVerticalScrollIndicator style={styles.changelogList} testID="ota-changelog-scroll">
339
+ {changelogs.map((entry, index) => (<View key={entry.version} style={[
340
+ styles.changelogEntry,
341
+ index > 0 && styles.changelogEntryDivider,
342
+ index > 0 && { borderTopColor: colors.border },
343
+ ]}>
344
+ <Text selectable style={[styles.changelogVersion, { color: colors.foreground }]}>
345
+ {entry.version}
346
+ </Text>
347
+ <ChangelogMarkdown colors={colors} markdown={entry.markdown}/>
348
+ </View>))}
349
+ </ScrollView>
350
+ </View>);
351
+ }
475
352
  function FlowButton({ colors, disabled = false, label, onPress, secondary = false, }) {
476
353
  return (<Pressable accessibilityRole="button" disabled={disabled} onPress={onPress} style={({ pressed }) => [
477
354
  styles.button,
478
355
  {
479
- backgroundColor: secondary ? colors.background : colors.primary,
480
- borderColor: secondary ? colors.border : colors.primary,
356
+ backgroundColor: secondary ? colors.background : colors.foreground,
357
+ borderColor: secondary ? colors.border : colors.foreground,
481
358
  opacity: disabled ? 0.45 : pressed ? 0.75 : 1,
482
359
  },
483
360
  ]} testID={`button-${label}`}>
484
- <Text style={[styles.buttonText, { color: secondary ? colors.foreground : colors.primaryText }]}>{label}</Text>
361
+ <Text style={[styles.buttonText, { color: secondary ? colors.foreground : colors.background }]}>{label}</Text>
485
362
  </Pressable>);
486
363
  }
487
364
  function MentraMark({ color }) {
@@ -507,6 +384,7 @@ const styles = StyleSheet.create({
507
384
  },
508
385
  page: { flex: 1, paddingBottom: 24, paddingHorizontal: 24 },
509
386
  centerContent: { alignItems: "center", flex: 1, gap: 16, justifyContent: "center" },
387
+ topContent: { justifyContent: "flex-start", paddingTop: 12 },
510
388
  actionSpacer: { height: 48 },
511
389
  actions: { gap: 12 },
512
390
  icon: { fontSize: 64, fontWeight: "500", lineHeight: 72, textAlign: "center" },
@@ -515,14 +393,31 @@ const styles = StyleSheet.create({
515
393
  percent: { fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700" },
516
394
  progressTrack: { borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%" },
517
395
  progressFill: { borderRadius: 4, height: 8 },
396
+ changelogCard: {
397
+ borderRadius: 16,
398
+ borderWidth: 1,
399
+ flexShrink: 1,
400
+ gap: 12,
401
+ maxHeight: 300,
402
+ maxWidth: 420,
403
+ padding: 16,
404
+ width: "100%",
405
+ },
406
+ changelogTitle: { fontSize: 16, fontWeight: "700" },
407
+ changelogList: { flexShrink: 1, maxHeight: 232, width: "100%" },
408
+ changelogContent: { gap: 20, paddingBottom: 2 },
409
+ changelogEntry: { gap: 8 },
410
+ changelogEntryDivider: { borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20 },
411
+ changelogVersion: { fontSize: 14, fontWeight: "600" },
412
+ changelogMarkdownContent: { gap: 10 },
518
413
  button: {
519
414
  alignItems: "center",
520
- borderRadius: 10,
415
+ borderRadius: 50,
521
416
  borderWidth: 1,
522
417
  justifyContent: "center",
523
- minHeight: 48,
418
+ minHeight: 44,
524
419
  paddingHorizontal: 20,
525
420
  },
526
- buttonText: { fontSize: 15, fontWeight: "700" },
421
+ buttonText: { fontSize: 14, fontWeight: "500" },
527
422
  });
528
423
  //# sourceMappingURL=MentraLiveOtaFlow.js.map