Merge develop -> main: full session (KC-51 through KC-71) #28

Merged
kutesir merged 64 commits from develop into main 2026-07-12 22:57:14 +00:00
3 changed files with 69 additions and 0 deletions
Showing only changes of commit dd69a6a2e4 - Show all commits

View File

@@ -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.

View File

@@ -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 } }
} }

View File

@@ -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) }