6fe16d8e29
- MealSlot.isSkipped: opcion "No planificar esta comida" en el picker de hueco vacio — cuenta como resuelto (semana completa, notificaciones, stats) y el autocompletado lo respeta; vista gris con guion, tap para desmarcar; viaja en undo, snapshot KV (junto con isEatingOut, que faltaba en el payload) y exports (— en imagen, omitido en texto) - ShoppingItem.isDismissed: "Ya lo tengo" por ingrediente (swipe izquierdo) y "Ya esta hecho" por plato entero (boton en la cabecera de su seccion); van a la seccion "Ya en casa" con restauracion de un tap, y quedan fuera del export de texto. No se borran porque reconcile los recrearia - Esquema CloudKit Development: CD_isSkipped, CD_isDismissed y el record type CD_ShoppingItem entero, que no existia — la lista de la compra no estaba sincronizando entre dispositivos (mismo bug que photoData) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
121 lines
4.9 KiB
Swift
121 lines
4.9 KiB
Swift
import Foundation
|
|
import UserNotifications
|
|
|
|
@MainActor
|
|
final class NotificationService {
|
|
static let shared = NotificationService()
|
|
static let remindersEnabledKey = "notifications_reminders_enabled"
|
|
|
|
private static let allReminderIdentifiers = [
|
|
"mealmood.next-week-planning",
|
|
"mealmood.sunday-planning"
|
|
]
|
|
|
|
private init() {}
|
|
|
|
/// User-level preference for weekly planning reminders (independent of OS permission).
|
|
var remindersEnabled: Bool {
|
|
get {
|
|
UserDefaults.standard.object(forKey: Self.remindersEnabledKey) as? Bool ?? true
|
|
}
|
|
set {
|
|
UserDefaults.standard.set(newValue, forKey: Self.remindersEnabledKey)
|
|
if !newValue { cancelAllReminders() }
|
|
}
|
|
}
|
|
|
|
func authorizationStatus() async -> UNAuthorizationStatus {
|
|
await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
|
}
|
|
|
|
/// Explicit permission request for contextual prompts (onboarding, settings toggle).
|
|
/// Returns whether notifications are authorized after the request.
|
|
@discardableResult
|
|
func requestPermission() async -> Bool {
|
|
let center = UNUserNotificationCenter.current()
|
|
let settings = await center.notificationSettings()
|
|
switch settings.authorizationStatus {
|
|
case .notDetermined:
|
|
return (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
|
case .authorized, .provisional, .ephemeral:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func requestPermissionIfNeeded() async {
|
|
let center = UNUserNotificationCenter.current()
|
|
let settings = await center.notificationSettings()
|
|
guard settings.authorizationStatus == .notDetermined else { return }
|
|
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
|
}
|
|
|
|
func cancelAllReminders() {
|
|
UNUserNotificationCenter.current()
|
|
.removePendingNotificationRequests(withIdentifiers: Self.allReminderIdentifiers)
|
|
}
|
|
|
|
func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
|
guard remindersEnabled else {
|
|
cancelAllReminders()
|
|
return
|
|
}
|
|
let center = UNUserNotificationCenter.current()
|
|
let identifier = "mealmood.next-week-planning"
|
|
|
|
let isComplete = nextWeekPlan?.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut || $0.isSkipped } ?? false
|
|
if isComplete {
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
} else {
|
|
let nextWeekStart = Date().startOfWeek().addingDays(7)
|
|
let reminderDay = nextWeekStart.addingDays(-1)
|
|
let calendar = Calendar.current
|
|
var components = calendar.dateComponents([.year, .month, .day], from: reminderDay)
|
|
components.hour = 19
|
|
components.minute = 0
|
|
|
|
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
|
let content = UNMutableNotificationContent()
|
|
content.title = localizedString("notification_planning_title", language: language)
|
|
content.body = localizedString("notification_planning_body", language: language)
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
center.add(request)
|
|
}
|
|
|
|
scheduleSundayReminderIfNeeded(nextWeekPlan: nextWeekPlan, language: language)
|
|
}
|
|
|
|
private func scheduleSundayReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
|
let center = UNUserNotificationCenter.current()
|
|
let identifier = "mealmood.sunday-planning"
|
|
|
|
// Cancel if next week is already planned
|
|
let hasAnySlot = nextWeekPlan?.slotList.contains { $0.dishId != nil || $0.isEatingOut || $0.isSkipped } ?? false
|
|
if hasAnySlot {
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
return
|
|
}
|
|
|
|
// Schedule for next Sunday at 17:00
|
|
var components = DateComponents()
|
|
components.weekday = 1 // Sunday
|
|
components.hour = 17
|
|
components.minute = 0
|
|
|
|
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
|
let content = UNMutableNotificationContent()
|
|
content.title = localizedString("notification_sunday_title", language: language)
|
|
content.body = localizedString("notification_sunday_body", language: language)
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
center.add(request)
|
|
}
|
|
}
|
|
|