tutorial: introduce habit reminders in the Settings tutorial too (KC-60)
Habit reminders (KC-55) were in Onboarding but not the app's separate spotlight/coachmark tutorial system -- skipping onboarding meant no second chance to discover Prayer/Bed/Coffee reminders exist. Adds a fourth TutorialManager track (settingsTips/settingsStep/settingsDone), matching the existing Today/Workout tip-track pattern exactly, and wires it into SettingsView via the same shared InViewTutorialCard component -- shown once, first time Settings opens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2029,3 +2029,36 @@ offset from an unrelated comment, verified via stash — not introduced by this
|
|||||||
change). `OverdueCard`'s wiring is technically unreachable today since
|
change). `OverdueCard`'s wiring is technically unreachable today since
|
||||||
`overdueTasks` already excludes recurring tasks by design — wired anyway for
|
`overdueTasks` already excludes recurring tasks by design — wired anyway for
|
||||||
consistency in case that changes.
|
consistency in case that changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KC-60 — Habit reminders also introduced in the in-app tutorial, not just onboarding
|
||||||
|
|
||||||
|
**Status:** Implemented (not build-verified)
|
||||||
|
**Reported by:** User: "do we have the behavior reminders as part of onboarding
|
||||||
|
and also in the tutorial"
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Habit reminders (KC-55) were added to Onboarding, but **not** to the app's
|
||||||
|
separate tutorial system (`TutorialManager` — the spotlight/coachmark walkthrough
|
||||||
|
shown per-tab: intro tour, Today tips, Workout tips). Someone who skips or
|
||||||
|
doesn't linger on onboarding's toggles had no second chance to discover the
|
||||||
|
feature exists.
|
||||||
|
|
||||||
|
### Change
|
||||||
|
- New fourth tutorial track in `TutorialManager`: `settingsTips` /
|
||||||
|
`settingsStep` / `settingsDone` (persisted key `kisani.tutorial.settings.v1`),
|
||||||
|
following the exact same pattern as the existing Today/Workout tip tracks
|
||||||
|
(`advanceSettings()`/`skipSettings()`).
|
||||||
|
- One tip: "Habit Reminders" — introduces Prayer/Bed/Coffee and the private
|
||||||
|
streak via the notification's "Done" action.
|
||||||
|
- Wired into `SettingsView` via the same `InViewTutorialCard` component
|
||||||
|
Today/Workout already use — shown once, the first time Settings is opened.
|
||||||
|
|
||||||
|
Files: `TutorialManager.swift`, `SettingsView.swift`.
|
||||||
|
|
||||||
|
### Note
|
||||||
|
Not build-verified (no iOS runtime here). Self-reviewed: balanced braces/
|
||||||
|
parens confirmed; wiring mirrors `TodayView`/`WorkoutView`'s existing
|
||||||
|
`tm.xIsActive` + `InViewTutorialCard` pattern exactly, reusing the same shared
|
||||||
|
component rather than introducing a new one.
|
||||||
|
|||||||
@@ -104,4 +104,24 @@ final class TutorialManager: ObservableObject {
|
|||||||
else { withAnimation(KisaniSpring.entrance) { workoutStep += 1 } }
|
else { withAnimation(KisaniSpring.entrance) { workoutStep += 1 } }
|
||||||
}
|
}
|
||||||
func skipWorkout() { withAnimation(KisaniSpring.exit) { workoutDone = true } }
|
func skipWorkout() { withAnimation(KisaniSpring.exit) { workoutDone = true } }
|
||||||
|
|
||||||
|
// MARK: - Settings view tutorial
|
||||||
|
@AppStorage("kisani.tutorial.settings.v1") var settingsDone: Bool = false
|
||||||
|
@Published var settingsStep: Int = 0
|
||||||
|
|
||||||
|
let settingsTips: [PageTip] = [
|
||||||
|
.init(title: "Habit Reminders",
|
||||||
|
body: "Prayer, making your bed, coffee — under Habits, set daily nudges for the small routines you don't want to forget. Tap Done on the notification to keep a private streak.",
|
||||||
|
icon: "hands.sparkles"),
|
||||||
|
]
|
||||||
|
|
||||||
|
var settingsIsActive: Bool { !settingsDone }
|
||||||
|
var settingsCurrent: PageTip { settingsTips[min(settingsStep, settingsTips.count - 1)] }
|
||||||
|
var settingsIsLast: Bool { settingsStep == settingsTips.count - 1 }
|
||||||
|
|
||||||
|
func advanceSettings() {
|
||||||
|
if settingsIsLast { withAnimation(KisaniSpring.exit) { settingsDone = true } }
|
||||||
|
else { withAnimation(KisaniSpring.entrance) { settingsStep += 1 } }
|
||||||
|
}
|
||||||
|
func skipSettings() { withAnimation(KisaniSpring.exit) { settingsDone = true } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct SettingsView: View {
|
|||||||
@EnvironmentObject var taskVM: TaskViewModel
|
@EnvironmentObject var taskVM: TaskViewModel
|
||||||
@ObservedObject private var auth = AuthManager.shared
|
@ObservedObject private var auth = AuthManager.shared
|
||||||
@ObservedObject private var calStore = CalendarStore.shared
|
@ObservedObject private var calStore = CalendarStore.shared
|
||||||
|
@ObservedObject private var tm = TutorialManager.shared
|
||||||
|
|
||||||
// Persisted settings
|
// Persisted settings
|
||||||
@AppStorage("appearanceMode") private var appearanceMode = 0
|
@AppStorage("appearanceMode") private var appearanceMode = 0
|
||||||
@@ -189,6 +190,21 @@ struct SettingsView: View {
|
|||||||
Spacer().frame(height: 40)
|
Spacer().frame(height: 40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tm.settingsIsActive {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
InViewTutorialCard(
|
||||||
|
tips: tm.settingsTips,
|
||||||
|
step: $tm.settingsStep,
|
||||||
|
onAdvance: { tm.advanceSettings() },
|
||||||
|
onSkip: { tm.skipSettings() }
|
||||||
|
)
|
||||||
|
.padding(.bottom, 84)
|
||||||
|
}
|
||||||
|
.transition(.opacity)
|
||||||
|
.zIndex(10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showProfile) { ProfileStatsSheet().environmentObject(taskVM).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
|
.sheet(isPresented: $showProfile) { ProfileStatsSheet().environmentObject(taskVM).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
|
||||||
.sheet(isPresented: $showTabBar) { TabBarSheet(showMatrix: $showMatrixTab, showWorkout: $showWorkoutTab).presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
|
.sheet(isPresented: $showTabBar) { TabBarSheet(showMatrix: $showMatrixTab, showWorkout: $showWorkoutTab).presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
|
||||||
|
|||||||
Reference in New Issue
Block a user