diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 4ba8a3933..652b66084 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -202,6 +202,10 @@ 70D41D6728B0235100613510 /* MigrateObjCSDKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D41D6628B0235100613510 /* MigrateObjCSDKTests.swift */; }; 70D41D6B28B294C100613510 /* MigrateObjCSDKCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D41D6A28B294C100613510 /* MigrateObjCSDKCombineTests.swift */; }; 70D41D8028B520E200613510 /* ParseKeychainAccessGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D41D7F28B520E200613510 /* ParseKeychainAccessGroup.swift */; }; + 70DDD0752C99079500C92D34 /* ParsePushPayloadAppleLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70DDD0742C99077D00C92D34 /* ParsePushPayloadAppleLiveActivity.swift */; }; + 70DDD0772C990F6C00C92D34 /* ParsePushApplePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70DDD0762C990F5E00C92D34 /* ParsePushApplePayload.swift */; }; + 70DDD0792C99535F00C92D34 /* ParsePushAppleNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70DDD0782C99535200C92D34 /* ParsePushAppleNotification.swift */; }; + 70DDD07B2C99F85D00C92D34 /* ParsePushNotificationBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70DDD07A2C99F85600C92D34 /* ParsePushNotificationBody.swift */; }; 70DFEA8A2618E77800F8EB4B /* InitializeSDKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */; }; 70E09E1C262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E09E1B262F0634002DD451 /* ParsePointerCombineTests.swift */; }; 70E6B016286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E6B015286120E00043EC4A /* ParseHookFunctionTests.swift */; }; @@ -552,6 +556,10 @@ 70D41D6628B0235100613510 /* MigrateObjCSDKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateObjCSDKTests.swift; sourceTree = ""; }; 70D41D6A28B294C100613510 /* MigrateObjCSDKCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateObjCSDKCombineTests.swift; sourceTree = ""; }; 70D41D7F28B520E200613510 /* ParseKeychainAccessGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseKeychainAccessGroup.swift; sourceTree = ""; }; + 70DDD0742C99077D00C92D34 /* ParsePushPayloadAppleLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePushPayloadAppleLiveActivity.swift; sourceTree = ""; }; + 70DDD0762C990F5E00C92D34 /* ParsePushApplePayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePushApplePayload.swift; sourceTree = ""; }; + 70DDD0782C99535200C92D34 /* ParsePushAppleNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePushAppleNotification.swift; sourceTree = ""; }; + 70DDD07A2C99F85600C92D34 /* ParsePushNotificationBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePushNotificationBody.swift; sourceTree = ""; }; 70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializeSDKTests.swift; sourceTree = ""; }; 70E09E1B262F0634002DD451 /* ParsePointerCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePointerCombineTests.swift; sourceTree = ""; }; 70E6B015286120E00043EC4A /* ParseHookFunctionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHookFunctionTests.swift; sourceTree = ""; }; @@ -949,6 +957,7 @@ 705025EA285153BC008D6624 /* ParsePushApplePayloadable.swift */, 705025EF2851542D008D6624 /* ParsePushFirebasePayloadable.swift */, 705025CB284CE4C2008D6624 /* ParsePushPayloadable.swift */, + 70DDD0762C990F5E00C92D34 /* ParsePushApplePayload.swift */, 70A98D812794AB3C009B58F2 /* ParseQueryScorable.swift */, 919823642B3A134000E9591A /* ParsePointerable.swift */, 700A8A652B4CC1E40087ADBE /* ParsePointerable+async.swift */, @@ -1045,8 +1054,10 @@ isa = PBXGroup; children = ( 705025D0284CFCDE008D6624 /* ParsePushAppleAlert.swift */, + 70DDD0782C99535200C92D34 /* ParsePushAppleNotification.swift */, 705025DA284D0D56008D6624 /* ParsePushAppleSound.swift */, 705025D5284D0C1D008D6624 /* ParsePushPayloadApple.swift */, + 70DDD0742C99077D00C92D34 /* ParsePushPayloadAppleLiveActivity.swift */, ); path = Apple; sourceTree = ""; @@ -1254,6 +1265,7 @@ 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */, 91285B1B26990D7F0051B544 /* ParsePolygon.swift */, 705025BC284C610C008D6624 /* ParsePush.swift */, + 70DDD07A2C99F85600C92D34 /* ParsePushNotificationBody.swift */, 705025C1284C7841008D6624 /* ParsePush+async.swift */, 705025C6284C7883008D6624 /* ParsePush+combine.swift */, 705025B22845C302008D6624 /* ParsePushStatus.swift */, @@ -1534,6 +1546,7 @@ F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, 70B4E0C12762F313004C9757 /* QueryWhere.swift in Sources */, 70170A442656B02D0070C905 /* ParseAnalytics.swift in Sources */, + 70DDD0792C99535F00C92D34 /* ParsePushAppleNotification.swift in Sources */, 70110D52250680140091CC1D /* ParseConstants.swift in Sources */, 91B79AC326EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */, 708EF0BD28D5F4140052EF35 /* API+Command+async.swift in Sources */, @@ -1567,6 +1580,7 @@ 704E781C28CFFAF80075F952 /* ParseFileDefaultTransfer.swift in Sources */, 7045769826BD917500F86F71 /* Query+async.swift in Sources */, 703B094E26BF47E3005A112F /* ParseTwitter+combine.swift in Sources */, + 70DDD0752C99079500C92D34 /* ParsePushPayloadAppleLiveActivity.swift in Sources */, 70386A3825D998D90048EC1B /* ParseLDAP.swift in Sources */, 709A14A02839CABD00BF85E5 /* ParseCLP.swift in Sources */, 700A8A662B4CC1E40087ADBE /* ParsePointerable+async.swift in Sources */, @@ -1642,6 +1656,7 @@ 70C5509225B4A99100B5DBC2 /* ParseOperationAddRelation.swift in Sources */, 708D035225215F9B00646C70 /* Deletable.swift in Sources */, F97B466424D9C88600F4A88B /* SecureStorable.swift in Sources */, + 70DDD07B2C99F85D00C92D34 /* ParsePushNotificationBody.swift in Sources */, 7030E08B29BBBF790021970D /* ParseConfigCodable+async.swift in Sources */, 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959525A10DFC0052CB31 /* Messages.swift in Sources */, @@ -1665,6 +1680,7 @@ 700395D125A147BE0052CB31 /* QuerySubscribable.swift in Sources */, 70170A492656E2FE0070C905 /* ParseAnalytics+combine.swift in Sources */, 703B092B26BF290B005A112F /* ParseAuthentication+async.swift in Sources */, + 70DDD0772C990F6C00C92D34 /* ParsePushApplePayload.swift in Sources */, 70CE0AB7285A83B100DAEA86 /* ParseHookable.swift in Sources */, F97B45F624D9C6F200F4A88B /* ParseError.swift in Sources */, 7045769D26BD934000F86F71 /* ParseFile+async.swift in Sources */, diff --git a/Sources/ParseSwift/Protocols/ParsePushApplePayload.swift b/Sources/ParseSwift/Protocols/ParsePushApplePayload.swift new file mode 100644 index 000000000..099541597 --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParsePushApplePayload.swift @@ -0,0 +1,73 @@ +// +// ParsePushApplePayload.swift +// ParseSwift +// +// Created by Corey Baker on 9/16/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +// swiftlint:disable line_length + +protocol ParsePushAppleHeader { + + /** + The unique ID for the notification. + */ + var id: UUID? { get set } + /** + The unique ID for this request. + - note: Used for broadcast push notifications. + */ + var requestId: UUID? { get set } + /** + A base64-encoded string that identifies the channel to publish the payload. + The channel ID is generated by sending channel creation request to APNs. + - note: Used for broadcast push notifications. + */ + var channelId: String? { get set } + /** + Multiple notifications with same collapse identifier are displayed to the user as a single + notification. The value should not exceed 64 bytes. + */ + var collapseId: String? { get set } + /** + The priority of the notification. Specify 10 to send the notification immediately. + Specify 5 to send the notification based on power considerations on the user’s device. + See Apple's [documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns) + for more information. + - warning: For Apple OS's only. + */ + var priority: Int? { get set } + /** + The destination topic for the notification. + */ + var topic: String? { get set } + /** + The type of the notification. The value is alert or background. Specify alert when the + delivery of your notification displays an alert, plays a sound, or badges your app’s icon. + Specify background for silent notifications that do not interact with the user. + Defaults to alert if no value is set. + - warning: Required when delivering notifications to + devices running iOS 13 and later, or watchOS 6 and later. Ignored on earlier OS versions. + */ + var pushType: ParsePushPayloadApple.PushType? { get set } +} + +public protocol ParsePushApplePayload: ParsePushApplePayloadable { + /** + The background notification flag. If you are a writing an app using the Remote Notification + Background Mode introduced in iOS7 (a.k.a. “Background Push”), set this value to + 1 to trigger a background update. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app). + - warning: For Apple OS's only. You also have to set `pushType` starting iOS 13 + and watchOS 6. + */ + var contentAvailable: Int? { get set } + /** + The notification service app extension flag. Set this value to 1 to trigger the system to pass the notification to your notification service app extension before delivery. Use your extension to modify the notification’s content. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications). + - warning: You also have to set `pushType` starting iOS 13 + and watchOS 6. + */ + var mutableContent: Int? { get set } +} diff --git a/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift b/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift index 71826cac2..5867bc53f 100644 --- a/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift +++ b/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift @@ -17,19 +17,8 @@ import Foundation need to implement `CodingKeys`, see `ParsePushPayloadApple` for an example. */ public protocol ParsePushApplePayloadable: ParsePushPayloadable { - /** - The payload for displaying an alert. - */ - var alert: ParsePushAppleAlert? { get set } - /** - The destination topic for the notification. - */ - var topic: String? { get set } - /** - Multiple notifications with same collapse identifier are displayed to the user as a single - notification. The value should not exceed 64 bytes. - */ - var collapseId: String? { get set } + + // MARK: Header and other high level information. /** The type of the notification. The value is alert or background. Specify alert when the delivery of your notification displays an alert, plays a sound, or badges your app’s icon. @@ -39,6 +28,28 @@ public protocol ParsePushApplePayloadable: ParsePushPayloadable { devices running iOS 13 and later, or watchOS 6 and later. Ignored on earlier OS versions. */ var pushType: ParsePushPayloadApple.PushType? { get set } + + // MARK: APS information. + + /** + The background notification flag. If you are a writing an app using the Remote Notification + Background Mode introduced in iOS7 (a.k.a. “Background Push”), set this value to + 1 to trigger a background update. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app). + - warning: For Apple OS's only. You also have to set `pushType` starting iOS 13 + and watchOS 6. + */ + var contentAvailable: Int? { get set } + /** + The notification service app extension flag. Set this value to 1 to trigger the system to pass the notification to your notification service app extension before delivery. Use your extension to modify the notification’s content. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications). + - warning: You also have to set `pushType` starting iOS 13 + and watchOS 6. + */ + var mutableContent: Int? { get set } + + /** + The payload for displaying an alert. + */ + var alert: ParsePushAppleAlert? { get set } /** The identifier of the `UNNotification​Category` for this push notification. See Apple's @@ -78,8 +89,44 @@ public protocol ParsePushApplePayloadable: ParsePushPayloadable { notification summary. See [relevanceScore](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/3821031-relevancescore). */ var relevanceScore: Double? { get set } + + init() +} + +public extension ParsePushApplePayloadable { + /** - Specify for the `mdm` field where applicable. + The content of the alert message. */ - var mdm: String? { get set } + var body: String? { + get { + alert?.body + } + set { + if alert != nil { + alert?.body = newValue + } else if let newBody = newValue { + alert = .init(body: newBody) + } + } + } + + /** + Create an instance of `ParsePushPayloadApple` . + - parameter alert: The alert payload for the Apple push notification. + */ + init(alert: ParsePushAppleAlert) { + self.init() + self.alert = alert + } + + /** + Create an instance of `ParsePushPayloadApple` . + - parameter body: The body message to display for the Apple push notification. + */ + init(body: String) { + self.init() + self.body = body + } + } diff --git a/Sources/ParseSwift/Types/ParsePush.swift b/Sources/ParseSwift/Types/ParsePush.swift index d9bf97860..ff963445b 100644 --- a/Sources/ParseSwift/Types/ParsePush.swift +++ b/Sources/ParseSwift/Types/ParsePush.swift @@ -30,6 +30,7 @@ public struct ParsePush: ParseTypeable { public var payload: V? /// When to send the notification. public var pushTime: Date? + /** The UNIX timestamp when the notification should expire. If the notification cannot be delivered to the device, will retry until it expires. @@ -37,7 +38,7 @@ public struct ParsePush: ParseTypeable { no retries will be attempted. - note: This should not be set directly using a **Date** type. Instead it should be set using `expirationDate`. - - warning: Cannot send a notification with this valuel and `expirationInterval` both set. + - warning: Cannot send a notification with this value and `expirationInterval` both set. */ var expirationTime: TimeInterval? @@ -46,7 +47,7 @@ public struct ParsePush: ParseTypeable { If the notification cannot be delivered to the device, will retry until it expires. - note: This takes any date and turns it into a UNIX timestamp and sets the value of `expirationTime`. - - warning: Cannot send a notification with this valuel and `expirationInterval` both set. + - warning: Cannot send a notification with this value and `expirationInterval` both set. */ var expirationDate: Date? { get { @@ -59,9 +60,10 @@ public struct ParsePush: ParseTypeable { expirationTime = newValue?.timeIntervalSince1970 } } + /** The seconds from now to expire the notification. - - warning: Cannot send a notification with this valuel and `expirationTime` both set. + - warning: Cannot send a notification with this value and `expirationTime` both set. */ public var expirationInterval: Int? @@ -203,11 +205,13 @@ extension ParsePush { } } - func sendCommand() -> API.NonParseBodyCommand { - - return API.NonParseBodyCommand(method: .POST, - path: .push, - body: self) { (data) -> String in + func sendCommand() -> API.NonParseBodyCommand { + let body = ParsePushNotificationBody(push: self) + let command = API.NonParseBodyCommand( + method: .POST, + path: .push, + body: body + ) { (data) -> String in guard let response = try? ParseCoding.jsonDecoder().decode(PushResponse.self, from: data) else { throw ParseError(code: .otherCause, message: "The server is missing \"X-Parse-Push-Status-Id\" in its header response") @@ -222,6 +226,7 @@ extension ParsePush { throw ParseError(code: .otherCause, message: "Push was unsuccessful") } } + return command } } diff --git a/Sources/ParseSwift/Types/ParsePushNotificationBody.swift b/Sources/ParseSwift/Types/ParsePushNotificationBody.swift new file mode 100644 index 000000000..ba358b21e --- /dev/null +++ b/Sources/ParseSwift/Types/ParsePushNotificationBody.swift @@ -0,0 +1,51 @@ +// +// ParsePushNotificationBody.swift +// ParseSwift +// +// Created by Corey Baker on 9/17/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +struct ParsePushNotificationBody: ParseTypeable { + var `where`: QueryWhere? + var channels: Set? + var data: AnyCodable? + var pushTime: Date? + var expirationTime: TimeInterval? + var expirationInterval: Int? + + enum CodingKeys: String, CodingKey { + case pushTime = "push_time" + case expirationTime = "expiration_time" + case expirationInterval = "expiration_interval" + case `where`, channels, data + } + + init(push: ParsePush) { + self.where = push.where + self.channels = push.channels + self.pushTime = push.pushTime + self.expirationTime = push.expirationTime + self.expirationInterval = push.expirationInterval + if let payload = push.payload { + self.data = AnyCodable( + ParsePushAppleNotification(payload: payload) + ) + } + } + + init(push: ParsePush) { + self.where = push.where + self.channels = push.channels + self.pushTime = push.pushTime + self.expirationTime = push.expirationTime + self.expirationInterval = push.expirationInterval + if let payload = push.payload { + self.data = AnyCodable( + payload + ) + } + } +} diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleAlert.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleAlert.swift index 2127ae650..d71543006 100644 --- a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleAlert.swift +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleAlert.swift @@ -15,6 +15,7 @@ import Foundation for more information. */ public struct ParsePushAppleAlert: ParseTypeable { + /** The content of the alert message. */ diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleNotification.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleNotification.swift new file mode 100644 index 000000000..2a9efaca0 --- /dev/null +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleNotification.swift @@ -0,0 +1,75 @@ +// +// ParsePushAppleNotification.swift +// ParseSwift +// +// Created by Corey Baker on 9/16/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +struct ParsePushAppleNotification: ParsePushAppleHeader, ParsePushPayloadable { + + struct APS: ParseTypeable { + var payload: P + + enum CodingKeys: String, CodingKey { + case payload = "aps" + } + } + + // Notification Header properties set by parse-server-push-adapter. + var collapseId: String? + var priority: Int? + var pushType: ParsePushPayloadApple.PushType? + var topic: String? + + // Notification Header properties set directly. + var id: UUID? + var requestId: UUID? + var channelId: String? + var payload: APS? + + public init() {} + + init( + id: UUID? = nil, + collapseId: String? = nil, + requestId: UUID? = nil, + channelId: String? = nil, + priority: Int? = nil, + topic: String? = nil, + payload: P + ) { + self.id = id + self.collapseId = collapseId + self.requestId = requestId + self.channelId = channelId + self.priority = priority + self.topic = topic + self.pushType = payload.pushType + self.payload = APS(payload: payload) + } + + enum CodingKeys: String, CodingKey { + case pushType = "push_type" + case payload = "rawPayload" + case collapseId, priority, topic, id, requestId, channelId + } + +} + +extension ParsePushAppleNotification where P: ParsePushApplePayload & ParsePushAppleHeader { + + init( + id: UUID? = nil, + payload: P + ) { + self.id = id + self.collapseId = payload.collapseId + self.pushType = payload.pushType + self.priority = payload.priority + self.topic = payload.topic + self.payload = APS(payload: payload) + } +} diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift index e058b7dc5..cde8eb86d 100644 --- a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift @@ -8,111 +8,82 @@ import Foundation -// swiftlint:disable line_length - /// The payload data for an Apple push notification. -public struct ParsePushPayloadApple: ParsePushApplePayloadable { - /** - The background notification flag. If you are a writing an app using the Remote Notification - Background Mode introduced in iOS7 (a.k.a. “Background Push”), set this value to - 1 to trigger a background update. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app). - - warning: For Apple OS's only. You also have to set `pushType` starting iOS 13 - and watchOS 6. - */ - public var contentAvailable: Int? - /** - The notification service app extension flag. Set this value to 1 to trigger the system to pass the notification to your notification service app extension before delivery. Use your extension to modify the notification’s content. For more informaiton, see [Apple's documentation](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications). - - warning: You also have to set `pushType` starting iOS 13 - and watchOS 6. - */ - public var mutableContent: Int? - /** - The priority of the notification. Specify 10 to send the notification immediately. - Specify 5 to send the notification based on power considerations on the user’s device. - See Apple's [documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns) - for more information. - - warning: For Apple OS's only. - */ - public var priority: Int? - - public var topic: String? +public struct ParsePushPayloadApple: ParsePushApplePayload { + // MARK: Header and other high level information. public var collapseId: String? + public var mdm: String? + public var priority: Int? + public var pushType: PushType? = .alert + public var topic: String? + // MARK: APS information. + public var contentAvailable: Int? + public var mutableContent: Int? public var relevanceScore: Double? - public var targetContentId: String? - public var interruptionLevel: String? - - public var pushType: PushType? = .alert - public var category: String? - public var urlArgs: [String]? - public var threadId: String? - - public var mdm: String? - public var alert: ParsePushAppleAlert? - /** - The content of the alert message. - */ - public var body: String? { - get { - alert?.body - } - set { - if alert != nil { - alert?.body = newValue - } else if let newBody = newValue { - alert = .init(body: newBody) - } - } - } var badge: AnyCodable? var sound: AnyCodable? /// The type of notification. + /// For more details, see [Apple Documentation](https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns). public enum PushType: String, Codable, Sendable { /// Send as an alert. case alert /// Send as a background notification. case background + /// Send as a Push to Talk notification. + case location + case voip + case complication + case fileprovider + case mdm + case pushtotalk + /// Send as a Live Activity notification. + case liveactivity + + func appendRequiredInformationToTopic( + _ topic: String + ) -> String { + switch self { + case .location: + return "\(topic).location-query" + case .voip: + return "\(topic).voip" + case .complication: + return "\(topic).complication" + case .fileprovider: + return "\(topic).pushkit.fileprovider" + case .liveactivity: + return "\(topic).push-type.liveactivity" + case .pushtotalk: + return "\(topic).voip-ptt" + default: + return topic + } + } } enum CodingKeys: String, CodingKey { case relevanceScore = "relevance-score" - case targetContentId = "targetContentIdentifier" + case targetContentId = "target-content-id" case mutableContent = "mutable-content" case contentAvailable = "content-available" - case pushType = "push_type" - case collapseId = "collapse_id" - case category, sound, badge, alert, threadId, - mdm, priority, topic, interruptionLevel, - urlArgs + case interruptionLevel = "interruption-level" + case urlArgs = "url-args" + case threadId = "thread-id" + case category, sound, badge, alert } public init() {} - /** - Create an instance of `ParsePushPayloadApple` . - - parameter alert: The alert payload for the Apple push notification. - */ - public init(alert: ParsePushAppleAlert) { - self.alert = alert - } - - /** - Create an instance of `ParsePushPayloadApple` . - - parameter body: The body message to display for the Apple push notification. - */ - public init(body: String) { - self.body = body - } - public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) do { @@ -126,18 +97,16 @@ public struct ParsePushPayloadApple: ParsePushApplePayloadable { targetContentId = try values.decodeIfPresent(String.self, forKey: .targetContentId) mutableContent = try values.decodeIfPresent(Int.self, forKey: .mutableContent) contentAvailable = try values.decodeIfPresent(Int.self, forKey: .contentAvailable) - priority = try values.decodeIfPresent(Int.self, forKey: .priority) - pushType = try values.decodeIfPresent(Self.PushType.self, forKey: .pushType) - collapseId = try values.decodeIfPresent(String.self, forKey: .collapseId) category = try values.decodeIfPresent(String.self, forKey: .category) sound = try values.decodeIfPresent(AnyCodable.self, forKey: .sound) badge = try values.decodeIfPresent(AnyCodable.self, forKey: .badge) threadId = try values.decodeIfPresent(String.self, forKey: .threadId) - mdm = try values.decodeIfPresent(String.self, forKey: .mdm) - topic = try values.decodeIfPresent(String.self, forKey: .topic) interruptionLevel = try values.decodeIfPresent(String.self, forKey: .interruptionLevel) urlArgs = try values.decodeIfPresent([String].self, forKey: .urlArgs) } +} + +extension ParsePushPayloadApple { /** Set the name of a sound file in your app’s main bundle or in the Library/Sounds folder diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift new file mode 100644 index 000000000..03b6f294a --- /dev/null +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift @@ -0,0 +1,92 @@ +// +// ParsePushPayloadAppleLiveActivity.swift +// ParseSwift +// +// Created by Corey Baker on 9/16/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +/// The payload data for an Apple LiveActivity push notification. +public struct ParsePushPayloadAppleLiveActivity: ParsePushApplePayload { + + /// A LiveActivity event. + public enum Event: String, Sendable, Codable { + /// Start a LiveActivity. + case start + /// Update a LiveActivity. + case update + /// End a LiveActivity. + case end + } + + public var event: Event? + + public var contentAvailable: Int? + + public var mutableContent: Int? + + public var priority: Int? + + public var topic: String? + + public var collapseId: String? + + public var relevanceScore: Double? + + public var targetContentId: String? + + public var interruptionLevel: String? + + public var pushType: ParsePushPayloadApple.PushType? = .liveactivity + + public var category: String? + + public var urlArgs: [String]? + + public var threadId: String? + + public var mdm: String? + + public var alert: ParsePushAppleAlert? + + var badge: AnyCodable? + var sound: AnyCodable? + + enum CodingKeys: String, CodingKey { + case relevanceScore = "relevance-score" + case targetContentId = "target-content-id" + case mutableContent = "mutable-content" + case contentAvailable = "content-available" + case interruptionLevel = "interruption-level" + case urlArgs = "url-args" + case threadId = "thread-id" + case category, sound, badge, alert, topic + } + + public init() { + // Set to the lowest live activity priority by default. + priority = 5 + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + do { + alert = try values.decode(ParsePushAppleAlert.self, forKey: .alert) + } catch { + if let alertBody = try values.decodeIfPresent(String.self, forKey: .alert) { + alert = ParsePushAppleAlert(body: alertBody) + } + } + relevanceScore = try values.decodeIfPresent(Double.self, forKey: .relevanceScore) + targetContentId = try values.decodeIfPresent(String.self, forKey: .targetContentId) + mutableContent = try values.decodeIfPresent(Int.self, forKey: .mutableContent) + contentAvailable = try values.decodeIfPresent(Int.self, forKey: .contentAvailable) + category = try values.decodeIfPresent(String.self, forKey: .category) + sound = try values.decodeIfPresent(AnyCodable.self, forKey: .sound) + badge = try values.decodeIfPresent(AnyCodable.self, forKey: .badge) + threadId = try values.decodeIfPresent(String.self, forKey: .threadId) + topic = try values.decodeIfPresent(String.self, forKey: .topic) + interruptionLevel = try values.decodeIfPresent(String.self, forKey: .interruptionLevel) + urlArgs = try values.decodeIfPresent([String].self, forKey: .urlArgs) + } +} diff --git a/Sources/ParseSwift/Types/ParsePushPayload/ParsePushPayloadAny.swift b/Sources/ParseSwift/Types/ParsePushPayload/ParsePushPayloadAny.swift index c4949864c..1e039e8e8 100644 --- a/Sources/ParseSwift/Types/ParsePushPayload/ParsePushPayloadAny.swift +++ b/Sources/ParseSwift/Types/ParsePushPayload/ParsePushPayloadAny.swift @@ -22,7 +22,6 @@ public struct ParsePushPayloadAny: ParsePushApplePayloadable, ParsePushFirebaseP public var threadId: String? public var interruptionLevel: String? public var relevanceScore: Double? - public var mdm: String? public var uri: URL? public var title: String? public var collapseKey: String? @@ -39,30 +38,62 @@ public struct ParsePushPayloadAny: ParsePushApplePayloadable, ParsePushFirebaseP var mutableContent: AnyCodable? public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: RawCodingKey.self) - relevanceScore = try values.decodeIfPresent(Double.self, forKey: .key("relevance-score")) - targetContentId = try values.decodeIfPresent(String.self, forKey: .key("targetContentIdentifier")) + let values = try decoder.container( + keyedBy: RawCodingKey.self + ) + relevanceScore = try values.decodeIfPresent( + Double.self, + forKey: .key("relevance-score") + ) + targetContentId = try values.decodeIfPresent( + String.self, + forKey: .key("targetContentIdentifier") + ) do { - mutableContent = try values.decode(AnyCodable.self, forKey: .key("mutable-content")) + mutableContent = try values.decode( + AnyCodable.self, + forKey: .key("mutable-content") + ) } catch { - mutableContent = try values.decodeIfPresent(AnyCodable.self, forKey: .key("mutableContent")) + mutableContent = try values.decodeIfPresent( + AnyCodable.self, + forKey: .key("mutableContent") + ) } do { - contentAvailable = try values.decode(AnyCodable.self, forKey: .key("content-available")) + contentAvailable = try values.decode( + AnyCodable.self, + forKey: .key("content-available") + ) } catch { - contentAvailable = try values.decodeIfPresent(AnyCodable.self, forKey: .key("contentAvailable")) + contentAvailable = try values.decodeIfPresent( + AnyCodable.self, + forKey: .key("contentAvailable") + ) } do { - let priorityInt = try values.decode(Int.self, forKey: .key("priority")) + let priorityInt = try values.decode( + Int.self, + forKey: .key("priority") + ) priority = AnyCodable(priorityInt) } catch { - if let priorityString = try values.decodeIfPresent(String.self, forKey: .key("priority")), - let priorityEnum = ParsePushPayloadFirebase.PushPriority(rawValue: priorityString) { + if let priorityString = try values.decodeIfPresent( + String.self, + forKey: .key("priority") + ), + let priorityEnum = ParsePushPayloadFirebase.PushPriority( + rawValue: priorityString + ) { priority = AnyCodable(priorityEnum) } } - pushType = try values.decodeIfPresent(ParsePushPayloadApple.PushType.self, forKey: .key("push_type")) - collapseId = try values.decodeIfPresent(String.self, forKey: .key("collapse_id")) + pushType = try values.decodeIfPresent( + ParsePushPayloadApple.PushType.self, + forKey: .key("push_type") + ) + collapseId = try values.decodeIfPresent( + String.self, forKey: .key("collapse_id")) category = try values.decodeIfPresent(String.self, forKey: .key("category")) sound = try values.decodeIfPresent(AnyCodable.self, forKey: .key("sound")) badge = try values.decodeIfPresent(AnyCodable.self, forKey: .key("badge")) @@ -74,7 +105,6 @@ public struct ParsePushPayloadAny: ParsePushApplePayloadable, ParsePushFirebaseP } } threadId = try values.decodeIfPresent(String.self, forKey: .key("threadId")) - mdm = try values.decodeIfPresent(String.self, forKey: .key("mdm")) topic = try values.decodeIfPresent(String.self, forKey: .key("topic")) interruptionLevel = try values.decodeIfPresent(String.self, forKey: .key("interruptionLevel")) urlArgs = try values.decodeIfPresent([String].self, forKey: .key("urlArgs")) @@ -105,7 +135,6 @@ public struct ParsePushPayloadAny: ParsePushApplePayloadable, ParsePushFirebaseP payload.threadId = threadId payload.interruptionLevel = interruptionLevel payload.relevanceScore = relevanceScore - payload.mdm = mdm payload.alert = alert payload.badge = badge payload.sound = sound diff --git a/Tests/ParseSwiftTests/ParsePushAsyncTests.swift b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift index 655e94187..31b8fbf39 100644 --- a/Tests/ParseSwiftTests/ParsePushAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift @@ -275,6 +275,7 @@ class ParsePushAsyncTests: XCTestCase { let appleAlert = ParsePushAppleAlert(body: "hello world") var anyPayload = ParsePushPayloadAny() anyPayload.alert = appleAlert + anyPayload.pushType = .alert var statusOnServer = ParsePushStatus() statusOnServer.payload = anyPayload statusOnServer.objectId = objectId @@ -304,6 +305,7 @@ class ParsePushAsyncTests: XCTestCase { let appleAlert = ParsePushAppleAlert(body: "hello world") var anyPayload = ParsePushPayloadAny() anyPayload.alert = appleAlert + anyPayload.pushType = .alert let query = Installation.query("peace" == "out") var statusOnServer = try ParsePushStatusResponse() .setPayload(anyPayload) diff --git a/Tests/ParseSwiftTests/ParsePushCombineTests.swift b/Tests/ParseSwiftTests/ParsePushCombineTests.swift index 131fe3623..a84bb445f 100644 --- a/Tests/ParseSwiftTests/ParsePushCombineTests.swift +++ b/Tests/ParseSwiftTests/ParsePushCombineTests.swift @@ -309,6 +309,7 @@ class ParsePushCombineTests: XCTestCase { let appleAlert = ParsePushAppleAlert(body: "hello world") var anyPayload = ParsePushPayloadAny() anyPayload.alert = appleAlert + anyPayload.pushType = .alert var statusOnServer = ParsePushStatus() statusOnServer.payload = anyPayload statusOnServer.objectId = objectId diff --git a/Tests/ParseSwiftTests/ParsePushPayloadAppleTests.swift b/Tests/ParseSwiftTests/ParsePushPayloadAppleTests.swift index 8ad4f6c64..9ef0f6c5f 100644 --- a/Tests/ParseSwiftTests/ParsePushPayloadAppleTests.swift +++ b/Tests/ParseSwiftTests/ParsePushPayloadAppleTests.swift @@ -39,10 +39,12 @@ class ParsePushPayloadAppleTests: XCTestCase { func testInitializers() throws { let body = "Hello from ParseSwift!" var applePayload = ParsePushPayloadApple(body: body) - XCTAssertEqual(applePayload.description, - "{\"alert\":{\"body\":\"\(body)\"},\"push_type\":\"alert\"}") + let appleNotification = ParsePushAppleNotification(payload: applePayload) + XCTAssertEqual(appleNotification.description, + "{\"aps\":{\"alert\":{\"body\":\"\(body)\"}},\"push_type\":\"alert\"}") let applePayload2 = ParsePushPayloadApple(alert: .init(body: body)) - XCTAssertEqual(applePayload, applePayload2) + let appleNotification2 = ParsePushAppleNotification(payload: applePayload2) + XCTAssertEqual(appleNotification, appleNotification2) XCTAssertEqual(applePayload.body, body) applePayload.alert = nil XCTAssertNil(applePayload.body) @@ -50,29 +52,43 @@ class ParsePushPayloadAppleTests: XCTestCase { XCTAssertEqual(applePayload.alert, applePayload2.alert) } + func testParsePushAppleNotification() throws { + let body = "Hello from ParseSwift!" + var applePayload = ParsePushPayloadApple(body: body) + applePayload.collapseId = "hello" + applePayload.pushType = .background + applePayload.priority = 1 + applePayload.mdm = "naw" + let appleNotification = ParsePushAppleNotification(payload: applePayload) + XCTAssertEqual( + appleNotification.description, + "{\"_mdm\":\"naw\",\"aps\":{\"alert\":{\"body\":\"\(body)\"}},\"collapse_id\":\"hello\",\"priority\":1,\"push_type\":\"background\"}" + ) + } + func testBadge() throws { let applePayload = ParsePushPayloadApple() .setBadge(1) XCTAssertEqual(applePayload.description, - "{\"badge\":1,\"push_type\":\"alert\"}") + "{\"badge\":1}") let applePayload2 = ParsePushPayloadApple() .incrementBadge() XCTAssertEqual(applePayload2.description, - "{\"badge\":{\"__op\":\"Increment\",\"amount\":1},\"push_type\":\"alert\"}") + "{\"badge\":{\"__op\":\"Increment\",\"amount\":1}}") } func testSound() throws { let applePayload = ParsePushPayloadApple() .setSound("hello") XCTAssertEqual(applePayload.description, - "{\"push_type\":\"alert\",\"sound\":\"hello\"}") + "{\"sound\":\"hello\"}") let soundString: String = try applePayload.getSound() XCTAssertEqual(soundString, "hello") let sound = ParsePushAppleSound(critical: true, name: "hello", volume: 7) let applePayload2 = ParsePushPayloadApple() .setSound(sound) XCTAssertEqual(applePayload2.description, - "{\"push_type\":\"alert\",\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7}}") + "{\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7}}") let soundObject: ParsePushAppleSound = try applePayload2.getSound() XCTAssertEqual(soundObject, sound) XCTAssertThrowsError(try applePayload2.getSound() as String) @@ -100,9 +116,9 @@ class ParsePushPayloadAppleTests: XCTestCase { applePayload.interruptionLevel = "yolo" applePayload.topic = "naw" applePayload.threadId = "yep" - applePayload.collapseId = "nope" - applePayload.pushType = .background - applePayload.priority = 6 + // applePayload.collapseId = "nope" + // applePayload.pushType = .background + // applePayload.priority = 6 applePayload.contentAvailable = 1 applePayload.mutableContent = 1 applePayload.targetContentId = "press" @@ -111,7 +127,7 @@ class ParsePushPayloadAppleTests: XCTestCase { let decoded = try ParseCoding.jsonDecoder().decode(ParsePushPayloadApple.self, from: encoded) XCTAssertEqual(applePayload, decoded) XCTAssertEqual(applePayload.description, - "{\"alert\":{\"action\":\"to\",\"action-loc-key\":\"icon\",\"body\":\"pull up\",\"launch-image\":\"it\",\"loc-args\":[\"mother\"],\"loc-key\":\"cousin\",\"subtitle\":\"trip\",\"subtitle-loc-args\":[\"gone\"],\"subtitle-loc-key\":\"far\",\"title\":\"you\",\"title-loc-args\":[\"arg\"],\"title-loc-key\":\"it\"},\"badge\":1,\"collapse_id\":\"nope\",\"content-available\":1,\"interruptionLevel\":\"yolo\",\"mutable-content\":1,\"priority\":6,\"push_type\":\"background\",\"relevance-score\":2,\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7},\"targetContentIdentifier\":\"press\",\"threadId\":\"yep\",\"topic\":\"naw\",\"urlArgs\":[\"help\"]}") + "{\"alert\":{\"action\":\"to\",\"action-loc-key\":\"icon\",\"body\":\"pull up\",\"launch-image\":\"it\",\"loc-args\":[\"mother\"],\"loc-key\":\"cousin\",\"subtitle\":\"trip\",\"subtitle-loc-args\":[\"gone\"],\"subtitle-loc-key\":\"far\",\"title\":\"you\",\"title-loc-args\":[\"arg\"],\"title-loc-key\":\"it\"},\"badge\":1,\"content-available\":1,\"interruption-level\":\"yolo\",\"mutable-content\":1,\"relevance-score\":2,\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7},\"target-content-id\":\"press\",\"thread-id\":\"yep\",\"topic\":\"naw\",\"url-args\":[\"help\"]}") XCTAssertEqual(alert.description, "{\"action\":\"to\",\"action-loc-key\":\"icon\",\"body\":\"pull up\",\"launch-image\":\"it\",\"loc-args\":[\"mother\"],\"loc-key\":\"cousin\",\"subtitle\":\"trip\",\"subtitle-loc-args\":[\"gone\"],\"subtitle-loc-key\":\"far\",\"title\":\"you\",\"title-loc-args\":[\"arg\"],\"title-loc-key\":\"it\"}") let alert2 = ParsePushAppleAlert() XCTAssertNotEqual(alert, alert2) @@ -144,15 +160,12 @@ class ParsePushPayloadAppleTests: XCTestCase { applePayload.interruptionLevel = "yolo" applePayload.topic = "naw" applePayload.threadId = "yep" - applePayload.collapseId = "nope" - applePayload.pushType = .background applePayload.targetContentId = "press" applePayload.relevanceScore = 2.0 - applePayload.priority = 6 applePayload.contentAvailable = 1 applePayload.mutableContent = 1 - guard let jsonData = "{\"alert\":\"pull up\",\"badge\":1,\"collapse_id\":\"nope\",\"content-available\":1,\"interruptionLevel\":\"yolo\",\"mutable-content\":1,\"priority\":6,\"push_type\":\"background\",\"relevance-score\":2,\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7},\"targetContentIdentifier\":\"press\",\"threadId\":\"yep\",\"topic\":\"naw\",\"urlArgs\":[\"help\"]}".data(using: .utf8) else { + guard let jsonData = "{\"alert\":\"pull up\",\"badge\":1,\"content-available\":1,\"interruption-level\":\"yolo\",\"mutable-content\":1,\"relevance-score\":2,\"sound\":{\"critical\":true,\"name\":\"hello\",\"volume\":7},\"target-content-id\":\"press\",\"thread-id\":\"yep\",\"topic\":\"naw\",\"url-args\":[\"help\"]}".data(using: .utf8) else { XCTFail("Should have unwrapped") return }