// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT import SwiftRs import Tauri import UIKit import UserNotifications import WebKit enum ShowNotificationError: LocalizedError { case make(Error) case create(Error) var errorDescription: String? { switch self { case .make(let error): return "Unable to make notification: \(error)" case .create(let error): return "Unable to create notification: \(error)" } } } enum ScheduleEveryKind: String, Decodable { case year case month case twoWeeks case week case day case hour case minute case second } struct ScheduleInterval: Decodable { var year: Int? var month: Int? var day: Int? var weekday: Int? var hour: Int? var minute: Int? var second: Int? } enum NotificationSchedule: Decodable { case at(date: String, repeating: Bool) case interval(interval: ScheduleInterval) case every(interval: ScheduleEveryKind, count: Int) } struct NotificationAttachmentOptions: Codable { let iosUNNotificationAttachmentOptionsTypeHintKey: String? let iosUNNotificationAttachmentOptionsThumbnailHiddenKey: String? let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey: String? let iosUNNotificationAttachmentOptionsThumbnailTimeKey: String? } struct NotificationAttachment: Codable { let id: String let url: String let options: NotificationAttachmentOptions? } struct Notification: Decodable { let id: Int var title: String var body: String? var extra: [String: String]? var schedule: NotificationSchedule? var attachments: [NotificationAttachment]? var sound: String? var group: String? var actionTypeId: String? var summary: String? var silent: Bool? } struct RemoveActiveNotification: Decodable { let id: Int } struct RemoveActiveArgs: Decodable { let notifications: [RemoveActiveNotification] } func showNotification(invoke: Invoke, notification: Notification) throws -> UNNotificationRequest { var content: UNNotificationContent do { content = try makeNotificationContent(notification) } catch { throw ShowNotificationError.make(error) } var trigger: UNNotificationTrigger? do { if let schedule = notification.schedule { try trigger = handleScheduledNotification(schedule) } } catch { throw ShowNotificationError.create(error) } // Schedule the request. let request = UNNotificationRequest( identifier: "\(notification.id)", content: content, trigger: trigger ) let center = UNUserNotificationCenter.current() center.add(request) { (error: Error?) in if let theError = error { invoke.reject(theError.localizedDescription) } } return request } struct CancelArgs: Decodable { let notifications: [Int] } struct Action: Decodable { let id: String let title: String var requiresAuthentication: Bool? var foreground: Bool? var destructive: Bool? var input: Bool? var inputButtonTitle: String? var inputPlaceholder: String? } struct ActionType: Decodable { let id: String let actions: [Action] var hiddenPreviewsBodyPlaceholder: String? var customDismissAction: Bool? var allowInCarPlay: Bool? var hiddenPreviewsShowTitle: Bool? var hiddenPreviewsShowSubtitle: Bool? var hiddenBodyPlaceholder: String? } struct RegisterActionTypesArgs: Decodable { let types: [ActionType] } struct BatchArgs: Decodable { let notifications: [Notification] } class NotificationPlugin: Plugin { let notificationHandler = NotificationHandler() let notificationManager = NotificationManager() override init() { super.init() notificationManager.notificationHandler = notificationHandler notificationHandler.plugin = self } @objc public func show(_ invoke: Invoke) throws { let notification = try invoke.parseArgs(Notification.self) let request = try showNotification(invoke: invoke, notification: notification) notificationHandler.saveNotification(request.identifier, notification) invoke.resolve(Int(request.identifier) ?? -1) } @objc public func batch(_ invoke: Invoke) throws { let args = try invoke.parseArgs(BatchArgs.self) var ids = [Int]() for notification in args.notifications { let request = try showNotification(invoke: invoke, notification: notification) notificationHandler.saveNotification(request.identifier, notification) ids.append(Int(request.identifier) ?? -1) } invoke.resolve(ids) } @objc public override func requestPermissions(_ invoke: Invoke) { notificationHandler.requestPermissions { granted, error in guard error == nil else { invoke.reject(error!.localizedDescription) return } invoke.resolve(["permissionState": granted ? "granted" : "denied"]) } } @objc public override func checkPermissions(_ invoke: Invoke) { notificationHandler.checkPermissions { status in let permission: String switch status { case .authorized, .ephemeral, .provisional: permission = "granted" case .denied: permission = "denied" case .notDetermined: permission = "prompt" @unknown default: permission = "prompt" } invoke.resolve(["permissionState": permission]) } } @objc func cancel(_ invoke: Invoke) throws { let args = try invoke.parseArgs(CancelArgs.self) UNUserNotificationCenter.current().removePendingNotificationRequests( withIdentifiers: args.notifications.map { String($0) } ) invoke.resolve() } @objc func getPending(_ invoke: Invoke) { UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { (notifications) in let ret = notifications.compactMap({ [weak self] (notification) -> PendingNotification? in return self?.notificationHandler.toPendingNotification(notification) }) invoke.resolve(ret) }) } @objc func registerActionTypes(_ invoke: Invoke) throws { let args = try invoke.parseArgs(RegisterActionTypesArgs.self) makeCategories(args.types) invoke.resolve() } @objc func removeActive(_ invoke: Invoke) { do { let args = try invoke.parseArgs(RemoveActiveArgs.self) UNUserNotificationCenter.current().removeDeliveredNotifications( withIdentifiers: args.notifications.map { String($0.id) }) invoke.resolve() } catch { UNUserNotificationCenter.current().removeAllDeliveredNotifications() DispatchQueue.main.async(execute: { UIApplication.shared.applicationIconBadgeNumber = 0 }) invoke.resolve() } } @objc func getActive(_ invoke: Invoke) { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { (notifications) in let ret = notifications.map({ (notification) -> ActiveNotification in return self.notificationHandler.toActiveNotification( notification.request) }) invoke.resolve(ret) }) } @objc func createChannel(_ invoke: Invoke) { invoke.reject("not implemented") } @objc func deleteChannel(_ invoke: Invoke) { invoke.reject("not implemented") } @objc func listChannels(_ invoke: Invoke) { invoke.reject("not implemented") } } @_cdecl("init_plugin_notification") func initPlugin() -> Plugin { return NotificationPlugin() }