OnboardingRootView.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  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. var body: some View {
  271. ScrollViewReader { scrollProxy in
  272. ScrollView(.vertical, showsIndicators: true) {
  273. VStack(alignment: .leading, spacing: 20) {
  274. Color.clear.frame(height: 0).id("top")
  275. if currentStep != .welcome, currentStep != .completed, showingChapterCompletion == nil {
  276. contentHeader
  277. }
  278. if let chapter = showingChapterCompletion {
  279. CompletedStepView(isOnboardingCompleted: false, currentChapter: chapter)
  280. } else {
  281. Group {
  282. switch currentStep {
  283. case .welcome:
  284. WelcomeStepView()
  285. case .startupInfo:
  286. switch currentStartupSubstep {
  287. case .startupGuide:
  288. StartupGuideStepView(state: state)
  289. case .returningUser:
  290. StartupReturningUserStepView(state: state, wasMigrationSuccessful: wasMigrationSuccessful)
  291. case .forceCloseWarning:
  292. StartupForceCloseWarningStepView(state: state)
  293. }
  294. case .overview:
  295. OverviewStepView()
  296. case .diagnostics:
  297. DiagnosticsStepView(state: state)
  298. case .nightscout:
  299. switch currentNightscoutSubstep {
  300. case .setupSelection:
  301. NightscoutSetupStepView(state: state)
  302. case .connectToNightscout:
  303. NightscoutLoginStepView(state: state)
  304. case .importFromNightscout:
  305. NightscoutImportStepView(state: state)
  306. }
  307. case .unitSelection:
  308. UnitSelectionStepView(state: state)
  309. case .glucoseTarget:
  310. GlucoseTargetStepView(state: state)
  311. case .basalRates:
  312. BasalProfileStepView(state: state)
  313. case .carbRatio:
  314. CarbRatioStepView(state: state)
  315. case .insulinSensitivity:
  316. InsulinSensitivityStepView(state: state)
  317. case .deliveryLimits:
  318. DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
  319. case .algorithmSettings:
  320. switch currentAlgorithmSettingsOverviewSubstep {
  321. case .contents:
  322. AlgorithmSettingsContentsStepView(state: state)
  323. case .importantNotes:
  324. AlgorithmSettingsImportantNotesStepView(state: state)
  325. }
  326. case .autosensSettings:
  327. AlgorithmSettingsSubstepView(state: state, substep: currentAutosensSubstep)
  328. case .smbSettings:
  329. AlgorithmSettingsSubstepView(state: state, substep: currentSMBSubstep)
  330. case .targetBehavior:
  331. AlgorithmSettingsSubstepView(state: state, substep: currentTargetBehaviorSubstep)
  332. case .notifications:
  333. NotificationPermissionStepView(state: state, currentStep: $currentStep)
  334. case .bluetooth:
  335. BluetoothPermissionStepView(
  336. state: state,
  337. bluetoothManager: state.bluetoothManager,
  338. currentStep: $currentStep
  339. )
  340. case .completed:
  341. CompletedStepView(isOnboardingCompleted: true, currentChapter: nil)
  342. }
  343. }
  344. .transition(
  345. navigationDirection == .forward
  346. ? .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
  347. : .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
  348. )
  349. .padding(.horizontal)
  350. .id(currentStep.id)
  351. }
  352. }
  353. .padding(.bottom, 80)
  354. }
  355. .onChange(of: currentStep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  356. .onChange(of: currentStartupSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  357. .onChange(of: currentNightscoutSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  358. .onChange(of: currentDeliverySubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  359. .onChange(of: currentAlgorithmSettingsOverviewSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  360. .onChange(of: currentAutosensSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  361. .onChange(of: currentSMBSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  362. .onChange(of: currentTargetBehaviorSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
  363. .safeAreaInset(edge: .top) {
  364. // avoid letting content scroll beneath the status bar / dynamic island for content views with not progress bar (which adds top spacing)
  365. if currentStep == .startupInfo || currentStep == .completed {
  366. Color.clear.frame(height: 0)
  367. }
  368. }
  369. }
  370. }
  371. private var contentHeader: some View {
  372. HStack {
  373. if currentStep == .nightscout {
  374. Image(currentStep.iconName)
  375. .resizable()
  376. .scaledToFit()
  377. .frame(width: 60, height: 60)
  378. } else if currentStep == .bluetooth {
  379. Image(currentStep.iconName)
  380. .font(.system(size: 40))
  381. .foregroundColor(currentStep.accentColor)
  382. .frame(width: 60, height: 60)
  383. .background(
  384. Circle()
  385. .fill(currentStep.accentColor.opacity(0.2))
  386. )
  387. } else {
  388. Image(systemName: currentStep.iconName)
  389. .font(.system(size: 40))
  390. .foregroundColor(currentStep.accentColor)
  391. .frame(width: 60, height: 60)
  392. .background(
  393. Circle()
  394. .fill(currentStep.accentColor.opacity(0.2))
  395. )
  396. }
  397. VStack(alignment: .leading) {
  398. Text(currentStep.title)
  399. .font(.title)
  400. .fontWeight(.bold)
  401. .foregroundColor(.primary)
  402. Text(currentStep.description)
  403. .font(.subheadline)
  404. .foregroundColor(.secondary)
  405. .fixedSize(horizontal: false, vertical: true)
  406. }
  407. }
  408. .padding(.horizontal)
  409. }
  410. }
  411. struct OnboardingNavigationButtons: View {
  412. @Binding var currentStep: OnboardingStep
  413. @Binding var showingChapterCompletion: OnboardingChapter?
  414. @Binding var currentStartupSubstep: StartupSubstep
  415. @Binding var currentNightscoutSubstep: NightscoutSubstep
  416. @Binding var currentDeliverySubstep: DeliveryLimitSubstep
  417. @Binding var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep
  418. @Binding var currentAutosensSubstep: AutosensSettingsSubstep
  419. @Binding var currentSMBSubstep: SMBSettingsSubstep
  420. @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
  421. let onboardingManager: OnboardingManager
  422. let isFreshTrioInstall: Bool
  423. @Bindable var state: Onboarding.StateModel
  424. var shouldDisableNextButton: Bool
  425. var navigationDirectionChanged: (OnboardingNavigationDirection) -> Void
  426. var body: some View {
  427. HStack {
  428. if currentStep != .welcome {
  429. Button(action: {
  430. navigationDirectionChanged(.backward)
  431. withAnimation {
  432. handleBackNavigation()
  433. }
  434. }) {
  435. HStack {
  436. Image(systemName: "chevron.left")
  437. Text("Back")
  438. }
  439. .padding()
  440. .foregroundColor(.primary)
  441. }
  442. }
  443. Spacer()
  444. Button(action: {
  445. navigationDirectionChanged(.forward)
  446. withAnimation {
  447. handleNextNavigation()
  448. }
  449. }) {
  450. HStack {
  451. Text(currentStep == .completed ? "Get Started" : "Next")
  452. Image(systemName: "chevron.right")
  453. }
  454. .padding()
  455. .foregroundColor(.white)
  456. .background(Capsule().fill(!shouldDisableNextButton ? Color.blue : Color(.systemGray)))
  457. }
  458. .disabled(shouldDisableNextButton)
  459. }
  460. .padding(.horizontal)
  461. .padding(.bottom)
  462. }
  463. // MARK: - Navigation Logic
  464. private func handleBackNavigation() {
  465. if showingChapterCompletion != nil {
  466. showingChapterCompletion = nil
  467. return
  468. }
  469. switch currentStep {
  470. case .startupInfo:
  471. var previous = StartupSubstep(rawValue: currentStartupSubstep.rawValue - 1)
  472. /// Skip `.returningUser` if this is a fresh install
  473. if previous == .returningUser, isFreshTrioInstall == true {
  474. previous = StartupSubstep(rawValue: previous!.rawValue - 1)
  475. }
  476. if let previousSub = previous {
  477. currentStartupSubstep = previousSub
  478. } else if let previous = currentStep.previous {
  479. currentStep = previous
  480. currentStartupSubstep = .startupGuide
  481. }
  482. case .overview:
  483. currentStartupSubstep = .forceCloseWarning
  484. if let previous = currentStep.previous {
  485. currentStep = previous
  486. }
  487. case .nightscout:
  488. if currentNightscoutSubstep == .setupSelection,
  489. let previous = currentStep.previous
  490. {
  491. currentStep = previous
  492. currentNightscoutSubstep = .setupSelection
  493. } else {
  494. currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue - 1)!
  495. }
  496. case .deliveryLimits:
  497. if let previousSub = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue - 1) {
  498. currentDeliverySubstep = previousSub
  499. } else if let previous = currentStep.previous {
  500. currentStep = previous
  501. currentDeliverySubstep = .maxIOB
  502. }
  503. case .algorithmSettings:
  504. if let previousSub = AlgorithmSettingsOverviewSubstep(
  505. rawValue: currentAlgorithmSettingsOverviewSubstep
  506. .rawValue - 1
  507. ) {
  508. currentAlgorithmSettingsOverviewSubstep = previousSub
  509. } else if let previous = currentStep.previous {
  510. currentStep = previous
  511. currentDeliverySubstep = .minimumSafetyThreshold
  512. currentAutosensSubstep = .autosensMin
  513. }
  514. case .autosensSettings:
  515. let steps = state.filteredAutosensSettingsSubsteps
  516. if let current = steps.firstIndex(of: currentAutosensSubstep),
  517. current > 0
  518. {
  519. currentAutosensSubstep = steps[current - 1]
  520. } else if let previousStep = currentStep.previous {
  521. currentStep = previousStep
  522. currentAutosensSubstep = steps.first ?? .autosensMin
  523. }
  524. case .smbSettings:
  525. currentAlgorithmSettingsOverviewSubstep = .importantNotes
  526. if let previous = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue - 1) {
  527. /// If user has activated setting `.enableSMBAlways`, when navigating backwards
  528. /// skip other redundant "Enable SMB"-settings and go straight to `enableSMBAlways`
  529. /// from current substep `.allowSMBWithHighTempTarget`.
  530. if state.enableSMBAlways, currentSMBSubstep == .allowSMBWithHighTempTarget {
  531. currentSMBSubstep = .enableSMBAlways
  532. } else {
  533. currentSMBSubstep = previous
  534. }
  535. } else if let previousStep = currentStep.previous {
  536. currentStep = previousStep
  537. currentSMBSubstep = .enableSMBAlways
  538. switch state.pumpOptionForOnboardingUnits {
  539. case .dana,
  540. .minimed:
  541. currentAutosensSubstep = .rewindResetsAutosens
  542. case .omnipodDash,
  543. .omnipodEros:
  544. currentAutosensSubstep = .autosensMax
  545. }
  546. }
  547. case .targetBehavior:
  548. if let previous = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue - 1) {
  549. currentTargetBehaviorSubstep = previous
  550. } else if let previousStep = currentStep.previous {
  551. currentStep = previousStep
  552. currentTargetBehaviorSubstep = .highTempTargetRaisesSensitivity
  553. currentSMBSubstep = .maxDeltaGlucoseThreshold
  554. }
  555. case .notifications:
  556. currentTargetBehaviorSubstep = .halfBasalTarget
  557. if let previous = currentStep.previous {
  558. currentStep = previous
  559. }
  560. case .completed:
  561. currentStep = .bluetooth
  562. default:
  563. if let previous = currentStep.previous {
  564. currentStep = previous
  565. }
  566. }
  567. }
  568. private func handleNextNavigation() {
  569. if showingChapterCompletion != nil {
  570. showingChapterCompletion = nil
  571. if let next = currentStep.next {
  572. currentStep = next
  573. }
  574. return
  575. }
  576. if let chapter = currentStep.chapterCompletion {
  577. showingChapterCompletion = chapter
  578. return
  579. }
  580. switch currentStep {
  581. case .startupInfo:
  582. let nextSubstepRaw = currentStartupSubstep.rawValue + 1
  583. if isFreshTrioInstall, StartupSubstep(rawValue: nextSubstepRaw) == .returningUser {
  584. /// Skip `.returningUser` if it's a fresh install
  585. if let nextAfterSkip = StartupSubstep(rawValue: nextSubstepRaw + 1) {
  586. currentStartupSubstep = nextAfterSkip
  587. } else if let nextStep = currentStep.next {
  588. currentStep = nextStep
  589. currentStartupSubstep = .startupGuide
  590. }
  591. } else if let next = StartupSubstep(rawValue: nextSubstepRaw) {
  592. currentStartupSubstep = next
  593. } else if let nextStep = currentStep.next {
  594. currentStep = nextStep
  595. currentStartupSubstep = .startupGuide
  596. }
  597. case .nightscout:
  598. if currentNightscoutSubstep != .importFromNightscout {
  599. if currentNightscoutSubstep == .setupSelection,
  600. state.nightscoutSetupOption == .skipNightscoutSetup,
  601. let next = currentStep.next
  602. {
  603. currentStep = next
  604. } else {
  605. currentNightscoutSubstep = NightscoutSubstep(rawValue: currentNightscoutSubstep.rawValue + 1)!
  606. }
  607. } else if currentNightscoutSubstep == .importFromNightscout,
  608. state.nightscoutImportOption == .useImport
  609. {
  610. Task {
  611. await state.importSettingsFromNightscout(currentStep: $currentStep)
  612. }
  613. } else if let next = currentStep.next {
  614. currentStep = next
  615. }
  616. case .deliveryLimits:
  617. if let next = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
  618. currentDeliverySubstep = next
  619. } else {
  620. /// Setting delivery substep to the last substep (`.minimumSafetyThreshold`) and `showingChapterCompletion` to non-`nil`
  621. /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
  622. currentDeliverySubstep = .minimumSafetyThreshold
  623. showingChapterCompletion = .deliveryLimits
  624. }
  625. case .algorithmSettings:
  626. if let next = AlgorithmSettingsOverviewSubstep(rawValue: currentAlgorithmSettingsOverviewSubstep.rawValue + 1) {
  627. currentAlgorithmSettingsOverviewSubstep = next
  628. } else if let nextStep = currentStep.next {
  629. currentStep = nextStep
  630. currentAlgorithmSettingsOverviewSubstep = .contents
  631. }
  632. case .autosensSettings:
  633. let steps = state.filteredAutosensSettingsSubsteps
  634. if let current = steps.firstIndex(of: currentAutosensSubstep),
  635. current + 1 < steps.count
  636. {
  637. currentAutosensSubstep = steps[current + 1]
  638. } else if let nextStep = currentStep.next {
  639. currentStep = nextStep
  640. currentAutosensSubstep = steps.first ?? .autosensMin
  641. }
  642. case .smbSettings:
  643. if let next = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue + 1) {
  644. /// If user has activated setting `.enableSMBAlways`, when navigating forward
  645. /// skip other redundant "Enable SMB"-settings and go straight to `.allowSMBWithHighTempTarget`
  646. /// from current substep `.enableSMBAlways`.
  647. if state.enableSMBAlways, currentSMBSubstep == .enableSMBAlways {
  648. currentSMBSubstep = .allowSMBWithHighTempTarget
  649. } else {
  650. currentSMBSubstep = next
  651. }
  652. } else if let nextStep = currentStep.next {
  653. currentStep = nextStep
  654. currentSMBSubstep = .enableSMBAlways
  655. }
  656. case .targetBehavior:
  657. if let next = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue + 1) {
  658. currentTargetBehaviorSubstep = next
  659. } else {
  660. /// Setting target behavior substep to the last substep (`.halfBasalTarget`) and `showingChapterCompletion` to non-`nil`
  661. /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
  662. currentTargetBehaviorSubstep = .halfBasalTarget
  663. showingChapterCompletion = .algorithmSettings
  664. }
  665. case .notifications:
  666. currentTargetBehaviorSubstep = .halfBasalTarget
  667. if let next = currentStep.next {
  668. state.notificationsManager.getNotificationSettings { notificationSettings in
  669. switch notificationSettings.authorizationStatus {
  670. case .notDetermined:
  671. state.notificationsManager.requestNotificationPermissions { granted in
  672. state.hasNotificationsGranted = granted
  673. currentStep = next
  674. }
  675. case .denied:
  676. state.shouldDisplayCustomNotificationAlert = true
  677. case .authorized,
  678. .ephemeral,
  679. .provisional:
  680. currentStep = next
  681. break
  682. @unknown default:
  683. currentStep = next
  684. }
  685. }
  686. }
  687. case .bluetooth:
  688. if let next = currentStep.next {
  689. if state.bluetoothManager.bluetoothAuthorization != .authorized {
  690. state.shouldDisplayBluetoothRequestAlert = true
  691. } else {
  692. currentStep = next
  693. }
  694. }
  695. case .completed:
  696. state.saveOnboardingData()
  697. onboardingManager.completeOnboarding()
  698. Foundation.NotificationCenter.default.post(name: .onboardingCompleted, object: nil)
  699. default:
  700. if let next = currentStep.next {
  701. currentStep = next
  702. }
  703. }
  704. }
  705. }
  706. struct Onboarding_Preview: PreviewProvider {
  707. static var previews: some View {
  708. Group {
  709. let resolver = TrioApp.resolver
  710. let onboardingManager = OnboardingManager()
  711. Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager, wasMigrationSuccessful: true)
  712. .previewDisplayName("Onboarding Flow")
  713. }
  714. }
  715. }