Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2c5ee7490c
commit
ed26a9d367
@ -19,8 +19,9 @@ struct AndromidaApp: App {
|
|||||||
fatalError("Unable to create model container: \(error)")
|
fatalError("Unable to create model container: \(error)")
|
||||||
}
|
}
|
||||||
modelContainer = container
|
modelContainer = container
|
||||||
_store = State(initialValue: RitualStore(modelContext: container.mainContext, seedService: RitualSeedService()))
|
let settings = SettingsStore()
|
||||||
_settingsStore = State(initialValue: SettingsStore())
|
_settingsStore = State(initialValue: settings)
|
||||||
|
_store = State(initialValue: RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"" : {
|
||||||
|
|
||||||
|
},
|
||||||
"%lld of %lld habits complete" : {
|
"%lld of %lld habits complete" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -67,6 +70,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"A relaxed pace for building habits slowly" : {
|
||||||
|
"comment" : "Description of what \"Gentle\" focus style means for the user.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"About" : {
|
"About" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -159,6 +166,10 @@
|
|||||||
"comment" : "Subtitle for a feature in the \"Rituals Pro\" upgrade screen, related to \"Advanced Insights\".",
|
"comment" : "Subtitle for a feature in the \"Rituals Pro\" upgrade screen, related to \"Advanced Insights\".",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Balanced daily check-ins" : {
|
||||||
|
"comment" : "Description of what the \"Steady\" focus style means for the user.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Begin a four-week arc" : {
|
"Begin a four-week arc" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -646,6 +657,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Focused approach with more accountability" : {
|
||||||
|
"comment" : "Description of the \"Intense\" focus style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Four-week arc in progress" : {
|
"Four-week arc in progress" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1156,6 +1171,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Notifications disabled in Settings" : {
|
||||||
|
"comment" : "Subtitle text for the reminder toggle when notifications are disabled in settings.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Play subtle completion sounds" : {
|
"Play subtle completion sounds" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1272,6 +1291,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Reminder time" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Reset Onboarding" : {
|
"Reset Onboarding" : {
|
||||||
"comment" : "Title of a navigation row in the Settings view that resets the user's onboarding state.",
|
"comment" : "Title of a navigation row in the Settings view that resets the user's onboarding state.",
|
||||||
@ -1758,6 +1780,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Take a moment to check in on your daily habits." : {
|
||||||
|
"comment" : "Body text for a daily reminder notification.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Tap a habit to check in" : {
|
"Tap a habit to check in" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1802,6 +1828,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Time for your rituals" : {
|
||||||
|
"comment" : "Title of a notification displayed at the start of the day.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Today" : {
|
"Today" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -5,7 +5,9 @@ struct AppSettingsData: PersistableData {
|
|||||||
static var dataIdentifier: String = "rituals.settings"
|
static var dataIdentifier: String = "rituals.settings"
|
||||||
static var empty = AppSettingsData()
|
static var empty = AppSettingsData()
|
||||||
|
|
||||||
var remindersEnabled: Bool = true
|
var remindersEnabled: Bool = false // Default off until user enables
|
||||||
|
var reminderHour: Int = 8 // Default 8:00 AM
|
||||||
|
var reminderMinute: Int = 0
|
||||||
var hapticsEnabled: Bool = true
|
var hapticsEnabled: Bool = true
|
||||||
var soundEnabled: Bool = true
|
var soundEnabled: Bool = true
|
||||||
var focusStyle: FocusStyle = .gentle
|
var focusStyle: FocusStyle = .gentle
|
||||||
@ -13,8 +15,21 @@ struct AppSettingsData: PersistableData {
|
|||||||
var lastModified: Date = .now
|
var lastModified: Date = .now
|
||||||
|
|
||||||
var syncPriority: Int { ritualLengthDays }
|
var syncPriority: Int { ritualLengthDays }
|
||||||
|
|
||||||
|
/// Returns the reminder time as DateComponents for scheduling.
|
||||||
|
var reminderTimeComponents: DateComponents {
|
||||||
|
var components = DateComponents()
|
||||||
|
components.hour = reminderHour
|
||||||
|
components.minute = reminderMinute
|
||||||
|
return components
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines the intensity of ritual pacing.
|
||||||
|
///
|
||||||
|
/// - `gentle`: Relaxed approach with softer reminders. Good for building habits slowly.
|
||||||
|
/// - `steady`: Balanced default for most users. Consistent daily check-ins.
|
||||||
|
/// - `intense`: More focused approach with more frequent engagement. For users who want accountability.
|
||||||
enum FocusStyle: String, CaseIterable, Codable, Identifiable {
|
enum FocusStyle: String, CaseIterable, Codable, Identifiable {
|
||||||
case gentle
|
case gentle
|
||||||
case steady
|
case steady
|
||||||
@ -29,4 +44,13 @@ enum FocusStyle: String, CaseIterable, Codable, Identifiable {
|
|||||||
case .intense: return String(localized: "Intense")
|
case .intense: return String(localized: "Intense")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Description of what this focus style means for the user.
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .gentle: return String(localized: "A relaxed pace for building habits slowly")
|
||||||
|
case .steady: return String(localized: "Balanced daily check-ins")
|
||||||
|
case .intense: return String(localized: "Focused approach with more accountability")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
Andromida/App/Services/NotificationService.swift
Normal file
111
Andromida/App/Services/NotificationService.swift
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// NotificationService.swift
|
||||||
|
// Andromida
|
||||||
|
//
|
||||||
|
// Service for scheduling daily reminder notifications.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class NotificationService {
|
||||||
|
|
||||||
|
// MARK: - Singleton
|
||||||
|
|
||||||
|
static let shared = NotificationService()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
// MARK: - Authorization
|
||||||
|
|
||||||
|
/// Requests notification authorization from the user.
|
||||||
|
/// - Returns: `true` if authorization was granted.
|
||||||
|
func requestAuthorization() async -> Bool {
|
||||||
|
do {
|
||||||
|
let granted = try await UNUserNotificationCenter.current().requestAuthorization(
|
||||||
|
options: [.alert, .sound, .badge]
|
||||||
|
)
|
||||||
|
return granted
|
||||||
|
} catch {
|
||||||
|
print("Notification authorization error: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current authorization status.
|
||||||
|
var authorizationStatus: UNAuthorizationStatus {
|
||||||
|
get async {
|
||||||
|
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||||
|
return settings.authorizationStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether notifications are currently authorized.
|
||||||
|
var isAuthorized: Bool {
|
||||||
|
get async {
|
||||||
|
let status = await authorizationStatus
|
||||||
|
return status == .authorized || status == .provisional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Scheduling
|
||||||
|
|
||||||
|
/// Schedules a daily reminder notification at the specified time.
|
||||||
|
/// - Parameter time: The time components (hour, minute) for the reminder.
|
||||||
|
func scheduleDailyReminder(at time: DateComponents) async {
|
||||||
|
// Request authorization if not already granted
|
||||||
|
var canSchedule = await isAuthorized
|
||||||
|
if !canSchedule {
|
||||||
|
canSchedule = await requestAuthorization()
|
||||||
|
}
|
||||||
|
guard canSchedule else { return }
|
||||||
|
|
||||||
|
// Cancel existing reminders before scheduling new one
|
||||||
|
cancelAllReminders()
|
||||||
|
|
||||||
|
// Create notification content
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = String(localized: "Time for your rituals")
|
||||||
|
content.body = String(localized: "Take a moment to check in on your daily habits.")
|
||||||
|
content.sound = .default
|
||||||
|
content.badge = 1
|
||||||
|
|
||||||
|
// Create daily trigger
|
||||||
|
var triggerComponents = DateComponents()
|
||||||
|
triggerComponents.hour = time.hour ?? 8
|
||||||
|
triggerComponents.minute = time.minute ?? 0
|
||||||
|
|
||||||
|
let trigger = UNCalendarNotificationTrigger(
|
||||||
|
dateMatching: triggerComponents,
|
||||||
|
repeats: true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create and schedule the request
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: "rituals.daily.reminder",
|
||||||
|
content: content,
|
||||||
|
trigger: trigger
|
||||||
|
)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await UNUserNotificationCenter.current().add(request)
|
||||||
|
} catch {
|
||||||
|
print("Failed to schedule notification: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancels all scheduled reminder notifications.
|
||||||
|
func cancelAllReminders() {
|
||||||
|
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||||
|
withIdentifiers: ["rituals.daily.reminder"]
|
||||||
|
)
|
||||||
|
// Also clear the badge
|
||||||
|
UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the app badge count.
|
||||||
|
func clearBadge() {
|
||||||
|
UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,6 @@ extension RitualStore {
|
|||||||
} catch {
|
} catch {
|
||||||
fatalError("Preview container failed: \(error)")
|
fatalError("Preview container failed: \(error)")
|
||||||
}
|
}
|
||||||
return RitualStore(modelContext: container.mainContext, seedService: RitualSeedService())
|
return RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: SettingsStore())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
final class RitualStore: RitualStoreProviding {
|
final class RitualStore: RitualStoreProviding {
|
||||||
@ObservationIgnored private let modelContext: ModelContext
|
@ObservationIgnored private let modelContext: ModelContext
|
||||||
@ObservationIgnored private let seedService: RitualSeedProviding
|
@ObservationIgnored private let seedService: RitualSeedProviding
|
||||||
|
@ObservationIgnored private let settingsStore: SettingsStore
|
||||||
@ObservationIgnored private let calendar: Calendar
|
@ObservationIgnored private let calendar: Calendar
|
||||||
@ObservationIgnored private let dayFormatter: DateFormatter
|
@ObservationIgnored private let dayFormatter: DateFormatter
|
||||||
@ObservationIgnored private let displayFormatter: DateFormatter
|
@ObservationIgnored private let displayFormatter: DateFormatter
|
||||||
@ -17,10 +19,12 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
init(
|
init(
|
||||||
modelContext: ModelContext,
|
modelContext: ModelContext,
|
||||||
seedService: RitualSeedProviding,
|
seedService: RitualSeedProviding,
|
||||||
|
settingsStore: SettingsStore,
|
||||||
calendar: Calendar = .current
|
calendar: Calendar = .current
|
||||||
) {
|
) {
|
||||||
self.modelContext = modelContext
|
self.modelContext = modelContext
|
||||||
self.seedService = seedService
|
self.seedService = seedService
|
||||||
|
self.settingsStore = settingsStore
|
||||||
self.calendar = calendar
|
self.calendar = calendar
|
||||||
self.dayFormatter = DateFormatter()
|
self.dayFormatter = DateFormatter()
|
||||||
self.displayFormatter = DateFormatter()
|
self.displayFormatter = DateFormatter()
|
||||||
@ -72,10 +76,19 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
func toggleHabitCompletion(_ habit: Habit) {
|
func toggleHabitCompletion(_ habit: Habit) {
|
||||||
let dayID = dayIdentifier(for: Date())
|
let dayID = dayIdentifier(for: Date())
|
||||||
if habit.completedDayIDs.contains(dayID) {
|
let wasCompleted = habit.completedDayIDs.contains(dayID)
|
||||||
|
|
||||||
|
if wasCompleted {
|
||||||
habit.completedDayIDs.removeAll { $0 == dayID }
|
habit.completedDayIDs.removeAll { $0 == dayID }
|
||||||
} else {
|
} else {
|
||||||
habit.completedDayIDs.append(dayID)
|
habit.completedDayIDs.append(dayID)
|
||||||
|
// Play feedback on check-in (not on uncheck)
|
||||||
|
if settingsStore.hapticsEnabled {
|
||||||
|
SoundManager.shared.playHaptic(.success)
|
||||||
|
}
|
||||||
|
if settingsStore.soundEnabled {
|
||||||
|
SoundManager.shared.playSystemSound(SystemSound.success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
@ -150,7 +163,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
title: String(localized: "Custom Ritual"),
|
title: String(localized: "Custom Ritual"),
|
||||||
theme: String(localized: "Your next chapter"),
|
theme: String(localized: "Your next chapter"),
|
||||||
startDate: Date(),
|
startDate: Date(),
|
||||||
durationDays: 28,
|
durationDays: Int(settingsStore.ritualLengthDays),
|
||||||
habits: habits,
|
habits: habits,
|
||||||
notes: String(localized: "A fresh ritual created from your focus today.")
|
notes: String(localized: "A fresh ritual created from your focus today.")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,15 +1,57 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Bedrock
|
import Bedrock
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
final class SettingsStore: CloudSyncable {
|
final class SettingsStore: CloudSyncable {
|
||||||
@ObservationIgnored private let cloudSync = CloudSyncManager<AppSettingsData>()
|
@ObservationIgnored private let cloudSync = CloudSyncManager<AppSettingsData>()
|
||||||
|
|
||||||
|
/// The current notification authorization status.
|
||||||
|
private(set) var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
|
||||||
|
|
||||||
var remindersEnabled: Bool {
|
var remindersEnabled: Bool {
|
||||||
get { cloudSync.data.remindersEnabled }
|
get { cloudSync.data.remindersEnabled }
|
||||||
set { update { $0.remindersEnabled = newValue } }
|
set {
|
||||||
|
update { $0.remindersEnabled = newValue }
|
||||||
|
Task { await handleReminderToggle(enabled: newValue) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reminderHour: Int {
|
||||||
|
get { cloudSync.data.reminderHour }
|
||||||
|
set {
|
||||||
|
update { $0.reminderHour = newValue }
|
||||||
|
if remindersEnabled {
|
||||||
|
Task { await scheduleReminder() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reminderMinute: Int {
|
||||||
|
get { cloudSync.data.reminderMinute }
|
||||||
|
set {
|
||||||
|
update { $0.reminderMinute = newValue }
|
||||||
|
if remindersEnabled {
|
||||||
|
Task { await scheduleReminder() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Date representing the reminder time for use with DatePicker.
|
||||||
|
var reminderTime: Date {
|
||||||
|
get {
|
||||||
|
var components = DateComponents()
|
||||||
|
components.hour = reminderHour
|
||||||
|
components.minute = reminderMinute
|
||||||
|
return Calendar.current.date(from: components) ?? Date()
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let components = Calendar.current.dateComponents([.hour, .minute], from: newValue)
|
||||||
|
reminderHour = components.hour ?? 8
|
||||||
|
reminderMinute = components.minute ?? 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hapticsEnabled: Bool {
|
var hapticsEnabled: Bool {
|
||||||
@ -43,15 +85,41 @@ final class SettingsStore: CloudSyncable {
|
|||||||
var syncStatus: String { cloudSync.syncStatus }
|
var syncStatus: String { cloudSync.syncStatus }
|
||||||
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Task { await refreshNotificationStatus() }
|
||||||
|
}
|
||||||
|
|
||||||
func forceSync() {
|
func forceSync() {
|
||||||
cloudSync.sync()
|
cloudSync.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refreshes the notification authorization status.
|
||||||
|
func refreshNotificationStatus() async {
|
||||||
|
notificationAuthStatus = await NotificationService.shared.authorizationStatus
|
||||||
|
}
|
||||||
|
|
||||||
private func update(_ transform: (inout AppSettingsData) -> Void) {
|
private func update(_ transform: (inout AppSettingsData) -> Void) {
|
||||||
cloudSync.update { data in
|
cloudSync.update { data in
|
||||||
transform(&data)
|
transform(&data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification Handling
|
||||||
|
|
||||||
|
private func handleReminderToggle(enabled: Bool) async {
|
||||||
|
if enabled {
|
||||||
|
await scheduleReminder()
|
||||||
|
} else {
|
||||||
|
NotificationService.shared.cancelAllReminders()
|
||||||
|
}
|
||||||
|
await refreshNotificationStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleReminder() async {
|
||||||
|
let components = cloudSync.data.reminderTimeComponents
|
||||||
|
await NotificationService.shared.scheduleDailyReminder(at: components)
|
||||||
|
await refreshNotificationStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SettingsStore {
|
extension SettingsStore {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bedrock
|
import Bedrock
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Bindable var store: SettingsStore
|
@Bindable var store: SettingsStore
|
||||||
@ -18,14 +19,30 @@ struct SettingsView: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: String(localized: "Daily reminders"),
|
title: String(localized: "Daily reminders"),
|
||||||
subtitle: String(localized: "Get a gentle check-in each morning"),
|
subtitle: reminderSubtitle,
|
||||||
isOn: $store.remindersEnabled,
|
isOn: $store.remindersEnabled,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if store.remindersEnabled {
|
||||||
|
HStack {
|
||||||
|
Text(String(localized: "Reminder time"))
|
||||||
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
Spacer()
|
||||||
|
DatePicker(
|
||||||
|
"",
|
||||||
|
selection: $store.reminderTime,
|
||||||
|
displayedComponents: .hourAndMinute
|
||||||
|
)
|
||||||
|
.labelsHidden()
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: String(localized: "Haptics"),
|
title: String(localized: "Haptics"),
|
||||||
subtitle: String(localized: "Feel a soft response on check-in"),
|
subtitle: String(localized: "Vibrate when completing habits"),
|
||||||
isOn: $store.hapticsEnabled,
|
isOn: $store.hapticsEnabled,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -161,6 +178,23 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Computed Properties
|
||||||
|
|
||||||
|
extension SettingsView {
|
||||||
|
private var reminderSubtitle: String {
|
||||||
|
switch store.notificationAuthStatus {
|
||||||
|
case .denied:
|
||||||
|
return String(localized: "Notifications disabled in Settings")
|
||||||
|
case .notDetermined:
|
||||||
|
return String(localized: "Get a gentle check-in each morning")
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
return String(localized: "Get a gentle check-in each morning")
|
||||||
|
@unknown default:
|
||||||
|
return String(localized: "Get a gentle check-in each morning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
SettingsView(store: SettingsStore.preview)
|
SettingsView(store: SettingsStore.preview)
|
||||||
|
|||||||
72
TODO.md
72
TODO.md
@ -15,17 +15,71 @@
|
|||||||
- [x] Re-add accessibility value/hint for habit rows once Sherpa-related ambiguity is resolved.
|
- [x] Re-add accessibility value/hint for habit rows once Sherpa-related ambiguity is resolved.
|
||||||
- [x] Confirm focus ritual card and habit rows still match the intended visual hierarchy after refactors.
|
- [x] Confirm focus ritual card and habit rows still match the intended visual hierarchy after refactors.
|
||||||
|
|
||||||
## 4) Settings & product readiness
|
## 4) QA checklist
|
||||||
- [x] Add a paid-app placeholder (e.g., "Pro unlock" copy) without backend requirements.
|
|
||||||
- [ ] Confirm default settings and theme in Settings match Bedrock branding.
|
|
||||||
|
|
||||||
## 5) Data & defaults
|
|
||||||
- [ ] Confirm seed ritual creation and quick ritual creation behave as expected.
|
|
||||||
- [ ] Validate SwiftData sync (if enabled) doesn't require any external API.
|
|
||||||
|
|
||||||
## 6) QA checklist
|
|
||||||
- [x] First-launch walkthrough appears on a clean install.
|
- [x] First-launch walkthrough appears on a clean install.
|
||||||
- [x] Onboarding can be manually reset from Settings.
|
- [x] Onboarding can be manually reset from Settings.
|
||||||
- [x] No build warnings or Swift compiler crashes.
|
- [x] No build warnings or Swift compiler crashes.
|
||||||
- [x] iPhone 17 Pro Max simulator layout verified on Today, Rituals, Insights, Settings.
|
- [x] iPhone 17 Pro Max simulator layout verified on Today, Rituals, Insights, Settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRIORITY: Wire up existing settings
|
||||||
|
|
||||||
|
### 5) Haptic feedback ⚡
|
||||||
|
- [ ] Add haptic feedback on habit check-in using `UIImpactFeedbackGenerator`.
|
||||||
|
- [ ] Respect `hapticsEnabled` setting from SettingsStore.
|
||||||
|
- [ ] Add haptics to other interactions (ritual creation, onboarding completion).
|
||||||
|
|
||||||
|
### 6) Sound effects ⚡
|
||||||
|
- [ ] Add completion sound when habit is checked in.
|
||||||
|
- [ ] Respect `soundEnabled` setting from SettingsStore.
|
||||||
|
- [ ] Use Bedrock `SoundManager` if available, or create audio service.
|
||||||
|
|
||||||
|
### 7) Daily reminders (notifications) ⚡
|
||||||
|
- [ ] Request notification permission when "Daily reminders" is enabled.
|
||||||
|
- [ ] Schedule daily local notification at user-preferred time.
|
||||||
|
- [ ] Add time picker to Settings for reminder time.
|
||||||
|
- [ ] Cancel notifications when setting is disabled.
|
||||||
|
- [ ] Handle notification authorization denied state in UI.
|
||||||
|
|
||||||
|
### 8) Ritual pacing settings ⚡
|
||||||
|
- [ ] Use `ritualLengthDays` setting when creating new rituals via `createQuickRitual()`.
|
||||||
|
- [ ] Use `focusStyle` setting to affect ritual recommendations or insights.
|
||||||
|
- [ ] Consider adding visual indicator of current pacing in Today view.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lower priority
|
||||||
|
|
||||||
|
### 9) Settings & product readiness
|
||||||
|
- [x] Add a paid-app placeholder (e.g., "Pro unlock" copy) without backend requirements.
|
||||||
|
- [ ] Confirm default settings and theme in Settings match Bedrock branding.
|
||||||
|
|
||||||
|
### 10) Data & defaults
|
||||||
|
- [ ] Confirm seed ritual creation and quick ritual creation behave as expected.
|
||||||
|
- [ ] Validate SwiftData sync (if enabled) doesn't require any external API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future features
|
||||||
|
|
||||||
|
### 11) History view
|
||||||
|
- [ ] Add History tab or section to view completed/past rituals.
|
||||||
|
- [ ] Show completion percentage for each past ritual arc.
|
||||||
|
- [ ] Allow viewing habits and check-in history for past rituals.
|
||||||
|
|
||||||
|
### 12) Ritual management
|
||||||
|
- [ ] Add ability to create custom rituals (not just quick ritual).
|
||||||
|
- [ ] Add ability to edit existing rituals (title, theme, habits).
|
||||||
|
- [ ] Add ability to delete rituals.
|
||||||
|
- [ ] Add ability to archive completed rituals.
|
||||||
|
|
||||||
|
### 13) Insights enhancements
|
||||||
|
- [ ] Show weekly/monthly trends.
|
||||||
|
- [ ] Show streak data (consecutive days with all habits completed).
|
||||||
|
- [ ] Add charts or visualizations for progress over time.
|
||||||
|
|
||||||
|
### 14) Future enhancements
|
||||||
|
- [ ] **HealthKit integration** – See plan: `.cursor/plans/healthkit_integration_plan_ce4f933c.plan.md`
|
||||||
|
- [ ] **Widget** – Home screen widget showing today's progress.
|
||||||
|
- [ ] **Watch app** – Companion app for quick habit check-ins.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user