@glassly/engine 0.1.0-dev.61 → 0.1.0-dev.89

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 (147) hide show
  1. package/README.md +6 -2
  2. package/build/engine.d.ts +7 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +3 -0
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/display.d.ts +10 -3
  7. package/build/facades/display.d.ts.map +1 -1
  8. package/build/facades/display.js +11 -1
  9. package/build/facades/display.js.map +1 -1
  10. package/build/facades/displayMirror.d.ts +2 -6
  11. package/build/facades/displayMirror.d.ts.map +1 -1
  12. package/build/facades/displayMirror.js +7 -14
  13. package/build/facades/displayMirror.js.map +1 -1
  14. package/build/facades/glasses.d.ts +1 -0
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +2 -0
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/facades/ota.d.ts +3 -1
  19. package/build/facades/ota.d.ts.map +1 -1
  20. package/build/facades/ota.js +4 -2
  21. package/build/facades/ota.js.map +1 -1
  22. package/build/facades/phoneNotifications.d.ts +12 -2
  23. package/build/facades/phoneNotifications.d.ts.map +1 -1
  24. package/build/facades/phoneNotifications.js +30 -10
  25. package/build/facades/phoneNotifications.js.map +1 -1
  26. package/build/generated/releaseMetadata.js +3 -3
  27. package/build/generated/releaseMetadata.js.map +1 -1
  28. package/build/index.d.ts +2 -2
  29. package/build/index.d.ts.map +1 -1
  30. package/build/index.js +2 -2
  31. package/build/index.js.map +1 -1
  32. package/build/react/MentraLiveOtaFlow.d.ts +6 -1
  33. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  34. package/build/react/MentraLiveOtaFlow.js +136 -14
  35. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  36. package/build/react/useMentraLiveOta.d.ts +5 -1
  37. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  38. package/build/react/useMentraLiveOta.js +135 -58
  39. package/build/react/useMentraLiveOta.js.map +1 -1
  40. package/build/services/CustomDashboardService.d.ts +11 -10
  41. package/build/services/CustomDashboardService.d.ts.map +1 -1
  42. package/build/services/CustomDashboardService.js +70 -69
  43. package/build/services/CustomDashboardService.js.map +1 -1
  44. package/build/services/DashboardCoordinator.d.ts +26 -0
  45. package/build/services/DashboardCoordinator.d.ts.map +1 -0
  46. package/build/services/DashboardCoordinator.js +125 -0
  47. package/build/services/DashboardCoordinator.js.map +1 -0
  48. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  49. package/build/services/DeviceEventRouter.js +5 -7
  50. package/build/services/DeviceEventRouter.js.map +1 -1
  51. package/build/services/DisplayProcessor.d.ts +3 -3
  52. package/build/services/DisplayProcessor.js +3 -3
  53. package/build/services/DisplayProcessor.js.map +1 -1
  54. package/build/services/LocalDisplayManager.d.ts +12 -2
  55. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  56. package/build/services/LocalDisplayManager.js +68 -14
  57. package/build/services/LocalDisplayManager.js.map +1 -1
  58. package/build/services/OtaInstallCoordinator.d.ts +2 -0
  59. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  60. package/build/services/OtaInstallCoordinator.js +21 -1
  61. package/build/services/OtaInstallCoordinator.js.map +1 -1
  62. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  63. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  64. package/build/services/PhoneNotificationsSync.js +101 -9
  65. package/build/services/PhoneNotificationsSync.js.map +1 -1
  66. package/build/services/PhonePhotoCoordinator.js +1 -1
  67. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  68. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  69. package/build/services/PhoneStreamCoordinator.js +9 -6
  70. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  71. package/build/services/SceneRenderer.d.ts.map +1 -1
  72. package/build/services/SceneRenderer.js +5 -0
  73. package/build/services/SceneRenderer.js.map +1 -1
  74. package/build/services/StreamLifecycleController.d.ts +4 -0
  75. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  76. package/build/services/StreamLifecycleController.js +4 -0
  77. package/build/services/StreamLifecycleController.js.map +1 -1
  78. package/build/services/effectiveCapabilities.d.ts +1 -1
  79. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  80. package/build/services/effectiveCapabilities.js +9 -4
  81. package/build/services/effectiveCapabilities.js.map +1 -1
  82. package/build/services/iosNotificationAppCatalog.d.ts +13 -0
  83. package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
  84. package/build/services/iosNotificationAppCatalog.js +28 -0
  85. package/build/services/iosNotificationAppCatalog.js.map +1 -0
  86. package/build/services/otaInstallPolicy.d.ts +4 -0
  87. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  88. package/build/services/otaInstallPolicy.js +4 -0
  89. package/build/services/otaInstallPolicy.js.map +1 -1
  90. package/build/stores/display.d.ts +4 -4
  91. package/build/stores/display.d.ts.map +1 -1
  92. package/build/stores/display.js +12 -36
  93. package/build/stores/display.js.map +1 -1
  94. package/build/stores/glasses.d.ts.map +1 -1
  95. package/build/stores/glasses.js +2 -0
  96. package/build/stores/glasses.js.map +1 -1
  97. package/build/stores/settings.d.ts.map +1 -1
  98. package/build/stores/settings.js +53 -2
  99. package/build/stores/settings.js.map +1 -1
  100. package/build/types/applet.d.ts +1 -1
  101. package/build/types/applet.js.map +1 -1
  102. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  103. package/build/types/capabilities/even-realities-g2.js +2 -2
  104. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  105. package/build/types/hardware.d.ts.map +1 -1
  106. package/build/types/hardware.js +1 -0
  107. package/build/types/hardware.js.map +1 -1
  108. package/build/types/index.d.ts +1 -1
  109. package/build/types/index.d.ts.map +1 -1
  110. package/build/types/index.js +1 -1
  111. package/build/types/index.js.map +1 -1
  112. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  113. package/build/utils/display/composer/ColumnComposer.js +1 -1
  114. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  115. package/package.json +8 -7
  116. package/src/engine.ts +3 -0
  117. package/src/facades/display.ts +11 -1
  118. package/src/facades/displayMirror.ts +7 -15
  119. package/src/facades/glasses.ts +2 -0
  120. package/src/facades/ota.ts +4 -2
  121. package/src/facades/phoneNotifications.ts +32 -10
  122. package/src/generated/releaseMetadata.ts +3 -3
  123. package/src/index.ts +2 -1
  124. package/src/react/MentraLiveOtaFlow.tsx +226 -41
  125. package/src/react/useMentraLiveOta.ts +146 -60
  126. package/src/services/CustomDashboardService.ts +67 -66
  127. package/src/services/DashboardCoordinator.ts +121 -0
  128. package/src/services/DeviceEventRouter.ts +5 -7
  129. package/src/services/DisplayProcessor.ts +3 -3
  130. package/src/services/LocalDisplayManager.ts +68 -16
  131. package/src/services/OtaInstallCoordinator.ts +21 -0
  132. package/src/services/PhoneNotificationsSync.ts +125 -9
  133. package/src/services/PhonePhotoCoordinator.ts +1 -1
  134. package/src/services/PhoneStreamCoordinator.ts +10 -6
  135. package/src/services/SceneRenderer.ts +4 -0
  136. package/src/services/StreamLifecycleController.ts +4 -0
  137. package/src/services/effectiveCapabilities.ts +9 -4
  138. package/src/services/iosNotificationAppCatalog.ts +39 -0
  139. package/src/services/otaInstallPolicy.ts +4 -0
  140. package/src/stores/display.ts +16 -48
  141. package/src/stores/glasses.ts +2 -0
  142. package/src/stores/settings.ts +53 -2
  143. package/src/types/applet.ts +1 -1
  144. package/src/types/capabilities/even-realities-g2.ts +3 -2
  145. package/src/types/hardware.ts +1 -0
  146. package/src/types/index.ts +1 -1
  147. package/src/utils/display/composer/ColumnComposer.ts +1 -1
@@ -10,10 +10,16 @@ import {
10
10
  type StyleProp,
11
11
  type ViewStyle,
12
12
  } from "react-native"
13
+ import {useMarkdown, type MarkedStyles, type useMarkdownHookOptions} from "react-native-marked"
13
14
  import {SafeAreaView} from "react-native-safe-area-context"
14
15
  import Svg, {Path, Rect} from "react-native-svg"
15
16
 
16
- import {useMentraLiveOta, type MentraLiveOtaController, type MentraLiveOtaFlowPage} from "./useMentraLiveOta"
17
+ import {
18
+ MINIMUM_OTA_BATTERY_LEVEL,
19
+ useMentraLiveOta,
20
+ type MentraLiveOtaController,
21
+ type MentraLiveOtaFlowPage,
22
+ } from "./useMentraLiveOta"
17
23
 
18
24
  export type {MentraLiveOtaFlowPage} from "./useMentraLiveOta"
19
25
 
@@ -65,13 +71,23 @@ const DEFAULT_THEME: MentraLiveOtaFlowTheme = {
65
71
 
66
72
  const ENGLISH_COPY: Record<string, string> = {
67
73
  "common:continue": "Continue",
74
+ "common:done": "Done",
68
75
  "ota:checkingForUpdates": "Checking for updates",
69
76
  "ota:checkingForUpdatesMessage":
70
77
  "Connected devices will perform automatic updates. Automatic updates can be disabled in Device Settings",
78
+ "ota:finishingUpdate": "Finishing your update",
79
+ "ota:checkingAdditionalUpdates": "Checking whether your glasses need any additional updates.",
71
80
  "ota:updateAvailable": "{{deviceName}} Update Available",
81
+ "ota:batteryRequiredTitle": "Charge {{deviceName}} to Update",
82
+ "ota:batteryRequiredMessage":
83
+ "{{deviceName}} is currently at {{batteryLevel}}%. Charge it to at least {{minimumBatteryLevel}}% before updating.",
84
+ "ota:batteryRequiredLiveUpdate": "This screen will update automatically as the battery charges.",
72
85
  "ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
86
+ "ota:wifiRequiredTitle": "WiFi Needed for Update",
73
87
  "ota:updateDescription":
74
88
  "A new update is available for your glasses. We recommend updating now for the best experience.",
89
+ "ota:updateSequenceMessage":
90
+ "Your glasses may install more than one update and restart several times. Keep them nearby until finished.",
75
91
  "ota:releaseTransition": "{{fromVersion}} → {{toVersion}}",
76
92
  "ota:releaseTransitionUnknown": "Current version unknown → {{toVersion}}",
77
93
  "ota:updatedToVersion": "Updated to {{version}}",
@@ -81,6 +97,8 @@ const ENGLISH_COPY: Record<string, string> = {
81
97
  "ota:updateNow": "Update Now",
82
98
  "ota:setupWifi": "Setup WiFi",
83
99
  "ota:updateLater": "Later",
100
+ "ota:updateComplete": "Update complete",
101
+ "ota:whatsNew": "What's new",
84
102
  "ota:upToDate": "Up To Date",
85
103
  "ota:devBuild": "Development Build",
86
104
  "ota:devBuildNoOta":
@@ -95,6 +113,10 @@ const ENGLISH_COPY: Record<string, string> = {
95
113
  "ota:versionChangeRestarting": "Installing a different version…",
96
114
  "ota:versionChangeVerifying": "Verifying your glasses…",
97
115
  "ota:versionChangeKeepNearby": "Keep your glasses nearby and connected. They will restart on their own.",
116
+ "ota:restartingGlasses": "Restarting {{deviceName}}…",
117
+ "ota:restartingGlassesMessage":
118
+ "The update is installed. Keep your glasses nearby and leave this screen open while they finish starting.",
119
+ "ota:restartingGlassesAutomatic": "We'll continue automatically when they're ready.",
98
120
  "ota:versionChangeComplete": "Version Change Complete",
99
121
  "ota:versionChangeCompleteMessage":
100
122
  "Your glasses are now on the required version. Their settings were reset and are being restored automatically.",
@@ -185,12 +207,53 @@ function OtaFlowContent({
185
207
  )
186
208
  }
187
209
 
210
+ if (state.screen === "battery_required") {
211
+ return (
212
+ <FlowPage
213
+ actions={
214
+ <>
215
+ <FlowButton colors={colors} disabled label={translate("ota:updateNow")} onPress={controller.install} />
216
+ {state.canDismiss ? (
217
+ <FlowButton colors={colors} label={translate("ota:updateLater")} onPress={controller.finish} secondary />
218
+ ) : null}
219
+ </>
220
+ }
221
+ colors={colors}
222
+ icon="alert"
223
+ title={translate("ota:batteryRequiredTitle", {deviceName})}>
224
+ <BodyText colors={colors}>
225
+ {translate("ota:batteryRequiredMessage", {
226
+ batteryLevel: String(state.batteryLevel),
227
+ deviceName,
228
+ minimumBatteryLevel: String(MINIMUM_OTA_BATTERY_LEVEL),
229
+ })}
230
+ </BodyText>
231
+ <BodyText colors={colors}>{translate("ota:batteryRequiredLiveUpdate")}</BodyText>
232
+ </FlowPage>
233
+ )
234
+ }
235
+
236
+ if (state.screen === "finishing") {
237
+ return (
238
+ <FlowPage colors={colors} icon="download" title={translate("ota:finishingUpdate")}>
239
+ <BodyText colors={colors}>{translate("ota:checkingAdditionalUpdates")}</BodyText>
240
+ <ActivityIndicator size="large" color={colors.foreground} />
241
+ </FlowPage>
242
+ )
243
+ }
244
+
188
245
  if (state.screen === "update_available" || state.screen === "wifi_required") {
246
+ const titleKey =
247
+ state.screen === "wifi_required"
248
+ ? "ota:wifiRequiredTitle"
249
+ : state.versionChange
250
+ ? "ota:downgradeAvailable"
251
+ : "ota:updateAvailable"
189
252
  return (
190
253
  <FlowPage
191
254
  colors={colors}
192
255
  icon="download"
193
- title={translate(state.versionChange ? "ota:downgradeAvailable" : "ota:updateAvailable", {deviceName})}
256
+ title={translate(titleKey, {deviceName})}
194
257
  actions={
195
258
  <>
196
259
  <FlowButton
@@ -222,6 +285,7 @@ function OtaFlowContent({
222
285
  ? translate("ota:updateConnectWifi", {deviceName})
223
286
  : translate(state.versionChange ? "ota:downgradeDescription" : "ota:updateDescription")}
224
287
  </BodyText>
288
+ <BodyText colors={colors}>{translate("ota:updateSequenceMessage")}</BodyText>
225
289
  </FlowPage>
226
290
  )
227
291
  }
@@ -241,17 +305,24 @@ function OtaFlowContent({
241
305
  if (state.screen === "up_to_date") {
242
306
  return (
243
307
  <FlowPage
244
- actions={<FlowButton colors={colors} label={translate("common:continue")} onPress={controller.finish} />}
308
+ actions={
309
+ <FlowButton
310
+ colors={colors}
311
+ label={translate(state.completedUpdate ? "common:done" : "common:continue")}
312
+ onPress={controller.finish}
313
+ />
314
+ }
245
315
  colors={colors}
316
+ contentAlignment={state.changelogs.length > 0 ? "top" : "center"}
246
317
  icon="check"
247
- title={translate("ota:upToDate")}>
318
+ title={translate(state.completedUpdate ? "ota:updateComplete" : "ota:upToDate")}>
248
319
  <BodyText colors={colors}>{translate("ota:noUpdatesAvailable")}</BodyText>
249
320
  {state.releaseTransition ? (
250
321
  <BodyText colors={colors}>
251
322
  {translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
252
323
  </BodyText>
253
324
  ) : null}
254
- <ChangelogList changelogs={state.changelogs} colors={colors} />
325
+ <ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")} />
255
326
  </FlowPage>
256
327
  )
257
328
  }
@@ -307,10 +378,10 @@ function OtaFlowContent({
307
378
  state.hotspotPhase === "downloading"
308
379
  ? "Downloading update to phone..."
309
380
  : state.hotspotPhase === "starting_hotspot"
310
- ? "Starting glasses hotspot..."
311
- : state.hotspotPhase === "joining_hotspot"
312
- ? "Connecting phone to glasses..."
313
- : "Starting update..."
381
+ ? "Starting glasses hotspot..."
382
+ : state.hotspotPhase === "joining_hotspot"
383
+ ? "Connecting phone to glasses..."
384
+ : "Starting update..."
314
385
  return (
315
386
  <FlowPage colors={colors} icon="download" title={title}>
316
387
  {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
@@ -347,14 +418,11 @@ function OtaFlowContent({
347
418
 
348
419
  if (state.screen === "restarting") {
349
420
  return (
350
- <FlowPage
351
- actions={
352
- <FlowButton colors={colors} disabled={state.continueDisabled} label="Continue" onPress={controller.finish} />
353
- }
354
- colors={colors}
355
- icon="check"
356
- title="Update Installed"
357
- />
421
+ <FlowPage colors={colors} icon="download" title={translate("ota:restartingGlasses", {deviceName})}>
422
+ <ActivityIndicator size="large" color={colors.foreground} />
423
+ <BodyText colors={colors}>{translate("ota:restartingGlassesMessage")}</BodyText>
424
+ <BodyText colors={colors}>{translate("ota:restartingGlassesAutomatic")}</BodyText>
425
+ </FlowPage>
358
426
  )
359
427
  }
360
428
 
@@ -362,13 +430,13 @@ function OtaFlowContent({
362
430
  const title = state.versionChangeConverged
363
431
  ? translate("ota:versionChangeComplete")
364
432
  : state.versionChange
365
- ? translate("ota:versionChangeFirmwarePassComplete")
366
- : "Update complete!"
433
+ ? translate("ota:versionChangeFirmwarePassComplete")
434
+ : "Update complete!"
367
435
  const message = state.versionChangeConverged
368
436
  ? translate("ota:versionChangeCompleteMessage")
369
437
  : state.versionChange
370
- ? translate("ota:versionChangeFirmwarePassCompleteMessage")
371
- : "Your glasses are up to date."
438
+ ? translate("ota:versionChangeFirmwarePassCompleteMessage")
439
+ : "Your glasses are up to date."
372
440
  return (
373
441
  <FlowPage
374
442
  actions={
@@ -379,6 +447,7 @@ function OtaFlowContent({
379
447
  />
380
448
  }
381
449
  colors={colors}
450
+ contentAlignment={state.changelogs.length > 0 ? "top" : "center"}
382
451
  icon="check"
383
452
  title={title}>
384
453
  <BodyText colors={colors}>{message}</BodyText>
@@ -387,7 +456,7 @@ function OtaFlowContent({
387
456
  {translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
388
457
  </BodyText>
389
458
  ) : null}
390
- <ChangelogList changelogs={state.changelogs} colors={colors} />
459
+ <ChangelogList changelogs={state.changelogs} colors={colors} title={translate("ota:whatsNew")} />
391
460
  </FlowPage>
392
461
  )
393
462
  }
@@ -435,14 +504,15 @@ type FlowPageProps = {
435
504
  actions?: React.ReactNode
436
505
  children?: React.ReactNode
437
506
  colors: MentraLiveOtaFlowTheme
507
+ contentAlignment?: "center" | "top"
438
508
  icon: "alert" | "bluetooth" | "check" | "download" | "settings"
439
509
  title: string
440
510
  }
441
511
 
442
- function FlowPage({actions, children, colors, icon, title}: FlowPageProps) {
512
+ function FlowPage({actions, children, colors, contentAlignment = "center", icon, title}: FlowPageProps) {
443
513
  return (
444
514
  <View style={styles.page} testID="mentra-live-ota-flow">
445
- <View style={styles.centerContent}>
515
+ <View style={[styles.centerContent, contentAlignment === "top" && styles.topContent]}>
446
516
  <FlowIcon colors={colors} name={icon} />
447
517
  <Text style={[styles.title, {color: colors.foreground}]}>{title}</Text>
448
518
  {children}
@@ -460,27 +530,129 @@ function PercentText({colors, percent}: {colors: MentraLiveOtaFlowTheme; percent
460
530
  return <Text style={[styles.percent, {color: colors.primary}]}>{Math.round(percent)}%</Text>
461
531
  }
462
532
 
463
- function ChangelogList({
533
+ function ChangelogMarkdown({colors, markdown}: {colors: MentraLiveOtaFlowTheme; markdown: string}) {
534
+ const markdownStyles = useMemo<MarkedStyles>(
535
+ () => ({
536
+ blockquote: {
537
+ borderLeftColor: colors.primary,
538
+ borderLeftWidth: 3,
539
+ marginVertical: 2,
540
+ opacity: 1,
541
+ paddingLeft: 12,
542
+ },
543
+ code: {
544
+ backgroundColor: colors.background,
545
+ borderColor: colors.border,
546
+ borderRadius: 8,
547
+ borderWidth: 1,
548
+ padding: 12,
549
+ },
550
+ codespan: {
551
+ backgroundColor: colors.border,
552
+ color: colors.foreground,
553
+ fontFamily: "monospace",
554
+ fontSize: 13,
555
+ fontStyle: "normal",
556
+ fontWeight: "400",
557
+ },
558
+ em: {color: colors.textDim, fontSize: 14, lineHeight: 20},
559
+ h1: {
560
+ borderBottomWidth: 0,
561
+ color: colors.foreground,
562
+ fontSize: 18,
563
+ fontWeight: "700",
564
+ lineHeight: 24,
565
+ marginVertical: 0,
566
+ paddingBottom: 0,
567
+ },
568
+ h2: {
569
+ borderBottomWidth: 0,
570
+ color: colors.foreground,
571
+ fontSize: 17,
572
+ fontWeight: "700",
573
+ lineHeight: 23,
574
+ marginVertical: 0,
575
+ paddingBottom: 0,
576
+ },
577
+ h3: {color: colors.foreground, fontSize: 16, fontWeight: "700", lineHeight: 22, marginVertical: 0},
578
+ h4: {color: colors.foreground, fontSize: 15, fontWeight: "700", lineHeight: 21, marginVertical: 0},
579
+ h5: {color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20, marginVertical: 0},
580
+ h6: {color: colors.textDim, fontSize: 14, fontWeight: "600", lineHeight: 20, marginVertical: 0},
581
+ hr: {borderBottomColor: colors.border, marginVertical: 2},
582
+ image: {borderRadius: 8},
583
+ li: {color: colors.textDim, fontSize: 14, lineHeight: 20},
584
+ link: {
585
+ color: colors.primary,
586
+ fontSize: 14,
587
+ fontStyle: "normal",
588
+ lineHeight: 20,
589
+ textDecorationLine: "underline",
590
+ },
591
+ paragraph: {paddingVertical: 0},
592
+ strikethrough: {color: colors.textDim, fontSize: 14, lineHeight: 20},
593
+ strong: {color: colors.foreground, fontSize: 14, fontWeight: "700", lineHeight: 20},
594
+ table: {borderColor: colors.border, borderRadius: 8},
595
+ tableCell: {padding: 8},
596
+ text: {color: colors.textDim, fontSize: 14, lineHeight: 20},
597
+ }),
598
+ [colors],
599
+ )
600
+ const markdownTheme = useMemo<NonNullable<useMarkdownHookOptions["theme"]>>(
601
+ () => ({
602
+ colors: {
603
+ background: colors.background,
604
+ border: colors.border,
605
+ code: colors.border,
606
+ link: colors.primary,
607
+ text: colors.textDim,
608
+ },
609
+ }),
610
+ [colors],
611
+ )
612
+ const elements = useMarkdown(markdown, {colorScheme: "light", styles: markdownStyles, theme: markdownTheme})
613
+
614
+ return (
615
+ <View style={styles.changelogMarkdownContent} testID="ota-changelog-markdown">
616
+ {elements}
617
+ </View>
618
+ )
619
+ }
620
+
621
+ export function ChangelogList({
464
622
  changelogs,
465
623
  colors,
624
+ title,
466
625
  }: {
467
626
  changelogs: MentraLiveOtaController["state"]["changelogs"]
468
627
  colors: MentraLiveOtaFlowTheme
628
+ title: string
469
629
  }) {
470
630
  if (changelogs.length === 0) return null
471
631
  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>
632
+ <View style={[styles.changelogCard, {borderColor: colors.border}]} testID="ota-changelog-card">
633
+ <Text style={[styles.changelogTitle, {color: colors.foreground}]}>{title}</Text>
634
+ <ScrollView
635
+ contentContainerStyle={styles.changelogContent}
636
+ nestedScrollEnabled
637
+ showsVerticalScrollIndicator
638
+ style={styles.changelogList}
639
+ testID="ota-changelog-scroll">
640
+ {changelogs.map((entry, index) => (
641
+ <View
642
+ key={entry.version}
643
+ style={[
644
+ styles.changelogEntry,
645
+ index > 0 && styles.changelogEntryDivider,
646
+ index > 0 && {borderTopColor: colors.border},
647
+ ]}>
648
+ <Text selectable style={[styles.changelogVersion, {color: colors.foreground}]}>
649
+ {entry.version}
650
+ </Text>
651
+ <ChangelogMarkdown colors={colors} markdown={entry.markdown} />
652
+ </View>
653
+ ))}
654
+ </ScrollView>
655
+ </View>
484
656
  )
485
657
  }
486
658
 
@@ -544,6 +716,7 @@ const styles = StyleSheet.create({
544
716
  },
545
717
  page: {flex: 1, paddingBottom: 24, paddingHorizontal: 24},
546
718
  centerContent: {alignItems: "center", flex: 1, gap: 16, justifyContent: "center"},
719
+ topContent: {justifyContent: "flex-start", paddingTop: 12},
547
720
  actionSpacer: {height: 48},
548
721
  actions: {gap: 12},
549
722
  icon: {fontSize: 64, fontWeight: "500", lineHeight: 72, textAlign: "center"},
@@ -552,11 +725,23 @@ const styles = StyleSheet.create({
552
725
  percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
553
726
  progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
554
727
  progressFill: {borderRadius: 4, height: 8},
555
- changelogList: {flexShrink: 1, maxHeight: 260, maxWidth: 420, width: "100%"},
556
- changelogContent: {gap: 18, paddingVertical: 4},
728
+ changelogCard: {
729
+ borderRadius: 16,
730
+ borderWidth: 1,
731
+ flexShrink: 1,
732
+ gap: 12,
733
+ maxHeight: 300,
734
+ maxWidth: 420,
735
+ padding: 16,
736
+ width: "100%",
737
+ },
738
+ changelogTitle: {fontSize: 16, fontWeight: "700"},
739
+ changelogList: {flexShrink: 1, maxHeight: 232, width: "100%"},
740
+ changelogContent: {gap: 20, paddingBottom: 2},
557
741
  changelogEntry: {gap: 8},
558
- changelogVersion: {fontSize: 16, fontWeight: "600"},
559
- changelogMarkdown: {fontSize: 14, lineHeight: 20},
742
+ changelogEntryDivider: {borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20},
743
+ changelogVersion: {fontSize: 14, fontWeight: "600"},
744
+ changelogMarkdownContent: {gap: 10},
560
745
  button: {
561
746
  alignItems: "center",
562
747
  borderRadius: 50,