Przeglądaj źródła

Merge pull request #440 from nightscout/onboarding

Trio Onboarding Wizard
Sam King 1 rok temu
rodzic
commit
71b9f265fa
49 zmienionych plików z 4348 dodań i 601 usunięć
  1. 113 0
      PRIVACY_POLICY.md
  2. 210 16
      Trio.xcodeproj/project.pbxproj
  3. 4 0
      Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  4. 118 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  5. 30 0
      Trio/GoogleService-Info.plist
  6. 14 2
      Trio/Sources/Application/AppDelegate.swift
  7. 32 2
      Trio/Sources/Application/TrioApp.swift
  8. 278 2
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  9. 3 1
      Trio/Sources/Models/BloodGlucose.swift
  10. 8 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  11. 5 0
      Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsDataFlow.swift
  12. 3 0
      Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsProvider.swift
  13. 35 0
      Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsStateModel.swift
  14. 82 0
      Trio/Sources/Modules/AppDiagnostics/View/AppDiagnosticsRootView.swift
  15. 1 1
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  16. 3 3
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  17. 3 16
      Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  18. 0 264
      Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  19. 0 94
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  20. 0 128
      Trio/Sources/Modules/NightscoutConfig/View/ProfileImport/NightscoutImportResultView.swift
  21. 0 67
      Trio/Sources/Modules/NightscoutConfig/View/ProfileImport/ReviewInsulinActionView.swift
  22. 5 0
      Trio/Sources/Modules/Onboarding/OnboardingDataFlow.swift
  23. 67 0
      Trio/Sources/Modules/Onboarding/OnboardingProvider.swift
  24. 263 0
      Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift
  25. 523 0
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  26. 385 0
      Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift
  27. 213 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BasalProfileStepView.swift
  28. 188 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CarbRatioStepView.swift
  29. 42 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CompletedStepView.swift
  30. 109 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DeliveryLimitsStepView.swift
  31. 58 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DiagnosticsStepView.swift
  32. 128 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift
  33. 204 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/InsulinSensitivityStepView.swift
  34. 42 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/LogoAnimation.swift
  35. 86 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutImportStepView.swift
  36. 78 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutLoginStepView.swift
  37. 35 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutSetupStepView.swift
  38. 29 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OverviewStepView.swift
  39. 47 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuideStepView.swift
  40. 51 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/UnitSelectionStepView.swift
  41. 36 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift
  42. 464 0
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  43. 279 0
      Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift
  44. 9 1
      Trio/Sources/Modules/Settings/SettingItems.swift
  45. 8 0
      Trio/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift
  46. 2 2
      Trio/Sources/Modules/Settings/View/Subviews/TherapySettingsView.swift
  47. 3 0
      Trio/Sources/Router/Screen.swift
  48. 50 0
      Trio/Sources/Services/OnboardingManager/OnboardingManager.swift
  49. 2 0
      Trio/Sources/Views/SettingInputSection.swift

+ 113 - 0
PRIVACY_POLICY.md

@@ -0,0 +1,113 @@
+# Privacy Policy
+
+## Introduction
+
+This Privacy Policy explains how we collect, use, and share
+information when you use Trio. We respect your privacy and are
+committed to protecting your personal data. Please read this Privacy
+Policy carefully to understand our practices regarding your personal
+data.
+
+## Information We Collect
+
+### Crash Reporting (Opt-In by default, with ability to Opt-Out)
+
+Our App uses Google Firebase Crashlytics to collect crash reports. You
+will be asked to opt in to crash reporting when you first use Trio,
+and you can change this setting at any time.
+
+For users who use Trio without going through the onboarding process,
+we opt them in to crash reporting by default, but you can opt out at
+any time.
+
+The following information may be sent to Crashlytics when the App
+crashes:
+
+- Time and date of the crash
+- Device state at the time of the crash
+- Stack trace information
+- Device model and OS version
+- A generated unique identifier (not personally identifiable)
+
+### Debug Symbols (dSYMs)
+
+As an open source project, our build scripts upload debug symbols
+(dSYMs) to Google's servers. We use these files to give us
+deobfuscated and human-readable crash reports, and contain mapping
+information that helps us interpret crash reports. dSYM files only
+contain code-related mapping information to decode a stack-trace into
+a readable format, such as function names, class names, method names,
+and line numbers. They are used to create human-readable crash reports
+to help us understand crashes. These files do not contain any personal
+information about you or your device usage.
+
+## How We Use Your Information
+
+We use anonymous crash report information exclusively to:
+
+- Identify and fix bugs and crashes
+- Improve the App's stability
+
+We do not use this information for any other purpose, such as
+analytics, marketing, or user profiling.
+
+## Data Sharing and Third-Party Services
+
+### Crashlytics
+
+We use Google Firebase Crashlytics to collect and analyze crash
+reports. Crashlytics' privacy practices are governed by the [Google
+Privacy Policy](https://policies.google.com/privacy). For more
+information about how Crashlytics processes data, please visit their
+documentation.
+
+### Open Source Contributors
+
+As an open source project, crash reports and debugging information may
+be visible to project contributors who help maintain and improve the
+App. All contributors are expected to adhere to this privacy policy
+and handle any data responsibly.
+
+## Opting Out
+
+You can opt out of crash reporting at any time through the Trio
+settings. If you opt out:
+
+- No crash data will be collected or sent to us
+- Previously collected crash data will still be retained as described below
+
+To avoid sending dSYMs to Crashlytics, you can delete the Trio target
+Build Phase script, titled "Copy dSYMs to Crashlytics".
+
+## Data Retention
+
+Crash data and associated debugging information are retained only as
+long as necessary to analyze and fix issues. Typically, this is for a
+period of 90 days.
+
+## Your Rights
+
+You have certain rights regarding your personal information,
+including:
+
+- The right to access the information we have about you
+- The right to request deletion of your data
+- The right to opt-out of crash reporting (as described above)
+
+To exercise these rights, please contact us using the information
+provided below.
+
+## Changes to This Privacy Policy
+
+We may update this Privacy Policy from time to time. We will notify
+you of any changes by posting the new Privacy Policy on this page and
+updating the "Last Updated" date.
+
+## Contact Us
+
+If you have any questions about this Privacy Policy, please contact us
+on [Discord](http://discord.diy-trio.org/).
+
+## Last Updated
+
+April 6th, 2025

+ 210 - 16
Trio.xcodeproj/project.pbxproj

@@ -204,7 +204,9 @@
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */; };
 		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
+		3B47C6102DA0A28F00B0E5EF /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */; };
 		3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; };
 		3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; };
@@ -332,6 +334,7 @@
 		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
+		BD10516D2DA986E1007C6D89 /* LogoAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
 		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
@@ -354,6 +357,14 @@
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
 		BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
 		BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */; };
+		BD47FD132D88AA700043966B /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD122D88AA6B0043966B /* OnboardingManager.swift */; };
+		BD47FD172D88AAF50043966B /* CompletedStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD162D88AAEF0043966B /* CompletedStepView.swift */; };
+		BD47FD192D88AAFE0043966B /* OnboardingRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD182D88AAF90043966B /* OnboardingRootView.swift */; };
+		BD47FD1B2D88AB4F0043966B /* OnboardingStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */; };
+		BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDD62D8B64CC0043966B /* CarbRatioStepView.swift */; };
+		BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */; };
+		BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */; };
+		BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD47FDDC2D8B65AD0043966B /* GlucoseTargetStepView.swift */; };
 		BD4D738D2D15A4080052227B /* TDDStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */; };
 		BD4D738E2D15A4080052227B /* TDDStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */; };
 		BD4D73A22D15A42A0052227B /* TDDStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D73A12D15A4220052227B /* TDDStorage.swift */; };
@@ -377,6 +388,8 @@
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
 		BD8207CE2D2B42E70023339D /* Trio Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		BD8E6B212D9036CA00ABF8FA /* OnboardingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */; };
+		BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */; };
 		BD8FC0542D66186000B95AED /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0532D66186000B95AED /* TestError.swift */; };
 		BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */; };
 		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
@@ -540,6 +553,12 @@
 		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */; };
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
+		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
+		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
+		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
+		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
+		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
+		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -555,8 +574,6 @@
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
-		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
-		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */; };
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
@@ -581,6 +598,8 @@
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
+		DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */; };
+		DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */; };
 		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -621,6 +640,14 @@
 		DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */; };
 		DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */; };
 		DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */; };
+		DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */; };
+		DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */; };
+		DDF6905C2DA0AFC5008BF16C /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */; };
+		DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */; };
+		DDF691032DA2CA1E008BF16C /* AppDiagnosticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */; };
+		DDF691052DA2CA23008BF16C /* AppDiagnosticsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */; };
+		DDF691072DA2CA2D008BF16C /* AppDiagnosticsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */; };
+		DDF691372DA30332008BF16C /* StartupGuideStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF691362DA30332008BF16C /* StartupGuideStepView.swift */; };
 		DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */; };
 		DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */; };
 		DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */; };
@@ -985,6 +1012,7 @@
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1094,6 +1122,7 @@
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
+		BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoAnimation.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
@@ -1115,6 +1144,14 @@
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
 		BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessageKeys.swift; sourceTree = "<group>"; };
+		BD47FD122D88AA6B0043966B /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = "<group>"; };
+		BD47FD162D88AAEF0043966B /* CompletedStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedStepView.swift; sourceTree = "<group>"; };
+		BD47FD182D88AAF90043966B /* OnboardingRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingRootView.swift; sourceTree = "<group>"; };
+		BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStateModel.swift; sourceTree = "<group>"; };
+		BD47FDD62D8B64CC0043966B /* CarbRatioStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatioStepView.swift; sourceTree = "<group>"; };
+		BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinSensitivityStepView.swift; sourceTree = "<group>"; };
+		BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalProfileStepView.swift; sourceTree = "<group>"; };
+		BD47FDDC2D8B65AD0043966B /* GlucoseTargetStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTargetStepView.swift; sourceTree = "<group>"; };
 		BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D73A12D15A4220052227B /* TDDStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDStorage.swift; sourceTree = "<group>"; };
@@ -1134,6 +1171,8 @@
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProvider.swift; sourceTree = "<group>"; };
+		BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDataFlow.swift; sourceTree = "<group>"; };
 		BD8FC0532D66186000B95AED /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = "<group>"; };
 		BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
@@ -1306,6 +1345,12 @@
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
+		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
+		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
+		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
+		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
+		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
+		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
 		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
 		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
@@ -1318,8 +1363,6 @@
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
-		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
-		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentMenuView.swift; sourceTree = "<group>"; };
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
@@ -1347,6 +1390,8 @@
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
+		DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewStepView.swift; sourceTree = "<group>"; };
+		DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Util.swift"; sourceTree = "<group>"; };
 		DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsUtil.swift; sourceTree = "<group>"; };
 		DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmodulesView.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
@@ -1387,6 +1432,14 @@
 		DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = "<group>"; };
+		DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingStateModel+Nightscout.swift"; sourceTree = "<group>"; };
+		DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsStepView.swift; sourceTree = "<group>"; };
+		DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = "<group>"; };
+		DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsDataFlow.swift; sourceTree = "<group>"; };
+		DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsProvider.swift; sourceTree = "<group>"; };
+		DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsStateModel.swift; sourceTree = "<group>"; };
+		DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDiagnosticsRootView.swift; sourceTree = "<group>"; };
+		DDF691362DA30332008BF16C /* StartupGuideStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupGuideStepView.swift; sourceTree = "<group>"; };
 		DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsDataFlow.swift; sourceTree = "<group>"; };
 		DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsProvider.swift; sourceTree = "<group>"; };
 		DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1460,6 +1513,7 @@
 				3833B46D26012030003021B3 /* Algorithms in Frameworks */,
 				3B4BA7822D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Frameworks */,
 				3B4BA76E2D8DBD690069D5B8 /* DanaKit.framework in Frameworks */,
+				3B47C6102DA0A28F00B0E5EF /* FirebaseCrashlytics in Frameworks */,
 				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
@@ -1749,6 +1803,7 @@
 			children = (
 				DDD163032C4C67B400CD525A /* Adjustments */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
+				DDF690FE2DA2C9EE008BF16C /* AppDiagnostics */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */,
 				3811DE0425C9D32E00A708ED /* Base */,
@@ -1772,6 +1827,7 @@
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				19D466A129AA2B0A004D5F33 /* MealSettings */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
+				BD47FD142D88AACC0043966B /* Onboarding */,
 				99C01B871ACAB3F32CE755C7 /* PumpConfig */,
 				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
 				3811DE3825C9D4A100A708ED /* Settings */,
@@ -1897,6 +1953,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				BD47FD112D88AA630043966B /* OnboardingManager */,
 				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3811DE9225C9D88200A708ED /* Appearance */,
@@ -2194,6 +2251,7 @@
 		388E595A25AD948C0019842D /* Trio */ = {
 			isa = PBXGroup;
 			children = (
+				3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */,
 				3811DED425C9E1E300A708ED /* Resources */,
 				3811DE1325C9D39E00A708ED /* Sources */,
 			);
@@ -2472,7 +2530,6 @@
 		4F4AE4D901E8BA872B207D7F /* View */ = {
 			isa = PBXGroup;
 			children = (
-				DD6B7CB72C7BAC1B00B75029 /* ProfileImport */,
 				8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */,
 				5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */,
 				5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */,
@@ -2677,6 +2734,57 @@
 			path = "StatStateModel+Setup";
 			sourceTree = "<group>";
 		};
+		BD47FD112D88AA630043966B /* OnboardingManager */ = {
+			isa = PBXGroup;
+			children = (
+				BD47FD122D88AA6B0043966B /* OnboardingManager.swift */,
+			);
+			path = OnboardingManager;
+			sourceTree = "<group>";
+		};
+		BD47FD142D88AACC0043966B /* Onboarding */ = {
+			isa = PBXGroup;
+			children = (
+				BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */,
+				BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */,
+				BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */,
+				DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */,
+				BD47FD152D88AAD80043966B /* View */,
+			);
+			path = Onboarding;
+			sourceTree = "<group>";
+		};
+		BD47FD152D88AAD80043966B /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */,
+				DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */,
+				BD47FD182D88AAF90043966B /* OnboardingRootView.swift */,
+				BD47FDD52D8B64AE0043966B /* OnboardingSteps */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		BD47FDD52D8B64AE0043966B /* OnboardingSteps */ = {
+			isa = PBXGroup;
+			children = (
+				DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */,
+				BD10516C2DA986DC007C6D89 /* LogoAnimation.swift */,
+				DDF691362DA30332008BF16C /* StartupGuideStepView.swift */,
+				DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */,
+				DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */,
+				DD3F1F8E2D9E151200DCE7B3 /* Nightscout */,
+				DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */,
+				DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */,
+				BD47FD162D88AAEF0043966B /* CompletedStepView.swift */,
+				BD47FDDC2D8B65AD0043966B /* GlucoseTargetStepView.swift */,
+				BD47FDDA2D8B65960043966B /* BasalProfileStepView.swift */,
+				BD47FDD82D8B65730043966B /* InsulinSensitivityStepView.swift */,
+				BD47FDD62D8B64CC0043966B /* CarbRatioStepView.swift */,
+			);
+			path = OnboardingSteps;
+			sourceTree = "<group>";
+		};
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
@@ -3121,6 +3229,16 @@
 			path = Helper;
 			sourceTree = "<group>";
 		};
+		DD3F1F8E2D9E151200DCE7B3 /* Nightscout */ = {
+			isa = PBXGroup;
+			children = (
+				DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */,
+				DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */,
+				DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */,
+			);
+			path = Nightscout;
+			sourceTree = "<group>";
+		};
 		DD4C57A42D73ADDA001BFF2C /* LiveActivity */ = {
 			isa = PBXGroup;
 			children = (
@@ -3149,15 +3267,6 @@
 			path = ViewElements;
 			sourceTree = "<group>";
 		};
-		DD6B7CB72C7BAC1B00B75029 /* ProfileImport */ = {
-			isa = PBXGroup;
-			children = (
-				DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */,
-				DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */,
-			);
-			path = ProfileImport;
-			sourceTree = "<group>";
-		};
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
@@ -3311,6 +3420,25 @@
 			path = "Classes+Properties";
 			sourceTree = "<group>";
 		};
+		DDF690FE2DA2C9EE008BF16C /* AppDiagnostics */ = {
+			isa = PBXGroup;
+			children = (
+				DDF691042DA2CA20008BF16C /* AppDiagnosticsStateModel.swift */,
+				DDF691022DA2CA14008BF16C /* AppDiagnosticsProvider.swift */,
+				DDF691002DA2CA0B008BF16C /* AppDiagnosticsDataFlow.swift */,
+				DDF690FF2DA2CA03008BF16C /* View */,
+			);
+			path = AppDiagnostics;
+			sourceTree = "<group>";
+		};
+		DDF690FF2DA2CA03008BF16C /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		DDF847DB2C5C28550049BB3B /* LiveActivitySettings */ = {
 			isa = PBXGroup;
 			children = (
@@ -3467,6 +3595,7 @@
 				38E8753D27554D5900975559 /* Embed Watch Content */,
 				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
 				DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */,
+				3B47C6112DA0A52C00B0E5EF /* Run Script: Copy dSYM to Crashlytics */,
 			);
 			buildRules = (
 			);
@@ -3484,6 +3613,7 @@
 				B958F1B62BA0711600484851 /* MKRingProgressView */,
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
 				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
+				3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */,
 			);
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3659,6 +3789,7 @@
 				B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */,
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
 				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
+				3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -3681,6 +3812,7 @@
 			files = (
 				8A91342C2D63D9A2007F8874 /* InfoPlist.xcstrings in Resources */,
 				CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */,
+				3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */,
 				38DF178D27733E6800B3528F /* snow.sks in Resources */,
 				388E597225AD9CF10019842D /* json in Resources */,
 				38DF178E27733E6800B3528F /* Assets.xcassets in Resources */,
@@ -3753,6 +3885,29 @@
 			shellPath = /bin/sh;
 			shellScript = "source \"${SRCROOT}\"/scripts/swiftformat.sh\n\n";
 		};
+		3B47C6112DA0A52C00B0E5EF /* Run Script: Copy dSYM to Crashlytics */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",
+				"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}",
+				"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",
+				"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
+				"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
+			);
+			name = "Run Script: Copy dSYM to Crashlytics";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n";
+		};
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 			isa = PBXShellScriptBuildPhase;
 			alwaysOutOfDate = 1;
@@ -3789,6 +3944,7 @@
 				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
+				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* TrioSettings.swift in Sources */,
@@ -3796,11 +3952,13 @@
 				58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
+				DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
+				BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -3815,7 +3973,6 @@
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */,
-				DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
@@ -3836,6 +3993,7 @@
 				DDF847E82C5DABA30049BB3B /* WatchConfigAppleWatchView.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
+				DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
@@ -3855,6 +4013,7 @@
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */,
 				DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */,
+				DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
@@ -3867,6 +4026,7 @@
 				5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				CEE9A6582BBB418300EB5194 /* CalibrationsStateModel.swift in Sources */,
+				BD47FD192D88AAFE0043966B /* OnboardingRootView.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
@@ -3875,6 +4035,7 @@
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				DD1745322C55AE6000211FAC /* TargetBehavoirStateModel.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
+				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
@@ -3893,6 +4054,7 @@
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
 				DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */,
 				38E44528274E401C00EC9A94 /* Protected.swift in Sources */,
+				DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
 				581516A42BCED84A00BF67D7 /* DebuggingIdentifiers.swift in Sources */,
 				E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */,
@@ -3915,6 +4077,7 @@
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
 				DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */,
+				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
@@ -3960,6 +4123,7 @@
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
+				BD47FD1B2D88AB4F0043966B /* OnboardingStateModel.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */,
@@ -3993,6 +4157,7 @@
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
+				DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
@@ -4012,6 +4177,7 @@
 				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
+				DDF691072DA2CA2D008BF16C /* AppDiagnosticsRootView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
@@ -4054,7 +4220,6 @@
 				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
 				CE7CA3532A064973004BE681 /* TempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
-				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
 				BDB899882C564509006F3298 /* ForecastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
@@ -4081,12 +4246,15 @@
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
+				DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
 				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
+				BD8E6B212D9036CA00ABF8FA /* OnboardingProvider.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
+				DDF691372DA30332008BF16C /* StartupGuideStepView.swift in Sources */,
 				58645B9B2CA2D24F008AFCE7 /* CarbSetup.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
@@ -4132,10 +4300,12 @@
 				3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
+				DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */,
 				582DF9772C8CDBE7001F516D /* InsulinView.swift in Sources */,
+				BD47FD132D88AA700043966B /* OnboardingManager.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */,
 				5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */,
@@ -4163,8 +4333,10 @@
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
+				BD47FD172D88AAF50043966B /* CompletedStepView.swift in Sources */,
 				DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */,
 				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
+				BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
@@ -4208,6 +4380,7 @@
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* TreatmentsStateModel.swift in Sources */,
+				DDF6905C2DA0AFC5008BF16C /* WelcomeStepView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				BDC531162D10629000088832 /* ContactPicture.swift in Sources */,
@@ -4230,9 +4403,12 @@
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
+				DDF691052DA2CA23008BF16C /* AppDiagnosticsStateModel.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
+				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
+				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
@@ -4241,6 +4417,7 @@
 				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
 				6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
+				DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
@@ -4259,6 +4436,7 @@
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
+				DDF691032DA2CA1E008BF16C /* AppDiagnosticsProvider.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
@@ -4271,6 +4449,7 @@
 				BDC530FF2D0F6BE300088832 /* ContactImageManager.swift in Sources */,
 				BDC531122D1060FA00088832 /* ContactImageDetailView.swift in Sources */,
 				DDE179552C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift in Sources */,
+				BD10516D2DA986E1007C6D89 /* LogoAnimation.swift in Sources */,
 				DDE179562C910127003CDDB7 /* BolusStored+CoreDataClass.swift in Sources */,
 				DDE179572C910127003CDDB7 /* BolusStored+CoreDataProperties.swift in Sources */,
 				BD4D738D2D15A4080052227B /* TDDStored+CoreDataClass.swift in Sources */,
@@ -4283,6 +4462,7 @@
 				BDDAF9EF2D00554500B34E7A /* SelectionPopoverView.swift in Sources */,
 				DDE1795F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DDE179602C910127003CDDB7 /* StatsData+CoreDataClass.swift in Sources */,
+				DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */,
 				DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */,
 				DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */,
 				DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */,
@@ -4561,6 +4741,7 @@
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPER_TEAM = "$(DEVELOPER_TEAM)";
 				DEVELOPMENT_ASSET_PATHS = "";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
@@ -5037,6 +5218,14 @@
 				minimumVersion = 9.0.0;
 			};
 		};
+		3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 11.11.0;
+			};
+		};
 		3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/no-comment/SlideButton";
@@ -5092,6 +5281,11 @@
 			package = 38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */;
 			productName = SwiftMessages;
 		};
+		3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+			productName = FirebaseCrashlytics;
+		};
 		3BD9687B2D8DDD4600899469 /* SlideButton */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */;

+ 4 - 0
Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -244,6 +244,10 @@
             isEnabled = "YES">
          </CommandLineArgument>
          <CommandLineArgument
+            argument = "-FIRDebugEnabled "
+            isEnabled = "NO">
+         </CommandLineArgument>
+         <CommandLineArgument
             argument = "-com.apple.CoreData.SQLDebug 1"
             isEnabled = "NO">
          </CommandLineArgument>

+ 118 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,7 +1,25 @@
 {
-  "originHash" : "52d77fc35af7fe71614051dee0b291e2a0d38522eac7ae4d37d2442e81c7530c",
+  "originHash" : "b10fee57248e5d754951672d55dd1e425fadd3089d06858aed6f0f5206be7e5c",
   "pins" : [
     {
+      "identity" : "abseil-cpp-binary",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/abseil-cpp-binary.git",
+      "state" : {
+        "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+        "version" : "1.2024072200.0"
+      }
+    },
+    {
+      "identity" : "app-check",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/app-check.git",
+      "state" : {
+        "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
+        "version" : "11.2.0"
+      }
+    },
+    {
       "identity" : "cryptoswift",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/krzyzanowskim/CryptoSwift",
@@ -11,6 +29,78 @@
       }
     },
     {
+      "identity" : "firebase-ios-sdk",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/firebase-ios-sdk.git",
+      "state" : {
+        "revision" : "d1f7c7e8eaa74d7e44467184dc5f592268247d33",
+        "version" : "11.11.0"
+      }
+    },
+    {
+      "identity" : "googleappmeasurement",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleAppMeasurement.git",
+      "state" : {
+        "revision" : "dd89fc79a77183830742a16866d87e4e54785734",
+        "version" : "11.11.0"
+      }
+    },
+    {
+      "identity" : "googledatatransport",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleDataTransport.git",
+      "state" : {
+        "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+        "version" : "10.1.0"
+      }
+    },
+    {
+      "identity" : "googleutilities",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleUtilities.git",
+      "state" : {
+        "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
+        "version" : "8.0.2"
+      }
+    },
+    {
+      "identity" : "grpc-binary",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/grpc-binary.git",
+      "state" : {
+        "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
+        "version" : "1.69.0"
+      }
+    },
+    {
+      "identity" : "gtm-session-fetcher",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/gtm-session-fetcher.git",
+      "state" : {
+        "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
+        "version" : "4.4.0"
+      }
+    },
+    {
+      "identity" : "interop-ios-for-google-sdks",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+      "state" : {
+        "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+        "version" : "101.0.0"
+      }
+    },
+    {
+      "identity" : "leveldb",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/leveldb.git",
+      "state" : {
+        "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+        "version" : "1.22.5"
+      }
+    },
+    {
       "identity" : "mkringprogressview",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
@@ -20,6 +110,24 @@
       }
     },
     {
+      "identity" : "nanopb",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/nanopb.git",
+      "state" : {
+        "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
+        "version" : "2.30910.0"
+      }
+    },
+    {
+      "identity" : "promises",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/promises.git",
+      "state" : {
+        "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
+        "version" : "2.4.0"
+      }
+    },
+    {
       "identity" : "slidebutton",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/no-comment/SlideButton",
@@ -47,6 +155,15 @@
       }
     },
     {
+      "identity" : "swift-protobuf",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-protobuf.git",
+      "state" : {
+        "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
+        "version" : "1.29.0"
+      }
+    },
+    {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/ivanschuetz/SwiftCharts.git",

+ 30 - 0
Trio/GoogleService-Info.plist

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>API_KEY</key>
+	<string>AIzaSyBXceaGy1LrHLHfCNxhGqjxyjDR0fOLkOM</string>
+	<key>GCM_SENDER_ID</key>
+	<string>376584015262</string>
+	<key>PLIST_VERSION</key>
+	<string>1</string>
+	<key>BUNDLE_ID</key>
+	<string>org.nightscout.trio</string>
+	<key>PROJECT_ID</key>
+	<string>trio-e776c</string>
+	<key>STORAGE_BUCKET</key>
+	<string>trio-e776c.firebasestorage.app</string>
+	<key>IS_ADS_ENABLED</key>
+	<false></false>
+	<key>IS_ANALYTICS_ENABLED</key>
+	<false></false>
+	<key>IS_APPINVITE_ENABLED</key>
+	<false></false>
+	<key>IS_GCM_ENABLED</key>
+	<false></false>
+	<key>IS_SIGNIN_ENABLED</key>
+	<false></false>
+	<key>GOOGLE_APP_ID</key>
+	<string>1:376584015262:ios:7a4dd770ce4c46b486da8f</string>
+</dict>
+</plist>

+ 14 - 2
Trio/Sources/Application/AppDelegate.swift

@@ -1,3 +1,5 @@
+import FirebaseCore
+import FirebaseCrashlytics
 import SwiftUI
 import UIKit
 import UserNotifications
@@ -7,8 +9,18 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         _: UIApplication,
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
     ) -> Bool {
-        // application.registerForRemoteNotifications()
-        true
+        FirebaseApp.configure()
+
+        let userDefaults = UserDefaults.standard
+        // Default to `true` if the key doesn't exist
+        let crashReportingEnabled: Bool = userDefaults.getValue(Bool.self, forKey: "DiagnosticsSharing") ?? true
+
+        // The docs say that changes to this don't take effect until
+        // the next app boot, but this is fine since the app will need
+        // to boot after a crash
+        Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashReportingEnabled)
+
+        return true
     }
 
     func application(

+ 32 - 2
Trio/Sources/Application/TrioApp.swift

@@ -1,4 +1,3 @@
-import ActivityKit
 import BackgroundTasks
 import CoreData
 import Foundation
@@ -8,6 +7,7 @@ import Swinject
 extension Notification.Name {
     static let initializationCompleted = Notification.Name("initializationCompleted")
     static let initializationError = Notification.Name("initializationError")
+    static let onboardingCompleted = Notification.Name("onboardingCompleted")
 }
 
 @main struct TrioApp: App {
@@ -19,6 +19,8 @@ extension Notification.Name {
     @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
 
     let coreDataStack = CoreDataStack.shared
+    let onboardingManager = OnboardingManager.shared
+
     class InitState {
         var complete = false
         var error = false
@@ -33,6 +35,7 @@ extension Notification.Name {
     @State private var appState = AppState()
     @State private var showLoadingView = true
     @State private var showLoadingError = false
+    @State private var showOnboardingView = false
 
     // Dependencies Assembler
     // contain all dependencies Assemblies
@@ -80,6 +83,29 @@ extension Notification.Name {
     }
 
     init() {
+        let notificationCenter = Foundation.NotificationCenter.default
+        notificationCenter.addObserver(
+            forName: .initializationCompleted,
+            object: nil,
+            queue: .main
+        ) { [self] _ in
+            showLoadingView = false
+        }
+        notificationCenter.addObserver(
+            forName: .initializationError,
+            object: nil,
+            queue: .main
+        ) { [self] _ in
+            showLoadingError = true
+        }
+        notificationCenter.addObserver(
+            forName: .onboardingCompleted,
+            object: nil,
+            queue: .main
+        ) { [self] _ in
+            showOnboardingView = false
+        }
+
         let submodulesInfo = BuildDetails.shared.submodules.map { key, value in
             "\(key): \(value.branch) \(value.commitSHA)"
         }.joined(separator: ", ")
@@ -167,7 +193,11 @@ extension Notification.Name {
                     .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
                         self.showLoadingError = true
                     }
-
+            } else if onboardingManager.shouldShowOnboarding {
+                // Show onboarding if needed
+                Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
+                    .preferredColorScheme(colorScheme(for: .dark) ?? nil)
+                    .transition(.opacity)
             } else {
                 Main.RootView(resolver: resolver)
                     .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)

+ 278 - 2
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -2696,6 +2696,9 @@
         }
       }
     },
+    " the app before finishing onboarding, " : {
+
+    },
     " U" : {
       "comment" : "Insulin unit\nUnit in number of units delivered (keep the space character!)",
       "localizations" : {
@@ -8827,6 +8830,46 @@
         }
       }
     },
+    "•" : {
+
+    },
+    "•  App diagnostic insights help us enhance app stability, ensure safety for all users, and enable us to quickly identify and resolve critical issues." : {
+
+    },
+    "•  Trio collects the app's state on crash, device, iOS and general system info, and a stack trace." : {
+
+    },
+    "•  Trio does not collect any health related data, e.g. glucose readings, insulin rates or doses, meal data, setting values, or similar." : {
+
+    },
+    "•  Trio does not track any usage metrics or any other personal data about users other than the used iPhone model and iOS version." : {
+
+    },
+    "• A higher number means you need less insulin for the same amount of carbs" : {
+
+    },
+    "• A higher number means you're less sensitive to insulin" : {
+
+    },
+    "• A lower number means you need more insulin for the same amount of carbs" : {
+
+    },
+    "• A lower number means you're more sensitive to insulin" : {
+
+    },
+    "• A ratio of 10 g/U means 1 unit of insulin covers 10g of carbs" : {
+
+    },
+    "• An ISF of %@ means 1 U lowers your glucose by %@" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "• An ISF of %1$@ means 1 U lowers your glucose by %2$@"
+          }
+        }
+      }
+    },
     "• Basal Rate" : {
       "localizations" : {
         "bg" : {
@@ -9028,6 +9071,7 @@
       }
     },
     "• Basal Rates\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -9328,6 +9372,7 @@
       }
     },
     "• Carb Ratios\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -10527,7 +10572,11 @@
         }
       }
     },
+    "• Different times of day may require different ratios" : {
+
+    },
     "• Duration of Insulin Action" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -11928,6 +11977,7 @@
       }
     },
     "• Glucose Targets\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -12428,6 +12478,7 @@
       }
     },
     "• Insulin Sensitivities\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -31015,6 +31066,9 @@
         }
       }
     },
+    "All Set!" : {
+
+    },
     "All settings are at default values." : {
       "localizations" : {
         "bg" : {
@@ -36619,6 +36673,9 @@
         }
       }
     },
+    "Anonymized Data Sharing" : {
+
+    },
     "API secret" : {
       "comment" : "API secret in NS",
       "localizations" : {
@@ -36720,6 +36777,9 @@
         }
       }
     },
+    "App Diagnostics" : {
+
+    },
     "App Expires" : {
       "localizations" : {
         "bg" : {
@@ -38429,6 +38489,7 @@
       }
     },
     "Are you sure you want to import profile settings from Nightscout?\n\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -41386,6 +41447,9 @@
         }
       }
     },
+    "Back" : {
+
+    },
     "Backfill Failed" : {
       "localizations" : {
         "bg" : {
@@ -42536,6 +42600,7 @@
       }
     },
     "Basal Profile" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -43650,6 +43715,12 @@
         }
       }
     },
+    "Before you begin…" : {
+
+    },
+    "Before you can begin with configuring your therapy settigns, Trio needs to know which units you use for your glucose and insulin measurements (based on your pump model)." : {
+
+    },
     "Beta (TestFlight) Expires" : {
       "localizations" : {
         "bg" : {
@@ -46827,6 +46898,9 @@
         }
       }
     },
+    "By default, Trio collects crash reports and other anonymized data related to errors, exceptions, and overall app performance." : {
+
+    },
     "Calculate a new Insulin Sensitivity Setting (ISF) upon every loop cycle. The new ISF will be based on your current Glucose, total daily dose of insulin (TDD, past 24 hours of all delivered insulin) and an individual Adjustment Factor (recommendation to start with is 0.5 if using Sigmoid Function and 0.8 if not).\n\nAll of the Dynamic ISF and CR adjustments will be limited by your autosens.min/max limits." : {
       "comment" : "Enable Dynamic ISF",
       "extractionState" : "manual",
@@ -57952,6 +58026,9 @@
         }
       }
     },
+    "Configure Yourself" : {
+
+    },
     "Confirm" : {
       "localizations" : {
         "bg" : {
@@ -58860,6 +58937,9 @@
         }
       }
     },
+    "Connected" : {
+
+    },
     "Connected Services" : {
       "localizations" : {
         "bg" : {
@@ -65330,7 +65410,11 @@
         }
       }
     },
+    "Default: 2 %@" : {
+
+    },
     "Default: 2 units" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -71429,6 +71513,9 @@
         }
       }
     },
+    "Delivery Limits" : {
+
+    },
     "Delta" : {
       "localizations" : {
         "bg" : {
@@ -73364,6 +73451,12 @@
         }
       }
     },
+    "Diagnostics" : {
+
+    },
+    "Diagnostics are sent to a Google Firebase Crashlytics project, which is securely maintained and accessed only by the Trio team." : {
+
+    },
     "Direct connection with Libre 1 transmitters or European Libre 2 sensors" : {
       "comment" : "Direct connection with Libre 1 transmitters or Libre 2",
       "extractionState" : "manual",
@@ -78298,6 +78391,7 @@
       }
     },
     "Duration of Insulin Action (DIA)" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -87654,6 +87748,9 @@
         }
       }
     },
+    "Entries" : {
+
+    },
     "Error" : {
       "comment" : "Error title",
       "localizations" : {
@@ -88806,6 +88903,12 @@
         }
       }
     },
+    "Everything you enter here can be adjusted later in the app." : {
+
+    },
+    "Example Calculation" : {
+
+    },
     "exceeded" : {
       "comment" : "Limit Exceeded label",
       "extractionState" : "manual",
@@ -94174,6 +94277,7 @@
       }
     },
     "Finish" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -95081,6 +95185,9 @@
         }
       }
     },
+    "For 45g of carbs, you would need:" : {
+
+    },
     "For example, at a temp target of %@ %@, your basal is reduced to 50%%, but this scales depending on the target (e.g., 75%% at %@ %@, 60%% at %@ %@)." : {
       "localizations" : {
         "bg" : {
@@ -95595,6 +95702,9 @@
         }
       }
     },
+    "force quit" : {
+
+    },
     "Forecast Display Type" : {
       "localizations" : {
         "bg" : {
@@ -98236,6 +98346,9 @@
         }
       }
     },
+    "Get Started" : {
+
+    },
     "Getting everything ready for you..." : {
       "localizations" : {
         "bg" : {
@@ -102414,6 +102527,9 @@
         }
       }
     },
+    "Great job! You've completed the initial setup of Trio. You can always adjust these settings later in the app." : {
+
+    },
     "Green = At Target" : {
       "localizations" : {
         "bg" : {
@@ -103751,6 +103867,12 @@
         }
       }
     },
+    "Here is an overview of what to expect:" : {
+
+    },
+    "Hi there!" : {
+
+    },
     "Hidden" : {
       "localizations" : {
         "bg" : {
@@ -107137,6 +107259,9 @@
         }
       }
     },
+    "https://triodocs.org/startup-guide" : {
+
+    },
     "If \"Display IOB and COB\" is also enabled, \"IOB\" and \"COB\" will be replaced with the following emojis:" : {
       "localizations" : {
         "bg" : {
@@ -108449,6 +108574,16 @@
         }
       }
     },
+    "If you are %@ %@ above target:" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "If you are %1$@ %2$@ above target:"
+          }
+        }
+      }
+    },
     "If you are not seeing calendars to choose here, please go to Settings -> Trio -> Calendars and change permissions to \"Full Access\"" : {
       "localizations" : {
         "bg" : {
@@ -108850,6 +108985,9 @@
         }
       }
     },
+    "If you prefer not to share this anonymized data, you can opt-out of data sharing." : {
+
+    },
     "If your CGM readings are below the Low value or above the High value, you will receive a glucose alarm." : {
       "localizations" : {
         "bg" : {
@@ -110280,6 +110418,7 @@
       }
     },
     "Import Settings from Nightscout" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110487,6 +110626,7 @@
       }
     },
     "Import therapy settings from Nightscout.\nSee hint for the list of settings available for import." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110587,6 +110727,7 @@
       }
     },
     "Import Therapy Settings?" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110794,6 +110935,7 @@
       }
     },
     "Imported Nightscout Data" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110895,6 +111037,7 @@
     },
     "Importing Profile..." : {
       "comment" : "Progress text when importing profile via Nightscout",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110994,6 +111137,9 @@
         }
       }
     },
+    "Importing Settings..." : {
+      "comment" : "Progress text when importing settings via Nightscout"
+    },
     "In case you're using both profiles and temp targets" : {
       "comment" : "UI/UX option",
       "extractionState" : "manual",
@@ -117936,6 +118082,9 @@
         }
       }
     },
+    "Let's go through a few quick steps to ensure Trio works optimally for you." : {
+
+    },
     "Libre 2 Direct" : {
       "comment" : "Libre 2 Direct",
       "extractionState" : "manual",
@@ -126707,6 +126856,7 @@
     },
     "Max Basal" : {
       "comment" : "Max setting",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -126806,6 +126956,9 @@
         }
       }
     },
+    "Max Basal Rate" : {
+
+    },
     "Max bolus" : {
       "localizations" : {
         "bg" : {
@@ -132613,6 +132766,9 @@
         }
       }
     },
+    "mg/dL/U" : {
+
+    },
     "Middleware" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -134773,6 +134929,9 @@
         }
       }
     },
+    "mmol/L/U" : {
+
+    },
     "mmol/mol" : {
       "localizations" : {
         "bg" : {
@@ -136219,6 +136378,9 @@
         }
       }
     },
+    "Next" : {
+
+    },
     "Nightscout" : {
       "localizations" : {
         "bg" : {
@@ -136628,6 +136790,9 @@
         }
       }
     },
+    "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." : {
+
+    },
     "Nightscout ping: %d ms" : {
       "comment" : "Nightscout ping",
       "localizations" : {
@@ -136830,6 +136995,9 @@
         }
       }
     },
+    "Nightscout use is entirely optional. You can also setup Nightscout at a later time." : {
+
+    },
     "No" : {
       "comment" : "Button",
       "localizations" : {
@@ -141742,6 +141910,9 @@
         }
       }
     },
+    "Note: Choosing your pump model determines which increments for setting up your basal rates are available. You will pair your actual pump after finishing the onboarding process." : {
+
+    },
     "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app." : {
       "localizations" : {
         "bg" : {
@@ -142942,6 +143113,9 @@
         }
       }
     },
+    "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio." : {
+
+    },
     "Note: UAM SMBs must be enabled to use this limit." : {
       "localizations" : {
         "bg" : {
@@ -144382,6 +144556,7 @@
       }
     },
     "Number of hours insulin will contribute to IOB after dosing." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -148777,6 +148952,9 @@
         }
       }
     },
+    "Overview" : {
+
+    },
     "𝑷 = protein(g) × 40%" : {
       "localizations" : {
         "bg" : {
@@ -151561,6 +151739,15 @@
         }
       }
     },
+    "Please choose from the options below." : {
+
+    },
+    "Please choose if you want to import existing therapy settings from Nightscout or start from scratch." : {
+
+    },
+    "Please enter your credentials:" : {
+
+    },
     "Please make sure that your Libre 2 sensor is already activated and finished warming up. If you have other apps connecting to the sensor via bluetooth, these need to be shut down or uninstalled. \n\n You can only have one app communicating with the sensor via bluetooth. Then press the \"pariring and connection\" button below to start the process. Please note that the bluetooth connection might take up to a couple of minutes before it starts working." : {
       "extractionState" : "manual",
       "localizations" : {
@@ -151669,6 +151856,7 @@
       }
     },
     "Please review the following settings:" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -159515,6 +159703,9 @@
         }
       }
     },
+    "Remember, you can adjust these settings at any time in the app settings if needed." : {
+
+    },
     "Remote Control" : {
       "comment" : "Allow remote control from NS",
       "localizations" : {
@@ -161554,6 +161745,7 @@
       }
     },
     "Review Import" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -161654,6 +161846,7 @@
       }
     },
     "Review imported DIA" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -170795,6 +170988,9 @@
         }
       }
     },
+    "Setup Nightscout for Trio" : {
+
+    },
     "Shape" : {
       "extractionState" : "stale",
       "localizations" : {
@@ -174231,6 +174427,9 @@
         }
       }
     },
+    "Skip Nightscout Setup" : {
+
+    },
     "Slope" : {
       "comment" : "v",
       "localizations" : {
@@ -178507,6 +178706,9 @@
         }
       }
     },
+    "Startup Guide" : {
+
+    },
     "State was restored" : {
       "comment" : "State was restored",
       "extractionState" : "manual",
@@ -183064,6 +183266,9 @@
         }
       }
     },
+    "Take a deep breath — you've got this." : {
+
+    },
     "Tap and hold a bar to reveal more details." : {
       "localizations" : {
         "bg" : {
@@ -190781,6 +190986,9 @@
         }
       }
     },
+    "There's no rush. Take all the time you need." : {
+
+    },
     "These two settings determine the range outside of which you will be notified via push notifications." : {
       "localizations" : {
         "bg" : {
@@ -192929,6 +193137,7 @@
       }
     },
     "This has replaced your previous therapy settings." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -194377,7 +194586,6 @@
       }
     },
     "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus." : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -197011,6 +197219,7 @@
       }
     },
     "This will overwrite the following Trio therapy settings:" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -197111,6 +197320,7 @@
       }
     },
     "This will overwrite the following Trio therapy settings:\n" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -199029,6 +199239,7 @@
       }
     },
     "Tip: It is better to use a Custom Peak Time than to adjust Duration of Insulin Action (DIA)." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -203296,6 +203507,9 @@
         }
       }
     },
+    "Trio comes with a helpful Startup Guide. We recommend opening it now and following along as you go — side by side." : {
+
+    },
     "Trio Configuration" : {
       "localizations" : {
         "bg" : {
@@ -204197,6 +204411,7 @@
       }
     },
     "Trio has successfully imported your default Nightscout profile and stored it as therapy settings. " : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -204396,6 +204611,9 @@
         }
       }
     },
+    "Trio includes several safety limits for insulin delivery and carbohydrate entry, helping ensure a safe and effective experience." : {
+
+    },
     "Trio Information Notifications" : {
       "localizations" : {
         "bg" : {
@@ -204496,6 +204714,9 @@
         }
       }
     },
+    "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." : {
+
+    },
     "Trio is currently using these metrics and values as determined by the oref algorithm:" : {
       "localizations" : {
         "bg" : {
@@ -204596,6 +204817,9 @@
         }
       }
     },
+    "Trio is designed to help manage your diabetes efficiently. To get the most out of the app, we'll guide you through setting up some essential parameters." : {
+
+    },
     "Trio lets you create automations using iOS Shortcuts. Go to the Shortcuts app to create new automations." : {
       "localizations" : {
         "bg" : {
@@ -206003,6 +206227,9 @@
         }
       }
     },
+    "Trio will import the following therapy settings from your Nightscout instance:" : {
+
+    },
     "Trio will use the larger of the default setting calculation below and the value entered here." : {
       "localizations" : {
         "bg" : {
@@ -206103,6 +206330,9 @@
         }
       }
     },
+    "Trio's Onboarding consists of several steps. It takes about 5-10 minutes to complete. We'll guide you through each step." : {
+
+    },
     "Trio's Simple Lock Screen Widget displays current glucose reading, trend arrow, delta and the timestamp of the current reading." : {
       "localizations" : {
         "bg" : {
@@ -206806,8 +207036,11 @@
         }
       }
     },
+    "U/day" : {
+
+    },
     "U/hr" : {
-      "comment" : "Insulin unit per hour",
+      "comment" : "Insulin unit per hour abbreviation",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -208044,6 +208277,9 @@
         }
       }
     },
+    "Units & Pump" : {
+
+    },
     "Units and Limits" : {
       "localizations" : {
         "bg" : {
@@ -213368,6 +213604,9 @@
         }
       }
     },
+    "Want a hand? You can open our full Startup Guide here:" : {
+
+    },
     "Warning" : {
       "comment" : "Warning title",
       "localizations" : {
@@ -215300,6 +215539,12 @@
         }
       }
     },
+    "Welcome to Trio" : {
+
+    },
+    "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations." : {
+
+    },
     "What is the numeric value of the carb to add" : {
       "localizations" : {
         "bg" : {
@@ -215500,6 +215745,9 @@
         }
       }
     },
+    "What This Means" : {
+
+    },
     "When \"Fatty Meal\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Fatty Meal Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"." : {
       "localizations" : {
         "bg" : {
@@ -217537,6 +217785,9 @@
         }
       }
     },
+    "Why does Trio collect this data?" : {
+
+    },
     "Wide BG Target Range" : {
       "comment" : "Headline \"Wide BG Target Range\"",
       "extractionState" : "manual",
@@ -219070,6 +219321,7 @@
       }
     },
     "Yes, Import!" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -219369,6 +219621,9 @@
         }
       }
     },
+    "You can pause at any time. Just be aware: if you " : {
+
+    },
     "You can personalize the conversion calculation by adjusting the following settings that will appear when this option is enabled:" : {
       "localizations" : {
         "bg" : {
@@ -219469,6 +219724,18 @@
         }
       }
     },
+    "You're All Set!" : {
+
+    },
+    "You've successfully completed the initial setup of Trio. Tap 'Get Started' to save your settings and get ready to start using Trio." : {
+
+    },
+    "Your basal profile represents the amount of background insulin you need throughout the day. This helps Trio calculate your insulin needs." : {
+
+    },
+    "Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover. This is essential for accurate meal bolus calculations." : {
+
+    },
     "Your entered amount was limited by your max Bolus setting of %d%@" : {
       "comment" : "For the  Bolus View pop-up",
       "extractionState" : "manual",
@@ -219577,6 +219844,12 @@
         }
       }
     },
+    "Your glucose target is the blood glucose level you aim to maintain. Trio will use this to calculate insulin doses and provide recommendations." : {
+
+    },
+    "Your insulin sensitivity factor (ISF) indicates how much one unit of insulin will lower your blood glucose. This helps calculate correction boluses." : {
+
+    },
     "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -219892,6 +220165,9 @@
         }
       }
     },
+    "your progress will not be saved." : {
+
+    },
     "ZT" : {
       "localizations" : {
         "bg" : {

+ 3 - 1
Trio/Sources/Models/BloodGlucose.swift

@@ -157,11 +157,13 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
     }
 }
 
-enum GlucoseUnits: String, JSON, Equatable {
+enum GlucoseUnits: String, JSON, Equatable, CaseIterable, Identifiable {
     case mgdL = "mg/dL"
     case mmolL = "mmol/L"
 
     static let exchangeRate: Decimal = 0.0555
+
+    var id: String { rawValue }
 }
 
 extension Int {

+ 8 - 1
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -137,7 +137,13 @@ struct DecimalPickerSettings {
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
-    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxBasal = PickerSetting(
+        value: 10,
+        step: 0.5,
+        min: 0.5,
+        max: 30,
+        type: PickerSetting.PickerSettingType.insulinUnitPerHour
+    )
 }
 
 struct PickerSetting {
@@ -152,6 +158,7 @@ struct PickerSetting {
         case factor
         case gram
         case insulinUnit
+        case insulinUnitPerHour
         case minute
         case hour
     }

+ 5 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsDataFlow.swift

@@ -0,0 +1,5 @@
+enum AppDiagnostics {
+    enum Config {}
+}
+
+protocol AppDiagnosticsProvider {}

+ 3 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsProvider.swift

@@ -0,0 +1,3 @@
+extension AppDiagnostics {
+    final class Provider: BaseProvider, AppDiagnosticsProvider {}
+}

+ 35 - 0
Trio/Sources/Modules/AppDiagnostics/AppDiagnosticsStateModel.swift

@@ -0,0 +1,35 @@
+import FirebaseCrashlytics
+import Observation
+import SwiftUI
+
+extension AppDiagnostics {
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        // MARK: - Diagnostics Sharing Option
+
+        var diagnosticsSharingOption: DiagnosticsSharingOption = .enabled
+
+        override func subscribe() {
+            loadDiagnostics()
+        }
+
+        /// Loads the diagnostics sharing option from UserDefaults as a boolean.
+        func loadDiagnostics() {
+            if let storedDiagnosticsSharingOption = UserDefaults.standard.value(forKey: "DiagnosticsSharing") as? Bool {
+                diagnosticsSharingOption = storedDiagnosticsSharingOption ? .enabled : .disabled
+            } else {
+                diagnosticsSharingOption = .enabled
+            }
+        }
+
+        /// Persists the current diagnostics sharing option to UserDefaults as a boolean.
+        func applyDiagnostics() {
+            let booleanValue: Bool = diagnosticsSharingOption == .enabled
+            UserDefaults.standard.set(booleanValue, forKey: "DiagnosticsSharing")
+            Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(booleanValue)
+        }
+    }
+}
+
+extension AppDiagnostics.StateModel: SettingsObserver {
+    func settingsDidChange(_: TrioSettings) {}
+}

+ 82 - 0
Trio/Sources/Modules/AppDiagnostics/View/AppDiagnosticsRootView.swift

@@ -0,0 +1,82 @@
+import SwiftUI
+import Swinject
+
+extension AppDiagnostics {
+    struct RootView: BaseView {
+        let resolver: Resolver
+
+        @State var state = StateModel()
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                Section(
+                    header: Text("Anonymized Data Sharing"),
+                    content: {
+                        VStack {
+                            ForEach(DiagnosticsSharingOption.allCases, id: \.self) { option in
+                                Button(action: {
+                                    state.diagnosticsSharingOption = option
+                                }) {
+                                    HStack {
+                                        Image(
+                                            systemName: state
+                                                .diagnosticsSharingOption == option ? "largecircle.fill.circle" : "circle"
+                                        )
+                                        .foregroundColor(state.diagnosticsSharingOption == option ? .accentColor : .secondary)
+                                        .imageScale(.large)
+
+                                        Text(option.displayName)
+                                            .foregroundColor(.primary)
+
+                                        Spacer()
+                                    }
+                                    .background(Color.chart.opacity(0.65))
+                                    .cornerRadius(10)
+                                }
+                                .buttonStyle(.plain)
+                            }
+                            .padding()
+                        }
+                        .onChange(of: state.diagnosticsSharingOption) {
+                            state.applyDiagnostics()
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                Section {
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Why does Trio collect this data?").bold()
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text(
+                                "•  App diagnostic insights help us enhance app stability, ensure safety for all users, and enable us to quickly identify and resolve critical issues."
+                            )
+                            Text(
+                                "•  Trio collects the app's state on crash, device, iOS and general system info, and a stack trace."
+                            )
+                            Text(
+                                "•  Trio does not collect any health related data, e.g. glucose readings, insulin rates or doses, meal data, setting values, or similar."
+                            )
+                            Text(
+                                "•  Trio does not track any usage metrics or any other personal data about users other than the used iPhone model and iOS version."
+                            )
+                        }
+                        Text(
+                            "Diagnostics are sent to a Google Firebase Crashlytics project, which is securely maintained and accessed only by the Trio team."
+                        )
+                    }
+                    .font(.footnote)
+                    .multilineTextAlignment(.leading)
+                    .foregroundStyle(Color.secondary)
+                }.listRowBackground(Color.clear)
+            }
+            .listSectionSpacing(sectionSpacing)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitle("App Diagnostics")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 1 - 1
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -176,7 +176,7 @@ extension BasalProfileEditor {
                 state.calculateChartData()
             }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Basal Profile")
+            .navigationTitle("Basal Rates")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
                 ToolbarItem(placement: .topBarTrailing) {

+ 3 - 3
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -95,16 +95,16 @@ extension UnitsLimitsSettings {
                         get: { selectedVerboseHint },
                         set: {
                             selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Max Basal")
+                            hintLabel = String(localized: "Max Basal Rate")
                         }
                     ),
                     units: state.units,
                     type: .decimal("maxBasal"),
-                    label: String(localized: "Max Basal"),
+                    label: String(localized: "Max Basal Rate"),
                     miniHint: String(localized: "Largest basal rate allowed."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: 2 units").bold()
+                        Text("Default: 2 \(String(localized: "U/hr", comment: "Insulin unit per hour abbreviation"))").bold()
                         Text(
                             "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
                         )

+ 3 - 16
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -26,22 +26,9 @@ extension ISFEditor {
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
         var rateValues: [Decimal] {
-            var values = stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
-
-            if units == .mmolL {
-                var mmolValues = values.filter { Int(truncating: $0 as NSNumber) % 2 == 0 }
-                // check for any missing values
-                var valuesInMmolSet = Set(mmolValues.map(\.asMmolL))
-                for value in values {
-                    let valueInMmmol = value.asMmolL
-                    if valuesInMmolSet.insert(valueInMmmol).inserted {
-                        mmolValues.append(value)
-                    }
-                }
-                values = mmolValues.sorted()
-            }
-
-            return values
+            let settingsProvider = PickerSettingsProvider.shared
+            let sensitivityPickerSetting = PickerSetting(value: 100, step: 1, min: 9, max: 540, type: .glucose)
+            return settingsProvider.generatePickerValues(from: sensitivityPickerSetting, units: units)
         }
 
         var canAdd: Bool {

+ 0 - 264
Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -34,19 +34,6 @@ extension NightscoutConfig {
         @Published var maxBolus: Decimal = 10
         @Published var isConnectedToNS: Bool = false
 
-        @Published var isImportResultReviewPresented: Bool = false
-        @Published var importErrors: [String] = []
-        @Published var importStatus: ImportStatus = .finished
-        @Published var importedInsulinActionCurve: Decimal = 6
-
-        var pumpSettings: PumpSettings {
-            provider.getPumpSettings()
-        }
-
-        var isPumpSettingUnchanged: Bool {
-            pumpSettings.insulinActionCurve == importedInsulinActionCurve
-        }
-
         override func subscribe() {
             url = keychain.getValue(String.self, forKey: Config.urlKey) ?? ""
             secret = keychain.getValue(String.self, forKey: Config.secretKey) ?? ""
@@ -61,8 +48,6 @@ extension NightscoutConfig {
             subscribeSetting(\.localGlucosePort, on: $localPort.map(Int.init)) { localPort = Decimal($0) }
             subscribeSetting(\.uploadGlucose, on: $uploadGlucose, initial: { uploadGlucose = $0 })
 
-            importedInsulinActionCurve = pumpSettings.insulinActionCurve
-
             isConnectedToNS = nightscoutAPI != nil
 
             $isUploadEnabled
@@ -152,217 +137,6 @@ extension NightscoutConfig {
             return lowTargetValue
         }
 
-        func correctUnitParsingOffsets(_ parsedValue: Decimal) -> Decimal {
-            Int(parsedValue) % 2 == 0 ? parsedValue : parsedValue + 1
-        }
-
-        func importSettings() async {
-            importStatus = .running
-
-            do {
-                guard let fetchedProfile = await nightscoutManager.importSettings() else {
-                    await MainActor.run {
-                        importStatus = .failed
-                    }
-                    throw NSError(
-                        domain: "ImportError",
-                        code: 1,
-                        userInfo: [NSLocalizedDescriptionKey: "Cannot find the default Nightscout Profile."]
-                    )
-                }
-
-                // determine, i.e. guesstimate, whether fetched values are mmol/L or mg/dL values
-                let shouldConvertToMgdL = fetchedProfile.units.contains("mmol") || fetchedProfile.target_low
-                    .contains(where: { $0.value <= 39 }) || fetchedProfile.target_high.contains(where: { $0.value <= 39 })
-
-                // Carb Ratios
-                let carbratios = fetchedProfile.carbratio.map { carbratio in
-                    CarbRatioEntry(
-                        start: carbratio.time,
-                        offset: offset(carbratio.time) / 60,
-                        ratio: carbratio.value
-                    )
-                }
-
-                if carbratios.contains(where: { $0.ratio <= 0 }) {
-                    await MainActor.run {
-                        importStatus = .failed
-                    }
-                    throw NSError(
-                        domain: "ImportError",
-                        code: 2,
-                        userInfo: [NSLocalizedDescriptionKey: "Invalid Carb Ratio settings in Nightscout. Import aborted."]
-                    )
-                }
-
-                let carbratiosProfile = CarbRatios(units: .grams, schedule: carbratios)
-
-                // Basal Profile
-                let pumpName = apsManager.pumpName.value
-                let basals = fetchedProfile.basal.map { basal in
-                    BasalProfileEntry(
-                        start: basal.time,
-                        minutes: offset(basal.time) / 60,
-                        rate: basal.value
-                    )
-                }
-
-                if pumpName != "Omnipod DASH", basals.contains(where: { $0.rate <= 0 }) {
-                    await MainActor.run {
-                        importStatus = .failed
-                    }
-                    throw NSError(
-                        domain: "ImportError",
-                        code: 3,
-                        userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Import aborted."]
-                    )
-                }
-
-                if pumpName == "Omnipod DASH", basals.reduce(0, { $0 + $1.rate }) <= 0 {
-                    await MainActor.run {
-                        importStatus = .failed
-                    }
-                    throw NSError(
-                        domain: "ImportError",
-                        code: 4,
-                        userInfo: [
-                            NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Basal rate total cannot be 0 U/hr. Import aborted."
-                        ]
-                    )
-                }
-
-                // Sensitivities
-                let sensitivities = fetchedProfile.sens.map { sensitivity in
-                    InsulinSensitivityEntry(
-                        sensitivity: shouldConvertToMgdL ? correctUnitParsingOffsets(sensitivity.value.asMgdL) : sensitivity
-                            .value,
-                        offset: offset(sensitivity.time) / 60,
-                        start: sensitivity.time
-                    )
-                }
-
-                if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
-                    await MainActor.run {
-                        importStatus = .failed
-                    }
-                    throw NSError(
-                        domain: "ImportError",
-                        code: 5,
-                        userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout insulin sensitivity profile. Import aborted."]
-                    )
-                }
-
-                let sensitivitiesProfile = InsulinSensitivities(
-                    units: .mgdL,
-                    userPreferredUnits: .mgdL,
-                    sensitivities: sensitivities
-                )
-
-                // Targets
-                let targets = fetchedProfile.target_low.map { target in
-                    BGTargetEntry(
-                        low: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
-                        high: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
-                        start: target.time,
-                        offset: offset(target.time) / 60
-                    )
-                }
-
-                let targetsProfile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
-
-                // Save to storage and pump
-                if let pump = apsManager.pumpManager {
-                    let syncValues = basals.map {
-                        RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
-                    }
-
-                    await withCheckedContinuation { continuation in
-                        pump.syncBasalRateSchedule(items: syncValues) { [weak self] result in
-                            guard let self else {
-                                continuation.resume()
-                                return
-                            }
-
-                            switch result {
-                            case .success:
-                                self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
-                                self.finalizeImport(
-                                    carbratiosProfile: carbratiosProfile,
-                                    sensitivitiesProfile: sensitivitiesProfile,
-                                    targetsProfile: targetsProfile,
-                                    dia: fetchedProfile.dia
-                                )
-                            case .failure:
-                                Task { @MainActor in
-                                    self.importErrors.append(
-                                        "Settings were imported but the basal rates could not be saved to pump (communication error)."
-                                    )
-                                    self.importStatus = .failed
-                                }
-                            }
-                            continuation.resume()
-                        }
-                    }
-
-                    if await MainActor.run(body: { importErrors.isNotEmpty && importStatus == .failed }) {
-                        throw NSError(
-                            domain: "ImportError",
-                            code: 6,
-                            userInfo: [
-                                NSLocalizedDescriptionKey: "Settings were imported but the basal rates could not be saved to pump (communication error)."
-                            ]
-                        )
-                    }
-                } else {
-                    storage.save(basals, as: OpenAPS.Settings.basalProfile)
-                    finalizeImport(
-                        carbratiosProfile: carbratiosProfile,
-                        sensitivitiesProfile: sensitivitiesProfile,
-                        targetsProfile: targetsProfile,
-                        dia: fetchedProfile.dia
-                    )
-                }
-            } catch {
-                await MainActor.run {
-                    self.importErrors.append(error.localizedDescription)
-                    debug(.service, "Settings import failed with error: \(error.localizedDescription)")
-                }
-            }
-        }
-
-        private func finalizeImport(
-            carbratiosProfile: CarbRatios,
-            sensitivitiesProfile: InsulinSensitivities,
-            targetsProfile: BGTargets,
-            dia: Decimal
-        ) {
-            storage.save(carbratiosProfile, as: OpenAPS.Settings.carbRatios)
-            storage.save(sensitivitiesProfile, as: OpenAPS.Settings.insulinSensitivities)
-            storage.save(targetsProfile, as: OpenAPS.Settings.bgTargets)
-
-            // Save DIA if different
-            if dia != self.dia, dia >= 0 {
-                let file = PumpSettings(insulinActionCurve: dia, maxBolus: maxBolus, maxBasal: maxBasal)
-                storage.save(file, as: OpenAPS.Settings.settings)
-                debug(.nightscout, "DIA setting updated to \(dia) after a NS import.")
-            }
-
-            debug(.service, "Settings imported successfully.")
-
-            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
-                // stop blur
-                self.importStatus = .finished
-                // display next import rewview step
-                self.isImportResultReviewPresented = true
-            }
-        }
-
-        func offset(_ string: String) -> Int {
-            let hours = Int(string.prefix(2)) ?? 0
-            let minutes = Int(string.suffix(2)) ?? 0
-            return ((hours * 60) + minutes) * 60
-        }
-
         func backfillGlucose() async {
             await MainActor.run {
                 backfilling = true
@@ -400,35 +174,6 @@ extension NightscoutConfig {
             secret = ""
             isConnectedToNS = false
         }
-
-        func saveReviewedInsulinAction() {
-            if !isPumpSettingUnchanged {
-                let settings = PumpSettings(
-                    insulinActionCurve: importedInsulinActionCurve,
-                    maxBolus: pumpSettings.maxBolus,
-                    maxBasal: pumpSettings.maxBasal
-                )
-                provider.savePumpSettings(settings: settings)
-                    .receive(on: DispatchQueue.main)
-                    .sink { _ in
-                        let settings = self.provider.getPumpSettings()
-                        self.importedInsulinActionCurve = settings.insulinActionCurve
-
-                        Task.detached(priority: .low) {
-                            do {
-                                debug(.nightscout, "Attempting to upload DIA to Nightscout after import review")
-                                try await self.nightscoutManager.uploadProfiles()
-                            } catch {
-                                debug(
-                                    .default,
-                                    "\(DebuggingIdentifiers.failed) failed to upload DIA to Nightscout: \(error.localizedDescription)"
-                                )
-                            }
-                        }
-                    } receiveValue: {}
-                    .store(in: &lifetime)
-            }
-        }
     }
 }
 
@@ -437,12 +182,3 @@ extension NightscoutConfig.StateModel: SettingsObserver {
         units = settingsManager.settings.units
     }
 }
-
-extension NightscoutConfig.StateModel {
-    enum ImportStatus {
-        case running
-        case finished
-        case failed
-        case noPumpConnected
-    }
-}

+ 0 - 94
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -47,89 +47,6 @@ extension NightscoutConfig {
                         }
                     ).listRowBackground(Color.chart)
 
-                    Section {
-                        VStack {
-                            Button {
-                                importAlert = Alert(
-                                    title: Text("Import Therapy Settings?"),
-                                    message: Text("Are you sure you want to import profile settings from Nightscout?\n\n")
-                                        + Text("This will overwrite the following Trio therapy settings:\n")
-                                        + Text("• Basal Rates\n")
-                                        + Text("• Insulin Sensitivities\n")
-                                        + Text("• Carb Ratios\n")
-                                        + Text("• Glucose Targets\n")
-                                        + Text("• Duration of Insulin Action"),
-                                    primaryButton: .default(
-                                        Text("Yes, Import!"),
-                                        action: {
-                                            Task {
-                                                await state.importSettings()
-                                                if state.importStatus == .failed, state.importErrors.isNotEmpty,
-                                                   let errorMessage = state.importErrors.first
-                                                {
-                                                    DispatchQueue.main.async {
-                                                        importAlert = Alert(
-                                                            title: Text("Import Failed"),
-                                                            message: Text(errorMessage.description),
-                                                            dismissButton: .default(Text("OK"))
-                                                        )
-                                                        isImportAlertPresented = true
-                                                    }
-                                                }
-                                            }
-                                        }
-                                    ),
-                                    secondaryButton: .cancel()
-                                )
-                                isImportAlertPresented = true
-                            } label: {
-                                Text("Import Settings")
-                                    .font(.title3) }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                                .disabled(state.url.isEmpty || state.connecting)
-
-                            HStack(alignment: .center) {
-                                Text(
-                                    "Import therapy settings from Nightscout.\nSee hint for the list of settings available for import."
-                                )
-                                .font(.footnote)
-                                .foregroundColor(.secondary)
-                                .lineLimit(nil)
-                                Spacer()
-                                Button(
-                                    action: {
-                                        hintLabel = String(localized: "Import Settings from Nightscout")
-                                        selectedVerboseHint =
-                                            AnyView(
-                                                VStack(alignment: .leading, spacing: 10) {
-                                                    Text(
-                                                        "This will overwrite the following Trio therapy settings:"
-                                                    )
-                                                    VStack(alignment: .leading) {
-                                                        Text("• Basal Rates")
-                                                        Text("• Insulin Sensitivities")
-                                                        Text("• Carb Ratios")
-                                                        Text("• Glucose Targets")
-                                                        Text("• Duration of Insulin Action")
-                                                    }
-                                                }
-                                            )
-                                        shouldDisplayHint.toggle()
-                                    },
-                                    label: {
-                                        HStack {
-                                            Image(systemName: "questionmark.circle")
-                                        }
-                                    }
-                                ).buttonStyle(BorderlessButtonStyle())
-                                    .alert(isPresented: $isImportAlertPresented) {
-                                        importAlert ?? Alert(title: Text("Unknown Error"))
-                                    }
-                            }.padding(.top)
-                        }.padding(.vertical)
-                    }.listRowBackground(Color.chart)
-
                     Section(
                         content:
                         {
@@ -189,18 +106,7 @@ extension NightscoutConfig {
                     ).listRowBackground(Color.chart)
                 }
                 .listSectionSpacing(sectionSpacing)
-                .blur(radius: state.importStatus == .running ? 5 : 0)
-
-                if state.importStatus == .running {
-                    CustomProgressView(text: String(
-                        localized: "Importing Profile...",
-                        comment: "Progress text when importing profile via Nightscout"
-                    ))
-                }
             }
-            .fullScreenCover(isPresented: $state.isImportResultReviewPresented, content: {
-                NightscoutImportResultView(resolver: resolver, state: state)
-            })
             .sheet(isPresented: $shouldDisplayHint) {
                 SettingInputHintView(
                     hintDetent: $hintDetent,

+ 0 - 128
Trio/Sources/Modules/NightscoutConfig/View/ProfileImport/NightscoutImportResultView.swift

@@ -1,128 +0,0 @@
-import SwiftUI
-import Swinject
-
-struct NightscoutImportResultView: BaseView {
-    var resolver: any Swinject.Resolver
-
-    @ObservedObject var state: NightscoutConfig.StateModel
-
-    @State private var shouldDisplayHint: Bool = false
-    @State private var hintDetent = PresentationDetent.large
-    @State private var selectedVerboseHint: String?
-    @State private var hintLabel: String?
-    @State private var decimalPlaceholder: Decimal = 0.0
-    @State private var booleanPlaceholder: Bool = false
-
-    @State private var hasVisitedBasalProfileEditor = false
-    @State private var hasVisitedISFEditor = false
-    @State private var hasVisitedCREditor = false
-    @State private var hasVisitedPumpSettingsEditor = false
-
-    @Environment(\.colorScheme) var colorScheme
-    @Environment(AppState.self) var appState
-
-    private var allViewsVisited: Bool {
-        hasVisitedBasalProfileEditor &&
-            hasVisitedISFEditor &&
-            hasVisitedCREditor &&
-            hasVisitedPumpSettingsEditor
-    }
-
-    var body: some View {
-        NavigationStack {
-            Form {
-                Section(
-                    header: Text("Imported Nightscout Data"),
-                    content: {
-                        Text(
-                            "Trio has successfully imported your default Nightscout profile and stored it as therapy settings. "
-                        ) +
-                            Text("This has replaced your previous therapy settings.").bold().foregroundColor(.accentColor)
-                        Text("Please review the following settings:").bold()
-                    }
-                ).listRowBackground(Color.chart)
-
-                Section {
-                    NavigationLink(
-                        destination: BasalProfileEditor.RootView(resolver: resolver)
-                            .onDisappear { hasVisitedBasalProfileEditor = true }
-                    ) {
-                        HStack {
-                            Text("Basal Rates")
-                            if hasVisitedBasalProfileEditor {
-                                Image(systemName: "checkmark.circle.fill")
-                                    .imageScale(.large)
-                                    .fontWeight(.bold)
-                                    .foregroundStyle(Color.green)
-                            }
-                        }.foregroundColor(hasVisitedBasalProfileEditor ? .secondary : .primary)
-                    }
-
-                    NavigationLink(
-                        destination: ISFEditor.RootView(resolver: resolver)
-                            .onDisappear { hasVisitedISFEditor = true }
-                    ) {
-                        HStack {
-                            Text("Insulin Sensitivities")
-                            if hasVisitedISFEditor {
-                                Image(systemName: "checkmark.circle.fill")
-                                    .imageScale(.large)
-                                    .fontWeight(.bold)
-                                    .foregroundStyle(Color.green)
-                            }
-                        }.foregroundColor(hasVisitedISFEditor ? .secondary : .primary)
-                    }
-
-                    NavigationLink(
-                        destination: CarbRatioEditor.RootView(resolver: resolver)
-                            .onDisappear { hasVisitedCREditor = true }
-                    ) {
-                        HStack {
-                            Text("Carb Ratios")
-                            if hasVisitedCREditor {
-                                Image(systemName: "checkmark.circle.fill")
-                                    .imageScale(.large)
-                                    .fontWeight(.bold)
-                                    .foregroundStyle(Color.green)
-                            }
-                        }.foregroundColor(hasVisitedCREditor ? .secondary : .primary)
-                    }
-
-                    NavigationLink(
-                        destination: ReviewInsulinActionView(resolver: resolver, state: state)
-                            .onDisappear { hasVisitedPumpSettingsEditor = true }
-                    ) {
-                        HStack {
-                            Text("Duration of Insulin Action (DIA)")
-                            if hasVisitedPumpSettingsEditor {
-                                Image(systemName: "checkmark.circle.fill")
-                                    .imageScale(.large)
-                                    .fontWeight(.bold)
-                                    .foregroundStyle(Color.green)
-                            }
-                        }.foregroundColor(hasVisitedPumpSettingsEditor ? .secondary : .primary)
-                    }
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        Button {
-                            state.isImportResultReviewPresented = false
-                        } label: {
-                            Text("Finish").font(.title3)
-                        }
-                        .disabled(!allViewsVisited)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(allViewsVisited ? Color(.systemBlue) : Color(.systemGray4))
-            }
-            .navigationTitle("Review Import")
-            .navigationBarTitleDisplayMode(.large)
-            .scrollContentBackground(.hidden)
-            .background(appState.trioBackgroundColor(for: colorScheme))
-            .interactiveDismissDisabled(true)
-            .screenNavigation(self)
-        }
-    }
-}

+ 0 - 67
Trio/Sources/Modules/NightscoutConfig/View/ProfileImport/ReviewInsulinActionView.swift

@@ -1,67 +0,0 @@
-import Foundation
-
-import SwiftUI
-import Swinject
-
-struct ReviewInsulinActionView: BaseView {
-    var resolver: any Swinject.Resolver
-
-    @ObservedObject var state: NightscoutConfig.StateModel
-
-    @State private var shouldDisplayHint: Bool = false
-    @State private var hintDetent = PresentationDetent.large
-    @State private var selectedVerboseHint: AnyView?
-    @State private var hintLabel: String?
-    @State private var decimalPlaceholder: Decimal = 0.0
-    @State private var booleanPlaceholder: Bool = false
-
-    @Environment(\.colorScheme) var colorScheme
-    @Environment(AppState.self) var appState
-
-    var body: some View {
-        List {
-            SettingInputSection(
-                decimalValue: $state.importedInsulinActionCurve,
-                booleanValue: $booleanPlaceholder,
-                shouldDisplayHint: $shouldDisplayHint,
-                selectedVerboseHint: Binding(
-                    get: { selectedVerboseHint },
-                    set: {
-                        selectedVerboseHint = $0.map { AnyView($0) }
-                        hintLabel = String(localized: "Duration of Insulin Action")
-                    }
-                ),
-                units: state.units,
-                type: .decimal("dia"),
-                label: String(localized: "Duration of Insulin Action"),
-                miniHint: String(localized: "Number of hours insulin is active in your body."),
-                verboseHint:
-                VStack(alignment: .leading, spacing: 10) {
-                    Text("Default: 10 hours").bold()
-                    Text("Number of hours insulin will contribute to IOB after dosing.")
-                    Text(
-                        "Tip: It is better to use a Custom Peak Time than to adjust Duration of Insulin Action (DIA)."
-                    )
-                },
-                headerText: String(localized: "Review imported DIA")
-            )
-        }
-        .sheet(isPresented: $shouldDisplayHint) {
-            SettingInputHintView(
-                hintDetent: $hintDetent,
-                shouldDisplayHint: $shouldDisplayHint,
-                hintLabel: hintLabel ?? "",
-                hintText: selectedVerboseHint ?? AnyView(EmptyView()),
-                sheetTitle: String(localized: "Help", comment: "Help sheet title")
-            )
-        }
-        .scrollContentBackground(.hidden)
-        .background(appState.trioBackgroundColor(for: colorScheme))
-        .onAppear(perform: configureView)
-        .navigationTitle("Duration of Insulin Action")
-        .navigationBarTitleDisplayMode(.automatic)
-        .onDisappear {
-            state.saveReviewedInsulinAction()
-        }
-    }
-}

+ 5 - 0
Trio/Sources/Modules/Onboarding/OnboardingDataFlow.swift

@@ -0,0 +1,5 @@
+enum Onboarding {
+    enum Config {}
+}
+
+protocol OnboardingProvider: Provider {}

+ 67 - 0
Trio/Sources/Modules/Onboarding/OnboardingProvider.swift

@@ -0,0 +1,67 @@
+import Combine
+
+extension Onboarding {
+    final class Provider: BaseProvider, MainProvider {
+        var glucoseTargetsOnFile: BGTargets {
+            var retrievedTargets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+                ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+
+            // migrate existing mmol/L Trio users from mmol/L settings to pure mg/dl settings
+            if retrievedTargets.units == .mmolL || retrievedTargets.userPreferredUnits == .mmolL {
+                let convertedTargets = retrievedTargets.targets.map { target in
+                    BGTargetEntry(
+                        low: storage.parseSettingIfMmolL(value: target.low),
+                        high: storage.parseSettingIfMmolL(value: target.high),
+                        start: target.start,
+                        offset: target.offset
+                    )
+                }
+                retrievedTargets = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: convertedTargets)
+            }
+
+            return retrievedTargets
+        }
+
+        var basalProfileOnFile: [BasalProfileEntry] {
+            storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+                ?? []
+        }
+
+        var carbRatiosOnFile: CarbRatios {
+            storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) ?? CarbRatios(units: .grams, schedule: [])
+        }
+
+        var isfOnFile: InsulinSensitivities {
+            var retrievedSensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
+                ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+                ?? InsulinSensitivities(
+                    units: .mgdL,
+                    userPreferredUnits: .mgdL,
+                    sensitivities: []
+                )
+
+            // migrate existing mmol/L Trio users from mmol/L settings to pure mg/dl settings
+            if retrievedSensitivities.units == .mmolL || retrievedSensitivities.userPreferredUnits == .mmolL {
+                let convertedSensitivities = retrievedSensitivities.sensitivities.map { isf in
+                    InsulinSensitivityEntry(
+                        sensitivity: storage.parseSettingIfMmolL(value: isf.sensitivity),
+                        offset: isf.offset,
+                        start: isf.start
+                    )
+                }
+                retrievedSensitivities = InsulinSensitivities(
+                    units: .mgdL,
+                    userPreferredUnits: .mgdL,
+                    sensitivities: convertedSensitivities
+                )
+            }
+
+            return retrievedSensitivities
+        }
+
+        var pumpSettingsFromFile: PumpSettings? {
+            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+        }
+    }
+}

+ 263 - 0
Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift

@@ -0,0 +1,263 @@
+import Combine
+import Foundation
+import SwiftUI
+
+// MARK: - Setup Nightscout Connection
+
+extension Onboarding.StateModel {
+    func connectToNightscout() {
+        if let CheckURL = nightscoutUrl.last, CheckURL == "/" {
+            let fixedURL = nightscoutUrl.dropLast()
+            nightscoutUrl = String(fixedURL)
+        }
+
+        guard let nightscoutUrl = URL(string: nightscoutUrl), self.nightscoutUrl.hasPrefix("https://") else {
+            nightscoutResponseMessage = "Invalid URL"
+            isValidNightscoutURL = false
+            return
+        }
+
+        isConnectingToNS = true
+        isValidNightscoutURL = true
+        nightscoutResponseMessage = ""
+
+        NightscoutAPI(url: nightscoutUrl, secret: nightscoutSecret).checkConnection()
+            .receive(on: DispatchQueue.main)
+            .sink { completion in
+                switch completion {
+                case .finished: break
+                case let .failure(error):
+                    self.nightscoutResponseMessage = "Error: \(error.localizedDescription)"
+                }
+                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+                    self.isConnectingToNS = false
+                }
+            } receiveValue: {
+                self.keychain.setValue(self.nightscoutUrl, forKey: NightscoutConfig.Config.urlKey)
+                self.keychain.setValue(self.nightscoutSecret, forKey: NightscoutConfig.Config.secretKey)
+                self.isConnectedToNS = true
+            }
+            .store(in: &lifetime)
+    }
+
+    var nightscoutAPI: NightscoutAPI? {
+        guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
+              let url = URL(string: urlString),
+              let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
+        else {
+            return nil
+        }
+        return NightscoutAPI(url: url, secret: secret)
+    }
+
+    func importSettingsFromNightscout(currentStep: Binding<OnboardingStep>) async {
+        guard nightscoutAPI != nil, isConnectedToNS else {
+            return
+        }
+
+        nightscoutImportStatus = .running
+
+        do {
+            guard let fetchedProfile = await nightscoutManager.importSettings() else {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 1,
+                    userInfo: [NSLocalizedDescriptionKey: "Cannot find the default Nightscout Profile."]
+                )
+            }
+
+            // determine, i.e. guesstimate, whether fetched values are mmol/L or mg/dL values
+            let shouldConvertToMgdL = fetchedProfile.units.contains("mmol") || fetchedProfile.target_low
+                .contains(where: { $0.value <= 39 }) || fetchedProfile.target_high.contains(where: { $0.value <= 39 })
+
+            // Carb Ratios
+            let carbratios = fetchedProfile.carbratio.map { carbratio in
+                CarbRatioEntry(
+                    start: carbratio.time,
+                    offset: offset(carbratio.time) / 60,
+                    ratio: carbratio.value
+                )
+            }
+
+            if carbratios.contains(where: { $0.ratio <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 2,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Carb Ratio settings in Nightscout. Import aborted."]
+                )
+            }
+
+            let carbratiosProfile = CarbRatios(units: .grams, schedule: carbratios)
+
+            // Basal Profile
+            let basals = fetchedProfile.basal.map { basal in
+                BasalProfileEntry(
+                    start: basal.time,
+                    minutes: offset(basal.time) / 60,
+                    rate: basal.value
+                )
+            }
+
+            if basals.contains(where: { $0.rate <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 3,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Import aborted."]
+                )
+            }
+
+            if basals.reduce(0, { $0 + $1.rate }) <= 0 {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 4,
+                    userInfo: [
+                        NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Basal rate total cannot be 0 U/hr. Import aborted."
+                    ]
+                )
+            }
+
+            // Sensitivities
+            let sensitivities = fetchedProfile.sens.map { sensitivity in
+                InsulinSensitivityEntry(
+                    sensitivity: shouldConvertToMgdL ? correctUnitParsingOffsets(sensitivity.value.asMgdL) : sensitivity
+                        .value,
+                    offset: offset(sensitivity.time) / 60,
+                    start: sensitivity.time
+                )
+            }
+
+            if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 5,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout insulin sensitivity profile. Import aborted."]
+                )
+            }
+
+            let sensitivitiesProfile = InsulinSensitivities(
+                units: .mgdL,
+                userPreferredUnits: .mgdL,
+                sensitivities: sensitivities
+            )
+
+            // Targets
+            let targets = fetchedProfile.target_low.map { target in
+                BGTargetEntry(
+                    low: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
+                    high: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
+                    start: target.time,
+                    offset: offset(target.time) / 60
+                )
+            }
+
+            let targetsProfile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
+
+            // Store therapy settings in-memory in state model for further review
+            finalizeImport(
+                targets: targetsProfile,
+                basals: basals,
+                carbRatios: carbratiosProfile,
+                sensitivities: sensitivitiesProfile,
+                userPreferredUnitsFromImport: fetchedProfile.units,
+                currentStep: currentStep
+            )
+        } catch {
+            await MainActor.run {
+                self.nightscoutImportErrors.append(error.localizedDescription)
+                debug(.service, "Settings import failed with error: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    fileprivate func finalizeImport(
+        targets targetsProfile: BGTargets,
+        basals: [BasalProfileEntry],
+        carbRatios carbratiosProfile: CarbRatios,
+        sensitivities sensitivitiesProfile: InsulinSensitivities,
+        userPreferredUnitsFromImport: String,
+        currentStep: Binding<OnboardingStep>
+    ) {
+        // Parse: targetsProfile → targetItems
+        targetItems = targetsProfile.targets.map { entry in
+            let timeIndex = targetTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let lowIndex = targetRateValues.enumerated().min(by: {
+                abs($0.element - entry.low) < abs($1.element - entry.low)
+            })?.offset ?? 0
+
+            return TargetsEditor.Item(lowIndex: lowIndex, highIndex: lowIndex, timeIndex: timeIndex)
+        }
+        initialTargetItems = targetItems
+
+        // Parse: basals → basalProfileItems
+        basalProfileItems = basals.map { entry in
+            let timeIndex = basalProfileTimeValues.firstIndex(where: { Int($0) == entry.minutes * 60 }) ?? 0
+            let rateIndex = basalProfileRateValues.enumerated().min(by: {
+                abs($0.element - entry.rate) < abs($1.element - entry.rate)
+            })?.offset ?? 0
+            return BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialBasalProfileItems = basalProfileItems
+
+        // Parse: carbratiosProfile → carbRatioItems
+        carbRatioItems = carbratiosProfile.schedule.map { entry in
+            let timeIndex = carbRatioTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let rateIndex = carbRatioRateValues.enumerated().min(by: {
+                abs($0.element - entry.ratio) < abs($1.element - entry.ratio)
+            })?.offset ?? 0
+            return CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialCarbRatioItems = carbRatioItems
+
+        // Parse: sensitivitiesProfile → isfItems
+        isfItems = sensitivitiesProfile.sensitivities.map { entry in
+            let timeIndex = isfTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let rateIndex = isfRateValues.enumerated().min(by: {
+                abs($0.element - entry.sensitivity) < abs($1.element - entry.sensitivity)
+            })?.offset ?? 0
+
+            return ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialISFItems = isfItems
+
+        units = userPreferredUnitsFromImport.contains("mmol") ? .mmolL : .mgdL
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
+            self.nightscoutImportStatus = .finished
+            // navigate to the next onboarding step
+            if let next = currentStep.wrappedValue.next {
+                currentStep.wrappedValue = next
+            }
+        }
+    }
+
+    fileprivate func correctUnitParsingOffsets(_ parsedValue: Decimal) -> Decimal {
+        Int(parsedValue) % 2 == 0 ? parsedValue : parsedValue + 1
+    }
+
+    fileprivate func offset(_ string: String) -> Int {
+        let hours = Int(string.prefix(2)) ?? 0
+        let minutes = Int(string.suffix(2)) ?? 0
+        return ((hours * 60) + minutes) * 60
+    }
+
+    enum ImportStatus {
+        case running
+        case finished
+        case failed
+    }
+}

+ 523 - 0
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -0,0 +1,523 @@
+import Combine
+import FirebaseCrashlytics
+import Foundation
+import LoopKit
+import Observation
+import SwiftUI
+
+/// Model that holds the data collected during onboarding.
+extension Onboarding {
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var fileStorage: FileStorage!
+        @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var keychain: Keychain!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
+
+        private let settingsProvider = PickerSettingsProvider.shared
+
+        // MARK: - App Diagnostics
+
+        var diagnosticsSharingOption: DiagnosticsSharingOption = .enabled
+
+        // MARK: - Nightscout Setup
+
+        var nightscoutSetupOption: NightscoutSetupOption = .noSelection
+        var nightscoutImportOption: NightscoutImportOption = .noSelection
+        var nightscoutUrl = ""
+        var nightscoutSecret = ""
+        var nightscoutResponseMessage = ""
+        var isValidNightscoutURL: Bool = false
+        var isConnectingToNS: Bool = false
+        var isConnectedToNS: Bool = false
+        var nightscoutImportErrors: [String] = []
+        var nightscoutImportStatus: ImportStatus = .finished
+
+        // MARK: - Units and Pump Omboarding Option
+
+        var units: GlucoseUnits = .mgdL
+        var pumpOptionForOnboardingUnits: PumpOptionForOnboardingUnits = .omnipodDash
+
+        // MARK: - Time Values (shared)
+
+        let sharedTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }.sorted()
+
+        // MARK: - Carb Ratio
+
+        let carbRatioPickerSetting = PickerSetting(value: 3, step: 0.1, min: 3, max: 50, type: .gram)
+        var carbRatioItems: [CarbRatioEditor.Item] = []
+        var initialCarbRatioItems: [CarbRatioEditor.Item] = []
+        var carbRatioTimeValues: [TimeInterval] { sharedTimeValues }
+        var carbRatioRateValues: [Decimal] { settingsProvider.generatePickerValues(from: carbRatioPickerSetting, units: units) }
+
+        // MARK: - Basal Profile
+
+        var basalRatePickerSetting: PickerSetting {
+            switch pumpOptionForOnboardingUnits {
+            case .dana:
+                return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 3, type: .insulinUnitPerHour)
+            case .minimed:
+                return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 35, type: .insulinUnitPerHour)
+            case .omnipodDash:
+                return PickerSetting(value: 0.05, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
+            case .omnipodEros:
+                return PickerSetting(value: 0.05, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour)
+            }
+        }
+
+        var basalProfileItems: [BasalProfileEditor.Item] = []
+        var initialBasalProfileItems: [BasalProfileEditor.Item] = []
+        var basalProfileTimeValues: [TimeInterval] { sharedTimeValues }
+        var basalProfileRateValues: [Decimal] { settingsProvider.generatePickerValues(from: basalRatePickerSetting, units: units)
+        }
+
+        // MARK: - Insulin Sensitivity Factor (ISF)
+
+        var sensitivityPickerSetting = PickerSetting(value: 100, step: 1, min: 9, max: 540, type: .glucose)
+        var isfItems: [ISFEditor.Item] = []
+        var initialISFItems: [ISFEditor.Item] = []
+        var isfTimeValues: [TimeInterval] { sharedTimeValues }
+        var isfRateValues: [Decimal] { settingsProvider.generatePickerValues(from: sensitivityPickerSetting, units: units) }
+
+        // MARK: - Glucose Targets
+
+        let letTargetPickerSetting = PickerSetting(value: 100, step: 1, min: 72, max: 180, type: .glucose)
+        var targetItems: [TargetsEditor.Item] = []
+        var initialTargetItems: [TargetsEditor.Item] = []
+        var targetTimeValues: [TimeInterval] { sharedTimeValues }
+        var targetRateValues: [Decimal] { settingsProvider.generatePickerValues(from: letTargetPickerSetting, units: units) }
+
+        // MARK: - Delivery Limit Defaults
+
+        var maxBolus: Decimal = 10
+        var maxBasal: Decimal = 2
+        var maxIOB: Decimal = 0
+        var maxCOB: Decimal = 120
+        var minimumSafetyThreshold: Decimal = 60
+
+        // MARK: - Subscribe
+
+        override func subscribe() {
+            // Keychain items are not removed, even after uninstalling the app. Attempt to read them initially.
+            nightscoutUrl = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey) ?? ""
+            nightscoutSecret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey) ?? ""
+            isConnectedToNS = false
+            isConnectingToNS = false
+            isValidNightscoutURL = false
+
+            // Attempt to fetch existing units, therapy settings and delivery limits from file
+            units = settingsManager.settings.units
+            fetchExistingTherapySettingsFromFile()
+            fetchExistingDeliveryLimtisFromFile()
+        }
+
+        // MARK: - Helpers
+
+        /// Finds the index of the closest `Decimal` value in the given array.
+        /// - Parameters:
+        ///   - value: The value to match.
+        ///   - array: The array to search in.
+        /// - Returns: Closest index in array.
+        func closestIndex(for value: Decimal, in array: [Decimal]) -> Int {
+            array.enumerated().min(by: {
+                abs($0.element - value) < abs($1.element - value)
+            })?.offset ?? 0
+        }
+
+        /// Finds the index of the closest `TimeInterval` value in the given array.
+        /// - Parameters:
+        ///   - value: The time value to match.
+        ///   - array: The array to search in.
+        /// - Returns: Closest index in array.
+        func closestIndex(for value: TimeInterval, in array: [TimeInterval]) -> Int {
+            array.enumerated().min(by: {
+                abs($0.element - value) < abs($1.element - value)
+            })?.offset ?? 0
+        }
+
+        /// A date formatter for time strings used in saved settings.
+        private var timeFormatter: DateFormatter {
+            let formatter = DateFormatter()
+            formatter.timeZone = TimeZone(secondsFromGMT: 0)
+            formatter.dateFormat = "HH:mm:ss"
+            return formatter
+        }
+
+        // MARK: - Fetch existing therapy settings from file
+
+        /// Loads existing therapy settings from the provider and maps them into UI editor items.
+        ///
+        /// This function processes therapy-related configurations (glucose targets, basal rates,
+        /// carb ratios, and insulin sensitivity factors) stored in file-backed models from the provider.
+        /// It calculates the closest matching indices for time and rate values to map them to corresponding
+        /// `Editor.Item` models for use in the UI.
+        ///
+        /// - Populates:
+        ///   - `targetItems` and `initialTargetItems` with glucose target entries.
+        ///   - `basalProfileItems` and `initialBasalProfileItems` with basal rate entries.
+        ///   - `carbRatioItems` and `initialCarbRatioItems` with carbohydrate ratio entries.
+        ///   - `isfItems` and `initialISFItems` with insulin sensitivity factor entries.
+        func fetchExistingTherapySettingsFromFile() {
+            targetItems = provider.glucoseTargetsOnFile.targets.map { value in
+                let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: targetTimeValues)
+                let lowIndex = closestIndex(for: value.low, in: targetRateValues)
+                let highIndex = closestIndex(for: value.high, in: targetRateValues)
+                return TargetsEditor.Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
+            }
+            initialTargetItems = targetItems
+                .map { TargetsEditor.Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
+
+            basalProfileItems = provider.basalProfileOnFile.map { value in
+                let timeIndex = closestIndex(for: TimeInterval(Double(value.minutes * 60)), in: basalProfileTimeValues)
+                let rateIndex = closestIndex(for: value.rate, in: basalProfileRateValues)
+                return BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+            initialBasalProfileItems = basalProfileItems
+                .map { BasalProfileEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
+            carbRatioItems = provider.carbRatiosOnFile.schedule.map { value in
+                let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: carbRatioTimeValues)
+                let rateIndex = closestIndex(for: value.ratio, in: carbRatioRateValues)
+                return CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+
+            initialCarbRatioItems = carbRatioItems.map { CarbRatioEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
+            isfItems = provider.isfOnFile.sensitivities.map { value in
+                let timeIndex = closestIndex(for: TimeInterval(Double(value.offset * 60)), in: isfTimeValues)
+                let rateIndex = closestIndex(for: value.sensitivity, in: isfRateValues)
+
+                return ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+
+            initialISFItems = isfItems.map { ISFEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+        }
+
+        /// Loads delivery limit settings (Units, Max IOB, Max COB, Max Bolus, Max Basal) from the provider.
+        ///
+        /// Retrieves pump-related safety and delivery limits from both the provider's
+        /// file-backed pump settings and app-specific preferences. These values are used
+        /// to pre-fill the delivery limits editor in the onboarding or settings UI.
+        ///
+        /// - Populates:
+        ///   - `maxBolus` and `maxBasal` from file-based pump settings.
+        ///   - `maxIOB`, `maxCOB`, and `minimumSafetyThreshold` from app preferences.
+        ///   - `units` from app settings.
+        func fetchExistingDeliveryLimtisFromFile() {
+            let pumpSettingsFromFile = provider.pumpSettingsFromFile
+
+            if let pumpSettingsFromFile = pumpSettingsFromFile {
+                maxBolus = pumpSettingsFromFile.maxBolus
+                maxBasal = pumpSettingsFromFile.maxBasal
+            }
+
+            let preferences = settingsManager.preferences
+            maxIOB = preferences.maxIOB
+            maxCOB = preferences.maxCOB
+            minimumSafetyThreshold = preferences.threshold_setting
+        }
+
+        // MARK: - Get Therapy Items
+
+        /// Converts ISF editor items to a list of `TherapySettingItem`.
+        /// - Returns: Sorted list of therapy setting items based on ISF.
+        func getISFTherapyItems() -> [TherapySettingItem] {
+            getTherapyItems(from: isfItems, rateValues: isfRateValues, timeValues: isfTimeValues)
+        }
+
+        /// Converts basal profile editor items to a list of `TherapySettingItem`.
+        /// - Returns: Sorted list of therapy setting items based on basal rates.
+        func getBasalTherapyItems() -> [TherapySettingItem] {
+            getTherapyItems(
+                from: basalProfileItems,
+                rateValues: basalProfileRateValues,
+                timeValues: basalProfileTimeValues
+            )
+        }
+
+        /// Converts carb ratio editor items to a list of `TherapySettingItem`.
+        /// - Returns: Sorted list of therapy setting items based on carb ratios.
+        func getCarbRatioTherapyItems() -> [TherapySettingItem] {
+            getTherapyItems(from: carbRatioItems, rateValues: carbRatioRateValues, timeValues: carbRatioTimeValues)
+        }
+
+        /// Converts glucose target editor items to a list of `TherapySettingItem`.
+        /// - Returns: Sorted list of therapy setting items based on glucose targets.
+        func getTargetTherapyItems() -> [TherapySettingItem] {
+            targetItems.map {
+                TherapySettingItem(
+                    time: targetTimeValues[$0.timeIndex],
+                    value: targetRateValues[$0.lowIndex]
+                )
+            }.sorted { $0.time < $1.time }
+        }
+
+        /// Generic helper to convert any type of editor item into therapy setting items.
+        /// - Parameters:
+        ///   - items: An array of items conforming to `TherapyItemConvertible`.
+        ///   - rateValues: The rate values to be used.
+        ///   - timeValues: The time values to be used.
+        /// - Returns: A sorted array of `TherapySettingItem`.
+        private func getTherapyItems<T: TherapyItemConvertible>(
+            from items: [T],
+            rateValues: [Decimal],
+            timeValues: [TimeInterval]
+        ) -> [TherapySettingItem] {
+            items.map {
+                TherapySettingItem(
+                    time: timeValues[$0.timeIndex],
+                    value: rateValues[$0.rateIndex]
+                )
+            }.sorted { $0.time < $1.time }
+        }
+
+        // MARK: - Unified Update Methods
+
+        /// Updates the ISF editor items based on the provided therapy setting items.
+        /// - Parameter therapyItems: The list of therapy items to update from.
+        func updateISF(from therapyItems: [TherapySettingItem]) {
+            isfItems = therapyItems.map {
+                ISFEditor.Item(
+                    rateIndex: closestIndex(for: $0.value, in: isfRateValues),
+                    timeIndex: closestIndex(for: $0.time, in: isfTimeValues)
+                )
+            }.sorted { $0.timeIndex < $1.timeIndex }
+        }
+
+        /// Updates the basal rate editor items based on the provided therapy setting items.
+        /// - Parameter therapyItems: The list of therapy items to update from.
+        func updateBasal(from therapyItems: [TherapySettingItem]) {
+            basalProfileItems = therapyItems.map {
+                BasalProfileEditor.Item(
+                    rateIndex: closestIndex(for: $0.value, in: basalProfileRateValues),
+                    timeIndex: closestIndex(for: $0.time, in: basalProfileTimeValues)
+                )
+            }.sorted { $0.timeIndex < $1.timeIndex }
+        }
+
+        /// Updates the carb ratio editor items based on the provided therapy setting items.
+        /// - Parameter therapyItems: The list of therapy items to update from.
+        func updateCarbRatio(from therapyItems: [TherapySettingItem]) {
+            carbRatioItems = therapyItems.map {
+                CarbRatioEditor.Item(
+                    rateIndex: closestIndex(for: $0.value, in: carbRatioRateValues),
+                    timeIndex: closestIndex(for: $0.time, in: carbRatioTimeValues)
+                )
+            }.sorted { $0.timeIndex < $1.timeIndex }
+        }
+
+        /// Updates the glucose target editor items based on the provided therapy setting items.
+        /// - Parameter therapyItems: The list of therapy items to update from.
+        func updateTargets(from therapyItems: [TherapySettingItem]) {
+            targetItems = therapyItems.map {
+                let rateIndex = closestIndex(for: $0.value, in: targetRateValues)
+                let timeIndex = closestIndex(for: $0.time, in: targetTimeValues)
+
+                return TargetsEditor.Item(
+                    lowIndex: rateIndex,
+                    highIndex: rateIndex,
+                    timeIndex: timeIndex
+                )
+            }.sorted { $0.timeIndex < $1.timeIndex }
+        }
+
+        // MARK: - Add Initials
+
+        /// Adds a default ISF editor item at 00:00 with a standard sensitivity value.
+        func addInitialISF() {
+            addInitialItem(
+                defaultValue: 50,
+                rateValues: isfRateValues,
+                assign: { isfItems = $0 },
+                makeItem: ISFEditor.Item.init
+            )
+        }
+
+        /// Adds a default basal rate editor item at 00:00 with a typical rate value.
+        func addInitialBasalRate() {
+            addInitialItem(
+                defaultValue: 0.1,
+                rateValues: basalProfileRateValues,
+                assign: { basalProfileItems = $0 },
+                makeItem: BasalProfileEditor.Item.init
+            )
+        }
+
+        /// Adds a default carb ratio editor item at 00:00 with a standard ratio.
+        func addInitialCarbRatio() {
+            addInitialItem(
+                defaultValue: 10,
+                rateValues: carbRatioRateValues,
+                assign: { carbRatioItems = $0 },
+                makeItem: CarbRatioEditor.Item.init
+            )
+        }
+
+        /// Adds a default glucose target item at 00:00 with a typical target value.
+        func addInitialTarget() {
+            let timeIndex = 0
+            let rateIndex = closestIndex(for: 100, in: targetRateValues)
+            targetItems = [TargetsEditor.Item(lowIndex: rateIndex, highIndex: rateIndex, timeIndex: timeIndex)]
+        }
+
+        /// Adds an initial therapy setting item for a given editor item type.
+        /// - Parameters:
+        ///   - defaultValue: The expected default value to use.
+        ///   - rateValues: The array of rate values for the item.
+        ///   - assign: A closure that assigns the newly created array to the correct property.
+        private func addInitialItem<ItemType>(
+            defaultValue: Decimal,
+            rateValues: [Decimal],
+            assign: ([ItemType]) -> Void,
+            makeItem: (Int, Int) -> ItemType
+        ) {
+            let timeIndex = 0
+            let rateIndex = closestIndex(for: defaultValue, in: rateValues)
+            assign([makeItem(rateIndex, timeIndex)])
+        }
+
+        // MARK: - Validate
+
+        /// Removes duplicate entries from `carbRatioItems`, ensures sorting by time index,
+        /// and forces the first entry to start at 00:00 (timeIndex 0).
+        func validateCarbRatios() {
+            carbRatioItems = validated(items: carbRatioItems, timeIndexKeyPath: \.timeIndex)
+        }
+
+        /// Removes duplicate entries from `basalProfileItems`, ensures sorting by time index,
+        /// and forces the first entry to start at 00:00 (timeIndex 0).
+        func validateBasal() {
+            basalProfileItems = validated(items: basalProfileItems, timeIndexKeyPath: \.timeIndex)
+        }
+
+        /// Removes duplicate entries from `isfItems`, ensures sorting by time index,
+        /// and forces the first entry to start at 00:00 (timeIndex 0).
+        func validateISF() {
+            isfItems = validated(items: isfItems, timeIndexKeyPath: \.timeIndex)
+        }
+
+        /// Removes duplicate entries from `targetItems`, ensures sorting by time index,
+        /// and forces the first entry to start at 00:00 (timeIndex 0).
+        func validateTarget() {
+            targetItems = validated(items: targetItems, timeIndexKeyPath: \.timeIndex)
+        }
+
+        /// Removes duplicates, sorts by time, and ensures the first entry starts at 00:00.
+        /// - Parameters:
+        ///   - items: The list of items to validate.
+        ///   - timeIndexKeyPath: A writable key path to the timeIndex property.
+        /// - Returns: A validated and sorted list of items with the first entry at 00:00.
+        private func validated<T: Hashable>(items: [T], timeIndexKeyPath: WritableKeyPath<T, Int>) -> [T] {
+            var result = Array(Set(items)).sorted { $0[keyPath: timeIndexKeyPath] < $1[keyPath: timeIndexKeyPath] }
+            if !result.isEmpty, result[0][keyPath: timeIndexKeyPath] != 0 {
+                result[0][keyPath: timeIndexKeyPath] = 0
+            }
+            return result
+        }
+
+        // MARK: - Save
+
+        /// Saves the carb ratio items to file storage and sets them as initial values.
+        func saveCarbRatios() {
+            let schedule = carbRatioItems.map { item in
+                let time = timeFormatter.string(from: Date(timeIntervalSince1970: carbRatioTimeValues[item.timeIndex]))
+                let offset = Int(carbRatioTimeValues[item.timeIndex] / 60)
+                let value = carbRatioRateValues[item.rateIndex]
+                return CarbRatioEntry(start: time, offset: offset, ratio: value)
+            }
+            fileStorage.save(CarbRatios(units: .grams, schedule: schedule), as: OpenAPS.Settings.carbRatios)
+            initialCarbRatioItems = carbRatioItems
+        }
+
+        /// Saves the basal profile items to file storage and sets them as initial values.
+        func saveBasalProfile() {
+            let profile = basalProfileItems.map { item in
+                let time = timeFormatter.string(from: Date(timeIntervalSince1970: basalProfileTimeValues[item.timeIndex]))
+                let offset = Int(basalProfileTimeValues[item.timeIndex] / 60)
+                let rate = basalProfileRateValues[item.rateIndex]
+                return BasalProfileEntry(start: time, minutes: offset, rate: rate)
+            }
+            fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
+            initialBasalProfileItems = basalProfileItems
+        }
+
+        /// Saves the insulin sensitivity (ISF) items to file storage and sets them as initial values.
+        func saveISFValues() {
+            let sensitivities = isfItems.map { item in
+                let time = timeFormatter.string(from: Date(timeIntervalSince1970: isfTimeValues[item.timeIndex]))
+                let offset = Int(isfTimeValues[item.timeIndex] / 60)
+                let value = isfRateValues[item.rateIndex]
+                return InsulinSensitivityEntry(sensitivity: value, offset: offset, start: time)
+            }
+            let profile = InsulinSensitivities(units: .mgdL, userPreferredUnits: .mgdL, sensitivities: sensitivities)
+            fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+            initialISFItems = isfItems
+        }
+
+        /// Saves the glucose target items to file storage and sets them as initial values.
+        func saveTargets() {
+            let targets = targetItems.map { item in
+                let time = timeFormatter.string(from: Date(timeIntervalSince1970: targetTimeValues[item.timeIndex]))
+                let offset = Int(targetTimeValues[item.timeIndex] / 60)
+                let value = targetRateValues[item.lowIndex]
+                return BGTargetEntry(low: value, high: value, start: time, offset: offset)
+            }
+            let profile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
+            fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
+            initialTargetItems = targetItems
+        }
+
+        /// Persists all onboarding data by applying settings and saving therapy values.
+        func saveOnboardingData() {
+            applyDiagnostics()
+            applyToSettings()
+            applyToPreferences()
+            applyToPumpSettings()
+            saveTargets()
+            saveBasalProfile()
+            saveCarbRatios()
+            saveISFValues()
+        }
+
+        /// Persists the current diagnostics sharing option to UserDefaults as a boolean.
+        func applyDiagnostics() {
+            let booleanValue: Bool = diagnosticsSharingOption == .enabled
+            UserDefaults.standard.set(booleanValue, forKey: "DiagnosticsSharing")
+            Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(booleanValue)
+        }
+
+        /// Applies the selected glucose units to the app's settings.
+        func applyToSettings() {
+            var settingsCopy = settingsManager.settings
+            settingsCopy.units = units
+            settingsManager.settings = settingsCopy
+        }
+
+        /// Applies the selected delivery preferences to the app's settings.
+        func applyToPreferences() {
+            var preferencesCopy = settingsManager.preferences
+            preferencesCopy.maxIOB = maxIOB
+            preferencesCopy.maxCOB = maxCOB
+            preferencesCopy.threshold_setting = minimumSafetyThreshold
+            settingsManager.preferences = preferencesCopy
+        }
+
+        /// Saves pump delivery limits to persistent storage and broadcasts changes.
+        func applyToPumpSettings() {
+            let defaultDIA = settingsProvider.settings.dia.value
+            let pumpSettings = PumpSettings(insulinActionCurve: defaultDIA, maxBolus: maxBolus, maxBasal: maxBasal)
+            fileStorage.save(pumpSettings, as: OpenAPS.Settings.settings)
+        }
+    }
+}
+
+// MARK: - Protocol (optional) to unify type mapping
+
+protocol TherapyItemConvertible {
+    var rateIndex: Int { get }
+    var timeIndex: Int { get }
+}
+
+extension ISFEditor.Item: TherapyItemConvertible {}
+extension CarbRatioEditor.Item: TherapyItemConvertible {}
+extension BasalProfileEditor.Item: TherapyItemConvertible {}

+ 385 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift

@@ -0,0 +1,385 @@
+import SwiftUI
+import Swinject
+
+/// The main onboarding view that manages navigation between onboarding steps.
+extension Onboarding {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @State var state = StateModel()
+        @State private var navigationDirection: OnboardingNavigationDirection = .forward
+        let onboardingManager: OnboardingManager
+        @State private var currentStep: OnboardingStep = .welcome
+        @State private var currentDeliverySubstep: DeliveryLimitSubstep = .maxIOB
+        @State private var currentNightscoutSubstep: NightscoutSubstep = .setupSelection
+
+        // Animation states
+        @State private var animationScale: CGFloat = 1.0
+        @State private var animationOpacity: Double = 0
+        @State private var isAnimating = false
+
+        // Conditional button states for Nightscout substeps
+        private var didSelectNightscoutSetupOption: Bool {
+            currentNightscoutSubstep == .setupSelection && state
+                .nightscoutSetupOption == .noSelection
+        }
+
+        private var hasValidNightscoutConnection: Bool {
+            currentNightscoutSubstep == .connectToNightscout && !state.isConnectedToNS
+        }
+
+        private var didSelectNightscoutImportOption: Bool {
+            currentNightscoutSubstep == .importFromNightscout && state.nightscoutImportOption == .noSelection
+        }
+
+        private var shouldDisableNextButton: Bool {
+            (currentStep == .nightscout && didSelectNightscoutSetupOption)
+                ||
+                (currentStep == .nightscout && hasValidNightscoutConnection)
+                ||
+                (currentStep == .nightscout && didSelectNightscoutImportOption)
+        }
+
+        var body: some View {
+            NavigationView {
+                ZStack {
+                    // Background gradient
+                    LinearGradient(
+                        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+                        startPoint: .top,
+                        endPoint: .bottom
+                    )
+                    .ignoresSafeArea()
+
+                    VStack(spacing: 0) {
+                        if (nonInfoOnboardingSteps + [OnboardingStep.overview, OnboardingStep.completed]).contains(currentStep) {
+                            // Progress bar
+                            OnboardingProgressBar(
+                                currentStep: currentStep,
+                                currentSubstep: {
+                                    switch currentStep {
+                                    case .deliveryLimits: return currentDeliverySubstep.rawValue
+                                    case .nightscout: return currentNightscoutSubstep.rawValue
+                                    default: return nil
+                                    }
+                                }(),
+                                stepsWithSubsteps: [
+                                    .nightscout: NightscoutSubstep.allCases.count,
+                                    .deliveryLimits: DeliveryLimitSubstep.allCases.count
+                                ],
+                                nightscoutSetupOption: state.nightscoutSetupOption
+                            )
+                            .padding(.top)
+                        }
+
+                        // Step content
+                        ScrollViewReader { scrollProxy in
+                            ScrollView {
+                                VStack(alignment: .leading, spacing: 20) {
+                                    // Scroll position marker at top
+                                    Color.clear.frame(height: 0).id("top")
+
+                                    // Header
+                                    if currentStep != .welcome && currentStep != .completed {
+                                        HStack {
+                                            if currentStep == .nightscout {
+                                                Image(currentStep.iconName)
+                                                    .resizable()
+                                                    .scaledToFit()
+                                                    .frame(width: 60, height: 60)
+
+                                            } else {
+                                                Image(systemName: currentStep.iconName)
+                                                    .font(.system(size: 40))
+                                                    .foregroundColor(currentStep.accentColor)
+                                                    .frame(width: 60, height: 60)
+                                                    .background(
+                                                        Circle()
+                                                            .fill(currentStep.accentColor.opacity(0.2))
+                                                    )
+                                            }
+
+                                            VStack(alignment: .leading) {
+                                                Text(currentStep.title)
+                                                    .font(.largeTitle)
+                                                    .fontWeight(.bold)
+                                                    .foregroundColor(.primary)
+
+                                                Text(currentStep.description)
+                                                    .font(.subheadline)
+                                                    .foregroundColor(.secondary)
+                                                    .fixedSize(horizontal: false, vertical: true)
+                                            }
+                                        }
+                                        .padding([.horizontal, .top])
+                                    }
+
+                                    // Step-specific content
+                                    Group {
+                                        switch currentStep {
+                                        case .welcome:
+                                            WelcomeStepView()
+                                        case .startupGuide:
+                                            StartupGuideStepView()
+                                        case .overview:
+                                            OverviewStepView()
+                                        case .diagnostics:
+                                            DiagnosticsStepView(state: state)
+                                        case .nightscout:
+                                            switch currentNightscoutSubstep {
+                                            case .setupSelection:
+                                                NightscoutSetupStepView(state: state)
+                                            case .connectToNightscout:
+                                                NightscoutLoginStepView(state: state)
+                                            case .importFromNightscout:
+                                                NightscoutImportStepView(state: state)
+                                            }
+                                        case .unitSelection:
+                                            UnitSelectionStepView(state: state)
+                                        case .glucoseTarget:
+                                            GlucoseTargetStepView(state: state)
+                                        case .basalRates:
+                                            BasalProfileStepView(state: state)
+                                        case .carbRatio:
+                                            CarbRatioStepView(state: state)
+                                        case .insulinSensitivity:
+                                            InsulinSensitivityStepView(state: state)
+                                        case .deliveryLimits:
+                                            DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
+                                        case .completed:
+                                            CompletedStepView()
+                                        }
+                                    }
+                                    .transition(
+                                        navigationDirection == .forward
+                                            ? .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
+                                            : .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
+                                    )
+                                    .padding(.horizontal)
+                                    .id(currentStep.id) // Force view recreation when step changes
+                                }
+                                .padding(.bottom, 80) // Make room for buttons at bottom
+                            }
+                            .onChange(of: currentStep) { _, _ in
+                                scrollProxy.scrollTo("top", anchor: .top)
+                            }
+                            .onChange(of: currentNightscoutSubstep) { _, _ in
+                                scrollProxy.scrollTo("top", anchor: .top)
+                            }
+                            .onChange(of: currentDeliverySubstep) { _, _ in
+                                scrollProxy.scrollTo("top", anchor: .top)
+                            }
+                        }
+
+                        Spacer()
+
+                        // Navigation buttons
+                        HStack {
+                            // Back button
+                            if currentStep != .welcome {
+                                Button(action: {
+                                    navigationDirection = .backward
+                                    withAnimation {
+                                        if currentStep == .completed {
+                                            currentStep = .deliveryLimits
+                                            currentDeliverySubstep =
+                                                .minimumSafetyThreshold // ensure we land on the last substep visually
+                                        } else if currentStep == .nightscout {
+                                            if currentNightscoutSubstep == .setupSelection {
+                                                // First substep: go to previous main step
+                                                if let previousMainStep = currentStep.previous {
+                                                    currentStep = previousMainStep
+                                                    currentNightscoutSubstep = .setupSelection // reset substep
+                                                }
+                                            } else {
+                                                // Go back one substep
+                                                currentNightscoutSubstep = NightscoutSubstep(
+                                                    rawValue: currentNightscoutSubstep
+                                                        .rawValue - 1
+                                                )!
+                                            }
+                                        } else if currentStep == .deliveryLimits {
+                                            if let previousSub = DeliveryLimitSubstep(
+                                                rawValue: currentDeliverySubstep
+                                                    .rawValue - 1
+                                            ) {
+                                                currentDeliverySubstep = previousSub
+                                            } else if let previousMainStep = currentStep.previous {
+                                                currentStep = previousMainStep
+                                                currentDeliverySubstep = .maxIOB // reset to first substep for later return
+                                            }
+                                        } else if let previous = currentStep.previous {
+                                            currentStep = previous
+                                        }
+                                    }
+                                }) {
+                                    HStack {
+                                        Image(systemName: "chevron.left")
+                                        Text("Back")
+                                    }
+                                    .padding()
+                                    .foregroundColor(.primary)
+                                }
+                            }
+
+                            Spacer()
+
+                            // Next/Finish button
+                            Button(action: {
+                                navigationDirection = .forward
+                                withAnimation {
+                                    if currentStep == .completed {
+                                        state.saveOnboardingData()
+                                        onboardingManager.completeOnboarding()
+                                        Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
+                                    } else if currentStep == .nightscout {
+                                        if currentNightscoutSubstep != .importFromNightscout {
+                                            // Handle conditional skip
+                                            if currentNightscoutSubstep == .setupSelection,
+                                               state.nightscoutSetupOption == .skipNightscoutSetup,
+                                               let next = currentStep.next
+                                            {
+                                                currentStep = next
+                                            } else {
+                                                currentNightscoutSubstep = NightscoutSubstep(
+                                                    rawValue: currentNightscoutSubstep
+                                                        .rawValue + 1
+                                                )!
+                                            }
+                                        } else if currentNightscoutSubstep == .importFromNightscout,
+                                                  state.nightscoutImportOption == .useImport
+                                        {
+                                            // TODO: trigger import, show animation, then proceed to next step
+                                            Task {
+                                                await state.importSettingsFromNightscout(currentStep: $currentStep)
+                                            }
+                                        } else if let next = currentStep.next {
+                                            currentStep = next
+                                        }
+                                    } else if currentStep == .deliveryLimits {
+                                        if let nextSub = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
+                                            currentDeliverySubstep = nextSub
+                                        } else if let next = currentStep.next {
+                                            currentStep = next
+                                            currentDeliverySubstep = .maxIOB
+                                        }
+                                    } else if let next = currentStep.next {
+                                        currentStep = next
+                                    }
+                                }
+                            }) {
+                                HStack {
+                                    Text(currentStep == .completed ? "Get Started" : "Next")
+                                    Image(systemName: "chevron.right")
+                                }
+                                .padding()
+                                .foregroundColor(.white)
+                                .background(Capsule().fill(!shouldDisableNextButton ? Color.blue : Color(.systemGray)))
+                            }.disabled(shouldDisableNextButton)
+                        }
+                        .padding(.horizontal)
+                        .padding(.bottom)
+                    }
+                }
+                .navigationBarHidden(true)
+            }
+            .onChange(of: currentStep) { _, _ in
+                // Reset animation when step changes
+                animationScale = 0.9
+                animationOpacity = 0
+                isAnimating = false
+
+                // Start new animation
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                    withAnimation(.easeInOut(duration: 0.7)) {
+                        animationOpacity = 1
+                        animationScale = 1.0
+                    }
+                    isAnimating = true
+                }
+            }
+            .onAppear(perform: configureView)
+        }
+    }
+}
+
+/// A progress bar that shows the user's progress through the onboarding process.
+struct OnboardingProgressBar: View {
+    let currentStep: OnboardingStep
+    let currentSubstep: Int?
+    let stepsWithSubsteps: [OnboardingStep: Int]
+    let nightscoutSetupOption: NightscoutSetupOption
+
+    var body: some View {
+        HStack(spacing: 4) {
+            ForEach(renderedSteps, id: \.id) { step in
+                ZStack(alignment: .leading) {
+                    Rectangle()
+                        .fill(Color.gray.opacity(0.3))
+                        .frame(height: 4)
+                        .cornerRadius(2)
+
+                    GeometryReader { geo in
+                        Rectangle()
+                            .fill(Color.blue)
+                            .frame(
+                                width: geo.size.width * fillFraction(for: step.step, totalSubsteps: step.substeps),
+                                height: 4
+                            )
+                            .cornerRadius(2)
+                    }
+                }
+                .frame(height: 4)
+            }
+        }
+        .padding(.horizontal)
+    }
+
+    private var renderedSteps: [(id: String, step: OnboardingStep, substeps: Int?)] {
+        nonInfoOnboardingSteps.map {
+            (id: "\($0.rawValue)", step: $0, substeps: stepsWithSubsteps[$0])
+        }
+    }
+
+    private func fillFraction(for step: OnboardingStep, totalSubsteps: Int?) -> CGFloat {
+        // If currentStep is .completed, fill everything
+        if currentStep == .completed { return 1.0 }
+
+        if let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
+           let stepIndex = nonInfoOnboardingSteps.firstIndex(of: step),
+           stepIndex < currentIndex
+        {
+            return 1.0
+        }
+
+        if step == currentStep {
+            if let total = totalSubsteps, let current = currentSubstep {
+                return CGFloat(current + 1) / CGFloat(total)
+            } else {
+                return 1.0
+            }
+        }
+
+        // Handle special case: Nightscout was skipped
+        if step == .nightscout,
+           nightscoutSetupOption == .skipNightscoutSetup,
+           let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
+           let nightscoutIndex = nonInfoOnboardingSteps.firstIndex(of: .nightscout),
+           currentIndex > nightscoutIndex
+        {
+            return 1.0
+        }
+
+        return 0.0
+    }
+}
+
+struct Onboarding_Preview: PreviewProvider {
+    static var previews: some View {
+        Group {
+            let resolver = TrioApp.resolver
+            let onboardingManager = OnboardingManager()
+            Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
+                .previewDisplayName("Onboarding Flow")
+        }
+    }
+}

+ 213 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BasalProfileStepView.swift

@@ -0,0 +1,213 @@
+//
+//  BasalProfileStepView.swift
+//  Trio
+//
+//  Created by Marvin Polscheit on 19.03.25.
+//
+import Charts
+import SwiftUI
+import UIKit
+
+/// Basal profile step view for setting basal insulin rates.
+struct BasalProfileStepView: View {
+    @Bindable var state: Onboarding.StateModel
+    @State private var refreshUI = UUID() // to update chart when slider value changes
+    @State private var therapyItems: [TherapySettingItem] = []
+    @State private var now = Date()
+
+    private var rateFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        return formatter
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        LazyVStack {
+            VStack(alignment: .leading, spacing: 0) {
+                // Chart visualization
+                if !state.basalProfileItems.isEmpty {
+                    VStack(alignment: .leading) {
+                        basalProfileChart
+                            .frame(height: 180)
+                            .padding(.horizontal)
+                    }
+                    .padding(.vertical)
+                    .background(Color.chart.opacity(0.65))
+                    .clipShape(
+                        .rect(
+                            topLeadingRadius: 10,
+                            bottomLeadingRadius: 0,
+                            bottomTrailingRadius: 0,
+                            topTrailingRadius: 10
+                        )
+                    )
+                }
+
+                TherapySettingEditorView(
+                    items: $therapyItems,
+                    unit: .unitPerHour,
+                    timeOptions: state.basalProfileTimeValues,
+                    valueOptions: state.basalProfileRateValues,
+                    validateOnDelete: state.validateBasal
+                )
+
+                Spacer(minLength: 20)
+
+                // Total daily basal calculation
+                if !state.basalProfileItems.isEmpty {
+                    VStack(alignment: .leading, spacing: 0) {
+                        HStack {
+                            Text("Total")
+                                .bold()
+
+                            Spacer()
+
+                            HStack {
+                                Text(rateFormatter.string(from: calculateTotalDailyBasal() as NSNumber) ?? "0")
+                                Text("U/day")
+                                    .foregroundStyle(Color.secondary)
+                            }
+                            .id(refreshUI) // Erzwingt die Aktualisierung des Totals
+                        }
+                    }
+                    .padding()
+                    .background(Color.chart.opacity(0.65))
+                    .cornerRadius(10)
+                }
+            }
+        }
+        .onAppear {
+            if state.basalProfileItems.isEmpty {
+                state.addInitialBasalRate()
+            }
+            state.validateBasal()
+            therapyItems = state.getBasalTherapyItems()
+        }.onChange(of: therapyItems) { _, newItems in
+            state.updateBasal(from: newItems)
+            refreshUI = UUID()
+        }
+    }
+
+    // Calculate the total daily basal insulin
+    private func calculateTotalDailyBasal() -> Double {
+        let items = state.basalProfileItems
+
+        // If there are no items, return 0
+        if items.isEmpty {
+            return 0.0
+        }
+
+        var total: Double = 0.0
+
+        // Safely create profile items with proper error checking
+        let profileItems = items.compactMap { item -> (timeIndex: Int, rate: Decimal)? in
+            // Safety check - make sure indices are within bounds
+            guard item.timeIndex >= 0 && item.timeIndex < state.basalProfileTimeValues.count,
+                  item.rateIndex >= 0 && item.rateIndex < state.basalProfileRateValues.count
+            else {
+                return nil
+            }
+
+            let timeValue = state.basalProfileTimeValues[item.timeIndex]
+            let rate = state.basalProfileRateValues[item.rateIndex]
+            return (Int(timeValue / 60), rate)
+        }.sorted(by: { $0.timeIndex < $1.timeIndex })
+
+        // If after safety checks we have no valid items, return 0
+        if profileItems.isEmpty {
+            return 0.0
+        }
+
+        // Create time points array safely
+        var timePoints = profileItems.map(\.timeIndex)
+
+        // Add the 24-hour mark to complete the cycle
+        timePoints.append(24 * 60) // Add 24 hours in minutes
+
+        // Calculate the total by multiplying each rate by its duration
+        for i in 0 ..< profileItems.count {
+            let rate = profileItems[i].rate
+            let currentTimeIndex = profileItems[i].timeIndex
+
+            // Calculate duration safely
+            let nextTimeIndex = i + 1 < timePoints.count ? timePoints[i + 1] : (24 * 60)
+            let duration = nextTimeIndex - currentTimeIndex
+
+            // Only add if duration is positive
+            if duration > 0 {
+                total += Double(rate) * Double(duration) / 60.0 // Convert to hours
+            }
+        }
+
+        return total
+    }
+
+    // Chart for visualizing basal profile
+    private var basalProfileChart: some View {
+        Chart {
+            ForEach(Array(state.basalProfileItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.basalProfileRateValues[item.rateIndex]
+
+                let startDate = Calendar.current
+                    .startOfDay(for: now)
+                    .addingTimeInterval(state.basalProfileTimeValues[item.timeIndex])
+
+                var offset: TimeInterval {
+                    if state.basalProfileItems.count > index + 1 {
+                        return state.basalProfileTimeValues[state.basalProfileItems[index + 1].timeIndex]
+                    } else {
+                        return state.basalProfileTimeValues.last! + 30 * 60
+                    }
+                }
+
+                let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
+                RectangleMark(
+                    xStart: .value("start", startDate),
+                    xEnd: .value("end", endDate),
+                    yStart: .value("rate-start", displayValue),
+                    yEnd: .value("rate-end", 0)
+                ).foregroundStyle(
+                    .linearGradient(
+                        colors: [
+                            Color.purple.opacity(0.6),
+                            Color.purple.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
+                    )
+                ).alignsMarkStylesWithPlotArea()
+
+                LineMark(x: .value("End Date", startDate), y: .value("Rate", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
+
+                LineMark(x: .value("Start Date", endDate), y: .value("Rate", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
+            }
+        }
+        .id(refreshUI) // Force chart update
+        .chartXAxis {
+            AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                AxisValueLabel(format: .dateTime.hour())
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+        .chartXScale(
+            domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                .addingTimeInterval(60 * 60 * 24)
+        )
+        .chartYAxis {
+            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                AxisValueLabel()
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+    }
+}

+ 188 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CarbRatioStepView.swift

@@ -0,0 +1,188 @@
+//
+//  CarbRatioStepView.swift
+//  Trio
+//
+//  Created by Marvin Polscheit on 19.03.25.
+//
+import Charts
+import SwiftUI
+import UIKit
+
+/// Carb ratio step view for setting insulin-to-carb ratio.
+struct CarbRatioStepView: View {
+    @Bindable var state: Onboarding.StateModel
+    @State private var refreshUI = UUID() // to update chart when slider value changes
+    @State private var therapyItems: [TherapySettingItem] = []
+    @State private var now = Date()
+
+    private var formatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        LazyVStack {
+            VStack(alignment: .leading, spacing: 0) {
+                // Chart visualization
+                if !state.carbRatioItems.isEmpty {
+                    VStack(alignment: .leading) {
+                        carbRatioChart
+                            .frame(height: 180)
+                            .padding(.horizontal)
+                    }
+                    .padding(.vertical)
+                    .background(Color.chart.opacity(0.65))
+                    .clipShape(
+                        .rect(
+                            topLeadingRadius: 10,
+                            bottomLeadingRadius: 0,
+                            bottomTrailingRadius: 0,
+                            topTrailingRadius: 10
+                        )
+                    )
+                }
+
+                TherapySettingEditorView(
+                    items: $therapyItems,
+                    unit: .gramPerUnit,
+                    timeOptions: state.carbRatioTimeValues,
+                    valueOptions: state.carbRatioRateValues,
+                    validateOnDelete: state.validateCarbRatios
+                )
+
+                // Example calculation based on first carb ratio
+                if !state.carbRatioItems.isEmpty {
+                    Spacer(minLength: 20)
+
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Example Calculation")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        VStack(alignment: .leading, spacing: 8) {
+                            Text("For 45g of carbs, you would need:")
+                                .font(.subheadline)
+                                .padding(.horizontal)
+
+                            let insulinNeeded = 45 /
+                                Double(
+                                    truncating: state
+                                        .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
+                                )
+                            Text(
+                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--")  = \(String(format: "%.1f", insulinNeeded))" +
+                                    " " + String(localized: "U", comment: "Insulin unit abbreviation")
+                            )
+                            .font(.system(.body, design: .monospaced))
+                            .foregroundColor(.orange)
+                            .padding()
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .background(Color.chart.opacity(0.65))
+                            .cornerRadius(10)
+                        }
+                    }
+
+                    Spacer(minLength: 20)
+
+                    // Information about the carb ratio
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("What This Means")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10g of carbs")
+                            Text("• A lower number means you need more insulin for the same amount of carbs")
+                            Text("• A higher number means you need less insulin for the same amount of carbs")
+                            Text("• Different times of day may require different ratios")
+                        }
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                        .padding(.horizontal)
+                    }
+                }
+            }
+        }
+        .onAppear {
+            if state.carbRatioItems.isEmpty {
+                state.addInitialCarbRatio()
+            }
+            state.validateCarbRatios()
+            therapyItems = state.getCarbRatioTherapyItems()
+        }.onChange(of: therapyItems) { _, newItems in
+            state.updateCarbRatio(from: newItems)
+            refreshUI = UUID()
+        }
+    }
+
+    // Chart for visualizing carb ratios
+    private var carbRatioChart: some View {
+        Chart {
+            ForEach(Array(state.carbRatioItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.carbRatioRateValues[item.rateIndex]
+
+                let startDate = Calendar.current
+                    .startOfDay(for: now)
+                    .addingTimeInterval(state.carbRatioTimeValues[item.timeIndex])
+
+                var offset: TimeInterval {
+                    if state.carbRatioItems.count > index + 1 {
+                        return state.carbRatioTimeValues[state.carbRatioItems[index + 1].timeIndex]
+                    } else {
+                        return state.carbRatioTimeValues.last! + 30 * 60
+                    }
+                }
+
+                let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
+                RectangleMark(
+                    xStart: .value("start", startDate),
+                    xEnd: .value("end", endDate),
+                    yStart: .value("rate-start", displayValue),
+                    yEnd: .value("rate-end", 0)
+                ).foregroundStyle(
+                    .linearGradient(
+                        colors: [
+                            Color.orange.opacity(0.6),
+                            Color.orange.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
+                    )
+                ).alignsMarkStylesWithPlotArea()
+
+                LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
+
+                LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
+            }
+        }
+        .id(refreshUI) // Force chart update
+        .chartXAxis {
+            AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                AxisValueLabel(format: .dateTime.hour())
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+        .chartXScale(
+            domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                .addingTimeInterval(60 * 60 * 24)
+        )
+        .chartYAxis {
+            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                AxisValueLabel()
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+    }
+}

+ 42 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CompletedStepView.swift

@@ -0,0 +1,42 @@
+import SwiftUI
+
+/// Completed step view shown at the end of onboarding.
+struct CompletedStepView: View {
+    var body: some View {
+        VStack(alignment: .center, spacing: 20) {
+            Image(systemName: "checkmark.circle.fill")
+                .font(.system(size: 80))
+                .foregroundColor(.green)
+
+            Text("You're All Set!")
+                .font(.title)
+                .fontWeight(.bold)
+                .multilineTextAlignment(.center)
+
+            Text(
+                "You've successfully completed the initial setup of Trio. Tap 'Get Started' to save your settings and get ready to start using Trio."
+            )
+            .multilineTextAlignment(.center)
+            .foregroundColor(.secondary)
+
+            VStack(alignment: .leading, spacing: 12) {
+                ForEach(
+                    nonInfoOnboardingSteps,
+                    id: \.self
+                ) { step in
+                    SettingItemView(step: step, icon: step.iconName, title: step.title, type: .complete)
+                }
+            }
+            .padding()
+            .background(Color.green.opacity(0.1))
+            .cornerRadius(12)
+
+            Text("Remember, you can adjust these settings at any time in the app settings if needed.")
+                .multilineTextAlignment(.center)
+                .foregroundColor(.primary)
+                .bold()
+        }
+        .padding()
+        .frame(maxWidth: .infinity)
+    }
+}

+ 109 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DeliveryLimitsStepView.swift

@@ -0,0 +1,109 @@
+import SwiftUI
+
+struct DeliveryLimitsStepView: View {
+    @Bindable var state: Onboarding.StateModel
+    let substep: DeliveryLimitSubstep
+
+    @State private var shouldDisplayPicker: Bool = false
+
+    private let settingsProvider = PickerSettingsProvider.shared
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 16) {
+            Text(substep.hint)
+                .padding(.horizontal)
+                .font(.headline)
+
+            switch substep {
+            case .maxIOB:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxIOB,
+                    decimalValue: $state.maxIOB
+                )
+            case .maxBolus:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxBolus,
+                    decimalValue: $state.maxBolus
+                )
+            case .maxBasal:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxBasal,
+                    decimalValue: $state.maxBasal
+                )
+            case .maxCOB:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.maxCOB,
+                    decimalValue: $state.maxCOB
+                )
+            case .minimumSafetyThreshold:
+                deliveryLimitInputSection(
+                    label: substep.title,
+                    displayPicker: $shouldDisplayPicker,
+                    setting: settingsProvider.settings.threshold_setting,
+                    decimalValue: $state.minimumSafetyThreshold
+                )
+            }
+
+            AnyView(substep.description(units: state.units))
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+                .padding(.horizontal)
+                .multilineTextAlignment(.leading)
+        }
+    }
+
+    @ViewBuilder private func deliveryLimitInputSection(
+        label: String,
+        displayPicker: Binding<Bool>,
+        setting: PickerSetting,
+        decimalValue: Binding<Decimal>
+    ) -> some View {
+        VStack {
+            HStack {
+                Text(label)
+                Spacer()
+                displayText(for: substep, decimalValue: decimalValue.wrappedValue)
+                    .foregroundColor(!displayPicker.wrappedValue ? .primary : .accentColor)
+                    .onTapGesture {
+                        displayPicker.wrappedValue.toggle()
+                    }
+            }
+
+            if displayPicker.wrappedValue {
+                Picker(selection: decimalValue, label: Text(label)) {
+                    ForEach(settingsProvider.generatePickerValues(from: setting, units: state.units), id: \.self) { value in
+                        displayText(for: substep, decimalValue: value).tag(value)
+                    }
+                }
+                .pickerStyle(WheelPickerStyle())
+                .frame(maxWidth: .infinity)
+            }
+        }
+        .padding()
+        .background(Color.chart.opacity(0.65))
+        .cornerRadius(10)
+    }
+
+    private func displayText(for substep: DeliveryLimitSubstep, decimalValue: Decimal) -> Text {
+        switch substep {
+        case .maxBasal:
+            return Text("\(decimalValue) \(String(localized: "U/hr", comment: "Insulin unit per hour abbreviation"))")
+        case .maxBolus,
+             .maxIOB:
+            return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
+        case .maxCOB:
+            return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
+        case .minimumSafetyThreshold:
+            let optionallyParsedValue = state.units == .mgdL ? decimalValue : decimalValue.asMmolL
+            return Text("\(optionallyParsedValue) \(state.units.rawValue)")
+        }
+    }
+}

+ 58 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DiagnosticsStepView.swift

@@ -0,0 +1,58 @@
+import SwiftUI
+
+struct DiagnosticsStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("If you prefer not to share this anonymized data, you can opt-out of data sharing.")
+                .font(.headline)
+                .padding(.horizontal)
+                .multilineTextAlignment(.leading)
+
+            ForEach(DiagnosticsSharingOption.allCases, id: \.self) { option in
+                Button(action: {
+                    state.diagnosticsSharingOption = option
+                }) {
+                    HStack {
+                        Image(systemName: state.diagnosticsSharingOption == option ? "largecircle.fill.circle" : "circle")
+                            .foregroundColor(state.diagnosticsSharingOption == option ? .accentColor : .secondary)
+                            .imageScale(.large)
+
+                        Text(option.displayName)
+                            .foregroundColor(.primary)
+
+                        Spacer()
+                    }
+                    .padding()
+                    .background(Color.chart.opacity(0.65))
+                    .cornerRadius(10)
+                }
+                .buttonStyle(.plain)
+            }
+
+            VStack(alignment: .leading, spacing: 8) {
+                Text("Why does Trio collect this data?").bold()
+                VStack(alignment: .leading, spacing: 4) {
+                    Text(
+                        "•  App diagnostic insights help us enhance app stability, ensure safety for all users, and enable us to quickly identify and resolve critical issues."
+                    )
+                    Text("•  Trio collects the app's state on crash, device, iOS and general system info, and a stack trace.")
+                    Text(
+                        "•  Trio does not collect any health related data, e.g. glucose readings, insulin rates or doses, meal data, setting values, or similar."
+                    )
+                    Text(
+                        "•  Trio does not track any usage metrics or any other personal data about users other than the used iPhone model and iOS version."
+                    )
+                }
+                Text(
+                    "Diagnostics are sent to a Google Firebase Crashlytics project, which is securely maintained and accessed only by the Trio team."
+                )
+            }
+            .multilineTextAlignment(.leading)
+            .padding(.horizontal)
+            .font(.footnote)
+            .foregroundStyle(Color.secondary)
+        }
+    }
+}

+ 128 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -0,0 +1,128 @@
+//
+//  GlucoseTargetStepView.swift
+//  Trio
+//
+//  Created by Marvin Polscheit on 19.03.25.
+//
+import Charts
+import Foundation
+import SwiftUI
+import UIKit
+
+/// Glucose target step view for setting target glucose range.
+struct GlucoseTargetStepView: View {
+    @Bindable var state: Onboarding.StateModel
+    @State private var refreshUI = UUID() // to update chart when slider value changes
+    @State private var therapyItems: [TherapySettingItem] = []
+    @State private var now = Date()
+
+    // Formatter for glucose values
+    private var numberFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
+        return formatter
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        LazyVStack {
+            VStack(alignment: .leading, spacing: 0) {
+                // Chart visualization
+                if !state.targetItems.isEmpty {
+                    VStack(alignment: .leading) {
+                        glucoseTargetChart
+                            .frame(height: 180)
+                            .padding(.horizontal)
+                    }
+                    .padding(.vertical)
+                    .background(Color.chart.opacity(0.65))
+                    .clipShape(
+                        .rect(
+                            topLeadingRadius: 10,
+                            bottomLeadingRadius: 0,
+                            bottomTrailingRadius: 0,
+                            topTrailingRadius: 10
+                        )
+                    )
+                }
+
+                // Glucose target list
+                TherapySettingEditorView(
+                    items: $therapyItems,
+                    unit: state.units == .mgdL ? .mgdL : .mmolL,
+                    timeOptions: state.targetTimeValues,
+                    valueOptions: state.targetRateValues,
+                    validateOnDelete: state.validateTarget
+                )
+            }
+        }
+        .onAppear {
+            if state.targetItems.isEmpty {
+                state.addInitialTarget()
+            }
+            state.validateTarget()
+            therapyItems = state.getTargetTherapyItems()
+        }.onChange(of: therapyItems) { _, newItems in
+            state.updateTargets(from: newItems)
+            refreshUI = UUID()
+        }
+    }
+
+    // Chart for visualizing glucose targets
+    private var glucoseTargetChart: some View {
+        Chart {
+            ForEach(Array(state.targetItems.enumerated()), id: \.element.id) { index, item in
+                let rawValue = state.targetRateValues[item.lowIndex]
+                let displayValue = state.units == .mgdL ? rawValue : rawValue.asMmolL
+
+                let startDate = Calendar.current
+                    .startOfDay(for: now)
+                    .addingTimeInterval(state.targetTimeValues[item.timeIndex])
+
+                var offset: TimeInterval {
+                    if state.targetItems.count > index + 1 {
+                        return state.targetTimeValues[state.targetItems[index + 1].timeIndex]
+                    } else {
+                        return state.targetTimeValues.last! + 30 * 60
+                    }
+                }
+
+                let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
+                LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
+
+                LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
+            }
+        }
+        .id(refreshUI) // Force chart update
+        .chartXAxis {
+            AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                AxisValueLabel(format: .dateTime.hour())
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+        .chartXScale(
+            domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                .addingTimeInterval(60 * 60 * 24)
+        )
+        .chartYAxis {
+            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                AxisValueLabel()
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+        .chartYScale(
+            domain: (state.units == .mgdL ? Decimal(72) : Decimal(72).asMmolL) ...
+                (state.units == .mgdL ? Decimal(180) : Decimal(180).asMmolL)
+        )
+    }
+}

+ 204 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/InsulinSensitivityStepView.swift

@@ -0,0 +1,204 @@
+//
+//  InsulinSensitivityStepView.swift
+//  Trio
+//
+//  Created by Marvin Polscheit on 19.03.25.
+//
+import Charts
+import SwiftUI
+import UIKit
+
+/// Insulin sensitivity step view for setting insulin sensitivity factor.
+struct InsulinSensitivityStepView: View {
+    @Bindable var state: Onboarding.StateModel
+    @State private var refreshUI = UUID() // to update chart when slider value changes
+    @State private var therapyItems: [TherapySettingItem] = []
+    @State private var now = Date()
+
+    // For chart scaling
+    private let chartScale = Calendar.current
+        .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
+    private var numberFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
+        return formatter
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        LazyVStack {
+            VStack(alignment: .leading, spacing: 0) {
+                // Chart visualization
+                if !state.isfItems.isEmpty {
+                    VStack(alignment: .leading) {
+                        isfChart
+                            .frame(height: 180)
+                            .padding(.horizontal)
+                    }
+                    .padding(.vertical)
+                    .background(Color.chart.opacity(0.65))
+                    .clipShape(
+                        .rect(
+                            topLeadingRadius: 10,
+                            bottomLeadingRadius: 0,
+                            bottomTrailingRadius: 0,
+                            topTrailingRadius: 10
+                        )
+                    )
+                }
+
+                TherapySettingEditorView(
+                    items: $therapyItems,
+                    unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
+                    timeOptions: state.isfTimeValues,
+                    valueOptions: state.isfRateValues,
+                    validateOnDelete: state.validateISF
+                )
+
+                // Example calculation based on first ISF
+                if !state.isfItems.isEmpty {
+                    Spacer(minLength: 20)
+
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Example Calculation")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        VStack(alignment: .leading, spacing: 8) {
+                            // Current glucose is 40 mg/dL or 2.2 mmol/L above target
+                            let aboveTarget = state.units == .mgdL ? Decimal(40) : 40.asMmolL
+
+                            let isfValue = state.isfRateValues.isEmpty || state.isfItems.isEmpty ?
+                                Double(truncating: 50 as NSNumber) :
+                                Double(
+                                    truncating: state
+                                        .isfRateValues[state.isfItems.first!.rateIndex] as NSNumber
+                                )
+
+                            let insulinNeeded = aboveTarget / Decimal(isfValue)
+
+                            Text(
+                                "If you are \(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") \(state.units.rawValue) above target:"
+                            )
+                            .font(.subheadline)
+                            .padding(.horizontal)
+
+                            Text(
+                                "\(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") / \(numberFormatter.string(from: isfValue as NSNumber) ?? "--") = \(String(format: "%.1f", Double(insulinNeeded)))" +
+                                    " " + String(localized: "U", comment: "Insulin unit abbreviation")
+                            )
+                            .font(.system(.body, design: .monospaced))
+                            .foregroundColor(.red)
+                            .padding()
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .background(Color.chart.opacity(0.65))
+                            .cornerRadius(10)
+                        }
+                    }
+
+                    Spacer(minLength: 20)
+
+                    // Information about ISF
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("What This Means")
+                            .font(.headline)
+                            .padding(.horizontal)
+
+                        VStack(alignment: .leading, spacing: 4) {
+                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
+                                "\(state.units.rawValue)"
+                            Text(
+                                "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
+                            )
+                            Text("• A lower number means you're more sensitive to insulin")
+                            Text("• A higher number means you're less sensitive to insulin")
+                        }
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                        .padding(.horizontal)
+                    }
+                }
+            }
+        }
+        .onAppear {
+            if state.isfItems.isEmpty {
+                state.addInitialISF()
+            }
+            state.validateISF()
+            therapyItems = state.getISFTherapyItems()
+        }.onChange(of: therapyItems) { _, newItems in
+            state.updateISF(from: newItems)
+            refreshUI = UUID()
+        }
+    }
+
+    // Chart for visualizing ISF profile
+    private var isfChart: some View {
+        Chart {
+            ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
+                let displayValue = state.isfRateValues[item.rateIndex]
+
+                let startDate = Calendar.current
+                    .startOfDay(for: now)
+                    .addingTimeInterval(state.isfTimeValues[item.timeIndex])
+
+                var offset: TimeInterval {
+                    if state.isfItems.count > index + 1 {
+                        return state.isfTimeValues[state.isfItems[index + 1].timeIndex]
+                    } else {
+                        return state.isfTimeValues.last! + 30 * 60
+                    }
+                }
+
+                let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
+                RectangleMark(
+                    xStart: .value("start", startDate),
+                    xEnd: .value("end", endDate),
+                    yStart: .value("rate-start", displayValue),
+                    yEnd: .value("rate-end", 0)
+                ).foregroundStyle(
+                    .linearGradient(
+                        colors: [
+                            Color.red.opacity(0.6),
+                            Color.red.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
+                    )
+                ).alignsMarkStylesWithPlotArea()
+
+                LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.red)
+
+                LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
+                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.red)
+            }
+        }
+        .id(refreshUI) // Force chart update
+        .chartXAxis {
+            AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                AxisValueLabel(format: .dateTime.hour())
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+        .chartXScale(
+            domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                .addingTimeInterval(60 * 60 * 24)
+        )
+        .chartYAxis {
+            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
+                AxisValueLabel()
+                AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+            }
+        }
+    }
+}

+ 42 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/LogoAnimation.swift

@@ -0,0 +1,42 @@
+//
+//  LogoAnimation.swift
+//  Trio
+//
+//  Created by Marvin Polscheit on 11.04.25.
+//
+import SwiftUI
+
+struct PulsingLogoAnimation: View {
+    @State private var scale = 0.5
+    @State private var opacity = 0.0
+    @State private var rotation = 0.0
+    @State private var isPulsing = false
+
+    var body: some View {
+        Image("trioCircledNoBackground")
+            .resizable()
+            .scaledToFit()
+            .frame(height: 100)
+            .scaleEffect(scale)
+            .opacity(opacity)
+            .rotationEffect(.degrees(rotation))
+            .scaleEffect(isPulsing ? 1.1 : 1.0)
+            .onAppear {
+                withAnimation(.easeInOut(duration: 1.0)) {
+                    scale = 1.0
+                    opacity = 1.0
+                    rotation = 360
+                }
+
+                withAnimation(.easeInOut(duration: 1.0).repeatForever()) {
+                    isPulsing.toggle()
+                }
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
+                    withAnimation(.easeOut(duration: 1.0)) {
+                        isPulsing = false
+                    }
+                }
+            }
+    }
+}

+ 86 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutImportStepView.swift

@@ -0,0 +1,86 @@
+import SwiftUI
+
+struct NightscoutImportStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    @State var importAlert: Alert?
+    @State var isImportAlertPresented: Bool = false
+
+    var body: some View {
+        ZStack {
+            if state.nightscoutImportStatus == .running {
+                VStack(alignment: .center) {
+                    Spacer(minLength: 150)
+                    CustomProgressView(
+                        text: String(
+                            localized: "Importing Settings...",
+                            comment: "Progress text when importing settings via Nightscout"
+                        )
+                    )
+                }
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+                .background(Color.clear)
+            } else {
+                VStack(alignment: .leading, spacing: 20) {
+                    Text(
+                        "Please choose if you want to import existing therapy settings from Nightscout or start from scratch."
+                    )
+                    .font(.headline)
+                    .padding(.horizontal)
+                    .multilineTextAlignment(.leading)
+
+                    ForEach([NightscoutImportOption.useImport, NightscoutImportOption.skipImport], id: \.self) { option in
+                        Button(action: {
+                            state.nightscoutImportOption = option
+                        }) {
+                            HStack {
+                                Image(systemName: state.nightscoutImportOption == option ? "largecircle.fill.circle" : "circle")
+                                    .foregroundColor(state.nightscoutImportOption == option ? .accentColor : .secondary)
+                                    .imageScale(.large)
+
+                                Text(option.displayName)
+                                    .foregroundColor(.primary)
+
+                                Spacer()
+                            }
+                            .padding()
+                            .background(Color.chart.opacity(0.65))
+                            .cornerRadius(10)
+                        }
+                        .buttonStyle(.plain)
+                    }
+
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text("Trio will import the following therapy settings from your Nightscout instance:")
+                        VStack(alignment: .leading) {
+                            Text("• Glucose Targets")
+                            Text("• Basal Rates")
+                            Text("• Carb Ratios")
+                            Text("• Insulin Sensitivities")
+                        }
+                    }
+                    .padding(.horizontal)
+                    .font(.footnote)
+                    .foregroundStyle(Color.secondary)
+                    .multilineTextAlignment(.leading)
+                }
+            }
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .alert(isPresented: $isImportAlertPresented) {
+            if state.nightscoutImportStatus == .failed, state.nightscoutImportErrors.isNotEmpty,
+               let errorMessage = state.nightscoutImportErrors.first
+            {
+                DispatchQueue.main.async {
+                    importAlert = Alert(
+                        title: Text("Import Failed"),
+                        message: Text(errorMessage.description),
+                        dismissButton: .default(Text("OK"))
+                    )
+                    isImportAlertPresented = true
+                }
+            }
+            return importAlert ?? Alert(title: Text("Unknown Error"))
+        }
+    }
+}

+ 78 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutLoginStepView.swift

@@ -0,0 +1,78 @@
+import SwiftUI
+
+struct NightscoutLoginStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Please enter your credentials:")
+                .font(.headline)
+                .padding(.horizontal)
+
+            HStack {
+                TextField("URL", text: $state.nightscoutUrl)
+                    .disableAutocorrection(true)
+                    .textContentType(.URL)
+                    .autocapitalization(.none)
+                    .keyboardType(.URL)
+                if state.nightscoutResponseMessage.isNotEmpty && !state.isValidNightscoutURL {
+                    Image(systemName: "exclamationmark.triangle.fill")
+                        .foregroundStyle(.orange)
+                }
+            }.padding()
+                .background(Color.chart.opacity(0.65))
+                .cornerRadius(10)
+
+            HStack {
+                SecureField("API secret", text: $state.nightscoutSecret)
+                    .disableAutocorrection(true)
+                    .autocapitalization(.none)
+                    .textContentType(.password)
+                    .keyboardType(.asciiCapable)
+            }.padding()
+                .background(Color.chart.opacity(0.65))
+                .cornerRadius(10)
+
+            Spacer(minLength: 10)
+
+            Button(action: {
+                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                impactHeavy.impactOccurred()
+                state.connectToNightscout()
+            }) {
+                HStack {
+                    if state.isConnectingToNS {
+                        ProgressView().padding(.trailing, 10)
+                    }
+                    Text(state.isConnectingToNS ? "Connecting..." : "Connect to Nightscout")
+                        .bold()
+                }
+                .frame(maxWidth: .infinity, alignment: .center)
+                .padding(.vertical, 8)
+            }
+            .disabled(state.isConnectedToNS || state.nightscoutUrl.isEmpty || state.nightscoutSecret.isEmpty)
+            .buttonStyle(.borderedProminent)
+
+            if state.nightscoutResponseMessage.isNotEmpty {
+                VStack(alignment: .center) {
+                    Text(state.nightscoutResponseMessage)
+                        .font(.subheadline)
+                        .foregroundStyle(Color.orange)
+                }
+            } else if state.isConnectedToNS {
+                HStack {
+                    Spacer()
+                    Text("Connected")
+                        .font(.subheadline)
+                        .foregroundColor(.green)
+                    ZStack {
+                        Image(systemName: "network")
+                        Image(systemName: "checkmark.circle.fill").foregroundColor(.green).font(.caption2)
+                            .offset(x: 9, y: 6)
+                    }
+                    Spacer()
+                }
+            }
+        }
+    }
+}

+ 35 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutSetupStepView.swift

@@ -0,0 +1,35 @@
+import SwiftUI
+
+struct NightscoutSetupStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Nightscout use is entirely optional. You can also setup Nightscout at a later time.")
+                .font(.headline)
+                .padding(.horizontal)
+                .multilineTextAlignment(.leading)
+
+            ForEach([NightscoutSetupOption.setupNightscout, NightscoutSetupOption.skipNightscoutSetup], id: \.self) { option in
+                Button(action: {
+                    state.nightscoutSetupOption = option
+                }) {
+                    HStack {
+                        Image(systemName: state.nightscoutSetupOption == option ? "largecircle.fill.circle" : "circle")
+                            .foregroundColor(state.nightscoutSetupOption == option ? .accentColor : .secondary)
+                            .imageScale(.large)
+
+                        Text(option.displayName)
+                            .foregroundColor(.primary)
+
+                        Spacer()
+                    }
+                    .padding()
+                    .background(Color.chart.opacity(0.65))
+                    .cornerRadius(10)
+                }
+                .buttonStyle(.plain)
+            }
+        }
+    }
+}

+ 29 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OverviewStepView.swift

@@ -0,0 +1,29 @@
+//
+//  OverviewStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 06.04.25.
+//
+import SwiftUI
+
+struct OverviewStepView: View {
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Here is an overview of what to expect:")
+                .font(.headline)
+                .padding(.horizontal)
+
+            VStack(alignment: .center, spacing: 12) {
+                ForEach(
+                    nonInfoOnboardingSteps,
+                    id: \.self
+                ) { step in
+                    SettingItemView(step: step, icon: step.iconName, title: step.title, type: .overview)
+                }
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+        }
+    }
+}

+ 47 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuideStepView.swift

@@ -0,0 +1,47 @@
+//
+//  StartupGuideStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 06.04.25.
+//
+import SwiftUI
+
+struct StartupGuideStepView: View {
+    @Environment(\.openURL) var openURL
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Before you begin…")
+                .font(.title.bold())
+                .padding(.horizontal)
+
+            VStack(alignment: .leading, spacing: 10) {
+                BulletPoint(String(localized: "Take a deep breath — you've got this."))
+                BulletPoint(String(localized: "There's no rush. Take all the time you need."))
+                BulletPoint(String(localized: "Everything you enter here can be adjusted later in the app."))
+                BulletPoint(String(localized: "Want a hand? You can open our full Startup Guide here:"))
+
+                Button {
+                    openURL(URL(string: "https://triodocs.org/startup-guide")!)
+                } label: {
+                    Text("https://triodocs.org/startup-guide")
+                        .padding(.horizontal, 12)
+                        .padding(.vertical, 8)
+                        .background(Color.blue.opacity(0.2))
+                        .cornerRadius(8)
+                }
+                .frame(maxWidth: .infinity, alignment: .center)
+                .padding([.top, .horizontal])
+            }.padding(.horizontal)
+
+            HStack {
+                Text("You can pause at any time. Just be aware: if you ")
+                    + Text("force quit").bold()
+                    + Text(" the app before finishing onboarding, ")
+                    + Text("your progress will not be saved.").bold()
+            }
+            .multilineTextAlignment(.leading)
+            .padding(.horizontal)
+        }
+    }
+}

+ 51 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/UnitSelectionStepView.swift

@@ -0,0 +1,51 @@
+import SwiftUI
+
+struct UnitSelectionStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Please choose from the options below.")
+                .font(.headline)
+                .padding(.horizontal)
+
+            HStack {
+                Text("Glucose Units")
+                Spacer()
+                Picker("Glucose Units", selection: $state.units) {
+                    ForEach(GlucoseUnits.allCases, id: \.self) { unit in
+                        Text(unit.rawValue).tag(unit)
+                    }
+                }
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+
+            HStack {
+                Text("Pump Model")
+                Spacer()
+                Picker("Pump Model", selection: $state.pumpOptionForOnboardingUnits) {
+                    ForEach(PumpOptionForOnboardingUnits.allCases, id: \.self) { pumpModel in
+                        Text(pumpModel.displayName).tag(pumpModel)
+                    }
+                }
+                .onChange(of: state.pumpOptionForOnboardingUnits, { _, _ in
+                    // Reset basal profile and related values when pump model changes
+                    state.basalProfileItems = []
+                })
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+
+            Text(
+                "Note: Choosing your pump model determines which increments for setting up your basal rates are available. You will pair your actual pump after finishing the onboarding process."
+            )
+            .padding(.horizontal)
+            .font(.footnote)
+            .foregroundStyle(Color.secondary)
+            .multilineTextAlignment(.leading)
+        }
+    }
+}

+ 36 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift

@@ -0,0 +1,36 @@
+import SwiftUI
+
+/// Welcome step view shown at the beginning of onboarding.
+struct WelcomeStepView: View {
+    var body: some View {
+        VStack(alignment: .center, spacing: 20) {
+            PulsingLogoAnimation()
+
+            Spacer(minLength: 10)
+
+            Text("Hi there!")
+                .font(.title2)
+                .fontWeight(.bold)
+                .multilineTextAlignment(.center)
+
+            Text(
+                "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
+            )
+            .multilineTextAlignment(.center)
+            .foregroundColor(.secondary)
+
+            Text(
+                "Trio is designed to help manage your diabetes efficiently. To get the most out of the app, we'll guide you through setting up some essential parameters."
+            )
+            .multilineTextAlignment(.center)
+            .foregroundColor(.secondary)
+
+            Text("Let's go through a few quick steps to ensure Trio works optimally for you.")
+                .multilineTextAlignment(.center)
+                .foregroundColor(.primary)
+                .bold()
+        }
+        .padding()
+        .frame(maxWidth: .infinity)
+    }
+}

+ 464 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -0,0 +1,464 @@
+import SwiftUI
+
+/// Represents the navigation direction in the onboarding flow
+enum OnboardingNavigationDirection {
+    case forward
+    case backward
+}
+
+/// Represents the different steps in the onboarding process.
+enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
+    case welcome
+    case startupGuide
+    case overview
+    case diagnostics
+    case nightscout
+    case unitSelection
+    case glucoseTarget
+    case basalRates
+    case carbRatio
+    case insulinSensitivity
+    case deliveryLimits
+    case completed
+
+    var id: Int { rawValue }
+
+    var hasSubsteps: Bool {
+        self == .deliveryLimits
+    }
+
+    var substeps: [DeliveryLimitSubstep] {
+        guard hasSubsteps else { return [] }
+        return DeliveryLimitSubstep.allCases
+    }
+
+    /// The title to display for this onboarding step.
+    var title: String {
+        switch self {
+        case .welcome:
+            return String(localized: "Welcome to Trio")
+        case .startupGuide:
+            return String(localized: "Startup Guide")
+        case .overview:
+            return String(localized: "Overview")
+        case .diagnostics:
+            return String(localized: "Diagnostics")
+        case .nightscout:
+            return String(localized: "Nightscout")
+        case .unitSelection:
+            return String(localized: "Units & Pump")
+        case .glucoseTarget:
+            return String(localized: "Glucose Targets")
+        case .basalRates:
+            return String(localized: "Basal Rates")
+        case .carbRatio:
+            return String(localized: "Carb Ratios")
+        case .insulinSensitivity:
+            return String(localized: "Insulin Sensitivities")
+        case .deliveryLimits:
+            return String(localized: "Delivery Limits")
+        case .completed:
+            return String(localized: "All Set!")
+        }
+    }
+
+    /// A detailed description of what this onboarding step is about.
+    var description: String {
+        switch self {
+        case .welcome:
+            return String(
+                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."
+            )
+        case .startupGuide:
+            return String(
+                localized: "Trio comes with a helpful Startup Guide. We recommend opening it now and following along as you go — side by side."
+            )
+        case .overview:
+            return String(
+                localized: "Trio's Onboarding consists of several steps. It takes about 5-10 minutes to complete. We'll guide you through each step."
+            )
+        case .diagnostics:
+            return String(
+                localized: "By default, Trio collects crash reports and other anonymized data related to errors, exceptions, and overall app performance."
+            )
+        case .nightscout:
+            return String(
+                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."
+            )
+        case .unitSelection:
+            return String(
+                localized: "Before you can begin with configuring your therapy settigns, Trio needs to know which units you use for your glucose and insulin measurements (based on your pump model)."
+            )
+        case .glucoseTarget:
+            return String(
+                localized: "Your glucose target is the blood glucose level you aim to maintain. Trio will use this to calculate insulin doses and provide recommendations."
+            )
+        case .basalRates:
+            return String(
+                localized: "Your basal profile represents the amount of background insulin you need throughout the day. This helps Trio calculate your insulin needs."
+            )
+        case .carbRatio:
+            return String(
+                localized: "Your carb ratio tells how many grams of carbohydrates one unit of insulin will cover. This is essential for accurate meal bolus calculations."
+            )
+        case .insulinSensitivity:
+            return String(
+                localized: "Your insulin sensitivity factor (ISF) indicates how much one unit of insulin will lower your blood glucose. This helps calculate correction boluses."
+            )
+        case .deliveryLimits:
+            return String(
+                localized: "Trio includes several safety limits for insulin delivery and carbohydrate entry, helping ensure a safe and effective experience."
+            )
+        case .completed:
+            return String(
+                localized: "Great job! You've completed the initial setup of Trio. You can always adjust these settings later in the app."
+            )
+        }
+    }
+
+    /// The system icon name associated with this step.
+    var iconName: String {
+        switch self {
+        case .welcome:
+            return "hand.wave.fill"
+        case .startupGuide:
+            return "list.bullet.clipboard.fill"
+        case .overview:
+            return "checklist.unchecked"
+        case .diagnostics:
+            return "waveform.badge.magnifyingglass"
+        case .nightscout:
+            return "owl"
+        case .unitSelection:
+            return "numbers.rectangle"
+        case .glucoseTarget:
+            return "target"
+        case .basalRates:
+            return "chart.xyaxis.line"
+        case .carbRatio:
+            return "fork.knife"
+        case .insulinSensitivity:
+            return "drop.fill"
+        case .deliveryLimits:
+            return "slider.horizontal.3"
+        case .completed:
+            return "checkmark.circle.fill"
+        }
+    }
+
+    /// Returns the next step in the onboarding process, or nil if this is the last step.
+    var next: OnboardingStep? {
+        let allCases = OnboardingStep.allCases
+        let currentIndex = allCases.firstIndex(of: self) ?? 0
+        let nextIndex = currentIndex + 1
+        return nextIndex < allCases.count ? allCases[nextIndex] : nil
+    }
+
+    /// Returns the previous step in the onboarding process, or nil if this is the first step.
+    var previous: OnboardingStep? {
+        let allCases = OnboardingStep.allCases
+        let currentIndex = allCases.firstIndex(of: self) ?? 0
+        let previousIndex = currentIndex - 1
+        return previousIndex >= 0 ? allCases[previousIndex] : nil
+    }
+
+    /// The accent color to use for this step.
+    var accentColor: Color {
+        switch self {
+        case .completed,
+             .deliveryLimits,
+             .diagnostics,
+             .nightscout,
+             .overview,
+             .startupGuide,
+             .unitSelection,
+             .welcome:
+            return Color.blue
+        case .glucoseTarget:
+            return Color.green
+        case .basalRates:
+            return Color.purple
+        case .carbRatio:
+            return Color.orange
+        case .insulinSensitivity:
+            return Color.red
+        }
+    }
+}
+
+var nonInfoOnboardingSteps: [OnboardingStep] { OnboardingStep.allCases
+    .filter { $0 != .welcome && $0 != .startupGuide && $0 != .overview && $0 != .completed }
+}
+
+enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
+    case maxIOB
+    case maxBolus
+    case maxBasal
+    case maxCOB
+    case minimumSafetyThreshold
+
+    var id: Int { rawValue }
+
+    var title: String {
+        switch self {
+        case .maxIOB: return String(localized: "Max IOB", comment: "Max IOB")
+        case .maxBolus: return String(localized: "Max Bolus")
+        case .maxBasal: return String(localized: "Max Basal Rate")
+        case .maxCOB: return String(localized: "Max COB", comment: "Max COB")
+        case .minimumSafetyThreshold: return String(localized: "Minimum Safety Threshold")
+        }
+    }
+
+    var hint: String {
+        switch self {
+        case .maxIOB: return String(localized: "Maximum units of insulin allowed to be active.")
+        case .maxBolus: return String(localized: "Largest bolus of insulin allowed.")
+        case .maxBasal: return String(localized: "Largest basal rate allowed.")
+        case .maxCOB: return String(localized: "Maximum Carbs On Board (COB) allowed.")
+        case .minimumSafetyThreshold: return String(localized: "Increase the safety threshold used to suspend insulin delivery.")
+        }
+    }
+
+    func description(units: GlucoseUnits) -> any View {
+        switch self {
+        case .maxIOB:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio."
+                ).bold().foregroundStyle(Color.primary)
+
+                Text(
+                    "This is the maximum amount of Insulin On Board (IOB) above profile basal rates from all sources - positive temporary basal rates, manual or meal boluses, and SMBs - that Trio is allowed to accumulate to address an above target glucose."
+                )
+                Text(
+                    "If a calculated amount exceeds this limit, the suggested and / or delivered amount will be reduced so that active insulin on board (IOB) will not exceed this safety limit."
+                )
+                Text(
+                    "Note: You can still manually bolus above this limit, but the suggested bolus amount will never exceed this in the bolus calculator."
+                )
+            }
+        case .maxBolus:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus."
+                )
+                Text("Most set this to their largest meal bolus. Then, adjust if needed.")
+                Text("If you attempt to request a bolus larger than this, the bolus will not be accepted.")
+            }
+        case .maxBasal:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
+                )
+                Text(
+                    "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
+                )
+            }
+        case .maxCOB:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text(
+                    "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."
+                )
+                Text(
+                    "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."
+                )
+                Text("This is an important limit when UAM is ON.")
+            }
+        case .minimumSafetyThreshold:
+            return VStack(alignment: .leading, spacing: 8) {
+                Text("Default: Set by Algorithm").bold()
+                Text(
+                    "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."
+                )
+                Text(
+                    "Trio will use the larger of the default setting calculation below and the value entered here."
+                )
+                VStack(alignment: .leading, spacing: 8) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text("The default setting is based on this calculation:").bold()
+                        Text("TargetGlucose - 0.5 × (TargetGlucose - 40)")
+                    }
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "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)."
+                        )
+                        Text(
+                            "\(units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(units == .mgdL ? "110" : 110.formattedAsMmolL) - \(units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(units == .mgdL ? "75" : 75.formattedAsMmolL)"
+                        )
+                    }
+                    Text(
+                        "This setting is limited to values between \(units == .mgdL ? "60" : 60.formattedAsMmolL) - \(units == .mgdL ? "120" : 120.formattedAsMmolL) \(units.rawValue)"
+                    )
+                    Text(
+                        "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
+                    )
+                }
+            }
+        }
+    }
+}
+
+enum DiagnosticsSharingOption: String, Equatable, CaseIterable, Identifiable {
+    case enabled
+    case disabled
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .enabled:
+            return "Enable Sharing"
+        case .disabled:
+            return "Disable Sharing"
+        }
+    }
+}
+
+enum PumpOptionForOnboardingUnits: String, Equatable, CaseIterable, Identifiable {
+    case minimed
+    case omnipodEros
+    case omnipodDash
+    case dana
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .minimed:
+            return "Medtronic"
+        case .omnipodEros:
+            return "Omnipod Eros"
+        case .omnipodDash:
+            return "Omnipod Dash"
+        case .dana:
+            return "Dana (RS/-i)"
+        }
+    }
+}
+
+enum NightscoutSetupOption: String, Equatable, CaseIterable, Identifiable {
+    case setupNightscout
+    case skipNightscoutSetup
+    case noSelection
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .setupNightscout:
+            return String(localized: "Setup Nightscout for Trio")
+        case .skipNightscoutSetup:
+            return String(localized: "Skip Nightscout Setup")
+        case .noSelection:
+            return ""
+        }
+    }
+}
+
+enum NightscoutImportOption: String, Equatable, CaseIterable, Identifiable {
+    case useImport
+    case skipImport
+    case noSelection
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .useImport:
+            return String(localized: "Import Settings")
+        case .skipImport:
+            return String(localized: "Configure Yourself")
+        case .noSelection:
+            return ""
+        }
+    }
+}
+
+enum NightscoutSubstep: Int, CaseIterable, Identifiable {
+    case setupSelection
+    case connectToNightscout
+    case importFromNightscout
+
+    var id: Int { rawValue }
+}
+
+struct BulletPoint: View {
+    let text: String
+
+    init(_ text: String) {
+        self.text = text
+    }
+
+    var body: some View {
+        HStack(alignment: .top) {
+            Text("•")
+            Text(text)
+        }
+    }
+}
+
+enum OnboardingSettingItemType: Equatable, CaseIterable, Identifiable {
+    case overview
+    case complete
+
+    var id: UUID {
+        UUID()
+    }
+}
+
+/// A reusable view for displaying setting items in the completed step.
+struct SettingItemView: View {
+    let step: OnboardingStep
+    let icon: String
+    let title: String
+    let type: OnboardingSettingItemType
+
+    private var accentColor: Color {
+        switch type {
+        case .overview:
+            Color.blue
+        case .complete:
+            Color.green
+        }
+    }
+
+    var body: some View {
+        HStack(spacing: 10) {
+            if step == .nightscout {
+                Image(icon)
+                    .resizable()
+                    .scaledToFit()
+                    .frame(width: 40, height: 24)
+                    .colorMultiply(accentColor)
+            } else {
+                Image(systemName: icon)
+                    .font(.system(size: 24))
+                    .foregroundStyle(accentColor)
+                    .frame(width: 40)
+            }
+
+            VStack(alignment: .leading, spacing: 2) {
+                Text(title)
+                    .font(.headline)
+            }
+
+            Spacer()
+
+            switch type {
+            case .overview:
+                let index = nonInfoOnboardingSteps.firstIndex(of: step) ?? 0
+                let stepNumber = index + 1
+                Text(stepNumber.description)
+                    .bold()
+                    .frame(width: 32, height: 32, alignment: .center)
+                    .background(accentColor)
+                    .foregroundStyle(.white)
+                    .clipShape(Capsule())
+            case .complete:
+                Image(systemName: "checkmark")
+                    .foregroundStyle(accentColor)
+            }
+        }
+        .padding(.vertical, 8)
+    }
+}

+ 279 - 0
Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift

@@ -0,0 +1,279 @@
+import SwiftUI
+
+struct TherapySettingEditorView: View {
+    @Binding var items: [TherapySettingItem]
+    var unit: TherapySettingUnit
+    var timeOptions: [TimeInterval]
+    var valueOptions: [Decimal]
+    var validateOnDelete: (() -> Void)?
+
+    @State private var selectedItemID: UUID?
+
+    var body: some View {
+        List {
+            HStack {
+                Text("Entries").bold()
+                Spacer()
+                Button {
+                    // Prepare and add new entry
+                    let lastTime = items.last?.time ?? 0
+                    let newTime = min(lastTime + 1800, 23 * 3600 + 1800)
+                    let newValue = items.last?.value ?? 1.0
+                    items.append(TherapySettingItem(time: newTime, value: newValue))
+
+                    // Reset selected item to close picker
+                    selectedItemID = nil
+
+                    // Sort items, in case user has changed time of one item, then taps 'Add'
+                    sortTherapyItems()
+                } label: {
+                    HStack {
+                        Image(systemName: "plus.circle.fill")
+                        Text("Add")
+                    }.foregroundColor(.accentColor)
+                }
+                .disabled(items.count >= 48)
+            }
+            .listRowBackground(Color.chart.opacity(0.65))
+            .padding(.vertical, 5)
+
+            ForEach($items) { $item in
+                VStack(spacing: 0) {
+                    Button {
+                        selectedItemID = selectedItemID == item.id ? nil : item.id
+                        sortTherapyItems()
+                    } label: {
+                        HStack {
+                            HStack {
+                                Text(displayText(for: unit, decimalValue: item.value))
+                                    .foregroundStyle(
+                                        selectedItemID == item.id ? Color.accentColor : Color
+                                            .primary
+                                    )
+                                Text(unit.displayName)
+                                    .foregroundStyle(Color.secondary)
+                            }
+
+                            Spacer()
+
+                            HStack {
+                                Text("starts at").foregroundStyle(Color.secondary)
+                                let timeIndex = timeOptions.firstIndex { abs($0 - item.time) < 1 } ?? 0
+                                let time = timeOptions[timeIndex]
+                                let date = Date(timeIntervalSince1970: time)
+                                let timeString = timeFormatter.string(from: date)
+                                Text(timeString).foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
+                            }
+                        }
+                        .contentShape(Rectangle())
+                    }
+                    .buttonStyle(.plain)
+
+                    if selectedItemID == item.id {
+                        timeValuePickerRow(
+                            item: $item,
+                            timeOptions: timeOptions,
+                            valueOptions: valueOptions,
+                            unit: unit
+                        )
+                        .transition(.slide)
+                    }
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
+                        Button(role: .destructive) {
+                            items.remove(at: index)
+                            selectedItemID = nil
+                            validateTherapySettingItems()
+                        } label: {
+                            Label("Delete", systemImage: "trash")
+                        }
+                    }
+                }
+            }
+            .listRowBackground(Color.chart.opacity(0.65))
+
+            Rectangle().fill(Color.chart.opacity(0.65)).frame(height: 10)
+                .clipShape(
+                    .rect(
+                        topLeadingRadius: 0,
+                        bottomLeadingRadius: 10,
+                        bottomTrailingRadius: 10,
+                        topTrailingRadius: 0
+                    )
+                )
+                .listRowBackground(Color.clear)
+                .listRowInsets(EdgeInsets(top: -22, leading: 0, bottom: 0, trailing: 0))
+                .listRowSeparator(.hidden)
+        }
+        .listStyle(.plain)
+        .scrollDisabled(true)
+        .scrollContentBackground(.hidden)
+        // 55 for header row, item counts x 45 for every entry row + 230 for a visible picker row
+        .frame(height: 55 + CGFloat(items.count) * 45 + (items.contains(where: { $0.id == selectedItemID }) ? 230 : 0))
+        .onAppear {
+            // ensure picker is closed when view appears
+            selectedItemID = nil
+            validateTherapySettingItems()
+        }
+    }
+
+    @ViewBuilder private func timeValuePickerRow(
+        item: Binding<TherapySettingItem>,
+        timeOptions: [TimeInterval],
+        valueOptions: [Decimal],
+        unit: TherapySettingUnit
+    ) -> some View {
+        // Compute unavailable times (already taken by other entries)
+        let takenTimes = Set(items.filter { $0.id != item.wrappedValue.id }.map(\.time))
+        // Allow current selection even if it’s in the set of taken times.
+        let availableTimes = timeOptions.filter { $0 == item.wrappedValue.time || !takenTimes.contains($0) }
+        // Determine if this is first item in list (which is locked to 00:00)
+        var isFirstItem: Bool {
+            items.first == item.wrappedValue
+        }
+
+        VStack(spacing: 8) {
+            HStack {
+                Picker("Value", selection: Binding(
+                    get: { Double(item.wrappedValue.value) },
+                    set: {
+                        item.wrappedValue.value = Decimal($0)
+                    }
+                )) {
+                    ForEach(valueOptions, id: \.self) { value in
+                        Text("\(displayText(for: unit, decimalValue: value)) \(unit.displayName)").tag(Double(value))
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .clipped()
+
+                Picker("Time", selection: Binding(
+                    get: { item.wrappedValue.time },
+                    set: { newTime in
+                        // Only update if new time is either not taken, or it is the current value
+                        if newTime == item.wrappedValue.time || !takenTimes.contains(newTime) {
+                            item.wrappedValue.time = newTime
+                            validateTherapySettingItems()
+                        }
+                    }
+                )) {
+                    ForEach(availableTimes, id: \.self) { time in
+                        Text(timeFormatter.string(from: Date(timeIntervalSince1970: time)))
+                            .tag(time)
+                            .foregroundStyle(item.wrappedValue.time != 0 ? Color.primary : Color.secondary)
+                    }
+                }
+                // Lock time picker if first item and make it slightly opague
+                .opacity(isFirstItem ? 0.5 : 1)
+                .disabled(isFirstItem)
+                .frame(maxWidth: .infinity)
+                .clipped()
+            }
+            .pickerStyle(.wheel)
+        }
+        .padding(.vertical, 8)
+    }
+
+    private func sortTherapyItems() {
+        Task { @MainActor in
+            withAnimation {
+                items = items.sorted { $0.time < $1.time }
+            }
+        }
+    }
+
+    private func validateTherapySettingItems() {
+        // validates therapy items (i.e. parsed therapy settings into wrapper class)
+        let newItems = Array(Set(items)).sorted { $0.time < $1.time }
+        if var first = newItems.first, first.time != 0 {
+            first.time = 0
+            items = newItems
+        }
+
+        // validates underlying "raw" therapy setting (i.e. item of type basal, target, isf, carb ratio)
+        validateOnDelete?()
+    }
+
+    private var timeFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    private func displayText(for unit: TherapySettingUnit, decimalValue: Decimal) -> String {
+        switch unit {
+        case .mmolL,
+             .mmolLPerUnit:
+            return decimalValue.formattedAsMmolL
+        case .gramPerUnit,
+             .mgdL,
+             .mgdLPerUnit,
+             .unitPerHour:
+            return decimalValue.description
+        }
+    }
+}
+
+struct TherapySettingItem: Identifiable, Equatable, Hashable {
+    var id = UUID()
+    var time: TimeInterval = 0 // seconds since start of day
+    var value: Decimal = 0
+
+    init(time: TimeInterval, value: Decimal) {
+        self.time = time
+        self.value = value
+    }
+
+    static func == (lhs: TherapySettingItem, rhs: TherapySettingItem) -> Bool {
+        lhs.time == rhs.time && lhs.value == rhs.value
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(time)
+        hasher.combine(value)
+    }
+}
+
+enum TherapySettingUnit: String, CaseIterable {
+    case mmolLPerUnit
+    case mgdLPerUnit
+    case unitPerHour
+    case gramPerUnit
+    case mmolL
+    case mgdL
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .mmolLPerUnit:
+            return String(localized: "mmol/L/U")
+        case .mgdLPerUnit:
+            return String(localized: "mg/dL/U")
+        case .unitPerHour:
+            return String(localized: "U/hr")
+        case .gramPerUnit:
+            return String(localized: "g/U")
+        case .mmolL:
+            return "mmol/L"
+        case .mgdL:
+            return "mg/dL"
+        }
+    }
+}
+
+#Preview {
+    @Previewable @State var previewItems = [
+        TherapySettingItem(time: 0, value: 1.0),
+        TherapySettingItem(time: 1800, value: 1.2)
+    ]
+
+    TherapySettingEditorView(
+        items: $previewItems,
+        unit: .unitPerHour,
+        timeOptions: stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 },
+        valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) }
+    )
+}

+ 9 - 1
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -65,7 +65,15 @@ enum SettingItems {
         SettingItem(
             title: "Units and Limits",
             view: .unitsAndLimits,
-            searchContents: ["Glucose Units", "Max Basal", "Max Bolus", "Max IOB", "Max COB", "Minimum Safety Threshold"],
+            searchContents: [
+                "Glucose Units",
+                "Max Basal",
+                "Max Bolus",
+                "Max IOB",
+                "Max COB",
+                "Minimum Safety Threshold",
+                "Delivery Limits"
+            ],
             path: ["Therapy Settings", "Units and Limits"]
         ),
         SettingItem(title: "Basal Rates", view: .basalProfileEditor, path: ["Therapy Settings"]),

+ 8 - 0
Trio/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift

@@ -37,6 +37,14 @@ struct FeatureSettingsView: BaseView {
                 }
             )
             .listRowBackground(Color.chart)
+
+            Section(
+                header: Text("Anonymized Data Sharing"),
+                content: {
+                    Text("App Diagnostics").navigationLink(to: .appDiagnostics, from: self)
+                }
+            )
+            .listRowBackground(Color.chart)
         }
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))

+ 2 - 2
Trio/Sources/Modules/Settings/View/Subviews/TherapySettingsView.swift

@@ -29,10 +29,10 @@ struct TherapySettingsView: BaseView {
             Section(
                 header: Text("Basic Insulin Rates & Targets"),
                 content: {
+                    Text("Glucose Targets").navigationLink(to: .targetsEditor, from: self)
                     Text("Basal Rates").navigationLink(to: .basalProfileEditor, from: self)
-                    Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
-                    Text("Glucose Targets").navigationLink(to: .targetsEditor, from: self)
+                    Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
                 }
             )
             .listRowBackground(Color.chart)

+ 3 - 0
Trio/Sources/Router/Screen.swift

@@ -48,6 +48,7 @@ enum Screen: Identifiable, Hashable {
     case targetBehavior
     case algorithmAdvancedSettings
     case unitsAndLimits
+    case appDiagnostics
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -147,6 +148,8 @@ extension Screen {
             AlgorithmAdvancedSettings.RootView(resolver: resolver)
         case .unitsAndLimits:
             UnitsLimitsSettings.RootView(resolver: resolver)
+        case .appDiagnostics:
+            AppDiagnostics.RootView(resolver: resolver)
         }
     }
 

+ 50 - 0
Trio/Sources/Services/OnboardingManager/OnboardingManager.swift

@@ -0,0 +1,50 @@
+import Foundation
+import SwiftUI
+import Swinject
+
+/// Manages the app's onboarding experience, ensuring it's only shown to new users.
+/// Coordinates the display of onboarding screens when the app is launched for the first time.
+@Observable final class OnboardingManager: Injectable {
+    /// Shared singleton instance.
+    static let shared = OnboardingManager()
+
+    /// Indicates whether the onboarding flow should be presented.
+    var shouldShowOnboarding: Bool = false
+
+    /// Initialize the OnboardingManager with the required dependencies.
+    init() {
+        checkOnboardingStatus()
+    }
+
+    /// Checks if onboarding has been completed and updates the shouldShowOnboarding flag accordingly.
+    private func checkOnboardingStatus() {
+        shouldShowOnboarding = !UserDefaults.standard.onboardingCompleted
+
+        // Only for Debugging purposes
+//        shouldShowOnboarding = true
+    }
+
+    /// Marks onboarding as completed and updates the shouldShowOnboarding flag.
+    func completeOnboarding() {
+        UserDefaults.standard.onboardingCompleted = true
+        shouldShowOnboarding = false
+    }
+
+    /// Resets the onboarding status for testing purposes.
+    func resetOnboarding() {
+        UserDefaults.standard.onboardingCompleted = false
+        shouldShowOnboarding = true
+    }
+}
+
+extension UserDefaults {
+    /// Flag that indicates if onboarding has been completed.
+    var onboardingCompleted: Bool {
+        get {
+            bool(forKey: "onboardingCompleted")
+        }
+        set {
+            set(newValue, forKey: "onboardingCompleted")
+        }
+    }
+}

+ 2 - 0
Trio/Sources/Views/SettingInputSection.swift

@@ -222,6 +222,8 @@ struct SettingInputSection<VerboseHint: View>: View {
             return Text("\(decimalValue * 100) \(String(localized: "%", comment: "Percentage symbol"))")
         case .insulinUnit:
             return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
+        case .insulinUnitPerHour:
+            return Text("\(decimalValue) \(String(localized: "U/hr", comment: "Insulin unit per hour abbreviation"))")
         case .gram:
             return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
         case .minute: