import UserNotifications /// Notification Service Extension — runs as a separate process even when the main app /// is terminated. Intercepts push notifications with `mutable-content: 1` and: /// 1. Adds `.default` sound for vibration (server payload has no sound) /// 2. Increments the app icon badge from shared App Group storage final class NotificationService: UNNotificationServiceExtension { private static let appGroupID = "group.com.rosetta.dev" private static let badgeKey = "app_badge_count" private var contentHandler: ((UNNotificationContent) -> Void)? private var bestAttemptContent: UNMutableNotificationContent? override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { self.contentHandler = contentHandler bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent guard let content = bestAttemptContent else { contentHandler(request.content) return } // 1. Add sound for vibration — server APNs payload has no sound field. content.sound = .default // 2. Increment badge count from shared App Group storage. if let shared = UserDefaults(suiteName: Self.appGroupID) { let current = shared.integer(forKey: Self.badgeKey) let newBadge = current + 1 shared.set(newBadge, forKey: Self.badgeKey) content.badge = NSNumber(value: newBadge) } // 3. Ensure notification category for CarPlay parity. if content.categoryIdentifier.isEmpty { content.categoryIdentifier = "message" } contentHandler(content) } /// Called if the extension takes too long (30s limit). /// Deliver the best attempt content with at least the sound set. override func serviceExtensionTimeWillExpire() { if let handler = contentHandler, let content = bestAttemptContent { content.sound = .default handler(content) } } }