Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-27 13:29:24 -06:00
parent b104bc3212
commit 07f75bd766
5 changed files with 158 additions and 2 deletions

View File

@ -2386,6 +2386,10 @@
"comment" : "A hint that appears when a user taps on a day cell to explain that it will navigate to more details about that day.", "comment" : "A hint that appears when a user taps on a day cell to explain that it will navigate to more details about that day.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Test Notification" : {
"comment" : "Title of a test notification used to verify notification delivery.",
"isCommentAutoGenerated" : true
},
"Thank someone" : { "Thank someone" : {
"comment" : "Habit title for a ritual preset focused on gratitude practice.", "comment" : "Habit title for a ritual preset focused on gratitude practice.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2409,6 +2413,10 @@
"comment" : "A description displayed when a day has no habit completion data.", "comment" : "A description displayed when a day has no habit completion data.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"This is a test notification to verify delivery." : {
"comment" : "Title and body of a test notification used to verify notification delivery.",
"isCommentAutoGenerated" : true
},
"This ritual is not currently active" : { "This ritual is not currently active" : {
"comment" : "A message displayed when a user views a past ritual and it is not currently active.", "comment" : "A message displayed when a user views a past ritual and it is not currently active.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2506,6 +2514,10 @@
"comment" : "Accessibility label for a trend direction indicating an increase.", "comment" : "Accessibility label for a trend direction indicating an increase.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Trigger Test Notification (5s)" : {
"comment" : "Title of a settings option that triggers a test local notification.",
"isCommentAutoGenerated" : true
},
"Try a different search term" : { "Try a different search term" : {
"comment" : "A description text displayed below the \"No icons found\" message in the icon picker sheet.", "comment" : "A description text displayed below the \"No icons found\" message in the icon picker sheet.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true

View File

@ -48,7 +48,7 @@ enum ReminderSlot: String, CaseIterable {
/// Only schedules reminders for time slots that have active rituals. /// Only schedules reminders for time slots that have active rituals.
@MainActor @MainActor
@Observable @Observable
final class ReminderScheduler { final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
private(set) var authorizationStatus: UNAuthorizationStatus = .notDetermined private(set) var authorizationStatus: UNAuthorizationStatus = .notDetermined
private(set) var scheduledSlots: Set<ReminderSlot> = [] private(set) var scheduledSlots: Set<ReminderSlot> = []
@ -69,10 +69,33 @@ final class ReminderScheduler {
/// The rituals to base reminders on - set this from RitualStore /// The rituals to base reminders on - set this from RitualStore
private var activeRituals: [Ritual] = [] private var activeRituals: [Ritual] = []
init() { override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
Task { await refreshAuthorizationStatus() } Task { await refreshAuthorizationStatus() }
} }
// MARK: - UNUserNotificationCenterDelegate
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Show the notification even when the app is in the foreground
completionHandler([.banner, .list, .sound, .badge])
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Clear badge when user interacts with notification
clearBadge()
completionHandler()
}
// MARK: - Public API // MARK: - Public API
/// Updates the scheduled reminders based on the current active rituals. /// Updates the scheduled reminders based on the current active rituals.
@ -116,6 +139,28 @@ final class ReminderScheduler {
UNUserNotificationCenter.current().setBadgeCount(0) { _ in } UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
} }
/// Schedules a test notification to appear in 5 seconds.
func scheduleTestNotification() {
let content = UNMutableNotificationContent()
content.title = String(localized: "Test Notification")
content.body = String(localized: "This is a test notification to verify delivery.")
content.sound = .default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(
identifier: "rituals.test.notification",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule test notification: \(error)")
}
}
}
/// Refreshes authorization status and reschedules if enabled. /// Refreshes authorization status and reschedules if enabled.
func refreshStatus() async { func refreshStatus() async {
await refreshAuthorizationStatus() await refreshAuthorizationStatus()

View File

@ -72,6 +72,7 @@ struct RootView: View {
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.onChange(of: scenePhase) { _, newPhase in .onChange(of: scenePhase) { _, newPhase in
if newPhase == .active { if newPhase == .active {
store.reminderScheduler.clearBadge()
refreshCurrentTab() refreshCurrentTab()
} }
} }

View File

@ -154,6 +154,14 @@ struct SettingsView: View {
) { ) {
ritualStore.clearAllCompletions() ritualStore.clearAllCompletions()
} }
SettingsRow(
systemImage: "bell.badge",
title: String(localized: "Trigger Test Notification (5s)"),
iconColor: AppStatus.info
) {
ritualStore.reminderScheduler.scheduleTestNotification()
}
} }
} }
#endif #endif

View File

@ -6,6 +6,96 @@
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"
}, },
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
},
{ {
"appearances" : [ "appearances" : [
{ {