From 8a5fe1ab7c469f94727133f1143dde74a3493beb Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 2 Oct 2024 16:31:53 -0700 Subject: [PATCH 1/2] fix: Apple push notificaiton not working for live activities --- ParseSwift.xcodeproj/project.pbxproj | 16 ++ .../Protocols/ParsePushApplePayload.swift | 109 ++++++++++++++ .../Protocols/ParsePushApplePayloadable.swift | 40 +++++ Sources/ParseSwift/Types/ParsePush.swift | 21 ++- .../Types/ParsePushNotificationBody.swift | 51 +++++++ .../Apple/ParsePushAppleAlert.swift | 1 + .../Apple/ParsePushAppleNotification.swift | 33 ++++ .../Apple/ParsePushPayloadApple.swift | 141 ++---------------- .../ParsePushPayloadAppleLiveActivity.swift | 92 ++++++++++++ .../ParsePushPayloadAppleTests.swift | 43 ++++-- 10 files changed, 394 insertions(+), 153 deletions(-) create mode 100644 Sources/ParseSwift/Protocols/ParsePushApplePayload.swift create mode 100644 Sources/ParseSwift/Types/ParsePushNotificationBody.swift create mode 100644 Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleNotification.swift create mode 100644 Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift 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..fcd154125 --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParsePushApplePayload.swift @@ -0,0 +1,109 @@ +// +// ParsePushApplePayload.swift +// ParseSwift +// +// Created by Corey Baker on 9/16/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +// swiftlint:disable line_length + +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 } + /** + 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 } + + var pushType: ParsePushPayloadApple.PushType? { get set } + + var badge: AnyCodable? { get set } + var sound: AnyCodable? { get set } +} + +extension ParsePushApplePayload { + + /** + Set the name of a sound file in your app’s main bundle or in the Library/Sounds folder + of your app’s container directory. For information about how to prepare sounds, see + [UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound). + - parameter sound: An instance of `ParsePushAppleSound`. + - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. + - warning: For Apple OS's only. + */ + public func setSound(_ sound: ParsePushAppleSound) -> Self { + var mutablePayload = self + mutablePayload.sound = AnyCodable(sound) + return mutablePayload + } + + /** + Set the name of a sound file in your app’s main bundle or in the Library/Sounds folder + of your app’s container directory. Specify the string “default” to play the system + sound. Pass a string for **regular** notifications. For critical alerts, pass the sound + `ParsePushAppleSound` instead. For information about how to prepare sounds, see + [UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound). + - parameter sound: A `String` or any `Codable` object that can be sent to APN. + - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. + - warning: For Apple OS's only. + */ + public func setSound(_ sound: V) -> Self where V: Codable { + var mutablePayload = self + mutablePayload.sound = AnyCodable(sound) + return mutablePayload + } + + /** + Get the sound using any type that conforms to `Codable`. + - returns: The sound casted to the inferred type. + - throws: An error of type `ParseError`. + */ + public func getSound() throws -> V where V: Codable { + guard let sound = sound?.value as? V else { + throw ParseError(code: .otherCause, + message: "Cannot be casted to the inferred type") + } + return sound + } + + /** + Set the badge to a specific value to display on your app's icon. + - parameter badge: The number to display in a badge on your app’s icon. + Specify 0 to remove the current badge, if any. + - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. + - warning: For Apple OS's only. + */ + public func setBadge(_ number: Int) -> Self { + var mutablePayload = self + mutablePayload.badge = AnyCodable(number) + return mutablePayload + } + + /** + Increment the badge value by 1 to display on your app's icon. + - warning: For Apple OS's only. + - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. + */ + public func incrementBadge() -> Self { + var mutablePayload = self + mutablePayload.badge = AnyCodable(ParseOperationIncrement(amount: 1)) + return mutablePayload + } +} diff --git a/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift b/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift index 71826cac2..1ab9ec23c 100644 --- a/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift +++ b/Sources/ParseSwift/Protocols/ParsePushApplePayloadable.swift @@ -82,4 +82,44 @@ public protocol ParsePushApplePayloadable: ParsePushPayloadable { Specify for the `mdm` field where applicable. */ var mdm: String? { get set } + + init() +} + +public extension ParsePushApplePayloadable { + + /** + The content of the alert message. + */ + 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..b295b475f --- /dev/null +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushAppleNotification.swift @@ -0,0 +1,33 @@ +// +// ParsePushAppleNotification.swift +// ParseSwift +// +// Created by Corey Baker on 9/16/24. +// Copyright © 2024 Network Reconnaissance Lab. All rights reserved. +// + +struct ParsePushAppleNotification: ParsePushPayloadable { + + var aps: P? + var collapseId: String? + var pushType: ParsePushPayloadApple.PushType? + var priority: Int? + var mdm: String? + public init() {} + + public init(payload: P) { + self.aps = payload + self.collapseId = payload.collapseId + self.pushType = payload.pushType + self.priority = payload.priority + self.mdm = payload.mdm + } + + enum CodingKeys: String, CodingKey { + case pushType = "push_type" + case collapseId = "collapse_id" + case mdm = "_mdm" + case aps, priority + } + +} diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift index e058b7dc5..e19bf4c63 100644 --- a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadApple.swift @@ -8,31 +8,13 @@ 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 struct ParsePushPayloadApple: ParsePushApplePayload { + 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? @@ -57,21 +39,6 @@ public struct ParsePushPayloadApple: ParsePushApplePayloadable { 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? @@ -81,38 +48,23 @@ public struct ParsePushPayloadApple: ParsePushApplePayloadable { case alert /// Send as a background notification. case background + /// Send as a Live Activity notification. + case liveactivity } 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, topic } 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,83 +78,12 @@ 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) } - - /** - Set the name of a sound file in your app’s main bundle or in the Library/Sounds folder - of your app’s container directory. For information about how to prepare sounds, see - [UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound). - - parameter sound: An instance of `ParsePushAppleSound`. - - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. - - warning: For Apple OS's only. - */ - public func setSound(_ sound: ParsePushAppleSound) -> Self { - var mutablePayload = self - mutablePayload.sound = AnyCodable(sound) - return mutablePayload - } - - /** - Set the name of a sound file in your app’s main bundle or in the Library/Sounds folder - of your app’s container directory. Specify the string “default” to play the system - sound. Pass a string for **regular** notifications. For critical alerts, pass the sound - `ParsePushAppleSound` instead. For information about how to prepare sounds, see - [UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound). - - parameter sound: A `String` or any `Codable` object that can be sent to APN. - - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. - - warning: For Apple OS's only. - */ - public func setSound(_ sound: V) -> Self where V: Codable { - var mutablePayload = self - mutablePayload.sound = AnyCodable(sound) - return mutablePayload - } - - /** - Get the sound using any type that conforms to `Codable`. - - returns: The sound casted to the inferred type. - - throws: An error of type `ParseError`. - */ - public func getSound() throws -> V where V: Codable { - guard let sound = sound?.value as? V else { - throw ParseError(code: .otherCause, - message: "Cannot be casted to the inferred type") - } - return sound - } - - /** - Set the badge to a specific value to display on your app's icon. - - parameter badge: The number to display in a badge on your app’s icon. - Specify 0 to remove the current badge, if any. - - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. - - warning: For Apple OS's only. - */ - public func setBadge(_ number: Int) -> Self { - var mutablePayload = self - mutablePayload.badge = AnyCodable(number) - return mutablePayload - } - - /** - Increment the badge value by 1 to display on your app's icon. - - warning: For Apple OS's only. - - returns: A mutated instance of `ParsePushPayloadApple` for easy chaining. - */ - public func incrementBadge() -> Self { - var mutablePayload = self - mutablePayload.badge = AnyCodable(ParseOperationIncrement(amount: 1)) - return mutablePayload - } } diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift new file mode 100644 index 000000000..d07345dec --- /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/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 } From 093542eb2b96f034ea3efe758790580a7e09aada Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 2 Oct 2024 17:02:35 -0700 Subject: [PATCH 2/2] lint --- .swiftlint.yml | 1 + Sources/ParseSwift/API/API+Command.swift | 4 ++-- Sources/ParseSwift/API/API+NonParseBodyCommand.swift | 2 +- Sources/ParseSwift/Extensions/URLSession.swift | 8 ++++---- Sources/ParseSwift/Storage/ParseFileManager.swift | 10 +++++----- .../Apple/ParsePushPayloadAppleLiveActivity.swift | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index bc875a404..223f29c23 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,7 @@ disabled_rules: - identifier_name - blanket_disable_command - non_optional_string_data_conversion + - optional_data_string_conversion excluded: # paths to ignore during linting. Takes precedence over `included`. - Tests/ParseSwiftTests/ParseEncoderTests - DerivedData diff --git a/Sources/ParseSwift/API/API+Command.swift b/Sources/ParseSwift/API/API+Command.swift index e65003857..1d7bec4e7 100644 --- a/Sources/ParseSwift/API/API+Command.swift +++ b/Sources/ParseSwift/API/API+Command.swift @@ -100,7 +100,7 @@ internal extension API { allowIntermediateResponses: Bool = false, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil, - completion: @escaping(Result) -> Void) async { + completion: @escaping (Result) -> Void) async { let currentNotificationQueue: DispatchQueue! if let notificationQueue = notificationQueue { currentNotificationQueue = notificationQueue @@ -257,7 +257,7 @@ internal extension API { batching: Bool = false, childObjects: [String: PointerType]? = nil, childFiles: [String: ParseFile]? = nil, - completion: @escaping(Result) -> Void) { + completion: @escaping (Result) -> Void) { let params = self.params?.getURLQueryItems() Task { do { diff --git a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift index 77265d773..c5ac841d7 100644 --- a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift +++ b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift @@ -37,7 +37,7 @@ internal extension API { func execute(options: API.Options, callbackQueue: DispatchQueue, allowIntermediateResponses: Bool = false, - completion: @escaping(Result) -> Void) async { + completion: @escaping (Result) -> Void) async { switch await self.prepareURLRequest(options: options) { case .success(let urlRequest): diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 78cba485c..6edaebdeb 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -143,7 +143,7 @@ internal extension URLSession { attempts: Int = 1, allowIntermediateResponses: Bool, mapper: @escaping (Data) async throws -> U, - completion: @escaping(Result) -> Void + completion: @escaping (Result) -> Void ) async { do { let (responseData, urlResponse) = try await dataTask(for: request) @@ -288,7 +288,7 @@ internal extension URLSession { from file: URL?, progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)?, mapper: @escaping (Data) async throws -> U, - completion: @escaping(Result) -> Void + completion: @escaping (Result) -> Void ) { var task: URLSessionTask? if let data = data { @@ -354,7 +354,7 @@ internal extension URLSession { with request: URLRequest, progress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?, mapper: @escaping (Data) async throws -> U, - completion: @escaping(Result) -> Void + completion: @escaping (Result) -> Void ) async { let task = downloadTask(with: request) { (location, urlResponse, responseError) in Task { @@ -374,7 +374,7 @@ internal extension URLSession { func downloadTask( with request: URLRequest, mapper: @escaping (Data) async throws -> U, - completion: @escaping(Result) -> Void + completion: @escaping (Result) -> Void ) { Task { do { diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index 357d2b2b1..8a2dadbad 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -112,7 +112,7 @@ extension ParseFileManager { } } - func writeString(_ string: String, filePath: URL, completion: @escaping(Error?) -> Void) { + func writeString(_ string: String, filePath: URL, completion: @escaping (Error?) -> Void) { synchronizationQueue.async { do { guard let data = string.data(using: .utf8) else { @@ -127,7 +127,7 @@ extension ParseFileManager { } } - func writeData(_ data: Data, filePath: URL, completion: @escaping(Error?) -> Void) { + func writeData(_ data: Data, filePath: URL, completion: @escaping (Error?) -> Void) { synchronizationQueue.async { do { try data.write(to: filePath, options: self.defaultDataWritingOptions) @@ -138,7 +138,7 @@ extension ParseFileManager { } } - func copyItem(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) { + func copyItem(_ fromPath: URL, toPath: URL, completion: @escaping (Error?) -> Void) { synchronizationQueue.async { do { try FileManager.default.copyItem(at: fromPath, to: toPath) @@ -149,7 +149,7 @@ extension ParseFileManager { } } - func moveItem(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) { + func moveItem(_ fromPath: URL, toPath: URL, completion: @escaping (Error?) -> Void) { synchronizationQueue.async { if fromPath != toPath { do { @@ -164,7 +164,7 @@ extension ParseFileManager { } } - func moveContentsOfDirectory(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) { + func moveContentsOfDirectory(_ fromPath: URL, toPath: URL, completion: @escaping (Error?) -> Void) { synchronizationQueue.async { do { if fromPath == toPath { diff --git a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift index d07345dec..03b6f294a 100644 --- a/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift +++ b/Sources/ParseSwift/Types/ParsePushPayload/Apple/ParsePushPayloadAppleLiveActivity.swift @@ -19,7 +19,7 @@ public struct ParsePushPayloadAppleLiveActivity: ParsePushApplePayload { case end } - public var event : Event? + public var event: Event? public var contentAvailable: Int?