diff --git a/Sources/APNSCore/APNSPushType.swift b/Sources/APNSCore/APNSPushType.swift index 3e27d28..ff4e49e 100644 --- a/Sources/APNSCore/APNSPushType.swift +++ b/Sources/APNSCore/APNSPushType.swift @@ -25,6 +25,7 @@ public struct APNSPushType: Hashable, Sendable, CustomStringConvertible { case mdm case liveactivity case pushtotalk + case widgets } public var description: String { @@ -108,4 +109,10 @@ public struct APNSPushType: Hashable, Sendable, CustomStringConvertible { /// - 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) + + /// Use the widgets push type for notifications that trigger widget updates. + /// + /// - Important: if you set this push type, the topic must use your app’s bundle ID with `.push-type.widgets` appended to the end. + /// + public static let widgets = Self(configuration: .widgets) } diff --git a/Sources/APNSCore/Widgets/APNSClient+Widgets.swift b/Sources/APNSCore/Widgets/APNSClient+Widgets.swift new file mode 100644 index 0000000..e6a72b8 --- /dev/null +++ b/Sources/APNSCore/Widgets/APNSClient+Widgets.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2025 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 widgets update 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. + /// + @discardableResult + @inlinable + public func sendWidgetsNotification( + notification: APNSWidgetsNotification, + deviceToken: String + ) async throws -> APNSResponse { + let request = APNSRequest( + message: notification, + deviceToken: deviceToken, + pushType: .widgets, + expiration: notification.expiration, + priority: notification.priority, + apnsID: notification.apnsID, + topic: notification.topic, + collapseID: nil + ) + return try await send(request) + } +} diff --git a/Sources/APNSCore/Widgets/APNSWidgetsNotification.swift b/Sources/APNSCore/Widgets/APNSWidgetsNotification.swift new file mode 100644 index 0000000..a8199e9 --- /dev/null +++ b/Sources/APNSCore/Widgets/APNSWidgetsNotification.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 widget update notification. +public struct APNSWidgetsNotification: APNSMessage { + @usableFromInline + struct APS: Encodable, Sendable { + enum CodingKeys: String, CodingKey { + case contentAvailable = "content-changed" + } + + let contentAvailable: Bool = true + } + + @usableFromInline + enum CodingKeys: CodingKey { + case aps + } + + /// The fixed content to indicate that this is a background notification. + @usableFromInline + internal let aps = APS() + + /// 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 `.push-type.widgets`. + 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 + + /// Initializes a new ``APNSWidgetsNotification``. + /// + /// - Parameters: + /// - expiration: The date when the notification is no longer valid and can be discarded. + /// - priority: The priority of the notification. + /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.push-type.widgets`. + /// - apnsID: A canonical UUID that identifies the notification. + @inlinable + public init( + expiration: APNSNotificationExpiration, + priority: APNSPriority, + appID: String, + apnsID: UUID? = nil + ) { + self.init( + expiration: expiration, + priority: priority, + topic: appID + ".push-type.widgets", + apnsID: apnsID + ) + } + + /// Initializes a new ``APNSWidgetsNotification``. + /// + /// - Parameters: + /// - expiration: The date when the notification is no longer valid and can be discarded. + /// - priority: The priority of the notification. + /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.push-type.widgets`. + /// - apnsID: A canonical UUID that identifies the notification. + @inlinable + public init( + expiration: APNSNotificationExpiration, + priority: APNSPriority, + topic: String, + apnsID: UUID? = nil + ) { + self.expiration = expiration + self.priority = priority + self.topic = topic + self.apnsID = apnsID + } +} diff --git a/Tests/APNSTests/Widgets/APNSWidgetsNotificationTests.swift b/Tests/APNSTests/Widgets/APNSWidgetsNotificationTests.swift new file mode 100644 index 0000000..727e326 --- /dev/null +++ b/Tests/APNSTests/Widgets/APNSWidgetsNotificationTests.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 APNSWidgetsNotificationTests: XCTestCase { + func testAppID() { + let widgetsNotification = APNSWidgetsNotification( + expiration: .none, + priority: .immediately, + appID: "com.example.app" + ) + + XCTAssertEqual(widgetsNotification.topic, "com.example.app.push-type.widgets") + } + + func testEncode() throws { + let widgetsNotification = APNSWidgetsNotification( + expiration: .none, + priority: .immediately, + appID: "com.example.app" + ) + + let encoder = JSONEncoder() + let data = try encoder.encode(widgetsNotification) + + let expectedJSONString = """ + {"aps":{"content-changed":true}} + """ + let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary + let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary + XCTAssertEqual(jsonObject1, jsonObject2) + } + +}