parent
fea1d84682
commit
eff7041d62
@ -0,0 +1,253 @@
|
|||||||
|
import Tauri
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
enum NotificationError: LocalizedError {
|
||||||
|
case contentNoId
|
||||||
|
case contentNoTitle
|
||||||
|
case contentNoBody
|
||||||
|
case triggerConstructionFailed
|
||||||
|
case triggerRepeatIntervalTooShort
|
||||||
|
case attachmentNoId
|
||||||
|
case attachmentNoUrl
|
||||||
|
case attachmentFileNotFound(path: String)
|
||||||
|
case attachmentUnableToCreate(String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .attachmentFileNotFound(let path):
|
||||||
|
return "Unable to find file \(path) for attachment"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNotificationContent(_ notification: JSObject) throws -> UNNotificationContent {
|
||||||
|
guard let title = notification["title"] as? String else {
|
||||||
|
throw NotificationError.contentNoTitle
|
||||||
|
}
|
||||||
|
guard let body = notification["body"] as? String else {
|
||||||
|
throw NotificationError.contentNoBody
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra = notification["extra"] as? JSObject ?? [:]
|
||||||
|
let schedule = notification["schedule"] as? JSObject ?? [:]
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil)
|
||||||
|
content.body = NSString.localizedUserNotificationString(
|
||||||
|
forKey: body,
|
||||||
|
arguments: nil)
|
||||||
|
|
||||||
|
content.userInfo = [
|
||||||
|
"__EXTRA__": extra,
|
||||||
|
"__SCHEDULE__": schedule,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let actionTypeId = notification["actionTypeId"] as? String {
|
||||||
|
content.categoryIdentifier = actionTypeId
|
||||||
|
}
|
||||||
|
|
||||||
|
if let threadIdentifier = notification["group"] as? String {
|
||||||
|
content.threadIdentifier = threadIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
if let summaryArgument = notification["summary"] as? String {
|
||||||
|
content.summaryArgument = summaryArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sound = notification["sound"] as? String {
|
||||||
|
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let attachments = notification["attachments"] as? [JSObject] {
|
||||||
|
content.attachments = try makeAttachments(attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] {
|
||||||
|
var createdAttachments = [UNNotificationAttachment]()
|
||||||
|
|
||||||
|
for attachment in attachments {
|
||||||
|
guard let id = attachment["id"] as? String else {
|
||||||
|
throw NotificationError.attachmentNoId
|
||||||
|
}
|
||||||
|
guard let url = attachment["url"] as? String else {
|
||||||
|
throw NotificationError.attachmentNoUrl
|
||||||
|
}
|
||||||
|
guard let urlObject = makeAttachmentUrl(url) else {
|
||||||
|
throw NotificationError.attachmentFileNotFound(path: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = attachment["options"] as? JSObject ?? [:]
|
||||||
|
|
||||||
|
do {
|
||||||
|
let newAttachment = try UNNotificationAttachment(
|
||||||
|
identifier: id, url: urlObject, options: makeAttachmentOptions(options))
|
||||||
|
createdAttachments.append(newAttachment)
|
||||||
|
} catch {
|
||||||
|
throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdAttachments
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAttachmentUrl(_ path: String) -> URL? {
|
||||||
|
return URL(string: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAttachmentOptions(_ options: JSObject) -> JSObject {
|
||||||
|
var opts: JSObject = [:]
|
||||||
|
|
||||||
|
if let iosUNNotificationAttachmentOptionsTypeHintKey = options[
|
||||||
|
"iosUNNotificationAttachmentOptionsTypeHintKey"] as? String
|
||||||
|
{
|
||||||
|
opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
|
||||||
|
}
|
||||||
|
if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[
|
||||||
|
"iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String
|
||||||
|
{
|
||||||
|
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
|
||||||
|
iosUNNotificationAttachmentOptionsThumbnailHiddenKey
|
||||||
|
}
|
||||||
|
if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[
|
||||||
|
"iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String
|
||||||
|
{
|
||||||
|
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
|
||||||
|
iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
|
||||||
|
}
|
||||||
|
if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[
|
||||||
|
"iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String
|
||||||
|
{
|
||||||
|
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] =
|
||||||
|
iosUNNotificationAttachmentOptionsThumbnailTimeKey
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleScheduledNotification(_ invoke: Invoke, _ schedule: JSObject) throws
|
||||||
|
-> UNNotificationTrigger?
|
||||||
|
{
|
||||||
|
let kind = schedule["kind"] as? String ?? ""
|
||||||
|
let payload = schedule["data"] as? JSObject ?? [:]
|
||||||
|
switch kind {
|
||||||
|
case "At":
|
||||||
|
let date = payload["at"] as? String ?? ""
|
||||||
|
let dateFormatter = ISO8601DateFormatter()
|
||||||
|
if let at = dateFormatter.date(from: date) {
|
||||||
|
let repeats = payload["repeats"] as? Bool ?? false
|
||||||
|
|
||||||
|
let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
|
||||||
|
|
||||||
|
if dateInfo.date! < Date() {
|
||||||
|
invoke.reject("Scheduled time must be *after* current time")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
|
||||||
|
|
||||||
|
// Notifications that repeat have to be at least a minute between each other
|
||||||
|
if repeats && dateInterval.duration < 60 {
|
||||||
|
throw NotificationError.triggerRepeatIntervalTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNTimeIntervalNotificationTrigger(
|
||||||
|
timeInterval: dateInterval.duration, repeats: repeats)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
invoke.reject("could not parse `at` date")
|
||||||
|
}
|
||||||
|
case "Interval":
|
||||||
|
let dateComponents = getDateComponents(payload)
|
||||||
|
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
|
||||||
|
case "Every":
|
||||||
|
let interval = payload["interval"] as? String ?? ""
|
||||||
|
let count = schedule["count"] as? Int ?? 1
|
||||||
|
|
||||||
|
if let repeatDateInterval = getRepeatDateInterval(interval, count) {
|
||||||
|
// Notifications that repeat have to be at least a minute between each other
|
||||||
|
if repeatDateInterval.duration < 60 {
|
||||||
|
throw NotificationError.triggerRepeatIntervalTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNTimeIntervalNotificationTrigger(
|
||||||
|
timeInterval: repeatDateInterval.duration, repeats: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given our schedule format, return a DateComponents object
|
||||||
|
/// that only contains the components passed in.
|
||||||
|
|
||||||
|
func getDateComponents(_ at: JSObject) -> DateComponents {
|
||||||
|
// var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
|
||||||
|
// dateInfo.calendar = Calendar.current
|
||||||
|
var dateInfo = DateComponents()
|
||||||
|
|
||||||
|
if let year = at["year"] as? Int {
|
||||||
|
dateInfo.year = year
|
||||||
|
}
|
||||||
|
if let month = at["month"] as? Int {
|
||||||
|
dateInfo.month = month
|
||||||
|
}
|
||||||
|
if let day = at["day"] as? Int {
|
||||||
|
dateInfo.day = day
|
||||||
|
}
|
||||||
|
if let hour = at["hour"] as? Int {
|
||||||
|
dateInfo.hour = hour
|
||||||
|
}
|
||||||
|
if let minute = at["minute"] as? Int {
|
||||||
|
dateInfo.minute = minute
|
||||||
|
}
|
||||||
|
if let second = at["second"] as? Int {
|
||||||
|
dateInfo.second = second
|
||||||
|
}
|
||||||
|
if let weekday = at["weekday"] as? Int {
|
||||||
|
dateInfo.weekday = weekday
|
||||||
|
}
|
||||||
|
return dateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the difference between the string representation of a date
|
||||||
|
/// interval and today. For example, if every is "month", then we
|
||||||
|
/// return the interval between today and a month from today.
|
||||||
|
|
||||||
|
func getRepeatDateInterval(_ every: String, _ count: Int) -> DateInterval? {
|
||||||
|
let cal = Calendar.current
|
||||||
|
let now = Date()
|
||||||
|
switch every {
|
||||||
|
case "Year":
|
||||||
|
let newDate = cal.date(byAdding: .year, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Month":
|
||||||
|
let newDate = cal.date(byAdding: .month, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "TwoWeeks":
|
||||||
|
let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Week":
|
||||||
|
let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Day":
|
||||||
|
let newDate = cal.date(byAdding: .day, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Hour":
|
||||||
|
let newDate = cal.date(byAdding: .hour, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Minute":
|
||||||
|
let newDate = cal.date(byAdding: .minute, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
case "Second":
|
||||||
|
let newDate = cal.date(byAdding: .second, value: count, to: now)!
|
||||||
|
return DateInterval(start: now, end: newDate)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
import Tauri
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
public func makeCategories(_ actionTypes: [JSObject]) {
|
||||||
|
var createdCategories = [UNNotificationCategory]()
|
||||||
|
|
||||||
|
let generalCategory = UNNotificationCategory(
|
||||||
|
identifier: "GENERAL",
|
||||||
|
actions: [],
|
||||||
|
intentIdentifiers: [],
|
||||||
|
options: .customDismissAction)
|
||||||
|
|
||||||
|
createdCategories.append(generalCategory)
|
||||||
|
for type in actionTypes {
|
||||||
|
guard let id = type["id"] as? String else {
|
||||||
|
Logger.error("Action type must have an id field")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let hiddenBodyPlaceholder = type["iosHiddenPreviewsBodyPlaceholder"] as? String ?? ""
|
||||||
|
let actions = type["actions"] as? [JSObject] ?? []
|
||||||
|
|
||||||
|
let newActions = makeActions(actions)
|
||||||
|
|
||||||
|
// Create the custom actions for the TIMER_EXPIRED category.
|
||||||
|
var newCategory: UNNotificationCategory?
|
||||||
|
|
||||||
|
newCategory = UNNotificationCategory(
|
||||||
|
identifier: id,
|
||||||
|
actions: newActions,
|
||||||
|
intentIdentifiers: [],
|
||||||
|
hiddenPreviewsBodyPlaceholder: hiddenBodyPlaceholder,
|
||||||
|
options: makeCategoryOptions(type))
|
||||||
|
|
||||||
|
createdCategories.append(newCategory!)
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.setNotificationCategories(Set(createdCategories))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeActions(_ actions: [JSObject]) -> [UNNotificationAction] {
|
||||||
|
var createdActions = [UNNotificationAction]()
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
guard let id = action["id"] as? String else {
|
||||||
|
Logger.error("Action must have an id field")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let title = action["title"] as? String ?? ""
|
||||||
|
let input = action["input"] as? Bool ?? false
|
||||||
|
|
||||||
|
var newAction: UNNotificationAction
|
||||||
|
if input {
|
||||||
|
let inputButtonTitle = action["inputButtonTitle"] as? String
|
||||||
|
let inputPlaceholder = action["inputPlaceholder"] as? String ?? ""
|
||||||
|
|
||||||
|
if inputButtonTitle != nil {
|
||||||
|
newAction = UNTextInputNotificationAction(
|
||||||
|
identifier: id,
|
||||||
|
title: title,
|
||||||
|
options: makeActionOptions(action),
|
||||||
|
textInputButtonTitle: inputButtonTitle!,
|
||||||
|
textInputPlaceholder: inputPlaceholder)
|
||||||
|
} else {
|
||||||
|
newAction = UNTextInputNotificationAction(
|
||||||
|
identifier: id, title: title, options: makeActionOptions(action))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create the custom actions for the TIMER_EXPIRED category.
|
||||||
|
newAction = UNNotificationAction(
|
||||||
|
identifier: id,
|
||||||
|
title: title,
|
||||||
|
options: makeActionOptions(action))
|
||||||
|
}
|
||||||
|
createdActions.append(newAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdActions
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeActionOptions(_ action: JSObject) -> UNNotificationActionOptions {
|
||||||
|
let foreground = action["foreground"] as? Bool ?? false
|
||||||
|
let destructive = action["destructive"] as? Bool ?? false
|
||||||
|
let requiresAuthentication = action["requiresAuthentication"] as? Bool ?? false
|
||||||
|
|
||||||
|
if foreground {
|
||||||
|
return .foreground
|
||||||
|
}
|
||||||
|
if destructive {
|
||||||
|
return .destructive
|
||||||
|
}
|
||||||
|
if requiresAuthentication {
|
||||||
|
return .authenticationRequired
|
||||||
|
}
|
||||||
|
return UNNotificationActionOptions(rawValue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCategoryOptions(_ type: JSObject) -> UNNotificationCategoryOptions {
|
||||||
|
let customDismiss = type["iosCustomDismissAction"] as? Bool ?? false
|
||||||
|
let carPlay = type["iosAllowInCarPlay"] as? Bool ?? false
|
||||||
|
let hiddenPreviewsShowTitle = type["iosHiddenPreviewsShowTitle"] as? Bool ?? false
|
||||||
|
let hiddenPreviewsShowSubtitle = type["iosHiddenPreviewsShowSubtitle"] as? Bool ?? false
|
||||||
|
|
||||||
|
if customDismiss {
|
||||||
|
return .customDismissAction
|
||||||
|
}
|
||||||
|
if carPlay {
|
||||||
|
return .allowInCarPlay
|
||||||
|
}
|
||||||
|
|
||||||
|
if hiddenPreviewsShowTitle {
|
||||||
|
return .hiddenPreviewsShowTitle
|
||||||
|
}
|
||||||
|
if hiddenPreviewsShowSubtitle {
|
||||||
|
return .hiddenPreviewsShowSubtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNNotificationCategoryOptions(rawValue: 0)
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
import Tauri
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||||
|
|
||||||
|
public weak var plugin: Plugin?
|
||||||
|
|
||||||
|
var notificationsMap = [String: JSObject]()
|
||||||
|
|
||||||
|
public func requestPermissions(with completion: ((Bool, Error?) -> Void)? = nil) {
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
|
||||||
|
completion?(granted, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func checkPermissions(with completion: ((UNAuthorizationStatus) -> Void)? = nil) {
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.getNotificationSettings { settings in
|
||||||
|
completion?(settings.authorizationStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
|
||||||
|
// let notificationData = makeNotificationRequestJSObject(notification.request)
|
||||||
|
// TODO self.plugin?.trigger("received", data: notificationData)
|
||||||
|
|
||||||
|
if let options = notificationsMap[notification.request.identifier] {
|
||||||
|
let silent = options["silent"] as? Bool ?? false
|
||||||
|
if silent {
|
||||||
|
return UNNotificationPresentationOptions.init(rawValue: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
.badge,
|
||||||
|
.sound,
|
||||||
|
.alert,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func didReceive(response: UNNotificationResponse) {
|
||||||
|
var data = JSObject()
|
||||||
|
|
||||||
|
let originalNotificationRequest = response.notification.request
|
||||||
|
let actionId = response.actionIdentifier
|
||||||
|
|
||||||
|
// We turn the two default actions (open/dismiss) into generic strings
|
||||||
|
if actionId == UNNotificationDefaultActionIdentifier {
|
||||||
|
data["actionId"] = "tap"
|
||||||
|
} else if actionId == UNNotificationDismissActionIdentifier {
|
||||||
|
data["actionId"] = "dismiss"
|
||||||
|
} else {
|
||||||
|
data["actionId"] = actionId
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the type of action was for an input type, get the value
|
||||||
|
if let inputType = response as? UNTextInputNotificationResponse {
|
||||||
|
data["inputValue"] = inputType.userText
|
||||||
|
}
|
||||||
|
|
||||||
|
data["notification"] = makeNotificationRequestJSObject(originalNotificationRequest)
|
||||||
|
|
||||||
|
// TODO self.plugin?.trigger("localNotificationActionPerformed", data: data, retainUntilConsumed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a UNNotificationRequest into a JSObject to return back to the client.
|
||||||
|
*/
|
||||||
|
func makeNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
|
||||||
|
let notificationRequest = notificationsMap[request.identifier] ?? [:]
|
||||||
|
var notification = makePendingNotificationRequestJSObject(request)
|
||||||
|
notification["sound"] = notificationRequest["sound"] ?? ""
|
||||||
|
notification["actionTypeId"] = request.content.categoryIdentifier
|
||||||
|
notification["attachments"] = notificationRequest["attachments"] ?? []
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePendingNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
|
||||||
|
var notification: JSObject = [
|
||||||
|
"id": Int(request.identifier) ?? -1,
|
||||||
|
"title": request.content.title,
|
||||||
|
"body": request.content.body,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let userInfo = JSTypes.coerceDictionaryToJSObject(request.content.userInfo) {
|
||||||
|
var extra = userInfo["__EXTRA__"] as? JSObject ?? userInfo
|
||||||
|
|
||||||
|
// check for any dates and convert them to strings
|
||||||
|
for (key, value) in extra {
|
||||||
|
if let date = value as? Date {
|
||||||
|
let dateString = ISO8601DateFormatter().string(from: date)
|
||||||
|
extra[key] = dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notification["extra"] = extra
|
||||||
|
|
||||||
|
if var schedule = userInfo["__SCHEDULE__"] as? JSObject {
|
||||||
|
// convert schedule at date to string
|
||||||
|
if let date = schedule["at"] as? Date {
|
||||||
|
let dateString = ISO8601DateFormatter().string(from: date)
|
||||||
|
schedule["at"] = dateString
|
||||||
|
}
|
||||||
|
|
||||||
|
notification["schedule"] = schedule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
@objc public protocol NotificationHandlerProtocol {
|
||||||
|
func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions
|
||||||
|
func didReceive(response: UNNotificationResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
|
||||||
|
public weak var notificationHandler: NotificationHandlerProtocol?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
var presentationOptions: UNNotificationPresentationOptions? = nil
|
||||||
|
|
||||||
|
if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
|
||||||
|
presentationOptions = notificationHandler?.willPresent(notification: notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(presentationOptions ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
|
didReceive response: UNNotificationResponse,
|
||||||
|
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
|
||||||
|
notificationHandler?.didReceive(response: response)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,228 @@
|
|||||||
|
import SwiftRs
|
||||||
|
import Tauri
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
import WebKit
|
import WebKit
|
||||||
import Tauri
|
|
||||||
import SwiftRs
|
enum ShowNotificationError: LocalizedError {
|
||||||
|
case noId
|
||||||
|
case make(Error)
|
||||||
|
case create(Error)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .noId:
|
||||||
|
return "notification `id` missing"
|
||||||
|
case .make(let error):
|
||||||
|
return "Unable to make notification: \(error)"
|
||||||
|
case .create(let error):
|
||||||
|
return "Unable to create notification: \(error)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNotification(invoke: Invoke, notification: JSObject)
|
||||||
|
throws -> UNNotificationRequest
|
||||||
|
{
|
||||||
|
guard let identifier = notification["id"] as? Int else {
|
||||||
|
throw ShowNotificationError.noId
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: UNNotificationContent
|
||||||
|
do {
|
||||||
|
content = try makeNotificationContent(notification)
|
||||||
|
} catch {
|
||||||
|
throw ShowNotificationError.make(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var trigger: UNNotificationTrigger?
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let schedule = notification["schedule"] as? JSObject {
|
||||||
|
try trigger = handleScheduledNotification(invoke, schedule)
|
||||||
|
} else {
|
||||||
|
let match = Calendar.current.dateComponents(
|
||||||
|
[.timeZone, .year, .month, .day, .hour, .minute], from: Date())
|
||||||
|
trigger = UNCalendarNotificationTrigger(dateMatching: match, repeats: false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw ShowNotificationError.create(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule the request.
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: "\(identifier)", content: content, trigger: trigger
|
||||||
|
)
|
||||||
|
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.add(request) { (error: Error?) in
|
||||||
|
if let theError = error {
|
||||||
|
invoke.reject(theError.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationPlugin: Plugin {
|
class NotificationPlugin: Plugin {
|
||||||
@objc public func requestPermission(_ invoke: Invoke) throws {
|
let notificationHandler = NotificationHandler()
|
||||||
invoke.resolve(["permissionState": "granted"])
|
let notificationManager = NotificationManager()
|
||||||
}
|
|
||||||
|
override init() {
|
||||||
@objc public func permissionState(_ invoke: Invoke) throws {
|
super.init()
|
||||||
invoke.resolve(["permissionState": "granted"])
|
notificationHandler.plugin = self
|
||||||
}
|
notificationManager.notificationHandler = notificationHandler
|
||||||
|
}
|
||||||
@objc public func notify(_ invoke: Invoke) throws {
|
|
||||||
// TODO
|
@objc public func show(_ invoke: Invoke) throws {
|
||||||
invoke.resolve()
|
let request = try showNotification(invoke: invoke, notification: invoke.data)
|
||||||
}
|
// TODO self.notificationHandler.notificationsMap[request.identifier] = invoke.data
|
||||||
|
invoke.resolve([
|
||||||
|
"id": Int(request.identifier) ?? -1
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func batch(_ invoke: Invoke) throws {
|
||||||
|
guard let notifications = invoke.getArray("notifications", JSObject.self) else {
|
||||||
|
invoke.reject("`notifications` array is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ids = [String]()
|
||||||
|
|
||||||
|
for notification in notifications {
|
||||||
|
let request = try showNotification(invoke: invoke, notification: notification)
|
||||||
|
// TODO self.notificationHandler.notificationsMap[request.identifier] = notification
|
||||||
|
ids.append(request.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = ids.map({ (id) -> JSObject in
|
||||||
|
return [
|
||||||
|
"id": Int(id) ?? -1
|
||||||
|
]
|
||||||
|
})
|
||||||
|
invoke.resolve([
|
||||||
|
"notifications": ret
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = "default"
|
||||||
|
@unknown default:
|
||||||
|
permission = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke.resolve(["permissionState": permission])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func cancel(_ invoke: Invoke) {
|
||||||
|
guard let notifications = invoke.getArray("notifications", JSObject.self),
|
||||||
|
notifications.count > 0
|
||||||
|
else {
|
||||||
|
invoke.reject("`notifications` input is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = notifications.map({ (value: JSObject) -> String in
|
||||||
|
if let idString = value["id"] as? String {
|
||||||
|
return idString
|
||||||
|
} else if let idNum = value["id"] as? NSNumber {
|
||||||
|
return idNum.stringValue
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getPending(_ invoke: Invoke) {
|
||||||
|
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: {
|
||||||
|
(notifications) in
|
||||||
|
let ret = notifications.compactMap({ [weak self] (notification) -> JSObject? in
|
||||||
|
return self?.notificationHandler.makePendingNotificationRequestJSObject(notification)
|
||||||
|
})
|
||||||
|
|
||||||
|
invoke.resolve([
|
||||||
|
"notifications": ret
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func registerActionTypes(_ invoke: Invoke) {
|
||||||
|
guard let types = invoke.getArray("types", JSObject.self) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
makeCategories(types)
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeDeliveredNotifications(_ invoke: Invoke) {
|
||||||
|
guard let notifications = invoke.getArray("notifications", JSObject.self) else {
|
||||||
|
invoke.reject("`notifications` input is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = notifications.map { "\($0["id"] ?? "")" }
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeAllDeliveredNotifications(_ invoke: Invoke) {
|
||||||
|
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||||
|
DispatchQueue.main.async(execute: {
|
||||||
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
|
})
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getDeliveredNotifications(_ invoke: Invoke) {
|
||||||
|
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
|
||||||
|
(notifications) in
|
||||||
|
let ret = notifications.map({ (notification) -> [String: Any] in
|
||||||
|
return self.notificationHandler.makeNotificationRequestJSObject(
|
||||||
|
notification.request)
|
||||||
|
})
|
||||||
|
invoke.resolve([
|
||||||
|
"notifications": 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")
|
@_cdecl("init_plugin_notification")
|
||||||
func initPlugin(name: SRString, webview: WKWebView?) {
|
func initPlugin() -> Plugin {
|
||||||
Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: NotificationPlugin())
|
return NotificationPlugin()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue