snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
10
plugins/notification/ios/.gitignore
vendored
Normal file
10
plugins/notification/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
34
plugins/notification/ios/Package.swift
Normal file
34
plugins/notification/ios/Package.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.5
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-notification",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-notification",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-notification"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-notification",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
3
plugins/notification/ios/README.md
Normal file
3
plugins/notification/ios/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin {{ plugin_name_original }}
|
||||
|
||||
A description of this package.
|
||||
286
plugins/notification/ios/Sources/Notification.swift
Normal file
286
plugins/notification/ios/Sources/Notification.swift
Normal file
@@ -0,0 +1,286 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
enum NotificationError: LocalizedError {
|
||||
case triggerRepeatIntervalTooShort
|
||||
case attachmentFileNotFound(path: String)
|
||||
case attachmentUnableToCreate(String)
|
||||
case pastScheduledTime
|
||||
case invalidDate(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .triggerRepeatIntervalTooShort:
|
||||
return "Schedule interval too short, must be a least 1 minute"
|
||||
case .attachmentFileNotFound(let path):
|
||||
return "Unable to find file \(path) for attachment"
|
||||
case .attachmentUnableToCreate(let error):
|
||||
return "Failed to create attachment: \(error)"
|
||||
case .pastScheduledTime:
|
||||
return "Scheduled time must be *after* current time"
|
||||
case .invalidDate(let date):
|
||||
return "Could not parse date \(date)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeNotificationContent(_ notification: Notification) throws -> UNNotificationContent {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSString.localizedUserNotificationString(
|
||||
forKey: notification.title, arguments: nil)
|
||||
if let body = notification.body {
|
||||
content.body = NSString.localizedUserNotificationString(
|
||||
forKey: body,
|
||||
arguments: nil)
|
||||
}
|
||||
|
||||
var userInfo: [String: Any] = [:]
|
||||
|
||||
if let extra = notification.extra {
|
||||
userInfo["__EXTRA__"] = extra
|
||||
}
|
||||
|
||||
if let schedule = notification.schedule {
|
||||
userInfo["__SCHEDULE__"] = scheduleToDictionary(schedule)
|
||||
}
|
||||
|
||||
content.userInfo = userInfo
|
||||
|
||||
if let actionTypeId = notification.actionTypeId {
|
||||
content.categoryIdentifier = actionTypeId
|
||||
}
|
||||
|
||||
if let threadIdentifier = notification.group {
|
||||
content.threadIdentifier = threadIdentifier
|
||||
}
|
||||
|
||||
if let summaryArgument = notification.summary {
|
||||
content.summaryArgument = summaryArgument
|
||||
}
|
||||
|
||||
if let sound = notification.sound {
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
|
||||
}
|
||||
|
||||
if let attachments = notification.attachments {
|
||||
content.attachments = try makeAttachments(attachments)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func scheduleToDictionary(_ schedule: NotificationSchedule) -> [String: Any] {
|
||||
switch schedule {
|
||||
case .at(let date, let repeating):
|
||||
return [
|
||||
"type": "at",
|
||||
"date": date,
|
||||
"repeating": repeating
|
||||
]
|
||||
case .interval(let interval):
|
||||
return [
|
||||
"type": "interval",
|
||||
"interval": scheduleIntervalToDictionary(interval)
|
||||
]
|
||||
case .every(let interval, let count):
|
||||
return [
|
||||
"type": "every",
|
||||
"interval": interval.rawValue,
|
||||
"count": count
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleIntervalToDictionary(_ interval: ScheduleInterval) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
if let year = interval.year {
|
||||
dict["year"] = year
|
||||
}
|
||||
if let month = interval.month {
|
||||
dict["month"] = month
|
||||
}
|
||||
if let day = interval.day {
|
||||
dict["day"] = day
|
||||
}
|
||||
if let weekday = interval.weekday {
|
||||
dict["weekday"] = weekday
|
||||
}
|
||||
if let hour = interval.hour {
|
||||
dict["hour"] = hour
|
||||
}
|
||||
if let minute = interval.minute {
|
||||
dict["minute"] = minute
|
||||
}
|
||||
if let second = interval.second {
|
||||
dict["second"] = second
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
func makeAttachments(_ attachments: [NotificationAttachment]) throws -> [UNNotificationAttachment] {
|
||||
var createdAttachments = [UNNotificationAttachment]()
|
||||
|
||||
for attachment in attachments {
|
||||
|
||||
guard let urlObject = makeAttachmentUrl(attachment.url) else {
|
||||
throw NotificationError.attachmentFileNotFound(path: attachment.url)
|
||||
}
|
||||
|
||||
let options = attachment.options != nil ? makeAttachmentOptions(attachment.options!) : nil
|
||||
|
||||
do {
|
||||
let newAttachment = try UNNotificationAttachment(
|
||||
identifier: attachment.id, url: urlObject, options: options)
|
||||
createdAttachments.append(newAttachment)
|
||||
} catch {
|
||||
throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
return createdAttachments
|
||||
}
|
||||
|
||||
func makeAttachmentUrl(_ path: String) -> URL? {
|
||||
return URL(string: path)
|
||||
}
|
||||
|
||||
func makeAttachmentOptions(_ options: NotificationAttachmentOptions) -> [AnyHashable: Any] {
|
||||
var opts: [AnyHashable: Any] = [:]
|
||||
|
||||
if let value = options.iosUNNotificationAttachmentOptionsTypeHintKey {
|
||||
opts[UNNotificationAttachmentOptionsTypeHintKey] = value
|
||||
}
|
||||
if let value = options.iosUNNotificationAttachmentOptionsThumbnailHiddenKey {
|
||||
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] = value
|
||||
}
|
||||
if let value = options.iosUNNotificationAttachmentOptionsThumbnailClippingRectKey {
|
||||
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = value
|
||||
}
|
||||
if let value = options
|
||||
.iosUNNotificationAttachmentOptionsThumbnailTimeKey
|
||||
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = value
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func handleScheduledNotification(_ schedule: NotificationSchedule) throws
|
||||
-> UNNotificationTrigger?
|
||||
{
|
||||
switch schedule {
|
||||
case .at(let date, let repeating):
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
|
||||
if let at = dateFormatter.date(from: date) {
|
||||
let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
|
||||
|
||||
if dateInfo.date! < Date() {
|
||||
throw NotificationError.pastScheduledTime
|
||||
}
|
||||
|
||||
let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
|
||||
|
||||
// Notifications that repeat have to be at least a minute between each other
|
||||
if repeating && dateInterval.duration < 60 {
|
||||
throw NotificationError.triggerRepeatIntervalTooShort
|
||||
}
|
||||
|
||||
return UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: dateInterval.duration, repeats: repeating)
|
||||
|
||||
} else {
|
||||
throw NotificationError.invalidDate(date)
|
||||
}
|
||||
case .interval(let interval):
|
||||
let dateComponents = getDateComponents(interval)
|
||||
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
|
||||
case .every(let interval, let count):
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Given our schedule format, return a DateComponents object
|
||||
/// that only contains the components passed in.
|
||||
|
||||
func getDateComponents(_ at: ScheduleInterval) -> DateComponents {
|
||||
// var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
|
||||
// dateInfo.calendar = Calendar.current
|
||||
var dateInfo = DateComponents()
|
||||
|
||||
if let year = at.year {
|
||||
dateInfo.year = year
|
||||
}
|
||||
if let month = at.month {
|
||||
dateInfo.month = month
|
||||
}
|
||||
if let day = at.day {
|
||||
dateInfo.day = day
|
||||
}
|
||||
if let hour = at.hour {
|
||||
dateInfo.hour = hour
|
||||
}
|
||||
if let minute = at.minute {
|
||||
dateInfo.minute = minute
|
||||
}
|
||||
if let second = at.second {
|
||||
dateInfo.second = second
|
||||
}
|
||||
if let weekday = at.weekday {
|
||||
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: ScheduleEveryKind, _ 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)
|
||||
}
|
||||
}
|
||||
97
plugins/notification/ios/Sources/NotificationCategory.swift
Normal file
97
plugins/notification/ios/Sources/NotificationCategory.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
internal func makeCategories(_ actionTypes: [ActionType]) {
|
||||
var createdCategories = [UNNotificationCategory]()
|
||||
|
||||
let generalCategory = UNNotificationCategory(
|
||||
identifier: "GENERAL",
|
||||
actions: [],
|
||||
intentIdentifiers: [],
|
||||
options: .customDismissAction)
|
||||
|
||||
createdCategories.append(generalCategory)
|
||||
for type in actionTypes {
|
||||
let newActions = makeActions(type.actions)
|
||||
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
var newCategory: UNNotificationCategory?
|
||||
|
||||
newCategory = UNNotificationCategory(
|
||||
identifier: type.id,
|
||||
actions: newActions,
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: type.hiddenBodyPlaceholder ?? "",
|
||||
options: makeCategoryOptions(type))
|
||||
|
||||
createdCategories.append(newCategory!)
|
||||
}
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.setNotificationCategories(Set(createdCategories))
|
||||
}
|
||||
|
||||
func makeActions(_ actions: [Action]) -> [UNNotificationAction] {
|
||||
var createdActions = [UNNotificationAction]()
|
||||
|
||||
for action in actions {
|
||||
var newAction: UNNotificationAction
|
||||
if action.input ?? false {
|
||||
if action.inputButtonTitle != nil {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: action.id,
|
||||
title: action.title,
|
||||
options: makeActionOptions(action),
|
||||
textInputButtonTitle: action.inputButtonTitle ?? "",
|
||||
textInputPlaceholder: action.inputPlaceholder ?? "")
|
||||
} else {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: action.id, title: action.title, options: makeActionOptions(action))
|
||||
}
|
||||
} else {
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
newAction = UNNotificationAction(
|
||||
identifier: action.id,
|
||||
title: action.title,
|
||||
options: makeActionOptions(action))
|
||||
}
|
||||
createdActions.append(newAction)
|
||||
}
|
||||
|
||||
return createdActions
|
||||
}
|
||||
|
||||
func makeActionOptions(_ action: Action) -> UNNotificationActionOptions {
|
||||
if action.foreground ?? false {
|
||||
return .foreground
|
||||
}
|
||||
if action.destructive ?? false {
|
||||
return .destructive
|
||||
}
|
||||
if action.requiresAuthentication ?? false {
|
||||
return .authenticationRequired
|
||||
}
|
||||
return UNNotificationActionOptions(rawValue: 0)
|
||||
}
|
||||
|
||||
func makeCategoryOptions(_ type: ActionType) -> UNNotificationCategoryOptions {
|
||||
if type.customDismissAction ?? false {
|
||||
return .customDismissAction
|
||||
}
|
||||
if type.allowInCarPlay ?? false {
|
||||
return .allowInCarPlay
|
||||
}
|
||||
|
||||
if type.hiddenPreviewsShowTitle ?? false {
|
||||
return .hiddenPreviewsShowTitle
|
||||
}
|
||||
if type.hiddenPreviewsShowSubtitle ?? false {
|
||||
return .hiddenPreviewsShowSubtitle
|
||||
}
|
||||
|
||||
return UNNotificationCategoryOptions(rawValue: 0)
|
||||
}
|
||||
118
plugins/notification/ios/Sources/NotificationHandler.swift
Normal file
118
plugins/notification/ios/Sources/NotificationHandler.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||
|
||||
public weak var plugin: Plugin?
|
||||
|
||||
private var notificationsMap = [String: Notification]()
|
||||
|
||||
internal func saveNotification(_ key: String, _ notification: Notification) {
|
||||
notificationsMap.updateValue(notification, forKey: key)
|
||||
}
|
||||
|
||||
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 = toActiveNotification(notification.request)
|
||||
try? self.plugin?.trigger("notification", data: notificationData)
|
||||
|
||||
if let options = notificationsMap[notification.request.identifier] {
|
||||
if options.silent ?? false {
|
||||
return UNNotificationPresentationOptions.init(rawValue: 0)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
.badge,
|
||||
.sound,
|
||||
.alert,
|
||||
]
|
||||
}
|
||||
|
||||
public func didReceive(response: UNNotificationResponse) {
|
||||
let originalNotificationRequest = response.notification.request
|
||||
let actionId = response.actionIdentifier
|
||||
|
||||
var actionIdValue: String
|
||||
// We turn the two default actions (open/dismiss) into generic strings
|
||||
if actionId == UNNotificationDefaultActionIdentifier {
|
||||
actionIdValue = "tap"
|
||||
} else if actionId == UNNotificationDismissActionIdentifier {
|
||||
actionIdValue = "dismiss"
|
||||
} else {
|
||||
actionIdValue = actionId
|
||||
}
|
||||
|
||||
var inputValue: String? = nil
|
||||
// If the type of action was for an input type, get the value
|
||||
if let inputType = response as? UNTextInputNotificationResponse {
|
||||
inputValue = inputType.userText
|
||||
}
|
||||
|
||||
try? self.plugin?.trigger(
|
||||
"actionPerformed",
|
||||
data: ReceivedNotification(
|
||||
actionId: actionIdValue,
|
||||
inputValue: inputValue,
|
||||
notification: toActiveNotification(originalNotificationRequest)
|
||||
))
|
||||
}
|
||||
|
||||
func toActiveNotification(_ request: UNNotificationRequest) -> ActiveNotification {
|
||||
let notificationRequest = notificationsMap[request.identifier]!
|
||||
return ActiveNotification(
|
||||
id: Int(request.identifier) ?? -1,
|
||||
title: request.content.title,
|
||||
body: request.content.body,
|
||||
sound: notificationRequest.sound ?? "",
|
||||
actionTypeId: request.content.categoryIdentifier,
|
||||
attachments: notificationRequest.attachments
|
||||
)
|
||||
}
|
||||
|
||||
func toPendingNotification(_ request: UNNotificationRequest) -> PendingNotification {
|
||||
return PendingNotification(
|
||||
id: Int(request.identifier) ?? -1,
|
||||
title: request.content.title,
|
||||
body: request.content.body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingNotification: Encodable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let body: String
|
||||
}
|
||||
|
||||
struct ActiveNotification: Encodable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let body: String
|
||||
let sound: String
|
||||
let actionTypeId: String
|
||||
let attachments: [NotificationAttachment]?
|
||||
}
|
||||
|
||||
struct ReceivedNotification: Encodable {
|
||||
let actionId: String
|
||||
let inputValue: String?
|
||||
let notification: ActiveNotification
|
||||
}
|
||||
47
plugins/notification/ios/Sources/NotificationManager.swift
Normal file
47
plugins/notification/ios/Sources/NotificationManager.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
290
plugins/notification/ios/Sources/NotificationPlugin.swift
Normal file
290
plugins/notification/ios/Sources/NotificationPlugin.swift
Normal file
@@ -0,0 +1,290 @@
|
||||
// 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 {
|
||||
// `cancelAll()` sends no list: cancel every pending notification
|
||||
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)
|
||||
|
||||
if let notifications = args.notifications {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: notifications.map { String($0) }
|
||||
)
|
||||
} else {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
}
|
||||
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()
|
||||
}
|
||||
12
plugins/notification/ios/Tests/PluginTests/PluginTests.swift
Normal file
12
plugins/notification/ios/Tests/PluginTests/PluginTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user