- Add KeychainService for encrypted token storage (auth, refresh, health JWT, API key) - Remove hardcoded email/password from HealthAPIService, store in Keychain - Move all tokens from UserDefaults to Keychain - API key sent via X-API-Key header instead of URL query parameter - Replace force unwrap URL(string:)! with guard let + throws - Fix force unwrap Calendar.date() in HealthKitService - Mark HealthKitService @MainActor for thread-safe @Published - Use withTaskGroup for parallel habit log fetching in TrackerView - Check notification permission before scheduling reminders - Add input validation (title max 200 chars) - Add privacy policy and terms links in Settings - Update CLAUDE.md with security section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
173 lines
6.4 KiB
Swift
173 lines
6.4 KiB
Swift
import UserNotifications
|
||
import Foundation
|
||
|
||
class NotificationService {
|
||
static let shared = NotificationService()
|
||
|
||
// MARK: - Permission
|
||
|
||
func requestPermission() async -> Bool {
|
||
do {
|
||
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
|
||
return granted
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isAuthorized() async -> Bool {
|
||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||
return settings.authorizationStatus == .authorized
|
||
}
|
||
|
||
// MARK: - Morning Reminder
|
||
|
||
func scheduleMorningReminder(hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Доброе утро!"
|
||
content.body = "Посмотри свои привычки и задачи на сегодня"
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "morning_reminder", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Evening Reminder
|
||
|
||
func scheduleEveningReminder(hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Итоги дня"
|
||
content.body = "Проверь, все ли привычки выполнены сегодня"
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "evening_reminder", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Task Deadline Reminder
|
||
|
||
func scheduleTaskReminder(taskId: Int, title: String, dueDate: Date) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Задача скоро"
|
||
content.body = title
|
||
content.sound = .default
|
||
|
||
// За 1 час до дедлайна
|
||
let reminderDate = dueDate.addingTimeInterval(-3600)
|
||
guard reminderDate > Date() else { return }
|
||
|
||
let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminderDate)
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
||
let request = UNNotificationRequest(identifier: "task_\(taskId)", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Habit Reminder
|
||
|
||
func scheduleHabitReminder(habitId: Int, name: String, hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Привычка"
|
||
content.body = name
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "habit_\(habitId)", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Cancel
|
||
|
||
func cancelReminder(_ identifier: String) {
|
||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||
}
|
||
|
||
func cancelAllReminders() {
|
||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||
}
|
||
|
||
// MARK: - Payment Reminders
|
||
|
||
func schedulePaymentReminders(payments: [MonthlyPaymentDetail]) {
|
||
cancelPaymentReminders()
|
||
|
||
let cal = Calendar.current
|
||
let now = Date()
|
||
|
||
for payment in payments {
|
||
let day = payment.day
|
||
guard day >= 1, day <= 28 else { continue }
|
||
|
||
// Build due date for this month
|
||
var components = cal.dateComponents([.year, .month], from: now)
|
||
components.day = day
|
||
components.hour = 11
|
||
components.minute = 0
|
||
guard let dueDate = cal.date(from: components) else { continue }
|
||
|
||
let offsets = [(-5, "через 5 дней"), (-1, "завтра"), (0, "сегодня")]
|
||
|
||
for (offset, label) in offsets {
|
||
guard let notifDate = cal.date(byAdding: .day, value: offset, to: dueDate),
|
||
notifDate > now else { continue }
|
||
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Платёж \(label)"
|
||
content.body = "\(payment.categoryName): \(Int(payment.amount)) ₽ — \(day) числа"
|
||
content.sound = .default
|
||
|
||
let trigger = UNCalendarNotificationTrigger(
|
||
dateMatching: cal.dateComponents([.year, .month, .day, .hour, .minute], from: notifDate),
|
||
repeats: false
|
||
)
|
||
let id = "payment_\(payment.categoryId)_\(offset)"
|
||
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: id, content: content, trigger: trigger))
|
||
}
|
||
}
|
||
}
|
||
|
||
func cancelPaymentReminders() {
|
||
let center = UNUserNotificationCenter.current()
|
||
center.getPendingNotificationRequests { requests in
|
||
let ids = requests.filter { $0.identifier.hasPrefix("payment_") }.map(\.identifier)
|
||
center.removePendingNotificationRequests(withIdentifiers: ids)
|
||
}
|
||
}
|
||
|
||
// MARK: - Update from settings
|
||
|
||
func updateSchedule(morning: Bool, morningTime: String, evening: Bool, eveningTime: String) {
|
||
cancelReminder("morning_reminder")
|
||
cancelReminder("evening_reminder")
|
||
|
||
Task {
|
||
guard await isAuthorized() else { return }
|
||
if morning {
|
||
let parts = morningTime.split(separator: ":").compactMap { Int($0) }
|
||
if parts.count == 2 { scheduleMorningReminder(hour: parts[0], minute: parts[1]) }
|
||
}
|
||
if evening {
|
||
let parts = eveningTime.split(separator: ":").compactMap { Int($0) }
|
||
if parts.count == 2 { scheduleEveningReminder(hour: parts[0], minute: parts[1]) }
|
||
}
|
||
}
|
||
}
|
||
}
|