diff --git a/Sources/APNS/APNSClient.swift b/Sources/APNS/APNSClient.swift index a0e1dfd..4842cdb 100644 --- a/Sources/APNS/APNSClient.swift +++ b/Sources/APNS/APNSClient.swift @@ -130,7 +130,7 @@ extension APNSClient { var headers = self.defaultRequestHeaders // Push type - headers.add(name: "apns-push-type", value: request.pushType.configuration.rawValue) + headers.add(name: "apns-push-type", value: request.pushType.description) // APNS ID if let apnsID = request.apnsID { diff --git a/Sources/APNSCore/APNSPushType.swift b/Sources/APNSCore/APNSPushType.swift index 8850453..3e27d28 100644 --- a/Sources/APNSCore/APNSPushType.swift +++ b/Sources/APNSCore/APNSPushType.swift @@ -13,8 +13,9 @@ //===----------------------------------------------------------------------===// /// A struct which represents the different supported APNs push types. -public struct APNSPushType: Hashable, Sendable { - public enum Configuration: String, Hashable, Sendable { +public struct APNSPushType: Hashable, Sendable, CustomStringConvertible { + + internal enum Configuration: String, Hashable, Sendable { case alert case background case location @@ -23,10 +24,15 @@ public struct APNSPushType: Hashable, Sendable { case fileprovider case mdm case liveactivity + case pushtotalk + } + + public var description: String { + configuration.rawValue } /// The underlying raw value that is send to APNs. - public var configuration: Configuration + internal var configuration: Configuration /// Use the alert push type for notifications that trigger a user interaction—for example, an alert, badge, or sound. /// @@ -94,4 +100,12 @@ public struct APNSPushType: Hashable, Sendable { /// Use the live activity push type to update your live activity. /// public static let liveactivity = Self(configuration: .liveactivity) + + /// Use the pushtotalk push type for notifications that provide information about an incoming Push to Talk (Ptt). + /// + /// Push to Talk services aren’t available to compatible iPad and iPhone apps running in visionOS. + /// + /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.voip-ptt` appended to the end. + /// + public static let pushtotalk = Self(configuration: .pushtotalk) } diff --git a/Sources/APNSCore/PushToTalk/APNSClient+PushToTalk.swift b/Sources/APNSCore/PushToTalk/APNSClient+PushToTalk.swift new file mode 100644 index 0000000..a225332 --- /dev/null +++ b/Sources/APNSCore/PushToTalk/APNSClient+PushToTalk.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2022 the APNSwift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of APNSwift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +extension APNSClientProtocol { + /// Sends a Push To Talk (PTT) notification to APNs. + /// + /// - Parameters: + /// - notification: The notification to send. + /// + /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token + /// when registering for remote notifications. + /// + /// + /// - logger: The logger to use for sending this notification. + @discardableResult + @inlinable + public func sendPushToTalkNotification( + _ notification: APNSPushToTalkNotification, + deviceToken: String + ) async throws -> APNSResponse { + let request = APNSRequest( + message: notification, + deviceToken: deviceToken, + pushType: .pushtotalk, + expiration: notification.expiration, + priority: notification.priority, + apnsID: notification.apnsID, + topic: notification.topic, + collapseID: nil + ) + return try await send(request) + } +} diff --git a/Sources/APNSCore/PushToTalk/APNSPushToTalkNotification.swift b/Sources/APNSCore/PushToTalk/APNSPushToTalkNotification.swift new file mode 100644 index 0000000..dea7930 --- /dev/null +++ b/Sources/APNSCore/PushToTalk/APNSPushToTalkNotification.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2022 the APNSwift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of APNSwift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import struct Foundation.UUID + +/// A Push to Talk (PTT) notification. +public struct APNSPushToTalkNotification: APNSMessage { + /// A canonical UUID that identifies the notification. If there is an error sending the notification, + /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, + /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: + /// `123e4567-e89b-12d3-a456-42665544000`. + /// + /// If you omit this, a new UUID is created by APNs and returned in the response. + public var apnsID: UUID? + + /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip-ptt`. + public var topic: String + + /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, + /// APNs stores the notification and tries to deliver it at least once, + /// repeating the attempt as needed if it is unable to deliver the notification the first time. + /// If the value is `immediately`, APNs treats the notification as if it expires immediately + /// and does not store the notification or attempt to redeliver it. + public var expiration: APNSNotificationExpiration + + /// The priority of the notification. + public var priority: APNSPriority + + /// Your custom payload. + public var payload: Payload + + /// Initializes a new ``APNSPushToTalkNotification``. + /// + /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. + /// It is **important** that you do not encode anything with the key `aps` + /// + /// - Parameters: + /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` + /// - priority: The priority of the notification. Defaults to `.immediately` + /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip-ptt`. + /// - payload: Payload contain speaker data like name of active speaker or indication the session has ended. + /// - apnsID: A canonical UUID that identifies the notification. + @inlinable + public init( + expiration: APNSNotificationExpiration = .immediately, + priority: APNSPriority = .immediately, + appID: String, + payload: Payload, + apnsID: UUID? = nil + ) { + self.init( + expiration: expiration, + priority: priority, + topic: appID + ".voip-ptt", + payload: payload, + apnsID: apnsID + ) + } + + /// Initializes a new ``APNSPushToTalkNotification``. + /// + /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. + /// It is **important** that you do not encode anything with the key `aps` + /// + /// - Parameters: + /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` + /// - priority: The priority of the notification. Defaults to `.immediately` + /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip-ptt`. + /// - payload: Payload contain speaker data like name of active speaker or indication the session has ended. + /// - apnsID: A canonical UUID that identifies the notification. + @inlinable + public init( + expiration: APNSNotificationExpiration = .immediately, + priority: APNSPriority = .immediately, + topic: String, + payload: Payload, + apnsID: UUID? = nil + ) { + self.expiration = expiration + self.priority = priority + self.topic = topic + self.payload = payload + self.apnsID = apnsID + } +} + +extension APNSPushToTalkNotification where Payload == EmptyPayload { + /// Initializes a new ``APNSPushToTalkNotification`` with an EmptyPayload. + /// + /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. + /// It is **important** that you do not encode anything with the key `aps` + /// + /// - Parameters: + /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` + /// - priority: The priority of the notification. Defaults to `.immediately` + /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip-ptt`. + /// - payload: Empty Payload. + /// - apnsID: A canonical UUID that identifies the notification. + public init( + expiration: APNSNotificationExpiration = .immediately, + priority: APNSPriority = .immediately, + appID: String, + apnsID: UUID? = nil + ) { + self.init( + expiration: expiration, + priority: priority, + topic: appID + ".voip-ptt", + payload: EmptyPayload(), + apnsID: apnsID + ) + } +} diff --git a/Sources/APNSExample/Program.swift b/Sources/APNSExample/Program.swift index e319279..1c4e753 100644 --- a/Sources/APNSExample/Program.swift +++ b/Sources/APNSExample/Program.swift @@ -23,6 +23,7 @@ struct Main { /// To use this example app please provide proper values for variable below. static let deviceToken = "" static let pushKitDeviceToken = "" + static let ephemeralPushToken = "" // PTT static let fileProviderDeviceToken = "" static let appBundleID = "" static let privateKey = """ @@ -56,6 +57,7 @@ struct Main { try await Self.sendBackground(with: client) try await Self.sendVoIP(with: client) try await Self.sendFileProvider(with: client) + try await Self.sendPushToTalk(with: client) } catch { logger.warning("error sending push: \(error)") } @@ -210,3 +212,21 @@ extension Main { ) } } + + +// MARK: Push to Talk (PTT) + +@available(macOS 11.0, *) +extension Main { + static func sendPushToTalk(with client: some APNSClientProtocol) async throws { + try await client.sendPushToTalkNotification( + .init( + expiration: .immediately, + priority: .immediately, + appID: self.appBundleID, + payload: EmptyPayload() + ), + deviceToken: self.ephemeralPushToken + ) + } +} diff --git a/Tests/APNSTests/PushToTalk/APNSPushToTalkNotificationTests.swift b/Tests/APNSTests/PushToTalk/APNSPushToTalkNotificationTests.swift new file mode 100644 index 0000000..c3ab13d --- /dev/null +++ b/Tests/APNSTests/PushToTalk/APNSPushToTalkNotificationTests.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2022 the APNSwift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of APNSwift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import APNSCore +import XCTest + +final class APNSPushToTalkNotificationTests: XCTestCase { + func testAppID() { + struct Payload: Encodable { + let foo = "bar" + } + let voipNotification = APNSPushToTalkNotification( + appID: "com.example.app", + payload: Payload() + ) + + XCTAssertEqual(voipNotification.topic, "com.example.app.voip-ptt") + } +}