OnboardingView+Util.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import SwiftUI
  2. /// Represents the navigation direction in the onboarding flow
  3. enum OnboardingNavigationDirection {
  4. case forward
  5. case backward
  6. }
  7. enum OnboardingChapter: Int, CaseIterable {
  8. case prepareTrio
  9. case therapySettings
  10. case deliveryLimits
  11. case algorithmSettings
  12. case permissionRequests
  13. var id: Int { rawValue }
  14. var title: String {
  15. switch self {
  16. case .prepareTrio:
  17. return String(localized: "Prepare Trio")
  18. case .therapySettings:
  19. return String(localized: "Therapy Settings")
  20. case .deliveryLimits:
  21. return String(localized: "Delivery Limits")
  22. case .algorithmSettings:
  23. return String(localized: "Algorithm Settings")
  24. case .permissionRequests:
  25. return String(localized: "Permission Requests")
  26. }
  27. }
  28. var overviewDescription: String {
  29. switch self {
  30. case .prepareTrio:
  31. return String(
  32. localized: "Configure diagnostics sharing, optionally sync with Nightscout, and enter essentials."
  33. )
  34. case .therapySettings:
  35. return String(
  36. localized: "Define your glucose targets, basal rates, carb ratios, and insulin sensitivities."
  37. )
  38. case .deliveryLimits:
  39. return String(
  40. localized: "Set boundaries for insulin delivery and carb entries to help Trio keep you safe."
  41. )
  42. case .algorithmSettings:
  43. return String(
  44. localized: "Customize Trio’s algorithm features. Most users start with the recommended settings."
  45. )
  46. case .permissionRequests:
  47. return String(
  48. localized: "Authorize Trio to send notifications and use Bluetooth. You must allow both for Trio to work properly."
  49. )
  50. }
  51. }
  52. var duration: String {
  53. switch self {
  54. case .prepareTrio:
  55. return "3-5"
  56. case .therapySettings:
  57. return "5-10"
  58. case .deliveryLimits:
  59. return "3-5"
  60. case .algorithmSettings:
  61. return "5-10"
  62. case .permissionRequests:
  63. return "1"
  64. }
  65. }
  66. var completedDescription: String {
  67. switch self {
  68. case .prepareTrio:
  69. return String(
  70. localized: "App diagnostics sharing, Nightscout setup, and unit and pump model selection are all complete."
  71. )
  72. case .therapySettings:
  73. return String(
  74. localized: "Glucose target, basal rates, carb ratios, and insulin sensitivity match your needs."
  75. )
  76. case .deliveryLimits:
  77. return String(
  78. localized: "Safety boundaries for insulin delivery and carb entries are set to help Trio keep you safe."
  79. )
  80. case .algorithmSettings:
  81. return String(localized: "Trio’s algorithm features are customized to fit your preferences and needs.")
  82. case .permissionRequests:
  83. return String(localized: "Notifications and Bluetooth permissions are handled to your liking.")
  84. }
  85. }
  86. }
  87. /// Represents the different steps in the onboarding process.
  88. enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
  89. case welcome
  90. case startupInfo
  91. case overview
  92. case diagnostics
  93. case nightscout
  94. case unitSelection
  95. case glucoseTarget
  96. case basalRates
  97. case carbRatio
  98. case insulinSensitivity
  99. case deliveryLimits
  100. case algorithmSettings
  101. case autosensSettings
  102. case smbSettings
  103. case targetBehavior
  104. case notifications
  105. case bluetooth
  106. case completed
  107. var id: Int { rawValue }
  108. var hasSubsteps: Bool {
  109. self == .deliveryLimits
  110. }
  111. /// The title to display for this onboarding step.
  112. var title: String {
  113. switch self {
  114. case .welcome:
  115. return String(localized: "Welcome to Trio")
  116. case .startupInfo:
  117. return String(localized: "Startup Guide")
  118. case .overview:
  119. return String(localized: "Overview")
  120. case .diagnostics:
  121. return String(localized: "Diagnostics")
  122. case .nightscout:
  123. return String(localized: "Nightscout")
  124. case .unitSelection:
  125. return String(localized: "Units & Pump")
  126. case .glucoseTarget:
  127. return String(localized: "Glucose Targets")
  128. case .basalRates:
  129. return String(localized: "Basal Rates")
  130. case .carbRatio:
  131. return String(localized: "Carb Ratios")
  132. case .insulinSensitivity:
  133. return String(localized: "Insulin Sensitivities")
  134. case .deliveryLimits:
  135. return String(localized: "Delivery Limits")
  136. case .algorithmSettings:
  137. return String(localized: "Algorithm Settings")
  138. case .autosensSettings:
  139. return String(localized: "Autosens")
  140. case .smbSettings:
  141. return String(localized: "Super Micro Bolus")
  142. case .targetBehavior:
  143. return String(localized: "Target Behavior")
  144. case .notifications:
  145. return String(localized: "Notifications")
  146. case .bluetooth:
  147. return String(localized: "Bluetooth")
  148. case .completed:
  149. return String(localized: "All Set!")
  150. }
  151. }
  152. /// A detailed description of what this onboarding step is about.
  153. var description: String {
  154. switch self {
  155. case .welcome:
  156. return String(
  157. localized: "Trio is a powerful app that helps you manage your diabetes. Let's get started by setting up a few important parameters that will help Trio work effectively for you."
  158. )
  159. case .startupInfo:
  160. return String(
  161. localized: "Trio comes with a helpful Startup Guide. We recommend opening it now and following along as you go — side by side."
  162. )
  163. case .overview:
  164. return String(
  165. localized: "Trio's Onboarding takes about 15-30 minutes to complete. We'll guide you through each step."
  166. )
  167. case .diagnostics:
  168. return String(
  169. localized: "By default, Trio collects crash reports and other anonymized data related to errors, exceptions, and overall app performance."
  170. )
  171. case .nightscout:
  172. return String(
  173. localized: "Nightscout is a cloud-based platform that allows you to store your diabetes data. It's often used by caregivers to remotely monitor what Trio is doing."
  174. )
  175. case .unitSelection:
  176. return String(
  177. localized: "Before you can begin with configuring your therapy settings, Trio needs to know which units you use for your glucose and insulin measurements (based on your pump model)."
  178. )
  179. case .glucoseTarget:
  180. return String(
  181. localized: "Your glucose target is the blood glucose level you aim to maintain. Trio will use this to calculate insulin doses and provide recommendations."
  182. )
  183. case .basalRates:
  184. return String(
  185. localized: "Your basal profile represents the amount of background insulin you need throughout the day. This helps Trio calculate your insulin needs."
  186. )
  187. case .carbRatio:
  188. return String(
  189. localized: "Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover. This is essential for accurate meal bolus calculations."
  190. )
  191. case .insulinSensitivity:
  192. return String(
  193. localized: "Your insulin sensitivity factor (ISF) indicates how much one unit of insulin will lower your blood glucose. This helps calculate correction boluses."
  194. )
  195. case .deliveryLimits:
  196. return String(
  197. localized: "Trio includes several safety limits for insulin delivery and carbohydrate entry, helping ensure a safe and effective experience."
  198. )
  199. case .algorithmSettings:
  200. return String(
  201. localized: "Trio includes several algorithm settings that allow you to customize the oref algorithm behavior to suit your specific needs."
  202. )
  203. case .autosensSettings:
  204. return String(
  205. localized: "Auto-sensitivity (Autosens) adjusts insulin delivery based on observed sensitivity or resistance."
  206. )
  207. case .smbSettings:
  208. return String(
  209. localized: "SMB (Super Micro Bolus) is an oref algorithm feature that delivers small frequent boluses instead of temporary basals for faster glucose control."
  210. )
  211. case .targetBehavior:
  212. return String(
  213. localized: "Target Behavior allows you to adjust how temporary targets influence ISF, basal, and auto-targeting based on sensitivity or resistance."
  214. )
  215. case .notifications:
  216. return String(localized: " Allow Trio to send you Notifications. These may include alerts, sounds, and icon badges.")
  217. case .bluetooth:
  218. return String(localized: "Allow Trio to use Bluetooth to communicate with your insulin pump and CGM.")
  219. case .completed:
  220. return String(
  221. localized: "Great job! You've completed the initial setup of Trio. You can always adjust these settings later in the app."
  222. )
  223. }
  224. }
  225. /// The system icon name associated with this step.
  226. var iconName: String {
  227. switch self {
  228. case .welcome:
  229. return "hand.wave.fill"
  230. case .startupInfo:
  231. return "list.bullet.clipboard.fill"
  232. case .overview:
  233. return "checklist.unchecked"
  234. case .diagnostics:
  235. return "waveform.badge.magnifyingglass"
  236. case .nightscout:
  237. return "owl"
  238. case .unitSelection:
  239. return "numbers.rectangle"
  240. case .glucoseTarget:
  241. return "target"
  242. case .basalRates:
  243. return "chart.xyaxis.line"
  244. case .carbRatio:
  245. return "fork.knife"
  246. case .insulinSensitivity:
  247. return "drop.fill"
  248. case .deliveryLimits:
  249. return "slider.horizontal.3"
  250. case .algorithmSettings:
  251. return "gearshape.2.fill"
  252. case .autosensSettings:
  253. return "dial.low.fill"
  254. case .smbSettings:
  255. return "bolt.fill"
  256. case .targetBehavior:
  257. return "gyroscope"
  258. case .notifications:
  259. return "bell.badge.fill"
  260. case .bluetooth:
  261. return "logo.bluetooth.capsule.portrait.fill"
  262. case .completed:
  263. return "checkmark.circle.fill"
  264. }
  265. }
  266. /// Returns the next step in the onboarding process, or nil if this is the last step.
  267. var next: OnboardingStep? {
  268. let allCases = OnboardingStep.allCases
  269. let currentIndex = allCases.firstIndex(of: self) ?? 0
  270. let nextIndex = currentIndex + 1
  271. return nextIndex < allCases.count ? allCases[nextIndex] : nil
  272. }
  273. /// Returns the previous step in the onboarding process, or nil if this is the first step.
  274. var previous: OnboardingStep? {
  275. let allCases = OnboardingStep.allCases
  276. let currentIndex = allCases.firstIndex(of: self) ?? 0
  277. let previousIndex = currentIndex - 1
  278. return previousIndex >= 0 ? allCases[previousIndex] : nil
  279. }
  280. /// The accent color to use for this step.
  281. var accentColor: Color {
  282. switch self {
  283. case .algorithmSettings,
  284. .autosensSettings,
  285. .bluetooth,
  286. .completed,
  287. .deliveryLimits,
  288. .diagnostics,
  289. .nightscout,
  290. .notifications,
  291. .overview,
  292. .smbSettings,
  293. .startupInfo,
  294. .targetBehavior,
  295. .unitSelection,
  296. .welcome:
  297. return Color.blue
  298. case .glucoseTarget:
  299. return Color.green
  300. case .basalRates:
  301. return Color.purple
  302. case .carbRatio:
  303. return Color.orange
  304. case .insulinSensitivity:
  305. return Color.cyan
  306. }
  307. }
  308. var chapterCompletion: OnboardingChapter? {
  309. switch self {
  310. case .unitSelection:
  311. return .prepareTrio
  312. case .insulinSensitivity:
  313. return .therapySettings
  314. case .deliveryLimits:
  315. // ❗ Delivery Limits depends on the substep, not just the step.
  316. // Skip here
  317. return nil
  318. case .targetBehavior:
  319. // ❗ Target Behavior depends on the substep, not just the step.
  320. // Skip here
  321. return nil
  322. default:
  323. return nil
  324. }
  325. }
  326. }
  327. var nonInfoOnboardingSteps: [OnboardingStep] { OnboardingStep.allCases
  328. .filter { $0 != .welcome && $0 != .startupInfo && $0 != .overview && $0 != .completed }
  329. }
  330. enum StartupSubstep: Int, CaseIterable, Identifiable {
  331. case startupGuide
  332. case returningUser
  333. case forceCloseWarning
  334. var id: Int { rawValue }
  335. }
  336. enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
  337. case maxIOB
  338. case maxBolus
  339. case maxBasal
  340. case maxCOB
  341. case minimumSafetyThreshold
  342. var id: Int { rawValue }
  343. var title: String {
  344. switch self {
  345. case .maxIOB: return String(localized: "Maximum Insulin on Board (IOB)", comment: "Max IOB")
  346. case .maxBolus: return String(localized: "Maximum Bolus")
  347. case .maxBasal: return String(localized: "Maximum Basal Rate")
  348. case .maxCOB: return String(localized: "Maximum Carbs on Board (COB)", comment: "Max COB")
  349. case .minimumSafetyThreshold: return String(localized: "Minimum Safety Threshold")
  350. }
  351. }
  352. var hint: String {
  353. switch self {
  354. case .maxIOB: return String(localized: "Maximum units of insulin allowed to be active.")
  355. case .maxBolus: return String(localized: "Largest bolus of insulin allowed.")
  356. case .maxBasal: return String(localized: "Largest basal rate allowed.")
  357. case .maxCOB: return String(localized: "Maximum amount of active carbs considered by the algorithm.")
  358. case .minimumSafetyThreshold: return String(localized: "Increase the safety threshold used to suspend insulin delivery.")
  359. }
  360. }
  361. func description(units: GlucoseUnits) -> any View {
  362. switch self {
  363. case .maxIOB:
  364. return VStack(alignment: .leading, spacing: 10) {
  365. Text("Default: 0 units").bold()
  366. Text(
  367. "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio (unless you currently have negative IOB)."
  368. )
  369. .bold()
  370. .foregroundStyle(Color.orange)
  371. Text(
  372. "Choose a value that covers your highest insulin needs — think about a correction for a very high glucose reading plus your biggest meal bolus. This gives Trio room to work while keeping you safe."
  373. )
  374. Text(
  375. "Max IOB sets a safety limit on how much insulin Trio can automatically deliver above your scheduled basal rates. This prevents the system from giving too much insulin at once."
  376. )
  377. VStack(alignment: .leading, spacing: 0) {
  378. Text("Trio calculates your current Insulin On Board (IOB) from:")
  379. Text("• Boluses (including SMBs)")
  380. Text("• Temporary Basal Rates (TBRs)")
  381. Text(" ◦ A TBR higher than your scheduled rate will increase IOB")
  382. Text(" ◦ A TBR lower than your scheduled rate will decrease IOB")
  383. }
  384. Text(
  385. "If delivering more insulin would push your IOB above this limit, Trio will reduce or skip the dose to stay within the safety boundary. This applies to SMBs, TBRs, and the recommendation from the bolus calculator."
  386. )
  387. VStack(alignment: .leading, spacing: 0) {
  388. Text("What's NOT limited:")
  389. Text("• Manual boluses you enter yourself")
  390. Text("• Manual temporary basal rates you set yourself")
  391. }
  392. }
  393. .fixedSize(horizontal: false, vertical: true)
  394. case .maxBolus:
  395. return VStack(alignment: .leading, spacing: 8) {
  396. Text(
  397. "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus."
  398. )
  399. Text("Most set this to their largest meal bolus. Then, adjust if needed.")
  400. Text("If you attempt to request a bolus larger than this, the bolus will not be accepted.")
  401. }
  402. case .maxBasal:
  403. return VStack(alignment: .leading, spacing: 8) {
  404. Text(
  405. "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
  406. )
  407. Text(
  408. "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
  409. )
  410. }
  411. case .maxCOB:
  412. return VStack(alignment: .leading, spacing: 8) {
  413. Text(
  414. "This setting defines the maximum amount of Carbs On Board (COB) at any given time for Trio to use in dosing calculations. If more carbs are entered than allowed by this limit, Trio will cap the current COB in calculations to Max COB and remain at max until all remaining carbs have shown to be absorbed."
  415. )
  416. Text(
  417. "For example, if Max COB is 120 g and you enter a meal containing 150 g of carbs, your COB will remain at 120 g until the remaining 30 g have been absorbed."
  418. )
  419. Text("This is an important limit when UAM is ON.")
  420. }
  421. case .minimumSafetyThreshold:
  422. return VStack(alignment: .leading, spacing: 8) {
  423. Text("Default: Set by Algorithm").bold()
  424. Text(
  425. "Minimum Threshold Setting is, by default, determined by your set Glucose Target. This threshold automatically suspends insulin delivery if your glucose levels are forecasted to fall below this value. It’s designed to protect against hypoglycemia, particularly during sleep or other vulnerable times."
  426. )
  427. Text(
  428. "Trio will use the larger of the default setting calculation below and the value entered here."
  429. )
  430. VStack(alignment: .leading, spacing: 8) {
  431. VStack(alignment: .leading, spacing: 5) {
  432. Text("The default setting is based on this calculation:").bold()
  433. Text("TargetGlucose - 0.5 × (TargetGlucose - 40)")
  434. }
  435. VStack(alignment: .leading, spacing: 5) {
  436. Text(
  437. "If your glucose target is \(units == .mgdL ? "110" : 110.formattedAsMmolL) \(units.rawValue), Trio will use a safety threshold of \(units == .mgdL ? "75" : 75.formattedAsMmolL) \(units.rawValue), unless you set Minimum Safety Threshold to something > \(units == .mgdL ? "75" : 75.formattedAsMmolL) \(units.rawValue)."
  438. )
  439. Text(
  440. "\(units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(units == .mgdL ? "110" : 110.formattedAsMmolL) - \(units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(units == .mgdL ? "75" : 75.formattedAsMmolL)"
  441. )
  442. }
  443. Text(
  444. "This setting is limited to values between \(units == .mgdL ? "60" : 60.formattedAsMmolL) - \(units == .mgdL ? "120" : 120.formattedAsMmolL) \(units.rawValue)"
  445. )
  446. Text(
  447. "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
  448. )
  449. }
  450. }
  451. }
  452. }
  453. }
  454. /// Three-state diagnostics-sharing consent.
  455. ///
  456. /// Maps to a pair of independent `Bool?` flags in `PropertyPersistentFlags`:
  457. /// `diagnosticsSharingEnabled` (Crashlytics) and `telemetryEnabled` (the
  458. /// anonymous-usage POST). See `TelemetryClient`.
  459. enum DiagnosticsSharingOption: String, Equatable, CaseIterable, Identifiable {
  460. case full
  461. case crashOnly
  462. case disabled
  463. var id: String { rawValue }
  464. var displayName: String {
  465. switch self {
  466. case .full:
  467. return String(localized: "Enable Full Sharing")
  468. case .crashOnly:
  469. return String(localized: "Crash Reports Only")
  470. case .disabled:
  471. return String(localized: "Disable Sharing")
  472. }
  473. }
  474. var caption: String {
  475. switch self {
  476. case .full:
  477. return String(localized: "Share anonymous crash reports + usage data.")
  478. case .crashOnly:
  479. return String(localized: "Share only crash reports — no usage data.")
  480. case .disabled:
  481. return String(localized: "Do not share any diagnostic data.")
  482. }
  483. }
  484. var crashlyticsEnabled: Bool {
  485. switch self {
  486. case .crashOnly,
  487. .full: return true
  488. case .disabled: return false
  489. }
  490. }
  491. var telemetryEnabled: Bool {
  492. switch self {
  493. case .full: return true
  494. case .crashOnly,
  495. .disabled: return false
  496. }
  497. }
  498. init(crashlyticsEnabled: Bool, telemetryEnabled: Bool) {
  499. switch (crashlyticsEnabled, telemetryEnabled) {
  500. case (true, true): self = .full
  501. case (true, false): self = .crashOnly
  502. case (false, true): self = .full // unreachable in normal flow
  503. case (false, false): self = .disabled
  504. }
  505. }
  506. }
  507. enum PumpOptionForOnboardingUnits: String, Equatable, CaseIterable, Identifiable {
  508. case minimed
  509. case omnipodEros
  510. case omnipodDash
  511. case dana
  512. case medtrum
  513. var id: String { rawValue }
  514. var displayName: String {
  515. switch self {
  516. case .minimed:
  517. return "Medtronic"
  518. case .omnipodEros:
  519. return "Omnipod Eros"
  520. case .omnipodDash:
  521. return "Omnipod DASH"
  522. case .dana:
  523. return "Dana (RS/-i)"
  524. case .medtrum:
  525. return "Medtrum Nano"
  526. }
  527. }
  528. }
  529. enum NightscoutSetupOption: String, Equatable, CaseIterable, Identifiable {
  530. case setupNightscout
  531. case skipNightscoutSetup
  532. case noSelection
  533. var id: String { rawValue }
  534. var displayName: String {
  535. switch self {
  536. case .setupNightscout:
  537. return String(localized: "Setup Nightscout for Trio")
  538. case .skipNightscoutSetup:
  539. return String(localized: "Skip Nightscout Setup")
  540. case .noSelection:
  541. return ""
  542. }
  543. }
  544. }
  545. enum NightscoutImportOption: String, Equatable, CaseIterable, Identifiable {
  546. case useImport
  547. case skipImport
  548. case noSelection
  549. var id: String { rawValue }
  550. var displayName: String {
  551. switch self {
  552. case .useImport:
  553. return String(localized: "Import Settings")
  554. case .skipImport:
  555. return String(localized: "Configure Yourself")
  556. case .noSelection:
  557. return ""
  558. }
  559. }
  560. }
  561. enum NightscoutSubstep: Int, CaseIterable, Identifiable {
  562. case setupSelection
  563. case connectToNightscout
  564. case uploadToNightscout
  565. case uploadGlucoseToNightscout
  566. case importFromNightscout
  567. var id: Int { rawValue }
  568. }
  569. struct BulletPoint: View {
  570. let text: String
  571. init(_ text: String) {
  572. self.text = text
  573. }
  574. var body: some View {
  575. HStack(alignment: .top) {
  576. Text("•")
  577. Text(text)
  578. .fixedSize(horizontal: false, vertical: true)
  579. .multilineTextAlignment(.leading)
  580. }
  581. }
  582. }
  583. enum OnboardingInputSectionType: Equatable {
  584. case decimal
  585. case boolean
  586. static func == (lhs: OnboardingInputSectionType, rhs: OnboardingInputSectionType) -> Bool {
  587. switch (lhs, rhs) {
  588. case (.boolean, .boolean):
  589. return true
  590. case (.decimal, .decimal):
  591. return true
  592. default:
  593. return false
  594. }
  595. }
  596. }