Files
pulse-mobile/PulseHealth/Services/NotificationService.swift
Daniil Klimov 44c759c190 fix: security hardening — Keychain, no hardcoded creds, safe URLs
- 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>
2026-04-06 14:11:10 +03:00

173 lines
6.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]) }
}
}
}
}