diff --git a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotification.swift b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotification.swift index 2f08a20a..135a193b 100644 --- a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotification.swift +++ b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotification.swift @@ -30,34 +30,34 @@ public struct APNSLiveActivityNotification: APNSMessage get { return self.aps.timestamp } - + set { self.aps.timestamp = newValue } } - + /// Event type e.g. update public var event: APNSLiveActivityNotificationEvent { get { return APNSLiveActivityNotificationEvent(rawValue: self.aps.event) } - + set { self.aps.event = newValue.rawValue } } - + /// The dynamic content of a Live Activity. public var contentState: ContentState { get { return self.aps.contentState } - + set { self.aps.contentState = newValue } } - + public var dismissalDate: APNSLiveActivityDismissalDate? { get { return .init(dismissal: self.aps.dismissalDate) @@ -66,7 +66,7 @@ public struct APNSLiveActivityNotification: APNSMessage self.aps.dismissalDate = newValue?.dismissal } } - + /// 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: @@ -123,7 +123,6 @@ public struct APNSLiveActivityNotification: APNSMessage dismissalDate: dismissalDate ) } - /// Initializes a new ``APNSLiveActivityNotification``. /// diff --git a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationAPSStorage.swift b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationAPSStorage.swift index 8a975e96..756c747c 100644 --- a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationAPSStorage.swift +++ b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationAPSStorage.swift @@ -24,7 +24,7 @@ struct APNSLiveActivityNotificationAPSStorage: Encodabl var event: String var contentState: ContentState var dismissalDate: Int? - + init( timestamp: Int, event: String, @@ -32,8 +32,8 @@ struct APNSLiveActivityNotificationAPSStorage: Encodabl dismissalDate: Int? ) { self.timestamp = timestamp - self.event = event self.contentState = contentState self.dismissalDate = dismissalDate + self.event = event } } diff --git a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationEvent.swift b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationEvent.swift index a64b329a..9f925f80 100644 --- a/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationEvent.swift +++ b/Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationEvent.swift @@ -13,14 +13,13 @@ //===----------------------------------------------------------------------===// public struct APNSLiveActivityNotificationEvent: Hashable { - /// The underlying raw value that is send to APNs. @usableFromInline internal let rawValue: String - + /// Specifies that live activity should be updated public static let update = Self(rawValue: "update") - + /// Specifies that live activity should be ended public static let end = Self(rawValue: "end") } diff --git a/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotification.swift b/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotification.swift new file mode 100644 index 00000000..4a722465 --- /dev/null +++ b/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotification.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// 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 notification that starts a live activity +/// +/// It is **important** that you do not encode anything with the key `aps`. +public struct APNSStartLiveActivityNotification: + APNSMessage +{ + enum CodingKeys: CodingKey { + case aps + } + + /// The fixed content to indicate that this is a background notification. + private var aps: APNSStartLiveActivityNotificationAPSStorage + + /// Timestamp when sending notification + public var timestamp: Int { + get { + return self.aps.timestamp + } + + set { + self.aps.timestamp = newValue + } + } + + public var alert: APNSAlertNotificationContent { + get { + return self.aps.alert + } + + set { + self.aps.alert = newValue + } + } + + /// The dynamic content of a Live Activity. + public var contentState: ContentState { + get { + return self.aps.contentState + } + + set { + self.aps.contentState = newValue + } + } + + public var dismissalDate: APNSLiveActivityDismissalDate? { + get { + return .init(dismissal: self.aps.dismissalDate) + } + set { + self.aps.dismissalDate = newValue?.dismissal + } + } + + /// 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 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 + + /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID. + public var topic: String + + /// Initializes a new ``APNSStartLiveActivityNotification``. + /// + /// - 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. + /// - priority: The priority of the notification. + /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.push-type.liveactivity`. + /// - contentState: Updated content-state of live activity + /// - timestamp: Timestamp when sending notification + /// - dismissalDate: Timestamp when to dismiss live notification when sent with `end`, if in the past + /// dismiss immediately + /// - apnsID: A canonical UUID that identifies the notification. + /// - attributes: The ActivityAttributes of the live activity to start + /// - attributesType: The type name of the ActivityAttributes you want to send + /// - alert: An alert that will be sent along with the notification + public init( + expiration: APNSNotificationExpiration, + priority: APNSPriority, + appID: String, + contentState: ContentState, + timestamp: Int, + dismissalDate: APNSLiveActivityDismissalDate = .none, + apnsID: UUID? = nil, + attributes: Attributes, + attributesType: String, + alert: APNSAlertNotificationContent + ) { + self.aps = APNSStartLiveActivityNotificationAPSStorage( + timestamp: timestamp, + contentState: contentState, + dismissalDate: dismissalDate.dismissal, + alert: alert, + attributes: attributes, + attributesType: attributesType + ) + self.apnsID = apnsID + self.expiration = expiration + self.priority = priority + self.topic = appID + ".push-type.liveactivity" + } +} diff --git a/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotificationAPSStorage.swift b/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotificationAPSStorage.swift new file mode 100644 index 00000000..2fd1947e --- /dev/null +++ b/Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotificationAPSStorage.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +struct APNSStartLiveActivityNotificationAPSStorage: + Encodable +{ + enum CodingKeys: String, CodingKey { + case timestamp = "timestamp" + case event = "event" + case contentState = "content-state" + case dismissalDate = "dismissal-date" + case alert = "alert" + case attributes = "attributes" + case attributesType = "attributes-type" + } + + var timestamp: Int + var event: String = "start" + var contentState: ContentState + var dismissalDate: Int? + var alert: APNSAlertNotificationContent + var attributes: Attributes + var attributesType: String + + init( + timestamp: Int, + contentState: ContentState, + dismissalDate: Int?, + alert: APNSAlertNotificationContent, + attributes: Attributes, + attributesType: String + ) { + self.timestamp = timestamp + self.contentState = contentState + self.dismissalDate = dismissalDate + self.alert = alert + self.attributes = attributes + self.attributesType = attributesType + } +} diff --git a/Tests/APNSTests/LiveActivity/APNSLiveActivityNotificationTests.swift b/Tests/APNSTests/LiveActivity/APNSLiveActivityNotificationTests.swift index 01b5ac82..01008417 100644 --- a/Tests/APNSTests/LiveActivity/APNSLiveActivityNotificationTests.swift +++ b/Tests/APNSTests/LiveActivity/APNSLiveActivityNotificationTests.swift @@ -17,11 +17,15 @@ import XCTest final class APNSLiveActivityNotificationTests: XCTestCase { + struct Attributes: Encodable { + let name: String = "Test Attribute" + } + struct State: Encodable, Hashable { let string: String = "Test" let number: Int = 123 } - + func testEncodeUpdate() throws { let notification = APNSLiveActivityNotification( expiration: .immediately, @@ -29,20 +33,48 @@ final class APNSLiveActivityNotificationTests: XCTestCase { appID: "test.app.id", contentState: State(), event: .update, - timestamp: 1672680658) - + timestamp: 1_672_680_658) + let encoder = JSONEncoder() let data = try encoder.encode(notification) let expectedJSONString = """ - {"aps":{"event":"update","content-state":{"string":"Test","number":123},"timestamp":1672680658}} - """ + {"aps":{"event":"update","content-state":{"string":"Test","number":123},"timestamp":1672680658}} + """ let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary - let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary + let jsonObject2 = + try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) + as! NSDictionary XCTAssertEqual(jsonObject1, jsonObject2) } - + + func testEncodeStart() throws { + let notification = APNSStartLiveActivityNotification( + expiration: .immediately, + priority: .immediately, + appID: "test.app.id", + contentState: State(), + timestamp: 1_672_680_658, + attributes: Attributes(), + attributesType: "Attributes", + alert: .init(title: .raw("Hi"), body: .raw("Hello")) + ) + + let encoder = JSONEncoder() + let data = try encoder.encode(notification) + + let expectedJSONString = """ + {"aps":{"event":"start", "alert": { "title": "Hi", "body": "Hello" }, "attributes-type": "Attributes", "attributes": {"name":"Test Attribute"},"content-state":{"string":"Test","number":123},"timestamp":1672680658}} + """ + + let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary + let jsonObject2 = + try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) + as! NSDictionary + XCTAssertEqual(jsonObject1, jsonObject2) + } + func testEncodeEndNoDismiss() throws { let notification = APNSLiveActivityNotification( expiration: .immediately, @@ -50,17 +82,19 @@ final class APNSLiveActivityNotificationTests: XCTestCase { appID: "test.app.id", contentState: State(), event: .end, - timestamp: 1672680658) - + timestamp: 1_672_680_658) + let encoder = JSONEncoder() let data = try encoder.encode(notification) let expectedJSONString = """ - {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658}} - """ + {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658}} + """ let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary - let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary + let jsonObject2 = + try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) + as! NSDictionary XCTAssertEqual(jsonObject1, jsonObject2) } @@ -71,19 +105,21 @@ final class APNSLiveActivityNotificationTests: XCTestCase { appID: "test.app.id", contentState: State(), event: .end, - timestamp: 1672680658, - dismissalDate: .timeIntervalSince1970InSeconds(1672680800)) - + timestamp: 1_672_680_658, + dismissalDate: .timeIntervalSince1970InSeconds(1_672_680_800)) + let encoder = JSONEncoder() let data = try encoder.encode(notification) let expectedJSONString = """ - {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658, - "dismissal-date":1672680800}} - """ + {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658, + "dismissal-date":1672680800}} + """ let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary - let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary + let jsonObject2 = + try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) + as! NSDictionary XCTAssertEqual(jsonObject1, jsonObject2) } }