OnboardingRootView.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. import Foundation
  2. import SwiftUI
  3. import Swinject
  4. /// The main onboarding view that manages navigation between onboarding steps.
  5. extension Onboarding {
  6. struct RootView: BaseView {
  7. let resolver: Resolver
  8. @State var state = StateModel()
  9. @State private var navigationDirection: OnboardingNavigationDirection = .forward
  10. let onboardingManager: OnboardingManager
  11. let wasMigrationSuccessful: Bool
  12. // Step management
  13. @State private var currentChapter: OnboardingChapter = .prepareTrio
  14. @State private var showingChapterCompletion: OnboardingChapter? = nil
  15. @State private var currentStep: OnboardingStep = .welcome
  16. @State private var currentStartupSubstep: StartupSubstep = .startupGuide
  17. @State private var currentNightscoutSubstep: NightscoutSubstep = .setupSelection
  18. @State private var currentDeliverySubstep: DeliveryLimitSubstep = .maxIOB
  19. @State private var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep = .contents
  20. @State private var currentAutosensSubstep: AutosensSettingsSubstep = .autosensMin
  21. @State private var currentSMBSubstep: SMBSettingsSubstep = .enableSMBAlways
  22. @State private var currentTargetBehaviorSubstep: TargetBehaviorSubstep = .highTempTargetRaisesSensitivity
  23. private func updateCurrentChapter() {
  24. switch currentStep {
  25. case .diagnostics,
  26. .nightscout,
  27. .unitSelection:
  28. currentChapter = .prepareTrio
  29. case .basalRates,
  30. .carbRatio,
  31. .glucoseTarget,
  32. .insulinSensitivity:
  33. currentChapter = .therapySettings
  34. case .deliveryLimits:
  35. currentChapter = .deliveryLimits
  36. case .algorithmSettings,
  37. .autosensSettings,
  38. .smbSettings,
  39. .targetBehavior:
  40. currentChapter = .algorithmSettings
  41. case .bluetooth,
  42. .notifications:
  43. currentChapter = .permissionRequests
  44. default:
  45. break
  46. }
  47. }
  48. // Animation states
  49. @State private var animationScale: CGFloat = 1.0
  50. @State private var animationOpacity: Double = 0
  51. @State private var isAnimating = false
  52. // Conditional button states for Nightscout substeps
  53. private var didSelectNightscoutSetupOption: Bool {
  54. currentNightscoutSubstep == .setupSelection && state
  55. .nightscoutSetupOption == .noSelection
  56. }
  57. private var hasValidNightscoutConnection: Bool {
  58. currentNightscoutSubstep == .connectToNightscout && !state.isConnectedToNS
  59. }
  60. private var didSelectNightscoutImportOption: Bool {
  61. currentNightscoutSubstep == .importFromNightscout && state.nightscoutImportOption == .noSelection
  62. }
  63. // Next button conditional
  64. private var shouldDisableNextButton: Bool {
  65. (currentStep == .diagnostics && state.diagnosticsSharingOption == .enabled && !state.hasAcceptedPrivacyPolicy)
  66. ||
  67. (currentStep == .nightscout && didSelectNightscoutSetupOption)
  68. ||
  69. (currentStep == .nightscout && hasValidNightscoutConnection)
  70. ||
  71. (currentStep == .nightscout && didSelectNightscoutImportOption)
  72. }
  73. var body: some View {
  74. NavigationView {
  75. ZStack {
  76. // Background gradient
  77. LinearGradient(
  78. gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
  79. startPoint: .top,
  80. endPoint: .bottom
  81. )
  82. .ignoresSafeArea()
  83. VStack(spacing: 0) {
  84. if (nonInfoOnboardingSteps + [OnboardingStep.overview, OnboardingStep.completed]).contains(currentStep) {
  85. // Progress bar
  86. OnboardingProgressBar(
  87. currentChapter: currentChapter,
  88. shouldDisplayChapterTitle: showingChapterCompletion == nil,
  89. currentStep: currentStep,
  90. currentSubstep: {
  91. switch currentStep {
  92. case .deliveryLimits: return currentDeliverySubstep.rawValue
  93. case .nightscout: return currentNightscoutSubstep.rawValue
  94. case .algorithmSettings: return currentAlgorithmSettingsOverviewSubstep.rawValue
  95. case .autosensSettings: return currentAutosensSubstep.rawValue
  96. case .smbSettings: return currentSMBSubstep.rawValue
  97. case .targetBehavior: return currentTargetBehaviorSubstep.rawValue
  98. default: return nil
  99. }
  100. }(),
  101. stepsWithSubsteps: [
  102. .nightscout: NightscoutSubstep.allCases.count,
  103. .deliveryLimits: DeliveryLimitSubstep.allCases.count,
  104. .algorithmSettings: AlgorithmSettingsOverviewSubstep.allCases.count,
  105. .autosensSettings: state.filteredAutosensSettingsSubsteps.count,
  106. .smbSettings: SMBSettingsSubstep.allCases.count,
  107. .targetBehavior: TargetBehaviorSubstep.allCases.count
  108. ],
  109. nightscoutSetupOption: state.nightscoutSetupOption
  110. )
  111. .padding(.top)
  112. } else {
  113. // avoid letting content scroll beneath the status bar / dynamic island for content views with no progress bar (which adds top spacing)
  114. Color.clear.frame(height: 1)
  115. }
  116. OnboardingStepContent(
  117. wasMigrationSuccessful: wasMigrationSuccessful,
  118. currentStep: $currentStep,
  119. showingChapterCompletion: $showingChapterCompletion,
  120. currentStartupSubstep: $currentStartupSubstep,
  121. currentNightscoutSubstep: $currentNightscoutSubstep,
  122. currentDeliverySubstep: $currentDeliverySubstep,
  123. currentAlgorithmSettingsOverviewSubstep: $currentAlgorithmSettingsOverviewSubstep,
  124. currentAutosensSubstep: $currentAutosensSubstep,
  125. currentSMBSubstep: $currentSMBSubstep,
  126. currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
  127. state: state,
  128. navigationDirection: navigationDirection
  129. )
  130. Spacer()
  131. OnboardingNavigationButtons(
  132. currentStep: $currentStep,
  133. showingChapterCompletion: $showingChapterCompletion,
  134. currentStartupSubstep: $currentStartupSubstep,
  135. currentNightscoutSubstep: $currentNightscoutSubstep,
  136. currentDeliverySubstep: $currentDeliverySubstep,
  137. currentAlgorithmSettingsOverviewSubstep: $currentAlgorithmSettingsOverviewSubstep,
  138. currentAutosensSubstep: $currentAutosensSubstep,
  139. currentSMBSubstep: $currentSMBSubstep,
  140. currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
  141. onboardingManager: onboardingManager,
  142. isFreshTrioInstall: state.isFreshTrioInstall,
  143. state: state,
  144. shouldDisableNextButton: shouldDisableNextButton,
  145. navigationDirectionChanged: { navigationDirection = $0 }
  146. )
  147. }
  148. }
  149. .navigationBarHidden(true)
  150. }
  151. .onChange(of: currentStep) { _, _ in
  152. // Reset animation when step changes
  153. animationScale = 0.9
  154. animationOpacity = 0
  155. isAnimating = false
  156. // Start new animation
  157. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  158. withAnimation(.easeInOut(duration: 0.7)) {
  159. animationOpacity = 1
  160. animationScale = 1.0
  161. }
  162. isAnimating = true
  163. }
  164. updateCurrentChapter()
  165. }
  166. .onAppear(perform: configureView)
  167. }
  168. }
  169. }
  170. /// A progress bar that shows the user's progress through the onboarding process.
  171. struct OnboardingProgressBar: View {
  172. let currentChapter: OnboardingChapter
  173. let shouldDisplayChapterTitle: Bool
  174. let currentStep: OnboardingStep
  175. let currentSubstep: Int?
  176. let stepsWithSubsteps: [OnboardingStep: Int]
  177. let nightscoutSetupOption: NightscoutSetupOption
  178. private let capsuleSize = CGFloat(UIFont.preferredFont(forTextStyle: .subheadline).pointSize) * 1.3
  179. private var shouldShowCurrentChapter: Bool {
  180. shouldDisplayChapterTitle && currentStep != .overview && currentStep != .completed
  181. }
  182. var body: some View {
  183. VStack(alignment: .leading, spacing: 10) {
  184. // only show this for the actual chapters, not the overview of chapters or completed view
  185. if shouldShowCurrentChapter {
  186. HStack(spacing: CGFloat(UIFont.preferredFont(forTextStyle: .subheadline).pointSize)) {
  187. Text("\(currentChapter.rawValue + 1)")
  188. .font(.subheadline)
  189. .fontWeight(.heavy)
  190. .frame(width: capsuleSize, height: capsuleSize, alignment: .center)
  191. .background(Color.blue)
  192. .foregroundStyle(Color.bgDarkBlue)
  193. .clipShape(Capsule())
  194. Text(currentChapter.title)
  195. .font(.subheadline)
  196. .kerning(capsuleSize / 4)
  197. .textCase(.uppercase)
  198. .bold()
  199. .foregroundStyle(Color.secondary)
  200. }
  201. }
  202. HStack(spacing: 4) {
  203. ForEach(renderedSteps, id: \.id) { step in
  204. ZStack(alignment: .leading) {
  205. Rectangle()
  206. .fill(Color.gray.opacity(0.3))
  207. .frame(height: 4)
  208. .cornerRadius(2)
  209. GeometryReader { geo in
  210. Rectangle()
  211. .fill(Color.blue)
  212. .frame(
  213. width: geo.size.width * fillFraction(for: step.step, totalSubsteps: step.substeps),
  214. height: 4
  215. )
  216. .cornerRadius(2)
  217. }
  218. }
  219. .frame(height: 4)
  220. }
  221. }
  222. }.padding(.horizontal)
  223. }
  224. private var renderedSteps: [(id: String, step: OnboardingStep, substeps: Int?)] {
  225. nonInfoOnboardingSteps.map {
  226. (id: "\($0.rawValue)", step: $0, substeps: stepsWithSubsteps[$0])
  227. }
  228. }
  229. private func fillFraction(for step: OnboardingStep, totalSubsteps: Int?) -> CGFloat {
  230. // If currentStep is .completed, fill everything
  231. if currentStep == .completed { return 1.0 }
  232. if let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
  233. let stepIndex = nonInfoOnboardingSteps.firstIndex(of: step),
  234. stepIndex < currentIndex
  235. {
  236. return 1.0
  237. }
  238. if step == currentStep {
  239. if let total = totalSubsteps, let current = currentSubstep {
  240. return CGFloat(current + 1) / CGFloat(total)
  241. } else {
  242. return 1.0
  243. }
  244. }
  245. // Handle special case: Nightscout was skipped
  246. if step == .nightscout,
  247. nightscoutSetupOption == .skipNightscoutSetup,
  248. let currentIndex = nonInfoOnboardingSteps.firstIndex(of: currentStep),
  249. let nightscoutIndex = nonInfoOnboardingSteps.firstIndex(of: .nightscout),
  250. currentIndex > nightscoutIndex
  251. {
  252. return 1.0
  253. }
  254. return 0.0
  255. }
  256. }
  257. struct OnboardingStepContent: View {
  258. var wasMigrationSuccessful: Bool
  259. @Binding var currentStep: OnboardingStep
  260. @Binding var showingChapterCompletion: OnboardingChapter?
  261. @Binding var currentStartupSubstep: StartupSubstep
  262. @Binding var currentNightscoutSubstep: NightscoutSubstep
  263. @Binding var currentDeliverySubstep: DeliveryLimitSubstep
  264. @Binding var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep
  265. @Binding var currentAutosensSubstep: AutosensSettingsSubstep
  266. @Binding var currentSMBSubstep: SMBSettingsSubstep
  267. @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
  268. @Bindable var state: Onboarding.StateModel
  269. var navigationDirection: OnboardingNavigationDirection
  270. @Environment(\.accessibilityReduceMotion) var reduceMotion
  271. private var transition: AnyTransition {
  272. if reduceMotion {
  273. return .opacity
  274. }
  275. switch navigationDirection {
  276. case .forward:
  277. return .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
  278. case .backward:
  279. return .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
  280. }
  281. }
  282. var body: some View {
  283. ScrollViewReader { scrollProxy in
  284. ScrollView(.vertical, showsIndicators: true) {
  285. VStack(alignment: .leading, spacing: 20) {
  286. Color.clear.frame(height: 0).id("top")
  287. if currentStep != .welcome, currentStep != .completed, showingChapterCompletion == nil {
  288. contentHeader
  289. }
  290. if let chapter = showingChapterCompletion {
  291. CompletedStepView(isOnboardingCompleted: false, currentChapter: chapter)
  292. } else {
  293. Group {
  294. switch currentStep {
  295. case .welcome:
  296. WelcomeStepView()
  297. case .startupInfo:
  298. switch currentStartupSubstep {
  299. case .startupGuide:
  300. StartupGuideStepView(state: state)
  301. case .returningUser:
  302. StartupReturningUserStepView(state: state, wasMigrationSuccessful: wasMigrationSuccessful)
  303. case .forceCloseWarning:
  304. StartupForceCloseWarningStepView(state: state)
  305. }
  306. case .overview:
  307. OverviewStepView()
  308. case .diagnostics:
  309. DiagnosticsStepView(state: state)
  310. case .nightscout:
  311. switch currentNightscoutSubstep {
  312. case .setupSelection:
  313. NightscoutSetupStepView(state: state)
  314. case .connectToNightscout:
  315. NightscoutLoginStepView(state: state)
  316. case .uploadToNightscout:
  317. NightscoutUploadStepView(state: state)
  318. case .uploadGlucoseToNightscout:
  319. NightscoutUploadGlucoseStepView(state: state)
  320. case .importFromNightscout:
  321. NightscoutImportStepView(state: state)
  322. }
  323. case .unitSelection:
  324. UnitSelectionStepView(state: state)
  325. case .glucoseTarget:
  326. GlucoseTargetStepView(state: state)
  327. case .basalRates:
  328. BasalProfileStepView(state: state)
  329. case .carbRatio:
  330. CarbRatioStepView(state: state)
  331. case .insulinSensitivity:
  332. InsulinSensitivityStepView(state: state)
  333. case .deliveryLimits:
  334. DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
  335. case .algorithmSettings:
  336. switch currentAlgorithmSettingsOverviewSubstep {
  337. case .contents:
  338. AlgorithmSettingsContentsStepView(state: state)
  339. case .importantNotes:
  340. AlgorithmSettingsImportantNotesStepView(state: state)
  341. }
  342. case .autosensSettings:
  343. AlgorithmSettingsSubstepView(state: state, substep: currentAutosensSubstep)
  344. case .smbSettings:
  345. AlgorithmSettingsSubstepView(state: state, substep: currentSMBSubstep)
  346. case .targetBehavior:
  347. AlgorithmSettingsSubstepView(state: state, substep: currentTargetBehaviorSubstep)
  348. case .notifications:
  349. NotificationPermissionStepView(state: state, currentStep: $currentStep)
  350. case .bluetooth:
  351. BluetoothPermissionStepView(
  352. state: state,
  353. bluetoothManager: state.bluetoothManager,
  354. currentStep: $currentStep
  355. )
  356. case .completed:
  357. CompletedStepView(isOnboardingCompleted: true, currentChapter: nil)
  358. }
  359. }
  360. .transition(transition)
  361. .padding(.horizontal)
  362. .id(currentStep.id)
  363. }
  364. }
  365. .padding(.bottom, 80)
  366. }
  367. .onChange(of: currentStep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  368. .onChange(of: currentStartupSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  369. .onChange(of: currentNightscoutSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  370. .onChange(of: currentDeliverySubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  371. .onChange(of: currentAlgorithmSettingsOverviewSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  372. .onChange(of: currentAutosensSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  373. .onChange(of: currentSMBSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  374. .onChange(of: currentTargetBehaviorSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  375. .safeAreaInset(edge: .top) {
  376. // avoid letting content scroll beneath the status bar / dynamic island for content views with not progress bar (which adds top spacing)
  377. if currentStep == .startupInfo || currentStep == .completed {
  378. Color.clear.frame(height: 0)
  379. }
  380. }
  381. }
  382. }
  383. private var contentHeader: some View {
  384. HStack {
  385. if currentStep == .nightscout {
  386. Image(currentStep.iconName)
  387. .resizable()
  388. .scaledToFit()
  389. .frame(width: 60, height: 60)
  390. } else if currentStep == .bluetooth {
  391. Image(currentStep.iconName)
  392. .font(.system(size: 40))
  393. .foregroundColor(currentStep.accentColor)
  394. .frame(width: 60, height: 60)
  395. .background(
  396. Circle()
  397. .fill(currentStep.accentColor.opacity(0.2))
  398. )
  399. } else {
  400. Image(systemName: currentStep.iconName)
  401. .font(.system(size: 40))
  402. .foregroundColor(currentStep.accentColor)
  403. .frame(width: 60, height: 60)
  404. .background(
  405. Circle()
  406. .fill(currentStep.accentColor.opacity(0.2))
  407. )
  408. }
  409. VStack(alignment: .leading) {
  410. Text(currentStep.title)
  411. .font(.title)
  412. .fontWeight(.bold)
  413. .foregroundColor(.primary)
  414. Text(currentStep.description)
  415. .font(.subheadline)
  416. .foregroundColor(.secondary)
  417. .fixedSize(horizontal: false, vertical: true)
  418. }
  419. }
  420. .padding(.horizontal)
  421. }
  422. }
  423. struct OnboardingNavigationButtons: View {
  424. @Binding var currentStep: OnboardingStep
  425. @Binding var showingChapterCompletion: OnboardingChapter?
  426. @Binding var currentStartupSubstep: StartupSubstep
  427. @Binding var currentNightscoutSubstep: NightscoutSubstep
  428. @Binding var currentDeliverySubstep: DeliveryLimitSubstep
  429. @Binding var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep
  430. @Binding var currentAutosensSubstep: AutosensSettingsSubstep
  431. @Binding var currentSMBSubstep: SMBSettingsSubstep
  432. @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
  433. let onboardingManager: OnboardingManager
  434. let isFreshTrioInstall: Bool
  435. @Bindable var state: Onboarding.StateModel
  436. var shouldDisableNextButton: Bool
  437. var navigationDirectionChanged: (OnboardingNavigationDirection) -> Void
  438. @Environment(\.accessibilityReduceMotion) var reduceMotion
  439. var body: some View {
  440. HStack {
  441. if currentStep != .welcome {
  442. Button(action: {
  443. navigationDirectionChanged(.backward)
  444. withAnimation(reduceMotion ? .easeInOut(duration: 0.25) : .default) {
  445. handleBackNavigation()
  446. }
  447. }) {
  448. HStack {
  449. Image(systemName: "chevron.left")
  450. Text("Back")
  451. }
  452. .padding()
  453. .foregroundColor(.primary)
  454. }
  455. }
  456. Spacer()
  457. Button(action: {
  458. navigationDirectionChanged(.forward)
  459. withAnimation(reduceMotion ? .easeInOut(duration: 0.25) : .default) {
  460. handleNextNavigation()
  461. }
  462. }) {
  463. HStack {
  464. Text(currentStep == .completed ? "Get Started" : "Next")
  465. Image(systemName: "chevron.right")
  466. }
  467. .padding()
  468. .foregroundColor(.white)
  469. .background(Capsule().fill(!shouldDisableNextButton ? Color.blue : Color(.systemGray)))
  470. }
  471. .disabled(shouldDisableNextButton)
  472. }
  473. .padding(.horizontal)
  474. .padding(.bottom)
  475. }
  476. // MARK: - Navigation Logic
  477. private func handleBackNavigation() {
  478. if showingChapterCompletion != nil {
  479. showingChapterCompletion = nil
  480. return
  481. }
  482. switch currentStep {
  483. case .startupInfo:
  484. var previous = StartupSubstep(rawValue: currentStartupSubstep.rawValue - 1)
  485. /// Skip `.returningUser` if this is a fresh install
  486. if previous == .returningUser, isFreshTrioInstall == true {
  487. previous = StartupSubstep(rawValue: previous!.rawValue - 1)
  488. }
  489. if let previousSub = previous {
  490. currentStartupSubstep = previousSub
  491. } else if let previous = currentStep.previous {
  492. currentStep = previous
  493. currentStartupSubstep = .startupGuide
  494. }
  495. case .overview:
  496. currentStartupSubstep = .forceCloseWarning
  497. if let previous = currentStep.previous {
  498. currentStep = previous
  499. }
  500. case .nightscout:
  501. if currentNightscoutSubstep == .setupSelection,
  502. let previous = currentStep.previous
  503. {
  504. currentStep = previous
  505. currentNightscoutSubstep = .setupSelection
  506. } else {
  507. currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue - 1)!
  508. }
  509. case .deliveryLimits:
  510. if let previousSub = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue - 1) {
  511. currentDeliverySubstep = previousSub
  512. } else if let previous = currentStep.previous {
  513. currentStep = previous
  514. currentDeliverySubstep = .maxIOB
  515. }
  516. case .algorithmSettings:
  517. if let previousSub = AlgorithmSettingsOverviewSubstep(
  518. rawValue: currentAlgorithmSettingsOverviewSubstep
  519. .rawValue - 1
  520. ) {
  521. currentAlgorithmSettingsOverviewSubstep = previousSub
  522. } else if let previous = currentStep.previous {
  523. currentStep = previous
  524. currentDeliverySubstep = .minimumSafetyThreshold
  525. currentAutosensSubstep = .autosensMin
  526. }
  527. case .autosensSettings:
  528. let steps = state.filteredAutosensSettingsSubsteps
  529. if let current = steps.firstIndex(of: currentAutosensSubstep),
  530. current > 0
  531. {
  532. currentAutosensSubstep = steps[current - 1]
  533. } else if let previousStep = currentStep.previous {
  534. currentStep = previousStep
  535. currentAutosensSubstep = steps.first ?? .autosensMin
  536. }
  537. case .smbSettings:
  538. currentAlgorithmSettingsOverviewSubstep = .importantNotes
  539. if let previous = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue - 1) {
  540. /// If user has activated setting `.enableSMBAlways`, when navigating backwards
  541. /// skip other redundant "Enable SMB"-settings and go straight to `enableSMBAlways`
  542. /// from current substep `.allowSMBWithHighTempTarget`.
  543. if state.enableSMBAlways, currentSMBSubstep == .allowSMBWithHighTempTarget {
  544. currentSMBSubstep = .enableSMBAlways
  545. } else {
  546. currentSMBSubstep = previous
  547. }
  548. } else if let previousStep = currentStep.previous {
  549. currentStep = previousStep
  550. currentSMBSubstep = .enableSMBAlways
  551. switch state.pumpOptionForOnboardingUnits {
  552. case .dana,
  553. .minimed:
  554. currentAutosensSubstep = .rewindResetsAutosens
  555. case .medtrum,
  556. .omnipodDash,
  557. .omnipodEros:
  558. currentAutosensSubstep = .autosensMax
  559. }
  560. }
  561. case .targetBehavior:
  562. if let previous = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue - 1) {
  563. currentTargetBehaviorSubstep = previous
  564. } else if let previousStep = currentStep.previous {
  565. currentStep = previousStep
  566. currentTargetBehaviorSubstep = .highTempTargetRaisesSensitivity
  567. currentSMBSubstep = .maxDeltaGlucoseThreshold
  568. }
  569. case .notifications:
  570. currentTargetBehaviorSubstep = .halfBasalTarget
  571. if let previous = currentStep.previous {
  572. currentStep = previous
  573. }
  574. case .completed:
  575. currentStep = .bluetooth
  576. default:
  577. if let previous = currentStep.previous {
  578. currentStep = previous
  579. }
  580. }
  581. }
  582. private func handleNextNavigation() {
  583. if showingChapterCompletion != nil {
  584. showingChapterCompletion = nil
  585. if let next = currentStep.next {
  586. currentStep = next
  587. }
  588. return
  589. }
  590. if let chapter = currentStep.chapterCompletion {
  591. showingChapterCompletion = chapter
  592. return
  593. }
  594. switch currentStep {
  595. case .startupInfo:
  596. let nextSubstepRaw = currentStartupSubstep.rawValue + 1
  597. if isFreshTrioInstall, StartupSubstep(rawValue: nextSubstepRaw) == .returningUser {
  598. /// Skip `.returningUser` if it's a fresh install
  599. if let nextAfterSkip = StartupSubstep(rawValue: nextSubstepRaw + 1) {
  600. currentStartupSubstep = nextAfterSkip
  601. } else if let nextStep = currentStep.next {
  602. currentStep = nextStep
  603. currentStartupSubstep = .startupGuide
  604. }
  605. } else if let next = StartupSubstep(rawValue: nextSubstepRaw) {
  606. currentStartupSubstep = next
  607. } else if let nextStep = currentStep.next {
  608. currentStep = nextStep
  609. currentStartupSubstep = .startupGuide
  610. }
  611. case .nightscout:
  612. if currentNightscoutSubstep != .importFromNightscout {
  613. if currentNightscoutSubstep == .setupSelection,
  614. state.nightscoutSetupOption == .skipNightscoutSetup,
  615. let next = currentStep.next
  616. {
  617. currentStep = next
  618. } else {
  619. currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue + 1)!
  620. }
  621. } else if currentNightscoutSubstep == .importFromNightscout,
  622. state.nightscoutImportOption == .useImport
  623. {
  624. Task {
  625. await state.importSettingsFromNightscout(currentStep: $currentStep)
  626. }
  627. } else if let next = currentStep.next {
  628. currentStep = next
  629. }
  630. case .deliveryLimits:
  631. if let next = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
  632. currentDeliverySubstep = next
  633. } else {
  634. /// Setting delivery substep to the last substep (`.minimumSafetyThreshold`) and `showingChapterCompletion` to non-`nil`
  635. /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
  636. currentDeliverySubstep = .minimumSafetyThreshold
  637. showingChapterCompletion = .deliveryLimits
  638. }
  639. case .algorithmSettings:
  640. if let next = AlgorithmSettingsOverviewSubstep(rawValue: currentAlgorithmSettingsOverviewSubstep.rawValue + 1) {
  641. currentAlgorithmSettingsOverviewSubstep = next
  642. } else if let nextStep = currentStep.next {
  643. currentStep = nextStep
  644. currentAlgorithmSettingsOverviewSubstep = .contents
  645. }
  646. case .autosensSettings:
  647. let steps = state.filteredAutosensSettingsSubsteps
  648. if let current = steps.firstIndex(of: currentAutosensSubstep),
  649. current + 1 < steps.count
  650. {
  651. currentAutosensSubstep = steps[current + 1]
  652. } else if let nextStep = currentStep.next {
  653. currentStep = nextStep
  654. currentAutosensSubstep = steps.first ?? .autosensMin
  655. }
  656. case .smbSettings:
  657. if let next = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue + 1) {
  658. /// If user has activated setting `.enableSMBAlways`, when navigating forward
  659. /// skip other redundant "Enable SMB"-settings and go straight to `.allowSMBWithHighTempTarget`
  660. /// from current substep `.enableSMBAlways`.
  661. if state.enableSMBAlways, currentSMBSubstep == .enableSMBAlways {
  662. currentSMBSubstep = .allowSMBWithHighTempTarget
  663. } else {
  664. currentSMBSubstep = next
  665. }
  666. } else if let nextStep = currentStep.next {
  667. currentStep = nextStep
  668. currentSMBSubstep = .enableSMBAlways
  669. }
  670. case .targetBehavior:
  671. if let next = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue + 1) {
  672. currentTargetBehaviorSubstep = next
  673. } else {
  674. /// Setting target behavior substep to the last substep (`.halfBasalTarget`) and `showingChapterCompletion` to non-`nil`
  675. /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
  676. currentTargetBehaviorSubstep = .halfBasalTarget
  677. showingChapterCompletion = .algorithmSettings
  678. }
  679. case .notifications:
  680. currentTargetBehaviorSubstep = .halfBasalTarget
  681. if let next = currentStep.next {
  682. state.notificationsManager.getNotificationSettings { notificationSettings in
  683. switch notificationSettings.authorizationStatus {
  684. case .notDetermined:
  685. state.notificationsManager.requestNotificationPermissions { granted in
  686. state.hasNotificationsGranted = granted
  687. currentStep = next
  688. }
  689. case .denied:
  690. state.shouldDisplayCustomNotificationAlert = true
  691. case .authorized,
  692. .ephemeral,
  693. .provisional:
  694. currentStep = next
  695. break
  696. @unknown default:
  697. currentStep = next
  698. }
  699. }
  700. }
  701. case .bluetooth:
  702. if let next = currentStep.next {
  703. if state.bluetoothManager.bluetoothAuthorization != .authorized {
  704. state.shouldDisplayBluetoothRequestAlert = true
  705. } else {
  706. currentStep = next
  707. }
  708. }
  709. case .completed:
  710. state.saveOnboardingData()
  711. onboardingManager.completeOnboarding()
  712. Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
  713. default:
  714. if let next = currentStep.next {
  715. currentStep = next
  716. }
  717. }
  718. }
  719. }
  720. struct Onboarding_Preview: PreviewProvider {
  721. static var previews: some View {
  722. Group {
  723. let resolver = TrioApp.resolver
  724. let onboardingManager = OnboardingManager()
  725. Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager, wasMigrationSuccessful: true)
  726. .previewDisplayName("Onboarding Flow")
  727. }
  728. }
  729. }