diff --git a/iOS/RSSuper/Services/NotificationManager.swift b/iOS/RSSuper/Services/NotificationManager.swift new file mode 100644 index 0000000..e2d4294 --- /dev/null +++ b/iOS/RSSuper/Services/NotificationManager.swift @@ -0,0 +1,59 @@ +import UserNotifications +import Foundation + +final class NotificationManager { + private init() {} + static let shared = NotificationManager() + + private let notificationService = NotificationService.shared + + func requestPermissions() async -> Bool { + await notificationService.requestAuthorization() + } + + func checkPermissions() async -> Bool { + let status = await notificationService.getAuthorizationStatus() + return status == .authorized || status == .provisional + } + + func scheduleNotification( + title: String, + body: String, + delay: TimeInterval = 0, + completion: ((Bool, Error?) -> Void)? = nil + ) { + notificationService.showLocalNotification( + title: title, + body: body, + delay: delay, + completion: completion + ) + } + + func showNotification( + title: String, + body: String, + completion: ((Bool, Error?) -> Void)? = nil + ) { + notificationService.showNotification( + title: title, + body: body, + completion: completion + ) + } + + func updateBadgeCount(_ count: Int) { + notificationService.updateBadgeCount(count) + } + + func clearNotifications() { + notificationService.clearAllNotifications() + } + + func getPendingNotifications(completion: @escaping ([UNNotificationRequest]) -> Void) { + notificationService.getDeliveredNotifications { notifications in + let requests = notifications.map { $0.request } + completion(requests) + } + } +} diff --git a/iOS/RSSuper/Services/NotificationPreferencesStore.swift b/iOS/RSSuper/Services/NotificationPreferencesStore.swift new file mode 100644 index 0000000..f6cf5a6 --- /dev/null +++ b/iOS/RSSuper/Services/NotificationPreferencesStore.swift @@ -0,0 +1,40 @@ +import Foundation + +final class NotificationPreferencesStore { + private static let userDefaultsKey = "notification_preferences" + + private init() {} + static let shared = NotificationPreferencesStore() + + private let userDefaults: UserDefaults + + private init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func save(_ preferences: NotificationPreferences) { + do { + let data = try JSONEncoder().encode(preferences) + userDefaults.set(data, forKey: Self.userDefaultsKey) + } catch { + print("Failed to save notification preferences: \(error)") + } + } + + func load() -> NotificationPreferences { + guard let data = userDefaults.data(forKey: Self.userDefaultsKey), + let preferences = try? JSONDecoder().decode(NotificationPreferences.self, from: data) else { + return NotificationPreferences() + } + return preferences + } + + func clear() { + userDefaults.removeObject(forKey: Self.userDefaultsKey) + } + + func resetToDefaults() { + let defaults = NotificationPreferences() + save(defaults) + } +} diff --git a/iOS/RSSuper/Services/NotificationService.swift b/iOS/RSSuper/Services/NotificationService.swift new file mode 100644 index 0000000..9cf5145 --- /dev/null +++ b/iOS/RSSuper/Services/NotificationService.swift @@ -0,0 +1,138 @@ +import UserNotifications +import Foundation + +final class NotificationService { + private init() {} + static let shared = NotificationService() + + private var notificationCenter: UNUserNotificationCenter { + UNUserNotificationCenter.current() + } + + func requestAuthorization() async -> Bool { + do { + let status = try await notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) + return status + } catch { + return false + } + } + + func getAuthorizationStatus() async -> UNAuthorizationStatus { + await notificationCenter.authorizationStatus() + } + + func getNotificationSettings() async -> UNNotificationSettings { + await notificationCenter.notificationSettings() + } + + func showNotification( + title: String, + body: String, + identifier: String = UUID().uuidString, + completion: ((Bool, Error?) -> Void)? = nil + ) { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.categoryIdentifier = "rssuper_notification" + content.sound = .default + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil + ) + + notificationCenter.add(request) { error in + completion?(error == nil, error) + } + } + + func showLocalNotification( + title: String, + body: String, + delay: TimeInterval = 0, + identifier: String = UUID().uuidString, + completion: ((Bool, Error?) -> Void)? = nil + ) { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.categoryIdentifier = "rssuper_notification" + content.sound = .default + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false) + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: trigger + ) + + notificationCenter.add(request) { error in + completion?(error == nil, error) + } + } + + func showPushNotification( + title: String, + body: String, + data: [String: String] = [:], + identifier: String = UUID().uuidString, + completion: ((Bool, Error?) -> Void)? = nil + ) { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.categoryIdentifier = "rssuper_notification" + content.sound = .default + + for (key, value) in data { + content.userInfo[key] = value + } + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil + ) + + notificationCenter.add(request) { error in + completion?(error == nil, error) + } + } + + func updateBadgeCount(_ count: Int) { + UIApplication.shared.applicationIconBadgeNumber = count + } + + func clearAllNotifications() { + notificationCenter.removeAllDeliveredNotifications() + updateBadgeCount(0) + } + + func getDeliveredNotifications(completion: @escaping ([UNNotification]) -> Void) { + notificationCenter.getDeliveredNotifications { notifications in + completion(notifications) + } + } + + func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { + notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers) + } + + func removePendingNotificationRequests(withIdentifiers identifiers: [String]) { + notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers) + } + + func addNotificationCategory() { + let category = UNNotificationCategory( + identifier: "rssuper_notification", + actions: [], + intentIdentifiers: [], + options: [] + ) + notificationCenter.setNotificationCategories([category]) + } +} diff --git a/native-route/ios/RSSuper/Info.plist b/native-route/ios/RSSuper/Info.plist index 50b275a..602cff5 100644 --- a/native-route/ios/RSSuper/Info.plist +++ b/native-route/ios/RSSuper/Info.plist @@ -18,6 +18,8 @@ 1.0 CFBundleVersion 1 + AppGroupID + group.com.rssuper.shared NSLocationWhenInUseUsageDescription We need your location to provide nearby feed updates. NSUserNotificationsUsageDescription