OnboardingRootView.swift 35 KB

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