diff --git a/Sources/APNS/APNSClient.swift b/Sources/APNS/APNSClient.swift index f17bfe76..a1c38268 100644 --- a/Sources/APNS/APNSClient.swift +++ b/Sources/APNS/APNSClient.swift @@ -192,9 +192,10 @@ extension APNSClient { let response = try await self.httpClient.execute(httpClientRequest, deadline: .distantFuture) let apnsID = response.headers.first(name: "apns-id").flatMap { UUID(uuidString: $0) } + let apnsUniqueID = response.headers.first(name: "apns-unique-id").flatMap { UUID(uuidString: $0) } if response.status == .ok { - return APNSResponse(apnsID: apnsID) + return APNSResponse(apnsID: apnsID, apnsUniqueID: apnsUniqueID) } let body = try await response.body.collect(upTo: 1024) @@ -203,6 +204,7 @@ extension APNSClient { let error = APNSError( responseStatus: Int(response.status.code), apnsID: apnsID, + apnsUniqueID: apnsUniqueID, apnsResponse: errorResponse, timestamp: errorResponse.timestampInSeconds.flatMap { Date(timeIntervalSince1970: $0) } ) diff --git a/Sources/APNSCore/APNSError.swift b/Sources/APNSCore/APNSError.swift index 4b279c06..41bd2c6c 100644 --- a/Sources/APNSCore/APNSError.swift +++ b/Sources/APNSCore/APNSError.swift @@ -393,6 +393,11 @@ public struct APNSError: Error { /// Use this value to identify the notification. If you don’t specify an `apnsID` in your request, /// APNs creates a new `UUID` and returns it in this header. public let apnsID: UUID? + + /// A unique ID for the notification used for development, as determined by the APNs servers. + /// + /// In the development or sandbox environement, this value can be used to look up information about notifications on the [Push Notifications Console](https://icloud.developer.apple.com/dashboard/notifications). This value is not provided in the production environement. + public var apnsUniqueID: UUID? /// The error code indicating the reason for the failure. public let reason: ErrorReason? @@ -405,11 +410,13 @@ public struct APNSError: Error { public init( responseStatus: Int, apnsID: UUID? = nil, + apnsUniqueID: UUID? = nil, apnsResponse: APNSErrorResponse? = nil, timestamp: Date? = nil ) { self.responseStatus = responseStatus self.apnsID = apnsID + self.apnsUniqueID = apnsUniqueID if let apnsResponse { self.reason = .init(_reason: .init(rawValue: apnsResponse.reason)) } else { @@ -425,6 +432,7 @@ extension APNSError: Hashable { return lhs.responseStatus == rhs.responseStatus && lhs.apnsID == rhs.apnsID && + lhs.apnsUniqueID == rhs.apnsUniqueID && lhs.reason == rhs.reason && lhs.timestamp == rhs.timestamp } @@ -432,6 +440,7 @@ extension APNSError: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(self.responseStatus) hasher.combine(self.apnsID) + hasher.combine(self.apnsUniqueID) hasher.combine(self.reason) hasher.combine(self.timestamp) } diff --git a/Sources/APNSCore/APNSResponse.swift b/Sources/APNSCore/APNSResponse.swift index 418cbd17..fe5c10e7 100644 --- a/Sources/APNSCore/APNSResponse.swift +++ b/Sources/APNSCore/APNSResponse.swift @@ -21,11 +21,25 @@ public struct APNSResponse: Hashable { /// Use this value to identify the notification. If you don’t specify an `apnsID` in your request, /// APNs creates a new `UUID` and returns it in this header. public var apnsID: UUID? + + /// A unique ID for the notification used for development, as determined by the APNs servers. + /// + /// In the development or sandbox environement, this value can be used to look up information about notifications on the [Push Notifications Console](https://icloud.developer.apple.com/dashboard/notifications). This value is not provided in the production environement. + public var apnsUniqueID: UUID? /// Initializes a new ``APNSResponse``. /// /// - Parameter apnsID: The same value as the `apnsID` send in the request. - public init(apnsID: UUID? = nil) { + /// - Parameter apnsUniqueID: A unique ID for the notification used only in the development environment. + public init(apnsID: UUID? = nil, apnsUniqueID: UUID? = nil) { self.apnsID = apnsID + self.apnsUniqueID = apnsUniqueID + } +} + +/// The [Push Notifications Console](https://icloud.developer.apple.com/dashboard/notifications) expects IDs to be lowercased, so prep them ahead of time here to make it easier for users to copy and paste these IDs. +extension APNSResponse: CustomStringConvertible { + public var description: String { + "APNSResponse(apns-id: \(apnsID?.uuidString.lowercased() ?? "nil"), apns-unique-id: \(apnsUniqueID?.uuidString.lowercased() ?? "nil"))" } } diff --git a/Sources/APNSURLSession/APNSUrlSessionClient.swift b/Sources/APNSURLSession/APNSUrlSessionClient.swift index 8ceb90a9..ea0c0eac 100644 --- a/Sources/APNSURLSession/APNSUrlSessionClient.swift +++ b/Sources/APNSURLSession/APNSUrlSessionClient.swift @@ -44,19 +44,21 @@ public struct APNSURLSessionClient: APNSClientProtocol { } let apnsID = UUID(uuidString: apnsIDString) + let apnsUniqueID = (response.allHeaderFields["apns-unique-id"] as? String).flatMap { UUID(uuidString: $0) } /// Detect an error if let errorResponse = try? decoder.decode(APNSErrorResponse.self, from: data) { let error = APNSError( responseStatus: response.statusCode, apnsID: apnsID, + apnsUniqueID: apnsUniqueID, apnsResponse: errorResponse, timestamp: errorResponse.timestampInSeconds.flatMap { Date(timeIntervalSince1970: $0) } ) throw error } else { /// Return APNSResponse - return APNSResponse(apnsID: apnsID) + return APNSResponse(apnsID: apnsID, apnsUniqueID: apnsUniqueID) } } }